Unterabfrage mit schlechter Leistung und Datumsvergleichen

15

Wenn Sie eine Unterabfrage verwenden, um die Gesamtanzahl aller vorherigen Datensätze mit einem übereinstimmenden Feld zu ermitteln, ist die Leistung in einer Tabelle mit nur 50.000 Datensätzen schlecht. Ohne die Unterabfrage wird die Abfrage in wenigen Millisekunden ausgeführt. Bei der Unterabfrage beträgt die Ausführungszeit mehr als eine Minute.

Für diese Abfrage muss das Ergebnis:

  • Schließen Sie nur diese Datensätze innerhalb eines bestimmten Datumsbereichs ein.
  • Fügen Sie eine Anzahl aller vorherigen Datensätze hinzu, ohne den aktuellen Datensatz, unabhängig vom Datumsbereich.

Grundlegendes Tabellenschema

Activity
======================
Id int Identifier
Address varchar(25)
ActionDate datetime2
Process varchar(50)
-- 7 other columns

Beispieldaten

Id  Address     ActionDate (Time part excluded for simplicity)
===========================
99  000         2017-05-30
98  111         2017-05-30
97  000         2017-05-29
96  000         2017-05-28
95  111         2017-05-19
94  222         2017-05-30

erwartete Ergebnisse

Für den Datumsbereich von 2017-05-29bis2017-05-30

Id  Address     ActionDate    PriorCount
=========================================
99  000         2017-05-30    2  (3 total, 2 prior to ActionDate)
98  111         2017-05-30    1  (2 total, 1 prior to ActionDate)
94  222         2017-05-30    0  (1 total, 0 prior to ActionDate)
97  000         2017-05-29    1  (3 total, 1 prior to ActionDate)

Die Datensätze 96 und 95 werden vom Ergebnis ausgeschlossen, sind jedoch in der PriorCountUnterabfrage enthalten

Aktuelle Abfrage

select 
    *.a
    , ( select count(*) 
        from Activity
        where 
            Activity.Address = a.Address
            and Activity.ActionDate < a.ActionDate
    ) as PriorCount
from Activity a
where a.ActionDate between '2017-05-29' and '2017-05-30'
order by a.ActionDate desc

Aktueller Index

CREATE NONCLUSTERED INDEX [IDX_my_nme] ON [dbo].[Activity]
(
    [ActionDate] ASC
)
INCLUDE ([Address]) WITH (
    PAD_INDEX = OFF, 
    STATISTICS_NORECOMPUTE = OFF, 
    SORT_IN_TEMPDB = OFF, 
    DROP_EXISTING = OFF, 
    ONLINE = OFF, 
    ALLOW_ROW_LOCKS = ON, 
    ALLOW_PAGE_LOCKS = ON
)

Frage

  • Welche Strategien könnten verwendet werden, um die Leistung dieser Abfrage zu verbessern?

Bearbeiten 1
Als Antwort auf die Frage, was ich an der DB ändern kann: Ich kann die Indizes ändern, nur nicht die Tabellenstruktur.

Bearbeiten 2
Ich habe jetzt einen Basisindex für die AddressSpalte hinzugefügt , aber das schien sich nicht wesentlich zu verbessern. Momentan finde ich eine viel bessere Leistung, wenn ich eine temporäre Tabelle erstelle und die Werte ohne die einfüge PriorCountund dann jede Zeile mit ihrer spezifischen Anzahl aktualisiere.

Edit 3
Die gefundene Index-Spool Joe Obbish (akzeptierte Antwort) war das Problem. Sobald ich eine neue hinzufügte nonclustered index [xyz] on [Activity] (Address) include (ActionDate), sanken die Abfragezeiten von einer Minute auf weniger als eine Sekunde, ohne eine temporäre Tabelle zu verwenden (siehe Bearbeiten 2).

Metro Schlumpf
quelle

Antworten:

17

Mit der Indexdefinition, über die Sie verfügen IDX_my_nme, kann SQL Server anhand der ActionDateSpalte suchen, jedoch nicht anhand der AddressSpalte. Der Index enthält alle Spalten, die zur Abdeckung der Unterabfrage benötigt werden, ist jedoch für diese Unterabfrage wahrscheinlich nicht sehr selektiv. Angenommen, fast alle Daten in der Tabelle haben einen ActionDateWert vor '2017-05-30'. Eine Suche nach ActionDate < '2017-05-30'gibt fast alle Zeilen aus dem Index zurück, die weiter gefiltert werden, nachdem die Zeile aus dem Index abgerufen wurde. Wenn Ihre Abfrage 200 Zeilen zurückgibt, würden Sie wahrscheinlich fast 200 vollständige Index-Scans durchführen IDX_my_nme, was bedeutet, dass Sie ungefähr 50000 * 200 = 10 Millionen Zeilen aus dem Index lesen.

Es ist wahrscheinlich, dass die Suche nach Addressfür Ihre Unterabfrage weitaus selektiver ist, obwohl Sie uns keine vollständigen statistischen Informationen zu der Abfrage gegeben haben. Dies ist also eine Annahme von meiner Seite. Angenommen, Sie erstellen einen Index für just Addressund Ihre Tabelle enthält 10.000 eindeutige Werte für Address. Mit dem neuen Index muss SQL Server für jede Ausführung der Unterabfrage nur 5 Zeilen aus dem Index suchen, sodass Sie ungefähr 200 * 5 = 1000 Zeilen aus dem Index lesen.

