Filtrer efficacement un grand ensemble avec des disjonctions

9

Disons que j'ai une seule table

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

Dans cet exemple, TicketIdla clé primaire.

Je souhaite que les utilisateurs puissent créer des requêtes "partiellement ad hoc" sur cette table. Je dis en partie parce que certaines parties de la requête seront toujours corrigées:

  1. La requête effectuera toujours un filtre de plage sur un InsertDateTime
  2. La requête sera toujours ORDER BY InsertDateTime DESC
  3. La requête affichera les résultats

L'utilisateur peut éventuellement filtrer sur n'importe quelle autre colonne. Ils peuvent filtrer sur aucun, un ou plusieurs. Et pour chaque colonne, l'utilisateur peut choisir parmi un ensemble de valeurs qui seront appliquées comme une disjonction. Par exemple:

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;

Supposons maintenant que la table comporte 100 000 000 lignes.

Le mieux que je puisse trouver est un index de couverture qui inclut chacune des colonnes "facultatives":

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

Cela me donne un plan de requête comme suit:

  • SÉLECTIONNER
    • Filtre
      • Haut
        • Projet de séquence (calcul scalaire)
          • Segment
            • Recherche d'index

Cela semble plutôt bien. Environ 80% à 90% du coût provient de l'opération Index Seek, ce qui est idéal.

Existe-t-il de meilleures stratégies pour mettre en œuvre ce type de recherche?

Je ne veux pas nécessairement décharger le filtrage optionnel sur le client car dans certains cas, le jeu de résultats de la partie "fixe" peut être de 100 ou de 1000. Le client serait alors également responsable du tri et de la pagination, ce qui pourrait trop fonctionner pour le client.

Joseph Daigle
la source
Serait-il possible de placer votre sous-requête dans une table temporaire ou une variable de table et de créer de cette façon? Avec mes tables plus grandes, je me fais parfois piquer par des sous-requêtes. Les indices de couverture ne vous emmènent que si loin.
Valkyrie
@Valkyrie qui semble incroyablement inefficace. Considérez également que les variantes de cette requête (différents paramètres et différentes clauses where facultatives) s'exécuteront probablement plusieurs fois par seconde toute la journée et devront renvoyer des résultats en moyenne en moins de 100 ms. Nous le faisons déjà, et cela fonctionne bien pour l'instant. Je suis juste à la recherche d'idées sur la façon de continuer à améliorer les performances pour l'évolutivité.
Joseph Daigle
À quel point vous souciez-vous de l'utilisation de l'espace de stockage?
Jon Seigel
@JonSeigel cela dépend de combien ... mais je veux voir des suggestions
Joseph Daigle
2
Et quelle est votre approche / requête pour obtenir la 2ème page des résultats? RowNum BETWEEN 101 AND 200?
ypercubeᵀᴹ

Réponses:

1

Si cette charge de travail particulière représente la majorité des requêtes sur la table, vous pouvez envisager:

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

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

Considérations:

  • pouvez-vous utiliser datetime2 (SQL 2008+; précision flexible)
  • InsertDateTime sera unique dans votre précision
  • si les heures ne sont pas contraintes, sql unique ajoutera une colonne uniquifier cachée de type int. Ceci est ajouté à tous les index non tronqués afin qu'ils puissent référencer l'enregistrement en cluster correct

Avantages:

  • Ajoute de nouvelles lignes à la fin du tableau
  • empêcher l'écriture des colonnes de filtre facultatives deux fois (une fois dans le cluster et une fois sur la feuille d'index pour l'inclusion)
  • la majorité de votre temps sera toujours sur une recherche d'index de cluster avec plus ou moins de déclarants.
  • puis ajoutez un autre index non cluster pour les paires de colonnes les plus populaires
Mat
la source
1

J'ai utilisé cette technique dans le passé. Le tableau n'était pas aussi grand mais les critères de recherche étaient plus complexes.

C'est la version courte.

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
la source
1

Compte tenu de vos deux premières conditions préalables, j'examinerais un index clusterisé InsertDateTime.

Michael Green
la source
0

pourquoi ne considérez-vous pas le partitionnement? Il est disponible dans SQL 2008 et nécessite une édition Enterprise (ou une édition Developer).

Fondamentalement, vous divisez votre table sur plusieurs partitions et vous définissez vos critères de partition (fonction), serait-ce votre plage de dates?

https://www.simple-talk.com/sql/database-administration/gail-shaws-sql-server-howlers/

AlexTheDeveloper
la source
-1

Si les clients filtrent presque toujours de la même manière, vous pouvez créer un index pour ces requêtes.

Par exemple, le client filtre sur SiteId et StatusId, vous pouvez créer un index supplémentaire:

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

De cette façon, la plupart des requêtes «les plus courantes» pourraient s'exécuter rapidement.

Ruud van de Beeten
la source