Filtrar com eficiência um conjunto grande com disjunções

9

Digamos que eu tenho uma única tabela

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

Neste exemplo TicketIdé a chave primária.

Desejo que os usuários possam criar consultas "parcialmente ad-hoc" nessa tabela. Eu digo parcialmente porque algumas partes da consulta sempre serão corrigidas:

  1. A consulta sempre executará um filtro de intervalo em um InsertDateTime
  2. A consulta sempre ORDER BY InsertDateTime DESC
  3. A consulta paginará os resultados

O usuário pode opcionalmente filtrar em qualquer uma das outras colunas. Eles podem filtrar nenhum, um ou muitos. E para cada coluna, o usuário pode selecionar um conjunto de valores que serão aplicados como uma disjunção. Por exemplo:

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;

Agora suponha que a tabela tenha 100.000.000 de linhas.

O melhor que posso apresentar é um índice de cobertura que inclua cada uma das colunas "opcionais":

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

Isso me dá um plano de consulta da seguinte maneira:

  • SELECT
    • Filtro
      • Topo
        • Projeto de sequência (escalar computacional)
          • Segmento
            • Busca de índice

Parece muito bom. Cerca de 80% a 90% do custo vem da operação Index Seek, o que é ideal.

Existem melhores estratégias para implementar esse tipo de pesquisa?

Não quero necessariamente descarregar a filtragem opcional para o cliente porque, em alguns casos, o conjunto de resultados da parte "fixa" pode ser 100s ou 1000s. O cliente também seria responsável pela classificação e paginação, o que pode funcionar muito para o cliente.

Joseph Daigle
fonte
Seria possível colocar sua subconsulta em uma tabela temporária ou variável de tabela e criar dessa maneira? Com minhas tabelas maiores, às vezes sou picado por subconsultas. Os índices de cobertura apenas levam você até agora.
Valkyrie
@Valkyrie que parece incrivelmente ineficiente. Considere também que as variantes dessa consulta (parâmetros diferentes e cláusulas where opcionais) provavelmente serão executadas várias vezes por segundo durante todo o dia e precisarão retornar resultados em média em menos de 100ms. Já fazemos isso, e ele funciona bem por enquanto. Estou apenas procurando idéias sobre como continuar a melhorar o desempenho para escalabilidade.
Joseph Daigle
Quanto você se preocupa em usar o espaço de armazenamento?
Jon Seigel
@JonSeigel isso depende de como muito ... mas eu quero ver todas as sugestões
Joseph Daigle
2
E qual é a sua abordagem / consulta para obter a segunda página dos resultados? RowNum BETWEEN 101 AND 200?
ypercubeᵀᴹ

Respostas:

1

Se essa carga de trabalho específica for a maioria das consultas na tabela, você poderá considerar:

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

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

Considerações:

  • você pode usar datetime2 (SQL 2008+; precisão flexível)
  • InsertDateTime será único dentro de sua precisão
  • se os horários não forem restritos, o sql exclusivo adicionará uma coluna uniquifier oculta do tipo int. Isso é adicionado a todos os índices não organizados para que eles possam fazer referência ao registro em cluster correto

Vantagens:

  • Adiciona novas linhas ao final da tabela
  • evite gravar as colunas de filtro opcionais duas vezes (uma vez no cluster e uma vez na folha de índice da inclusão)
  • a maior parte do tempo ainda estará em uma busca de índice de cluster com mais ou menos arquivadores.
  • adicione outro índice não clusterizado para os pares de colunas mais populares
Matt
fonte
1

Eu usei essa técnica no passado. A tabela não era tão grande, mas o critério de pesquisa era mais complexo.

Esta é a versão curta.

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
fonte
1

Dadas as suas duas primeiras condições prévias, eu examinaria um índice em cluster InsertDateTime.

Michael Green
fonte
-1

Se os clientes estiverem filtrando quase da mesma maneira repetidamente, você poderá criar um índice para essas consultas.

Por exemplo, o cliente está filtrando no SiteId e StatusId, você pode criar um índice adicional:

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

Dessa forma, a maioria das consultas 'mais comuns' pode ser executada rapidamente.

Ruud van de Beeten
fonte