web-dev-qa-db-fra.com

Pourquoi ma requête SELECT DISTINCT TOP N analyse-t-elle la table entière?

J'ai rencontré quelques SELECT DISTINCT TOP N requêtes qui semblent mal optimisées par l'optimiseur de requêtes SQL Server. Commençons par considérer un exemple trivial: une table d'un million de lignes avec deux valeurs alternées. Je vais utiliser la fonction GetNums pour générer les données:

DROP TABLE IF EXISTS X_2_DISTINCT_VALUES;

CREATE TABLE X_2_DISTINCT_VALUES (PK INT IDENTITY (1, 1), VAL INT NOT NULL);

INSERT INTO X_2_DISTINCT_VALUES WITH (TABLOCK) (VAL)
SELECT N % 2
FROM dbo.GetNums(1000000);

UPDATE STATISTICS X_2_DISTINCT_VALUES WITH FULLSCAN;

Pour la requête suivante:

SELECT DISTINCT TOP 2 VAL
FROM X_2_DISTINCT_VALUES
OPTION (MAXDOP 1);

SQL Server peut trouver deux valeurs distinctes simplement en analysant la première page de données de la table, mais il analyse toutes les données à la place . Pourquoi SQL Server n'analyse-t-il pas uniquement jusqu'à ce qu'il trouve le nombre demandé de valeurs distinctes?

Pour cette question, veuillez utiliser les données de test suivantes qui contiennent 10 millions de lignes avec 10 valeurs distinctes générées en blocs:

DROP TABLE IF EXISTS X_10_DISTINCT_HEAP;

CREATE TABLE X_10_DISTINCT_HEAP (VAL VARCHAR(10) NOT NULL);

INSERT INTO X_10_DISTINCT_HEAP WITH (TABLOCK)
SELECT REPLICATE(CHAR(65 + (N / 100000 ) % 10 ), 10)
FROM dbo.GetNums(10000000);

UPDATE STATISTICS X_10_DISTINCT_HEAP WITH FULLSCAN;

Les réponses pour une table avec un index cluster sont également acceptables:

DROP TABLE IF EXISTS X_10_DISTINCT_CI;

CREATE TABLE X_10_DISTINCT_CI (PK INT IDENTITY (1, 1), VAL VARCHAR(10) NOT NULL, PRIMARY KEY (PK));

INSERT INTO X_10_DISTINCT_CI WITH (TABLOCK) (VAL)
SELECT REPLICATE(CHAR(65 + (N / 100000 ) % 10 ), 10)
FROM dbo.GetNums(10000000);

UPDATE STATISTICS X_10_DISTINCT_CI WITH FULLSCAN;

La requête suivante analyse les 10 millions de lignes du tablea . Comment puis-je obtenir quelque chose qui ne balaye pas la table entière? J'utilise SQL Server 2016 SP1.

SELECT DISTINCT TOP 10 VAL
FROM X_10_DISTINCT_HEAP
OPTION (MAXDOP 1);
28
Joe Obbish

