web-dev-qa-db-fra.com

SQL Server: Améliorer les performances CTE avec des déclarations de cas

J'ai besoin de votre aide, j'ai besoin de la guanade pour améliorer la performance de la vue donnée suivante.

J'ai une vue écrive avec le code ci-dessous:

with timeframes  as
(
select p.SEARCH_NUM, 
case   when p.FROM_DATE is not null then p.FROM_DATE
       when p.FROM_DATE is null and P.SEARCH_DAYS is not null and p.TO_DATE is not null then DATEADD(day,p.SEARCH_DAYS*-1,p.TO_DATE)
       when p.FROM_DATE is null and P.SEARCH_DAYS is not null and p.TO_DATE is null then DATEADD(day,p.SEARCH_DAYS*-1,GetDate()) 
       when p.FROM_DATE is null and P.SEARCH_DAYS is null and p.TO_DATE is not null and p.DURATION = 'Yearly' then DATEADD(year,-1,p.TO_DATE)
       when p.FROM_DATE is null and P.SEARCH_DAYS is null and p.TO_DATE is null and p.DURATION = 'Yearly' then DATEADD(year,-1,GetDate())
       when p.FROM_DATE is null and P.SEARCH_DAYS is null and p.TO_DATE is not null and p.DURATION = 'Monthly' then DATEADD(month,-1,p.TO_DATE)
       when p.FROM_DATE is null and P.SEARCH_DAYS is null and p.TO_DATE is null and p.DURATION = 'Monthly' then DATEADD(MONTH, DATEDIFF(MONTH, 0, GETDATE())-1, 0)
       when p.FROM_DATE is null and P.SEARCH_DAYS is null and p.TO_DATE is not null and p.DURATION = 'Weekly' then DATEADD(day,-7,p.TO_DATE)
       when p.FROM_DATE is null and P.SEARCH_DAYS is null and p.TO_DATE is null and p.DURATION = 'Weekly' then DATEADD(day,-7,GetDate())
       else DATEADD(month,-1,GetDate())
  end as FROM_DATE
  ,case when p.TO_DATE is not null then p.TO_DATE
    when p.TO_DATE is null and p.DURATION = 'Monthly' then DATEADD(MONTH, DATEDIFF(MONTH, -1, GETDATE())-1, -1)
       else GetDate()
  end as TO_DATE
from dbo.parmeters_table as p
)
,
transactions as
(
  select tm.SEARCH_NUM, tr.id from dbo.ixf_transaction tr
  inner join timeframes tm on tr.transaction_date between tm.FROM_DATE and tm.TO_DATE
)
,
searchResults AS
(
select DISTINCT
  t.SEARCH_NUM
  ,trx.id as 'trx_id_FK'
  ,trx.institution_FK
  ,trx.branch_FK
  ,branch.code as 'branch_number'
  ,trx.account_FK
  ,trx.transaction_date as 'trx_date'
  ,case when trx.type_of_transaction = 'I' or trx.type_of_transaction = 'B'
    then trx.base_currency_amount else 0 end as 'cash_in'
  ,case when trx.type_of_transaction = 'O' or trx.type_of_transaction = 'B'
    then trx.base_currency_amount else 0 end as 'cash_out'  
  ,case when trx.type_of_transaction = 'B'
    then trx.base_currency_amount else 0 end as 'curr_exchange'  
  ,trx.base_currency_amount    
  ,trx.type_of_transaction
  ,trx.teller_id
  ,trx.unique_trans_id
  ,trx.serial_number
  ,trx.foreign_amount
  ,trx.country_of_currency_FK
  ,trx.flex_1
  ,trx.flex_2
  ,trx.customer_FK
  ,ttr.code as 'trx_code'
  ,ttr.description as 'trx_description'
  ,ttr.irs_transaction_id
 ,cust.full_name 
 ,str(cust.web_reference_id) as 'web_reference_id' 
 ,conben.customer_cif 
 ,conben.id_number 
 ,conben.id_type
 ,conben.other_description as 'id_type_other_description'
 ,conben.customer_tin 
 ,conben.cust_type_fk as 'TIN_Type'
 ,ctrtx.is_teller
from ixf_transaction trx
  inner join transactions t on trx.id = t.id
  inner join type_ref ttr on ttr.id=trx.transaction_type_FK
  inner join unit branch on branch.id=trx.branch_FK and branch.object_type='Branch'
  left join  cust on cust.id = trx.customer_FK
  left join  cashtx_cus conben on conben.transaction_id = trx.unique_trans_id    and conben.customer_FK = trx.customer_FK
  left join  trans ctrtx on ctrtx.cash_transaction_fk = trx.id
) 

  select trx_id_fk 
  ,case when account_FK is not null then (select count(1) from searchresults a where a.account_fk = searchresults.account_fk and a.SEARCH_NUM = searchresults.SEARCH_NUM) 
   when customer_cif is not null then (select count(1) from searchresults a where a.customer_cif = searchresults.customer_cif and a.SEARCH_NUM = searchresults.SEARCH_NUM) 
   when customer_tin is not null then (select count(1) from searchresults a where a.customer_tin = searchresults.customer_tin and a.TIN_Type = searchresults.TIN_Type and a.SEARCH_NUM = searchresults.SEARCH_NUM) 
   when ID_NUMBER is not null then (select count(1) from searchresults a where a.ID_NUMBER = searchresults.ID_NUMBER and a.SEARCH_NUM = searchresults.SEARCH_NUM)
   else (select count(1) from searchresults a where a.SEARCH_NUM = searchresults.SEARCH_NUM)
   end as 'trx_per_period'
  ,case when account_FK is not null then (select count(1) from searchresults a where a.account_FK = searchresults.account_FK and a.trx_date = searchresults.trx_date and a.SEARCH_NUM = searchresults.SEARCH_NUM) 
   when customer_cif is not null then (select count(1) from searchresults a where a.customer_cif = searchresults.customer_cif and a.trx_date = searchresults.trx_date and a.SEARCH_NUM = searchresults.SEARCH_NUM) 
   when customer_tin is not null then (select count(1) from searchresults a where a.customer_tin = searchresults.customer_tin and a.TIN_Type = searchresults.TIN_Type and a.trx_date = searchresults.trx_date and a.SEARCH_NUM = searchresults.SEARCH_NUM)
   when ID_NUMBER is not null then (select count(1) from searchresults a where a.ID_NUMBER = searchresults.ID_NUMBER and a.trx_date = searchresults.trx_date and a.SEARCH_NUM = searchresults.SEARCH_NUM)
   else (select count(1) from searchresults a where a.trx_date = searchresults.trx_date and a.SEARCH_NUM = searchresults.SEARCH_NUM)
   end as 'trx_per_day'
  ,case when account_FK is not null then (select sum(cash_in) from searchresults a where a.account_FK = searchresults.account_FK and a.trx_date = searchresults.trx_date and a.SEARCH_NUM = searchresults.SEARCH_NUM)
   when customer_cif is not null then (select sum(cash_in) from searchresults a where a.customer_cif = searchresults.customer_cif and a.trx_date = searchresults.trx_date and a.SEARCH_NUM = searchresults.SEARCH_NUM)
   when customer_tin is not null then (select sum(cash_in) from searchresults a where a.customer_tin = searchresults.customer_tin and a.TIN_Type = searchresults.TIN_Type and a.trx_date = searchresults.trx_date and a.SEARCH_NUM = searchresults.SEARCH_NUM)
   when ID_NUMBER is not null then (select sum(cash_in) from searchresults a where a.ID_NUMBER = searchresults.ID_NUMBER and a.trx_date = searchresults.trx_date and a.SEARCH_NUM = searchresults.SEARCH_NUM)
   else (select sum(cash_in) from searchresults a where a.trx_date = searchresults.trx_date and a.SEARCH_NUM = searchresults.SEARCH_NUM)
   end as 'total_per_day_cash_in'
  ,case when account_FK is not null then (select sum(cash_out) from searchresults a where a.account_FK = searchresults.account_FK and a.trx_date = searchresults.trx_date and a.SEARCH_NUM = searchresults.SEARCH_NUM)
   when customer_cif is not null then (select sum(cash_out) from searchresults a where a.customer_cif = searchresults.customer_cif and a.trx_date = searchresults.trx_date and a.SEARCH_NUM = searchresults.SEARCH_NUM)
   when customer_tin is not null then (select sum(cash_out) from searchresults a where a.customer_tin = searchresults.customer_tin and a.TIN_Type = searchresults.TIN_Type and a.trx_date = searchresults.trx_date and a.SEARCH_NUM = searchresults.SEARCH_NUM)
   when ID_NUMBER is not null then (select sum(cash_out) from searchresults a where a.ID_NUMBER = searchresults.ID_NUMBER and a.trx_date = searchresults.trx_date and a.SEARCH_NUM = searchresults.SEARCH_NUM)
   else (select sum(cash_out) from searchresults a where a.trx_date = searchresults.trx_date and a.SEARCH_NUM = searchresults.SEARCH_NUM)
   end as 'total_per_day_cash_out'
  ,case when account_FK is not null then (select sum(cash_in) from searchresults a where a.account_FK = searchresults.account_FK and a.SEARCH_NUM = searchresults.SEARCH_NUM)
   when customer_cif is not null then (select sum(cash_in) from searchresults a where a.customer_cif = searchresults.customer_cif and a.SEARCH_NUM = searchresults.SEARCH_NUM)
   when customer_tin is not null then (select sum(cash_in) from searchresults a where a.customer_tin = searchresults.customer_tin and a.TIN_Type = searchresults.TIN_Type and a.SEARCH_NUM = searchresults.SEARCH_NUM)
   when ID_NUMBER is not null then (select sum(cash_in) from searchresults a where a.ID_NUMBER = searchresults.ID_NUMBER and a.SEARCH_NUM = searchresults.SEARCH_NUM)
   else (select sum(cash_in) from searchresults a where a.SEARCH_NUM = searchresults.SEARCH_NUM)
   end as 'total_per_period_cash_in'
  ,case when account_FK is not null then (select sum(cash_out) from searchresults a where a.account_FK = searchresults.account_FK and a.SEARCH_NUM = searchresults.SEARCH_NUM)
   when customer_cif is not null then (select sum(cash_out) from searchresults a where a.customer_cif = searchresults.customer_cif and a.SEARCH_NUM = searchresults.SEARCH_NUM)
   when customer_tin is not null then (select sum(cash_out) from searchresults a where a.customer_tin = searchresults.customer_tin and a.TIN_Type = searchresults.TIN_Type and a.SEARCH_NUM = searchresults.SEARCH_NUM)
   when ID_NUMBER is not null then (select sum(cash_out) from searchresults a where a.ID_NUMBER = searchresults.ID_NUMBER and a.SEARCH_NUM = searchresults.SEARCH_NUM)
   else (select sum(cash_out) from searchresults a where a.SEARCH_NUM = searchresults.SEARCH_NUM)
   end as 'total_per_period_cash_out'
   from SearchResults 

