Il semble y avoir trois règles d'optimisation différentes qui peuvent effectuer l' DISTINCT
opération dans la requête ci-dessus. La requête suivante renvoie une erreur qui suggère que la liste est exhaustive:
SELECT DISTINCT TOP 10 ID
FROM X_10_DISTINCT_HEAP
OPTION (MAXDOP 1, QUERYRULEOFF GbAggToSort, QUERYRULEOFF GbAggToHS, QUERYRULEOFF GbAggToStrm);
Msg 8622, niveau 16, état 1, ligne 1
Le processeur de requêtes n'a pas pu produire un plan de requête en raison des indications définies dans cette requête. Renvoyez la requête sans spécifier d'indices et sans utiliser SET FORCEPLAN.
GbAggToSort
implémente l'agrégat groupé (distinct) en tant que tri distinct. Il s'agit d'un opérateur de blocage qui lira toutes les données de l'entrée avant de produire des lignes. GbAggToStrm
implémente l'agrégat de regroupement en tant qu'agrégat de flux (qui nécessite également un tri d'entrée dans ce cas). Il s'agit également d'un opérateur de blocage. GbAggToHS
implémente en tant que correspondance de hachage, ce que nous avons vu dans le mauvais plan de la question, mais il peut être implémenté en tant que correspondance de hachage (agrégat) ou correspondance de hachage (flux distinct).
L' opérateur de correspondance de hachage ( flux distinct ) est un moyen de résoudre ce problème car il ne bloque pas. SQL Server doit pouvoir arrêter l'analyse une fois qu'il trouve suffisamment de valeurs distinctes.
L'opérateur logique Flow Distinct analyse l'entrée, supprimant les doublons. Alors que l'opérateur Distinct consomme toutes les entrées avant de produire une sortie, l'opérateur Flow Distinct renvoie chaque ligne telle qu'elle est obtenue à partir de l'entrée (sauf si cette ligne est un doublon, auquel cas elle est supprimée).
Pourquoi la requête de la question utilise-t-elle une correspondance de hachage (agrégat) au lieu d'une correspondance de hachage (flux distinct)? À mesure que le nombre de valeurs distinctes change dans la table, je m'attends à ce que le coût de la requête de correspondance de hachage (flux distinct) diminue car l'estimation du nombre de lignes dont il a besoin pour numériser vers la table devrait diminuer. Je m'attendrais à ce que le coût du plan de concordance de hachage (agrégé) augmente, car la table de hachage qu'il doit construire augmentera. Une façon d'enquêter est de créer un guide de plan . Si je crée deux copies des données mais que j'applique un guide de plan à l'une d'entre elles, je devrais pouvoir comparer la correspondance de hachage (agrégée) à la correspondance de hachage (distincte) côte à côte avec les mêmes données. Notez que je ne peux pas faire cela en désactivant les règles de l'optimiseur de requête car la même règle s'applique aux deux plans ( GbAggToHS
).
Voici une façon d'obtenir le guide de plan que je recherche:
DROP TABLE IF EXISTS X_PLAN_GUIDE_TARGET;
CREATE TABLE X_PLAN_GUIDE_TARGET (VAL VARCHAR(10) NOT NULL);
INSERT INTO X_PLAN_GUIDE_TARGET WITH (TABLOCK)
SELECT CAST(N % 10000 AS VARCHAR(10))
FROM dbo.GetNums(10000000);
UPDATE STATISTICS X_PLAN_GUIDE_TARGET WITH FULLSCAN;
-- run this query
SELECT DISTINCT TOP 10 VAL FROM X_PLAN_GUIDE_TARGET OPTION (MAXDOP 1)
Obtenez le descripteur de plan et utilisez-le pour créer un guide de plan:
-- plan handle is 0x060007009014BC025097E88F6C01000001000000000000000000000000000000000000000000000000000000
SELECT qs.plan_handle, st.text FROM
sys.dm_exec_query_stats AS qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) AS st
WHERE st.text LIKE '%X[_]PLAN[_]GUIDE[_]TARGET%'
ORDER BY last_execution_time DESC;
EXEC sp_create_plan_guide_from_handle
'EVIL_PLAN_GUIDE',
0x060007009014BC025097E88F6C01000001000000000000000000000000000000000000000000000000000000;
Les guides de plan ne fonctionnent que sur le texte exact de la requête, nous allons donc le recopier à partir du guide de plan:
SELECT query_text
FROM sys.plan_guides
WHERE name = 'EVIL_PLAN_GUIDE';
Réinitialisez les données:
TRUNCATE TABLE X_PLAN_GUIDE_TARGET;
INSERT INTO X_PLAN_GUIDE_TARGET WITH (TABLOCK)
SELECT REPLICATE(CHAR(65 + (N / 100000 ) % 10 ), 10)
FROM dbo.GetNums(10000000);
Obtenez un plan de requête pour la requête avec le guide de plan appliqué:
SELECT DISTINCT TOP 10 VAL FROM X_PLAN_GUIDE_TARGET OPTION (MAXDOP 1)
Cela a l'opérateur de correspondance de hachage (flux distinct) que nous voulions avec nos données de test. Notez que SQL Server s'attend à lire toutes les lignes de la table et que le coût estimé est exactement le même que pour le plan avec la correspondance de hachage (agrégat). Les tests que j'ai effectués ont suggéré que les coûts des deux plans sont identiques lorsque l'objectif de ligne du plan est supérieur ou égal au nombre de valeurs distinctes que SQL Server attend de la table, qui dans ce cas peut simplement être dérivé de la statistiques. Malheureusement (pour notre requête), l'optimiseur choisit la correspondance de hachage (agrégat) sur la correspondance de hachage (flux distinct) lorsque les coûts sont les mêmes. Nous sommes donc à 0,0000001 unités d'optimisation magique loin du plan que nous voulons.
Une façon d'attaquer ce problème consiste à diminuer l'objectif de ligne. Si l'objectif de ligne du point de vue de l'optimiseur est inférieur au nombre distinct de lignes, nous obtiendrons probablement une correspondance de hachage (flux distinct). Cela peut être accompli avec l' OPTIMIZE FOR
indice de requête:
DECLARE @j INT = 10;
SELECT DISTINCT TOP (@j) VAL
FROM X_10_DISTINCT_HEAP
OPTION (MAXDOP 1, OPTIMIZE FOR (@j = 1));
Pour cette requête, l'optimiseur crée un plan comme si la requête avait juste besoin de la première ligne, mais lorsque la requête est exécutée, elle récupère les 10 premières lignes. Sur ma machine, cette requête scanne 892800 lignes X_10_DISTINCT_HEAP
et se termine en 299 ms avec 250 ms de temps CPU et 2537 lectures logiques.
Notez que cette technique ne fonctionnera pas si les statistiques ne rapportent qu'une seule valeur distincte, ce qui pourrait se produire pour les statistiques échantillonnées par rapport aux données asymétriques. Cependant, dans ce cas, il est peu probable que vos données soient suffisamment compactées pour justifier l'utilisation de techniques comme celle-ci. Vous ne perdrez peut-être pas grand-chose en scannant toutes les données du tableau, surtout si cela peut être fait en parallèle.
Une autre façon d'attaquer ce problème consiste à augmenter le nombre de valeurs distinctes estimées que SQL Server s'attend à obtenir de la table de base. C'était plus difficile que prévu. L'application d'une fonction déterministe ne peut pas augmenter le nombre distinct de résultats. Si l'optimiseur de requête est conscient de ce fait mathématique (certains tests suggèrent que c'est au moins pour nos besoins), alors l'application de fonctions déterministes (qui inclut toutes les fonctions de chaîne ) n'augmentera pas le nombre estimé de lignes distinctes.
Beaucoup de fonctions non déterministes n'ont pas fonctionné non plus, y compris les choix évidents de NEWID()
et RAND()
. Cependant, LAG()
fait l'affaire pour cette requête. L'optimiseur de requêtes attend 10 millions de valeurs distinctes par rapport à l' LAG
expression, ce qui encouragera un plan de correspondance de hachage (flux distinct) :
SELECT DISTINCT TOP 10 LAG(VAL, 0) OVER (ORDER BY (SELECT NULL)) AS ID
FROM X_10_DISTINCT_HEAP
OPTION (MAXDOP 1);
Sur ma machine, cette requête scanne 892800 lignes X_10_DISTINCT_HEAP
et se termine en 1165 ms avec 1109 ms de temps CPU et 2537 lectures logiques, ce qui LAG()
ajoute un peu de surcharge relative. @Paul White a suggéré d'essayer le traitement en mode batch pour cette requête. Sur SQL Server 2016, nous pouvons obtenir le traitement en mode batch même avec MAXDOP 1
. Une façon d'obtenir le traitement en mode batch pour une table rowstore est de se joindre à une CCI vide comme suit:
CREATE TABLE #X_DUMMY_CCI (ID INT NOT NULL);
CREATE CLUSTERED COLUMNSTORE INDEX X_DUMMY_CCI ON #X_DUMMY_CCI;
SELECT DISTINCT TOP 10 VAL
FROM
(
SELECT LAG(VAL, 1) OVER (ORDER BY (SELECT NULL)) AS VAL
FROM X_10_DISTINCT_HEAP
LEFT OUTER JOIN #X_DUMMY_CCI ON 1 = 0
) t
WHERE t.VAL IS NOT NULL
OPTION (MAXDOP 1);
Ce code entraîne ce plan de requête .
Paul a souligné que je devais changer la requête à utiliser LAG(..., 1)
car LAG(..., 0)
il ne semble pas être éligible pour l'optimisation de Window Aggregate. Ce changement a réduit le temps écoulé à 520 ms et le temps CPU à 454 ms.
Notez que l' LAG()
approche n'est pas la plus stable. Si Microsoft modifie l'hypothèse d'unicité par rapport à la fonction, cela peut ne plus fonctionner. Il a une estimation différente avec l'héritage CE. De plus, ce type d'optimisation par rapport à un tas n'est pas nécessairement une bonne idée. Si la table est reconstruite, il est possible de se retrouver dans le pire des cas où presque toutes les lignes doivent être lues dans la table.
Contre une table avec une colonne unique (comme l'exemple d'index clusterisé dans la question), nous avons de meilleures options. Par exemple, nous pouvons tromper l'optimiseur en utilisant une SUBSTRING
expression qui renvoie toujours une chaîne vide. SQL Server ne pense pas que cela SUBSTRING
changera le nombre de valeurs distinctes, donc si nous l'appliquons à une colonne unique, telle que PK, le nombre estimé de lignes distinctes est de 10 millions. Cette requête suivante obtient l'opérateur de correspondance de hachage (flux distinct):
SELECT DISTINCT TOP 10 VAL + SUBSTRING(CAST(PK AS VARCHAR(10)), 11, 1)
FROM X_10_DISTINCT_CI
OPTION (MAXDOP 1);
Sur ma machine, cette requête analyse 900 000 lignes X_10_DISTINCT_CI
et se termine en 333 ms avec 297 ms de temps CPU et 3011 lectures logiques.
En résumé, l'optimiseur de requêtes semble supposer que toutes les lignes seront lues dans la table pour les SELECT DISTINCT TOP N
requêtes lorsque N
> = le nombre de lignes distinctes estimées de la table. L'opérateur de correspondance de hachage (agrégat) peut avoir le même coût que l'opérateur de correspondance de hachage (flux distinct), mais l'optimiseur choisit toujours l'opérateur d'agrégation. Cela peut entraîner des lectures logiques inutiles lorsque suffisamment de valeurs distinctes sont situées près du début de l'analyse de la table. L'optimiseur peut être amené à utiliser l'opérateur de correspondance de hachage (flux distinct) de deux manières différentes: réduire l'objectif de ligne à l'aide de l' OPTIMIZE FOR
indice ou augmenter le nombre estimé de lignes distinctes à l'aide LAG()
ou SUBSTRING
sur une colonne unique.
Pour être complet, une autre façon d'aborder ce problème consiste à utiliser OUTER APPLY . Nous pouvons ajouter un
OUTER APPLY
opérateur pour chaque valeur distincte que nous devons trouver. Ceci est similaire dans son concept à l'approche récursive d'Ypercube, mais a effectivement la récursion écrite à la main. Un avantage est que nous pouvons utiliserTOP
dans les tables dérivées au lieu de laROW_NUMBER()
solution de contournement. Un gros inconvénient est que le texte de la requête s'allonge à mesure qu'ilN
augmente.Voici une implémentation de la requête sur le tas:
Voici le plan de requête réel pour la requête ci-dessus. Sur ma machine, cette requête se termine en 713 ms avec 625 ms de temps CPU et 12605 lectures logiques. Nous obtenons une nouvelle valeur distincte toutes les 100k lignes, je m'attends donc à ce que cette requête analyse environ 900000 * 10 * 0,5 = 4500000 lignes. En théorie, cette requête devrait faire cinq fois les lectures logiques de cette requête de l'autre réponse:
Cette requête a effectué 2537 lectures logiques. 2537 * 5 = 12685 qui est assez proche de 12605.
Pour la table avec l'index clusterisé, nous pouvons faire mieux. Cela est dû au fait que nous pouvons passer la dernière valeur de clé en cluster dans la table dérivée pour éviter d'analyser deux fois les mêmes lignes. Une mise en œuvre:
Voici le plan de requête réel pour la requête ci-dessus. Sur ma machine, cette requête se termine en 154 ms avec 140 ms de temps CPU et 3203 lectures logiques. Cela semblait fonctionner un peu plus vite que la
OPTIMIZE FOR
requête sur la table d'index cluster. Je ne m'attendais pas à cela, alors j'ai essayé de mesurer les performances plus attentivement. Ma méthodologie consistait à exécuter chaque requête dix fois sans jeux de résultats et à examiner les nombres agrégés à partir desys.dm_exec_sessions
etsys.dm_exec_session_wait_stats
. La session 56 était laAPPLY
requête et la session 63 était laOPTIMIZE FOR
requête.Sortie de
sys.dm_exec_sessions
:Il semble y avoir un net avantage dans cpu_time et elapsed_time pour la
APPLY
requête.Sortie de
sys.dm_exec_session_wait_stats
:La
OPTIMIZE FOR
requête a un type d'attente supplémentaire, RESERVED_MEMORY_ALLOCATION_EXT . Je ne sais pas exactement ce que cela signifie. Il peut simplement s'agir d'une mesure de la surcharge dans l'opérateur de correspondance de hachage (flux distinct). Dans tous les cas, cela ne vaut peut-être pas la peine de s'inquiéter d'une différence de 70 ms dans le temps CPU.la source
Je pense que vous avez une réponse sur la raison pour laquelle
cela peut être un moyen de le résoudre.Je
sais que cela semble désordonné, mais le plan d'exécution indique que le top 2 distinct représente 84% du coût.
la source