Glorf.it

Glorf IT

Bedenkliches aus dem IT-Alltag

22. Mai 2008 um 16:39

SQL Server 2000 auf Windows Vista

Weil mich jetzt innerhalb kurzer Zeit mehrere darauf ansprachen, poste ich hier mal den Link unter dem Microsoft sagt, welche SQL-Server-Versionen unter Windows Vista laufen. Im Artikel "Running SQL Server on "Microsoft Windows Server Longhorn" or Microsoft Windows Vista" schreibt Microsoft:

In an effort to provide customers with more secure products, Microsoft Windows Server "Longhorn" and Microsoft Windows Vista are supported by SQL Server 2005 [..]. Earlier versions of SQL Server, including SQL Server 2000 (all editions including Desktop Engine edition, a.k.a MSDE), SQL Server 7.0, and SQL Server 6.5, will not be supported on Windows Server "Longhorn" or Windows Vista.

Also muss man wenigstens SQL-Server-2005 haben, wenn die Kunden Vista einsetzen wollen. In den Download-Informationen der MSDE steht es auch noch mal explizit drin:

Wichtiger Hinweis Microsoft SQL Server Desktop Engine (MSDE) wird auf dem Betriebssystem Microsoft Vista nicht unterstützt.

Tja. Das hat natürlich den Absatz des SQL-Servers-2005 enorm angekurbelt.

21. Mai 2008 um 21:16

Prompte Antwort

Ermutigt durch Christoph schickt ich eine Beschreibung des Problems mit den nicht-deterministischen Funktionen an Craig Freedman (zum Glück privat, sonst wäre meine Mail vermutlich nie angekommen). Er antwortete sehr prompt und schrieb mir, dass er das Problem dem für den Teil zuständigen Entwickler gezeigt habe. Hier die Antwort des Entwicklers, die Craig mir weiterleitete:

In general, SQL Server does not guarantee the timing of execution of scalar operators. For non-deterministic scalars (built-in and user-defined), that means the timing semantics (number of times executed and when) is not defined. In addition, it may change from one plan to another, or from one release to the next.

Er selber fügte noch als persönlichen Kommentar an, dass er die Lösung mittels temporärer Tabelle favorisiere:

Unfortunately, I am not aware of (and was unable to identify) a better workaround than simply storing the GUIDs in a temp table before joining with the original table.

Er schlug auch vor, dass ich das Kundenfeedback unter http://connect.microsoft.com melde, obwohl ich da möglicherweise die gleiche Antwort bekomme. Ich stimme ihm in all diesen Punkten zu. Es lohnt sich sicher das dort einzukippen, weil es dann mal in einem Forum öffentlich diskutiert wird. Mal sehen, ob ich morgen dazu komme…

16. Mai 2008 um 11:41

SQL Server Is Not Aware of Nondeterministic Functions

Normally I write in German, but I hope that maybe some Microsoft Engineer might read it and put the problem on their schedule. So, I apologise to my German readers for writing in poor English… 😉

When the SQL Server optimizer internally transforms a SQL Statement to an other, it normally results in a better execution plan. When You write for example a subquery the optimizer tries to make a join instead. This is fine as long a the result set is the same. But now we discovered a situation when this is not true: If you use a non-deterministic function in a query, the optimizer use the same query plan as if the function were deterministic. Let us examine this simple example:

We have a table with 5 rows: the key consists of the tupel (ID1,ID2). We add a new column to add a GUID later.

ID1 ID2 GUID
1 1 NULL
1 2 NULL
2 1 NULL
2 2 NULL
3 1 NULL

For each different value of ID1 we want to add a GUID with the non-deterministic function NewID():

SELECT
ID1
, NEWID() as [NewId]
FROM Table1
GROUP BY ID1

This results in something like:

ID1 NewId
1 B30AD595-926B-40F6-A815-D8871C81CE89
2 6842624D-DC40-4A3F-A2EB-A8178814D12E
3 12E9F691-7F8B-41F8-829B-8CED5E26C127

Now, we want to use the above query to assign the new values to the column GUID. To make the execution plan easier to understand, I used a SELECT, not an UPDATE. But the result is the same:

SELECT
t.ID1
, sub.NewId
FROM (SELECT
ID1
, NEWID() as [NewId]
FROM Table1
GROUP BY ID1) as sub
JOIN Table1 as t
ON t.ID1=sub.ID1

