Créer un guide de plan pour mettre en cache (bobine différée) le résultat CTE


19

Normalement, je crée des guides de plan en construisant d'abord une requête qui utilise le plan correct, et en le copiant dans la requête similaire qui ne fonctionne pas. Cependant, cela est parfois délicat, surtout si la requête n'est pas exactement la même. Quelle est la bonne façon de créer des repères de plan à partir de zéro?

SQLKiwi a mentionné l'élaboration de plans dans SSIS, existe-t-il un moyen ou un outil utile pour aider à établir un bon plan pour SQL Server?

L'instance spécifique en question est ce CTE: SQLFiddle

with cte(guid,other) as (
  select newid(),1 union all
  select newid(),2 union all
  select newid(),3)
select a.guid, a.other, b.guid guidb, b.other otherb
from cte a
cross join cte b
order by a.other, b.other;

Y at - il QUELQUE moyen de rendre le résultat venir avec exactement 3 distinctes guids et pas plus? J'espère être en mesure de mieux répondre aux questions à l'avenir en incluant des guides de plan avec des requêtes de type CTE qui sont référencées plusieurs fois pour surmonter certaines bizarreries de SQL Server CTE.


@Quassnoi a
Martin Smith

Réponses:


14

Existe-t-il un moyen de rendre le résultat avec exactement 3 guides distincts et pas plus? J'espère être en mesure de mieux répondre aux questions à l'avenir en incluant des guides de plan avec des requêtes de type CTE qui sont référencées plusieurs fois pour surmonter certaines bizarreries de SQL Server CTE.

Pas aujourd'hui. Les expressions de table communes non récursives (CTE) sont traitées comme des définitions de vue en ligne et développées dans l'arborescence de requête logique à chaque endroit où elles sont référencées (tout comme les définitions de vue normales) avant l'optimisation. L'arbre logique de votre requête est:

LogOp_OrderByCOL: Union1007 ASC COL: Union1015 ASC 
    LogOp_Project COL: Union1006 COL: Union1007 COL: Union1014 COL: Union1015
        LogOp_Join
            LogOp_ViewAnchor
                LogOp_UnionAll
                    LogOp_Project ScaOp_Intrinsic newid, ScaOp_Const
                    LogOp_Project ScaOp_Intrinsic newid, ScaOp_Const
                    LogOp_Project ScaOp_Intrinsic newid, ScaOp_Const

            LogOp_ViewAnchor
                LogOp_UnionAll
                    LogOp_Project ScaOp_Intrinsic newid, ScaOp_Const
                    LogOp_Project ScaOp_Intrinsic newid, ScaOp_Const
                    LogOp_Project ScaOp_Intrinsic newid, ScaOp_Const

Remarquez les deux ancres de vue et les six appels à la fonction intrinsèque newidavant que l'optimisation ne démarre. Néanmoins, de nombreuses personnes considèrent que l'optimiseur devrait être capable d'identifier que les sous-arborescences développées étaient à l'origine un seul objet référencé et de simplifier en conséquence. Il y a également eu plusieurs demandes Connect pour permettre la matérialisation explicite d'un CTE ou d'une table dérivée.

Une implémentation plus générale aurait pour optimiseur d'envisager de matérialiser des expressions communes arbitraires pour améliorer les performances ( CASEavec une sous-requête est un autre exemple où des problèmes peuvent survenir aujourd'hui). Microsoft Research a publié un document (PDF) à ce sujet en 2007, bien qu'il reste à ce jour non implémenté. Pour le moment, nous sommes limités à une matérialisation explicite utilisant des choses comme les variables de table et les tables temporaires.

SQLKiwi a mentionné l'élaboration de plans dans SSIS, existe-t-il un moyen ou un outil utile pour aider à établir un bon plan pour SQL Server?

C'était juste un vœu pieux de ma part et cela allait bien au-delà de l'idée de modifier les guides de plan. Il est possible, en principe, d'écrire un outil pour manipuler directement le XML de show plan, mais sans une instrumentation d'optimiseur spécifique, l'utilisation de l'outil serait probablement une expérience frustrante pour l'utilisateur (et le développeur y penserait).

Dans le contexte particulier de cette question, un tel outil serait toujours incapable de matérialiser le contenu CTE d'une manière qui pourrait être utilisée par plusieurs consommateurs (pour alimenter les deux entrées à la jointure croisée dans ce cas). L'optimiseur et le moteur d'exécution prennent en charge les bobines multi-consommateurs, mais uniquement à des fins spécifiques - dont aucune ne pourrait être appliquée à cet exemple particulier.

