NEWID () in verbundenen virtuellen Tabellen verursacht unbeabsichtigtes Cross-Apply-Verhalten

9

Meine eigentliche Arbeitsabfrage war ein innerer Join, aber dieses einfache Beispiel mit Cross-Join scheint das Problem fast immer zu reproduzieren.

SELECT *
FROM (
    SELECT 1 UNION ALL
    SELECT 2
) AA ( A )
CROSS JOIN (
    SELECT NEWID() TEST_ID
) BB ( B )

Bei meinem inneren Join hatte ich viele Zeilen, für die ich mit der Funktion NEWID () jeweils eine GUID hinzufügte, und für etwa 9 von 10 solcher Zeilen ergab die Multiplikation mit der virtuellen 2-Zeilen-Tabelle die erwarteten Ergebnisse, nur 2 Kopien davon die gleiche GUID, während 1 von 10 unterschiedliche Ergebnisse liefern würde. Dies war gelinde gesagt unerwartet und machte es mir wirklich schwer, diesen Fehler in meinem Skript zur Generierung von Testdaten zu finden.

Wenn Sie sich die folgenden Abfragen mit nicht deterministischen Funktionen getdate und sysdatetime ansehen, werden Sie dies nicht sehen, ich jedenfalls nicht - ich sehe immer den gleichen datetime-Wert in beiden Endergebniszeilen.

SELECT *
FROM (
    SELECT 1 UNION ALL
    SELECT 2
) AA ( A )
CROSS JOIN (
    SELECT GETDATE() TEST_ID
) BB ( B )

SELECT *
FROM (
    SELECT 1 UNION ALL
    SELECT 2
) AA ( A )
CROSS JOIN (
    SELECT SYSDATETIME() TEST_ID
) BB ( B )

Ich verwende derzeit SQL Server 2008 und arbeite derzeit daran, meine Zeilen mit GUIDs in eine Tabellenvariable zu laden, bevor ich mein Skript zur Generierung zufälliger Daten fertigstelle. Sobald ich sie als Werte in einer Tabelle im Gegensatz zur virtuellen Tabelle habe, verschwindet das Problem.

Ich habe eine Problemumgehung, suche jedoch nach Möglichkeiten zur Problemumgehung ohne tatsächliche Tabellen oder Tabellenvariablen.

Während ich dies schrieb, versuchte ich erfolglos diese Möglichkeiten: 1) Platzieren der newid () in einer verschachtelten virtuellen Tabelle:

SELECT *
FROM (
    SELECT 1 UNION ALL
    SELECT 2
) AA ( A )
CROSS JOIN (
    SELECT TEST_ID
    FROM (
        SELECT NEWID() TEST_ID
    ) TT
) BB ( B )

2) Umschließen der newid () in einen Besetzungsausdruck wie:

SELECT CAST(NEWID() AS VARCHAR(100)) TEST_ID

3) Umkehren der Reihenfolge des Auftretens der virtuellen Tabellen innerhalb des Join-Ausdrucks

SELECT *
FROM (
    SELECT NEWID() TEST_ID
) BB ( B )
CROSS JOIN (
    SELECT 1 UNION ALL
    SELECT 2
) AA ( A )

4) mit unkorreliertem Kreuz anwenden

SELECT *
FROM (
    SELECT NEWID() TEST_ID
) BB ( B )
CROSS APPLY (
    SELECT 1 UNION ALL
    SELECT 2
) AA ( A )

Kurz bevor ich diese Frage endgültig postete, habe ich dies anscheinend mit Erfolg versucht, es scheint ein korreliertes Kreuz zu gelten:

SELECT *
FROM (
    SELECT NEWID() TEST_ID
) BB ( B )
CROSS APPLY (
    SELECT A
    FROM (
        SELECT 1 UNION ALL
        SELECT 2
    ) TT ( A )
    WHERE BB.B IS NOT NULL
) AA ( A )

Hat jemand eine andere elegantere, einfachere Problemumgehung? Ich möchte wirklich keine Kreuzanwendung oder Korrelation für eine einfache Zeilenmultiplikation verwenden, wenn ich nicht muss.

JM Hicks
quelle

Antworten:

20

