Eu estava olhando o artigo aqui Tabelas temporárias versus variáveis de tabela e seus efeitos no desempenho do SQL Server e no SQL Server 2008 foi capaz de reproduzir resultados semelhantes aos mostrados lá em 2005.
Ao executar os procedimentos armazenados (definições abaixo) com apenas 10 linhas, a versão da variável da tabela executa a versão da tabela temporária mais de duas vezes.
Limpei o cache do procedimento e executei os dois procedimentos armazenados 10.000 vezes e repeti o processo por mais 4 execuções. Resultados abaixo (tempo em ms por lote)
T2_Time V2_Time
----------- -----------
8578 2718
6641 2781
6469 2813
6766 2797
6156 2719
Minha pergunta é: Qual o motivo do melhor desempenho da versão da variável de tabela?
Eu fiz alguma investigação. por exemplo, olhando os contadores de desempenho com
SELECT cntr_value
from sys.dm_os_performance_counters
where counter_name = 'Temp Tables Creation Rate';
confirma que nos dois casos os objetos temporários estão sendo armazenados em cache após a primeira execução, conforme o esperado vez de serem criados do zero novamente para cada chamada.
Da mesma forma traçando o Auto Stats
, SP:Recompile
, SQL:StmtRecompile
eventos no Profiler (imagem abaixo) mostra que esses eventos ocorrem apenas uma vez (na primeira chamada do #temp
procedimento armazenado tabela) e os outros 9.999 execuções não levantam qualquer um desses eventos. (A versão da variável de tabela não recebe nenhum desses eventos)
A sobrecarga um pouco maior da primeira execução do procedimento armazenado não pode, de forma alguma, explicar a grande diferença geral, no entanto, como ainda são necessários apenas alguns ms para limpar o cache do procedimento e executar os dois procedimentos uma vez, então não acredito em estatísticas ou recompilar pode ser a causa.
Criar objetos de banco de dados necessários
CREATE DATABASE TESTDB_18Feb2012;
GO
USE TESTDB_18Feb2012;
CREATE TABLE NUM
(
n INT PRIMARY KEY,
s VARCHAR(128)
);
WITH NUMS(N)
AS (SELECT TOP 1000000 ROW_NUMBER() OVER (ORDER BY $/0)
FROM master..spt_values v1,
master..spt_values v2)
INSERT INTO NUM
SELECT N,
'Value: ' + CONVERT(VARCHAR, N)
FROM NUMS
GO
CREATE PROCEDURE [dbo].[T2] @total INT
AS
CREATE TABLE #T
(
n INT PRIMARY KEY,
s VARCHAR(128)
)
INSERT INTO #T
SELECT n,
s
FROM NUM
WHERE n%100 > 0
AND n <= @total
DECLARE @res VARCHAR(128)
SELECT @res = MAX(s)
FROM NUM
WHERE n <= @total
AND NOT EXISTS(SELECT *
FROM #T
WHERE #T.n = NUM.n)
GO
CREATE PROCEDURE [dbo].[V2] @total INT
AS
DECLARE @V TABLE (
n INT PRIMARY KEY,
s VARCHAR(128))
INSERT INTO @V
SELECT n,
s
FROM NUM
WHERE n%100 > 0
AND n <= @total
DECLARE @res VARCHAR(128)
SELECT @res = MAX(s)
FROM NUM
WHERE n <= @total
AND NOT EXISTS(SELECT *
FROM @V V
WHERE V.n = NUM.n)
GO
Script de teste
SET NOCOUNT ON;
DECLARE @T1 DATETIME2,
@T2 DATETIME2,
@T3 DATETIME2,
@Counter INT = 0
SET @T1 = SYSDATETIME()
WHILE ( @Counter < 10000)
BEGIN
EXEC dbo.T2 10
SET @Counter += 1
END
SET @T2 = SYSDATETIME()
SET @Counter = 0
WHILE ( @Counter < 10000)
BEGIN
EXEC dbo.V2 10
SET @Counter += 1
END
SET @T3 = SYSDATETIME()
SELECT DATEDIFF(MILLISECOND,@T1,@T2) AS T2_Time,
DATEDIFF(MILLISECOND,@T2,@T3) AS V2_Time
fonte
#temp
tabela uma vez, apesar de serem limpas e preenchidas novamente 9.999 vezes depois disso.Respostas:
A saída de
SET STATISTICS IO ON
para ambos parece semelhanteDá
E, como Aaron aponta nos comentários, o plano para a versão da variável de tabela é realmente menos eficiente, enquanto ambos têm um plano de loops aninhados acionado por uma pesquisa de índice na versão
dbo.NUM
da#temp
tabela realiza uma pesquisa no índice[#T].n = [dbo].[NUM].[n]
com predicado residual,[#T].[n]<=[@total]
enquanto a variável da tabela version executa uma busca de índice@V.n <= [@total]
com predicado residual@V.[n]=[dbo].[NUM].[n]
e, portanto, processa mais linhas (é por isso que esse plano tem um desempenho tão ruim para um número maior de linhas)O uso de Eventos estendidos para examinar os tipos de espera para o spid específico fornece esses resultados para 10.000 execuções de
EXEC dbo.T2 10
e esses resultados para 10.000 execuções de
EXEC dbo.V2 10
Portanto, fica claro que o número de
PAGELATCH_SH
esperas é muito maior no#temp
caso da tabela. Não tenho conhecimento de nenhuma maneira de adicionar o recurso de espera ao rastreamento de eventos estendidos, portanto, para investigar isso, executeiEnquanto em outra conexão de pesquisa
sys.dm_os_waiting_tasks
Depois de deixar essa operação por cerca de 15 segundos, ela reuniu os seguintes resultados
Ambas as páginas bloqueadas pertencem a índices (diferentes) não agrupados na
tempdb.sys.sysschobjs
tabela base denominada'nc1'
e'nc2'
.A consulta
tempdb.sys.fn_dblog
durante as execuções indica que o número de registros de log adicionados pela primeira execução de cada procedimento armazenado era um tanto variável, mas para execuções subseqüentes o número adicionado por cada iteração era muito consistente e previsível. Depois que os planos de procedimento são armazenados em cache, o número de entradas de log é cerca da metade das necessárias para o#temp
versão.Examinando as entradas do log de transações com mais detalhes para a
#temp
versão da tabela do SP, cada chamada subsequente do procedimento armazenado cria três transações e a variável da tabela uma apenas duas.O
INSERT
/TVQUERY
transações são idênticas, exceto pelo nome. Ele contém os registros de log para cada uma das 10 linhas inseridas na tabela temporária ou na variável da tabela mais as entradasLOP_BEGIN_XACT
/LOP_COMMIT_XACT
.A
CREATE TABLE
transação aparece apenas no#Temp
versão e tem a seguinte aparência.A
FCheckAndCleanupCachedTempTable
transação aparece nos dois, mas possui 6 entradas adicionais na#temp
versão. Estas são as 6 linhas referentessys.sysschobjs
e têm exatamente o mesmo padrão que acima.Observando essas 6 linhas nas duas transações, elas correspondem às mesmas operações. A primeira
LOP_MODIFY_ROW, LCX_CLUSTERED
é uma atualização para amodify_date
coluna emsys.objects
. As cinco linhas restantes estão relacionadas à renomeação de objetos. Porquename
é uma coluna-chave dos dois NCIs afetados (nc1
enc2
), isso é realizado como uma exclusão / inserção para aqueles, então ele volta ao índice em cluster e o atualiza também.Parece que, para a
#temp
versão da tabela, quando o procedimento armazenado termina parte da limpeza realizada pelaFCheckAndCleanupCachedTempTable
transação, renomeie a tabela temporária de algo como#T__________________________________________________________________________________________________________________00000000E316
um nome interno diferente, como#2F4A0079
quando é inserida, aCREATE TABLE
transação renomeia a mesma. Esse nome de flip-flop pode ser visto em uma conexão executandodbo.T2
em um loop enquanto em outraResultados de exemplo
Portanto, uma explicação potencial para o diferencial de desempenho observado, como aludido por Alex, é que esse trabalho adicional é manter as tabelas do sistema
tempdb
responsáveis.Executando os dois procedimentos em um loop, o criador de perfil do Visual Studio Code revela o seguinte
A versão da variável da tabela gasta cerca de 60% do tempo executando a instrução insert e a seleção subsequente, enquanto a tabela temporária é menos da metade disso. Isso está alinhado com os tempos mostrados no OP e com a conclusão acima de que a diferença no desempenho se deve ao tempo gasto na execução de trabalhos auxiliares, não devido ao tempo gasto na própria execução da consulta.
As funções mais importantes que contribuem para os 75% "ausentes" na versão temporária da tabela são
Nas funções de criação e liberação, a função
CMEDProxyObject::SetName
é mostrada com um valor de amostra inclusivo de19.6%
. Pelo qual deduzo que 39,2% do tempo no caso de tabela temporária é ocupado com a renomeação descrita anteriormente.E os maiores na versão variável de tabela que contribuem para os outros 40% são
Perfil da tabela temporária
Perfil da variável de tabela
fonte
Disco Inferno
Como essa é uma pergunta mais antiga, decidi revisar o problema nas versões mais recentes do SQL Server para verificar se o mesmo perfil de desempenho ainda existe ou se as características foram alteradas.
Especificamente, a adição de tabelas de sistema na memória para o SQL Server 2019 parece uma ocasião interessante para testar novamente.
Estou usando um equipamento de teste um pouco diferente, desde que deparei com esse problema enquanto trabalhava em outra coisa.
Teste, teste
Usando a versão 2013 do Stack Overflow , eu tenho esse índice e esses dois procedimentos:
Índice:
Tabela temporária:
Variável de tabela:
Para evitar qualquer potencial espera do ASYNC_NETWORK_IO , estou usando procedimentos de wrapper.
SQL Server 2017
Como 2014 e 2016 são basicamente RELICS neste momento, estou iniciando meus testes com 2017. Além disso, por uma questão de brevidade, vou direto ao perfil do código com Perfview . Na vida real, observei esperas, fechos, spinlocks, bandeiras malucas e outras coisas.
A criação de perfil do código é a única coisa que revelou algo de interesse.
Diferença de tempo:
Ainda é uma diferença muito clara, não é? Mas o que o SQL Server está atingindo agora?
Observando os dois principais aumentos nas amostras diferenciadas, vemos
sqlmin
esqlsqllang!TCacheStore<CacheClockAlgorithm>::GetNextUserDataInHashBucket
somos os dois maiores infratores.A julgar pelos nomes nas pilhas de chamadas, limpar e renomear internamente as tabelas temporárias parece ser o maior tempo gasto na chamada da tabela temporária versus a chamada da variável da tabela.
Mesmo que as variáveis de tabela sejam apoiadas internamente por tabelas temporárias, isso não parece ser um problema.
Examinar as pilhas de chamadas para o teste de variável de tabela não mostra nenhum dos principais infratores:
SQL Server 2019 (baunilha)
Tudo bem, então isso ainda é um problema no SQL Server 2017, há algo diferente em 2019 pronto para uso?
Primeiro, para mostrar que não há nada na manga:
Diferença de tempo:
Ambos os procedimentos foram diferentes. A chamada da tabela temporária foi alguns segundos mais rápida e a chamada da variável da tabela foi cerca de 1,5 segundos mais lenta. A desaceleração da variável da tabela pode ser parcialmente explicada pela compilação adiada pela variável da tabela , uma nova opção de otimizador em 2019.
Olhando para o diff no Perfview, ele mudou um pouco - o sqlmin não está mais lá - mas
sqllang!TCacheStore<CacheClockAlgorithm>::GetNextUserDataInHashBucket
está.SQL Server 2019 (tabelas de sistema Tempdb na memória)
E essa novidade da tabela do sistema de memória? Hum? Sup com isso?
Vamos ligá-lo!
Observe que isso requer uma reinicialização do SQL Server para iniciar, então me desculpe enquanto eu reinicializo o SQL nesta adorável tarde de sexta-feira.
Agora as coisas parecem diferentes:
Diferença de tempo:
As tabelas temporárias foram cerca de 4 segundos melhores! Isso é algo.
Eu gosto de alguma coisa
Desta vez, o diff Perfview não é muito interessante. Lado a lado, é interessante observar a proximidade dos horários:
Um ponto interessante no diff são as chamadas para
hkengine!
, o que pode parecer óbvio, já que os recursos hekaton-ish estão em uso agora.Quanto aos dois itens principais no diff, eu não consigo entender muito
ntoskrnl!?
:Ou
sqltses!CSqlSortManager_80::GetSortKey
, mas eles estão aqui para Smrtr Ppl ™ olhar:Observe que há um documento não documentado e definitivamente não é seguro para produção, portanto, não o use sinalizador de rastreamento de inicialização que você pode usar para ter objetos adicionais do sistema da tabela temporária (sysrowsets, sysallocunits e sysseobjvalues) incluídos no recurso de memória, mas não fez uma diferença notável nos tempos de execução nesse caso.
Arredondar para cima
Mesmo nas versões mais recentes do SQL Server, as chamadas de alta frequência para variáveis de tabela são muito mais rápidas que as chamadas de alta frequência para tabelas temporárias.
Embora seja tentador culpar compilações, recompilações, estatísticas automáticas, travas, spinlocks, cache ou outros problemas, o problema ainda está claramente relacionado ao gerenciamento da limpeza da tabela temporária.
É uma chamada mais próxima no SQL Server 2019 com as tabelas do sistema na memória ativadas, mas as variáveis da tabela ainda apresentam melhor desempenho quando a frequência de chamadas é alta.
Obviamente, como um sábio vaping pensou: "use variáveis de tabela quando a escolha do plano não for um problema".
fonte