SQL Server: Abfrageleistung (Suche in 2 Millionen Zeilen)


7

Ich habe ein interessantes für alle SQL-Guru da draußen. Jetzt dauert diese Suche nur ein paar Sekunden, ist aber ziemlich intensiv und es muss einen besseren Weg geben. Vielleicht erwarte ich zu viel?

Einfache Urlaubssuch-App. 2 Millionen Ferien. Paging / Sortieren von ca. 600.000 Zeilen.

Dies ist das Schema der Tabelle

CREATE TABLE [dbo].[Holiday](
        [Id] [int] NOT NULL,
        [PropertyId] [int] NOT NULL,
        [Price] [int] NOT NULL,
        [Rating] [int] NOT NULL,
        [Country] [char](2) NOT NULL,
        [ResortId] [int] NOT NULL,
        [DepartureAirport] [char](3) NOT NULL,
        [DestinationAirport] [char](3) NOT NULL,
        [DepartureDate] [datetime] NOT NULL,
        [Basis] [char](2) NOT NULL,
        [Duration] [int] NOT NULL,

     CONSTRAINT [PK_Holiday] PRIMARY KEY CLUSTERED ([Id] ASC)
  )

Wie Sie sehen können, ganz einfach. Wir haben eine Immobilie, einen Preis, eine Dauer, Abflug- / Zielflughäfen usw. Je mehr Felder zur Verfügung gestellt werden, desto schneller ist die Suche. Wenn ich einen Abflughafen, eine Immobilie und ein Datum habe, ist die Suche sehr schnell. Wenn ich jedoch nur ein Land und nichts anderes habe, müssen viele Daten verarbeitet werden.

Mit diesem CSV-Export meiner Tabelle gibt es insgesamt 2 Millionen Zeilen und ungefähr 666.000, nur mit dem Ländercode FR, was mein Beispiel ist.

Dies ist die Suchabfrage. Welches gibt zwei Tabellen zurück. Die erste ist eine Zusammenfassung, also die Gesamtzahl der Feiertage, die Ihren Kriterien entsprechen, und wie viele einzigartige Eigenschaften. Die zweite Tabelle enthält die tatsächlichen Ergebnisse der Suche.

--Build a temp table, and store everything we need in it
CREATE TABLE #Pricing (PropertyId int, Duration int, HolidayId int, Rating int, Price int, StartDate datetime, PropertyRow int);

INSERT INTO #Pricing
  SELECT 
    PropertyId, Duration, [Id], [Rating], [Price], DepartureDate,
    ROW_NUMBER() OVER (PARTITION BY PropertyId ORDER BY Price ASC) as PropertyRow
  FROM 
    dbo.Holiday
  WHERE 
    DepartureDate > GETDATE() AND Country = 'FR'

--Get a total number of holidays, and total number of properties
SELECT 
    COUNT(*) AS TotalHolidaysCount, 
    COUNT(DISTINCT PropertyId) AS PropertyCount
FROM 
    #Pricing

--Build the final table, which will contain all the holidays we actually want to return
DECLARE @FinalResults TABLE (HolidayId int, RowNumber int);

INSERT INTO 
    @FinalResults
  SELECT 
    HolidayId, RowNumber
  FROM
    (SELECT 
         PropertyRow, HolidayId, 
         ROW_NUMBER() OVER (order by (CASE WHEN StartDate <= '01/Apr/2013' THEN 1 ELSE 0 END) ASC, [Price] ASC) as RowNumber
     FROM  
        #Pricing 
     WHERE 
        PropertyRow = 1) as SearchResults
WHERE 
    (RowNumber > (10 * (1 - 1)) and RowNumber <= (1 * 10))
ORDER BY 
    RowNumber;

SELECT
     *
FROM
    @FinalResults
    INNER JOIN dbo.Holiday ON HolidayId = Holiday.Id

DROP TABLE #Pricing