Il semble y avoir trois règles d'optimisation différentes qui peuvent effectuer l'opération DISTINCT 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 de regroupement (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 hachage ( flow distinct ) est un moyen de résoudre ce problème car il n'est pas bloquant. 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'attends à ce que le coût du plan de concordance de hachage (agrégé) augmente, car la table de hachage qu'il doit créer 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'indicateur de requête OPTIMIZE FOR:

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 à partir de 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 pouvez ne pas perdre 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 ne fonctionnaient pas 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'expression LAG, 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 à partir de X_10_DISTINCT_HEAP Et se termine en 1165 ms avec 1109 ms de temps CPU et 2537 lectures logiques, donc la 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 un traitement en mode batch même avec MAXDOP 1. Une façon d'obtenir le traitement en mode batch pour une table rowstore consiste à 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 se traduit par ce plan de requête .

Paul a souligné que je devais changer la requête pour utiliser LAG(..., 1) parce que LAG(..., 0) ne semble pas être éligible pour l'optimisation de Window Aggregate. Cette modification a réduit le temps écoulé à 520 ms et le temps CPU à 454 ms.

Notez que l'approche LAG() 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 expression SUBSTRING qui renvoie toujours une chaîne vide. SQL Server ne pense pas que 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. Ce qui suit requête 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 scanne 900000 lignes à partir de 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 à partir du tableau pour les requêtes SELECT DISTINCT TOP N Lorsque N> = le nombre de lignes distinctes estimées du tableau. 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. Deux façons d'inciter l'optimiseur à utiliser l'opérateur de correspondance de hachage (flux distinct) consistent à abaisser l'objectif de ligne à l'aide de l'indice OPTIMIZE FOR Ou à augmenter le nombre estimé de lignes distinctes à l'aide de LAG() ou SUBSTRING sur une colonne unique.

30
Joe Obbish

Vous avez déjà répondu correctement à vos propres questions.

Je veux juste ajouter une observation que le moyen le plus efficace est en fait de scanner la table entière - si elle peut être organisée comme columnstore 'heap' :

CREATE CLUSTERED COLUMNSTORE INDEX CCSI 
ON dbo.X_10_DISTINCT_HEAP;

La simple requête:

SELECT DISTINCT TOP (10)
    XDH.VAL 
FROM dbo.X_10_DISTINCT_HEAP AS XDH
OPTION (MAXDOP 1);

donne alors:

Execution plan

 Table 'X_10_DISTINCT_HEAP'. Nombre de balayages 1, 
 Lectures logiques 0, lectures physiques 0, lecture anticipée lectures 0, 
 lob logique lit 66, lob physique lit 0, lob read-ahead lit 0. 
 Table 'X_10_DISTINCT_HEAP'. Le segment lit 13, le segment a sauté 0. 
 
 Temps d'exécution SQL Server: 
 Temps CPU = 0 ms, temps écoulé = 11 ms.

Hash Match (Flow Distinct) ne peut actuellement pas s'exécuter en mode batch. Les méthodes qui utilisent cela sont beaucoup plus lentes en raison de la transition coûteuse (invisible) du traitement par lots au traitement en ligne. Par exemple:

SET ROWCOUNT 10;

SELECT DISTINCT 
    XDH.VAL
FROM dbo.X_10_DISTINCT_HEAP AS XDH
OPTION (FAST 1);

SET ROWCOUNT 0;

Donne:

Flow Distinct Execution Plan

 Table 'X_10_DISTINCT_HEAP'. Nombre de balayages 1, 
 Lectures logiques 0, lectures physiques 0, lecture anticipée lectures 0, 
 lob logique lit 20, lob physique lit 0, lob read-ahead lit 0. 
 Table 'X_10_DISTINCT_HEAP'. Le segment se lit 4, segment ignoré 0. 
 
 Temps d'exécution SQL Server: 
 Temps CPU = 640 ms, temps écoulé = 680 ms.

Cela est plus lent que lorsque la table est organisée en tant que segment de magasin de lignes.

12
Paul White 9

Voici une tentative d'émulation d'un balayage partiel répété (similaire mais pas identique à un saut de balayage) à l'aide d'un CTE récursif. Le but - puisque nous n'avons pas d'index sur (id) - est d'éviter les tris et les scans multiples sur la table.

Il fait quelques astuces pour contourner certaines restrictions CTE récursives:

  • Non TOP autorisé dans la partie récursive. Nous utilisons une sous-requête et ROW_NUMBER() à la place.
  • Nous ne pouvons pas avoir plusieurs références à la partie constante ou utiliser LEFT JOIN Ou utiliser NOT IN (SELECT id FROM cte) à partir de la partie récursive. Pour contourner, nous construisons une chaîne VARCHAR qui accumule toutes les valeurs id, similaire à STRING_AGG Ou à hierarchyID, puis comparons avec LIKE.

Pour un tas (en supposant que la colonne est nommée id) test-1 sur rextester.com .

Cela - comme les tests l'ont montré - n'évite pas plusieurs analyses, mais fonctionne correctement lorsque différentes valeurs sont trouvées dans les premières pages. Si toutefois les valeurs ne sont pas réparties uniformément, il peut effectuer plusieurs analyses sur de grandes parties de la table - ce qui entraîne bien sûr de mauvaises performances.

WITH ct (id, found, list) AS
  ( SELECT TOP (1) id, 1, CAST('/' + id + '/' AS VARCHAR(MAX))
    FROM x_large_table_2
  UNION ALL
    SELECT y.ID, ct.found + 1, CAST(ct.list + y.id + '/' AS VARCHAR(MAX))
    FROM ct
      CROSS APPLY 
      ( SELECT x.id, 
               rn = ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
        FROM x_large_table_2 AS x
        WHERE ct.list NOT LIKE '%/' + id + '/%'
      ) AS y
    WHERE ct.found < 3         -- the TOP (n) parameter here
      AND y.rn = 1
  )
SELECT id FROM ct ;

et lorsque la table est groupée (CI sur unique_key), test-2 sur rextester.com .

Cela utilise l'index cluster (WHERE x.unique_key > ct.unique_key) Pour éviter plusieurs analyses:

WITH ct (unique_key, id, found, list) AS
  ( SELECT TOP (1) unique_key, id, 1, CAST(CONCAT('/',id, '/') AS VARCHAR(MAX))
    FROM x_large_table_2
  UNION ALL
    SELECT y.unique_key, y.ID, ct.found + 1, 
        CAST(CONCAT(ct.list, y.id, '/') AS VARCHAR(MAX))
    FROM ct
      CROSS APPLY 
      ( SELECT x.unique_key, x.id, 
               rn = ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
        FROM x_large_table_2 AS x
        WHERE x.unique_key > ct.unique_key
          AND ct.list NOT LIKE '%/' + id + '/%'
      ) AS y
    WHERE ct.found < 5       -- the TOP (n) parameter here
      AND y.rn = 1
  )
-- SELECT * FROM ct ;        -- for debugging
SELECT id FROM ct ;
5
ypercubeᵀᴹ

Pour être complet, une autre façon d'aborder ce problème est d'utiliser OUTER APPLY . Nous pouvons ajouter un opérateur OUTER APPLY Pour chaque valeur distincte que nous devons trouver. Ceci est similaire dans son concept à l'approche récursive de ypercube, mais a effectivement la récursion écrite à la main. Un avantage est que nous pouvons utiliser TOP dans les tables dérivées au lieu de la solution de contournement ROW_NUMBER(). Un gros inconvénient est que le texte de la requête s'allonge à mesure que N augmente.

Voici une implémentation de la requête sur le tas:

SELECT VAL
FROM (
    SELECT t1.VAL VAL1, t2.VAL VAL2, t3.VAL VAL3, t4.VAL VAL4, t5.VAL VAL5, t6.VAL VAL6, t7.VAL VAL7, t8.VAL VAL8, t9.VAL VAL9, t10.VAL VAL10
    FROM 
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP 
    ) t1
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t2 WHERE t2.VAL NOT IN (t1.VAL)
    ) t2
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t3 WHERE t3.VAL NOT IN (t1.VAL, t2.VAL)
    ) t3
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t4 WHERE t4.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL)
    ) t4
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t5 WHERE t5.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL)
    ) t5
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t6 WHERE t6.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL)
    ) t6
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t7 WHERE t7.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL, t6.VAL)
    ) t7
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t8 WHERE t8.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL, t6.VAL, t7.VAL)
    ) t8
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t9 WHERE t9.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL, t6.VAL, t7.VAL, t8.VAL)
    ) t9
    OUTER APPLY
    ( 
    SELECT TOP 1 VAL FROM X_10_DISTINCT_HEAP t10 WHERE t10.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL, t6.VAL, t7.VAL, t8.VAL, t9.VAL)
    ) t10
) t
UNPIVOT 
(
  VAL FOR VALS IN (VAL1, VAL2, VAL3, VAL4, VAL5, VAL6, VAL7, VAL8, VAL9, VAL10)
) AS upvt;