Ich teste gegen SQL Server 2016, daher kann es zu geringfügigen Syntaxunterschieden kommen. Im Folgenden sind einige Beispieldaten aufgeführt, bei denen ich für die Datenverteilung ähnliche Annahmen getroffen habe wie oben:

CREATE TABLE #Activity (
    Id int NOT NULL,
    [Address] varchar(25) NULL,
    ActionDate datetime2 NULL,
    FILLER varchar(100),
    PRIMARY KEY (Id)
);

INSERT INTO #Activity WITH (TABLOCK)
SELECT TOP (50000) -- 50k total rows
x.RN
, x.RN % 10000 -- 10k unique addresses
, DATEADD(DAY, x.RN / 100, '20160201') -- 100 rows per day
, REPLICATE('Z', 100)
FROM
(
    SELECT ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) RN
    FROM master..spt_values t1
    CROSS JOIN master..spt_values t2
) x;

CREATE NONCLUSTERED INDEX [IDX_my_nme] ON #Activity
([ActionDate] ASC) INCLUDE ([Address]);

Ich habe Ihren Index wie in der Frage beschrieben erstellt. Ich teste gegen diese Abfrage, die die gleichen Daten wie die in der Frage zurückgibt:

select 
    a.*
    , ( select count(*) 
        from #Activity Activity
        where 
            Activity.[Address] = a.[Address]
            and Activity.ActionDate < a.ActionDate
    ) as PriorCount
from #Activity a
where a.ActionDate between '2017-05-29' and '2017-05-30'
order by a.ActionDate desc;

Ich bekomme eine Indexspule. Grundsätzlich bedeutet dies, dass das Abfrageoptimierungsprogramm sofort einen temporären Index erstellt, da keiner der vorhandenen Indizes für die Tabelle geeignet war.

Index-Spool

Die Abfrage ist für mich noch schnell erledigt. Möglicherweise wird die Index-Spool-Optimierung auf Ihrem System nicht ausgeführt, oder die Tabellendefinition oder die Abfrage haben etwas anderes. Zu Unterrichtszwecken kann ich eine undokumentierte Funktion verwenden OPTION (QUERYRULEOFF BuildSpool), um die Index-Spool zu deaktivieren. So sieht der Plan aus:

schlechte Indexsuche

Lassen Sie sich nicht vom Auftreten einer einfachen Indexsuche täuschen. SQL Server liest fast 10 Millionen Zeilen aus dem Index:

10 Millionen Zeilen vom Index

Wenn ich die Abfrage mehrmals ausführen möchte, ist es wahrscheinlich nicht sinnvoll, dass das Abfrageoptimierungsprogramm bei jeder Ausführung einen Index erstellt. Ich könnte einen Index im Voraus erstellen, der für diese Abfrage selektiver ist:

CREATE NONCLUSTERED INDEX [IDX_my_nme_2] ON #Activity
([Address] ASC) INCLUDE (ActionDate);

Der Plan ist ähnlich wie zuvor:

Indexsuche

Mit dem neuen Index liest SQL Server jedoch nur 1000 Zeilen aus dem Index. 800 der Zeilen werden zurückgegeben, um gezählt zu werden. Der Index könnte selektiver definiert werden, dies kann jedoch in Abhängigkeit von Ihrer Datenverteilung ausreichend sein.

gute suche

Wenn Sie keine zusätzlichen Indizes für die Tabelle definieren können, würde ich die Verwendung von Fensterfunktionen in Betracht ziehen. Folgendes scheint zu funktionieren:

SELECT t.*
FROM
(
    select 
        a.*
        , -1 + ROW_NUMBER() OVER (PARTITION BY [Address] ORDER BY ActionDate) PriorCount
    from #Activity a
) t
where t.ActionDate between '2017-05-29' and '2017-05-30'
order by t.ActionDate desc;

Diese Abfrage führt einen einzelnen Scan der Daten durch, führt jedoch eine kostspielige Sortierung durch und berechnet die ROW_NUMBER()Funktion für jede Zeile in der Tabelle. Es scheint also , dass hier zusätzliche Arbeit geleistet wird:

schlechte Sorte

Wenn Sie dieses Codemuster jedoch wirklich mögen, können Sie einen Index definieren, um es effizienter zu gestalten:

CREATE NONCLUSTERED INDEX [IDX_my_nme] ON #Activity
([Address], [ActionDate]) INCLUDE (FILLER);

Das bewegt die Sorte gegen Ende, was viel billiger sein wird:

gute Sorte

Wenn dies nicht hilft, müssen Sie der Frage weitere Informationen hinzufügen, vorzugsweise einschließlich der tatsächlichen Ausführungspläne.

Joe Obbish
quelle
1
Der Index-Spool, den Sie gefunden haben, war das Problem. Sobald ich eine neue hinzufügte nonclustered index [xyz] on [Activity] (Address) include (ActionDate), sanken die Abfragezeiten von einer Minute auf weniger als eine Sekunde. +10 wenn ich könnte. Vielen Dank!
Metro Schlumpf