Multithreading von mehreren Datenbankabfragen

  • VB.NET

Es gibt 17 Antworten in diesem Thema. Der letzte Beitrag () ist von Sutarex.

    Multithreading von mehreren Datenbankabfragen

    Guten Morgen zusammen,

    ich beschäftige mich im Laufe eines kleinen Projektes gerade mit Threading.
    Das Projekt an sich soll so etwas wie ein DashBoard sein. Es sollen also möglichst aktuelle Daten dargestellt werden. Dazu habe ich für jeden relevanten Wert eine Datenbank Abfrage, die nach und nach durchgegangen wird und dann das entsprechende Label füllt.

    Bei wenigen Labels ging alles noch sehr flott. Nun hat man natürlich immer mehr Ideen was relevant sein könnte und möchte dieses darstellen. Aktuell 28 Labels die auch gefüllt werden wollen... :saint:

    Das Füllen der Labels habe ich bisher so gelöst: In meiner Methode datenFüllen() wird dem Label der Wert der Abfrage zugewiesen. Alles nach diesem System:

    Quellcode

    1. setLabelTxt(returnWert(My.Settings.sqlOffenePicksWoche), lblOffenePicksWoche)


    Methode um das Label zu beschrieben:

    Quellcode

    1. Public Sub setLabelTxt(ByVal text As String, ByVal lbl As Label)
    2. 'Benötigt für den BackgroundWorker. Setzt den Text des Labels, wenn kein Zugriff aktuell drauf ist.
    3. If lbl.InvokeRequired Then
    4. lbl.Invoke(New setLabelTxtInvoker(AddressOf setLabelTxt), text, lbl)
    5. Else
    6. lbl.Text = text
    7. End If
    8. End Sub


    Ich habe einen Aufruf von datenFüllen() im Form Load und dann im Timer, der alle 30 Sekunden die Daten aktualisieren soll.
    Durch die Reihe an Abfragen, die durch das datenFüllen() im Load ausgeführt werden, dauert es natürlich bis die User Form aufgebaut ist. Auch friert die Form durch das Aufrufen von datenFüllen() im Timer immer ein.

    Durch den Einsatz von Threads wollte ich das Einfrieren und die lange Aufbauzeit verhindern. Dazu habe ich mir gedacht, dass ich die 28 Abfragen auf 5 Threads aufteile.

    Quellcode

    1. threadÜbersicht = New System.Threading.Thread(AddressOf datenFüllenÜbersicht)
    2. threadÜbersicht.Start()


    Quellcode

    1. Private Sub datenFüllenÜbersicht()
    2. '### Übersicht ###
    3. setLabelTxt(returnWert(My.Settings.sqlOffenePicksWoche), lblOffenePicksWoche)
    4. setLabelTxt(returnWert(My.Settings.sqlPicksMin), lblPicksMin)
    5. setLabelTxt(returnWert(My.Settings.sqlPicksMinHeute), lblPicksMinHeute)
    6. setLabelTxt(returnWert(My.Settings.sqlPicksMinGestern), lblPicksMinGestern)
    7. setDataSource(returnDataTable(My.Settings.sqlKomOffeneAufgaben), dgvKomOffeneAufgaben) 'Ganzes DataTable an DataGridView übergeben
    8. End Sub


    Quellcode

    1. Public Function returnWert(strSQL As String) As String
    2. Dim strReturn As String
    3. Dim myDataTable As DataTable
    4. myDataTable = SQL.abfragenStarten(strSQL)
    5. If myDataTable.Columns.Count > 0 Then
    6. 'Daten vorhanden
    7. strReturn = myDataTable.Rows(0)(0).ToString()
    8. Else
    9. 'Keine Daten vorhanden
    10. strReturn = ""
    11. End If
    12. Return strReturn
    13. End Function


    Quellcode

    1. Public Function abfragenStarten(strSQL As String) As DataTable
    2. 'Führt die SQL Abfrage aus und gibt das Ergebnis als DataReader zurück.
    3. Dim dtDataTable As New DataTable
    4. If strSQL = "" Then
    5. 'keine Abfrage vorhanden: Lasse leeren DatTable returnen.
    6. Else
    7. 'Create a Command object.
    8. myCmd = myConn.CreateCommand
    9. myCmd.CommandText = strSQL
    10. 'Open the connection.
    11. myConn.Open()
    12. Dim drDaten As SqlDataReader
    13. drDaten = myCmd.ExecuteReader()
    14. dtDataTable.Load(drDaten)
    15. SQL.myConn.Close() 'Nachdem die Abfragewerte genutzt wurden, die Verbindung beenden.
    16. End If
    17. Return dtDataTable
    18. End Function

    Die Verbindung zur Datenbank stelle ich auch in Form Load her.

    Nun habe ich das Problem, dass jedes setLabelTxt natürlich eine Datenbank Abfrage ausführen möchte. Da es nun vor kommt, dass myConn.Open() von unterschiedlichen Threads gleichzeitig versucht wird aufzurufen, kommt es natürlich zu einem Fehler.

    Setzte ich alle Abfragen für das Label in einen Thread klappt auch fast alles. Die Form friert nicht mehr ein, es dauert aber trotzdem noch recht lange.

    Soweit sehr viel mehr Text als gedacht bis hierhin 8|

    Meine eigentlichen Fragen:
    Wie kann ich es so programmieren, dass es zu keinem Fehler durch die mehrfach Verwendung meiner Connection gibt?
    Kann ich dazu die Datenbank Abfrage in den Thread irgendwie auslagern?
    Müsste ich dazu zu jeden Thread die Datenbankverbindung neu öffnen?

    Falls mein oben dargestelltes Vorgehen eventuell noch zu verbessern wäre, bitte ich gerne um Vorschläge :)

    Vielen Dank für eure Hilfe!

    Grüße Jan
    Hallo

    Sutarex schrieb:

    Falls mein oben dargestelltes Vorgehen eventuell noch zu verbessern wäre, bitte ich gerne um Vorschläge


    Nicht nur viel schneller sondern auch Resourcenschonender sowie ohne deinen Threadingproblemen wäre es wenn du die Daten 1x abrufst.

    Du rufst also alle Settings welche du für dieses Form benötigst in einem Rutsch ab und speicherst diese in einer Klasse oder von mir aus in einer List(Of String).
    Aus dieser holst du dann in der setLabel Methode die Werte raus.

    Grüße
    Sascha
    If _work = worktype.hard Then Me.Drink(Coffee)
    Seht euch auch meine Tutorialreihe <WPF Lernen/> an oder abonniert meinen YouTube Kanal.

    ## Bitte markiere einen Thread als "Erledigt" wenn deine Frage beantwortet wurde. ##

    Hallo Sascha,

    danke für deine Antwort. Das würde sicher mein Threading Problem lösen.
    Da stoße ich nur auf ein Problem: Meine SQL Kenntnisse :/

    Ich vermute du meinst, dass ich dann alle gewünschten Ergebnisse mit einem SQL Befehl in einem DataTable darstellen müsste und dann einfach die entsprechende Spalte auslesen muss um das Label zu füllen? Korrekt?

    Grüße Jan
    Hallo

    Also, als erstes ohne irgendwas zu optimieren oder der gleichen.
    Als SQL kannst du mal sowas wie SELECT * FROM Settingstabellenname
    Die Daten liest du mit deinem DataReader ein und füllst damit eben entweder eine Klasse oder eine List(Of KeyValuePair) mit den Werten.

    Also ich denke SQL ist hier dein geringstes Problem. Probiers mal und wenn du wo hängst oder nicht weiter weist schreibe bitte genua WO und WARUM.

    Dir hier ALLE auf einem Tablett zu servieren bringt für den Lerneffekt nichts.

    Grüße
    sascha
    If _work = worktype.hard Then Me.Drink(Coffee)
    Seht euch auch meine Tutorialreihe <WPF Lernen/> an oder abonniert meinen YouTube Kanal.

    ## Bitte markiere einen Thread als "Erledigt" wenn deine Frage beantwortet wurde. ##

    Hallo Sascha,

    ich hoffe, dass ich Deine Hilfestellung so richtig verstanden habe:

    Quellcode

    1. ​ Dim strSQL_Abfrage As String
    2. strSQL_Abfrage = "SELECT [Artikelnummer], [AnzahlBuchungen] FROM [Top10Schnelldreher]"
    3. Dim myKVP_List As List(Of KeyValuePair(Of String, Integer)) = fncAbfrageErgebnisInKeyValuePair(strSQL_Abfrage) 'Einer KeyValuePair Liste das Ergebnis der Abfrage als KeyValuePair Liste übergeben.
    4. For Each pair As KeyValuePair(Of String, Integer) In myKVP_List 'KeyValuePair Liste durchlaufen und...
    5. Dim key As String = pair.Key 'Key Wert zuweisen
    6. Dim value As Integer = pair.Value 'Value Wert zuweisen
    7. Console.WriteLine("{0}, {1}", key, value) 'Konsolen Ausgabe
    8. Next


    Quellcode

    1. ​Public Function fncAbfrageErgebnisInKeyValuePair(strSQL As String) As List(Of KeyValuePair(Of String, Integer)) 'Führt die SQL Abfrage aus und gibt das Ergebnis als DataReader zurück.
    2. 'Create a Command object.
    3. myCmd = myConn.CreateCommand
    4. myCmd.CommandText = strSQL
    5. 'Open the connection.
    6. myConn.Open()
    7. Dim drDaten As SqlDataReader
    8. drDaten = myCmd.ExecuteReader()
    9. Dim myKVP_List As List(Of KeyValuePair(Of String, Integer)) = New List(Of KeyValuePair(Of String, Integer))
    10. While (drDaten.Read)
    11. myKVP_List.Add(New KeyValuePair(Of String, Integer)(drDaten.GetString(0), drDaten.GetInt32(1)))
    12. End While
    13. SQL.myConn.Close() 'Nachdem die Abfragewerte genutzt wurden, die Verbindung beenden.
    14. Return myKVP_List
    15. End Function


    Das funktioniert auch alles ohne Fehler.

    Grüße Jan

    Sutarex schrieb:

    ich hoffe, dass ich Deine Hilfestellung so richtig verstanden habe


    Sutarex schrieb:

    Das funktioniert auch alles ohne Fehler.


    Hast du dir die Antwort selbst gegeben oder? Wenn alles funktioniert dann passt es ja.
    Jetzt musst du den Abruf delbst nur noch Asyncron machen. Am besten mit Async/Await.

    Du bekommst ja nun die "myKVP_List" zurück. Und die werte darin weist du nun den Textboxen zu. Fertich.
    Läuft schneller als vorher - viel schneller und du kannst es sehr einfach Asyncron laufen lassen. Vermutlich benötigst du nun nicht mal mehr einen Nebenthread oder Async/Await - weils nun so schnell läuft. 8-)

    Grüße
    Sascha
    If _work = worktype.hard Then Me.Drink(Coffee)
    Seht euch auch meine Tutorialreihe <WPF Lernen/> an oder abonniert meinen YouTube Kanal.

    ## Bitte markiere einen Thread als "Erledigt" wenn deine Frage beantwortet wurde. ##

    Soweit funktioniert das schneller, Danke! :)

    Nun habe ich wohl aber noch ein weiteres Verständnis Problem.
    In dem Beispiel habe ich nur eine Select Abfrage. Die Übersicht füllt sich aber durch aktuell 28 verschiedene Abfragen.
    Die Abfragen kann ich nicht ohne weiteres in eine Abfrage packen um diese in mein myKVP_List zu füllen. Oder stehe ich auf dem Schlauch?

    Grüße Jan
    Hallo das kommt darauf an, was sind das für Abfragen.

    Oft sehe ich es das man zig Abfragen macht wie:

    SELECT * FROM meineTabelle WHERE Key= 'testKey12'
    SELECT ID,Value FROM meineTabelle WHERE Key= 'testKey18'
    SELECT * FROM meineTabelle WHERE ID= 12
    SELECT * FROM meineTabelle WHERE Key= 'testKey11'

    Welche man ja zusammenfassen:
    SELECT * FROM meineTabelle WHERE Key= 'testKey11' OR Key= 'testKey18' OR Key= 'testKey12' OR ID= 12 OrderBy ID

    Und dann ist es nur noch EINE Abfrage welche VIEL scneller geht als 4 Abfragen ja jeder Rountrip zur DB Zeit kostet.

    Grüße
    Sascha
    If _work = worktype.hard Then Me.Drink(Coffee)
    Seht euch auch meine Tutorialreihe <WPF Lernen/> an oder abonniert meinen YouTube Kanal.

    ## Bitte markiere einen Thread als "Erledigt" wenn deine Frage beantwortet wurde. ##

    Hi Sascha,

    leider handelt es sich dabei - meines bescheidenden Wissens nach - nicht um zusammenfassbare Abfragen.
    Hier nur ein paar Beispiele:

    SQL-Abfrage

    1. ​Select AVG(Pick_min) from (
    2. SELECT
    3. Cast((SUM([Picks-Soll])*1.00)/DATEDIFF(mi, '1900-01-01 00:00:00',[Pick-Dauer]) as Decimal(4,2)) as Pick_min
    4. FROM [Kommscheine]
    5. Where StatusID = 4
    6. AND DATEPART(day,[Erster Pick]) = DATEPART(day,[Letzter Pick])
    7. AND [Erster Pick] Between CAST(CAST(GETDATE()-1 AS DATE) AS DATETIME) and CAST(CAST(GETDATE() AS DATE) AS DATETIME)
    8. AND DATEDIFF(minute,[Begonnen am],[Beendet am]) IS NOT NULL
    9. AND DATEDIFF(minute,[Begonnen am],[Beendet am]) <> 0
    10. AND DATEDIFF(mi, '1900-01-01 00:00:00',[Pick-Dauer]) > 0
    11. AND DATEPART(day,[Begonnen am]) = DATEPART(day,[Beendet am])
    12. Group By [Pick-Dauer]
    13. ) as t


    SQL-Abfrage

    1. ​SELECT
    2. Distinct [Benutzer] as 'Name'
    3. ,Count( [Benutzer]) as 'Summe Picks'
    4. ,FORMAT(MIN([E-Uhrzeit]),'HH:mm') as 'Erster Pick'
    5. ,FORMAT(MAX([E-Uhrzeit]),'HH:mm') as 'Letzter Pick'
    6. FROM [Buchungshistorie]
    7. WHERE [KZ] = 'IU' AND [E-Datum]= CONVERT(date, GETDATE()) AND [Lager-HK] = 'KOMMI'
    8. Group by [Benutzer]
    9. Order By 'Summe Picks' DESC


    Über ein Sub Select alles in eine Abfrage zu zwengen wäre vermutlich keine schöne Lösung?

    Grüße Jan

    Sutarex schrieb:

    Über ein Sub Select alles in eine Abfrage zu zwengen wäre vermutlich keine schöne Lösung?

    Ist richtig.

    Ich würde wie folgt vorgehen:

    Daten x1 in eine Variable Asyncon laden
    Daten x2 in eine Variable Asyncron laden
    .....
    .....
    Alle Controls mit den Daten befüllen.

    So gehört es.

    Mach dir eine Methode wie z.b. GetDataAsync() und dort ladest du die Daten von der DB.

    Grüße
    Sascha
    If _work = worktype.hard Then Me.Drink(Coffee)
    Seht euch auch meine Tutorialreihe <WPF Lernen/> an oder abonniert meinen YouTube Kanal.

    ## Bitte markiere einen Thread als "Erledigt" wenn deine Frage beantwortet wurde. ##

    Hallo ihr zwei,

    danke für Eure Antworten. Sorry für die späte Antwort, mich hat letze Woche die Grippe erwischt und ich kann nun erst wieder einigermaßen klar denken :S

    Ich bin den Guide soweit kurz durchgegangen. Das hört sich erstmal nach der Lösung zu meinem Problem an, jedoch sehe ich da noch ein Problem:
    Wenn ich die Datenbankanfrage auch Async ausführe, dann habe ich doch weiterhin das Problem, dass bei

    VB.NET-Quellcode

    1. ​myConn.Open()
    in der Funktion abfragenStarten eine Fehlermeldung kommt, da diese bereits offen ist und von einer anderen Abfrage genutzt wird, oder?

    Grüße Jan
    Hallo ErfinderDesRades,

    ich habe es nun mal getestet und leider hatte ich mit meiner Vermutung recht...

    Bei folgender Methode kommt es zu der Meldung, dass die Verbindung bereits geöffnet ist aber ich schon wieder drauf zu greifen will.

    VB.NET-Quellcode

    1. ​Public Function abfragenStarten(strSQL As String) As DataTable
    2. 'Führt die SQL Abfrage aus und gibt das Ergebnis als DataReader zurück.
    3. Dim dtDataTable As New DataTable
    4. If strSQL = "" Then
    5. 'keine Abfrage vorhanden: Lasse leeren DatTable returnen.
    6. Else
    7. 'Create a Command object.
    8. myCmd = myConn.CreateCommand
    9. myCmd.CommandText = strSQL
    10. 'Open the connection.
    11. myConn.Open()
    12. Dim drDaten As SqlDataReader
    13. drDaten = myCmd.ExecuteReader()
    14. dtDataTable.Load(drDaten)
    15. SQL.myConn.Close() 'Nachdem die Abfragewerte genutzt wurden, die Verbindung beenden.
    16. End If
    17. Return dtDataTable
    18. End Function


    Aktuell vermute ich, dass es wenig helfen würde, wenn ich diese Methode als Async umbauen würde, da ich immer nur einen Zugriff auf die Verbindung nutzen kann. Ist die Vermutung korrekt?

    Aber wie gehe ich in so einen Fall den vor. Mehrere Verbindungen zu erstellen wäre auch dort vermutlich keine schöne Lösung?

    Grüße Jan

    Sutarex schrieb:

    Mehrere Verbindungen zu erstellen wäre auch dort vermutlich keine schöne Lösung?
    ich denk, so muss mans aber machen, wenn man unbedingt mehrere Abfragen gleichzeitig ausführen will.
    Lohnt sich das ühaupt - dauern die Abfragen denn so lange?
    (genauer: Wie lange dauert so eine Abfrage?)
    Hallo ErfinderDesRades,

    ich habe aktuell 44 einzelne Abfragen, die zusammen 26 Sekunden dauern bis diese durchgeführt sind.
    Würde ich als schon recht lange bezeichnen. ?(
    Leider ist zusammenfassen ja nicht möglich.

    Jetzt wäre meine Frage, was der beste Weg wäre:
    Teile ich die 44 Abfragen fest in beispielsweise 5 Gruppen auf und frage diesen durch 5 Verbindungen ab oder nutze ich die 5 Verbindungen und arbeite mit denen nach und nach alles ab. Immer wenn eine Abfrage fertig ist, wir die nächste rausgeschickt...

    Über die Umsetzung müsste ich mir da aber auch nochmal den Kopf zerbrechen...

    Grüße Jan