La jointure par hachage entre les tables maître / détail produit une estimation de cardinalité trop faible

9

Lors de la jonction d'une table principale à une table de détail, comment puis-je encourager SQL Server 2014 à utiliser l'estimation de cardinalité de la plus grande table (de détail) comme estimation de cardinalité de la sortie de jointure?

Par exemple, lors de la jonction de 10 000 lignes principales à 100 000 lignes de détail, je souhaite que SQL Server estime la jointure à 100 000 lignes - le même que le nombre estimé de lignes de détail. Comment dois-je structurer mes requêtes et / ou tables et / ou index pour aider l'estimateur de SQL Server à tirer parti du fait que chaque ligne de détail a toujours une ligne principale correspondante? (Cela signifie qu'une jointure entre eux ne devrait jamais réduire l'estimation de la cardinalité.)

Voici plus de détails. Notre base de données comprend une paire de tableaux maître / détail: VisitTargetune ligne pour chaque transaction de vente et VisitSaleune ligne pour chaque produit dans chaque transaction. C'est une relation un-à-plusieurs: une ligne VisitTarget pour une moyenne de 10 lignes VisitSale.

Les tableaux ressemblent à ceci: (je simplifie uniquement les colonnes pertinentes pour cette question)

-- "master" table
CREATE TABLE VisitTarget
(
  VisitTargetId int IDENTITY(1,1) NOT NULL PRIMARY KEY CLUSTERED,
  SaleDate date NOT NULL,
  StoreId int NOT NULL
  -- other columns omitted for clarity  
);
-- covering index for date-scoped queries
CREATE NONCLUSTERED INDEX IX_VisitTarget_SaleDate 
    ON VisitTarget (SaleDate) INCLUDE (StoreId /*, ...more columns */);

-- "detail" table
CREATE TABLE VisitSale
(
  VisitSaleId int IDENTITY(1,1) NOT NULL PRIMARY KEY CLUSTERED,
  VisitTargetId int NOT NULL,
  SaleDate date NOT NULL, -- denormalized; copied from VisitTarget
  StoreId int NOT NULL, -- denormalized; copied from VisitTarget
  ItemId int NOT NULL,
  SaleQty int NOT NULL,
  SalePrice decimal(9,2) NOT NULL
  -- other columns omitted for clarity  
);
-- covering index for date-scoped queries
CREATE NONCLUSTERED INDEX IX_VisitSale_SaleDate 
  ON VisitSale (SaleDate)
  INCLUDE (VisitTargetId, StoreId, ItemId, SaleQty, TotalSalePrice decimal(9,2) /*, ...more columns */
);
ALTER TABLE VisitSale 
  WITH CHECK ADD CONSTRAINT FK_VisitSale_VisitTargetId 
  FOREIGN KEY (VisitTargetId)
  REFERENCES VisitTarget (VisitTargetId);
ALTER TABLE VisitSale
  CHECK CONSTRAINT FK_VisitSale_VisitTargetId;

Pour des raisons de performances, nous avons partiellement dénormalisé en copiant les colonnes de filtrage les plus courantes (par exemple SaleDate) de la table principale dans les lignes de chaque table détaillée, puis nous avons ajouté des index de couverture sur les deux tables pour mieux prendre en charge les requêtes filtrées par date. Cela fonctionne très bien pour réduire les E / S lors de l'exécution de requêtes filtrées par date, mais je pense que cette approche provoque des problèmes d'estimation de cardinalité lors de la jonction des tables maître et détail.

Lorsque nous joignons ces deux tables, les requêtes ressemblent à ceci:

SELECT vt.StoreId, vt.SomeOtherColumn, Sales = sum(vs.SalePrice*vs.SaleQty)
FROM VisitTarget vt 
    JOIN VisitSale vs on vt.VisitTargetId = vs.VisitTargetId
WHERE
    vs.SaleDate BETWEEN '20170101' and '20171231'
    and vt.SaleDate BETWEEN '20170101' and '20171231'
    -- more filtering goes here, e.g. by store, by product, etc. 