It should result in something like this:

ID1 NewId
1 B30AD595-926B-40F6-A815-D8871C81CE89
1 B30AD595-926B-40F6-A815-D8871C81CE89
2 6842624D-DC40-4A3F-A2EB-A8178814D12E
2 6842624D-DC40-4A3F-A2EB-A8178814D12E
3 12E9F691-7F8B-41F8-829B-8CED5E26C127

But this is not the case. The result is:

ID1 NewId
1 B30AD595-926B-40F6-A815-D8871C81CE89
1 4EC62D6E-8ABC-4563-8A81-2DB8E655D3CA
2 6842624D-DC40-4A3F-A2EB-A8178814D12E
2 73B7386E-688D-48D2-A585-C66C6911EFC3
3 12E9F691-7F8B-41F8-829B-8CED5E26C127

Do you see the difference? The reason for this is the wrong execution plan:

|--Compute Scalar(DEFINE:([Expr1004]=newid()))
|–Merge Join(Inner Join, MERGE:([tempdb].[dbo].[Table1].[ID1])=([t].[ID1]), RESIDUAL:([tempdb].[dbo].[Table1].[ID1]=[tempdb].[dbo].[Table1].[ID1] as [t].[ID1]))
|–Sort(DISTINCT ORDER BY:([tempdb].[dbo].[Table1].[ID1] ASC))
| |–Table Scan(OBJECT:([tempdb].[dbo].[Table1]))
|–Sort(ORDER BY:([t].[ID1] ASC))
|–Table Scan(OBJECT:([tempdb].[dbo].[Table1] AS [t]))

The optimizer choose to do the join first and to execute the function for each row. This is not correct.

So the general rule I recommend: Do not use non-deterministic functions in complex SQL queries. The result depends on the chosen query plan. Today the result may be correct, but if the row numbers change or you add an index the result may get incorrect! This is not as it should be.

Unfortunately I was not able to convince the German support that this is a bug. They send me a workaround like this:

DECLARE @Rows AS BigInt;
SELECT @Rows = Count(*) FROM Table1;

SELECT
t.ID1
, sub.NewId
FROM (SELECT TOP(@Rows)
ID1
, NEWID() as [NewId]
FROM Table1
GROUP BY ID1
ORDER BY NewId) as sub
JOIN Table1 as t
ON t.ID1=sub.ID1

The performance is very poor and I would recommend using a temporary table to store the new GUIDs instead, if you have a big row number. This seems to perform a little better.


Update (21.5.2008): Encouraged by Christoph I send the problem to Craig Freedman. He answered the very next day, that he presented the problem to the developer responsible for this code for his comments on that question. Here is the response of the developer Craig forwarded to me:

In general, SQL Server does not guarantee the timing of execution of scalar operators. For non-deterministic scalars (built-in and user-defined), that means the timing semantics (number of times executed and when) is not defined. In addition, it may change from one plan to another, or from one release to the next.

Craig added:

Unfortunately, I am not aware of (and was unable to identify) a better workaround than simply storing the GUIDs in a temp table before joining with the original table.

(My Thanks to Craig! I am very impressed that he ask the responsible developer. And thanks to the unknown developer.)


Update (11.6.2008): Itzik Ben-Gan did it! Microsoft confirmed to him this is a bug.

Currently I read the very good book "T-SQL Querying" written by Itzik. In the very first chapter he described that the optimizer can make shurtcuts in the physical execution plan if the result set is the same like using the logical execution plan. I asked him about his opinion about this problem and he made Microsoft to accept that this is a bug. He wrote:

I got back a response from Microsoft saying it is a bug, and apparently a regression from SQL Server 2000.

If you think Microsoft should fix this bug, then "vote" for this bug, please. The more people vote, the more likely MS will fix it.


Update (19.6.2008): Thanks for your support. The MS Developer Jim answered to this bug:

Thankyou for this bug report. One of the most interesting discussions around.

This hits to the very heart of the issue – is optimization allowed to change a program's semantics? Ie: if a program yields certain answers, but runs slowly, is it legitimate for a Query Optimizer make that program run faster, yet also change the results given?