Bien que je ne sois pas certain, j'ai une intuition assez forte que les RelOps peuvent être suivies (boucle imbriquée, bobine différée) même si la requête n'est pas exactement la même que le plan - par exemple si vous avez ajouté 4 et 5 au CTE , il continue à utiliser le même plan (apparemment - testé sur SQL Server 2012 RTM Express).

Il y a ici une flexibilité raisonnable. La forme générale du plan XML est utilisée pour guider la recherche d'un plan final (bien que de nombreux attributs soient complètement ignorés, par exemple le type de partitionnement sur les échanges) et les règles de recherche normales sont également considérablement assouplies. Par exemple, l'élagage précoce des alternatives basées sur des considérations de coût est désactivé, l'introduction explicite de jointures croisées est autorisée et les opérations scalaires sont ignorées.

Il y a trop de détails pour être approfondi, mais le placement des filtres et des scalaires de calcul ne peut pas être forcé, et les prédicats du formulaire column = valuesont généralisés de sorte qu'un plan contenant X = 1ou X = @Xpeut être appliqué à une requête contenant X = 502ou X = @Y. Cette flexibilité particulière peut grandement aider à trouver un plan naturel à forcer.

Dans l'exemple spécifique, l'union constante peut toujours être implémentée en tant que balayage constant; le nombre d'entrées dans l'Union Tout n'a pas d'importance.


3

Il n'y a aucun moyen (versions SQL Server jusqu'en 2012) de réutiliser un seul spool pour les deux occurrences du CTE. Les détails peuvent être trouvés dans la réponse de SQLKiwi. Vous trouverez ci - dessous deux façons de matérialiser deux fois le CTE, ce qui est inévitable pour la nature de la requête. Les deux options entraînent un nombre net de guides distincts de 6.

Le lien entre le commentaire de Martin et le site de Quassnoi sur un blog sur le plan guidant un CTE a été une inspiration partielle pour cette question. Il décrit un moyen de matérialiser un CTE aux fins d'une sous-requête corrélée, qui n'est référencée qu'une seule fois bien que la corrélation puisse entraîner son évaluation plusieurs fois. Cela ne s'applique pas à la requête dans la question.

Option 1 - Guide du plan

En prenant des indices de la réponse de SQLKiwi, j'ai réduit le guide à un strict minimum qui fera toujours le travail, par exemple, les ConstantScannœuds ne répertorient que 2 opérateurs scalaires qui peuvent suffisamment s'étendre à n'importe quel nombre.

;with cte(guid,other) as (
  select newid(),1 union all
  select newid(),2 union all
  select newid(),3)