Ici est 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 effectuer cinq fois les lectures logiques de cette requête à partir de l'autre réponse:

DECLARE @j INT = 10;

SELECT DISTINCT TOP (@j) VAL
FROM X_10_DISTINCT_HEAP
OPTION (MAXDOP 1, OPTIMIZE FOR (@j = 1));

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. En effet, 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:

SELECT VAL
FROM (
    SELECT t1.VAL VAL1, t2.VAL VAL2, t3.VAL VAL3, t4.VAL VAL4, t5.VAL VAL5, t6.VAL VAL6, t7.VAL VAL7, t8.VAL VAL8, t9.VAL VAL9, t10.VAL VAL10
    FROM 
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI 
    ) t1
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t2 WHERE PK > t1.PK AND t2.VAL NOT IN (t1.VAL)
    ) t2
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t3 WHERE PK > t2.PK AND t3.VAL NOT IN (t1.VAL, t2.VAL)
    ) t3
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t4 WHERE PK > t3.PK AND t4.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL)
    ) t4
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t5 WHERE PK > t4.PK AND t5.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL)
    ) t5
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t6 WHERE PK > t5.PK AND t6.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL)
    ) t6
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t7 WHERE PK > t6.PK AND t7.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL, t6.VAL)
    ) t7
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t8 WHERE PK > t7.PK AND t8.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL, t6.VAL, t7.VAL)
    ) t8
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t9 WHERE PK > t8.PK AND t9.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL, t6.VAL, t7.VAL, t8.VAL)
    ) t9
    OUTER APPLY
    ( 
    SELECT TOP 1 PK, VAL FROM X_10_DISTINCT_CI t10 WHERE PK > t9.PK AND t10.VAL NOT IN (t1.VAL, t2.VAL, t3.VAL, t4.VAL, t5.VAL, t6.VAL, t7.VAL, t8.VAL, t9.VAL)
    ) t10
) t
UNPIVOT 
(
  VAL FOR VALS IN (VAL1, VAL2, VAL3, VAL4, VAL5, VAL6, VAL7, VAL8, VAL9, VAL10)
) AS upvt;