Le filtre de date sur la table détaillée ( VisitSale) est redondant. Il est là pour activer les E / S séquentielles (aka opérateur de recherche d'index) sur la table détaillée pour les requêtes filtrées par une plage de dates.

Le plan pour ces types de requêtes ressemble à ceci:

entrez la description de l'image ici

Un plan réel d'une requête avec le même problème peut être trouvé ici .

Comme vous pouvez le voir, l'estimation de cardinalité pour la jointure (l'info-bulle en bas à gauche de l'image) est plus de 4x trop faible: 2,1 M réels contre 0,5 M estimés. Cela provoque des problèmes de performances (par exemple, débordement sur tempdb), en particulier lorsque cette requête est une sous-requête utilisée dans une requête plus complexe.

Mais les estimations du nombre de lignes pour chaque branche de la jointure sont proches du nombre réel de lignes. La moitié supérieure de la jointure est 100K réelle contre 164K estimée. La moitié inférieure de la jointure représente 2,1 millions de lignes réelles contre 3,7 millions estimées. La distribution des seaux de hachage semble également bonne. Ces observations me suggèrent que les statistiques sont OK pour chaque table, et que le problème est l'estimation de la cardinalité de jointure.

Au début, je pensais que le problème était que SQL Server s'attendait à ce que les colonnes SaleDate dans chaque table soient indépendantes, alors qu'en réalité elles sont identiques. J'ai donc essayé d'ajouter une comparaison d'égalité pour les dates de vente à la condition de jointure ou à la clause WHERE, par exemple

ON vt.VisitTargetId = vs.VisitTargetId and vt.SaleDate = vs.SaleDate

ou

WHERE vt.SaleDate = vs.SaleDate

Ça n'a pas marché. Cela a même aggravé les estimations de cardinalité! Donc, soit SQL Server n'utilise pas cet indice d'égalité, soit quelque chose d'autre est la cause première du problème.

Vous avez des idées sur la façon de dépanner et, espérons-le, de résoudre ce problème d'estimation de cardinalité? Mon objectif est que la cardinalité de la jointure maître / détail soit estimée de la même manière que l'estimation pour l'entrée plus grande ("table de détail") de la jointure.

Si cela est important, nous exécutons SQL Server 2014 Enterprise SP2 CU8 build 12.0.5557.0 sur Windows Server. Aucun indicateur de trace n'est activé. Le niveau de compatibilité de la base de données est SQL Server 2014. Nous constatons le même comportement sur plusieurs serveurs SQL différents, il semble donc peu probable qu'il s'agisse d'un problème spécifique au serveur.

Il y a une optimisation dans l' estimateur de cardinalité SQL Server 2014 qui est exactement le comportement que je recherche:

Le nouveau CE, cependant, utilise un algorithme plus simple qui suppose qu'il existe une association de jointure un-à-plusieurs entre une grande table et une petite table. Cela suppose que chaque ligne du grand tableau correspond exactement à une ligne du petit tableau. Cet algorithme renvoie la taille estimée de l'entrée la plus grande comme cardinalité de jointure.

Idéalement, je pourrais obtenir ce comportement, où l'estimation de cardinalité pour la jointure serait la même que l'estimation pour la grande table, même si ma "petite" table retournera toujours plus de 100K lignes!

Justin Grant
la source

Réponses:

6

En supposant qu'aucune amélioration ne peut être obtenue en faisant quelque chose aux statistiques ou en utilisant l'héritage CE, alors le moyen le plus simple de contourner votre problème est de changer votre INNER JOINpour un LEFT OUTER JOIN:

SELECT vt.StoreId, vt.SomeOtherColumn, Sales = sum(vs.SalePrice*vs.SaleQty)
FROM VisitSale vs
    LEFT OUTER JOIN VisitTarget vt on vt.VisitTargetId = vs.VisitTargetId
            AND vt.SaleDate BETWEEN '20170101' and '20171231'
WHERE vs.SaleDate BETWEEN '20170101' and '20171231'

Si vous avez une clé étrangère entre les tables, vous filtrez toujours sur la même SaleDateplage pour les deux tables et SaleDatecorrespond toujours entre les tables, les résultats de votre requête ne doivent pas changer. Il peut sembler étrange d'utiliser une jointure externe comme celle-ci, mais pensez-y comme informant l'optimiseur de requête que la jointure à la VisitTargettable ne réduira jamais le nombre de lignes renvoyées par la requête. Malheureusement, les clés étrangères ne modifient pas les estimations de cardinalité, vous devez donc parfois recourir à des astuces comme celle-ci. (Suggestion Microsoft Connect: améliorez la précision des estimations de l'optimiseur à l'aide de métadonnées ).

Il est possible que l'écriture de la requête dans ce formulaire ne fonctionne pas bien en fonction de ce qui se passe dans la requête après la jointure. Vous pouvez essayer d'utiliser une table temporaire pour conserver les résultats intermédiaires du jeu de résultats avec l'estimation de cardinalité la plus importante.

Joe Obbish
la source