select a.guid, a.other, b.guid guidb, b.other otherb
from cte a
cross join cte b
order by a.other, b.other
OPTION(USE PLAN
N'<?xml version="1.0" encoding="utf-16"?>
<ShowPlanXML xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" Version="1.2" Build="11.0.2100.60" xmlns="http://schemas.microsoft.com/sqlserver/2004/07/showplan">
  <BatchSequence>
    <Batch>
      <Statements>
        <StmtSimple StatementCompId="1" StatementEstRows="1600" StatementId="1" StatementOptmLevel="FULL" StatementOptmEarlyAbortReason="GoodEnoughPlanFound" StatementSubTreeCost="0.0444433" StatementText="with cte(guid,other) as (&#xD;&#xA;  select newid(),1 union all&#xD;&#xA;  select newid(),2 union all&#xD;&#xA;  select newid(),3&#xD;&#xA;select a.guid, a.other, b.guid guidb, b.other otherb&#xD;&#xA;from cte a&#xD;&#xA;cross join cte b&#xD;&#xA;order by a.other, b.other;&#xD;&#xA;" StatementType="SELECT" QueryHash="0x43D93EF17C8E55DD" QueryPlanHash="0xF8E3B336792D84" RetrievedFromCache="true">
          <StatementSetOptions ANSI_NULLS="true" ANSI_PADDING="true" ANSI_WARNINGS="true" ARITHABORT="true" CONCAT_NULL_YIELDS_NULL="true" NUMERIC_ROUNDABORT="false" QUOTED_IDENTIFIER="true" />
          <QueryPlan NonParallelPlanReason="EstimatedDOPIsOne" CachedPlanSize="96" CompileTime="13" CompileCPU="13" CompileMemory="1152">
            <MemoryGrantInfo SerialRequiredMemory="0" SerialDesiredMemory="0" />
            <OptimizerHardwareDependentProperties EstimatedAvailableMemoryGrant="157240" EstimatedPagesCached="1420" EstimatedAvailableDegreeOfParallelism="1" />
            <RelOp AvgRowSize="47" EstimateCPU="0.006688" EstimateIO="0" EstimateRebinds="0" EstimateRewinds="0" EstimatedExecutionMode="Row" EstimateRows="1600" LogicalOp="Inner Join" NodeId="0" Parallel="false" PhysicalOp="Nested Loops" EstimatedTotalSubtreeCost="0.0444433">
              <OutputList>
                <ColumnReference Column="Union1163" />
              </OutputList>
              <Warnings NoJoinPredicate="true" />
              <NestedLoops Optimized="false">
                <RelOp AvgRowSize="27" EstimateCPU="0.000432115" EstimateIO="0.0112613" EstimateRebinds="0" EstimateRewinds="0" EstimatedExecutionMode="Row" EstimateRows="40" LogicalOp="Sort" NodeId="1" Parallel="false" PhysicalOp="Sort" EstimatedTotalSubtreeCost="0.0117335">
                  <OutputList>
                    <ColumnReference Column="Union1080" />
                    <ColumnReference Column="Union1081" />
                  </OutputList>
                  <MemoryFractions Input="0" Output="0" />
                  <Sort Distinct="false">
                    <OrderBy>
                      <OrderByColumn Ascending="true">
                        <ColumnReference Column="Union1081" />
                      </OrderByColumn>
                    </OrderBy>
                    <RelOp AvgRowSize="27" EstimateCPU="4.0157E-05" EstimateIO="0" EstimateRebinds="0" EstimateRewinds="0" EstimatedExecutionMode="Row" EstimateRows="40" LogicalOp="Constant Scan" NodeId="2" Parallel="false" PhysicalOp="Constant Scan" EstimatedTotalSubtreeCost="4.0157E-05">
                      <OutputList>
                        <ColumnReference Column="Union1080" />
                        <ColumnReference Column="Union1081" />
                      </OutputList>
                      <ConstantScan>
                        <Values>
                          <Row>
                            <ScalarOperator ScalarString="newid()">
                              <Intrinsic FunctionName="newid" />
                            </ScalarOperator>
                            <ScalarOperator ScalarString="(1)">
                              <Const ConstValue="(1)" />
                            </ScalarOperator>
                          </Row>
                          <Row>
                            <ScalarOperator ScalarString="newid()">
                              <Intrinsic FunctionName="newid" />
                            </ScalarOperator>
                            <ScalarOperator ScalarString="(2)">
                              <Const ConstValue="(2)" />
                            </ScalarOperator>
                          </Row>
                        </Values>
                      </ConstantScan>
                    </RelOp>
                  </Sort>
                </RelOp>
                <RelOp AvgRowSize="27" EstimateCPU="0.0001074" EstimateIO="0.01" EstimateRebinds="0" EstimateRewinds="39" EstimatedExecutionMode="Row" EstimateRows="40" LogicalOp="Lazy Spool" NodeId="83" Parallel="false" PhysicalOp="Table Spool" EstimatedTotalSubtreeCost="0.0260217">
                  <OutputList>
                    <ColumnReference Column="Union1162" />
                    <ColumnReference Column="Union1163" />
                  </OutputList>
                  <Spool>
                    <RelOp AvgRowSize="27" EstimateCPU="0.000432115" EstimateIO="0.0112613" EstimateRebinds="0" EstimateRewinds="0" EstimatedExecutionMode="Row" EstimateRows="40" LogicalOp="Sort" NodeId="84" Parallel="false" PhysicalOp="Sort" EstimatedTotalSubtreeCost="0.0117335">
                      <OutputList>
                        <ColumnReference Column="Union1162" />
                        <ColumnReference Column="Union1163" />
                      </OutputList>
                      <MemoryFractions Input="0" Output="0" />
                      <Sort Distinct="false">
                        <OrderBy>
                          <OrderByColumn Ascending="true">
                            <ColumnReference Column="Union1163" />
                          </OrderByColumn>
                        </OrderBy>
                        <RelOp AvgRowSize="27" EstimateCPU="4.0157E-05" EstimateIO="0" EstimateRebinds="0" EstimateRewinds="0" EstimatedExecutionMode="Row" EstimateRows="40" LogicalOp="Constant Scan" NodeId="85" Parallel="false" PhysicalOp="Constant Scan" EstimatedTotalSubtreeCost="4.0157E-05">
                          <OutputList>
                            <ColumnReference Column="Union1162" />
                            <ColumnReference Column="Union1163" />
                          </OutputList>
                          <ConstantScan>
                            <Values>
                              <Row>
                                <ScalarOperator ScalarString="newid()">
                                  <Intrinsic FunctionName="newid" />
                                </ScalarOperator>
                                <ScalarOperator ScalarString="(1)">
                                  <Const ConstValue="(1)" />
                                </ScalarOperator>
                              </Row>
                              <Row>
                                <ScalarOperator ScalarString="newid()">
                                  <Intrinsic FunctionName="newid" />
                                </ScalarOperator>
                                <ScalarOperator ScalarString="(2)">
                                  <Const ConstValue="(2)" />
                                </ScalarOperator>
                              </Row>
                            </Values>
                          </ConstantScan>
                        </RelOp>
                      </Sort>
                    </RelOp>
                  </Spool>
                </RelOp>
              </NestedLoops>
            </RelOp>
          </QueryPlan>
        </StmtSimple>
      </Statements>
    </Batch>
  </BatchSequence>
</ShowPlanXML>'
);

