Effizientes Filtern großer Mengen mit Disjunktionen

9

Nehmen wir an, ich habe einen einzigen Tisch

CREATE TABLE Ticket (
    TicketId int NOT NULL,
    InsertDateTime datetime NOT NULL,
    SiteId int NOT NULL,
    StatusId tinyint NOT NULL,
    AssignedId int NULL,
    ReportedById int NOT NULL,
    CategoryId int NULL
);

In diesem Beispiel TicketIdist der Primärschlüssel.

Ich möchte, dass Benutzer "teilweise Ad-hoc" -Abfragen für diese Tabelle erstellen können. Ich sage teilweise, weil einige Teile der Abfrage immer behoben werden:

  1. Die Abfrage führt immer einen Bereichsfilter für einen aus InsertDateTime
  2. Die Abfrage wird immer ORDER BY InsertDateTime DESC
  3. Die Abfrage zeigt die Ergebnisse an

Der Benutzer kann optional nach einer der anderen Spalten filtern. Sie können nach keiner, einer oder mehreren filtern. Und für jede Spalte kann der Benutzer aus einer Reihe von Werten auswählen, die als Disjunktion angewendet werden. Zum Beispiel:

SELECT
    TicketId
FROM (
    SELECT
        TicketId,
        ROW_NUMBER() OVER(ORDER BY InsertDateTime DESC) as RowNum
    FROM Ticket
    WHERE InsertDateTime >= '2013-01-01' AND InsertDateTime < '2013-02-01'
      AND StatusId IN (1,2,3)
      AND (CategoryId IN (10,11) OR CategoryId IS NULL)
    ) _
WHERE RowNum BETWEEN 1 AND 100;

Angenommen, die Tabelle enthält 100.000.000 Zeilen.

Das Beste, was ich finden kann, ist ein Deckungsindex, der jede der "optionalen" Spalten enthält:

CREATE NONCLUSTERED INDEX IX_Ticket_Covering ON Ticket (
    InsertDateTime DESC
) INCLUDE (
    SiteId, StatusId, AssignedId, ReportedById, CategoryId
);

Dies gibt mir einen Abfrageplan wie folgt:

  • WÄHLEN
    • Filter
      • oben
        • Sequenzprojekt (Skalar berechnen)
          • Segment
            • Index suchen

Es scheint ziemlich gut zu sein. Etwa 80% -90% der Kosten stammen aus der idealen Indexsuchoperation.

Gibt es bessere Strategien für die Implementierung dieser Art der Suche?

Ich möchte die optionale Filterung nicht unbedingt auf den Client verlagern, da in einigen Fällen die Ergebnismenge aus dem "festen" Teil 100 oder 1000 sein kann. Der Client ist dann auch für das Sortieren und Paging verantwortlich, was für den Client möglicherweise zu viel Arbeit bedeutet.

Joseph Daigle
quelle
Wäre es möglich, Ihre Unterabfrage in eine temporäre Tabelle oder Tabellenvariable zu platzieren und auf diese Weise zu erstellen? Bei meinen größeren Tabellen werde ich manchmal von Unterabfragen gestochen. Das Abdecken von Indizes bringt Sie nur so weit.
Walküre
@ Walküre, die unglaublich ineffizient scheint. Beachten Sie auch, dass Varianten dieser Abfrage (verschiedene Parameter und verschiedene optionale where-Klauseln) wahrscheinlich mehrmals pro Sekunde den ganzen Tag ausgeführt werden und Ergebnisse in durchschnittlich weniger als 100 ms zurückgeben müssen. Wir machen das schon und es funktioniert vorerst in Ordnung. Ich suche nur nach Ideen, wie die Leistung für Skalierbarkeit weiter verbessert werden kann.
Joseph Daigle
Wie wichtig ist Ihnen die Verwendung von Speicherplatz?
Jon Seigel
@ JonSeigel es kommt darauf an wie viel ... aber ich möchte irgendwelche Vorschläge sehen
Joseph Daigle
2
Und wie gehen Sie vor, um die zweite Seite der Ergebnisse zu erhalten? RowNum BETWEEN 101 AND 200?
Ypercubeᵀᴹ

Antworten:

1

Wenn diese bestimmte Arbeitslast die Mehrheit der Abfragen für die Tabelle darstellt, können Sie Folgendes berücksichtigen:

ALTER TABLE Ticket ADD CONSTRAINT PK_Ticket PRIMARY KEY NONCLUSTERED (TicketId);

CREATE UNIQUE CLUSTERED INDEX IX_Ticket_Covering ON Ticket (
    InsertDateTime ASC
);

Überlegungen:

  • können Sie datetime2 verwenden (SQL 2008+; flexible Genauigkeit)
  • wird InsertDateTime innerhalb Ihrer Präzision einzigartig sein
  • Wenn die Zeiten nicht eingeschränkt sind, fügt ein eindeutiges SQL eine versteckte Eindeutigkeitsspalte vom Typ int hinzu. Dies wird allen nicht gereinigten Indizes hinzugefügt, damit sie auf den richtigen gruppierten Datensatz verweisen können

Vorteile:

  • Fügt am Ende der Tabelle neue Zeilen hinzu
  • Verhindern Sie, dass die optionalen Filterspalten zweimal geschrieben werden (einmal im Cluster und einmal im Indexblatt für das Include).
  • Die meiste Zeit von Ihnen wird immer noch auf einer Cluster-Index-Suche mit mehr oder weniger Filern sein.
  • Fügen Sie dann einen anderen nicht gruppierten Index für die beliebtesten Spaltenpaare hinzu
Matt
quelle
1

Ich habe diese Technik in der Vergangenheit verwendet. Die Tabelle war bei weitem nicht so groß, aber die Suchkriterien waren komplexer.

Dies ist die Kurzversion.

CREATE PROC usp_Search
    (
    @StartDate  Date,
    @EndDate    Date,
    @Sites      Varchar(30) = NULL,
    @Assigned   Int = NULL, --Assuming only value possible
    @StartRow   Int,
    @EndRow     Int
    )
AS
DECLARE @TblSites   TABLE (ID Int)
IF @Sites IS NOT NULL
BEGIN
    -- Split @Sites into table @TblSites
END
SELECT  TicketId
FROM    (
        SELECT  TicketId,
                ROW_NUMBER() OVER(ORDER BY InsertDateTime DESC) as RowNum
        FROM    Ticket
                LEFT JOIN @TblSites
                    Ticket.SiteID = @TblSites.ID
        WHERE   InsertDateTime >= @StartDate 
                AND InsertDateTime < @EndDate
                AND (
                    @Assigned IS NULL 
                    OR AssignedId = @Assigned 
                    )
        ) _
WHERE   RowNum BETWEEN @StartRow AND @EndRow;
Dennis Post
quelle
1

Angesichts Ihrer ersten beiden Voraussetzungen würde ich mir einen Clustered-Index ansehen InsertDateTime.

Michael Green
quelle
-1

Wenn die Clients immer wieder auf fast dieselbe Weise filtern, können Sie einen Index für diese Abfragen erstellen.

Wenn der Client beispielsweise nach SiteId und StatusId filtert, können Sie einen zusätzlichen Index erstellen:

CREATE NONCLUSTERED INDEX IX_Ticket_InsertDateTime_SiteId_StatusId ON Ticket     
(InsertDateTime DESC,
 SiteId [ASC/DESC],
 StatusId [ASC/DESC] ) 
 INCLUDE ( ... );

Auf diese Weise können die meisten "häufigeren" Abfragen schnell ausgeführt werden.

Ruud van de Beeten
quelle