Uma função com valor de tabela com várias instruções retorna seu resultado em uma variável de tabela.
Esses resultados são sempre reutilizados ou a função é sempre totalmente avaliada toda vez que é chamada?
Uma função com valor de tabela com várias instruções retorna seu resultado em uma variável de tabela.
Esses resultados são sempre reutilizados ou a função é sempre totalmente avaliada toda vez que é chamada?
Respostas:
Os resultados de uma função com valor de tabela de múltiplas instruções (msTVF) nunca são armazenados em cache ou reutilizados entre instruções (ou conexões), mas existem algumas maneiras pelas quais um resultado de msTVF pode ser reutilizado na mesma instrução. Nessa medida, um msTVF não é necessariamente repovoado cada vez que é chamado.
Este (deliberadamente ineficiente) msTVF retorna um intervalo especificado de números inteiros, com um registro de data e hora em cada linha:
IF OBJECT_ID(N'dbo.IntegerRange', 'TF') IS NOT NULL
DROP FUNCTION dbo.IntegerRange;
GO
CREATE FUNCTION dbo.IntegerRange (@From integer, @To integer)
RETURNS @T table
(
n integer PRIMARY KEY,
ts datetime DEFAULT CURRENT_TIMESTAMP
)
WITH SCHEMABINDING
AS
BEGIN
WHILE @From <= @To
BEGIN
INSERT @T (n)
VALUES (@From);
SET @From = @From + 1;
END;
RETURN;
END;
Se todos os parâmetros para a chamada de função forem constantes (ou constantes de tempo de execução), o plano de execução preencherá o resultado da variável da tabela uma vez. O restante do plano pode acessar a variável da tabela várias vezes. A natureza estática da variável da tabela pode ser reconhecida no plano de execução. Por exemplo:
SELECT
IR.n,
IR.ts
FROM dbo.IntegerRange(1, 5) AS IR
ORDER BY
IR.n;
Retorna um resultado semelhante a:
O plano de execução é:
O operador Sequence primeiro chama o operador Table Valued Function, que preenche a variável da tabela (observe que este operador não retorna linhas). Em seguida, a Sequência chama sua segunda entrada, que retorna o conteúdo da variável da tabela (usando uma Varredura de Índice em Cluster neste caso).
A ideia de que o plano está usando um resultado de variável de tabela 'estática' é o operador Função com valor de tabela abaixo de uma sequência - a variável de tabela precisa ser totalmente preenchida uma vez antes que o restante do plano possa continuar.
Para mostrar o resultado da variável de tabela sendo acessado mais de uma vez, usaremos uma segunda tabela com linhas numeradas de 1 a 5:
IF OBJECT_ID(N'dbo.T', 'U') IS NOT NULL
DROP TABLE dbo.T;
CREATE TABLE dbo.T (i integer NOT NULL);
INSERT dbo.T (i)
VALUES (1), (2), (3), (4), (5);
E uma nova consulta que une essa tabela à nossa função (isso também pode ser escrito como um APPLY
):
SELECT T.i,
IR.n,
IR.ts
FROM dbo.T AS T
JOIN dbo.IntegerRange(1, 5) AS IR
ON IR.n = T.i;
O resultado é:
O plano de execução:
Como antes, a Sequência preenche o resultado da variável de tabela msTVF primeiro. Em seguida, loops aninhados são usados para associar cada linha da tabela T
a uma linha do resultado do msTVF. Como a definição da função incluiu um índice útil na variável da tabela, uma busca por índice pode ser usada.
O ponto principal é que, quando os parâmetros para o msTVF forem constantes (incluindo variáveis e parâmetros) ou tratados como constantes de tempo de execução para a instrução pelo mecanismo de execução, o plano apresentará dois operadores separados para o resultado da variável da tabela msTVF: um para preencher o tabela; outro para acessar os resultados, possivelmente acessando a tabela várias vezes e possivelmente fazendo uso dos índices declarados na definição da função.
Para destacar as diferenças quando parâmetros correlacionados (referências externas) ou parâmetros de função não constantes são usados, alteraremos o conteúdo da tabela T
para que a função tenha muito mais trabalho a fazer:
TRUNCATE TABLE dbo.T;
INSERT dbo.T (i)
VALUES (50001), (50002), (50003), (50004), (50005);
A consulta modificada a seguir agora usa uma referência externa para a tabela T
em um dos parâmetros de função:
SELECT T.i,
IR.n,
IR.ts
FROM dbo.T AS T
CROSS APPLY dbo.IntegerRange(1, T.i) AS IR
WHERE IR.n = T.i;
Essa consulta leva cerca de 8 segundos para retornar resultados como:
Observe a diferença de horário entre as linhas na coluna ts
. A WHERE
cláusula limita o resultado final para uma saída de tamanho sensato, mas a função ineficiente ainda leva um tempo para preencher a variável da tabela com 50.000 linhas ímpares (dependendo do valor correlacionado da i
tabela T
).
O plano de execução é:
Observe a falta de um operador Sequence. Agora, existe um único operador Table Valued Function que preenche a variável da tabela e retorna suas linhas em cada iteração da junção de loops aninhados.
Para ficar claro: com apenas 5 linhas na tabela T, o operador Função com valor de tabela é executado 5 vezes. Ele gera 50.001 linhas na primeira iteração, 50.002 na segunda ... e assim por diante. A variável de tabela é 'descartada' (truncada) entre iterações, portanto, cada uma das cinco chamadas é uma população completa. É por isso que é tão lento e cada linha leva aproximadamente o mesmo tempo para aparecer no resultado.
Notas laterais:
Naturalmente, o cenário acima é deliberadamente artificial para mostrar como o desempenho pode ser ruim quando o msTVF preenche muitas linhas em cada iteração.
Uma implementação sensata do código acima definiria ambos os parâmetros msTVF para i
e removeria a WHERE
cláusula redundante . A variável de tabela ainda seria truncada e preenchida novamente em cada iteração, mas apenas com uma linha de cada vez.
Também poderíamos buscar os i
valores mínimo e máximo T
e armazená-los em variáveis em uma etapa anterior. Chamar a função com variáveis em vez de parâmetros correlacionados permitiria que o padrão de variável da tabela 'estático' fosse usado como observado anteriormente.
Voltando a abordar a pergunta original mais uma vez, onde o padrão estático de Sequência não pode ser usado, o SQL Server pode evitar truncar e repovoar a variável de tabela msTVF se nenhum dos parâmetros correlatos tiver sido alterado desde a iteração anterior de uma junção de loop aninhada.
Para demonstrar isso, substituiremos o conteúdo de T
cinco valores idênticos i
:
TRUNCATE TABLE dbo.T;
INSERT dbo.T (i)
VALUES (50005), (50005), (50005), (50005), (50005);
A consulta com um parâmetro correlacionado novamente:
SELECT T.i,
IR.n,
IR.ts
FROM dbo.T AS T
CROSS APPLY dbo.IntegerRange(1, T.i) AS IR
WHERE IR.n = T.i;
Desta vez, os resultados aparecem em cerca de 1,5 segundos :
Observe os carimbos de data e hora idênticos em cada linha. O resultado armazenado em cache na variável de tabela é reutilizado para iterações subseqüentes em que o valor correlacionado i
é inalterado. Reutilizar o resultado é muito mais rápido do que inserir 50.005 linhas por vez.
O plano de execução é muito semelhante ao anterior:
A principal diferença é nos reais rebinds e rebobina reais propriedades do valor de tabela operador Função:
Quando os parâmetros correlatos não são alterados, o SQL Server pode reproduzir (retroceder) os resultados atuais na variável da tabela. Quando a correlação é alterada, o SQL Server deve truncar e preencher novamente a variável da tabela (religar). A única religação acontece na primeira iteração; as quatro iterações subseqüentes são todas rebobinadas, pois o valor de T.i
é inalterado.