Depois de blogar sobre como os índices filtrados podem ser mais poderosos e, mais recentemente, sobre como eles podem se tornar inúteis pela parametrização forçada, estou revisitando o tópico de índices/parametrização filtrados. Uma solução aparentemente simples demais surgiu no trabalho recentemente, e eu tive que compartilhar.
Veja o exemplo a seguir, onde temos um banco de dados de vendas contendo uma tabela de pedidos. Às vezes, queremos apenas uma lista (ou uma contagem) apenas dos pedidos ainda a serem enviados - que, com o tempo, (espero!) representam uma porcentagem cada vez menor da tabela geral:
CREATE DATABASE Sales; GO USE Sales; GO -- simplified, obviously: CREATE TABLE dbo.Orders ( OrderID int IDENTITY(1,1) PRIMARY KEY, OrderDate datetime NOT NULL, filler char(500) NOT NULL DEFAULT '', IsShipped bit NOT NULL DEFAULT 0 ); GO -- let's put some data in there; 7,000 shipped orders, and 50 unshipped: INSERT dbo.Orders(OrderDate, IsShipped) -- random dates over two years SELECT TOP (7000) DATEADD(DAY, ABS(object_id % 730), '20171101'), 1 FROM sys.all_columns UNION ALL -- random dates from this month SELECT TOP (50) DATEADD(DAY, ABS(object_id % 30), '20191201'), 0 FROM sys.all_columns;
Pode fazer sentido neste cenário criar um índice filtrado como este (o que facilita o trabalho de qualquer consulta que esteja tentando obter esses pedidos não enviados):
CREATE INDEX ix_OrdersNotShipped ON dbo.Orders(IsShipped, OrderDate) WHERE IsShipped = 0;
Podemos executar uma consulta rápida como esta para ver como ela usa o índice filtrado:
SELECT OrderID, OrderDate FROM dbo.Orders WHERE IsShipped = 0;
O plano de execução é bastante simples, mas há um aviso sobre UnmatchedIndexes:
O nome do aviso é um pouco enganoso - o otimizador foi capaz de usar o índice, mas está sugerindo que seria "melhor" sem parâmetros (que não usamos explicitamente), mesmo que a instrução pareça ter sido parametrizada:
Se você realmente quiser, pode eliminar o aviso, sem diferença no desempenho real (seria apenas cosmético). Uma maneira é adicionar um predicado de impacto zero, como
AND (1 > 0)
:SELECT wadd = OrderID, OrderDate FROM dbo.Orders WHERE IsShipped = 0 AND (1 > 0);
Outra (provavelmente mais comum) é adicionar
OPTION (RECOMPILE)
:SELECT wrecomp = OrderID, OrderDate FROM dbo.Orders WHERE IsShipped = 0 OPTION (RECOMPILE);
Ambas as opções geram o mesmo plano (uma busca sem avisos):
Até agora tudo bem; nosso índice filtrado está sendo usado (como esperado). Esses não são os únicos truques, é claro; veja os comentários abaixo para outros que os leitores já enviaram.
Então, a complicação
Como o banco de dados está sujeito a um grande número de consultas ad hoc, alguém ativa a parametrização forçada, tentando reduzir a compilação e eliminar os planos de baixo e único uso de poluir o cache do plano:
ALTER DATABASE Sales SET PARAMETERIZATION FORCED;
Agora nossa consulta original não pode usar o índice filtrado; é forçado a verificar o índice clusterizado:
SELECT OrderID, OrderDate FROM dbo.Orders WHERE IsShipped = 0;
O aviso sobre índices não correspondidos retorna e recebemos novos avisos sobre E/S residual. Observe que a instrução é parametrizada, mas parece um pouco diferente:
Isso ocorre por design, pois todo o objetivo da parametrização forçada é parametrizar consultas como essa. Mas isso anula o propósito do nosso índice filtrado, já que ele serve para dar suporte a um único valor no predicado, não a um parâmetro que pode ser alterado.
Tolice
Nossa consulta "truque" que usa o predicado adicional também não consegue usar o índice filtrado e acaba com um plano um pouco mais complicado para inicializar:
SELECT OrderID, OrderDate FROM dbo.Orders WHERE IsShipped = 0 AND (1 > 0);
OPÇÃO (RECOMPILAR)
A reação típica neste caso, assim como na remoção do aviso anterior, é adicionar
OPTION (RECOMPILE)
à declaração. Isso funciona e permite que o índice filtrado seja escolhido para uma busca eficiente… SELECT OrderID, OrderDate FROM dbo.Orders WHERE IsShipped = 0 OPTION (RECOMPILE);
…mas adicionando
OPTION (RECOMPILE)
e levar esse hit de compilação adicional em todas as execuções da consulta nem sempre será aceitável em ambientes de alto volume (especialmente se eles já estiverem vinculados à CPU). Dicas
Alguém sugeriu sugerir explicitamente o índice filtrado para evitar os custos de recompilação. Em geral, isso é bastante frágil, porque depende do índice que sobrevive ao código; Eu costumo usar isso como último recurso. Neste caso, não é válido de qualquer maneira. Quando as regras de parametrização impedem que o otimizador selecione o índice filtrado automaticamente, elas também impedem que você o selecione manualmente. Mesmo problema com um genérico
FORCESEEK
dica:SELECT OrderID, OrderDate FROM dbo.Orders WITH (INDEX (ix_OrdersNotShipped)) WHERE IsShipped = 0; SELECT OrderID, OrderDate FROM dbo.Orders WITH (FORCESEEK) WHERE IsShipped = 0;
Ambos dão este erro:
Msg 8622, Level 16, State 1
O processador de consultas não pôde produzir um plano de consulta devido às dicas definidas nesta consulta. Reenvie a consulta sem especificar nenhuma dica e sem usar SET FORCEPLAN.
E isso faz sentido, porque não há como saber que o valor desconhecido para o
IsShipped
O parâmetro corresponderá ao índice filtrado (ou oferecerá suporte a uma operação de busca em qualquer índice). SQL Dinâmico?
Eu sugeri que você pudesse usar SQL dinâmico, para pelo menos pagar apenas esse hit de recompilação quando você sabe que deseja atingir o índice menor:
DECLARE @IsShipped bit = 0; DECLARE @sql nvarchar(max) = N'SELECT dynsql = OrderID, OrderDate FROM dbo.Orders' + CASE WHEN @IsShipped IS NOT NULL THEN N' WHERE IsShipped = @IsShipped' ELSE N'' END + CASE WHEN @IsShipped = 0 THEN N' OPTION (RECOMPILE)' ELSE N'' END; EXEC sys.sp_executesql @sql, N'@IsShipped bit', @IsShipped;
Isso leva ao mesmo plano eficiente acima. Se você alterou a variável para
@IsShipped = 1
, você obtém a verificação de índice clusterizado mais cara que deve esperar:Mas ninguém gosta de usar SQL dinâmico em um caso extremo como este - torna o código mais difícil de ler e manter, e mesmo se esse código estivesse no aplicativo, ainda seria uma lógica adicional que teria que ser adicionada lá, tornando-o menos do que desejável .
Algo mais simples
Conversamos brevemente sobre a implementação de um guia de plano, que certamente não é mais simples, mas então um colega sugeriu que você poderia enganar o otimizador "escondendo" a instrução parametrizada dentro de um procedimento armazenado, exibição ou função com valor de tabela embutido. Era tão simples, eu não acreditava que iria funcionar.
Mas então eu tentei:
CREATE PROCEDURE dbo.GetUnshippedOrders AS BEGIN SET NOCOUNT ON; SELECT OrderID, OrderDate FROM dbo.Orders WHERE IsShipped = 0; END GO CREATE VIEW dbo.vUnshippedOrders AS SELECT OrderID, OrderDate FROM dbo.Orders WHERE IsShipped = 0; GO CREATE FUNCTION dbo.fnUnshippedOrders() RETURNS TABLE AS RETURN (SELECT OrderID, OrderDate FROM dbo.Orders WHERE IsShipped = 0); GO
Todas essas três consultas realizam a busca eficiente em relação ao índice filtrado:
EXEC dbo.GetUnshippedOrders; GO SELECT OrderID, OrderDate FROM dbo.vUnshippedOrders; GO SELECT OrderID, OrderDate FROM dbo.fnUnshippedOrders();
Conclusão
Fiquei surpreso que isso fosse tão eficaz. Obviamente, isso exige que você altere o aplicativo; se você não puder alterar o código do aplicativo para chamar um procedimento armazenado ou fazer referência à exibição ou função (ou até mesmo adicionar
OPTION (RECOMPILE)
), você terá que continuar procurando outras opções. Mas se você puder alterar o código do aplicativo, colocar o predicado em outro módulo pode ser o caminho a seguir.