web-dev-qa-db-fra.com

SQL Server choisit une boucle imbriquée rejoindre la table dimensionnelle et faire chercher chaque rangée

Je suis confronté à un problème où SQL Server génère un plan d'exécution non optimal: une boucle imbriquée rejoindre et cherche à la table dimensionnelle et exécute 2M le lit.

L'estimation de l'opération de tri est de 100 rangées au lieu de 450 k lignes et peut peut-être affecter la sélection du plan:

Néeshoop: https://www.brentozar.com/pastetheplan/?id=b110mz2pm ou plan de Néhoop

Ceci est dans un test DB. Nous avons une DB supplémentaire avec le même schéma et presque les mêmes données.

Exécuré exactement la même requête (à partir de SSMS) génère un plan différent à l'aide d'une jointure de hachage et d'une numérisation de table dimensionnelle (Lectures 32K):

Hashjoin: https://www.brentozar.com/pasetheplan/?id=r1jm7b2d7 ou plan de hachage

J'ai besoin d'une aide pour comprendre et résoudre le problème.

Je peux travailler autour de cela par l'articulation de hachage d'indice, mais cela n'a aucun sens que 2 DBS similaires sur le même cas génèrent différents plans.

Mise à jour n ° 1: J'ai constaté que le coût estimé est différent de sorte que SQL Server s'exécute parallèlement, il choisira une jointure de hachage.

Avec un seul fil sera imbriqué une boucle.

Mise à jour n ° 2: Le même problème est survenu lors d'une sélection de la même table. Dépend du nombre de colonnes (coût estimé). Lorsque je réduit le nombre de colonnes, le plan d'exécution tombe à une boucle imbriquée et recherche la table de dimension.

6
Yorik

Il semble y avoir trois raisons pour lesquelles vous obtenez un plan de jointure en boucle imbriquée en série dans l'un de vos environnements et une jointure de hachage de l'autre. Sur la base des informations que vous avez fournies, les meilleures corrections impliquent des allumettes de requêtes ou de fractionnez la requête en deux parties.

  1. Différences entre vos environnements

    Un environnement comporte 480662 rangées dans votre CCI et l'autre dispose de 686053 rangées. Je n'appellerais pas cela presque identique. Il semble également y avoir une différence de matériel ou de configuration entre vos environnements ou à tout le moins que vous obtenez très malchanceux. La série série de 251 Mo de données estimées a un coût IO de 0,0037538 unités. La sorte parallèle de 351 Mo de données estimées a un coût IO de 23,1377 unités, même s'il est actualisé par le parallélisme. Le moteur s'attend à renvoyer une quantité relativement significative de données pour le plan parallèle. Des différences comme cela peuvent conduire à différents plans entre les environnements.

  2. L'optimiseur mal applique un objectif de ligne réduction des coûts qui peuvent favoriser un plan de jointure de boucle imbriquée

    Le plan de boucle imbriquée est calculé comme si seulement 100 rangées doivent être émises du genre:

    bad row goal

    Cependant, la requête contient ce qui suit dans la clause SELECT: COUNT(*) OVER ()

    Le moteur doit lire toutes les lignes afin de produire le résultat correct pour l'agrégat. C'est en effet ce qui se passe dans le plan actuel et la recherche d'index est exécutée 450K fois au lieu de 100 fois. Cette réduction des coûts semble se produire sur une variété de versions (j'ai testé à la base de 2016 SP1), sur les deux CES, avec de nombreuses fonctions de fenêtres différentes, et pour le mode BATCH et le mode ligne. C'est une limitation du produit qui aboutit à un plan de requête sous-optimal ici.

  3. Le plan de jointure de la boucle imbriquée est en série due à une limitation avec Tries de mode par lots

    Il est possible que votre boucle imbriquée en série soit admissible au parallélisme (dépend de votre CTFP) et vous vous demandez peut-être pourquoi l'optimiseur n'a pas trouvé de plan parallèle moins coûteux. L'optimiseur a des heuristiques qui empêchent un mode de traitement parallèle parallèle d'être le premier enfant d'une jointure à boucle imbriquée (qui doit être exécuté en mode ligne). Le problème est que le tri par lot parallèle TRY mettra toutes les lignes sur un seul fil qui ne fonctionnera pas bien avec une boucle imbriquée parallèle. Déplacement de la sorte Pour être un parent de la boucle Joindre ne provoquera pas une diminution du nombre d'exécutions estimées pour la recherche d'index (en raison du problème optimiseur). En conséquence, vous êtes très susceptible de vous retrouver avec un plan de série, même si la CTFP était définie sur la valeur par défaut de 5.


