Estou tentando atualizar uma consulta que utiliza o IN
operador dentro de um WHERE
predicado de cláusula EXISTS
para comparar possíveis melhorias de desempenho e entender melhor o que está acontecendo nos bastidores quando os dois são trocados. É meu entendimento que na prática, o otimizador de consultas trata EXISTS
e IN
da mesma forma sempre que pode.
Estou percebendo que quando a consulta é executada com o IN
operador, ela retorna o conjunto de resultados desejado. No entanto, quando eu o substituo pelo EXISTS
equivalente, ele extrai todos os valores da tabela primária que quero filtrar. Ele está ignorando os valores de entrada fornecidos EXISTS (SELECT ...
e retornando todos os valores distintos possíveis.
O caso de uso é relativamente simples: a consulta aceita uma string delimitada por barra vertical @Series
que pode conter até 4 valores, por exemplo, S1|S2|S3|S4
ou S2|S4
. A partir daqui eu string_split
a entrada em uma variável de tabela @SeriesSplit
para determinar o interno correspondente [SeriesId]
para o arquivo [Series]
. O conjunto de resultados retornado é então filtrado para excluir os [Series]
que não foram passados.
Para ilustrar, aqui está uma definição de tabela semelhante:
DROP TABLE IF EXISTS [dbo].[Document]
IF OBJECT_ID('[dbo].[Document]', 'U') IS NULL
BEGIN
CREATE TABLE [dbo].[Document] (
[DocumentId] bigint IDENTITY(1,1) NOT NULL
,[DocumentSeriesId] [tinyint] NOT NULL
,CONSTRAINT [PK_Document] PRIMARY KEY CLUSTERED ([DocumentId] ASC)
,INDEX [IX_Document_SeriesId] NONCLUSTERED ([DocumentSeriesId] ASC)
);
END;
GO
Preencha a tabela de teste com dados fictícios.
SET IDENTITY_INSERT [dbo].[Document] ON;
;WITH [DocumentSeed] AS (
SELECT
1 AS [DocumentId]
UNION ALL
SELECT
[DocumentId] + 1
FROM
[DocumentSeed]
WHERE
[DocumentId] < 2048)
INSERT INTO [dbo].[Document] ([DocumentId], [DocumentSeriesId])
SELECT
[DocumentId]
,ABS(CHECKSUM(NEWID()) % 4) + 1
FROM
[DocumentSeed] OPTION (MAXRECURSION 2048);
SET IDENTITY_INSERT [dbo].[Document] OFF;
Primeiro, a consulta que utiliza o IN
operador e retorna os resultados desejados.
-- Specify the Document Series to be returned.
DECLARE @Series varchar(12) = 'S1|S2'
-- Split the user input and insert it into a table variable.
DECLARE @SeriesSplit table ([SeriesId] tinyint, [Series] varchar(2))
BEGIN
INSERT INTO @SeriesSplit ([SeriesId], [Series])
SELECT
CASE [value] WHEN 'S1' THEN 1 WHEN 'S2' THEN 2 WHEN 'S3' THEN 3 WHEN 'S4' THEN 4 ELSE 5 END AS [SeriesId]
,LTRIM(RTRIM([value])) AS [Series]
FROM
string_split(@Series,'|')
WHERE
[value] <> ''
END;
-- Return the result set of desired [DocumentSeriesId]
-- In the real use case, DISTINCT is not used and more columns are returned.
-- However, to illustrate the issue at hand, return only the [DocumentSeriesId] as this is what we are filtering off.
SELECT DISTINCT
D1.[DocumentSeriesId]
FROM
[dbo].[Document] D1
WHERE
D1.[DocumentSeriesId] IN (SELECT SS.[SeriesId] FROM @SeriesSplit SS)
As saídas duas linhas conforme desejado. O plano de execução (PasteThePlan) mostra-o executando um Distinct Sort
para filtrar as linhas apropriadas.
Se eu alterar a WHERE
cláusula para utilizar EXISTS
, sou recebido com todos os quatro resultados possíveis , embora tenha definido para retornar apenas dois.
-- Specify the Document Series to be returned.
DECLARE @Series varchar(14) = 'S1|S2'
-- Split the user input and insert it into a table variable.
DECLARE @SeriesSplit table ([SeriesId] tinyint, [Series] varchar(4))
BEGIN
INSERT INTO @SeriesSplit ([SeriesId], [Series])
SELECT
CASE [value] WHEN 'S1' THEN 1 WHEN 'S2' THEN 2 WHEN 'S3' THEN 3 WHEN 'S4' THEN 4 ELSE 5 END AS [SeriesId]
,LTRIM(RTRIM([value])) AS [Series]
FROM
string_split(@Series,'|')
WHERE
[value] <> ''
END;
-- Return the result set of desired [DocumentSeriesId]
-- In the real use case, DISTINCT is not used and more columns are returned.
-- However, to illustrate the issue at hand, return only the [DocumentSeriesId] as this is what we are filtering off.
SELECT DISTINCT
D1.[DocumentSeriesId]
FROM
[dbo].[Document] D1
WHERE
EXISTS (SELECT SS.[SeriesId] FROM @SeriesSplit SS JOIN [dbo].[Document] D2 ON SS.[SeriesId] = D2.[DocumentSeriesId])
O plano de execução (PasteThePlan) é muito diferente com essa pequena alteração. É lançar um aviso de que existe No Join Predicate
dentro do Left Semi Join
predicado. Foi minha suposição que WHERE EXISTS ...
satisfaria implicitamente esse argumento, como acontece com o WHERE [DocumentSeriesId] IN ...
. No entanto, dado que o Left Semi Join
operador retorna cada linha da entrada inicial quando há uma linha correspondente na segunda entrada e porque o No Join Predicate
aviso existe, o otimizador de consulta assume que cada linha é uma linha correspondente. Portanto, todas as operações em lote subsequentes são executadas em todas as 2.048 linhas da tabela.
O que posso analisar para interpretar melhor o que o plano de execução está descrevendo para que eu possa entender como resolver o problema adequadamente?
Ou, alternativamente, estou simplesmente perdendo o propósito do EXISTS
operador ao filtrar conjuntos de resultados com base em uma entrada estática?
Você não reescreveu a consulta corretamente. Ao converter uma
IN
subconsulta em umaEXISTS
, suaEXISTS
subconsulta deve ser correlacionada. Mas você definiu aDocument
tabela novamente na subconsulta, tornando-a não correlacionada.Sua consulta basicamente diz
Deveria ser: