A saída de SET STATISTICS IO ON
para ambos parece semelhante
SET STATISTICS IO ON;
PRINT 'V2'
EXEC dbo.V2 10
PRINT 'T2'
EXEC dbo.T2 10
Dá
V2
Table '#58B62A60'. Scan count 0, logical reads 20
Table 'NUM'. Scan count 1, logical reads 3
Table '#58B62A60'. Scan count 10, logical reads 20
Table 'NUM'. Scan count 1, logical reads 3
T2
Table '#T__ ... __00000000E2FE'. Scan count 0, logical reads 20
Table 'NUM'. Scan count 1, logical reads 3
Table '#T__ ... __00000000E2FE'. Scan count 0, logical reads 20
Table 'NUM'. Scan count 1, logical reads 3
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 deEXEC dbo.T2 10
+---------------------+------------+----------------+----------------+----------------+
| | | Total | Total Resource | Total Signal |
| Wait Type | Wait Count | Wait Time (ms) | Wait Time (ms) | Wait Time (ms) |
+---------------------+------------+----------------+----------------+----------------+
| SOS_SCHEDULER_YIELD | 16 | 19 | 19 | 0 |
| PAGELATCH_SH | 39998 | 14 | 0 | 14 |
| PAGELATCH_EX | 1 | 0 | 0 | 0 |
+---------------------+------------+----------------+----------------+----------------+
e esses resultados para 10.000 execuções de EXEC dbo.V2 10
+---------------------+------------+----------------+----------------+----------------+
| | | Total | Total Resource | Total Signal |
| Wait Type | Wait Count | Wait Time (ms) | Wait Time (ms) | Wait Time (ms) |
+---------------------+------------+----------------+----------------+----------------+
| PAGELATCH_EX | 2 | 0 | 0 | 0 |
| PAGELATCH_SH | 1 | 0 | 0 | 0 |
| SOS_SCHEDULER_YIELD | 676 | 0 | 0 | 0 |
+---------------------+------------+----------------+----------------+----------------+
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, executei
WHILE 1=1
EXEC dbo.T2 10
Enquanto em outra conexão de pesquisa sys.dm_os_waiting_tasks
CREATE TABLE #T(resource_description NVARCHAR(2048))
WHILE 1=1
INSERT INTO #T
SELECT resource_description
FROM sys.dm_os_waiting_tasks
WHERE session_id=<spid_of_other_session> and wait_type='PAGELATCH_SH'
Depois de deixar essa operação por cerca de 15 segundos, ela reuniu os seguintes resultados
+-------+----------------------+
| Count | resource_description |
+-------+----------------------+
| 1098 | 2:1:150 |
| 1689 | 2:1:146 |
+-------+----------------------+
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.
+-----------------+----------------+------------+
| | Table Variable | Temp Table |
+-----------------+----------------+------------+
| First Run | 126 | 72 or 136 |
| Subsequent Runs | 17 | 32 |
+-----------------+----------------+------------+
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.
+---------------------------------+----+---------------------------------+----+
| #Temp Table | @Table Variable |
+---------------------------------+----+---------------------------------+----+
| CREATE TABLE | 9 | | |
| INSERT | 12 | TVQuery | 12 |
| FCheckAndCleanupCachedTempTable | 11 | FCheckAndCleanupCachedTempTable | 5 |
+---------------------------------+----+---------------------------------+----+
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 entradas LOP_BEGIN_XACT
/ LOP_COMMIT_XACT
.
A CREATE TABLE
transação aparece apenas no#Temp
versão e tem a seguinte aparência.
+-----------------+-------------------+---------------------+
| Operation | Context | AllocUnitName |
+-----------------+-------------------+---------------------+
| LOP_BEGIN_XACT | LCX_NULL | |
| LOP_SHRINK_NOOP | LCX_NULL | |
| LOP_MODIFY_ROW | LCX_CLUSTERED | sys.sysschobjs.clst |
| LOP_DELETE_ROWS | LCX_MARK_AS_GHOST | sys.sysschobjs.nc1 |
| LOP_INSERT_ROWS | LCX_INDEX_LEAF | sys.sysschobjs.nc1 |
| LOP_DELETE_ROWS | LCX_MARK_AS_GHOST | sys.sysschobjs.nc2 |
| LOP_INSERT_ROWS | LCX_INDEX_LEAF | sys.sysschobjs.nc2 |
| LOP_MODIFY_ROW | LCX_CLUSTERED | sys.sysschobjs.clst |
| LOP_COMMIT_XACT | LCX_NULL | |
+-----------------+-------------------+---------------------+
A FCheckAndCleanupCachedTempTable
transação aparece nos dois, mas possui 6 entradas adicionais na #temp
versão. Estas são as 6 linhas referentes sys.sysschobjs
e têm exatamente o mesmo padrão que acima.
+-----------------+-------------------+----------------------------------------------+
| Operation | Context | AllocUnitName |
+-----------------+-------------------+----------------------------------------------+
| LOP_BEGIN_XACT | LCX_NULL | |
| LOP_DELETE_ROWS | LCX_NONSYS_SPLIT | dbo.#7240F239.PK__#T________3BD0199374293AAB |
| LOP_HOBT_DELTA | LCX_NULL | |
| LOP_HOBT_DELTA | LCX_NULL | |
| LOP_MODIFY_ROW | LCX_CLUSTERED | sys.sysschobjs.clst |
| LOP_DELETE_ROWS | LCX_MARK_AS_GHOST | sys.sysschobjs.nc1 |
| LOP_INSERT_ROWS | LCX_INDEX_LEAF | sys.sysschobjs.nc1 |
| LOP_DELETE_ROWS | LCX_MARK_AS_GHOST | sys.sysschobjs.nc2 |
| LOP_INSERT_ROWS | LCX_INDEX_LEAF | sys.sysschobjs.nc2 |
| LOP_MODIFY_ROW | LCX_CLUSTERED | sys.sysschobjs.clst |
| LOP_COMMIT_XACT | LCX_NULL | |
+-----------------+-------------------+----------------------------------------------+
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 a modify_date
coluna em sys.objects
. As cinco linhas restantes estão relacionadas à renomeação de objetos. Porque name
é 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 pela FCheckAndCleanupCachedTempTable
transação, renomeie a tabela temporária de algo como #T__________________________________________________________________________________________________________________00000000E316
um nome interno diferente, como #2F4A0079
quando é inserida, a CREATE TABLE
transação renomeia a mesma. Esse nome de flip-flop pode ser visto em uma conexão executando dbo.T2
em um loop enquanto em outra
WHILE 1=1
SELECT name, object_id, create_date, modify_date
FROM tempdb.sys.objects
WHERE name LIKE '#%'
Resultados 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
+-------------------------------+--------------------+-------+-----------+
| Function | Explanation | Temp | Table Var |
+-------------------------------+--------------------+-------+-----------+
| CXStmtDML::XretExecute | Insert ... Select | 16.93 | 37.31 |
| CXStmtQuery::ErsqExecuteQuery | Select Max | 8.77 | 23.19 |
+-------------------------------+--------------------+-------+-----------+
| Total | | 25.7 | 60.5 |
+-------------------------------+--------------------+-------+-----------+
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
+------------------------------------+-------------------+
| Function | Inclusive Samples |
+------------------------------------+-------------------+
| CXStmtCreateTableDDL::XretExecute | 26.26% |
| CXStmtDDL::FinishNormalImp | 4.17% |
| TmpObject::Release | 27.77% |
+------------------------------------+-------------------+
| Total | 58.20% |
+------------------------------------+-------------------+
Nas funções de criação e liberação, a função CMEDProxyObject::SetName
é mostrada com um valor de amostra inclusivo de 19.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
+-----------------------------------+-------------------+
| Function | Inclusive Samples |
+-----------------------------------+-------------------+
| CTableCreate::LCreate | 7.41% |
| TmpObject::Release | 12.87% |
+-----------------------------------+-------------------+
| Total | 20.28% |
+-----------------------------------+-------------------+
Perfil da tabela temporária
Perfil da variável de tabela
#temp
tabela uma vez, apesar de serem limpas e preenchidas novamente 9.999 vezes depois disso.