Before shouting "NO!" (my own personal inclination too :-), consider: the good news is that, in 99% of cases, the answers ARE the same. So Query Optimization is a clear win. The bad news is that, if the query contains side-effecting code, then different plans CAN indeed yield different results. And NEWID() is one such side-effecting (non-deterministic) 'function' that exposes the difference. [Actually, if you experiment, you can devise others – for example, short-circuit evaluation of AND clauses: make the second clause throw an arithmetic divide-by-zero – different optimizations may execute that second clause BEFORE the first clause] This reflects Craig's explanation, elsewhere in this thread, that SqlServer does not guarantee when scalar operators are executed.

So, we have a choice: if we want to guarantee a certain behavior in the presence of non-deterministic (side-effecting) code – so that results of JOINs, for example, follow the semantics of a nested-loop execution – then we can use appropriate OPTIONs to force that behavior – as UC points out. But the resulting code will run slow – that's the cost of, in effect, hobbling the Query Optimizer.

All that said, we are moving the Query Optimizer in the direction of "as expected" behavior for NEWID() – trading off performance for "results as expected".
[…]
Anyhow, this bug is now assigned to the QO Dev team for a deeper look.

I presume "QO Dev team" is the "Query Optimizer Development team". 😉


Update (27.7.2008): In the last issue of the SQL Server Magazine Newsletter Itzik Ben-Gan wrote some bad news about this bug:

I posted the bug on Microsoft Connect (FeedbackID=350485), and after consideration, Microsoft decided to close the item and mark it as “Won’t Fix”. The reasoning behind the decision not to fix the bug is that in the vast majority of the cases, the optimization aspects that lead to the bug yield better performance without sacrificing the correctness of the query, and if you fall into one of the unusual cases where the correctness of the query is compromised, you can consider alternatives (e.g., physically materialize the data along with the NEWID values in a table).

This was quite unexpected for me. But I think there is nothing we can do about it. Thats a pity…

15. Mai 2008 um 12:29

dynamische WHERE-Klausel bei statischem SQL?

Im Artikel "Implementing Dynamic WHERE-Clause in Static SQL" wird beschrieben wie man mittels COALESCE, ISNULL oder CASE eine dynamische WHERE-Klausel implementieren kann. Wenn ich mir die Beispiele ansehe, dann habe ich aber meine Zweifel, ob der Optimizer da noch eine Chance hat den Zugriff performant (über den richtigen Index) hinzubekommen.

Spätestens bei großen Datenmengen dürfte der Einsatz von echt dynamischem SQL mittels EXEC() schneller sein. Das wird ganz gut im Artikel "Introduction to Dynamic SQL (Part 2)" erklärt.

14. Mai 2008 um 09:59

Tool-Tipp: dbConstructor

Auf dem letzten SQLPASS-Treffen der Regionalgruppe Franken in Nürnberg stellte der Referent Peter Döring (MVP) ein nettes Tool vor:

Mit dbConstructor kann man sehr gut Datenbanken von einem System zu anderen migrieren (z.B. von Oracle nach DB/2 und umgekehrt). Man kann es auch einfach zum Entwerfen von Datenbanken verwenden und sogar den Zugriffscode template-gesteuert generieren lassen. Das hat mich schon beeindruckt. Der Preis von 150 USD ist (besonders beim aktuellen Umrechnungskurs) wirklich günstig.

13. Mai 2008 um 09:57

atemberaubende Arbeitszeiten bei Microsoft

Neulich meldete ich ein Problem zum Microsoft SQL-Server von dem ich immer noch denke, dass es ein Bug und kein Features ist. Ich lieferte ein leichtes Repro und belegte anhand des Zugriffsplans das Fehlverhalten. Der First-Level-Supportmitarbeiter antwortete in einer Mail, dass er das Verhalten auch nicht ganz erwartungskonform fand und wollte sich mit einem Backline-Supporter unterhalten. Das sind Leute hier in Europa, die sich ganz gut auskennen und dann entscheiden, ob das als potentieller Bug nach Redmond gemeldet wird oder nicht. Im nächsten Schritt wird so ein gemeldeter Bug-Verdacht für gewöhnlich von einem der Entwickler begutachtet, der für das entsprechende Modul verantwortlich ist. So war es jedenfalls bisher. Diesmal scheiterte ich an der zweiten Hürde (ohne dass ich mit dem Backline-Suppporter selber sprechen durfte).
Bug or not?
In der zweiten Antwort schrieb der First-Level-Supporter, dass sei ein Feature und kein Bug. Der Backline-Engineer habe ihn davon überzeugt und deswegen würde es nicht als potentieller Bug nach Redmond gemeldet. Ich lieferte weitere Argumente und bekam wieder eine abschlägige Antwort. Das passierte noch ein mal. Es gab dort intern also drei Diskussionen (bzw. manchmal mailen die sich auch bloß gegenseitig, das war diesmal offenbar nicht so).

