Warum ist das schneller und sicher zu bedienen? (WO der erste Buchstabe im Alphabet steht)

10

Kurz gesagt, wir aktualisieren kleine Personentabellen mit Werten aus einer sehr großen Personentabelle. In einem kürzlich durchgeführten Test dauert die Ausführung dieses Updates ca. 5 Minuten.

Wir sind auf die scheinbar albernste Optimierung gestoßen, die scheinbar perfekt funktioniert! Dieselbe Abfrage wird jetzt in weniger als 2 Minuten ausgeführt und führt perfekt zu denselben Ergebnissen.

Hier ist die Abfrage. Die letzte Zeile wird als "Optimierung" hinzugefügt. Warum die intensive Verkürzung der Abfragezeit? Vermissen wir etwas? Könnte dies in Zukunft zu Problemen führen?

UPDATE smallTbl
SET smallTbl.importantValue = largeTbl.importantValue
FROM smallTableOfPeople smallTbl
JOIN largeTableOfPeople largeTbl
    ON largeTbl.birth_date = smallTbl.birthDate
    AND DIFFERENCE(TRIM(smallTbl.last_name),TRIM(largeTbl.last_name)) = 4
    AND DIFFERENCE(TRIM(smallTbl.first_name),TRIM(largeTbl.first_name)) = 4
WHERE smallTbl.importantValue IS NULL
-- The following line is "the optimization"
AND LEFT(TRIM(largeTbl.last_name), 1) IN ('a','à','á','b','c','d','e','è','é','f','g','h','i','j','k','l','m','n','o','ô','ö','p','q','r','s','t','u','ü','v','w','x','y','z','æ','ä','ø','å')

Technische Hinweise: Wir sind uns bewusst, dass die Liste der zu testenden Buchstaben möglicherweise einige weitere Buchstaben benötigt. Wir sind uns auch der offensichtlichen Fehlerquote bei der Verwendung von "DIFFERENCE" bewusst.

Abfrageplan (regulär): https://www.brentozar.com/pastetheplan/?id=rypV84y7V
Abfrageplan (mit "Optimierung"): https://www.brentozar.com/pastetheplan/?id=r1aC2my7E

JohnF
quelle
4
Kleine Antwort auf Ihre technische Anmerkung: AND LEFT(TRIM(largeTbl.last_name), 1) BETWEEN 'a' AND 'z' COLLATE LATIN1_GENERAL_CI_AIsollte tun, was Sie dort wollen, ohne dass Sie alle Zeichen auflisten müssen und Code haben, der schwer zu lesen ist
Erik A
Haben Sie Zeilen, in denen die endgültige Bedingung in WHEREfalsch ist? Beachten Sie insbesondere, dass beim Vergleich zwischen Groß- und Kleinschreibung unterschieden werden kann.
jpmc26
@ErikvonAsmuth macht einen ausgezeichneten Punkt. Aber nur ein kleiner technischer Hinweis: Für SQL Server 2008 und 2008 R2 ist es am besten, die Kollatierungen der Version "100" zu verwenden (falls für die verwendete Kultur / das verwendete Gebietsschema verfügbar). Das wäre also so Latin1_General_100_CI_AI. Für SQL Server 2012 und höher (mindestens bis SQL Server 2019) ist es am besten, die für zusätzliche Zeichen aktivierten Kollatierungen in der höchsten Version für das verwendete Gebietsschema zu verwenden. Das wäre also Latin1_General_100_CI_AI_SCin diesem Fall. Versionen> 100 (bisher nur Japanisch) haben (oder brauchen) nicht _SC(zB Japanese_XJIS_140_CI_AI).
Solomon Rutzky

Antworten:

9

Dies hängt von den Daten in Ihren Tabellen, Ihren Indizes usw. ab. Schwer zu sagen, ohne die Ausführungspläne / die io + -Zeitstatistik vergleichen zu können.

