Keine Indices für nvarchar(MAX)

Heute habe ich an einer SQL-Datenbank, die ich via Entity Framework 4 von einer C#-Anwendung aus anspreche, „von Hand“ Kolonnen hinzugefügt, um Änderungen an meinen Entities nachzuvollziehen, ohne die Daten in den Tabellen zu verlieren. Dabei ist mir zum ersten mal richtig aufgefallen, dass alle meine textuellen Kolonnen vom Typ nvarchar(MAX) sind, bzw. dass EF4 die Kolonnen so für mich definiert hat.

MAX schien mir Overkill, aber richtig, ich hatte ja bei den Properties im EF-Designer keine Maximal-Längen-Angaben gemacht. Irgendwie verständlich, weil man schliesslich beim O/R-Mapping vom C#-Datentyp String her kommt, wo man sich keine Gedanken über Maximallängen machen muss.

Zur Sicherheit klärte ich ab, ob ich mir mit der Verwendung von nvarchar(MAX) irgendwelche Nachteile einhandle. Ich dachte da an eine mögliche Speicherverschwendung, die so zustandekommt. Eine Frage auf Stack Overflow brachte Klärung: Nein, Speicherverschwendung ist kein Problem, aber SQL Server mag keine normalen Indices bauen mit MAX-Kolonnen drin.

Es ist tatsächlich ein bisschen schwierig, einen B-Baum zu führen mit potentiell beliebig langen Schlüsseln in Seiten fixer Länge!

Neugierig geworden, forschte ich noch etwas weiter. Maximallängen setzen bei den Properties im EF4-Designer löst zwar das erwähnte Indizier-Problem, hat aber eine Schwäche: EF4 selbst überwacht die Maximallängen nicht, sondern gibt einfach alles an den SQL Server weiter, wo es dann zwar schon einen Fehler gibt, aber offenbar einen, bei dem man unter Umständen nicht versteht, wo das Problem liegt. Diese Geschichte ist hier näher beschrieben.

Weiterhin scheint es überraschend schwierig zu sein, die Maximallängen-Angaben, die man im Designer macht, zur Programmlaufzeit abzufragen, z.B. um irgendwelche Eingabe-Controls dynamisch und „generisch“ auf die betreffenden Längen einzuschränken. Nachlesen kann man das z.B. hier.

Schliesslich scheint es mit Code First in EF4.1 und/oder bei Einsatz von SQL Server Compact eine Reihe neuer Fragen rund um die Maximallänge von Text-Kolonnnen zu geben, wie man hier beschrieben findet.

Es ist schon interessant, wie sich beim Programmieren mit .NET immer mal wieder Dinge als vielschichtiger herausstellen, als man auf den ersten Blick annimmt.

Advertisements
Veröffentlicht in Allgemein. Schlagwörter: , . Leave a Comment »

Technologie-Inflation

Ein Artikel in der Zeitschrift iX hat mich heute darauf aufmerksam gemacht, dass zusammen mit dem Service Pack 1 für das .NET Framework 3.5 nun nach langem Warten die erste definitive Version des sogenannten ADO.Net Entity Framework zur Verfügung steht.

Es wurde schon eine ganze Menge darüber geschrieben, was es mit diesen „Entities“ auf sich hat. Z.B. ist die entsprechende Wikipedia-Seite ein guter Startpunkt, um sich einzulesen. Ich will deshalb nicht noch mehr Technisches darüber schreiben, sondern die Gelegenheit ergreifen, um ein bisschen über die Sache zu „philosophieren“.

Applikationen für vorwiegend administrative Zwecke, wie sie die Megos anbietet, sind sehr stark geprägt durch den Zugriff auf die Daten. Seit wir vor einiger Zeit beschlossen hatten, für die Programm-Entwicklung auf .NET umzusatteln, verfolgte ich deshalb ziemlich aufmerksam, was sich bei .NET in Sachen Datenzugriff und Datenverwaltung tut, und wurde schnell neugierig, als ich vor einiger Zeit zum ersten Mal von diesem Entity Framework hörte.

Was ich sah, als ich mich näher damit beschäftigte, hat mir zunächst fast die Sprache verschlagen: So ein Riesen-Ding, mit eigener SQL-Variante für die Abfrage, mit eigener Art von Datenbank-Modell, das über das relationale Modell hinausgeht, samt zugehörigem, nicht zu klein geratenem Modellierungs-Tool, plus jeder Menge neuer .NET-Klassen natürlich. Und fast noch bemerkenswerter als die Grösse der ganzen Geschichte kam mir deren Komplexität vor.