Ici est 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 rapidement que la requête OPTIMIZE FOR 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 de sys.dm_exec_sessions Et sys.dm_exec_session_wait_stats. La session 56 était la requête APPLY et la session 63 était la requête OPTIMIZE FOR.

Sortie de sys.dm_exec_sessions:

╔════════════╦══════════╦════════════════════╦═══════════════╗
║ session_id ║ cpu_time ║ total_elapsed_time ║ logical_reads ║
╠════════════╬══════════╬════════════════════╬═══════════════╣
║         56 ║     1360 ║               1373 ║         32030 ║
║         63 ║     2094 ║               2091 ║         30400 ║
╚════════════╩══════════╩════════════════════╩═══════════════╝

Il semble y avoir un net avantage dans cpu_time et elapsed_time pour la requête APPLY.

Sortie de sys.dm_exec_session_wait_stats:

╔════════════╦════════════════════════════════╦═════════════════════╦══════════════╦══════════════════╦═════════════════════╗
║ session_id ║           wait_type            ║ waiting_tasks_count ║ wait_time_ms ║ max_wait_time_ms ║ signal_wait_time_ms ║
╠════════════╬════════════════════════════════╬═════════════════════╬══════════════╬══════════════════╬═════════════════════╣
║         56 ║ SOS_SCHEDULER_YIELD            ║                 340 ║            0 ║                0 ║                   0 ║
║         56 ║ MEMORY_ALLOCATION_EXT          ║                  38 ║            0 ║                0 ║                   0 ║
║         63 ║ SOS_SCHEDULER_YIELD            ║                 518 ║            0 ║                0 ║                   0 ║
║         63 ║ MEMORY_ALLOCATION_EXT          ║                  98 ║            0 ║                0 ║                   0 ║
║         63 ║ RESERVED_MEMORY_ALLOCATION_EXT ║                 400 ║            0 ║                0 ║                   0 ║
╚════════════╩════════════════════════════════╩═════════════════════╩══════════════╩══════════════════╩═════════════════════╝

La requête OPTIMIZE FOR 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.

2
Joe Obbish

Je pense que vous avez une réponse sur pourquoi
Cela peut être un moyen de le résoudre
Je sais que cela semble désordonné, mais le plan d'exécution indiquait que le top 2 distinct représentait 84% du coût

SELECT distinct top (2)  [enumID]
FROM [ENRONbbb].[dbo].[docSVenum1]

declare @table table (enumID tinyint);
declare @enumID tinyint;
set @enumID = (select top (1) [enumID] from [docSVenum1]);
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
set @enumID = (select top (1) [enumID] from [docSVenum1] where [enumID] not in (select enumID from @table));
insert into @table values (@enumID);
select enumID from @table;
1
paparazzo