Der Unterschied, den ich erwarten würde, ist die zusätzliche Filterung vor dem JOIN zwischen den beiden Tabellen. In meinem Beispiel habe ich die Aktualisierungen in Auswahl geändert, um meine Tabellen wiederzuverwenden.

Der Ausführungsplan mit "der Optimierung" Geben Sie hier die Bildbeschreibung ein

Ausführungsplan

Sie sehen deutlich, dass ein Filtervorgang stattfindet. In meinen Testdaten wurden keine Datensätze herausgefiltert und daher wurden keine Verbesserungen vorgenommen.

Der Ausführungsplan ohne "die Optimierung" Geben Sie hier die Bildbeschreibung ein

Ausführungsplan

Der Filter ist weg, was bedeutet, dass wir uns auf den Join verlassen müssen, um nicht benötigte Datensätze herauszufiltern.

Andere Gründe Ein weiterer Grund / eine weitere Konsequenz für die Änderung der Abfrage könnte sein, dass beim Ändern der Abfrage ein neuer Ausführungsplan erstellt wurde, der zufällig schneller ist. Ein Beispiel hierfür ist die Engine, die einen anderen Join-Operator auswählt, aber das ist an dieser Stelle nur eine Vermutung.

BEARBEITEN:

Klarstellung nach Erhalt der beiden Abfragepläne:

Die Abfrage liest 550 Millionen Zeilen aus der großen Tabelle und filtert sie heraus. Geben Sie hier die Bildbeschreibung ein

Dies bedeutet, dass das Prädikat das meiste der Filterung ausführt, nicht das Suchprädikat. Dies führt dazu, dass die Daten gelesen, aber viel weniger zurückgegeben werden.

Wenn Sie SQL Server dazu bringen, einen anderen Index (Abfrageplan) zu verwenden, oder einen Index hinzufügen, kann dies behoben werden.

Warum hat die Optimierungsabfrage nicht dasselbe Problem?

Da ein anderer Abfrageplan verwendet wird, mit einem Scan anstelle einer Suche.

Geben Sie hier die Bildbeschreibung ein Geben Sie hier die Bildbeschreibung ein

Ohne irgendwelche Suchvorgänge durchzuführen, sondern nur 4 Millionen Zeilen zurückzugeben, um damit zu arbeiten.

Nächster Unterschied

Ohne Berücksichtigung des Aktualisierungsunterschieds (bei der optimierten Abfrage wird nichts aktualisiert) wird für die optimierte Abfrage eine Hash-Übereinstimmung verwendet:

Geben Sie hier die Bildbeschreibung ein

Anstelle eines verschachtelten Loop-Joins auf dem nicht optimierten:

Geben Sie hier die Bildbeschreibung ein

Eine verschachtelte Schleife ist am besten, wenn eine Tabelle klein und die andere groß ist. Da beide nahe an der gleichen Größe liegen, würde ich argumentieren, dass das Hash-Match in diesem Fall die bessere Wahl ist.

Überblick

Die optimierte Abfrage Geben Sie hier die Bildbeschreibung ein

Der Plan der optimierten Abfrage weist Parallellismus auf, verwendet einen Hash-Match-Join und muss weniger Rest-E / A-Filterung durchführen. Außerdem wird eine Bitmap verwendet, um Schlüsselwerte zu entfernen, die keine Verknüpfungszeilen erzeugen können. (Auch wird nichts aktualisiert)

Die nicht optimierte Abfrage Geben Sie hier die Bildbeschreibung ein Der Plan der nicht optimierten Abfrage weist keine Parallellismus auf, verwendet eine verschachtelte Schleifenverknüpfung und muss eine Rest-E / A-Filterung für 550 Millionen Datensätze durchführen. (Auch das Update findet statt)

Was können Sie tun, um die nicht optimierte Abfrage zu verbessern?

  • Ändern des Index in Vorname und Nachname in der Liste der Schlüsselspalten:

    CREATE INDEX IX_largeTableOfPeople_birth_date_first_name_last_name auf dbo.largeTableOfPeople (Geburtsdatum, Vorname, Nachname) include (id)

