Artikel

Oktober 2005 | Artikel

Höher, schneller, weiter …

(Link zum Artikel: http://www.it-republik.de/dotnet/artikel/0769)

Fünf Performance-Tipps für ADO.NET

Text: von Rob Steward
Genau wie es Benimmregeln für viele Situationen im Leben gibt, gibt es „Benimmregeln“ auch für Entwickler. Bei einer Reihe von diesen Regeln kann man es sich leisten, sie zu ignorieren, da die Folgen oft nur indirekt spürbar werden. Die Benimmregeln, die in diesem Artikel für den Umgang mit ADO.NET vorgestellt werden, sollte dagegen jeder Entwickler beherzigen - denn es geht um die Performance.

Datenbankschnittstellen zeichnen sich durch rigide Verwendungsvorschriften aus. Microsofts ADO.NET ist zwar einfacher und flexibler, führt aber immer wieder zu Performance-Problemen. Bei Beachtung einiger Grundregeln sind diese leicht zu vermeiden. Woran liegt es, dass eine Applikation zu langsam ist? Was kann man dagegen tun? Wäre es nicht praktisch, bereits während der Entstehung einer Applikation eine Meldung wie System.YourCodeIsRunningTooSlowException zu erhalten, die auf Performance-Probleme aufmerksam macht? Leider hat Microsoft ein solches Exception Handling im .NET Framework vergessen.
Aus leidvoller Erfahrung wissen Entwickler, dass sich Probleme bei der Arbeitsgeschwindigkeit einer Applikation erst im Alltag zeigen. Gerade wegen der großen Spielräume, die .NET erlaubt, ist es alles andere als ein Kinderspiel, Code zum Zugriff auf Datenbanken zu schreiben, der durch schnelle Antwortzeiten glänzt. Die ADO.NET-Dokumentation enthält für diese Fälle nicht mehr als einige dürftige Grundregeln und Schnittstellendefinitionen. Aus der Analyse einer Vielzahl von .NET-Applikationen, die DataDirect Technologies bei Unternehmen antraf, haben sich fünf typische Fehler herauskristallisiert, die auf jeden vermieden werden sollten.

Tipp 1: Kein CommandBuilder verwenden
Viele Programmierer benutzen das CommandBuilder-Objekt, um Informationen aus einer SQL-Datenbank zu holen - der Grund: Es spart Entwicklungszeit. Die vermeintliche Abkürzung entpuppt sich jedoch sehr schnell als Umweg, im schlimmsten Fall als Performance-Bremse. Insbesondere wegen der Restriktionen beim gleichzeitigen Zugriff auf Datenquellen produziert CommandBuilder häufig ineffiziente SQL-Statements. Das lässt sich an folgendem Beispiel beim Update einer aus acht Spalten bestehenden relationalen Tabelle namens EMP verdeutlichen. Der CommandBuilder würde ein Update-Statement wie in Listing 1 genieren.

Listing 1
Dieses „UPDATE-Monster“ wurde vom CommandBuilder generiert
  1. UPDATE EMP SET EMPNO = ?, ENAME = ?, JOB = ?, MGR = ?, HIREDATE = ?, SAL = ?, COMM = ?, DEPT = ? WHERE ( (EMPNO = ?) AND (ENAME = ?) AND (JOB = ?) AND ((MGR IS NULL AND ? IS NULL) OR (MGR = ?)) AND (HIREDATE = ?) AND (SAL = ?) AND ((COMM IS NULL AND ? IS NULL) OR (COMM = ?)) AND (DEPT = ?) )

Entwickler schreiben häufig weit effizientere Update- und Delete-Statements als die, die CommandBuilder generiert. Kennt der Programmierer das Datenbankschema der Tabelle EMP und weiß, dass die Spalte EMPNO der Primärschlüssel ist, genügt ein weit einfacheres Update-Statement. Es arbeitet schneller und führt zudem zum gleichen Resultat:
  1. UPDATE EMP SET EMPNO = ?, ENAME = ?, JOB = ?, MGR = ?, HIREDATE = ?, SAL = ?, COMM = ?, DEPT = ? WHERE EMPNO = ?

Ein weiterer Nachteil von CommandBuilder: Er generiert die SQL-Statements während der Laufzeit. Jedes Mal, wenn eine DataAdapter.Update-Methode aufgerufen wird, analysiert der CommandBuilder den Inhalt der Ergebnismenge und erzeugt die Update-, Insert- und Delete-Statements für den DataAdapter. Den zusätzlichen Verarbeitungsaufwand umgeht ein Entwickler, indem er selbst die Update-, Insert- und Delete-Statements für den DataAdapter spezifiziert.
Tipp 2: Nicht ohne Not-Long-Daten abfragen
Als Performance-Bremse schlechthin erweisen sich bei SQL-Abfragen Long-Daten. Derartige Daten vom Typ Character besitzen eine variable Länge von bis zu 65.535 Zeichen und werden zum Beispiel für Bilder verwendet; in aller Regel ist maximal eine Spalte vom Typ Long je Tabelle zulässig. Als Faustregel gilt: Wo Performance zählt, sollten Applikationen nur, wenn es unbedingt notwendig ist, Datenfelder mit Long-Daten abfragen. Enthält das Select-Statement Tabellenspalten mit
Long
-Daten, müssen die meisten .NET Data Provider (Datenbanktreiber) alle Tabelleneinträge in der Ergebnismenge zusammenstellen - und das selbst dann, wenn die Applikation die Long-Daten im konkreten Fall nicht benötigt. Wo immer möglich, sollten Entwickler daher Methoden verwenden, mit denen sich die Zahl der abgefragten Tabellenspalten einschränken lässt. Dies schließt nicht benötigte Long-Datenspalten von vornherein aus.

Mehr noch: Anwender wollen in einer Ergebnisliste den Inhalt einer derartigen Datenspalte meist gar nicht sehen. Es werden also unnötig Daten transportiert. Sollten die Inhalte jedoch tatsächlich weiterverarbeitet werden, genügt es, nur die Long-Daten in einer Select-Liste zu spezifizieren.
Tipp 3: Autocommit in Transaktionen vermeiden
Die Anweisung Commit Transaction speichert alle seit Beginn einer Transaktion (konkret: seit Ausführung der Anweisung Begin Transaction) unternommenen Operationen. Daher enthält eine Commit-Anweisung immer Zugriffe auf die Festplatte und belastet auch das Netzwerk. Stellt eine Applikation eine Verbindung zu einer Datenbank her, erfolgt dies häufig im Autocommit-Modus. Muss jede Operation bestätigt werden, hat das spürbare Auswirkungen auf die Performance. Zudem muss der Datenbankserver jede Data Page mit geänderten oder neuen Daten auf die Platte schreiben. Auch das beansprucht Zeit, selbst wenn der Zugriff sequentiell erfolgt. Statt im Modus Autocommit sollte eine Transaktion daher erst nach Aufbau einer Verbindung zur Datenbank gestartet werden.
Nun verfügen einige Datenbankserver nativ nicht über den Autocommit-Modus. In dem Fall muss der Datenbanktreiber explizit mit Begin Transaction und Commit arbeiten. Das Codefragment in Listing 2 startet eine Transaktion für Oracle.

Listing 2
  1. OracleConnection MyConn = new OracleConnection
  2. ("Connection String info");
  3. MyConn.Open()
  4. // Start a transaction
  5. OracleTransaction TransId = MyConn.BeginTransaction();
  6. // Enlist a command in the current transaction
  7. OracleCommand OracleToDS = new OracleCommand();
  8. OracleToDS.Transaction = TransId;
  9. // Continue on and do more useful work in the
  10. // transaction

Was bringt es, auf Autocommit zu verzichten? Ein Anwender von DataDirect Technologies hat den Test gemacht: Mit Autocommit benötigte er fast fünf Stunden, um 5.000.000 Datensätze in eine DB2-Datenbank einzupflegen, ohne Autocommit waren es nur zehn Minuten. Aber Vorsicht: Das bedeutet keinen permanenten Verzicht auf Autocommit. Denn Autocommit hat den Vorteil, dass Datensätze vor unerwünschtem Zugriff geschützt werden; der gleichzeitige Datenzugriff wird so verhindert. Idealerweise werden Transaktionen in bestimmten Zeitintervallen per Batch-Lauf - etwa in der Nacht - ausgeführt. In den anderen Zeiten steht die Datenbank dann mehr oder minder uneingeschränkt zur Verfügung.
Tipp 4: Vorsichtige Verwendung von Command.Prepare
Um einem SQL-Statement während der Laufzeit variable Werte mitzugeben, kommen so genannte Parameter-Marker zum Einsatz. Die meisten ADO.NET Data Provider benutzen als Parameter-Marker in den SQL-Statements die native Syntax des jeweiligen DBMS. Unter Verwendung von Microsofts SQL Server Data Provider könnte ein einfaches SQL-Statement etwa so lauten:
  1. Select * from table where <a>
  2. column=@parametername</a>

Mit dem Microsoft .NET Framework Data Provider für Oracle sähe das gleiche Statement so aus:
  1. Select * from table where column=:parametername

Entwickler, die in ihrer .NET-Anwendung sowohl auf SQL-Server- als auch auf Oracle-Daten zugreifen wollen, müssen die Unterschiede berücksichtigen und ihren Programmcode entsprechend ändern. DataDirect Technologies dagegen unterstützt mit DataDirect Connect for .NET auch Datenbanksysteme von Oracle oder Sybase. Für beide könnte das SQL-Statement so lauten:
  1. Select * from table where column=?

Beim Einsatz der Methode Command.Prepare
liegen Nutzen und Schaden oft nahe beieinander: Die Performance wird besser - oder schlechter. Command.Prepare veranlasst den Datenbanktreiber, Statements mit Parameter-Marker mehrmals auszuführen. Die Analogie dazu sind Stored Procedures auf einem Datenbankserver. Nun mag die Erstellung solcher Prozeduren zeitaufwändig sein, bei der wiederholten Ausführung der Statements können jedoch zusätzliche Optimierungspfade aufgezeichnet werden, die bei einer künftigen Ausführung der Stored Procedures deutliche Performance-Vorteile bringen. Das Prinzip gilt auch für Command.Prepare. Je häufiger es verwendet wird, desto eher zeigen sich die Geschwindigkeitsvorteile. Umgekehrt wäre der schlechteste Fall, Command.Prepare zur einmaligen Ausführung einer umfangreichen Query einzusetzen - das führt zwangsläufig zu einer schwachen Performance.
Tipp 5: Wo immer möglich: DataReader statt DataSet
Vieles wurde bereits geschrieben über den Unterschied zwischen DataReaders und DataSets und insbesondere über die Randbedingungen, wann DataReaders den DataSets vorzuziehen sind. Vereinfacht ausgedrückt sind DataReaders immer schneller bei der Datenbeschaffung. Ein DataReader liest die Datensätze aus einer relationalen Datenquelle. Die Ergebnisse der Abfrage können für weitere Analysezwecke in einem DataSet abgelegt werden. Ein DataSet verfügt bekanntlich über keinerlei Informationen über die Datenquelle, dagegen stehen dem Managed Data Provider ausführliche und spezifische Informationen zur Verfügung. Dessen Aufgabe besteht darin, die Verbindung mit den Datenspeichern aufzubauen sowie die Daten zwischen DataSet und Datenspeichern und umgekehrt auszutauschen und zu archivieren. Befindet sich die Datenbank einmal im Speicher, hält das DataSet sowohl die Originaldaten als auch alle Änderungen vor - das führt zu einem höheren Speicherbedarf und kann in der Folge Performance- und Skalierbarkeitsprobleme nach sich ziehen.
Ebenso wie bei den anderen erwähnten Tipps gilt auch hier: Wo immer möglich, sollten Entwickler den geraden Weg wählen - nach dem Motto „Keep it Simple“. Von dieser Strategie sollte nur abgewichen werden, wenn im konkreten Fall zusätzliche Funktionen wie im Fall von DataReader benötigt werden.
DataDirectTechnologies – gemeinsame Fundamente sorgen für echte Alternativen
Als Diensteplattform verfügt Microsoft .NET über eine Vielzahl von Funktionen zum Erstellen modernster Anwendungen. Die Microsoft-.NET-Plattform beruht auf Standard-Data-Connectivity-Komponenten. Microsoft bezeichnet sie als Data Provider. Sie sind als ADO.NET-Schnittstellen implementiert und bieten Applikationen eine Reihe von Diensten in Gestalt von Klassen. ADO.NET unterscheidet sich von anderen generischen Datenbankschnittstellen wie ADO oder ODBC insbesondere durch die bessere Kontrolle darüber, was mit den Daten geschieht. Der Preis dafür ist der Verlust bestimmter Vorteile, die generische Datenbankschnittstellen bieten. So gibt es beispielsweise in ADO.NET standardmäßig keinen Parameter-Marker. Diesen stellt DataDirect Technologies jedoch mit seiner Connectivity-Komponente DataDirect Connect for .NET zusätzlich bereit - Entwickler werden damit unabhängig vom jeweiligen Datenbankmanagement-System wie Oracle, Sybase oder SQL Server.
Rob Steward ist Program Manager für Connect for .NET bei DataDirect Technologies, einem der führenden Anbieter von onnectivity-Lösungen in Bedford, Massachusetts/USA.


Anzeige

Kommentare

zurück zum Seitenanfang