La partie lourde est le résultat de la recherche CTE qui s'appelle encore et encore, et le plan d'exécution a l'air ci-dessous:

enter image description here

Existe-t-il un moyen d'éviter cela en obtenant les mêmes résultatsset et d'éviter ce forte plan d'exécution?

1
giantLincecum

Je pense que vous pouvez écrire du code sous une hypothèse défectueuse: les résultats de la CTE sont persistés

Ils ne sont pas et chaque fois que vous les référez, la syntaxe est ré-exécutée.

Voici un exemple rapide:

CREATE TABLE #dummy
(
    id INT
);
INSERT #dummy ( id )
VALUES ( 1 );


WITH yourmom
AS ( SELECT d.id
     FROM   #dummy AS d )
SELECT ym.id
FROM   yourmom AS ym
JOIN   yourmom AS ym2
ON ym2.id = ym.id
JOIN   yourmom AS ym3
ON ym3.id = ym.id;

Si vous regardez le Plan de requête , il existe trois analyses de la table de base - une fois pour la version initiale FROM et une fois pour chaque JOIN. C'est trois au total.

Cela provoque des problèmes dans quelques endroits:

  1. Votre jointure dans le transactions CTE
  2. Votre à transactions in searchResults
  3. Tous ces COUNT sous-requêtes dans votre choix final de searchresults

Cela conduit à votre deuxième problème: ces deux CASE expressions que vous utilisez pour déterminer vos dates FROM et TO sont totalement non sargable .

Vous avez deux options:

  1. Collez le résultat de votre premier CTE dans un #temp table

  2. Ajoutez des colonnes calculées à votre table de base

J'en choisirais probablement les colonnes calculées dans ce cas

ALTER TABLE dbo.parmeters_table
ADD FROM_DATE_SEARCHED AS CASE WHEN FROM_DATE IS NOT NULL THEN FROM_DATE
                               WHEN FROM_DATE IS NULL
                                    AND SEARCH_DAYS IS NOT NULL
                                    AND TO_DATE IS NOT NULL THEN DATEADD(DAY, SEARCH_DAYS * -1, TO_DATE)
                               WHEN FROM_DATE IS NULL
                                    AND SEARCH_DAYS IS NOT NULL
                                    AND TO_DATE IS NULL THEN DATEADD(DAY, SEARCH_DAYS * -1, GETDATE())
                               WHEN FROM_DATE IS NULL
                                    AND SEARCH_DAYS IS NULL
                                    AND TO_DATE IS NOT NULL
                                    AND DURATION = 'Yearly' THEN DATEADD(YEAR, -1, TO_DATE)
                               WHEN FROM_DATE IS NULL
                                    AND SEARCH_DAYS IS NULL
                                    AND TO_DATE IS NULL
                                    AND DURATION = 'Yearly' THEN DATEADD(YEAR, -1, GETDATE())
                               WHEN FROM_DATE IS NULL
                                    AND SEARCH_DAYS IS NULL
                                    AND TO_DATE IS NOT NULL
                                    AND DURATION = 'Monthly' THEN DATEADD(MONTH, -1, TO_DATE)
                               WHEN FROM_DATE IS NULL
                                    AND SEARCH_DAYS IS NULL
                                    AND TO_DATE IS NULL
                                    AND DURATION = 'Monthly' THEN DATEADD(MONTH, DATEDIFF(MONTH, 0, GETDATE()) - 1, 0)
                               WHEN FROM_DATE IS NULL
                                    AND SEARCH_DAYS IS NULL
                                    AND TO_DATE IS NOT NULL
                                    AND DURATION = 'Weekly' THEN DATEADD(DAY, -7, TO_DATE)
                               WHEN FROM_DATE IS NULL
                                    AND SEARCH_DAYS IS NULL
                                    AND TO_DATE IS NULL
                                    AND DURATION = 'Weekly' THEN DATEADD(DAY, -7, GETDATE())
                               ELSE DATEADD(MONTH, -1, GETDATE())
                          END,
    TO_DATE_SEARCHED AS CASE WHEN TO_DATE IS NOT NULL THEN TO_DATE
                             WHEN TO_DATE IS NULL
                                  AND DURATION = 'Monthly' THEN DATEADD(MONTH, DATEDIFF(MONTH, -1, GETDATE()) - 1, -1)
                             ELSE GETDATE()
                        END;

Vous pouvez indexer ces colonnes pour faire votre jointure à dbo.ixf_transaction plus efficace et évitez de devoir pré-traiter toutes les données, puis y participer.

Et je me pencherais vers une table Temp pour persister les résultats de searchResults. Cela empêche le besoin de réécruder pour chaque expression CASE.

J'espère que cela t'aides!

7
Erik Darling

Existe-t-il un moyen d'éviter cela en obtenant les mêmes résultatsset et d'éviter ce forte plan d'exécution?

Essayez de persister le résultat de CTE SearchResult à une table Temp et utilisez la table TEM dans la requête principale à la place.

4
Mikael Eriksson

En bref, on dirait que vous essayez de faire trop d'une chose en une fois: o)

Ce dernier grand SELECT en bas avec tous les expressions parallèles CASE font partie du problème. Vous devriez tout faire en dessous de cette vue, puis casser cette grande requête à la fin dans l'une quelconque de l'une quelconque JOIN contre elle (avec ces conditions de la clause ON) ou éventuellement cinq requêtes distinctes pour correspondre chacune de ces conditions. De toute façon, vous voulez vraiment vraiment vous débarrasser de ces sous-sélectionnements dans les cas (surtout car ils semblent faire une SUM de la même colonne de chaque branche du CASE Sauf si j'ai raté quelque chose ).

EDIT: OK, je parlais trop tôt, vous semblez avoir des conditions différentes dans certaines des sous-sélectionneuses, mais même pour que vous puissiez réduire cela à deux JOINS, ou encore peut-être seulement deux requêtes distinctes, une pour les données qui nécessitent le a.trx_date = searchresults.trx_date condition et une pour les données qui ne le font pas. Vous comprenez probablement les données mieux à ce stade, alors je ne vais pas continuer à échapper à la profondeur de ma profondeur.

Je transformerais probablement quelques succursales CASE dans votre timeframes CTE dans COALESCE mais c'est juste une question de style et/ou de goût.

1
Will Crawford