J'ai un problème à comprendre pourquoi SQL Server décide d'appeler une fonction définie par l'utilisateur pour chaque valeur de la table même si une seule ligne doit être récupérée. Le SQL réel est beaucoup plus complexe, mais j'ai pu réduire le problème à ceci:
select
S.GROUPCODE,
H.ORDERCATEGORY
from
ORDERLINE L
join ORDERHDR H on H.ORDERID = L.ORDERID
join PRODUCT P on P.PRODUCT = L.PRODUCT
cross apply dbo.GetGroupCode (P.FACTORY) S
where
L.ORDERNUMBER = 'XXX/YYY-123456' and
L.RMPHASE = '0' and
L.ORDERLINE = '01'
Pour cette requête, SQL Server décide d'appeler la fonction GetGroupCode pour chaque valeur unique qui existe dans la table PRODUCT, même si l'estimation et le nombre réel de lignes renvoyées par ORDERLINE est 1 (c'est la clé primaire):
Même plan dans l'explorateur de plans montrant le nombre de lignes:
Les tables:
ORDERLINE: 1.5M rows, primary key: ORDERNUMBER + ORDERLINE + RMPHASE (clustered)
ORDERHDR: 900k rows, primary key: ORDERID (clustered)
PRODUCT: 6655 rows, primary key: PRODUCT (clustered)
L'index utilisé pour l'analyse est:
create unique nonclustered index PRODUCT_FACTORY on PRODUCT (PRODUCT, FACTORY)
La fonction est en fait légèrement plus complexe, mais la même chose se produit avec une fonction multi-instructions factice comme celle-ci:
create function GetGroupCode (@FACTORY varchar(4))
returns @t table(
TYPE varchar(8),
GROUPCODE varchar(30)
)
as begin
insert into @t (TYPE, GROUPCODE) values ('XX', 'YY')
return
end
J'ai pu "corriger" les performances en forçant SQL Server à récupérer le premier produit, bien que 1 soit le maximum que l'on puisse trouver:
select
S.GROUPCODE,
H.ORDERCAT
from
ORDERLINE L
join ORDERHDR H
on H.ORDERID = M.ORDERID
cross apply (select top 1 P.FACTORY from PRODUCT P where P.PRODUCT = L.PRODUCT) P
cross apply dbo.GetGroupCode (P.FACTORY) S
where
L.ORDERNUMBER = 'XXX/YYY-123456' and
L.RMPHASE = '0' and
L.ORDERLINE = '01'
Ensuite, la forme du plan change également pour être quelque chose que je m'attendais à ce qu'il soit à l'origine:
J'ai également pensé que l'index PRODUCT_FACTORY étant plus petit que l'index cluster PRODUCT_PK aurait un effet, mais même en forçant la requête à utiliser PRODUCT_PK, le plan est toujours le même que l'original, avec 6655 appels à la fonction.
Si je laisse complètement ORDERHDR, le plan commence par la boucle imbriquée entre ORDERLINE et PRODUCT en premier, et la fonction n'est appelée qu'une seule fois.
Je voudrais comprendre quelle pourrait être la raison de cela car toutes les opérations sont effectuées à l'aide de clés primaires et comment y remédier si cela se produit dans une requête plus complexe qui ne peut pas être résolue aussi facilement.
Modifier: créer des instructions de table:
CREATE TABLE dbo.ORDERHDR(
ORDERID varchar(8) NOT NULL,
ORDERCATEGORY varchar(2) NULL,
CONSTRAINT ORDERHDR_PK PRIMARY KEY CLUSTERED (ORDERID)
)
CREATE TABLE dbo.ORDERLINE(
ORDERNUMBER varchar(16) NOT NULL,
RMPHASE char(1) NOT NULL,
ORDERLINE char(2) NOT NULL,
ORDERID varchar(8) NOT NULL,
PRODUCT varchar(8) NOT NULL,
CONSTRAINT ORDERLINE_PK PRIMARY KEY CLUSTERED (ORDERNUMBER,ORDERLINE,RMPHASE)
)
CREATE TABLE dbo.PRODUCT(
PRODUCT varchar(8) NOT NULL,
FACTORY varchar(4) NULL,
CONSTRAINT PRODUCT_PK PRIMARY KEY CLUSTERED (PRODUCT)
)
Il y a trois raisons techniques principales pour lesquelles vous obtenez le plan que vous faites:
Compte tenu de la petite estimation de cardinalité attribuée à l'application UDF, l'heuristique d'expansion de jointure n-aire la repositionne malheureusement plus tôt dans l'arborescence que vous ne le souhaiteriez.
La requête se qualifie également pour l'optimisation de la recherche 0 en raison d'avoir au moins trois jointures (y compris s'applique). Le plan physique final que vous obtenez, avec l'analyse étrange, est basé sur cet ordre de jointure déduit heuristiquement. Son coût est suffisamment bas pour que l'optimiseur considère le plan comme "assez bon". L'estimation de faible coût et la cardinalité de l'UDF contribuent à cette finition précoce.
La recherche 0 (également appelée phase de traitement des transactions) cible les requêtes de type OLTP à faible cardinalité, avec des plans finaux qui comportent généralement des jointures de boucles imbriquées. Plus important encore, la recherche 0 n'exécute qu'un sous-ensemble relativement petit des capacités d'exploration de l'optimiseur. Ce sous-ensemble n'inclut pas l'extraction d'une application dans l'arborescence de requête sur une jointure (règle PullApplyOverJoin
). C'est exactement ce qui est nécessaire dans le cas de test pour repositionner l'application UDF au-dessus des jointures, pour apparaître en dernier dans la séquence des opérations (pour ainsi dire).
Il existe également un problème où l'optimiseur peut décider entre la jointure de boucles imbriquées naïve (prédicat de jointure sur la jointure elle-même) et une jointure indexée corrélée (appliquer) où le prédicat corrélé est appliqué sur le côté intérieur de la jointure à l'aide d'une recherche d'index. Ce dernier a généralement la forme de plan souhaitée, mais l'optimiseur est capable d'explorer les deux. Avec des estimations de coût et de cardinalité incorrectes, il peut choisir la jointure NL non applicable, comme dans les plans soumis (expliquant l'analyse).
Il existe donc plusieurs raisons d'interaction impliquant plusieurs fonctionnalités d'optimisation générales qui fonctionnent normalement bien pour trouver de bons plans dans un court laps de temps sans utiliser de ressources excessives. Il suffit d'éviter l'une des raisons pour produire la forme de plan "attendue" pour l'exemple de requête, même avec des tables vides:
Il n'existe aucun moyen pris en charge pour éviter la sélection du plan de recherche 0, l'arrêt anticipé de l'optimiseur ou pour améliorer le coût des FDU (à part les améliorations limitées du modèle SQL Server 2014 CE pour cela). Cela laisse des choses comme les guides de plan, les réécritures de requêtes manuelles (y compris l'idée TOP (1)
ou l'utilisation de tables temporaires intermédiaires) et évitant les `` boîtes noires '' peu coûteuses (du point de vue QO) comme les fonctions non en ligne.
Réécriture CROSS APPLY
comme OUTER APPLY
peut également fonctionner, car il empêche actuellement certains des premiers travaux de regroupement des jointures, mais vous devez faire attention à conserver la sémantique de la requête d'origine (par exemple, en rejetant toutes les lignes étendues NULL
- qui pourraient être introduites, sans l'optimiseur se repliant en croix). Cependant, vous devez savoir que ce comportement n'est pas garanti pour rester stable, vous devez donc vous rappeler de tester à nouveau tous les comportements observés à chaque fois que vous corrigez ou mettez à niveau SQL Server.
Dans l'ensemble, la bonne solution pour vous dépend d'une variété de facteurs que nous ne pouvons pas juger pour vous. Cependant, je vous encourage à envisager des solutions qui sont garanties de toujours fonctionner à l'avenir et qui fonctionnent avec (plutôt que contre) l'optimiseur dans la mesure du possible.
Il semble que ce soit une décision basée sur les coûts de l'optimiseur mais plutôt mauvaise.
Si vous ajoutez 50000 lignes à PRODUCT, l'optimiseur pense que le scan est trop de travail et vous donne un plan avec trois recherches et un appel à l'UDF.
Le plan que j'obtiens pour 6655 lignes dans PRODUCT
Avec 50000 lignes dans PRODUCT, j'obtiens ce plan à la place.
Je suppose que le coût d'appeler l'UDF est largement sous-estimé.
Une solution de contournement qui fonctionne correctement dans ce cas consiste à modifier la requête pour utiliser une application externe contre l'UDF. J'obtiens le bon plan, peu importe le nombre de lignes dans la table PRODUIT.
select
S.GROUPCODE,
H.ORDERCATEGORY
from
ORDERLINE L
join ORDERHDR H on H.ORDERID = L.ORDERID
join PRODUCT P on P.PRODUCT = L.PRODUCT
outer apply dbo.GetGroupCode (P.FACTORY) S
where
L.ORDERNUMBER = 'XXX/YYY-123456' and
L.RMPHASE = '0' and
L.ORDERLINE = '01' and
S.GROUPCODE is not null
La meilleure solution dans votre cas est probablement d'obtenir les valeurs dont vous avez besoin dans une table temporaire, puis d'interroger la table temporaire avec une croix à appliquer à l'UDF. De cette façon, vous êtes sûr que l'UDF ne sera pas exécuté plus que nécessaire.
select
P.FACTORY,
H.ORDERCATEGORY
into #T
from
ORDERLINE L
join ORDERHDR H on H.ORDERID = L.ORDERID
join PRODUCT P on P.PRODUCT = L.PRODUCT
where
L.ORDERNUMBER = 'XXX/YYY-123456' and
L.RMPHASE = '0' and
L.ORDERLINE = '01'
select
S.GROUPCODE,
T.ORDERCATEGORY
from #T as T
cross apply dbo.GetGroupCode (T.FACTORY) S
drop table #T
Au lieu de persister dans la table temporaire, vous pouvez utiliser top()
dans une table dérivée pour forcer SQL Server à évaluer le résultat des jointures avant l'appel de l'UDF. Utilisez simplement un nombre très élevé en haut, ce qui oblige SQL Server à compter vos lignes pour cette partie de la requête avant de pouvoir continuer et utiliser l'UDF.
select S.GROUPCODE,
T.ORDERCATEGORY
from (
select top(2147483647)
P.FACTORY,
H.ORDERCATEGORY
from
ORDERLINE L
join ORDERHDR H on H.ORDERID = L.ORDERID
join PRODUCT P on P.PRODUCT = L.PRODUCT
where
L.ORDERNUMBER = 'XXX/YYY-123456' and
L.RMPHASE = '0' and
L.ORDERLINE = '01'
) as T
cross apply dbo.GetGroupCode (T.FACTORY) S
Je voudrais comprendre quelle pourrait être la raison de cela car toutes les opérations sont effectuées à l'aide de clés primaires et comment y remédier si cela se produit dans une requête plus complexe qui ne peut pas être résolue aussi facilement.
Je ne peux vraiment pas répondre à cela, mais j'ai pensé que je devrais partager ce que je sais de toute façon. Je ne sais pas pourquoi une analyse de la table PRODUCT est envisagée. Il peut y avoir des cas où c'est la meilleure chose à faire et il y a des choses concernant la façon dont les optimiseurs traitent les FDU que je ne connais pas.
Une observation supplémentaire était que votre requête obtient un bon plan dans SQL Server 2014 avec le nouvel estimateur de cardinalité. En effet, le nombre estimé de lignes pour chaque appel à l'UDF est de 100 au lieu de 1, comme c'est le cas dans SQL Server 2012 et avant. Mais il prendra toujours la même décision basée sur les coûts entre la version de scan et la version de recherche du plan. Avec moins de 500 lignes (497 dans mon cas) dans PRODUCT, vous obtenez la version d'analyse du plan même dans SQL Server 2014.