Irgendwann scheint es jeden Programmierer mal zu erwischen, der mehr oder weniger intensiv mit dem MS SQ Server arbeitet: Plötzlich und ohne Vorwarnung scheinen Datenbankzugriffe wie in Kaugummi gebettet, Antwortzeiten von gefühlten fünf Minuten für einfachste Abfragen stellen sich ein und eine Flut von Time-Out-Meldungen aus der ODBC-Verbindungsschicht bricht über die Applikation herein.
Diese Situation kann einen schon zur Verzweiflung bringen. Es scheint nichts mit der puren Datenmenge zu tun zu haben, nichts mit der Netzwerkbandbreite und noch nicht einmal etwas mit der Performance der Servermaschine selbst zu tun zu haben – die Datenbank ist plötzlich langsam bis hin zum Stillstand. Ein Blick in den Performance-Monitor zeigt oft, dass einige Abfragen mit den Wartetypen ASYNC_NETWORK_IO, LATCH_EX oder einem der LCK_-Wartetypen in der Warteschlange stehen. Oft zeigen eine oder mehrere Abfragen auch den Status LOCKED an.
So ähnlich stellte sich die Situation auch in meinem Fall dar. Eine VBA-Applikation in MS Access greift auf eine MS SQL Server-Datenbank per DAO und DAO Passthrough zu, funktionierte jahrelang problemlos – und hat plötzlich mit den oben genannten Symtomen zu kämpfen.
Was mich stutzig hat werden lassen war die Tatsache, dass selbst simple SELECT-Abfragen, die per PassThrough direkt an den Server weitergeleitet werden, dort bisweilen den Status LOCKED erhalten. Eine nur lesende Abfrage sollte nach meinem Verständnis doch keine Sperren an Abfragen hervorrufen – und sich auch nicht durch diese Stoppen lassen. Oder doch?
Die Antwort darauf gibt ein Blick auf das Standardverhalten des MS SQL Server bei der Abarbeitung von SELECT-Statements: Tatsächlich wartet der Datenbankserver bei der Ausführung von SELECT-Statements die Ausführung von gegebenenfalls parallel ablaufenden Schreibvorgängen in den vom Lesevorgang betroffenen Tabellen ab. Damit sollen sogenannte "dirty reads", also das Auslesen von eigentlich bereits veralteten Informationen, vermieden werden.
Abgesehen davon, dass es für manche SELECT-Abfragen eigentlich unerheblich ist, ob die gelieferten Informationen auf dem absolut aktuellen Stand sind, kann diese Vorgehensweise bei ungeschickter Applikationsentwicklung auch zu einer Dead-Lock-Situation führen.
In meinem Fall passierte Folgendes: Über DAO PassThrough wurde von der Applikation ein SELECT-Statement an den SQL-Server abgesetzt, während praktisch zeitgleich über DAO Aktualisierungen an der Datenbank vorgenommen wurden – die unglücklicherweise auch Tabellen betrafen, die von dem über PassThrough abgesetzten Statement abgefragt wurden. Der Schreibvorgang bewirkte eine Sperrung der entsprechenden Tabellen, wodurch die SELECT-Abfrage den Status LOCKED erhielt. Passieren konnte das offenbar dadurch, dass die Applikation auf zwei unterschiedlichen Wegen mit der Datenbank kommunizierte und die Abfragen dadurch nicht sequentiell, sondern quasi parallel abgearbeitet worden sind.
Die Lösung für das Problem ist relativ einfach: Der Query Optimizer des MS SQL Server versteht sogenannte TABLE HINTS. Dabei handelt es sich um Anweisungen, die es dem Programmierer erlauben, das Standardverhalten des Optimizers zu beeinflussen und eine bessere Datenbankperformance zu erreichen. Die Table Hints werden in einem SQL-Statement hinter den Namen der betroffenen Tabelle geschrieben:
SELECT fieldname1, fieldname2
FROM tablename WITH(hint1, hint2)
WHERE fieldname = condition;
Es können mehrere table hints angegeben werden, die dann jeweils durch Komma getrennt werden müssen.
Für die beschriebene Fehlersituation sind die beiden table hints NOLOCK und READUNCOMMITTED besonders interessant. Durch Angabe dieser table hints wird dafür gesorgt, dass die SELECT-Anweisung ggf. vorhandene Sperren auf den Tabellen nicht abwartet und auch den Abschluss laufender Transaktionen nicht abwartet. Dadurch werden zwar unter Umständen veraltete Daten aus der Datenbank gelesen, aber die Abfrage wird ohne Unterbrechung schnellstmöglich abgearbeitet.
SELECT fieldname1, fieldname2
FROM tablename WITH(nolock, readuncommitted)
WHERE fieldname = condition;
Wichtig ist, dass diese beiden table hints ausschließlich bei SELECT-Statements verwendet werden können, bei allen anderen Abfragetypen ignoriert der Query Optimizer diese Anweisungen.
In meinem Fall beseitigte das Verwenden der Table Hints die Perfomanceprobleme beim Zugriff auf Datenbanken beim MS SQL Server 2000 und 2005.
Mehr Informationen zur Funktion und Verwendung von table hints findet man hier.