Voici une reproduction de votre problème, que je ne peux pas télécharger sur Pastheplan, car elle ne prend pas en charge ma version de SQL Server:

drop table if exists cci_216665;

create table cci_216665 (
    SORT_ID BIGINT,
    JOIN_ID BIGINT,
    COL1 BIGINT,
    COL2 BIGINT,
    COL3 BIGINT,
    INDEX CCI CLUSTERED COLUMNSTORE
);

INSERT INTO cci_216665 WITH (TABLOCK)
SELECT TOP (500000) ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
, ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) % 50
, 0, 0, 0
FROM master..spt_values t1
CROSS JOIN master..spt_values t2
OPTION (MAXDOP 1);

drop table if exists YEAH_NAH;

CREATE TABLE dbo.YEAH_NAH (ID INT, FILLER VARCHAR(20), PRIMARY KEY (ID));

INSERT INTO dbo.YEAH_NAH WITH (TABLOCK)
SELECT TOP (50) ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
, 'CHILLY BIN'
FROM master..spt_values t1;

GO

-- takes 780 ms of CPU with nested loops
SELECT TOP (100)
*, COUNT(*) OVER ()
FROM cci_216665 c
INNER JOIN YEAH_NAH y ON c.JOIN_ID = y.ID
ORDER BY SORT_ID;

-- takes 111 ms of CPU with hash join
SELECT TOP (100)
*, COUNT(*) OVER ()
FROM cci_216665 c
INNER JOIN YEAH_NAH y ON c.JOIN_ID = y.ID
ORDER BY SORT_ID
OPTION (HASH JOIN);

Le moyen le plus simple de résoudre votre problème est de scinder vos requêtes en deux. Voici une façon de le faire:

SELECT COUNT(*)
FROM cci_216665 c
INNER JOIN YEAH_NAH y ON c.JOIN_ID = y.ID;

SELECT TOP (100) *
FROM cci_216665 c
INNER JOIN YEAH_NAH y ON c.JOIN_ID = y.ID
ORDER BY SORT_ID;

Sur ma machine, cela est effectivement plus rapide que le plan de jointure de hachage, mais vous ne voyez peut-être pas les mêmes résultats. En général, ma première tentative pour une requête comme la vôtre serait d'éviter un agrégat de fenêtre sans une clause OVER lorsque seules les 100 premières lignes sont nécessaires.

Une alternative raisonnable consiste à utiliser le DISABLE_OPTIMIZER_ROWGOAL Utilisez l'indice introduit dans SQL Server 2016 SP1. Pour ce type de requête, il y a un problème avec les objectifs de la ligne afin que cette indice aborde directement le problème sans dépendance à la statistique ou à rien de ce genre. Je considérerais qu'il s'agisse d'un indice relativement sûr d'employer.

SELECT TOP (100)
*, COUNT(*) OVER ()
FROM cci_216665 c
INNER JOIN YEAH_NAH y ON c.JOIN_ID = y.ID
ORDER BY SORT_ID
OPTION (USE HINT('DISABLE_OPTIMIZER_ROWGOAL'));

Il en résulte un plan de jointure de hachage sur ma machine.

6
Joe Obbish