Sélectionner * dans la vue prend 4 minutes

11

Je rencontre un problème où, lorsque j'exécute une requête sur une vue, cela prend plus de 4 minutes. Cependant, lorsque j'exécute les entrailles de la requête, elle se termine en 1 seconde.

La seule chose dont je ne suis pas sûr, c'est que les tables jointes sont toutes les deux des tables temporelles.

Plan de requête ad hoc: https://www.brentozar.com/pastetheplan/?id=BykohB2p4

Afficher le plan de requête: https://www.brentozar.com/pastetheplan/?id=SkIfTHh6E

Des suggestions sur où essayer de comprendre cela?

Afficher le code:

ALTER VIEW [dbo].[vwDealHistoryPITA]
AS
SELECT ROW_NUMBER() OVER (PARTITION BY cm.CodeMasterID ORDER BY cm.CodeMasterID, cm.LastUpdateDate) AS Deal_HistoryID,
       cm.CodeMasterID,
       cm.ProjectName,
       cm.[Status],
       d.CompanyID,
       d.DealTypeMasterID,
       cm.[Description],
       d.PassiveInd,
       d.ApproxTPGOwnership,
       d.NumberBoardSeats,
       d.FollowonInvestmentInd,
       d.SocialImpactInd,
       d.EquityInd,
       d.DebtInd,
       d.RealEstateInd,
       d.TargetPctgReturn,
       d.ApproxTotalDealSize,
       cm.CurrencyCode,
       d.ConflictCheck,
       cm.CreatedDate,
       cm.CreatedBy,
       cm.LastUpdateDate,
       cm.LastUpdateBy,
       d.ExpensesExceedThresholdDate,
       d.CurrentTPGCheckSize,
       d.PreferredEquityInd,
       d.ConvertibleDebtInd,
       d.OtherRealAssetsInd,
       d.InitialTPGCheckSize,
       d.DirectLendingInd,
       cm.NameApproved,
       cm.FolderID,
       cm.CodaProcessedDateTime,
       cm.DeadDate,
       d.SectorMasterID,
       d.DTODataCompleteDate,
       cm.ValidFrom AS CodeMasterValidFrom,
       cm.ValidTo   AS CodeMasterValidTo,
       d.validFrom  AS DealValidFrom,
       d.validTo    AS DealValidTo
FROM dbo.CodeMaster FOR SYSTEM_TIME ALL cm 
INNER JOIN dbo.Deal FOR SYSTEM_TIME ALL d ON cm.CodeMasterID = d.CodeMasterID;
GO

Ajout de la partition par et obtention de résultats similaires à la requête ad hoc.

user761786
la source

Réponses:

18

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 CodeMasterIDsur 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

entrez la description de l'image ici

entrez la description de l'image ici

Table des transactions

entrez la description de l'image ici

entrez la description de l'image ici

J'aime l'odeur de chercher des prédicats le matin


La grande mauvaise requête

Table Codemaster

entrez la description de l'image ici

entrez la description de l'image ici

Ceci est une zone de prédicat uniquement

La table Deal

entrez la description de l'image ici

Mais l'optimiseur n'a pas lu "L'art du deal ™"

entrez la description de l'image ici

... et n'apprend pas du passé

Jusqu'à ce que toutes ces données atteignent l'opérateur de filtrage

entrez la description de l'image ici


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_EXECUTESQLpour 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 @P1prédicat aux tables en raison de la fonction de fenêtre et du paramétrage entraînant l'opérateur de filtre

entrez la description de l'image ici entrez la description de l'image ici

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: entrez la description de l'image ici

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;

entrez la description de l'image ici

Cette partie correspond à ce que nous avons vu lors de la déclaration du paramètre nous-mêmes / utilisation SP_EXECUTESQLsur 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
Randi Vertongen
la source