J'ai 3 "grandes" tables qui se joignent à une paire de colonnes (les deux int
s).
Chaque table a un indice en cluster sur Key1
, Key2
, puis une autre colonne. Key1
a une cardinalité faible et est très asymétrique. Il est toujours référencé dans la clause WHERE
. Key2
n'est jamais mentionné dans la clause WHERE
. Chaque jointure est plusieurs à plusieurs.
Le problème concerne l'estimation de la cardinalité. L'estimation de la sortie de chaque jointure obtient plus petite au lieu de plus grande . Cela se traduit par des estimations finales de basses centaines lorsque le résultat réel est bien dans les millions.
Y a-t-il un moyen pour moi d'indiquer le CE dans une meilleure estimation?
SELECT 1
FROM Table1 t1
JOIN Table2 t2
ON t1.Key1 = t2.Key1
AND t1.Key2 = t2.Key2
JOIN Table3 t3
ON t1.Key1 = t3.Key1
AND t1.Key2 = t3.Key2
WHERE t1.Key1 = 1;
Solutions j'ai essayé:
Key1
, Key2
Key1
(Cela aide beaucoup, mais je me retrouve avec des milliers de statistiques créées par l'utilisateur dans la base de données.)plan d'exécution masqué (désolé pour le mauvais masquage)
Dans le cas, je regarde, le résultat compte 9 millions de lignes. La nouvelle CE estime que 180 lignes; L'héritage CE estime 6100 rangées.
Voici un exemple reproductible:
DROP TABLE IF EXISTS #Table1, #Table2, #Table3;
CREATE TABLE #Table1 (Key1 INT NOT NULL, Key2 INT NOT NULL, T1Key3 INT NOT NULL, CONSTRAINT pk_t1 PRIMARY KEY CLUSTERED (Key1, Key2, T1Key3));
CREATE TABLE #Table2 (Key1 INT NOT NULL, Key2 INT NOT NULL, T2Key3 INT NOT NULL, CONSTRAINT pk_t2 PRIMARY KEY CLUSTERED (Key1, Key2, T2Key3));
CREATE TABLE #Table3 (Key1 INT NOT NULL, Key2 INT NOT NULL, T3Key3 INT NOT NULL, CONSTRAINT pk_t3 PRIMARY KEY CLUSTERED (Key1, Key2, T3Key3));
-- Table1
WITH Numbers
AS (SELECT TOP (1000000) Number = ROW_NUMBER() OVER(ORDER BY t1.number)
FROM master..spt_values t1
CROSS JOIN master..spt_values t2),
DataSize (Key1, NumberOfRows)
AS (SELECT 1, 2000 UNION
SELECT 2, 10000 UNION
SELECT 3, 25000 UNION
SELECT 4, 50000 UNION
SELECT 5, 200000)
INSERT INTO #Table1
SELECT Key1
, Key2 = ROW_NUMBER() OVER (PARTITION BY Key1, T1Key3 ORDER BY Number)
, T1Key3
FROM DataSize
CROSS APPLY (SELECT TOP(NumberOfRows)
Number
, T1Key3 = Number%(Key1*Key1) + 1
FROM Numbers
ORDER BY Number) size;
-- Table2 (same Key1, Key2 values; smaller number of distinct third Key)
WITH Numbers
AS (SELECT TOP (1000000) Number = ROW_NUMBER() OVER(ORDER BY t1.number)
FROM master..spt_values t1
CROSS JOIN master..spt_values t2)
INSERT INTO #Table2
SELECT DISTINCT
Key1
, Key2
, T2Key3
FROM #Table1
CROSS APPLY (SELECT TOP (Key1*10)
T2Key3 = Number
FROM Numbers
ORDER BY Number) size;
-- Table2 (same Key1, Key2 values; smallest number of distinct third Key)
WITH Numbers
AS (SELECT TOP (1000000) Number = ROW_NUMBER() OVER(ORDER BY t1.number)
FROM master..spt_values t1
CROSS JOIN master..spt_values t2)
INSERT INTO #Table3
SELECT DISTINCT
Key1
, Key2
, T3Key3
FROM #Table1
CROSS APPLY (SELECT TOP (Key1)
T3Key3 = Number
FROM Numbers
ORDER BY Number) size;
DROP TABLE IF EXISTS #a;
SELECT col = 1
INTO #a
FROM #Table1 t1
JOIN #Table2 t2
ON t1.Key1 = t2.Key1
AND t1.Key2 = t2.Key2
WHERE t1.Key1 = 1;
DROP TABLE IF EXISTS #b;
SELECT col = 1
INTO #b
FROM #Table1 t1
JOIN #Table2 t2
ON t1.Key1 = t2.Key1
AND t1.Key2 = t2.Key2
JOIN #Table3 t3
ON t1.Key1 = t3.Key1
AND t1.Key2 = t3.Key2
WHERE t1.Key1 = 1;
Juste pour être clair, l'optimiseur sait déjà que c'est une jointure à plusieurs à plusieurs. Si vous forcez que la fusion se joint et regarde un plan estimé, vous pouvez voir une propriété pour l'opérateur de jointure qui vous indique si la jointure pourrait être nombreuses à plusieurs. Le problème que vous devez résoudre ici est de heurter les estimations de la cardinalité, probablement pour obtenir un plan de requête plus efficace pour la partie de la requête que vous avez laissée de côté.
La première chose que j'essaierais consiste à mettre les résultats de la jointure de Object3
et Object5
dans une table Temp. Pour le plan que vous avez posté, il s'agit simplement d'une seule colonne sur 51393 lignes, il ne devrait donc guère d'occuper n'importe quel espace dans TEMPDB. Vous pouvez recueillir des statistiques complètes sur la table TEMP et que seule pourrait suffire à obtenir une estimation de la cardinalité finale précise suffisante. La collecte de statistiques complètes sur Object1
peut également aider. Les estimations de cardinalité empirent souvent pendant que vous traversez un plan de droite à gauche.
Si cela ne fonctionne pas, vous pouvez essayer l'indice ENABLE_QUERY_OPTIMIZER_HOTFIXES
requis si vous ne l'avez pas déjà activé au niveau de la base de données ou du serveur. Microsoft verrouille des corrections de performance affectant des performances pour SQL Server 2016 derrière ce paramètre. Certains d'entre eux sont liés aux estimations de la cardinalité, alors vous aurez peut-être chanceux et l'une des corrections aidera votre requête. Vous pouvez également essayer d'utiliser l'estimateur de cardinalité hérité avec un indice FORCE_LEGACY_CARDINALITY_ESTIMATION
requis. Certains ensembles de données peuvent obtenir de meilleures estimations avec le Legacy CE.
En dernier recours, vous pouvez augmenter manuellement l'estimation de la cardinalité par tout facteur que vous aimez utiliser l'utilisation de la fonction d'Adam Machanic's MANY()
. J'en parle dans une autre Réponse mais on dirait que le lien est mort. Si vous êtes intéressé, je peux essayer de creuser quelque chose.
SQL Server Statistics contient uniquement un histogramme pour la colonne principale de l'objet Statistiques. Par conséquent, vous pouvez créer des statistiques filtrées qui fournissent un histogramme de valeurs pour Key2
, mais uniquement entre des lignes avec Key1 = 1
. Créer ces statistiques filtrées sur chaque table fixe les estimations et conduit au comportement que vous attendez pour la requête de test: chaque nouvelle jointure n'a pas d'incidence sur l'estimation finale de cardinalité (confirmée dans SQL 2016 SP1 et SQL 2017).
-- Note: Add "WITH FULLSCAN" to each if you want a perfect 20,000 row estimate
CREATE STATISTICS st_#Table1 ON #Table1 (Key2) WHERE Key1 = 1
CREATE STATISTICS st_#Table2 ON #Table2 (Key2) WHERE Key1 = 1
CREATE STATISTICS st_#Table3 ON #Table3 (Key2) WHERE Key1 = 1
Sans ces statistiques filtrées, SQL Server prendra une approche plus heuristique pour estimer la cardinalité de votre jointure. Le papier blanc suivant contient de bonnes descriptions de haut niveau de certaines des heuristiques utilisées par SQL Server: optimiser vos plans de requête avec l'estimateur de cardinalité SQL Server 2014 .
Par exemple, l'ajout de la fonction USE HINT('ASSUME_JOIN_PREDICATE_DEPENDS_ON_FILTERS')
indice de votre requête modifiera la heuristique de confinement de jointure pour assumer une corrélation (plutôt que l'indépendance) entre le prédicat Key1
et le prédicat Key2
JOINT, qui peut être bénéfique pour votre requête. Pour la requête de test finale, cette indice augmente l'estimation de la cardinalité de 1,175
à 7,551
, mais reste un peu timide de l'estimation correcte 20,000
de ligne produite avec les statistiques filtrées.
Une autre approche que nous avons utilisée dans des situations similaires consiste à extraire le sous-ensemble pertinent des données dans les tables #Temp. Surtout maintenant que de nouvelles versions de SQL Server N'écriment plus avec impatience les tables #Temp à disque , nous avons eu de bons résultats avec cette approche. Votre description de votre jointure de plusieurs à plusieurs implique que chaque individu #TEpp table dans votre cas serait relativement petit (ou au moins plus petit que le résultat final), cette approche pourrait donc valoir la peine d'être essayée.
DROP TABLE IF EXISTS #Table1_extract, #Table2_extract, #Table3_extract, #c
-- Extract only the subset of rows that match the filter predicate
-- (Or better yet, extract only the subset of columns you need!)
SELECT * INTO #Table1_extract FROM #Table1 WHERE Key1 = 1
SELECT * INTO #Table2_extract FROM #Table2 WHERE Key1 = 1
SELECT * INTO #Table3_extract FROM #Table3 WHERE Key1 = 1
-- Now perform the join on those extracts, removing the filter predicate
SELECT col = 1
INTO #c
FROM #Table1_extract t1
JOIN #Table2_extract t2
ON t1.Key2 = t2.Key2
JOIN #Table3_extract t3
ON t1.Key2 = t3.Key2