Option 2 - Analyse à distance

En augmentant le coût de la requête et en introduisant un scan à distance, le résultat est matérialisé.

with cte(guid,other) as (
  select *
  from OPENQUERY([TESTSQL\V2012], '
  select newid(),1 union all
  select newid(),2 union all
  select newid(),3') x)
select a.guid, a.other, b.guid guidb, b.other otherb
from cte a
cross join cte b
order by a.other, b.other;

2

Sérieusement, vous ne pouvez pas découper les plans d'exécution xml à partir de zéro. Les créer à l'aide de SSIS est de la science-fiction. Oui, c'est tout XML, mais ils proviennent d'univers différents. En regardant le blog de Paul sur ce sujet , il dit "beaucoup dans la façon dont SSIS le permet ..." alors vous avez peut-être mal compris? Je ne pense pas qu'il dit "utilisez SSIS pour créer des plans" mais plutôt "ne serait-il pas formidable de pouvoir créer des plans en utilisant une interface glisser-déposer comme SSIS". Peut-être, pour une requête très simple, vous pourriez à peu près gérer cela, mais c'est un tronçon, peut-être même une perte de temps. On pourrait dire un travail chargé.

Si je crée un plan pour un indice ou un guide de plan UTILISATION, j'ai quelques approches. Par exemple, je peux supprimer des enregistrements des tables (par exemple sur une copie de la base de données) pour influencer les statistiques et encourager l'optimiseur à prendre une décision différente. J'ai également utilisé des variables de table au lieu de toutes les tables de la requête, de sorte que l'optimiseur pense que chaque table contient 1 enregistrement. Ensuite, dans le plan généré, remplacez toutes les variables de table par les noms de table d'origine et échangez-les en tant que plan. Une autre option serait d'utiliser l'option WITH STATS_STREAM de UPDATE STATISTICS pour usurper les statistiques qui est la méthode utilisée lors du clonage de copies de bases de données uniquement statistiques, par exemple

UPDATE STATISTICS 
    [dbo].[yourTable]([PK_yourTable]) 
WITH 
    STATS_STREAM = 0x0100etc, 
    ROWCOUNT = 10000, 
    PAGECOUNT = 93

J'ai passé du temps à bricoler des plans d'exécution xml dans le passé et j'ai trouvé qu'en fin de compte, SQL va juste "je ne l'utilise pas" et exécute la requête comme il veut de toute façon.

Pour votre exemple spécifique, je suis sûr que vous savez que vous pouvez utiliser set rowcount 3 ou TOP 3 dans la requête pour obtenir ce résultat, mais je suppose que ce n'est pas votre point. La bonne réponse serait vraiment: utilisez une table temporaire. Je voterais pour:) Pas une bonne réponse serait "passer des heures voire des jours à découper votre propre plan d'exécution XML personnalisé où vous essayez de tromper l'optimiseur en faisant une bobine paresseuse pour le CTE qui pourrait même ne pas fonctionner de toute façon, aurait l'air intelligent mais serait également impossible à maintenir ".

Ne pas essayer de ne pas être constructif là-bas, juste mon avis - j'espère que cela aide.


Sérieusement, les plans XML sont ignorables?!, Je pensais que c'était tout le problème? S'ils ne sont pas valides, il devrait lancer.
crokusek

Je faisais référence à l'événement Plan Guide infructueux.
wBob

2

Y at - il QUELQUE façon ...

Enfin, dans SQL 2016 CTP 3.0, il existe un moyen, une sorte de:)

En utilisant l'indicateur de trace et les événements étendus détaillés par Dmitry Pilugin ici , vous pouvez (quelque peu arbitrairement) repérer trois guides uniques des étapes intermédiaires de l'exécution de la requête.

NB Ce code n'est PAS destiné à la production ou à une utilisation sérieuse en ce qui concerne le forçage de plan CTE, simplement un regard léger sur un nouveau drapeau de trace et une manière différente de faire les choses:

-- Configure the XEvents session; with ring buffer target so we can collect it
CREATE EVENT SESSION [query_trace_column_values] ON SERVER 
ADD EVENT sqlserver.query_trace_column_values
ADD TARGET package0.ring_buffer( SET max_memory = 2048 )
WITH ( MAX_MEMORY = 4096 KB, EVENT_RETENTION_MODE = ALLOW_SINGLE_EVENT_LOSS, MAX_DISPATCH_LATENCY = 30 SECONDS, MAX_EVENT_SIZE = 0 KB, MEMORY_PARTITION_MODE = NONE, TRACK_CAUSALITY = OFF , STARTUP_STATE = OFF )
GO

-- Start the session
ALTER EVENT SESSION [query_trace_column_values] ON SERVER
STATE = START;
GO

-- Run the query, including traceflag
DBCC TRACEON(2486);
SET STATISTICS XML ON;
GO

-- Original query
;with cte(guid,other) as (
  select newid(),1 union all
  select newid(),2 union all
  select newid(),3)
select a.guid, a.other, b.guid guidb, b.other otherb
from cte a
cross join cte b
order by a.other, b.other
option ( recompile )
go

SET STATISTICS XML OFF;
DBCC TRACEOFF(2486);
GO

DECLARE @target_data XML

SELECT @target_data = CAST( target_data AS XML )
FROM sys.dm_xe_sessions AS s 
    INNER JOIN sys.dm_xe_session_targets AS t ON t.event_session_address = s.address
WHERE s.name = 'query_trace_column_values'


--SELECT @target_data td

-- Arbitrarily fish out 3 unique guids from intermediate stage of the query as collected by XEvent session
;WITH cte AS
(
SELECT
    n.c.value('(data[@name = "row_id"]/value/text())[1]', 'int') row_id,
    n.c.value('(data[@name = "column_value"]/value/text())[1]', 'char(36)') [guid]
FROM @target_data.nodes('//event[data[@name="column_id"]/value[. = 1]][data[@name="row_number"]/value[. < 4]][data[@name="node_name"]/value[. = "Nested Loops"]]') n(c)
)
SELECT *
FROM cte a
    CROSS JOIN cte b
GO

-- Stop the session
ALTER EVENT SESSION [query_trace_column_values] ON SERVER
STATE = STOP;
GO

-- Drop the session
IF EXISTS ( select * from sys.server_event_sessions where name = 'query_trace_column_values' )
DROP EVENT SESSION [query_trace_column_values] ON SERVER 
GO

Testé sur la version (CTP3.2) - 13.0.900.73 (x64), juste pour le plaisir.


1

J'ai trouvé que traceflag 8649 (forcer le plan parallèle) induisait ce comportement pour la colonne guid de gauche sur mes instances 2008, R2 et 2012. Je n'avais pas besoin d'utiliser l'indicateur sur SQL 2005 où le CTE s'est comporté correctement. J'ai essayé d'utiliser le plan généré dans SQL 2005 dans les instances supérieures, mais il n'a pas été validé.

with cte(guid,other) as (
  select newid(),1 union all
  select newid(),2 union all
  select newid(),3)
select a.guid, a.other, b.guid guidb, b.other otherb
from cte a
cross join cte b
order by a.other, b.other
option ( querytraceon 8649 )

Soit en utilisant l'indice, en utilisant un guide de plan incluant l'indice ou en utilisant le plan généré par la requête avec l'indice dans un PLAN D'UTILISATION, etc., tout a fonctionné. cte newid


Merci d'avoir réessayé. La requête ne semble pas différente avec ou sans cet indicateur de trace sur 2008/2012. Je ne sais pas vraiment si ce sont mes instances SQL Server ou ce que vous essayez de montrer. Je vois encore 18 guids. Que vois-tu?

3 guides distincts sur le côté gauche (colonne guid), chacun se répétant trois fois. 9 guides uniques sur le côté droit (colonne guidb), donc au moins le bit gauche se comporte comme vous le souhaitez lol. J'ai ajouté une image à une autre réponse pour, je l'espère, clarifier un peu. Petites étapes. Je dois également noter que dans SQL 2005, j'obtiens 6 guides uniques, 3 à gauche, 3 à droite.
wBob

Je viens également de remarquer que la suppression du `` tout '' obtient également les 6 guides uniques, 3 de chaque côté.
wBob

Peut faire en sorte que le traceflag ne fonctionne pas en ayant le serveur maxdop 1.
wBob
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.