As principais diferenças de desempenho
As principais diferenças aqui são que a consulta com melhor desempenho está pressionando o predicado de busca CodeMasterID
em todas as 4 tabelas (2 tabelas temporais (real e histórico)) em que a seleção na exibição parece não fazer isso até o final (operador de filtro) .
TL DR;
O problema ocorre devido aos parâmetros que não pressionam para baixo as funções da janela em certos casos, como visualizações. A solução mais fácil é adicionar OPTION(RECOMPILE)
a chamada de visualização para fazer o otimizador 'ver' os parâmetros em tempo de execução, se for possível. Se for muito caro recompilar o plano de execução para cada chamada de consulta, usar uma função com valor de tabela embutida que espera que um parâmetro possa ser uma solução. Há um excelente Blogpost de Paul White sobre isso. Para uma maneira mais detalhada de encontrar e resolver seu problema específico, continue lendo.
A consulta com melhor desempenho
Tabela Codemaster
Tabela de ofertas
Eu amo o cheiro de procurar predicados pela manhã
A grande consulta ruim
Tabela Codemaster
Esta é uma zona apenas de predicado
A tabela Deal
Mas o otimizador não leu 'The art of the deal ™ "
... e não aprende com o passado
Até que todos esses dados cheguem ao operador do filtro
Então, o que dá?
O principal problema aqui é o otimizador não 'vendo' os parâmetros em tempo de execução devido às funções da janela na exibição e não podendo usá-lo SelOnSeqPrj
(selecione no projeto de sequência, mais abaixo neste post para referência) .
Consegui replicar os mesmos resultados com uma amostra de teste e usando SP_EXECUTESQL
para parametrizar a chamada para a exibição. Consulte o adendo para o DDL / DML
executando uma consulta em uma exibição de teste com uma função de janela e um INNER JOIN
SET STATISTICS IO, TIME ON;
EXEC SP_EXECUTESQL
N'SELECT * FROM dbo.Bad
Where CodeMasterID = @P1',N'@P1 INT',@P1 = 37155;
Resultando em cerca de 4,5s de tempo da CPU e 3,2s de tempo decorrido
SQL Server Execution Times:
CPU time = 4595 ms, elapsed time = 3209 ms.
Quando adicionamos o doce abraço de OPTION(RECOMPILE)
SET STATISTICS IO, TIME ON;
EXEC SP_EXECUTESQL
N'SELECT * FROM dbo.Bad
Where CodeMasterID = @P1 OPTION(RECOMPILE)',N'@P1 INT',@P1 = 37155;
Esta tudo bem.
SQL Server Execution Times:
CPU time = 16 ms, elapsed time = 98 ms.
Por quê
Isso tudo novamente suporta o ponto de não poder aplicar o @P1
predicado às tabelas devido à função da janela e à parametrização que resultam no operador de filtro
Não é apenas um problema para tabelas temporais
Ver adendo 2
Mesmo quando não estiver usando tabelas temporais, isso acontece:
O mesmo resultado é visto ao escrever a consulta assim:
DECLARE @P1 int = 37155
SELECT * FROM dbo.Bad2
Where CodeMasterID = @P1;
Novamente, o otimizador não está pressionando o predicado antes de aplicar a função da janela.
Ao omitir o ROW_NUMBER ()
CREATE VIEW dbo.Bad3
as
SELECT
cm.CodeMasterID,CM.ManagerID,cm.ParentDeptID,d.DealID, d.CodeMasterID as dealcodemaster,d.EvenMoreBlaID
FROM dbo.CodeMaster2 cm
INNER JOIN dbo.Deal2 d ON cm.CodeMasterID = d.CodeMasterID;
Tudo está bem
SET STATISTICS IO, TIME ON;
EXEC SP_EXECUTESQL
N'SELECT * FROM dbo.Bad3
Where CodeMasterID = @P1',N'@P1 INT',@P1 = 37155
SQL Server Execution Times:
CPU time = 0 ms, elapsed time = 33 ms.
então onde tudo isso nos deixa?
O ROW_NUMBER()
é calculado antes que o filtro seja aplicado nas consultas incorretas.
E tudo isso nos leva a este post de 2013 de Paul White
sobre funções e visualizações da janela.
Uma das partes importantes para o nosso exemplo é esta afirmação:
Infelizmente, a regra de simplificação SelOnSeqPrj funciona apenas quando o predicado executa uma comparação com uma constante. Por esse motivo, a consulta a seguir produz o plano abaixo do ideal no SQL Server 2008 e posterior:
DECLARE @ProductID INT = 878;
SELECT
mrt.ProductID,
mrt.TransactionID,
mrt.ReferenceOrderID,
mrt.TransactionDate,
mrt.Quantity
FROM dbo.MostRecentTransactionsPerProduct AS mrt
WHERE
mrt.ProductID = @ProductID;
Esta parte corresponde ao que vimos ao declarar o parâmetro por nós mesmos / usando SP_EXECUTESQL
na exibição.
As soluções reais
1: OPÇÃO (RECOMPLE)
Sabemos que OPTION(RECOMPILE)
"ver" o valor em tempo de execução é uma possibilidade. Ao recompilar o plano de execução para cada chamada de consulta é muito caro, existem outras soluções.
2: Função com valor de tabela embutida com um parâmetro
CREATE FUNCTION dbo.BlaBla
(
@P1 INT
)
RETURNS TABLE
WITH SCHEMABINDING AS
RETURN
(
SELECT
ROW_NUMBER() OVER (PARTITION BY cm.CodeMasterID ORDER BY cm.CodeMasterID) AS Deal_HistoryID,
cm.CodeMasterID,CM.ManagerID,
cm.ParentDeptID,d.DealID,
d.CodeMasterID as dealcodemaster,
d.EvenMoreBlaID
FROM dbo.CodeMaster2 cm
INNER JOIN dbo.Deal2 d ON cm.CodeMasterID = d.CodeMasterID
Where cm.CodeMasterID = @P1
)
EXEC SP_EXECUTESQL
N'SELECT * FROM dbo.BlaBLa(@P1)',N'@P1 INT',@P1 = 37155
Resultando nos predicados de busca esperados
SQL Server Execution Times:
CPU time = 0 ms, elapsed time = 0 ms.
Com cerca de 9 leituras lógicas no meu teste
3: Escrevendo a consulta sem o uso de uma exibição.
A outra 'solução' pode estar gravando a consulta inteiramente sem o uso de uma exibição.
4: Não mantendo a ROW_NUMBER()
função na visualização, mas especificando-a na chamada para a visualização.
Um exemplo disso seria:
CREATE VIEW dbo.Bad2
as
SELECT
cm.CodeMasterID,CM.ManagerID,cm.ParentDeptID,d.DealID, d.CodeMasterID as dealcodemaster,d.EvenMoreBlaID
FROM dbo.CodeMaster2 cm
INNER JOIN dbo.Deal2 d ON cm.CodeMasterID = d.CodeMasterID;
GO
SET STATISTICS IO, TIME ON;
EXEC SP_EXECUTESQL
N'SELECT ROW_NUMBER() OVER (PARTITION BY CodeMasterID ORDER BY CodeMasterID) AS Deal_HistoryID,* FROM dbo.Bad2
Where CodeMasterID = @P1',N'@P1 INT',@P1 = 37155;
Deve haver outras maneiras criativas de contornar esse problema, a parte importante é saber o que o causa.
Adendo # 1
CREATE TABLE dbo.Codemaster
(
CodeMasterID int NOT NULL PRIMARY KEY CLUSTERED
, ManagerID INT NULL
, ParentDeptID int NULL
, SysStartTime datetime2 GENERATED ALWAYS AS ROW START NOT NULL
, SysEndTime datetime2 GENERATED ALWAYS AS ROW END NOT NULL
, PERIOD FOR SYSTEM_TIME (SysStartTime,SysEndTime)
)
WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = dbo.Codemaster_History))
;
CREATE TABLE dbo.Deal
(
DealID int NOT NULL PRIMARY KEY CLUSTERED
, CodeMasterID INT NULL
, EvenMoreBlaID int NULL
, SysStartTime datetime2 GENERATED ALWAYS AS ROW START NOT NULL
, SysEndTime datetime2 GENERATED ALWAYS AS ROW END NOT NULL
, PERIOD FOR SYSTEM_TIME (SysStartTime,SysEndTime)
)
WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = dbo.Deal_History))
;
INSERT INTO dbo.Codemaster(CodeMasterID,ManagerID,ParentDeptID)
SELECT TOP(1000000) ROW_NUMBER() OVER(ORDER BY(SELECT NULL)) as rownum1,
ROW_NUMBER() OVER(ORDER BY(SELECT NULL)) as rownum2,
ROW_NUMBER() OVER(ORDER BY(SELECT NULL)) as rownum3
FROM MASTER..spt_values as spt1
CROSS JOIN MASTER..spt_values as spt2;
INSERT INTO dbo.Deal(DealID,CodeMasterID,EvenMoreBlaID)
SELECT TOP(1000000) ROW_NUMBER() OVER(ORDER BY(SELECT NULL)) as rownum1,
ROW_NUMBER() OVER(ORDER BY(SELECT NULL)) as rownum2,
ROW_NUMBER() OVER(ORDER BY(SELECT NULL)) as rownum3
FROM MASTER..spt_values as spt1
CROSS JOIN MASTER..spt_values as spt2;
CREATE INDEX IX_CodeMasterID
ON dbo.Deal(CodeMasterId);
CREATE INDEX IX_CodeMasterID
ON dbo.Deal_History(CodeMasterId);
CREATE INDEX IX_CodeMasterID
ON dbo.Codemaster(CodeMasterId);
CREATE INDEX IX_CodeMasterID
ON dbo.Codemaster_History(CodeMasterId);
SELECT ROW_NUMBER() OVER (PARTITION BY cm.CodeMasterID ORDER BY cm.CodeMasterID, cm.SysStartTime) AS Deal_HistoryID,
cm.*, d.*
FROM dbo.CodeMaster FOR SYSTEM_TIME ALL cm
INNER JOIN dbo.Deal FOR SYSTEM_TIME ALL d ON cm.CodeMasterID = d.CodeMasterID
Where cm.CodeMasterID = 37155;
-- Guud
GO
CREATE VIEW dbo.Bad
as
SELECT ROW_NUMBER() OVER (PARTITION BY cm.CodeMasterID ORDER BY cm.CodeMasterID, cm.SysStartTime) AS Deal_HistoryID,
cm.CodeMasterID,CM.ManagerID,cm.ParentDeptID,d.DealID, d.CodeMasterID as dealcodemaster,d.EvenMoreBlaID
FROM dbo.CodeMaster FOR SYSTEM_TIME ALL cm
INNER JOIN dbo.Deal FOR SYSTEM_TIME ALL d ON cm.CodeMasterID = d.CodeMasterID
GO
EXEC SP_EXECUTESQL
N'SELECT * FROM dbo.Bad
Where CodeMasterID = @P1',N'@P1 INT',@P1 = 37155
-- Very bad shame on you
Adendo # 2
CREATE TABLE dbo.Codemaster2
(
CodeMasterID int NOT NULL PRIMARY KEY CLUSTERED
, ManagerID INT NULL
, ParentDeptID int NULL
);
CREATE TABLE dbo.Deal2
(
DealID int NOT NULL PRIMARY KEY CLUSTERED
, CodeMasterID INT NULL
, EvenMoreBlaID int NULL
);
INSERT INTO dbo.Codemaster2(CodeMasterID,ManagerID,ParentDeptID)
SELECT TOP(1000000) ROW_NUMBER() OVER(ORDER BY(SELECT NULL)) as rownum1,
ROW_NUMBER() OVER(ORDER BY(SELECT NULL)) as rownum2,
ROW_NUMBER() OVER(ORDER BY(SELECT NULL)) as rownum3
FROM MASTER..spt_values as spt1
CROSS JOIN MASTER..spt_values as spt2;
INSERT INTO dbo.Deal2(DealID,CodeMasterID,EvenMoreBlaID)
SELECT TOP(1000000) ROW_NUMBER() OVER(ORDER BY(SELECT NULL)) as rownum1,
ROW_NUMBER() OVER(ORDER BY(SELECT NULL)) as rownum2,
ROW_NUMBER() OVER(ORDER BY(SELECT NULL)) as rownum3
FROM MASTER..spt_values as spt1
CROSS JOIN MASTER..spt_values as spt2;
CREATE INDEX IX_CodeMasterID
ON dbo.Deal2(CodeMasterId);
CREATE INDEX IX_CodeMasterID
ON dbo.Codemaster2(CodeMasterId);
SELECT ROW_NUMBER() OVER (PARTITION BY cm.CodeMasterID ORDER BY cm.CodeMasterId) AS Deal_HistoryID,
cm.*, d.*
FROM dbo.CodeMaster2 cm
INNER JOIN dbo.Deal2 d ON cm.CodeMasterID = d.CodeMasterID
Where cm.CodeMasterID = 37155;
-- Guud
GO
CREATE VIEW dbo.Bad2
as
SELECT ROW_NUMBER() OVER (PARTITION BY cm.CodeMasterID ORDER BY cm.CodeMasterID) AS Deal_HistoryID,
cm.CodeMasterID,CM.ManagerID,cm.ParentDeptID,d.DealID, d.CodeMasterID as dealcodemaster,d.EvenMoreBlaID
FROM dbo.CodeMaster2 cm
INNER JOIN dbo.Deal2 d ON cm.CodeMasterID = d.CodeMasterID
GO
SET STATISTICS IO, TIME ON;
EXEC SP_EXECUTESQL
N'SELECT * FROM dbo.Bad2
Where CodeMasterID = @P1',N'@P1 INT',@P1 = 37155