Als nächstes fragte unser zuständiger Microsoft-Betreuer nach, ob sie an dem Problem wirklich dran bleiben sollen, es seien im MS-Support immerhin schon 11 Arbeitsstunden verbraucht worden, die wir bezahlen müssen (weil es ja kein Bug sei). Und wie viele Stunden uns das noch wert wäre? Ich war ziemlich platt, dass allein für die bisherigen drei Diskussionen zwischen Firstlevel- und Secondlevel-Support 11 Stunden veranschlagt wurden. Da es zwei Leute waren, dauerten die Diskussionen in Summe also 5,5 Stunden. Das zu glauben fällt mir zwar schwer, aber OK – manche verstehen Probleme schneller, manche brauchen etwas länger. Darüber zu urteilen steht mir nicht zu… 😉

Also antwortete ich (gegen Mittag), dass das ein wichtiges Problem sei und wir so lange dran bleiben bis eine abschließende Antwort vom Engineering aus Redmond käme. Um 15:30 Uhr mailte dann der First-Level-Mann, er habe jetzt auch noch mit demjenigen in Redmond gesprochen, der entscheidet ob etwas ein Bug sei oder nicht. Auch er sei der Meinung es sei kein Bug, daher werden sie es nicht als potentiellen Bug nach Redmond melden.
Aber in der Beschreibung des Features sei nicht alles ganz richtig wiedergegeben und das werde als Bug in der Doku gemeldet. Deswegen müssten wir nichts zahlen.

Das löste bei mir eine Reihe von Fragen aus:

  • Klingt nach Kuhhandel – wer kennt das Spiel? Offenbar dachte der Supporter es ginge mir darum, dass wir nichts zahlen wollen. Schade, denn tatsächlich ist das Problem für uns wichtig.
  • Gibt es in Redmond wirklich einen Mann, der entscheidet, ob etwas im SQL-Server ein Bug ist oder nicht? Als ich zuletzt dort war, war es noch anders.
  • Wenn das so ist, warum wurde mit ihm darüber inoffiziell diskutiert, anstelle ihm den Fall mit allen Informationen komplett zu schicken, damit er auch das Repro sieht?
  • Ist der Mann in Redmond wirklich zwischen 3 Uhr nachts und 6:30 Uhr am Morgen (Ortszeit in Redmond) erreichbar? Das nenne ich atemberaubend und mehr als beeindruckend!

Nun hatte ich die Wahl entweder das Ganze höher aufzuhängen (und vielleicht sogar den Supporter persönlich anzugreifen) oder den Fall zu schließen. Ich ließ ihn schließen… und werde versuchen das Problem über andere Kanäle bis Redmond zu bekommen. 🙁

9. Mai 2008 um 18:50

SQL-Server-2005: PIVOT auf JOINs

Als ich mit dem PIVOT-Konstrukt arbeitete und performante Ergebnisse haben wollte, muss ich erst verstehen, wie es intern arbeitet. Das beschrieb ich im Artikel "SQL-Server-2005: schnelles Pivot?". Diese Serie soll meine Erfahrungen weitergeben und bei Verstehen der Zusammenhänge helfen. Mit ein paar Kniffen kann man dann die Ausführung erheblich beschleunigen. Der Quellcode zu diesem letzten Artikel steht übrigens hier.

Performance von "gejointen" Daten

Wenn die Basistabelle des PIVOTs aus einer Derived-Table besteht, die mittels Join die Daten aus mehreren Tabellen sammelt, dann hängt es von relativ vielen Faktoren ab, ob das PIVOT schnell ist. Das gleiche gilt natürlich auch, wenn der PIVOT auf eine View oder eine Inline-Table-Valued-Function durchgeführt wird.

