Estimativa de cardinalidade ruim desqualifica INSERT do registro mínimo?

11

Por que a segunda INSERTdeclaração é ~ 5x mais lenta que a primeira?

Da quantidade de dados de log gerados, acho que o segundo não está qualificado para o log mínimo. No entanto, a documentação no Guia de desempenho de carregamento de dados indica que ambas as inserções devem poder ser minimamente registradas. Portanto, se o registro mínimo é a principal diferença de desempenho, por que a segunda consulta não se qualifica para o registro mínimo? O que pode ser feito para melhorar a situação?


Consulta nº 1: Inserindo linhas de 5MM usando INSERT ... WITH (TABLOCK)

Considere a seguinte consulta, que insere linhas de 5MM em um heap. Essa consulta é executada 1 seconde gera 64MBdados do log de transações, conforme relatado por sys.dm_tran_database_transactions.

CREATE TABLE dbo.minimalLoggingTest (n INT NOT NULL)
GO
INSERT INTO dbo.minimalLoggingTest WITH (TABLOCK) (n)
SELECT n
-- Any table/view/sub-query that correctly estimates that it will generate 5MM rows
FROM dbo.fiveMillionNumbers
-- Provides greater consistency on my laptop, where other processes are running
OPTION (MAXDOP 1)
GO


Consulta nº 2: inserindo os mesmos dados, mas o SQL subestima o número de linhas

Agora, considere essa consulta muito semelhante, que opera exatamente nos mesmos dados, mas é extraída de uma tabela (ou SELECTdeclaração complexa com muitas junções no meu caso de produção atual) em que a estimativa de cardinalidade é muito baixa. Essa consulta é executada 5.5 secondse gera 461MBdados do log de transações.

CREATE TABLE dbo.minimalLoggingTest (n INT NOT NULL)
GO
INSERT INTO dbo.minimalLoggingTest WITH (TABLOCK) (n)
SELECT n
-- Any table/view/sub-query that produces 5MM rows but SQL estimates just 1000 rows
FROM dbo.fiveMillionNumbersBadEstimate
-- Provides greater consistency on my laptop, where other processes are running
OPTION (MAXDOP 1)
GO


Script completo

Consulte este Pastebin para obter um conjunto completo de scripts para gerar os dados de teste e executar um desses cenários. Observe que você deve usar um banco de dados que esteja no SIMPLE modelo de recuperação .


Contexto empresarial

Estamos semi-frequentemente movendo milhões de linhas de dados e é importante que essas operações sejam o mais eficientes possível, tanto em termos de tempo de execução quanto de carga de E / S do disco. Inicialmente, tínhamos a impressão de que criar uma tabela de heap e usá-la INSERT...WITH (TABLOCK)era uma boa maneira de fazer isso, mas agora nos tornamos menos confiantes, pois observamos a situação demonstrada acima em um cenário de produção real (embora com consultas mais complexas, não as versão simplificada aqui).

Geoff Patterson
fonte

Respostas:

7

Por que a segunda consulta não se qualifica para o log mínimo?

O log mínimo está disponível para a segunda consulta, mas o mecanismo escolhe não usá-lo em tempo de execução.

Há um limite mínimo para INSERT...SELECTabaixo do qual ele escolhe não usar as otimizações de carga a granel. Há um custo envolvido na configuração de uma operação de conjunto de linhas em massa e a inserção em massa de apenas algumas linhas não resultaria em uma utilização eficiente do espaço.

O que pode ser feito para melhorar a situação?

Use um dos muitos outros métodos (por exemplo SELECT INTO) que não possuem esse limite. Como alternativa, você poderá reescrever a consulta de origem de alguma forma para aumentar o número estimado de linhas / páginas acima do limite de INSERT...SELECT.

Veja também a resposta automática de Geoff para obter informações mais úteis.


Curiosidades possivelmente interessantes: SET STATISTICS IO relata leituras lógicas para a tabela de destino somente quando otimizações de carregamento em massa não são usadas .

Paul White 9
fonte
5

Consegui recriar o problema com meu próprio equipamento de teste:

USE test;

CREATE TABLE dbo.SourceGood
(
    SourceGoodID INT NOT NULL
        CONSTRAINT PK_SourceGood
        PRIMARY KEY CLUSTERED
        IDENTITY(1,1)
    , SomeData VARCHAR(384) NOT NULL
);

CREATE TABLE dbo.SourceBad
(
    SourceBadID INT NOT NULL
        CONSTRAINT PK_SourceBad
        PRIMARY KEY CLUSTERED
        IDENTITY(-2147483647,1)
    , SomeData VARCHAR(384) NOT NULL
);

CREATE TABLE dbo.InsertTest
(
    SourceBadID INT NOT NULL
        CONSTRAINT PK_InsertTest
        PRIMARY KEY CLUSTERED
    , SomeData VARCHAR(384) NOT NULL
);
GO

INSERT INTO dbo.SourceGood WITH (TABLOCK) (SomeData) 
SELECT TOP(5000000) o.name + o1.name + o2.name
FROM syscolumns o
    , syscolumns o1
    , syscolumns o2;
GO

ALTER DATABASE test SET AUTO_UPDATE_STATISTICS OFF;
GO

