Les principales différences de performances
Les principales différences ici sont que la requête la plus performante pousse le prédicat de recherche CodeMasterID
sur les 4 tables (2 tables temporelles (réelles et historiques)) où la sélection dans la vue ne semble pas le faire jusqu'à la fin (opérateur de filtre) .
TL DR;
Le problème est dû au fait que les paramètres ne poussent pas vers les fonctions de fenêtre dans certains cas tels que les vues. La solution la plus simple consiste à ajouter OPTION(RECOMPILE)
à l'appel de vue pour que l'optimiseur «voit» les paramètres au moment de l'exécution si cela est possible. S'il est trop coûteux de recompiler le plan d'exécution pour chaque appel de requête, l'utilisation d'une fonction de valeur de table en ligne qui attend un paramètre pourrait être une solution. Il y a un excellent Blogpost de Paul White à ce sujet. Pour une façon plus détaillée de trouver et de résoudre votre problème particulier, continuez à lire.
La requête la plus performante
Table Codemaster
Table des transactions
J'aime l'odeur de chercher des prédicats le matin
La grande mauvaise requête
Table Codemaster
Ceci est une zone de prédicat uniquement
La table Deal
Mais l'optimiseur n'a pas lu "L'art du deal ™"
... et n'apprend pas du passé
Jusqu'à ce que toutes ces données atteignent l'opérateur de filtrage
Alors, qu'est-ce qui donne?
Le problème principal ici est que l'optimiseur ne «voit» pas les paramètres lors de l'exécution en raison des fonctions de la fenêtre dans la vue et ne peut pas utiliser le SelOnSeqPrj
(sélectionner sur le projet de séquence, plus bas dans cet article pour référence) .
J'ai pu reproduire les mêmes résultats avec un échantillon de test et utiliser SP_EXECUTESQL
pour paramétrer l'appel à la vue. Voir addendum pour le DDL / DML
exécution d'une requête sur une vue de test avec une fonction de fenêtre et un INNER JOIN
SET STATISTICS IO, TIME ON;
EXEC SP_EXECUTESQL
N'SELECT * FROM dbo.Bad
Where CodeMasterID = @P1',N'@P1 INT',@P1 = 37155;
Résultat: environ 4,5 s de temps processeur et 3,2 s de temps écoulé
SQL Server Execution Times:
CPU time = 4595 ms, elapsed time = 3209 ms.
Quand on ajoute la douce étreinte 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;
Tout va bien.
SQL Server Execution Times:
CPU time = 16 ms, elapsed time = 98 ms.
Pourquoi
Tout cela supporte à nouveau le point de ne pas pouvoir appliquer le @P1
prédicat aux tables en raison de la fonction de fenêtre et du paramétrage entraînant l'opérateur de filtre
Pas seulement un problème pour les tables temporelles
Voir addendum 2
Même lorsque vous n'utilisez pas de tables temporelles, cela se produit:
Le même résultat apparaît lors de l'écriture de la requête comme ceci:
DECLARE @P1 int = 37155
SELECT * FROM dbo.Bad2
Where CodeMasterID = @P1;
Encore une fois, l'optimiseur n'appuie pas sur le prédicat avant d'appliquer la fonction de fenêtre.
En omettant le 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;
Tout est bien
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.
alors où tout cela nous laisse-t-il?
Le ROW_NUMBER()
est calculé avant l'application du filtre sur les requêtes incorrectes.
Et tout cela nous amène à cet article de blog de 2013 de Paul White
sur les fonctions et les vues des fenêtres.
L'une des parties importantes de notre exemple est cette déclaration:
Malheureusement, la règle de simplification SelOnSeqPrj ne fonctionne que lorsque le prédicat effectue une comparaison avec une constante. Pour cette raison, la requête suivante produit le plan sous-optimal sur SQL Server 2008 et versions ultérieures:
DECLARE @ProductID INT = 878;
SELECT
mrt.ProductID,
mrt.TransactionID,
mrt.ReferenceOrderID,
mrt.TransactionDate,
mrt.Quantity
FROM dbo.MostRecentTransactionsPerProduct AS mrt
WHERE
mrt.ProductID = @ProductID;
Cette partie correspond à ce que nous avons vu lors de la déclaration du paramètre nous-mêmes / utilisation SP_EXECUTESQL
sur la vue.
Les solutions actuelles
1: OPTION (RECOMPILE)
Nous savons que OPTION(RECOMPILE)
«voir» la valeur au moment de l'exécution est une possibilité. Lorsque la recompilation du plan d'exécution pour chaque appel de requête est trop coûteuse, il existe d'autres solutions.
2: fonction de valeur de table en ligne avec un paramètre
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
Résultat dans les prédicats de recherche attendus
SQL Server Execution Times:
CPU time = 0 ms, elapsed time = 0 ms.
Avec environ 9 lectures logiques sur mon test
3: Écrire la requête sans utiliser de vue.
L'autre «solution» pourrait consister à écrire entièrement la requête sans utiliser de vue.
4: Ne pas conserver la ROW_NUMBER()
fonction dans la vue, au lieu de la spécifier dans l'appel à la vue.
Un exemple de ceci serait:
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;
Il devrait y avoir d'autres façons créatives de contourner ce problème, la partie importante est de savoir ce qui en est la cause.
Addendum # 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
Addendum # 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