Jetzt könnte ich mich mit der Indizierung befassen, die offensichtlich die Leistung verbessern würde. Was mich jedoch beunruhigt, ist die unglaubliche Verwendung von temporären Tischen. Sicherlich sollte das nicht so sein, wie es gemacht wird? Das Durchsuchen der letztendlich winzigen Datenmenge dauert 5 Sekunden. Sie werden nur deshalb verwendet, weil später auf die Daten verwiesen werden muss.

Wäre es vielleicht sinnvoll, die Abfrage zweimal auszuführen, anstatt alle Daten im Speicher zu speichern? Es scheint eine Verschwendung zu sein, immer wieder über 25% der Tabelle im Speicher auszuwählen.

Jedes hilfreiche Feedback wäre dankbar. Ich suche nicht nach der 'Antwort', sondern nur nach Hilfe.

Vielen Dank, Dean


4
Sicherlich wäre dieser Code aus der letzten where-Klausel: (RowNumber > (10 * (1 - 1)) and RowNumber <= (1 * 10))viel besser geschrieben alsRowNumber Between 1 and 10

Antworten:


2

Es ist sehr schwierig, eine Lösung ohne die Möglichkeit, Tests auszuführen oder zu sehen, wie die Datenbank indiziert ist usw., genau vorzuschlagen. Aber ich werde es trotzdem versuchen.

Idealerweise müssen Sie ein Gleichgewicht finden. Wenn Ihre Abfrage wahrscheinlich viele Daten zurückgibt und schnell ausgeführt wird, würde ich die Abfrage in der Haupttabelle zweimal ausführen, wenn es wahrscheinlich lange dauert und eine relativ kleine zurückgibt Anzahl der Zeilen würde ich dann beim temporären Tabellenansatz bleiben.

Angesichts der Informationen, die Sie in der Frage angegeben haben, scheint es kein Problem mit der Geschwindigkeit der Auswahl zu geben. In diesem Fall stimme ich Ihnen eher zu, die zusätzlichen Kosten für das Einfügen in eine temporäre Tabelle und das anschließende Auswählen aus der temporären Tabelle Tabelle ist so viel Aufwand wie zweimaliges Ausführen der Auswahlabfrage. Die Abfragen können wie folgt vereinfacht werden:

SELECT  COUNT(*) [TotalHolidaysCount],
        COUNT(DISTINCT PropertyID) [PropertyCount]
FROM    dbo.Holiday
WHERE   DepartureDate > GETDATE() 
AND     Country = 'FR'


SELECT  *
FROM    (   SELECT  *, ROW_NUMBER() OVER (ORDER BY(CASE WHEN StartDate <= '01/Apr/2013' THEN 1 ELSE 0 END) ASC, [Price] ASC) [RowNumber]
            FROM    (   SELECT  h.*, ROW_NUMBER(PARTITION BY PropertyID, ORDER BY Price ASC) [PropertyRow]
                        FROM    dbo.Holiday
                        WHERE   DepartureDate > GETDATE() 
                        AND     Country = 'FR'
                    ) h
            WHERE   PropertyRow = 1
        ) h
WHERE   Rownumber BETWEEN 1 AND 10

Wenn Sie feststellen, dass dies ein Leistungseinbruch ist und Sie die Ergebnisse der Auswahl lieber zwischenspeichern möchten, würde ich eher Tabellenvariablen anstelle von temporären Tabellen verwenden und nur den Primärschlüssel für die Urlaubs-ID speichern und schnell wieder zum Urlaub zurückkehren indizierter Join von Primärschlüssel = Primärschlüssel wie folgt:

DECLARE @Results TABLE (ID INT NOT NULL PRIMARY KEY)
INSERT @Results
SELECT  ID
FROM    dbo.Holiday
WHERE   DepartureDate > GETDATE() 
AND     Country = 'FR'

SELECT  COUNT(*) [TotalHolidaysCount],
        COUNT(DISTINCT PropertyID) [PropertyCount]
