INSERT sur une seule ligne… SELECT beaucoup plus lent que SELECT séparé

18

Étant donné le tableau de tas suivant avec 400 lignes numérotées de 1 à 400:

DROP TABLE IF EXISTS dbo.N;
GO
SELECT 
    SV.number
INTO dbo.N 
FROM master.dbo.spt_values AS SV
WHERE 
    SV.[type] = N'P'
    AND SV.number BETWEEN 1 AND 400;

et les paramètres suivants:

SET NOCOUNT ON;
SET STATISTICS IO, TIME OFF;
SET STATISTICS XML OFF;
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;

L' SELECTinstruction suivante se termine en environ 6 secondes ( démo , plan ):

DECLARE @n integer = 400;

SELECT
    c = COUNT_BIG(*) 
FROM dbo.N AS N
CROSS JOIN dbo.N AS N2
CROSS JOIN dbo.N AS N3
WHERE 
    N.number <= @n
    AND N2.number <= @n
    AND N3.number <= @n
OPTION
    (OPTIMIZE FOR (@n = 1));

Remarque: @La OPTIMIZE FORclause est uniquement destinée à produire une repro de taille raisonnable qui capture les détails essentiels du problème réel, y compris une erreur de cardinalité qui peut survenir pour diverses raisons.

Lorsque la sortie d'une seule ligne est écrite dans une table, cela prend 19 secondes ( démo , plan ):

DECLARE @T table (c bigint NOT NULL);

DECLARE @n integer = 400;

INSERT @T
    (c)
SELECT
    c = COUNT_BIG(*) 
FROM dbo.N AS N
CROSS JOIN dbo.N AS N2
CROSS JOIN dbo.N AS N3
WHERE 
    N.number <= @n
    AND N2.number <= @n
    AND N3.number <= @n
OPTION
    (OPTIMIZE FOR (@n = 1));

Les plans d'exécution semblent identiques à l'exception de l'insert d'une ligne.

Tout le temps supplémentaire semble être consommé par l'utilisation du processeur.

Pourquoi la INSERTdéclaration est-elle si lente?

Paul White réintègre Monica
la source

Réponses:

21

SQL Server choisit d'analyser les tables de segments de mémoire à l'intérieur des jointures de boucles à l'aide de verrous au niveau des lignes. Une analyse complète choisirait normalement le verrouillage au niveau de la page, mais une combinaison de la taille de la table et du prédicat signifie que le moteur de stockage choisit les verrous de ligne, car cela semble être la stratégie la moins chère.

La mauvaise estimation de la cardinalité a été délibérément introduite par le OPTIMIZE FORfait que les tas sont scannés beaucoup plus de fois que ne le prévoit l'optimiseur, et il n'introduit pas de bobine comme il le ferait normalement.

Cette combinaison de facteurs signifie que les performances sont très sensibles au nombre de verrous requis lors de l'exécution.

L' SELECTinstruction bénéficie d'une optimisation qui permet d'ignorer les verrous partagés au niveau de la ligne (en ne prenant que des verrous au niveau de la page partagés intentionnellement) lorsqu'il n'y a aucun danger de lire des données non validées et qu'il n'y a pas de données hors ligne.

L' INSERT...SELECTinstruction ne bénéficie pas de cette optimisation, de sorte que des millions de verrous RID sont pris et libérés chaque seconde dans le deuxième cas, ainsi que les verrous au niveau de la page partagés en intention.

L'énorme quantité d'activité de verrouillage explique le processeur supplémentaire et le temps écoulé.

La solution de contournement la plus naturelle consiste à garantir que l'optimiseur (et le moteur de stockage) obtiennent des estimations de cardinalité décentes afin qu'ils puissent faire de bons choix.

Si cela n'est pas pratique dans le cas d'utilisation réel, les instructions INSERTet SELECTpourraient être séparées, avec le résultat de la SELECTtenue dans une variable. Cela permettra à l' SELECTinstruction de bénéficier de l'optimisation du verrouillage des verrous.

La modification du niveau d'isolement peut également fonctionner, soit en ne prenant pas de verrous partagés, soit en s'assurant que l'escalade des verrous se déroule rapidement.

Comme dernier point d'intérêt, la requête peut être exécutée encore plus rapidement que le SELECTcas optimisé en forçant l'utilisation de spools à l'aide de l'indicateur de trace non documenté 8691.

Paul White réintègre Monica
la source