Lorsque vous rejoignez une table principale à une table de détail, comment puis-je encourager SQL Server 2014 à utiliser l'estimation de la cardinalité de la table plus grande (détaillée) comme l'estimation de la cardinalité de la sortie de jointure?
Par exemple, lorsque vous rejoignez les lignes maîtres de 10k à 100 000 lignes de détail, je souhaite que SQL Server soit estimer la jointure à 100 000 lignes - de la même manière 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 le fait que chaque ligne de détail a toujours une ligne principale correspondante? (Ce qui 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 a une paire de tables maître/détail: VisitTarget
a une ligne pour chaque transaction de vente et VisitSale
a une ligne pour chaque produit dans chaque transaction. C'est une relation unique: une rangée de visittarget pour une moyenne de 10 rangées Visitsale.
Les tables ressemblent à ceci: (Je simplifie uniquement les colonnes pertinentes de 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 nous sommes partiellement dénormés en copiant les colonnes de filtrage les plus courantes (par exemple, SaleDate
) de la table principale dans les lignes de la table de chaque détail, puis nous avons ajouté des index sur les deux tables afin de mieux soutenir la date de support. requêtes filtrées. Cela fonctionne bien pour réduire les E/S lors de la mise en marche des requêtes filtrées par la date, mais je pense que cette approche provoque des problèmes d'estimation de la cardinalité lors de la joignement des tables de maître et de détail ensemble.
Lorsque nous rejoignons 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 de détail (VisitSale
) est redondant. Il est là pour activer les E/S séquentielles (AKA Index Operateur de recherche) sur la table de détail des requêtes filtrées par une plage de dates.
Le plan de ces types de questions ressemble à ceci:
Un plan réel d'une requête avec le même problème peut être trouvé ici .
Comme vous pouvez le constater, l'estimation de la cardinalité pour la jointure (l'info-bulle à gauche de l'image) est supérieure à 4 fois trop bas: 2,1 m vs.5m estimée. Cela provoque des problèmes de performance (par exemple renversant à TEMPDB), en particulier lorsque cette requête est une sous-requête qui est utilisée dans une requête plus complexe.
Mais les estimations du nombre de lignes pour chaque branche de la jointure sont proches des comptes de rangée. La moitié supérieure de la jointure est de 100k réelle vs. 164k estimée. La moitié inférieure de la jointure est de 2,1 millions de lignes réelles vs. 3.7m estimées. La distribution de godets hachage a également l'air bien. Ces observations me suggèrent que les statistiques sont correctes pour chaque table et que le problème est l'estimation de la cardinalité de Join.
Au début, je pensais que le problème était SQL Server s'attendant à ce que les colonnes salées de chaque table soient indépendantes, alors qu'elles sont identiques. J'ai donc essayé d'ajouter une comparaison sur l'égalité des dates de vente à la condition de jointure ou de la clause OS.G.
ON vt.VisitTargetId = vs.VisitTargetId and vt.SaleDate = vs.SaleDate
ou alors
WHERE vt.SaleDate = vs.SaleDate
Cela n'a pas fonctionné. Il a même fait des estimations de cardinalité pire! Donc, soit SQL Server n'utilise pas cet indice d'égalité ou autre chose est la cause première du problème.
Vous avez des idées sur la manière de résoudre et d'espérer 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 de l'entrée la plus grande ("table de détail") de la jointure.
Si cela importe, nous exécutons SQL Server 2014 Enterprise SP2 CU8 Build 12.0.5557.0 sur Windows Server. Il n'y a pas d'indicateurs de trace activés. Le niveau de compatibilité de la base de données est SQL Server 2014. Nous voyons le même comportement sur plusieurs serveurs SQL différents. Il semble donc peu probable d'être un problème spécifique au serveur.
Il y a une optimisation dans l'estimateur de cardinalité SQL Server 2014 Server 2014 C'est exactement le comportement que je recherche:
Le nouveau CE utilise toutefois un algorithme plus simple qui suppose qu'il existe une association de jointure unique entre une grande table et une petite table. Cela suppose que chaque rangée de la grande table correspond exactement à une rangée dans la petite table. Cet algorithme renvoie la taille estimée de l'entrée plus grande en tant que cardinalité de jointure.
Idéalement, je pourrais obtenir ce comportement, où l'estimation de la cardinalité de la jointure serait la même que l'estimation de la grande table, même si ma table "petite" table reviendra toujours sur 100 000 rangées!
En supposant qu'aucune amélioration ne puisse être acquise en faisant quelque chose à la statistique ou en utilisant le Legacy CE, alors le moyen le plus simple autour de votre problème est de changer votre INNER JOIN
à 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 plage SaleDate
_ plage des tables et SaleDate
correspond toujours entre des tables, les résultats de votre requête ne doivent pas changer. Il peut sembler étrange d'utiliser une jointure extérieure comme celle-ci, mais pensez-y comme informant l'optimiseur de requête que la jointure à la table VisitTarget
ne réduira jamais le nombre de lignes retournées par la requête. Malheureusement, les clés étrangères ne modifient pas les estimations de la cardinalité, parfois parfois, vous devez recourir à des tours comme celle-ci. (Suggestion Microsoft Connect: Effectuez des estimations d'optimisation plus précises à 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 d'autre dans la requête après la jointure. Vous pouvez essayer d'utiliser une table temporaire pour contenir les résultats intermédiaires du jeu de résultats avec l'estimation de la cardinalité la plus importante.