Man kann es aber auf den Nenner bringen: Kann der Optimizer das PIVOT so begrenzen, dass die Gruppierung allein auf den Werten einer Tabelle basiert, die mittels Clustered-Index gelesen werden können, dann ist es schnell. In den anderen Fällen muss man mit mehr oder weniger starken Performanceeinbußen rechnen.

Als Beispiel möchte ich einen relativ gängigen Sonderfall darstellen: Das Beispiel des ersten Artikels wird so erweitert, dass die Spaltenüberschriften in einer Translation-Table TranTab stehen:
Translation-Table

In der Tabelle OpenSchema steht dann der Verweis auf den Namen des Attributes:
Tabelle Openschema mit Übersetzungstabelle

Um das gewünschte Ergebnis zu bekommen, muss man PIVOT auf eine Derived-Table ausführen:

SELECT ID, "Typ", "Datum", "Anzahl", "Dings", "Bums"
FROM (SELECT os.ID, tt.Attribute, os."Value"
FROM OpenSchema as os
JOIN TransTab as tt
ON os.AttId=tt.AttId) as dt
PIVOT
(
Max("Value")
FOR Attribute IN ("Typ", "Datum", "Anzahl", "Dings", "Bums")
) AS pvt
ORDER BY ID

Die Performance verschlechtert sich dadurch dramatisch. Im Vergleich zum reinen Lesen der Daten fast um Faktor 4. Das liegt am ungünstigen Zugriffsplan:
Zugriffsplan

Wenn man einen PIVOT ohne den Join durchführt, dann erhält man die gewohnte, gute Performance:
SELECT ID, "1" as "Typ", "2" as "Datum", "3" as "Anzahl", "4" as "Dings", "5" as "Bums"
FROM (SELECT os.ID, os.AttId as Attribute, os."Value"
FROM OpenSchema as os ) as dt
PIVOT
(
Max("Value")
FOR Attribute IN ("1", "2", "3", "4", "5")
) AS pvt
ORDER BY ID

Leider muss man dazu die Spaltenamen "hart" codieren. Das ist aber genau das, was man vermeiden wollte als man die Translation-Table einführte. Um auch mit flexiblen Spaltennamen zu einer guten Performance zu kommen, muss man im Batch zunächst die Spalten ermitteln, das PIVOT-Statement dann dynamisch zusammensetzen und ausführen:

DECLARE @sqlcmd nvarchar(1000),
@selectList nvarchar(1000),
@pivotList nvarchar(1000);

SELECT
– zunächst die Spalten aus der SELECT-Liste ermitteln
@selectList = STUFF(
(SELECT N', '+QUOTENAME(AttId)+N' AS '

+QUOTENAME(Attribute) AS [text()]
FROM TransTab
ORDER BY AttId
FOR XML PATH('')), 1, 1, N''),
– dann die Liste der Spalten ermitteln
@pivotList = STUFF(
(SELECT N', '+QUOTENAME(AttId) AS [text()]
FROM TransTab
Order BY AttId
FOR XML PATH('')), 1, 1, N'');

SET @sqlcmd = N'SELECT ID, '+@selectList +N'
FROM (SELECT os.ID, os.AttId as Attribute, os."Value"
FROM OpenSchema as os) as O
PIVOT (Max("Value") FOR Attribute IN ('+@pivotList+N')) as P
ORDER BY Id;'

EXEC sp_executesql @sqlcmd;

Das ausgeführte Statement sieht dann so aus:
SELECT ID, [1] AS [Typ], [2] AS [DATUM], [3] AS [Anzahl], [4] AS [Dings], [5] AS [Bums]
FROM (SELECT os.ID, os.AttId as Attribute, os."Value"
FROM OpenSchema as os) as O
PIVOT (Max("Value") FOR Attribute IN ( [1], [2], [3], [4], [5])) as P
ORDER BY Id;

Die Gesamtperformance dieses Beispiels ist wieder sehr gut.
Insgesamt ergeben sich mit Pivot eine Menge neuer Möglichkeiten. Wenn dabei die Performance wichtig ist, dann muss man sich jedoch genau überlegen, wie man das Statement auch wirklich schnell ausführen kann.

Diese Reihe schrieb ich letzten Oktober als Reaktion auf den Aufruf im SQL-PASS-Newsletter. Möglicherweise erscheint der Artikel noch dort. Ich veröffentliche ihn jetzt aber lieber mal bevor er mit dem bald erscheinenden SQL-Server-2008 möglicherweise veraltet… 😉
7. Mai 2008 um 18:58

