Skutecznie filtruj duży zestaw z rozróżnieniami

9

Powiedzmy, że mam jeden stolik

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
);

W tym przykładzie TicketIdjest klucz podstawowy.

Chcę, aby użytkownicy mogli tworzyć zapytania „częściowo ad-hoc” dla tej tabeli. Mówię częściowo, ponieważ kilka części zapytania zawsze zostanie naprawionych:

  1. Zapytanie zawsze wykona filtr zakresu na InsertDateTime
  2. Zapytanie będzie zawsze ORDER BY InsertDateTime DESC
  3. Zapytanie wyświetli wyniki strony

Użytkownik może opcjonalnie filtrować według dowolnej innej kolumny. Mogą filtrować według żadnego, jednego lub wielu. I dla każdej kolumny użytkownik może wybrać z zestawu wartości, które zostaną zastosowane jako rozłączenie. Na przykład:

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;

Załóżmy teraz, że tabela ma 100 000 000 wierszy.

Najlepsze, co mogę wymyślić, to indeks obejmujący każdą z „opcjonalnych” kolumn:

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

To daje mi plan zapytań w następujący sposób:

  • WYBIERZ
    • Filtr
      • Top
        • Sequence Project (Compute Scalar)
          • Człon
            • Indeks Szukaj

Wygląda całkiem nieźle. Około 80% -90% kosztów pochodzi z operacji indeksowania, która jest idealna.

Czy istnieją lepsze strategie wdrażania tego rodzaju wyszukiwania?

Niekoniecznie chcę odciążyć opcjonalne filtrowanie do klienta, ponieważ w niektórych przypadkach zestaw wyników z części „ustalonej” może wynosić 100s lub 1000s. Klient byłby wówczas także odpowiedzialny za sortowanie i stronicowanie, które może zbyt wiele dla niego działać.

Joseph Daigle
źródło
Czy można umieścić swoje podzapytanie w tabeli tymczasowej lub zmiennej tabeli i zbudować w ten sposób? Przy moich większych tabelach czasami ukąszają mnie podzapytania. Pokrycie indeksów zabierze cię do tej pory.
Valkyrie
@ Walkiria, która wydaje się niezwykle nieefektywna. Weź również pod uwagę, że warianty tego zapytania (różne parametry i różne opcjonalne klauzule gdzie) będą prawdopodobnie wykonywane kilka razy na sekundę przez cały dzień i będą musiały zwracać wyniki średnio w czasie krótszym niż 100 ms. Już to robimy i na razie działa dobrze. Po prostu szukam pomysłów, jak dalej poprawiać wydajność pod kątem skalowalności.
Joseph Daigle
Jak bardzo zależy ci na wykorzystaniu przestrzeni dyskowej?
Jon Seigel
@JonSeigel to zależy od tego, ile ... ale chcę zobaczyć jakieś sugestie
Joseph Daigle
2
A jakie jest twoje podejście / zapytanie, aby uzyskać drugą stronę wyników? RowNum BETWEEN 101 AND 200?
ypercubeᵀᴹ

Odpowiedzi:

1

Jeśli to szczególne obciążenie pracą stanowi większość zapytań do tabeli, możesz rozważyć:

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

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

Uwagi:

  • czy możesz używać datetime2 (SQL 2008+; elastyczna precyzja)
  • będzie InsertDateTime będzie unikalny w granicach Twojej precyzji
  • jeśli czasy nie są ograniczone, unikalny sql doda ukrytą kolumnę unikatową typu int. Jest to dodawane do wszystkich nieklastrowanych indeksów, aby mogły odwoływać się do poprawnego rekordu klastrowego

Zalety:

  • Dodaje nowe wiersze na końcu tabeli
  • zapobiec dwukrotnemu zapisywaniu opcjonalnych kolumn filtrów (raz w klastrze i raz na liście indeksu dla dołączenia)
  • większość czasu będzie nadal szukać indeksu klastrowego z większą lub mniejszą liczbą plików.
  • następnie dodaj inny indeks nieklastrowany dla najpopularniejszych par kolumn
Matt
źródło
1

Użyłem tej techniki w przeszłości. Tabela nie była prawie tak duża, ale kryteria wyszukiwania były bardziej złożone.

To jest krótka wersja.

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
źródło
1

Biorąc pod uwagę dwa pierwsze warunki, przyjrzałbym się indeksowi klastrowemu InsertDateTime.

Michael Green
źródło
-1

Jeśli klienci filtrują w prawie taki sam sposób w kółko, możesz utworzyć indeks dla tych zapytań.

Np. Klient filtruje na SiteId i StatusId, możesz utworzyć dodatkowy indeks:

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

W ten sposób większość „bardziej powszechnych” zapytań może działać szybko.

Ruud van de Beeten
źródło