Aufgrund der Verwendung von Funktionen und der Größe dieser Tabelle ist dies möglicherweise nicht die optimale Lösung.

  • Aktualisieren der Statistiken mithilfe der Neukompilierung, um den besseren Plan zu erhalten.
  • Hinzufügen von OPTION (HASH JOIN, MERGE JOIN)zur Abfrage
  • ...

Testdaten + Abfragen verwendet

CREATE TABLE #smallTableOfPeople(importantValue int, birthDate datetime2, first_name varchar(50),last_name varchar(50));
CREATE TABLE #largeTableOfPeople(importantValue int, birth_date datetime2, first_name varchar(50),last_name varchar(50));


set nocount on;
DECLARE @i int = 1
WHILE @i <= 1000
BEGIN
insert into #smallTableOfPeople (importantValue,birthDate,first_name,last_name)
VALUES(NULL, dateadd(mi,@i,'2018-01-18 11:05:29.067'),'Frodo','Baggins');

set @i += 1;
END


set nocount on;
DECLARE @j int = 1
WHILE @j <= 20000
BEGIN
insert into #largeTableOfPeople (importantValue,birth_Date,first_name,last_name)
VALUES(@j, dateadd(mi,@j,'2018-01-18 11:05:29.067'),'Frodo','Baggins');

set @j += 1;
END


SET STATISTICS IO, TIME ON;

SELECT  smallTbl.importantValue , largeTbl.importantValue
FROM #smallTableOfPeople smallTbl
JOIN #largeTableOfPeople largeTbl
    ON largeTbl.birth_date = smallTbl.birthDate
    AND DIFFERENCE(RTRIM(LTRIM(smallTbl.last_name)),RTRIM(LTRIM(largeTbl.last_name))) = 4
    AND DIFFERENCE(RTRIM(LTRIM(smallTbl.first_name)),RTRIM(LTRIM(largeTbl.first_name))) = 4
WHERE smallTbl.importantValue IS NULL
-- The following line is "the optimization"
AND LEFT(RTRIM(LTRIM(largeTbl.last_name)), 1) IN ('a','à','á','b','c','d','e','è','é','f','g','h','i','j','k','l','m','n','o','ô','ö','p','q','r','s','t','u','ü','v','w','x','y','z','æ','ä','ø','å');

SELECT  smallTbl.importantValue , largeTbl.importantValue
FROM #smallTableOfPeople smallTbl
JOIN #largeTableOfPeople largeTbl
    ON largeTbl.birth_date = smallTbl.birthDate
    AND DIFFERENCE(RTRIM(LTRIM(smallTbl.last_name)),RTRIM(LTRIM(largeTbl.last_name))) = 4
    AND DIFFERENCE(RTRIM(LTRIM(smallTbl.first_name)),RTRIM(LTRIM(largeTbl.first_name))) = 4
WHERE smallTbl.importantValue IS NULL
-- The following line is "the optimization"
--AND LEFT(RTRIM(LTRIM(largeTbl.last_name)), 1) IN ('a','à','á','b','c','d','e','è','é','f','g','h','i','j','k','l','m','n','o','ô','ö','p','q','r','s','t','u','ü','v','w','x','y','z','æ','ä','ø','å')




drop table #largeTableOfPeople;
drop table #smallTableOfPeople;
Randi Vertongen
quelle
8

Es ist nicht klar, dass die zweite Abfrage tatsächlich eine Verbesserung darstellt.

Die Ausführungspläne enthalten QueryTimeStats, die einen viel weniger dramatischen Unterschied aufweisen als in der Frage angegeben.

Der langsame Plan hatte eine verstrichene Zeit von 257,556 ms(4 Minuten 17 Sekunden). Der schnelle Plan hatte eine verstrichene Zeit von 190,992 ms(3 Minuten, 11 Sekunden), obwohl er mit einem Parallelitätsgrad von 3 lief.