SQL-Server-2005: PIVOT und Performance

Als ich mit dem PIVOT-Konstrukt arbeitete und performante Ergebnisse haben wollte, muss ich erst verstehen, wie es intern arbeitet. Das beschrieb ich im Artikel "SQL-Server-2005: schnelles Pivot?". Diese Serie soll meine Erfahrungen weitergeben und bei Verstehen der Zusammenhänge helfen. Mit ein paar Kniffen kann man dann die Ausführung erheblich beschleunigen. Der Quellcode zu diesem vorletzten Artikel steht übrigens hier.

Performance

Um ein Gefühl dafür zu bekommen, wird die im ersten Artikel beschriebene Tabelle "OpenSchema" mit 1,9 Mio Datensätzen gefüllt und ausgelesen. Interessant ist, wie viel "Overhead" ein PIVOT ausmacht. Wenn man ein SELECT auf die ganze Tabelle mit einem PIVOT im gleichen Batch ausführt und dazu mit "Strg+M" den tatsächlichen Zugriffsplan anzeigen lässt, dann bekommt man neben den Zugriffsplänen auch den geschätzten, prozentualen Anteil der Statements im Batch.

SELECT ID, Attribute, "Value"
FROM OpenSchema

SELECT ID, attr1 as Typ, attr2 as Datum, attr3 as Anzahl, attr4 as Dings, attr5 as Bums
FROM OpenSchema
PIVOT
(
Max("Value")
FOR Attribute IN ("attr1", "attr2", "attr3", "attr4", "attr5")
) AS pvt
ORDER BY ID

Liegt ein Clustered-Index auf der Spalte ID, dann ist das Verhältnis 44% zu 56%. Das bedeutet, der Optimizer schätzt etwa 22% Aufschlag für den PIVOT gegenüber dem einfachen Auslesen. Das deckt sich mit dem Zugriffsplan des PIVOT: Hier benötigt das einfache Lesen auch 78%:

Das ändert sich allerdings dramatisch, wenn kein geeigneter Index vorhanden ist. Dann ist das Verhältnis im Batch 1% zu 99%.

Da für die Gruppierung bereits anhand der ID sortiert werden muss, hat das nachfolgende „ORDER BY ID“ tatsächlich keinerlei Auswirkungen auf den Zugriffsplan. Das sieht allerdings anders aus, falls man nach anderen Kriterien sortieren lässt. Das wird aber nur benötigt, wenn man Joins im PIVOT verwendet. dazu komme ich im letzten Artikel.

Zusammenfassend kann man sagen, dass ein PIVOT immer dann "schnell" ist, wenn ein Clustered Index auf den Gruppierungsfeldern liegt. Ein normaler Index hilft hier nicht weiter. Wie schnell er tatsächlich ist, hängt – wie immer – von der darunter liegenden Hardware ab…

Diese Reihe schrieb ich letzten Oktober als Reaktion auf den Aufruf im SQL-PASS-Newsletter. Möglicherweise erscheint der Artikel noch dort. Ich veröffentliche ihn jetzt aber lieber mal bevor er mit dem bald erscheinenden SQL-Server-2008 möglicherweise veraltet… 😉
5. Mai 2008 um 18:57

SQL-Server-2005: Störer beim PIVOT eliminieren

Als ich mit dem PIVOT-Konstrukt arbeitete und performante Ergebnisse haben wollte, muss ich erst verstehen, wie es intern arbeitet. Das beschrieb ich im Artikel "SQL-Server-2005: schnelles Pivot?". Diese Serien soll meine Erfahrungen weitergeben und bei Verstehen der Zusammenhänge helfen. Mit ein paar Kniffen kann man dann die Ausführung erheblich beschleunigen. Das kommt im nächsten Artikel. Der Quellcode zu diesem Artikel steht übrigens hier.

Eliminierung von Störern

Wenn in der Tabelle weitere Spalten enthalten sind, dann kann das mit PIVOT zu unerwarteten Ergebnissen führen. Wenn die Tabelle OpenSchema bspw. auch noch die Spalte „Stoerer“ enthält:
Tabelle Openschema mit Störer

Wenn man nun einfach das PIVOT aus dem letzten Artikel einfach verwendet, dann führt das zu komischen Ergebnissen.

