Um aplicativo legado tem um trabalho noturno que chama repetidamente algum procedimento de armazenamento usando um TVP e passa em lotes de 10.000 ids que estão em ordem sequencial que ele precisa processar. Agora que os IDs estão na casa dos milhões, parece que esse processo está demorando visivelmente mais. Quase o mesmo número de chamadas em lote está sendo executado todas as noites, mas pelo perfil parecia que o procedimento estava ficando mais lento. Verificamos os culpados usuais, reconstruímos os índices e atualizamos as estatísticas nas tabelas em uso e tentamos fazer uma recompilação no procedimento. Mas nada corrigiu a regressão.
O procedimento faz algum processamento e retorna alguns resultados, cada um com uma cardinalidade de talvez 10.000 linhas. Um dos meus colegas olhou para ele e corrigiu a regressão de desempenho atualizando o procedimento de armazenamento simplesmente adicionando o seguinte ao topo da consulta:
select id into #t from @ids
e substituindo todos os usos de @ids
por #t
.
Fiquei impressionado com essa correção simples e estava tentando entendê-la mais. Tentei criar uma reprodução muito simples.
create table dbo.ids
(
id int primary key clustered,
timestamp
);
create type dbo.tvp as table(id int primary key clustered)
insert into dbo.ids(id)
select row_number() over (order by 1/0)
from string_split(space(1414),' ') a,string_split(space(1414),' ') b
go
create or alter procedure dbo.tvp_proc
(
@ids dbo.tvp readonly
)
as
begin
declare @_ int = 0, @r int = 5;
while(@r > 0)
select @_ = count(*), @r -= 1
from dbo.ids i
where exists (
select 1
from @ids t
where t.id = i.id
);
end
go
create or alter procedure dbo.temp_proc
(
@ids dbo.tvp readonly
)
as
begin
select * into #t from @ids
declare @_ int = 0, @r int = 5;
while(@r > 0)
select @_ = count(*), @r -= 1
from dbo.ids i
where exists (
select 1
from #t t
where t.id = i.id
);
end
E aqui está o meu benchmark simples.
set nocount on;
declare @s nvarchar(4000)=
'declare @ids tvp;
insert into @ids(id)
select @init + row_number() over (order by 1/0)
from string_split(space(99),char(32)) a,string_split(space(99),char(32)) b
declare @s datetime2 = sysutcdatetime()
create table #d(_ int)
insert into #d
exec dbo.tvp_proc @ids
print concat(right(concat(space(10),format(@init,''N0'')),10),char(9),datediff(ms, @s, sysutcdatetime()))',
@params nvarchar(20)=N'@init int'
print 'tvp result'
exec sp_executesql @s,@params,10000000
exec sp_executesql @s,@params,1000000
exec sp_executesql @s,@params,100000
exec sp_executesql @s,@params,10000
select @s=replace(@s,'tvp_proc','temp_proc')
print 'temp table result'
exec sp_executesql @s,@params,10000000
exec sp_executesql @s,@params,1000000
exec sp_executesql @s,@params,100000
exec sp_executesql @s,@params,10000
A execução deste benchmark na minha máquina produz os seguintes resultados:
tvp result
10,000,000 653
1,000,000 341
100,000 42
10,000 12
temp table result
10,000,000 52
1,000,000 60
100,000 57
10,000 59
Os resultados mostram que a abordagem tvp parece desacelerar à medida que os ids internos aumentam, enquanto a tabela temporária permanece bastante consistente. Alguém tem uma idéia de por que referenciar um tvp com valores maiores é mais lento que uma tabela temporária?
As variáveis de tabela , mesmo quando usadas como parâmetro ( TVP ), recebem estimativas de cardinalidade muito ruins , em oposição às tabelas temporárias , que são estimadas com muito mais precisão. Essa diferença é especialmente perceptível à medida que aumenta a quantidade de dados que estão sendo utilizados em uma TVP vs Temp Table . Se você observar atentamente o número estimado de linhas versus o número real de linhas nos planos de execução de cada implementação, verá que as tabelas temporárias são estimadas com muito mais precisão.
Você pode ler mais sobre TVPs e suas desvantagens neste post de Jeremiah Peschka . Especificamente a seção Gotchas :
Além disso, TVPs usados em procedimentos podem resultar em problemas de sniffing de parâmetros , como este outro post detalha. Esta citação adiciona alguns detalhes às estimativas de cardinalidade que você encontrará com TVPs , independentemente do tamanho da variável de tabela real :
Este é um bom artigo adicional sobre questões de estimativa de cardinalidade que resultam de Table Variables , de Pinal Dave.
Uma das principais razões pelas quais ter uma estimativa de cardinalidade ruim , como uma subestimativa nesse caso, é porque isso resultará no provisionamento insuficiente do SQL Engine dos recursos de servidor necessários para processar a consulta e fornecer os dados. Por exemplo, sua consulta provavelmente está solicitando muito menos memória do que precisa para ser processada devido à estimativa de baixa cardinalidade , fazendo com que o SQL Engine pense que há muito menos linhas a serem retornadas do que realmente haverá. Quanto maior for a variável da tabela , maior será a discrepância entre a estimativa e a real .
Você deve sempre optar por Temp Tables quando possível pelo motivo pelo qual elas têm muito mais benefícios de desempenho do que uma Table Variable e podem fazer quase tudo que uma Table Variable pode fazer e muito mais. Nos casos em que você precisa usar uma variável de tabela , selecioná-la primeiro em uma tabela temporária e usar essa tabela temporária em sua consulta subsequente é o caminho a seguir.