Estou executando uma comparação de desempenho entre o uso de 1000 instruções INSERT:
INSERT INTO T_TESTS (TestId, FirstName, LastName, Age)
VALUES ('6f3f7257-a3d8-4a78-b2e1-c9b767cfe1c1', 'First 0', 'Last 0', 0)
INSERT INTO T_TESTS (TestId, FirstName, LastName, Age)
VALUES ('32023304-2e55-4768-8e52-1ba589b82c8b', 'First 1', 'Last 1', 1)
...
INSERT INTO T_TESTS (TestId, FirstName, LastName, Age)
VALUES ('f34d95a7-90b1-4558-be10-6ceacd53e4c4', 'First 999', 'Last 999', 999)
..versus usando uma única instrução INSERT com 1000 valores:
INSERT INTO T_TESTS (TestId, FirstName, LastName, Age)
VALUES
('db72b358-e9b5-4101-8d11-7d7ea3a0ae7d', 'First 0', 'Last 0', 0),
('6a4874ab-b6a3-4aa4-8ed4-a167ab21dd3d', 'First 1', 'Last 1', 1),
...
('9d7f2a58-7e57-4ed4-ba54-5e9e335fb56c', 'First 999', 'Last 999', 999)
Para minha grande surpresa, os resultados são o oposto do que eu pensava:
- 1000 instruções INSERT: 290 mseg.
- 1 instrução INSERT com 1000 VALUES: 2800 mseg.
O teste é executado diretamente no MSSQL Management Studio com o SQL Server Profiler usado para medição (e obtive resultados semelhantes executando-o em código C # usando SqlClient, o que é ainda mais surpreendente considerando todas as viagens de ida e volta das camadas DAL)
Isso pode ser razoável ou de alguma forma explicado? Como é que um método supostamente mais rápido resulta em um desempenho 10 vezes (!) Pior ?
Obrigado.
EDIT: Anexando planos de execução para ambos:
Respostas:
Seu plano mostra que as inserções individuais estão usando procedimentos parametrizados (possivelmente parametrizados automaticamente), então o tempo de análise / compilação para eles deve ser mínimo.
Pensei em analisar isso um pouco mais, então configure um loop ( script ) e tente ajustar o número de
VALUES
cláusulas e registrar o tempo de compilação.Em seguida, dividi o tempo de compilação pelo número de linhas para obter o tempo médio de compilação por cláusula. Os resultados estão abaixo
Até 250
VALUES
cláusulas presentes, o tempo de compilação / número de cláusulas tem uma leve tendência de aumento, mas nada muito dramático.Mas então ocorre uma mudança repentina.
Essa seção dos dados é mostrada abaixo.
O tamanho do plano em cache, que vinha crescendo linearmente, cai de repente, mas o CompileTime aumenta 7 vezes e o CompileMemory dispara. Este é o ponto de corte entre o plano ser auto parametrizado (com 1.000 parâmetros) e um não parametrizado. Depois disso, parece se tornar linearmente menos eficiente (em termos de número de cláusulas de valor processadas em um determinado tempo).
Não sei por que isso deveria ser. Presumivelmente, ao compilar um plano para valores literais específicos, ele deve realizar alguma atividade que não seja escalonada linearmente (como classificação).
Não parece afetar o tamanho do plano de consulta em cache quando tentei uma consulta consistindo inteiramente em linhas duplicadas e nem afeta a ordem de saída da tabela das constantes (e como você está inserindo em um heap, o tempo gasto na classificação seria inútil de qualquer maneira, mesmo que fizesse).
Além disso, se um índice clusterizado for adicionado à tabela, o plano ainda mostra uma etapa de classificação explícita, portanto, não parece estar classificando em tempo de compilação para evitar uma classificação em tempo de execução.
Tentei ver isso em um depurador, mas os símbolos públicos para minha versão do SQL Server 2008 não parecem estar disponíveis, então, em vez disso, tive que olhar para a
UNION ALL
construção equivalente no SQL Server 2005.Um rastreamento de pilha típico está abaixo
Portanto, ir dos nomes no rastreamento de pilha parece levar muito tempo comparando strings.
Este artigo da base de conhecimento indica que
DeriveNormalizedGroupProperties
está associado ao que costumava ser chamado de estágio de normalização do processamento de consultaEste estágio agora é chamado de ligação ou algebrização e leva a saída da árvore de análise de expressão do estágio de análise anterior e produz uma árvore de expressão algbrizada (árvore do processador de consulta) para avançar para a otimização (otimização de plano trivial neste caso) [ref] .
Eu tentei mais um experimento ( Script ) que era repetir o teste original, mas olhando para três casos diferentes.
Pode-se ver claramente que quanto mais longas as strings, piores as coisas ficam e que, inversamente, quanto mais duplicatas, melhor as coisas ficam. Como mencionado anteriormente, as duplicatas não afetam o tamanho do plano em cache, então presumo que deve haver um processo de identificação de duplicatas ao construir a própria árvore de expressão algbrizada.
Editar
Um lugar onde essas informações são aproveitadas é mostrado por @Lieven aqui
Como em tempo de compilação, ele pode determinar que a
Name
coluna não tem duplicatas, ele pula a ordenação pela1/ (ID - ID)
expressão secundária em tempo de execução (a classificação no plano tem apenas umaORDER BY
coluna) e nenhum erro de divisão por zero é gerado. Se duplicatas forem adicionadas à tabela, o operador de classificação mostrará duas ordens por colunas e o erro esperado será gerado.fonte
<ParameterList>
que um com uma<ConstantScan><Values><Row>
lista.<ConstantScan><Values><Row>
vez de<ParameterList>
.Não é muito surpreendente: o plano de execução para a pequena inserção é calculado uma vez e, em seguida, reutilizado 1000 vezes. Analisar e preparar o plano é rápido, porque ele tem apenas quatro valores para delinear. Um plano de 1000 linhas, por outro lado, precisa lidar com 4000 valores (ou 4000 parâmetros se você parametrizou seus testes C #). Isso poderia consumir facilmente a economia de tempo que você ganha ao eliminar 999 viagens de ida e volta para o SQL Server, especialmente se sua rede não for excessivamente lenta.
fonte
O problema provavelmente está relacionado ao tempo que leva para compilar a consulta.
Se você quiser acelerar as inserções, o que realmente precisa fazer é envolvê-las em uma transação:
No C #, você também pode considerar o uso de um parâmetro com valor de tabela. A emissão de vários comandos em um único lote, separando-os com ponto e vírgula, é outra abordagem que também ajudará.
fonte
Eu me deparei com uma situação semelhante ao tentar converter uma tabela com várias linhas de 100k com um programa C ++ (MFC / ODBC).
Como essa operação demorou muito, imaginei agrupar várias inserções em uma (até 1000 devido às limitações do MSSQL ). Meu palpite de que muitas instruções de inserção simples criariam uma sobrecarga semelhante ao que é descrito aqui .
No entanto, descobriu-se que a conversão demorou um pouco mais:
Portanto, 1000 chamadas únicas para CDatabase :: ExecuteSql, cada uma com uma única instrução INSERT (método 1), são quase duas vezes mais rápidas do que uma única chamada para CDatabase :: ExecuteSql com uma instrução INSERT de várias linhas com 1000 tuplas de valor (método 2).
Atualização: Então, a próxima coisa que tentei foi agrupar 1000 instruções INSERT separadas em uma única string e fazer com que o servidor executasse isso (método 3). Acontece que isso é um pouco mais rápido do que o método 1.
Editar: Estou usando o Microsoft SQL Server Express Edition (64 bits) v10.0.2531.0
fonte