Consegui reproduzir um problema de desempenho de consulta que descreveria como inesperado. Estou procurando uma resposta focada em internos.
Na minha máquina, a consulta a seguir faz uma verificação de índice clusterizado e leva cerca de 6,8 segundos de tempo de CPU:
SELECT ID1, ID2
FROM two_col_key_test WITH (FORCESCAN)
WHERE ID1 NOT IN
(
N'1', N'2',N'3', N'4', N'5',
N'6', N'7', N'8', N'9', N'10',
N'11', N'12',N'13', N'14', N'15',
N'16', N'17', N'18', N'19', N'20'
)
AND (ID1 = N'FILLER TEXT' AND ID2 >= N'' OR (ID1 > N'FILLER TEXT'))
ORDER BY ID1, ID2 OFFSET 12000000 ROWS FETCH FIRST 1 ROW ONLY
OPTION (MAXDOP 1);
A consulta a seguir faz uma busca de índice clusterizado (a única diferença é remover a FORCESCAN
dica), mas leva cerca de 18,2 segundos de tempo de CPU:
SELECT ID1, ID2
FROM two_col_key_test
WHERE ID1 NOT IN
(
N'1', N'2',N'3', N'4', N'5',
N'6', N'7', N'8', N'9', N'10',
N'11', N'12',N'13', N'14', N'15',
N'16', N'17', N'18', N'19', N'20'
)
AND (ID1 = N'FILLER TEXT' AND ID2 >= N'' OR (ID1 > N'FILLER TEXT'))
ORDER BY ID1, ID2 OFFSET 12000000 ROWS FETCH FIRST 1 ROW ONLY
OPTION (MAXDOP 1);
Os planos de consulta são bastante semelhantes. Para ambas as consultas, há 120000001 linhas lidas do índice clusterizado:
Estou no SQL Server 2017 CU 10. Aqui está o código para criar e preencher a two_col_key_test
tabela:
drop table if exists dbo.two_col_key_test;
CREATE TABLE dbo.two_col_key_test (
ID1 NVARCHAR(50) NOT NULL,
ID2 NVARCHAR(50) NOT NULL,
FILLER NVARCHAR(50),
PRIMARY KEY (ID1, ID2)
);
DROP TABLE IF EXISTS #t;
SELECT TOP (4000) 0 ID INTO #t
FROM master..spt_values t1
CROSS JOIN master..spt_values t2
OPTION (MAXDOP 1);
INSERT INTO dbo.two_col_key_test WITH (TABLOCK)
SELECT N'FILLER TEXT' + CASE WHEN ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) > 8000000 THEN N' 2' ELSE N'' END
, ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
, NULL
FROM #t t1
CROSS JOIN #t t2;
Espero uma resposta que faça mais do que relatórios de pilha de chamadas. Por exemplo, posso ver que sqlmin!TCValSSInRowExprFilter<231,0,0>::GetDataX
leva significativamente mais ciclos de CPU na consulta lenta em comparação com a rápida:
Em vez de parar por aí, gostaria de entender o que é isso e por que há uma diferença tão grande entre as duas consultas.
Por que há uma grande diferença no tempo de CPU para essas duas consultas?
O plano de varredura avalia o seguinte predicado não sargável (residual) enviado para cada linha:
O plano de busca faz duas operações de busca:
...para corresponder a esta parte do predicado:
Um predicado residual é aplicado a linhas que passam pelas condições de busca acima (todas as linhas em seu exemplo).
No entanto, cada desigualdade é substituída por dois testes separados para menor que
OR
maior que :Reescrevendo cada desigualdade, por exemplo:
...é contraproducente aqui. As comparações de string com reconhecimento de agrupamento são caras. Dobrar o número de comparações explica a maior parte da diferença no tempo de CPU que você vê.
Você pode ver isso mais claramente desabilitando o envio de predicados não sargáveis com o sinalizador de rastreamento não documentado 9130. Isso mostrará o resíduo como um filtro separado, com informações de desempenho que você pode inspecionar separadamente:
Isso também destacará a pequena estimativa incorreta de cardinalidade na busca, o que explica por que o otimizador escolheu a busca sobre a varredura em primeiro lugar (esperava que a parte de busca eliminasse algumas linhas).
Embora a reescrita da desigualdade possa tornar possível a correspondência de índices (possivelmente filtrada) (para fazer o melhor uso da capacidade de busca dos índices b-tree), seria melhor reverter essa expansão posteriormente se ambas as metades terminarem no resíduo. Você pode sugerir isso como uma melhoria no site de comentários do SQL Server .
Observe também que o modelo de estimativa de cardinalidade original ("herdado") seleciona uma varredura por padrão para essa consulta.