Nous avons une requête spécifique qui s'exécute beaucoup plus lentement à l'intérieur d'un proc. Je dois ajouter ici qu'il est enfermé dans un curseur à deux niveaux. Cependant, les deux curseurs ont un ensemble de résultats d'itération d'une ligne.
Permettez-moi d’abord de dire ce que nous avons essayé et échoué:
Voici la requête telle que prise à l'intérieur des proc/curseurs.
select @tpdim1 = dim1, @tpdim2 = dim2, @typecalc = typecalc
from loyalty_policy where code=@loop2_loyalty_policy
Remarque: @ loop2_loyalty_policy est la variable extraite du résultat du curseur interne et a une valeur. code
est PK dans la table loyalty_policy
. Ainsi, @ tpdim1 et @ tpdim2 ont chacun une valeur unique.
SET STATISTICS PROFILE ON
SET STATISTICS xml on
insert into @tbl_loyal_loop2 (cnt, store, map, pda, insdate, line, item, loyalty_policy_data, loyal_calc, loyalty_policy)
select @cnt, t.store, t.map, t.pda, t.insdate, t.line, t.item, ld.tab_id,
case @typecalc
when 1 then convert(bigint,round(isnull(t.valueFromTran,0.00) * ld.value , 0 ) )
when 2 then convert(bigint,round(isnull(t.netvalue,0.00) * ld.value , 0 ) )
when 3 then convert(bigint,isnull(t.qty,0) * ld.value )
when 4 then convert(bigint,round(isnull(t.valueFromPrice2,0.00) * ld.value , 0 ) )
when 5 then convert(bigint,round(isnull(t.valueFromPrice3,0.00) * ld.value , 0 ) )
when 6 then convert(bigint,round(isnull(t.valueFromPrice4,0.00) * ld.value , 0 ) )
else 0 end
,@loop2_loyalty_policy
from loyalty_policy_data ld-- with (index=ind_loyalty_policy_02)
inner join #tbl_data t on t.insdate >= ld.fdateactive and t.insdate <= ld.tdateactive
where ld.loyalty_policy = @loop2_loyalty_policy
and ld.tdateactive >= @from_rundate and ld.fdateactive <= @to_rundate
and t.dbupddate > @loop1_dbupddate
and
case when @tpdim1 is null then ''
else
case @tpdim1
when 'STORE' then t.store when 'BRAND' then t.brand when 'CAT1' then t.cat1 when 'CAT2' then t.cat2 when 'CAT3' then t.cat3 when 'ITEM' then t.item
when 'CUSTGROUP' then t.custgroup when 'CUSTGROUP2' then t.custgroup2 when 'CUSTGROUP3' then t.custgroup3
when 'CUSTOMER' then @customer
else '' end
end
= case when @tpdim1 is null then '' else ld.dim1 end
and
case when @tpdim2 is null then ''
else
case @tpdim2
when 'STORE' then t.store when 'BRAND' then t.brand when 'CAT1' then t.cat1 when 'CAT2' then t.cat2 when 'CAT3' then t.cat3 when 'ITEM' then t.item
when 'CUSTGROUP' then t.custgroup when 'CUSTGROUP2' then t.custgroup2 when 'CUSTGROUP3' then t.custgroup3
when 'CUSTOMER' then @customer
else '' end
end
= case when @tpdim2 is null then '' else ld.dim2 end
SET STATISTICS xml off
Le SET STATISTICS XML
pour ce qui précède renvoie ce plan .
En essayant de le déboguer, nous avons isolé la requête sous la forme suivante (ici, vous pouvez également voir comment la table #a est créée, laquelle contient exactement les mêmes données que la précédente #tbl_data):
drop table #a;
select dt.dbupddate, dt.insdate, dt.map, dt.pda, pt.line, pt.item,
( pt.exp_qty - pt.imp_qty) as qty,
( pt.exp_value + pt.imp_value ) as netvalue,
( (document.exp_val - document.imp_val) * (pt.netvalue - pt.vat_value) ) as valueFromTran,
( (document.exp_val - document.imp_val) * ( ( (pt.qty - pt.qty_gift) * isnull(pt.price2,0.00) ) * (1.00-( pt.disc_perc / 100)) ) ) as valueFromPrice2,
( (document.exp_val - document.imp_val) * ( ( (pt.qty - pt.qty_gift) * isnull(pt.price3,0.00) ) * (1.00-( pt.disc_perc / 100)) ) ) as valueFromPrice3,
( (document.exp_val - document.imp_val) * ( ( (pt.qty - pt.qty_gift) * isnull(pt.price4,0.00) ) * (1.00-( pt.disc_perc / 100)) ) ) as valueFromPrice4,
dt.store, item.brand, item.cat1, item.cat2, item.cat3, customer.custgroup, customer.custgroup2, customer.custgroup3
into #a
from document with (nolock)
inner join dt with (nolock) on dt.doccode = document.code
inner join store with (nolock) on store.code = dt.store and store.calc_loyal = 1
inner join customer with (nolock) on customer.code = dt.customer
inner join pt with (nolock) on dt.map = pt.map and dt.pda=pt.pda
inner join item with (nolock) on item.code = pt.item and item.itemtype in (select code from itemtype with (nolock) where vsales = 1)
where dt.canceled = 0 and document.is_opposite = 0 and document.type = 3 and dt.customer=N'EL4444444'
and dt.insdate >= '20180109' and dt.insdate <= '20190108' ;
SET STATISTICS PROFILE ON
select t.store, t.map, t.pda, t.insdate, t.line, t.item, ld.tab_id,
case 4
when 1 then convert(bigint,round(isnull(t.valueFromTran,0.00) * ld.value , 0 ) )
when 2 then convert(bigint,round(isnull(t.netvalue,0.00) * ld.value , 0 ) )
when 3 then convert(bigint,isnull(t.qty,0) * ld.value )
when 4 then convert(bigint,round(isnull(t.valueFromPrice2,0.00) * ld.value , 0 ) )
when 5 then convert(bigint,round(isnull(t.valueFromPrice3,0.00) * ld.value , 0 ) )
when 6 then convert(bigint,round(isnull(t.valueFromPrice4,0.00) * ld.value , 0 ) )
else 0 end
,'003'
--select count(*)
from loyalty_policy_data ld with (index=ind_loyalty_policy_02)
inner join #a t on t.insdate >= ld.fdateactive and t.insdate <= ld.tdateactive
where ld.loyalty_policy = '003'
--and ld.tdateactive >= '20180109' and ld.fdateactive <= '20190108'
and t.dbupddate > '20000101'
and
case when 'CUSTOMER' is null then ''
else
case 'CUSTOMER'
when 'STORE' then t.store when 'BRAND' then t.brand when 'CAT1' then t.cat1 when 'CAT2' then t.cat2 when 'CAT3' then t.cat3 when 'ITEM' then t.item
when 'CUSTGROUP' then t.custgroup when 'CUSTGROUP2' then t.custgroup2 when 'CUSTGROUP3' then t.custgroup3
when 'CUSTOMER' then 'EL0134366'
else '' end
end
= case when 'CUSTOMER' is null then '' else ld.dim1 end
and
case when 'BRAND' is null then ''
else
case 'BRAND'
when 'STORE' then t.store when 'BRAND' then t.brand when 'CAT1' then t.cat1 when 'CAT2' then t.cat2 when 'CAT3' then t.cat3 when 'ITEM' then t.item
when 'CUSTGROUP' then t.custgroup when 'CUSTGROUP2' then t.custgroup2 when 'CUSTGROUP3' then t.custgroup3
when 'CUSTOMER' then 'EL0134366'
else '' end
end
= case when 'BRAND' is null then '' else ld.dim2 end
SET STATISTICS PROFILE off
Et ici est le plan d'exécution. Cela fonctionne beaucoup plus vite.
Pourquoi cette énorme différence? De ma connaissance limitée de l'analyse de l'exécution, j'ai remarqué
index spool
, a une estimation de lignes de ~ 9700 mais des lignes réelles de 3 millions. Pourriez-vous expliquer cette différence et proposer des conseils pour résoudre le problème?
Edit: Comme recommandé par Laughing Vergil, j’ai remplacé les littéraux de la seconde requête par des variables précédemment déclarées.
Edit 2: J'ai quelques informations supplémentaires provenant de recherches plus poussées.
Premièrement, j'ai isolé le problème à cette ligne:
case when @tpdim1 is null then ''
<- Ceci utilise le plan lent
case when 'CUSTOMER' is null then ''
<- Ceci utilise le plan rapide
Ceci est vrai dans la requête ad-hoc, pas besoin de se soucier de spcs et/ou de curseurs.
Cela continue même si je modifie le code selon la structure dynamique recommandée.
Je n'ai pas encore créé de données d'échantillons, mais l'info importante (comme on peut le voir dans les plans) est que loyalty_policy_data
contient environ 720k lignes si nous ne filtrons que par loyalty_policy = @loop2_loyalty_policy
. Toutefois, si nous évaluons la condition @ tpdim1, qui est essentiellement dim1 = N'EL0134366 ', les lignes renvoyées ne sont que 4.
La différence dans le plan réside donc dans l’évaluation de cette condition par rapport aux conditions de contrôle de la date.
Dans le plan rapide, il est évalué en premier. Lors de la recherche de l'index pour la valeur de la stratégie de fidélité, un prédicat (non recherché) est ajouté. Bien que ce prédicat ne soit pas dans l'index, les lignes renvoyées sont 4 et tous les autres opérateurs ont des tailles "logiques".
En revanche, le plan lent ignore douloureusement ce prédicat trop tard. Si j'ai correctement interprété, cela crée une boucle imbriquée dans loyalty_policy_data en tant que table externe (ce qui est fou). Il passe les colonnes nécessaires en tant que références externes. Pour chacun de ces Tuple, la spool d'index analyse la #table (~ 1k lignes) et trouve environ 250 résultats, puis transmet au filtre lequel enfin effectue le filtrage tpdim1. Ainsi, 250 * 700k lignes sont transmises à l'opérateur de filtrage.
Alors maintenant, je pense savoir ce qui se passe. Mais je ne peux pas comprendre pourquoi.
Pour répondre à ta question:
Une explication claire et reproductible du comment et du pourquoi de l’analyseur de requêtes se comporte différemment dans ces cas
L'optimiseur de requêtes se comporte différemment dans ces cas-là, car le plan avec variables doit être valide pour toute valeur future possible des paramètres any. Aussi, optimiser génère un plan générique complexe qui produirait des résultats corrects même lorsque les paramètres sont NULL.
Le plan avec des littéraux (et non des variables) est généralement plus efficace, car l’optimiseur peut grandement simplifier votre logique CASE
pendant la phase de compilation du plan. L'optimiseur a plus de chances de choisir la forme de plan optimale, car il est plus facile pour l'optimiseur de prendre en compte les informations disponibles sur les index et les estimations de cardinalité lorsque la requête est plus simple et que les filtres ont des valeurs connues.
Martin Smith dans le commentaire, vous indiquez que vous utilisez la version du serveur 10.0.2531.0, version 2008 SP1, pour laquelle l'optimisation de l'intégration du paramètre n'est pas activée. Vous aurez besoin de moins de SP1 CU5 sur cette branche pour que la fonction OPTION (RECOMPILE)
fonctionne correctement (comme je l’attendais dans les explications ci-dessous).
Erland Sommarskog en parle également dans son article mentionné ci-dessous. Il dit que vous devez être sur au moins SP2.
Si vous ne parvenez pas à mettre à jour le serveur, consultez l'ancienne version de l'article d'Erland Conditions de recherche dynamique dans la version T-SQL pour SQL 2005 et versions antérieures pour savoir comment gérer cette situation lorsque OPTION (RECOMPILE)
correct disponible.
Voici ma réponse originale.
Je sais que vous avez dit que vous aviez essayé, mais je vous demanderais quand même de vérifier. En regardant vos symptômes, OPTION (RECOMPILE)
devrait vous aider.
Vous devez ajouter cette option à la requête principale. Pas à l'ensemble de la procédure stockée. Comme ça:
insert into @tbl_loyal_loop2 (cnt, store, map, pda, insdate, line, item, loyalty_policy_data, loyal_calc, loyalty_policy)
select @cnt, t.store, t.map, t.pda, t.insdate, t.line, t.item, ld.tab_id,
case @typecalc
when 1 then convert(bigint,round(isnull(t.valueFromTran,0.00) * ld.value , 0 ) )
when 2 then convert(bigint,round(isnull(t.netvalue,0.00) * ld.value , 0 ) )
when 3 then convert(bigint,isnull(t.qty,0) * ld.value )
when 4 then convert(bigint,round(isnull(t.valueFromPrice2,0.00) * ld.value , 0 ) )
when 5 then convert(bigint,round(isnull(t.valueFromPrice3,0.00) * ld.value , 0 ) )
when 6 then convert(bigint,round(isnull(t.valueFromPrice4,0.00) * ld.value , 0 ) )
else 0 end
,@loop2_loyalty_policy
from loyalty_policy_data ld -- with (index=ind_loyalty_policy_02)
inner join #tbl_data t on t.insdate >= ld.fdateactive and t.insdate <= ld.tdateactive
where ld.loyalty_policy = @loop2_loyalty_policy
and ld.tdateactive >= @from_rundate and ld.fdateactive <= @to_rundate
and t.dbupddate > @loop1_dbupddate
and
case when @tpdim1 is null then ''
else
case @tpdim1
when 'STORE' then t.store when 'BRAND' then t.brand when 'CAT1' then t.cat1 when 'CAT2' then t.cat2 when 'CAT3' then t.cat3 when 'ITEM' then t.item
when 'CUSTGROUP' then t.custgroup when 'CUSTGROUP2' then t.custgroup2 when 'CUSTGROUP3' then t.custgroup3
when 'CUSTOMER' then @customer
else '' end
end
= case when @tpdim1 is null then '' else ld.dim1 end
and
case when @tpdim2 is null then ''
else
case @tpdim2
when 'STORE' then t.store when 'BRAND' then t.brand when 'CAT1' then t.cat1 when 'CAT2' then t.cat2 when 'CAT3' then t.cat3 when 'ITEM' then t.item
when 'CUSTGROUP' then t.custgroup when 'CUSTGROUP2' then t.custgroup2 when 'CUSTGROUP3' then t.custgroup3
when 'CUSTOMER' then @customer
else '' end
end
= case when @tpdim2 is null then '' else ld.dim2 end
OPTION(RECOMPILE);
OPTION (RECOMPILE)
n’est pas le cas pour alléger le reniflage de paramètres, mais pour permettre à l’optimiseur d’aligner les valeurs réelles des paramètres dans la requête. Cela donne à l'optimiseur la liberté de simplifier la logique de requête.
Le type de votre requête ressemble à Conditions de recherche dynamique et je vous recommande vivement de lire cet article de Erland Sommarskog.
Aussi, au lieu de
and
case when @tpdim1 is null then ''
else
case @tpdim1
when 'STORE' then t.store when 'BRAND' then t.brand when 'CAT1' then t.cat1 when 'CAT2' then t.cat2 when 'CAT3' then t.cat3 when 'ITEM' then t.item
when 'CUSTGROUP' then t.custgroup when 'CUSTGROUP2' then t.custgroup2 when 'CUSTGROUP3' then t.custgroup3
when 'CUSTOMER' then @customer
else '' end
end
= case when @tpdim1 is null then '' else ld.dim1 end
Je l'écrirais un peu différemment:
and
(
@tpdim1 is null
OR
(
ld.dim1 =
case @tpdim1
when 'STORE' then t.store
when 'BRAND' then t.brand
when 'CAT1' then t.cat1
when 'CAT2' then t.cat2
when 'CAT3' then t.cat3
when 'ITEM' then t.item
when 'CUSTGROUP' then t.custgroup
when 'CUSTGROUP2' then t.custgroup2
when 'CUSTGROUP3' then t.custgroup3
when 'CUSTOMER' then @customer
else ''
end
)
)
Avec OPTION (RECOMPILE)
lorsque @tpdim1
a pour valeur CUSTOMER
et que @customer
a pour valeur EL0134366
, l’optimiseur doit transformer cette instruction en une simple
and
(
ld.dim1 = `EL0134366`
)
il pourrait ensuite utiliser un index approprié ou estimer le nombre de lignes de manière plus précise et prendre une meilleure décision quant à la forme du plan. Avec cette option, le plan ne serait valide que pour cette valeur spécifique du paramètre.
Notez que option (optimize for UNKNOWN)
ne peut pas aider ici. optimize for UNKNOWN
devrait générer un plan générique valide pour toute valeur possible de paramètres.
après avoir nettoyé la requête à des fins de lisibilité, j’ai le texte suivant.
insert into @tbl_loyal_loop2
( cnt,
store,
map,
pda,
insdate,
line,
item,
loyalty_policy_data,
loyal_calc,
loyalty_policy
)
select
@cnt,
t.store,
t.map,
t.pda,
t.insdate,
t.line,
t.item,
ld.tab_id,
convert(bigint, round( coalesce(
case @typecalc
when 1 then t.valueFromTran
when 2 then t.netvalue
when 3 then t.qty
when 4 then t.valueFromPrice2
when 5 then t.valueFromPrice3
when 6 then t.valueFromPrice4
else 0
END, 0.00) * ld.value , 0 ) ),
@loop2_loyalty_policy
from
loyalty_policy_data ld -- with (index=ind_loyalty_policy_02)
inner join #tbl_data t
on t.insdate >= ld.fdateactive
and t.insdate <= ld.tdateactive
where
ld.loyalty_policy = @loop2_loyalty_policy
and ld.tdateactive >= @from_rundate
and ld.fdateactive <= @to_rundate
and t.dbupddate > @loop1_dbupddate
and ( @tpdim1 is null
OR ld.dim1 = case @tpdim1
when 'STORE' then t.store
when 'BRAND' then t.brand
when 'CAT1' then t.cat1
when 'CAT2' then t.cat2
when 'CAT3' then t.cat3
when 'ITEM' then t.item
when 'CUSTGROUP' then t.custgroup
when 'CUSTGROUP2' then t.custgroup2
when 'CUSTGROUP3' then t.custgroup3
when 'CUSTOMER' then @customer
else ''
END )
and ( @tpdim2 is null
OR ld.dim2 = case when @tpdim1
when 'STORE' then t.store
when 'BRAND' then t.brand
when 'CAT1' then t.cat1
when 'CAT2' then t.cat2
when 'CAT3' then t.cat3
when 'ITEM' then t.item
when 'CUSTGROUP' then t.custgroup
when 'CUSTGROUP2' then t.custgroup2
when 'CUSTGROUP3' then t.custgroup3
when 'CUSTOMER' then @customer
else ''
END )
De plus, je m'assurerais que vous ayez un index composite dans votre table loyalty_policy_data ... index sur (loyalty_policy, tdateactive, fdateactive, dbupdate, dim1, dim2)
De cette façon, vous qualifiez tous les champs utilisés dans vos critères de filtrage WHERE. Ne comptez pas uniquement sur l'index de la clé ... mais la clé PLUS les dates vous aidera à optimiser la plage de dates spécifique sans avoir à revenir aux pages de données brutes, mais vous pouvez optimiser les conditions de requête de la requête JOIN en fonction des valeurs de l'INDEX. .
En ce qui concerne votre table temporaire #tbl_data, assurez-vous que vous avez un index (insdate) car il s'agit du seul critère de base JOIN (au cas où vous ne possédez pas déjà d'index sur cette table).
COMMENTAIRE -
De votre commentaire sur la requête lente vs rapide basée sur la valeur null de
@ tpdim1 = NULL vs 'CUSTOMER' = NULL
une chaîne fixe 'CUSTOMER' n'est JAMAIS nulle, il n'est donc jamais nécessaire de la prendre en compte dans le chemin nul. Chaîne fixe 'CUSTOMER' vs la variable @ client étant nulle ou comparée à dans le cas/quand ld.dim1 et ld.dim2 respectivement comparés à null ... peut-être que ce qui doit être testé doit être changé
and ( @tpdim1 is null
OR ld.dim1 = case @tpdim1
when 'STORE' then t.store
when 'BRAND' then t.brand ... end
)
à
and ld.dim1 = case @tpdim1
when NULL then ''
when 'STORE' then t.store
when 'BRAND' then t.brand ... end
Idem avec le cas/quand ld.dim2. Incluez le "NULL" comme première valeur testée pour les tests @ tpdim1 (et @ tpdim2).
De manière générale, une requête avec literal value
est plus rapide qu'une requête avec proc parameter
ou local variable
.
Lorsque la valeur littérale est utilisée, Optimizer
créera un plan spécial uniquement pour cette valeur si Forced Parameterization
n'est pas activé.
Optimizer peut également créer Trivial Plan
ou Simple Parameterize Plan
, mais dans votre cas, cela n’est pas vrai.
Lorsque vous utilisez un paramètre, l'optimiseur crée un plan pour ce paramètre .__, appelé Parameter Sniffing
, .__, puis le réutilise.
Option Recompile
est un moyen de résoudre ce problème: Créez un plan pour chaque valeur de variable différente, afin de conserver «l'estimation de cardinalité». C'est très court
Par conséquent, une requête avec une valeur littérale sera toujours plus rapide.
Permettez-moi d’abord de dire ce que nous avons essayé et échoué:
• Éviter de rechercher des paramètres à l'aide de l'option (recompiler) et de l'option (optiimize for (@var UNKOWN)
•Ce fil. Les variables qui semblent être le problème sont en réalité les locales et non les paramètres de proc.
Vous échouez car votre requête est très mal écrite (avec tout le respect que je vous dois).
Ne pas utiliser le curseur. Il semble que le curseur peut être évité dans votre cas
Une requête proc complète avec un paramètre variable, car la logique pour obtenir une valeur dans @ loop2_loyalty_policy, etc., n'est pas claire. Cela aidera à donner la suggestion correcte "pour éviter le curseur".
case when @tpdim1 is null
: Cette logique complète peut être créée et insérée dans la table Temp elle-même, de sorte que la nouvelle colonne soit immédiatement utilisée dans join. J'espère que vous êtes capable de comprendre mon idée et son langage.
1.La première requête (lente), sur l'opération de spool d'index, contient environ 9700 lignes environ, mais les lignes effectives sont de 3 millions.
En raison d'une estimation élevée de la cardinalité par optmizer, en cas de mauvaise connexion
Je ne sais pas si cela améliorera certainement votre requête et l'estimation de la cardinalité, car je n'ai pas encore bien compris votre requête.
Mais changer de condition de joint aide souvent comme,
Lisez attentivement ici, je ne sais pas quelles sont les données présentes dans les colonnes loyalty_policy
et t.insdate
.Il ne semble pas que vous ayez besoin d'une jointure aussi compliquée que celle décrite ci-dessous.
Au cas où vous en auriez vraiment besoin, vous pouvez alter join condition
comme ci-dessous une fois.
from loyalty_policy_data ld with (nolock)
inner join #tbl_data t on ld.loyalty_policy = @loop2_loyalty_policy
and ld.tdateactive >= @from_rundate and ld.fdateactive <= @to_rundate
and t.insdate >= ld.fdateactive and t.insdate <= ld.tdateactive
where t.dbupddate > @loop1_dbupddate
L'objectif principal est d'éviter le curseur.