Dieses Verhalten ist beabsichtigt, wie in diesem Connect-Fehlerbericht ausführlich erläutert . Die relevanteste Microsoft-Antwort wird unten zur Vereinfachung wiedergegeben (und falls der Link irgendwann nicht mehr funktioniert):

Gepostet von Microsoft am 07.07.2008 um 09:27 Uhr

Den Kreis schließen . . . Ich habe diese Frage mit dem Entwicklerteam besprochen. Und schließlich haben wir uns aus folgenden Gründen entschieden, das aktuelle Verhalten nicht zu ändern:

  1. Der Optimierer garantiert nicht das Timing oder die Anzahl der Ausführungen von Skalarfunktionen. Dies ist ein seit langem etablierter Grundsatz. Dies ist der grundlegende „Spielraum“, der dem Optimierer genügend Freiheit lässt, um signifikante Verbesserungen bei der Ausführung von Abfrageplänen zu erzielen.

  2. Dieses "Verhalten einmal pro Zeile" ist kein neues Problem, obwohl es nicht allgemein diskutiert wird. Wir haben bereits in der Yukon-Version damit begonnen, sein Verhalten zu optimieren. Aber es ist ziemlich schwierig, in jedem Fall genau zu bestimmen, was es bedeutet! Gilt dies beispielsweise für Zwischenzeilen, die auf dem Weg zum Endergebnis berechnet wurden? - In diesem Fall hängt es eindeutig vom gewählten Plan ab. Oder gilt es nur für die Zeilen, die schließlich im fertigen Ergebnis erscheinen? - Hier findet eine böse Rekursion statt, da werden Sie sicher zustimmen!

  3. Wie bereits erwähnt, optimieren wir standardmäßig die Leistung - was in 99% der Fälle gut ist. Die 1% der Fälle, in denen sich die Ergebnisse ändern könnten, sind ziemlich leicht zu erkennen - nebeneffektive 'Funktionen' wie NEWID - und leicht 'zu beheben' (Handelsleistung als Folge). Diese Standardeinstellung zum erneuten "Optimieren der Leistung" ist seit langem etabliert und wird akzeptiert. (Ja, es ist nicht die Haltung, die Compiler für herkömmliche Programmiersprachen gewählt haben, aber so sei es).

Unsere Empfehlungen lauten also:

  1. Vermeiden Sie es, sich auf nicht garantierte Zeit- und Ausführungssemantik zu verlassen.
  2. Vermeiden Sie die Verwendung von NEWID () tief in Tabellenausdrücken.
  3. Verwenden Sie OPTION, um ein bestimmtes Verhalten zu erzwingen (Trading Perf)

Hoffe, diese Erklärung hilft, unsere Gründe für das Schließen dieses Fehlers zu klären, da "nicht behoben werden kann".

Die Funktionen GETDATEund SYSDATETIMEsind zwar nicht deterministisch, werden jedoch als Laufzeitkonstanten für eine bestimmte Abfrage behandelt. Im Allgemeinen bedeutet dies, dass der Wert der Funktion beim Start der Abfrageausführung zwischengespeichert wird und das Ergebnis für alle Referenzen innerhalb der Abfrage wiederverwendet wird.

Keine der Problemumgehungen in der Frage ist sicher. Es gibt keine Garantie dafür, dass sich das Verhalten beim nächsten Kompilieren des Plans nicht ändert, wenn Sie das nächste Mal ein Service Pack oder ein kumulatives Update anwenden ... oder aus anderen Gründen.

Die einzig sichere Lösung besteht darin, ein temporäres Objekt zu verwenden - beispielsweise eine Variable, eine Tabelle oder eine Funktion mit mehreren Anweisungen. Die Verwendung einer Problemumgehung, die heute auf der Grundlage von Beobachtungen zu funktionieren scheint, ist eine hervorragende Möglichkeit, um in Zukunft unerwartete Verhaltensweisen zu erleben, normalerweise in Form eines Paging-Alarms um 3 Uhr morgens am Sonntagmorgen.

Paul White 9
quelle
"Keine der 'Problemumgehungen' in der Frage ist sicher." das Gleiche gilt. Als ich versuchte, eine davon auf meine eigentliche Arbeitsabfrage anzuwenden, half das überhaupt nicht.
JM Hicks