INSERT linha única… SELECT muito mais devagar que SELECT separado


18

Dada a seguinte tabela de heap com 400 linhas numeradas de 1 a 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;

e as seguintes configurações:

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

A SELECTdeclaração a seguir é concluída em cerca de 6 segundos ( demonstração , plano ):

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));

Nota: @A OPTIMIZE FORcláusula é apenas para produzir uma reprodução de tamanho sensato que captura os detalhes essenciais do problema real, incluindo uma estimativa de cardinalidade que pode surgir por vários motivos.

Quando a saída de linha única é gravada em uma tabela, leva 19 segundos ( demo , plano ):

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));

Os planos de execução parecem idênticos, além da inserção de uma linha.

Todo o tempo extra parece ser consumido pelo uso da CPU.

Por que a INSERTdeclaração é muito mais lenta?

Respostas:


21

O SQL Server opta por verificar as tabelas de heap na parte interna das associações de loops usando bloqueios no nível de linha. Uma varredura completa normalmente escolheria o bloqueio no nível da página, mas uma combinação do tamanho da tabela e do predicado significa que o mecanismo de armazenamento escolhe bloqueios de linha, pois essa parece ser a estratégia mais barata.

A erro de estimativa da cardinalidade introduzida deliberadamente pelos OPTIMIZE FORmeios que os heaps são verificados muito mais vezes do que o otimizador espera, e não introduz um spool como faria normalmente.

Essa combinação de fatores significa que o desempenho é muito sensível ao número de bloqueios necessários no tempo de execução.

A SELECTinstrução se beneficia de uma otimização que permite que os bloqueios compartilhados no nível da linha sejam ignorados (tendo apenas bloqueios no nível da página compartilhados por intenção) quando não há perigo de ler dados não confirmados e não há dados fora da linha.

A INSERT...SELECTinstrução não se beneficia dessa otimização; portanto, milhões de bloqueios RID são capturados e liberados a cada segundo no segundo caso, juntamente com os bloqueios no nível da página compartilhados por intenção.

A enorme quantidade de atividades de bloqueio é responsável pela CPU extra e pelo tempo decorrido.

A solução mais natural é garantir que o otimizador (e o mecanismo de armazenamento) obtenha estimativas de cardinalidade decentes para que possam fazer boas escolhas.

Se isso não for prático no caso de uso real, as instruções INSERTe SELECTpoderão ser separadas, com o resultado da SELECTretenção em uma variável. Isso permitirá que a SELECTdeclaração se beneficie da otimização de ignorar bloqueio.

A alteração do nível de isolamento também pode ser feita, não impedindo bloqueios compartilhados ou garantindo que a escalação de bloqueios ocorra rapidamente.

Como ponto final de interesse, a consulta pode ser executada ainda mais rapidamente que o SELECTcaso otimizado , forçando o uso de spools usando o sinalizador de rastreamento não documentado 8691.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.