Darüber hinaus wurde der zweite Plan in einer Datenbank ausgeführt, in der nach dem Join keine Arbeit mehr zu erledigen war.

Erster Plan

Geben Sie hier die Bildbeschreibung ein

Zweiter Plan

Geben Sie hier die Bildbeschreibung ein

Diese zusätzliche Zeit könnte durchaus durch die Arbeit erklärt werden, die zum Aktualisieren von 3,5 Millionen Zeilen erforderlich ist (die Arbeit, die der Aktualisierungsoperator benötigt, um diese Zeilen zu lokalisieren, die Seite zu verriegeln, die Aktualisierung auf die Seite zu schreiben und das Transaktionsprotokoll ist nicht vernachlässigbar).

Wenn dies beim Vergleich von Gleichem mit Gleichem tatsächlich reproduzierbar ist, ist die Erklärung, dass Sie in diesem Fall gerade Glück gehabt haben.

Der Filter mit den 37 INBedingungen eliminierte nur 51 Zeilen von den 4.008.334 in der Tabelle, aber der Optimierer ging davon aus, dass er viel mehr eliminieren würde

Geben Sie hier die Bildbeschreibung ein

   LEFT(TRIM(largeTbl.last_name), 1) IN ( 'a', 'à', 'á', 'b',
                                          'c', 'd', 'e', 'è',
                                          'é', 'f', 'g', 'h',
                                          'i', 'j', 'k', 'l',
                                          'm', 'n', 'o', 'ô',
                                          'ö', 'p', 'q', 'r',
                                          's', 't', 'u', 'ü',
                                          'v', 'w', 'x', 'y',
                                          'z', 'æ', 'ä', 'ø', 'å' ) 

Solche falschen Kardinalitätsschätzungen sind normalerweise eine schlechte Sache. In diesem Fall wurde ein anders geformter (und paralleler) Plan erstellt, der anscheinend (?) Besser für Sie funktionierte, trotz der durch die massive Unterschätzung verursachten Hash-Verschüttungen.

Ohne TRIMSQL Server ist es möglich, dies in ein Bereichsintervall im Basisspaltenhistogramm zu konvertieren und viel genauere Schätzungen zu liefern, aber mit dem TRIMwird nur auf Vermutungen zurückgegriffen.

Die Art der Vermutung kann variieren, aber die Schätzung für ein einzelnes Prädikat LEFT(TRIM(largeTbl.last_name), 1)wird unter bestimmten Umständen * nur geschätzt table_cardinality/estimated_number_of_distinct_column_values.

Ich bin mir nicht sicher, unter welchen Umständen - die Datengröße scheint eine Rolle zu spielen. Ich konnte dies wie hier mit Datentypen mit großer fester Länge reproduzieren , bekam aber eine andere, höhere varcharSchätzung mit (die nur eine flache Schätzung von 10% und geschätzte 100.000 Zeilen verwendete). @Solomon Rutzky weist darauf hin, dass, wenn das varchar(100)mit Leerzeichen aufgefüllt ist, wie es für chardie untere Schätzung geschieht, verwendet wird

Die INListe wird erweitert ORund SQL Server verwendet exponentielles Backoff mit maximal 4 berücksichtigten Prädikaten. Die 219.707Schätzung ergibt sich also wie folgt.

DECLARE @TableCardinality FLOAT = 4008334, 
        @DistinctColumnValueEstimate FLOAT = 34207

DECLARE @NotSelectivity float = 1 - (1/@DistinctColumnValueEstimate)

SELECT @TableCardinality * ( 1 - (
@NotSelectivity * 
SQRT(@NotSelectivity) * 
SQRT(SQRT(@NotSelectivity)) * 
SQRT(SQRT(SQRT(@NotSelectivity)))
))
Martin Smith
quelle