Filtrar eficientemente un conjunto grande con disyunciones

9

Digamos que tengo una sola mesa

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

En este ejemplo TicketIdes la clave primaria.

Quiero que los usuarios puedan crear consultas "parcialmente ad-hoc" en esta tabla. Digo parcialmente porque algunas partes de la consulta siempre se solucionarán:

  1. La consulta siempre realizará un filtro de rango en un InsertDateTime
  2. La consulta siempre ORDER BY InsertDateTime DESC
  3. La consulta buscará resultados

El usuario puede filtrar opcionalmente en cualquiera de las otras columnas. Pueden filtrar en ninguno, uno o muchos. Y para cada columna, el usuario puede seleccionar entre un conjunto de valores que se aplicarán como una disyunción. Por ejemplo:

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;

Ahora suponga que la tabla tiene 100,000,000 filas.

Lo mejor que se me ocurre es un índice de cobertura que incluye cada una de las columnas "opcionales":

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

Esto me da un plan de consulta de la siguiente manera:

  • SELECCIONE
    • Filtrar
      • Parte superior
        • Proyecto de secuencia (calcular escalar)
          • Segmento
            • Búsqueda de índice

Parece bastante bueno Alrededor del 80% -90% del costo proviene de la operación Index Seek, que es ideal.

¿Existen mejores estrategias para implementar este tipo de búsqueda?

No necesariamente quiero descargar el filtrado opcional al cliente porque en algunos casos el conjunto de resultados de la parte "fija" podría ser 100 o 1000. El cliente también sería responsable de la clasificación y paginación, lo que podría funcionar demasiado para el cliente.

Joseph Daigle
fuente
¿Sería posible colocar su subconsulta en una tabla temporal o variable de tabla y construir de esa manera? Con mis tablas más grandes, a veces me pican las subconsultas. Los índices de cobertura solo lo llevan muy lejos.
Valkyrie
@Valkyrie que parece increíblemente ineficiente. También tenga en cuenta que las variantes de esta consulta (diferentes parámetros y diferentes cláusulas where opcionales) probablemente se ejecutarán varias veces por segundo durante todo el día y deberán devolver resultados en promedio en menos de 100 ms. Ya hacemos esto, y funciona bien por ahora. Solo estoy buscando ideas sobre cómo continuar mejorando el rendimiento para la escalabilidad.
Joseph Daigle
¿Cuánto te importa usar el espacio de almacenamiento?
Jon Seigel
@ JonSeigel depende de cuánto ... pero quiero ver alguna sugerencia
Joseph Daigle
2
¿Y cuál es su enfoque / consulta para obtener la segunda página de los resultados? RowNum BETWEEN 101 AND 200?
ypercubeᵀᴹ

Respuestas:

1

Si esta carga de trabajo en particular es la mayoría de las consultas en la tabla, puede considerar:

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

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

Consideraciones:

  • ¿Puedes usar datetime2 (SQL 2008+; precisión flexible)
  • InsertDateTime será único dentro de su precisión
  • si los tiempos no están limitados, sql único agregará una columna oculta de uniquifier de tipo int. Esto se agrega a todos los índices no saturados para que puedan hacer referencia al registro agrupado correcto

Ventajas:

  • Agrega nuevas filas al final de la tabla.
  • evite escribir las columnas de filtro opcionales dos veces (una vez en el grupo y una vez en la hoja de índice para la inclusión)
  • la mayor parte de su tiempo todavía estará en una búsqueda de índice de clúster con más o menos archivadores.
  • luego agregue otro índice no agrupado para los pares de columnas más populares
Mate
fuente
1

He usado esta técnica en el pasado. La tabla no era tan grande, pero el criterio de búsqueda era más complejo.

Esta es la version corta.

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

Dadas sus dos primeras condiciones previas, estaría mirando un índice agrupado InsertDateTime.

Michael Green
fuente
-1

Si los clientes están filtrando casi de la misma manera una y otra vez, puede crear un índice para esas consultas.

Por ejemplo, el cliente está filtrando en SiteId y StatusId, puede crear un índice adicional:

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

De esta manera, la mayoría de las consultas 'más comunes' podrían ejecutarse rápidamente.

Ruud van de Beeten
fuente