web-dev-qa-db-fra.com

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

É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'instruction SELECT 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: @The OPTIMIZE FOR est juste pour produire une repro de taille raisonnable qui capture les détails essentiels du problème réel, y compris une mauvaise estimation de la 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 l'instruction INSERT est-elle tellement plus lente?

18
Paul White 9

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é délibérément introduite par le OPTIMIZE FOR signifie que les tas sont analysés plusieurs fois plus 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'instruction SELECT bénéficie d'une optimisation qui permet les verrous partagés au niveau de la ligne doivent être ignorés (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 il n'y a pas de données hors ligne.

Le INSERT...SELECT l'instruction ne bénéficie pas de cette optimisation, donc 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 INSERT et SELECT peuvent être séparées, le résultat de SELECT étant conservé dans une variable. Cela permettra à l'instruction SELECT de bénéficier de l'optimisation de verrouillage de verrouillage.

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.

Enfin, la requête peut être exécutée encore plus rapidement que le cas optimisé SELECT en forçant l'utilisation de spools à l'aide de l'indicateur de trace non documenté 8691.

21
Paul White 9