SELECT ID,
attr1 as Typ,
attr2 as Datum,
attr3 as Anzahl,
attr4 as Dings,
attr5 as Bums
FROM OpenSchema
PIVOT (
MAX("Value")
FOR Attribute IN ("attr1", "attr2", "attr3", "attr4", "attr5")
) AS pvt
ORDER BY ID

Unerwartetes Ergebnis

Das liegt an der impliziten Gruppierung aller Felder aus der Basistabelle. Es kann also passieren, dass durch eine unbedachte Tabellenerweiterung einige SELECTs ein völlig anderes Ergebnis liefern. Das ist nicht im Geiste von SQL, ist unangenehm und soll vermieden werden. Daher rate ich dazu PIVOT-Statements immer so zu formulieren, dass sie nicht auf der Basistabelle arbeiten, sondern einer Derived-Table:

SELECT ID,
attr1 as Typ,
attr2 as Datum,
attr3 as Anzahl,
attr4 as Dings,
attr5 as Bums
FROM (
SELECT ID, Attribute, Value
FROM OpenSchema) as dt
PIVOT (
MAX("Value")
FOR Attribute IN ("attr1", "attr2", "attr3", "attr4", "attr5")
) AS pvt
ORDER BY ID

Das liefert nicht nur in diesem Fall das erwartete Ergebnis, sondern auch nach weiteren Tabellenänderungen. Der Zugriffsplan ist entspricht dem ersten Artikel.

Diese Reihe schrieb ich letzten Oktober als Reaktion auf den Aufruf im SQL-PASS-Newsletter. Möglicherweise erscheint der Artikel noch dort. Ich veröffentliche ihn jetzt aber lieber mal bevor er mit dem bald erscheinenden SQL-Server-2008 möglicherweise veraltet… 😉
3. Mai 2008 um 18:51

SQL-Server-2005: schnelles Pivot?

Als ich mit dem PIVOT-Konstrukt arbeitete und performante Ergebnisse haben wollte, muss ich erst verstehen, wie es intern arbeitet. Mit ein paar Kniffen kann man dann die Ausführung erheblich beschleunigen. Diese Serien soll meine Erfahrungen weitergeben und bei Verstehen der Zusammenhänge helfen. Der Quellcode zu diesem Artikel steht übrigens hier.

Wie wird PIVOT ausgeführt?

Angenommen die Tabelle „OpenSchema“ besteht aus drei Spalten: einer ID, einem Attributbezeichner und einer Wert-Spalte. Das sähe etwa so aus:
Tabelle Openschema

Diese Tabelle ist übrigens einem Beispiel von Itzik Ben-Gan (siehe www.sql.co.il) entnommen, weil er das PIVOT-Statement am besten erklären kann. Ehre wem Ehre gebührt.

Um die Ergebnisse dieser "Kennziffer-Wert-Tabelle" in "relationaler" Form zu präsentieren, müssen die Ergebnisse pivotiert werden:
gewünschtes Ergebnis

Vor dem SQL-Server-2005 war dazu ein recht kompliziert aussehendes Konstrukt notwendig: Man musste pro Ergebnis eine Aggregatfunktion auswerten, dass intern mittels CASE nur die gewünschten Ergebnisse auswertet:

SELECT ID,
MAX(CASE WHEN "Attribute" = 'attr1' THEN "VALUE" END) as "Typ",
MAX(CASE WHEN "Attribute" = 'attr2' THEN "VALUE" END) as "Datum",
MAX(CASE WHEN "Attribute" = 'attr3' THEN "VALUE" END) as "Anzahl",
MAX(CASE WHEN "Attribute" = 'attr4' THEN "VALUE" END) as "Dings",
MAX(CASE WHEN "Attribute" = 'attr5' THEN "VALUE" END) as "Bums"
FROM OpenSchema
GROUP by ID
ORDER BY ID

In dieser Form stellten es Rozenshtein, Abranovich und Birger schon 1992 im "SQL Server Forum 12/1992" vor. Damals gab es nur das CASE-Konstrukt noch nicht, deswegen verwendeten sie verschiedene Tricks, um ein CASE auf mathematische Weise zu erreichen. Der Vorteil bei dieser alten Schreibweise ist, dass man die Gruppierung explizit angeben kann.

Pivotieren mit Pivot