INSERT INTO dbo.SourceBad WITH (TABLOCK) (SomeData)
SELECT TOP(5000000) o.name + o1.name + o2.name
FROM syscolumns o
    , syscolumns o1
    , syscolumns o2;
GO

ALTER DATABASE test SET AUTO_UPDATE_STATISTICS ON;
GO

BEGIN TRANSACTION;

INSERT INTO dbo.InsertTest WITH (TABLOCK)
SELECT *
FROM dbo.SourceGood;

SELECT * FROM sys.dm_tran_database_transactions;

/*
database_transaction_log_record_count
472 
database_transaction_log_bytes_used
692136
*/

COMMIT TRANSACTION;


BEGIN TRANSACTION;

INSERT INTO dbo.InsertTest WITH (TABLOCK)
SELECT *
FROM dbo.SourceBad;

SELECT * FROM sys.dm_tran_database_transactions;

/*
database_transaction_log_record_count   
5000003 
database_transaction_log_bytes_used
642699256
*/

COMMIT TRANSACTION;

Isso levanta a questão: por que não "consertar" o problema atualizando as estatísticas nas tabelas de origem antes de executar a operação minimamente registrada?

TRUNCATE TABLE dbo.InsertTest;
UPDATE STATISTICS dbo.SourceBad;

BEGIN TRANSACTION;

INSERT INTO dbo.InsertTest WITH (TABLOCK)
SELECT *
FROM dbo.SourceBad;

SELECT * FROM sys.dm_tran_database_transactions;

/*
database_transaction_log_record_count
472
database_transaction_log_bytes_used
692136
*/

COMMIT TRANSACTION;
Max Vernon
fonte
2
No código real, há uma SELECTdeclaração complexa com inúmeras associações que gera o conjunto de resultados para o INSERT. Essas junções produzem estimativas de cardinalidade ruins para o operador de inserção da mesa final (que eu simulei no script de reprodução por meio de uma UPDATE STATISTICSchamada incorreta ) e, portanto, não é tão simples como emitir um UPDATE STATISTICScomando para corrigir o problema. Concordo plenamente que simplificar a consulta para facilitar o entendimento do Cardinality Estimator pode ser uma boa abordagem, mas não é um procedimento trivial implementar uma lógica de negócios complexa.
Geoff Patterson
Não tenho uma instância do SQL Server 2014 para testar isso, no entanto, a identificação de problemas no Estimador de nova cardinalidade do SQL Server 2014 e o aprimoramento do Service Pack 1 falam sobre a ativação do sinalizador de rastreamento 4199, entre outros, para ativar o novo estimador de cardinalidade. Você já tentou isso?
Max Vernon
Boa ideia, mas não ajudou. Eu apenas tentei o TF 4199, o TF 610 (afrouxa as condições mínimas de registro) e os dois juntos (ei, por que não?), Mas nenhuma alteração para a 2ª consulta de teste.
Geoff Patterson
4

Reescreva a consulta de origem de alguma forma para aumentar o número estimado de linhas

Expandindo a ideia de Paul, uma solução alternativa se você estiver realmente desesperado é adicionar uma tabela fictícia que garanta que o número estimado de linhas para a inserção seja alto o suficiente para obter otimizações de carregamento em massa. Confirmei que isso gera log mínimo e melhora o desempenho da consulta.

-- Create a dummy table that SQL Server thinks has a million rows
CREATE TABLE dbo.emptyTableWithMillionRowEstimate (
    n INT PRIMARY KEY
)
GO
UPDATE STATISTICS dbo.emptyTableWithMillionRowEstimate
WITH ROWCOUNT = 1000000
GO

-- Concatenate this table into the final rowset:
INSERT INTO dbo.minimalLoggingTest WITH (TABLOCK) (n)
SELECT n
-- Any table/view/sub-query that correctly estimates that it will generate 5MM rows
FROM dbo.fiveMillionNumbersBadEstimate
-- Add in dummy rowset to ensure row estimate is high enough for bulk load optimization
UNION ALL
SELECT NULL FROM dbo.emptyTableWithMillionRowEstimate
OPTION (MAXDOP 1)

Conclusões finais

  1. Use SELECT...INTOpara operações de inserção única se for necessário um registro mínimo. Como Paul aponta, isso garantirá o registro mínimo, independentemente da estimativa de linha
  2. Sempre que possível, escreva consultas de uma maneira simples que o otimizador de consultas possa analisar com eficácia. Pode ser possível dividir uma consulta em várias partes, por exemplo, para permitir que as estatísticas sejam construídas em uma tabela intermediária.
  3. Se você tiver acesso ao SQL Server 2014, experimente-o na sua consulta; no meu caso de produção real, tentei e o novo Cardinality Estimator produziu uma estimativa muito mais alta (e melhor); a consulta foi minimamente registrada. Mas isso pode não ser útil se você precisar dar suporte ao SQL 2012 e versões anteriores.
  4. Se você estiver desesperado, soluções hacky como esta podem ser aplicadas!

Um artigo relacionado

A publicação do blog de Paul White em maio de 2019 Minimal Logging with INSERT… SELECT into Heap Tables cobre algumas dessas informações com mais detalhes.

Geoff Patterson
fonte