Tenho uma consulta no seguinte formato:
IF EXISTS (
SELECT 1
FROM (
SELECT RowID, OETID
FROM @InMemoryTableTypeTable i
UNION
SELECT RowID, OETID
FROM @InMemoryTableTypeTable d
) AS t
WHERE NOT EXISTS (
SELECT 1
FROM dbo.MyTable m WITH(FORCESEEK, ROWLOCK, UPDLOCK)
WHERE (m.OETID = t.RowID)
AND (m.SRID = t.OETID)
AND (m.WTID = @WTID)
AND (m.Status <> 1)
AND (m.SRID > 0)
)
)
...
A definição de dbo.MyTable
é:
CREATE TABLE [dbo].[MyTable](
[ID] [bigint] IDENTITY(1,1) NOT NULL,
[RowGUID] [uniqueidentifier] ROWGUIDCOL NOT NULL,
[WTID] [bigint] NOT NULL,
[OETID] [int] NOT NULL,
[SRID] [bigint] NOT NULL,
[Status] [tinyint] NOT NULL,
CONSTRAINT [PK_MyTable] PRIMARY KEY CLUSTERED
(
[ID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]
GO
CREATE UNIQUE NONCLUSTERED INDEX [IDX] ON [dbo].[MyTable]
(
[WTID] ASC,
[OETID] ASC,
[SRID] ASC
)
INCLUDE([Status])
WHERE ([SRID]>(0))
WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
GO
ALTER TABLE [dbo].[MyTable] ADD CONSTRAINT [DF_MyTable_RowGUID] DEFAULT (NEWID()) FOR [RowGUID]
GO
A definição de @InMemoryTableTypeTable
é
CREATE TYPE [dbo].[TableType] AS TABLE(
[ID] [bigint] NOT NULL,
[RowID] [int] NOT NULL,
[OETID] [int] NOT NULL,
PRIMARY KEY NONCLUSTERED
(
[ID] ASC
)
)
WITH ( MEMORY_OPTIMIZED = ON )
GO
A tabela MyTable
contém aproximadamente 500 mil linhas e possui um índice filtrado exclusivo que possui:
WTID
,OETID
eSRID
como chaves nessa ordem- um filtro onde
SRID
> 0 Status
como uma coluna incluída
Isso significa que a EXISTS
instrução é SARGable.
No entanto, dependendo de quantos registros estão presentes @InMemoryTableTypeTable
e do humor que o SQL Server parece estar, às vezes a busca do índice apenas buscará WTID
e empurrará o restante da predicação para o Left Anti Semi Join. Se isso acontecer e a memória do próprio SQL Server estiver sob pressão, a consulta poderá permanecer lá por cerca de 20 minutos. Para alguns valores, @WTID
pode haver 1 linha ou 200k que acabaram de ser inseridos anteriormente na mesma sessão.
Aqui está o bom plano: https://www.brentozar.com/pastetheplan/?id=H1-V_Jz7R
Aqui está o plano ruim: https://www.brentozar.com/pastetheplan/?id=SJD-QZGQA
Existe uma maneira de forçar o SQL Server a aplicar a predicação a todas as três colunas na busca de índice sempre?
Eu tentei quebrar isso do IF e usar as dicas OPTIMIZE FOR UNKNOWN
e OPTIMIZE FOR (@WTID UNKNOWN)
sem sucesso.
A busca é mais por simultaneidade: as leituras e gravações nessa tabela para cada sessão serão segregadas pelo WTID. No entanto, remover essas dicas da tabela não faz diferença, ele sempre verifica t e procura m, é a posição da predicação OETID e SRID que parece fazer a diferença.
Esta postagem As linhas reais e estimadas diferem muito e me levou à ASSUME_MIN_SELECTIVITY_FOR_FILTER_ESTIMATES
dica que produz o plano que desejo (na maioria das vezes) junto com RECOMPILE
. Combinar isso com FORCE_LEGACY_CARDINALITY_ESTIMATION
reverte para o plano “errado”.
Solução
Você pode usar uma
FORCESEEK
dica expandida com as teclas de busca necessárias:Por exemplo:
Explicação
Como você percebeu, a causa é a estimativa de custos. As consultas introduzidas por
EXISTS
vêm com uma linha goal , o que complica as coisas. Juntamente com uma consulta que contém vários outros recursos difíceis de estimar, esse é o tipo de situação em que uma dica pode ser necessária para obter consistentemente o formato do plano desejado.Custeio e exploração
Os planos 'bons' e 'ruins' têm custos totais estimados muito baixos, de modo que o otimizador não gasta muito tempo pesquisando (observe o motivo do Plano Bom o Suficiente Encontrado para o encerramento antecipado no operador raiz).
O otimizador considera muitas alternativas que você não vê refletidas no plano final. Sem o baixo custo encontrado antecipadamente, continuaria a considerar outras estratégias como a ilustrada abaixo, que empurra a anti-semi-junção (como uma aplicação ) abaixo da união :
Independentemente disso, o otimizador não está 'confuso com
UNION
' - ele simplesmente não chegou ao ponto de considerar umAPPLY
, apenas uma junção (que poderia ser implementada como loops aninhados, hash ou mesclagem).O plano de junção de loops aninhados tem uma busca no lado interno, mas esse é o predicado não correlacionado
WTID = @WTID
, que também pode aparecer em um hash ou junção de mesclagem. Os predicados restantes estão todos correlacionados, portanto, exigiriam uma aplicação para serem empurrados para baixo. Veja meu artigo, Aplicar versus Nested Loops Join se os conceitos não estiverem claros.IF EXISTS
Normalmente, você poderia usar uma
OPTION (USE HINT ('DISABLE_OPTIMIZER_ROWGOAL'))
dica de consulta para desabilitar o comportamento da meta de linha, o que provavelmente também produziria de forma confiável o plano desejado.Infelizmente, as dicas de consulta se aplicam apenas à consulta de nível superior (
IF EXISTS
aqui) e não à consulta aninhada (aquela com a qual você está preocupado). Você viu planos diferentes ao adicionar dicas, mas isso ocorreu porque o texto da consulta era diferente e exigia uma nova compilação.Usando
IF EXISTS
, você precisaria definir o sinalizador de rastreamento documentado 4138 para desabilitar o objetivo da linha. Teria que ser definido no nível da sessão (usandoDBCC TRACEON
), porqueQUERYTRACEON
também se aplica apenas ao nível superior.Você pode evitar esse comportamento não óbvio usando um padrão como o seguinte, em vez de
IF EXISTS
(veja as perguntas e respostas relacionadas abaixo)Menciono tudo isso por interesse. Como você já está usando várias dicas, expandir
FORCESEEK
é o caminho a seguir.Perguntas e respostas relacionadas:
Por alguma razão pouco clara, o compilador parece estar ficando confuso com o
UNION
e não empurrando osNOT EXISTS
predicados anti-junção até o fim e, portanto, não correspondendo totalmente ao índice.As seguintes coisas parecem não fazer diferença:
RowId
éint
e deveria serbigint
(você deve fazer isso de qualquer maneira).UNION
paraUNION ALL
(você deve fazer isso de qualquer maneira).EXCEPT
em vez deWHERE NOT EXISTS
.GROUP BY
para obter um conjunto externo unificado.COUNT(*) = 0
.FORCESEEK
ouINDEX
dicas.A única solução alternativa que consigo ver é repetir o arquivo
WHERE NOT EXISTS
. Você pode usarOR EXISTS
ou pode usarUNION ALL
para isso.