Ordre des champs dans un ordre d'index composite avec des champs de sélectivité élevée et de faible sélectivité

11

J'ai une table SQL Server avec plus de 3 milliards de lignes. Une de mes requêtes prend un temps extrêmement long, je pense donc à l'optimiser. La requête ressemble à ceci:

SELECT [Enroll_Date]
      ,Count(*) AS [Record #]
      ,Count(Distinct UserID) AS [User #]
  FROM UserTable
  GROUP BY [Enroll_Date]

[Enroll_Date] est une colonne à faible sélectivité avec moins de 50 valeurs possibles, tandis que la colonne UserID est une colonne à haute sélectivité avec plus de 200 millions de valeurs distinctes. Sur la base de mes recherches, je pense que je devrais créer un indice composite non clusterisé sur ces deux colonnes, et en théorie, la colonne de haute sélectivité devrait être la première colonne. Mais je ne suis pas sûr dans mon cas, est-ce que cela fonctionnerait parce que j'utilise la colonne de faible sélectivité dans la clause group by.

Cette table n'a pas d'index cluster.

Thinkinger
la source
Pouvez-vous publier le plan d'exécution réel xml (utilisez pastebin et liez-le ici)? Quelle version du serveur SQL que vous utilisez?
Kin Shah
3
L'index avec la colonne hautement sélective en premier sera inutile pour la requête spécifique.
ypercubeᵀᴹ
Il est préférable d'utiliser la colonne de sélectivité supérieure comme première colonne clé d'un index (normalement). Dans ce scénario, comme vous l'avez deviné, cela ne vous aide pas du tout. Vous pourriez avoir besoin de deux index! Que se passe-t-il lorsque vous utilisez la fonction register_date d'abord et user_id ensuite?
paulbarbin

Réponses:

12

Comme alternative à la solution de @ AaronBertrand (si vous ne pouvez pas ou ne voulez pas créer une vue indexée), je vous recommande de créer un index sur (Enroll_Date, UserID). Si ce type de question est très courant sur votre table, cela devrait probablement même être votre index clusterisé.

Je ne recommanderais généralement pas les index à haute sélectivité en tant que "meilleure pratique" générale, mais je chercherais plutôt à savoir quel index donnera à votre requête les meilleures performances.

Un index sur (Enroll_Date, UserID)donnera à votre requête un plan de requête hautement optimisé et non bloquant avec Stream Aggregates.

Plan de requête agrégée de flux

"Non bloquant" dans ce contexte signifie que la requête n'a pas besoin de mettre en mémoire tampon des quantités importantes de données (comme, par exemple, un tri ou un agrégat de hachage), ce qui signifie qu'elle (a) commence immédiatement à renvoyer des lignes, et ( b) ne consomme pratiquement pas de mémoire de travail.

Daniel Hutmacher
la source
Drôle, à 4 secondes d'intervalle et la même réponse.
usr
11

La réponse d'Aarons est une excellente solution. Je vais répondre à la question en supposant que vous ne voulez pas adopter cette approche.

La requête que vous avez publiée sera généralement exécutée en regroupant d'abord (Enroll_Date, UserID), puis à nouveau (Enroll_Date). Cette optimisation est nouvelle dans SQL Server 2012. Elle prend effet en cas de single COUNT DISTINCT.

Un index sur ces deux colonnes dans l'ordre spécifique (Enroll_Date, UserID)suffira pour obtenir un plan efficace qui achemine une analyse d'index dans deux agrégats de flux consécutifs. L'ordre inverse ne permettrait pas ce plan.

Par conséquent, utilisez la commande (Enroll_Date, UserID). Vous n'avez pas le choix ici.

usr
la source
5 secondes d'intervalle et la même solution. Bien joué, monsieur. :)
Daniel Hutmacher
@DanielHutmacher OMG, parviendrons-nous à presque correspondre à nos messages pour une troisième fois?! +1 à vous! Comment pourrais-je ne pas voter pour une réponse identique?
usr
Glitch dans la matrice. :)
Daniel Hutmacher
Merci beaucoup. Je crée l'index et publierai l'amélioration une fois terminée. La version du serveur est Microsoft SQL Server 2008 R2 sur AWS, mais je suppose que c'est toujours le seul choince malgré tout.
Thinkinger
@Thinkinger au cas où vous n'accepteriez pas l'approche d'Aarons, vous avez un choix difficile :)
usr
11

Cela ressemble à un scénario idéal pour une vue indexée, qui vous permet de payer des calculs et des agrégats au moment de l'écriture au lieu du temps de la requête.

CREATE VIEW dbo.MyIndexedView
WITH SCHEMABINDING
AS 
  SELECT Enroll_Date, UserID, RawCount = COUNT_BIG(*)
  FROM dbo.UserTable
  GROUP BY Enroll_Date, UserID;
GO

CREATE UNIQUE CLUSTERED INDEX CIX_miv ON dbo.MyIndexedView(Enroll_Date, UserID);

Cela prendra un certain temps à créer et nécessitera bien sûr une maintenance tout au long de toutes les opérations DML, tout comme un index sur la table de base.

Maintenant, la requête par rapport à cette vue serait assez similaire - chaque ligne de la vue représente maintenant un combo utilisateur / date distinct, de sorte que le chiffre peut être calculé par un seul COUNT (*), tandis que le nombre total de lignes dans la table de base est déjà partiellement agrégées pour vous, il vous suffit maintenant de les additionner en utilisant SUM par date:

SELECT Enroll_Date, 
  [Record #] = SUM(RawCount),
  [User #] = COUNT(*)
FROM dbo.MyIndexedView WITH (NOEXPAND)
GROUP BY Enroll_Date; 

Ajout d'un indice NOEXPAND, après avoir rappelé ceci et cela .

Je peux vous dire sans aucun doute que cette requête sera plus rapide que votre requête actuelle (mais pas de combien), sauf dans les rares cas où vous avez exactement un utilisateur pour chaque date (auquel cas la même quantité de données aura à lire) et les colonnes que nous connaissons sont les seules colonnes de l'index de la table de base. Nous ne pouvons pas vous dire si cet accroissement des performances au moment de la lecture vaut le travail supplémentaire qui affectera la partie écriture de votre charge de travail - vous devrez le tester pour mesurer le compromis (aucun index n'est gratuit).

Et si vous utilisez fréquemment les mêmes clauses WHERE communes contre Enroll_Date pour des plages spécifiques et bien définies (par exemple, le trimestre ou l'année en cours à ce jour), vous pouvez ajouter des index filtrés correspondants qui réduisent encore plus les E / S (mais il y a toujours un troquer).

Vous pouvez également envisager de placer un index cluster sur la table de base. Cela ne semble pas être l'un de ces cas d'utilisation très rares qui bénéficient d'un tas.

Aaron Bertrand
la source
Je viens de confirmer avec notre informatique et il semble que je ne peux pas créer ce genre de vue. Mais appréciez toujours vos conseils, et cela aidera ceux qui peuvent les utiliser.
Thinkinger
1
Votre service informatique pense-t-il qu'il existe une différence significative entre une vue indexée et des index supplémentaires ou différents sur la table de base? Ne pas être combatif, juste curieux, car beaucoup de gens ont des idées fausses sur les vues indexées. J'aime à les considérer comme un index clusterisé plus skinnier sur la table, mais avec moins de lignes.
Aaron Bertrand
@Thinkinger également, les vues indexées ne sont pas uniquement EE. La correspondance des vues indexées est uniquement EE. Vous pouvez les cibler directement à l'aide de NOEXPAND.
usr