Dann wurde mir klar, dass .NET mit LINQ-to-SQL und dem Entity Framework gleich zwei Technologien enthalten wird, welche sich bezüglich der grundlegenden Aufgabe des sogenannten OR-Mapping überschneiden und konkurrenzieren. Die mögliche Antwort von Microsoft wird vielleicht – nicht unbedingt überraschend – noch mehr Neues, noch mehr Grosses und Komplexes sein, in Form von LINQ-to-Entities.

Ich habe mich unweigerlich gefragt, wie viele Leute wohl auf der Welt eine Datenbank bewirtschaften müssen mit einer Struktur, die so vielfältig und komplex ist, dass man dieses Entity Framework zum Einsatz bringen kann und netto etwas herausschaut dabei, will sagen: Der Gewinn an Funktionalität und Flexibilität beim Umgang mit der Datenbank in den .NET-Programmen wiegt die Arbeit auf, die man aufwenden muss, um sich in diese riesige Geschichte einzuarbeiten, und wiegt dazu noch den Ärger auf, den man hat durch Bugs, die es fast unweigerlich geben muss in so viel neuem Code.

Und die Frage „Soll ich auf das Entity Framework setzen?“ hat natürlich noch ganz andere Seiten. Soll ich meine Applikation um eine Technologie herum bauen, die ziemlich Microsoft-spezifisch ist? Wie langlebig wird diese Sache sein? Die Liste der Datenbank-Zugriffs-Technologien, die Microsoft zu „Legacy“ heruntergestuft hat, enthält schon einige Einträge (ODBC, DAO, OLE DB), und vielleicht kommt ja schon in relativ kurzer Zeit ein Eintrag dazu, wenn Microsoft sich eingestehen müsste, dass zwei OR Mappers einer zuviel ist.

Interessanterweise ist es mir bei einer Google-Suche als Vorbereitung auf das Schreiben dieses Blog-Eintrags nicht gelungen, Leute zu finden, die sich wie ich angesichts der Grösse, der Komplexität und des proprietären Anstrichs des Entity Framework Sorgen machen und erst einmal abwarten, bevor sie es einsetzen. Aber vielleicht ist es dafür einfach noch etwas zu früh…

Veröffentlicht in Keine Kategorie. Schlagwörter: , . Leave a Comment »

Die Millionen-DataGridView

Es gibt ein paar Aufgaben in der Welt der Programmierung, die seltsamerweise seit vielen Jahren auf eine elegante Lösung warten, obwohl es sich um wichtige Aufgaben handelt und sich immer wieder sehr intelligente Leute um eine Lösung bemühen. Die effiziente interaktive Anzeige eines grossen Datenbestandes, auf den man per SQL zugreift, scheint mir eine solche Aufgabe zu sein.

Konkret: Nehmen wird an, man hat eine Tabelle, die nicht zwangsläufig gross ist, aber doch gross werden kann, und will sie in einer DataGridView anzeigen. Das Ganze soll auch dann noch ordentlich funktionieren, wenn die Tabelle mal 1 Million Rows umfasst.

Auf meiner Suche nach einer Lösung für dieses Problem war ich zunächst optimistisch, weil die DataGridView vor einiger Zeit ein Feature namens VirtualMode dazugelernt hat. Vor diesem Feature hätte die View bei einem Einsatz von „DataBinding“ mal eben die Million Rows aus der Tabelle gelesen, was natürlich keine gute Idee ist. Mit VirtualMode kann man selbst die Daten häppchenweise laden, weil die View per CellValueNeeded jeweils bekanntgibt, welche Daten für die Anzeige gerade benötigt werden.

Das Lesen und Liefern der Daten aus einer SQL-Datenquelle in „Häppchen“ ist zwar nicht trivial, wie einschlägiger Demo-Code zeigt, aber machbar.

Was mir allerdings an dieser Lösung nicht gefallen wollte, war die Tatsache, dass die View auch im VirtualMode alle Zeilen und Zellen erzeugt, also im vorliegenden Beispiel 1 Million Zeilen. Es werden nur die Daten „virtualisiert“, das Grid ist aber immer in seiner ganzen monumentalen Grösse da und frisst Speicher.

Ich habe darum für die neue Programmierumgebung Triton der Megos, die gerade entsteht, und deren View auf grosse Tabellen einen Ansatz implementiert, den ich seltsamerweise so noch nirgendwie sonst gefunden habe. Ich will nicht behaupten, der Ansatz stelle die vielgesuchte Lösung für das hier diskutierte hartnäckige Problem dar, denn er hat auch seine Nachteile, aber ich denke, das Resultat ist ganz ordentlich:

Ich verwende eine DataGridView als reines Anzeige-Instrument: Kein DataBinding, kein VirtualMode, einfach eine Anzahl Zeilen, sagen wir als Beispiel einmal 30, die alle ganz sichtbar sind, so dass die View findet, vertikal gäbe es nichts zu rollen. Die View hat sozusagen keine Ahnung von den Daten, die sie anzeigt.

Ich lese Rows selbst aus der Tabelle mit Hilfe eines SqlDataReader. Um nach dem erstmaligen Öffnen und Anzeigen der View ihre 30 Zeilen mit den ersten 30 Rows aus den Daten zu füllen, führe ich ein SELECT * FROM Tabelle aus, hole mit Hilfe des Readers die ersten 30 Rows ab und fülle deren Daten in die entsprechenden Zellen.

Die SELECT-Anweisung ist schon richtig: Sie geht über die ganze Tabelle. Also kein SELECT TOP n oder ähnliches. Sorgfältige Tests haben gezeigt, dass eine solche Anweisung völlig harmlos ist, wenn die angegebene ORDER durch einen Index unterstützt wird. Weder wird der Datenbank-Server über Gebühr belastet, weil er versucht, die ganze Tabelle im Voraus zu lesen, noch werden Unmengen von Daten vorauseilend über das Netz gesendet.

Positioniert nun der Anwender die Selektion auf die letzte angezeigte Row und drückt Cursor Down, schiebe ich zunächst selbst die View „nach oben“, indem ich die Inhalte der Zellen umkopiere: Die Werte aus der 2. Zeile kommen in die 1. Zeile, die Werte aus der 3. Zeile kommen in die 2. Zeile, usw. Das mag Ihnen auf den ersten Blick vielleicht sehr seltsam vorkommen, denn natürlich schiebt im Normalfall die View völlig selbständig, aber hier kann sie es nicht, weil es eben keine 31. Zeile gibt, welche die View von sich aus in den sichtbaren Bereich hineinschieben könnte. Man muss es selber tun, was aber nicht weiter kompliziert ist.

Die Daten für die quasi „neu erscheinende“ Zeile beschaffe ich mir, indem ich per Reader noch eine Row mehr abhole. Der SqlDataReader hat ja – wie gesagt ohne schädliche Nebenwirkungen – brav auf mich gewartet, in einem „offenen“ Zustand, positioniert hinter Row 30.

Solange es also in der View abwärts geht, hole ich einfach weiter Rows ab mit Hilfe des Readers und schiebe Zelleninhalte selber nach oben.

Die Sache wird nur moderat komplizierter, wenn der Anwender etwas verlangt, was zu einer Repositionierung des Daten-Ausschnitts oder zumindest zu einem Richtungswechsel bei der Anzeige führt. Dann muss ich den Reader freigeben und einen neuen aufsetzen, in der Richtung, in der es bei der Anzeige gehen soll, rückwärts also mit Hilfe von DESC. Ich muss mir zudem die Schlüssel aller 30 in der View angezeigten Zeilen merken, damit ich bei einem Richtungswechsel die Schlüsselwerte für eine WHERE x < <key> bzw. WHERE x > <key>-Klausel zur Verfügung habe beim Öffnen des neuen Readers.

Was es auch noch braucht, ist eine vertikale Scrollbar, denn die View zeigt ja wie gesagt von sich aus keine solche an, weil alle ihre 30 Zeilen stets ganz sichtbar sind, und von mehr weiss sie nichts. Man muss sich also wiederum wie beim Umkopieren der Zellen-Werte um etwas kümmern, was die View sonst immer vollständig alleine macht, und alle 7 Scroll-Funktionen (Anfang, Ende, Zeile auf, Zeile ab, Seite auf, Seite ab, Positionieren) nachbauen.

Und zu guter Letzt muss man sogar beim Rollen die Selektion selber umpositionieren, denn vom Standpunkt der View aus wird nichts gerollt, weshalb sie auch die Selektion nicht bewegt.

Die resultierende Kombination DataGridView / VScrollBar / SqlDataReader braucht ein paar Seiten Code, um zu funktioneren, aber der Code ist über weite Strecken relativ einfach und durchsichtig. Auf irgendwelche komplizierten „Paging“-Konstrukte, stored procedures usw. kann man verzichten, und die Datenmenge in der Tabelle ist mehr oder weniger egal: Es dürfen auch 100 Millionen Records sein, wenn es sein muss.

Veröffentlicht in Keine Kategorie. Schlagwörter: , . 1 Comment »