FROM    dbo.Holiday h
        INNER JOIN @Results r
            ON r.ID = h.ID

SELECT  *
FROM    (   SELECT  *, ROW_NUMBER() OVER (ORDER BY(CASE WHEN StartDate <= '01/Apr/2013' THEN 1 ELSE 0 END) ASC, [Price] ASC) [RowNumber]
            FROM    (   SELECT  h.*, ROW_NUMBER(PARTITION BY PropertyID, ORDER BY Price ASC) [PropertyRow]
                        FROM    dbo.Holiday h
                                INNER JOIN @Results r
                                    ON r.ID = h.ID
                    ) h
            WHERE   PropertyRow = 1
        ) h
WHERE   Rownumber BETWEEN 1 AND 10

Auf diese Weise werden so wenig Daten wie möglich zwischengespeichert (eine Spalte mit Ganzzahlen), während immer noch genügend Daten für eine schnelle indizierte Suche auf dbo.Holiday gespeichert sind.

Letztendlich muss es darum gehen, Ihre Ausführungspläne zu überprüfen, geeignete Indizes zu erstellen und verschiedene Ansätze zu testen, um den für Sie am besten geeigneten zu finden.


Stimmen Sie den "Plänen, Indizes, Tests" zu, um den besten Ansatz zu finden.
Pheedbaq

2

Ohne Indizes müssen jedes Mal, wenn die Suchabfrage ausgeführt wird, alle 2 Millionen Datensätze durchsucht werden, um die 650K-FR-Instanzen zu finden. Mit Indizes kann die Datenbank im Wesentlichen direkt zu ihnen gehen. Selbst wenn Sie die Suchabfrage unverändert lassen, können Sie mit geeigneten Indizes die Geschwindigkeit verbessern.

Was die temporäre Tabelle betrifft, verstehe ich wirklich nicht, warum dies in der endgültigen Ergebnisabfrage nicht als Sub durchgeführt werden konnte. Abgesehen davon denke ich, dass temporäre Tabellen für dauerhafte Verbindungen zur Datenbank oder für Benutzerpools nützlicher wären. Wenn Sie nur die temporäre Tabelle erstellen und sie sofort zerstören, wird sie im Grunde nur als Unterabfrage verwendet.

Update: M_M macht einen guten Punkt in seinem Kommentar zu dieser Antwort. Ich bin jedoch immer noch der Meinung, dass ein Index besser wäre, wenn der Großteil der Aktivitäten nicht nur auf Sets erfolgt, bei denen das Land das einzige Kriterium ist. Für mich (nur meine Meinung) würde es darauf ankommen, wie oft die 'FR'-Teilmenge für sich allein ohne andere Kriterien benötigt würde. Andernfalls könnten Indizes für die Mehrheit der Suchvorgänge verwendet werden.


1
Es ist unwahrscheinlich, dass ein nicht gruppierter Index verwendet wird, wenn er 25% + der Tabelle zurückgibt. Die Kosten für 650K-Lesezeichen-Suchvorgänge würden die Kosten für einen 2-MM-Zeilenscan bei weitem überwiegen.

Hmm, guter Punkt.
Pheedbaq

Indizes machen es in der Tat schneller. Nicht unglaublich, aber schneller. Das Problem liegt immer noch darin, 650.000 Datenzeilen in einer temporären Tabelle zu speichern. Für mich klingt es verrückt, das zu tun.

@ DeanThomas: Ja, ich bin nicht sicher, ob ich den Punkt der temporären Tabelle sehe, unabhängig davon, was mit Indizes gemacht wird: \, es sei denn, es besteht eine dauerhafte Verbindung, bei der die Daten wiederverwendet werden.
Pheedbaq

1
Hmm, möglicherweise. Aber selbst wenn Sie dieselbe Unterabfrage nur zweimal durchgeführt hätten, hätte der Server wahrscheinlich bereits einen Ausführungsplan dafür (weiß es aber nicht genau).
Pheedbaq
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.