Im SQL-Server-2005 führte Microsoft bekanntlich das PIVOT-Konstrukt ein, um das Ganze zu vereinfachen. Im PIVOT-Statement wird die Gruppierung hingegen implizit durchgeführt: Es wird nach allen Spalten der Tabelle gruppiert, die nicht in den Klammern des PIVOT genannt werden. Unten werden "Value" und "Attribute" genannt, also wird nach allen verbleibenden gruppiert: hier nur "ID".

SELECT ID,
attr1 as Typ,
attr2 as Datum,
attr3 as Anzahl,
attr4 as Dings,
attr5 as Bums
FROM OpenSchema
PIVOT (
MAX("Value")
FOR Attribute IN ("attr1", "attr2", "attr3", "attr4", "attr5")
) AS pvt
ORDER BY ID

Das PIVOT-Konstrukt ist genauso implementiert, wie es bereits oben beschrieben wurde. Das schließe ich daraus, dass in beiden Fällen der gleiche Zugriffsplan verwendet wird:
Zugriffsplan

Natürlich passiert es öfters, dass der Optimizer einen semantisch äquivalenten Zugriffsplan verwendet, wenn er effizienter ist. Das ist hier jedoch unwahrscheinlich.
Die "ältere" Form hilft mir zu verstehen, wann ein PIVOT schnell ausgeführt werden kann und wann er einfach langsam sein muss. Sie hat zudem den Vorteil, dass man pro Spalte entscheiden kann, welche Aggregatfunktion verwendet werden soll. Beim neuen PIVOT wird für alle die gleiche genutzt.

Diese Reihe schrieb ich letzten Oktober als Reaktion auf den Aufruf im SQL-PASS-Newsletter. Möglicherweise erscheint der Artikel noch dort. Ich veröffentliche ihn jetzt aber lieber mal bevor er mit dem bald erscheinenden SQL-Server-2008 veraltet… 😉
24. April 2008 um 10:46

SQL-Server-Event in Wien

Wer sich demnächst mit dem SQL-Server-2008 rumschlagen muss oder ein paar richtig gute Vorträge zu fortgeschrittenen Themen im Bereich SQL-Server besuchen will, dem empfehle ich den "Solid Quality Summit" vom 26. bis 30.5.2008 in Wien. Ich denke schon alleine die "PostConference Class" am Freitag mit Itzik Ben-Gan zum Thema "Advanced T-SQL Querying and Programming" ist die Reise auf jeden Fall wert! Ich durfte diesen Kurs in London belegen und habe damals unglaublich viel gelernt.

Hier ist die Liste der restlichen Vorträge: Course Schedule

PS: Heute werden meine Posting wohl etwas kürzer. Die Handwerker machen einen unglaublichen Radau, der sich über die Heizkörper in jedes Zimmer überträgt… Der arme Hund ist schon ganz fertig.

19. April 2008 um 20:33

Professionelle Übernahme zehntausender Webseiten

Was man so bei Heise-Online liest ist ja ziemlich ernüchternd. Es gibt sogar ein chinesisches Tool mit dem sich einfach eine anfällige Seite finden und übernehmen lässt. Die "Zielgruppe" ist nicht gerade eine Werbung für Microsoft:

Das Windows-Werkzeug mit chinesischer Bedienoberfläche sucht mit der Google-Suchmaschine nach verwundbaren Servern und kann dann eine SQL-Injection-Attacke ausführen, die in den aus der Datenbank generierten Webseiten einen iframe einfügt, der den Code zum Angriff auf die Webseitenbesucher nachlädt.

Der voreingestellte iframe in dem Tool enthält denselben Link, der Anfang des Jahres auf zahlreichen der manipulierten Webseiten auftauchte. Die Angriffe scheinen auf den Microsoft-SQL-Server sowie den Internet Information Server zugeschnitten zu sein. Das Werkzeug kontaktiert den Analysen des ISC zufolge vor der Nutzung auch einen weiteren Server in China, um offenbar einen Bezahlvorgang anzustoßen.

Jetzt kann Microsoft nichts dazu, dass jemand in seiner Webapplikation SQL-Injection ermöglicht, aber das hilft auch nicht weiter. Wenn man den SQL-Server im Systemkontext installiert, das wird bei der Installation als Option angeboten, dann kann der SQL-Server einfach alles – er hat schließlich Admin-Rechte.