ROW_NUMBER () sans PARTITION BY génère toujours l'itérateur de segment

11

J'écris sur un article de blog à venir sur les fonctions de classement et d'agrégation de fenêtres, en particulier les itérateurs de Segment and Sequence Project. D'après ce que je comprends, Segment identifie les lignes d'un flux qui constituent la fin / le début d'un groupe, donc la requête suivante:

SELECT ROW_NUMBER() OVER (PARTITION BY someGroup ORDER BY someOrder)

Utilisera Segment pour savoir quand une ligne appartient à un groupe différent de la ligne précédente. L'itérateur Sequence Project effectue ensuite le calcul du numéro de ligne réel, sur la base de la sortie de la sortie de l'itérateur Segment.

Mais la requête suivante, utilisant cette logique, ne devrait pas avoir à inclure un segment, car il n'y a pas d'expression de partition.

SELECT ROW_NUMBER() OVER (ORDER BY someGroup, someOrder)

Cependant, lorsque j'essaie cette hypothèse, ces deux requêtes utilisent un opérateur de segment. La seule différence est que la deuxième requête n'a pas besoin d'un GroupBysur le segment. Cela n'élimine-t-il pas la nécessité d'un segment en premier lieu?

Exemple

CREATE TABLE dbo.someTable (
    someGroup   int NOT NULL,
    someOrder   int NOT NULL,
    someValue   numeric(8, 2) NOT NULL,
    PRIMARY KEY CLUSTERED (someGroup, someOrder)
);

--- Query 1:
SELECT ROW_NUMBER() OVER (PARTITION BY someGroup ORDER BY someOrder)
FROM dbo.someTable;

--- Query 2:
SELECT ROW_NUMBER() OVER (ORDER BY someGroup, someOrder)
FROM dbo.someTable;
Daniel Hutmacher
la source
1
Bien qu'il n'y ait pas d'expression de partition, je suppose que vous divisez toujours techniquement le jeu de résultats en partitions, mais seulement une dans ce cas?
Mark Sinkinson
Le QP montre un vide <GroupBy />donc le segment ne fait vraiment rien, presque, il sort la colonne du segment à l'opérateur Sequence Project. La raison pour laquelle l'opérateur de segment doit être présent pourrait être que l'opérateur de projet de séquence a besoin de cette valeur pour effectuer son travail.
Mikael Eriksson
C'est ma théorie aussi. Mais l'optimiseur supprime généralement ces types d'opérateurs inutiles, à mon humble avis.
Daniel Hutmacher

Réponses:

12

J'ai trouvé ce billet de blog de 6 ans mentionnant le même comportement.

Il semble ROW_NUMBER()toujours inclure un opérateur de segment, qu'il PARTITION BYsoit utilisé ou non. Si je devais deviner, je dirais que c'est parce que cela facilite la création d'un plan de requête sur le moteur.

Si le segment est nécessaire dans la plupart des cas, et dans les cas où il n'est pas nécessaire, il s'agit essentiellement d'une non-opération à coût nul, il est beaucoup plus simple de l'inclure toujours dans le plan lorsqu'une fonction de fenêtrage est utilisée.

JNK
la source
11

Selon le showplan.xsd du plan d'exécution, GroupByapparaît sans minOccursou les maxOccursattributs qui par défaut sont donc [1..1] rendant l'élément obligatoire, pas nécessairement le contenu. L'élément enfant ColumnReferencede type ( ColumnReferenceType) a minOccurs0 et maxOccurs[0 .. *] illimité, ce qui le rend facultatif , d'où l'élément vide autorisé. Si vous essayez manuellement de supprimer GroupByet de forcer le plan, vous obtenez l'erreur attendue:

Msg 6965, Level 16, State 1, Line 29
XML Validation: Invalid content. Expected element(s): '{http://schemas.microsoft.com/sqlserver/2004/07/showplan}GroupBy','{http://schemas.microsoft.com/sqlserver/2004/07/showplan}DefinedValues','{http://schemas.microsoft.com/sqlserver/2004/07/showplan}InternalInfo'. Found: element '{http://schemas.microsoft.com/sqlserver/2004/07/showplan}SegmentColumn' instead. Location: /*:ShowPlanXML[1]/*:BatchSequence[1]/*:Batch[1]/*:Statements[1]/*:StmtSimple[1]/*:QueryPlan[1]/*:RelOp[1]/*:SequenceProject[1]/*:RelOp[1]/*:Segment[1]/*:SegmentColumn[1].

Fait intéressant, j'ai trouvé que vous pouvez supprimer manuellement l'opérateur de segment pour obtenir un plan de forçage valide qui ressemble à ceci:

entrez la description de l'image ici

Cependant, lorsque vous exécutez avec ce plan (en utilisant OPTION ( USE PLAN ... )), l'opérateur de segment réapparaît comme par magie. Va juste pour montrer que l'optimiseur ne prend que les plans XML comme guide approximatif.

Mon banc d'essai:

USE tempdb
GO
SET NOCOUNT ON
GO
IF OBJECT_ID('dbo.someTable') IS NOT NULL DROP TABLE dbo.someTable
GO
CREATE TABLE dbo.someTable (
    someGroup   int NOT NULL,
    someOrder   int NOT NULL,
    someValue   numeric(8, 2) NOT NULL,
    PRIMARY KEY CLUSTERED (someGroup, someOrder)
);
GO

-- Generate some dummy data
;WITH cte AS (
SELECT TOP 1000 ROW_NUMBER() OVER ( ORDER BY ( SELECT 1 ) ) rn
FROM master.sys.columns c1
    CROSS JOIN master.sys.columns c2
    CROSS JOIN master.sys.columns c3
)
INSERT INTO dbo.someTable ( someGroup, someOrder, someValue )
SELECT rn % 333, rn % 444, rn % 55
FROM cte
GO


-- Try and force the plan
SELECT ROW_NUMBER() OVER (ORDER BY someGroup, someOrder)
FROM dbo.someTable
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="12.0.2000.8" xmlns="http://schemas.microsoft.com/sqlserver/2004/07/showplan">
  <BatchSequence>
    <Batch>
      <Statements>
        <StmtSimple StatementCompId="1" StatementEstRows="1000" StatementId="1" StatementOptmLevel="TRIVIAL" CardinalityEstimationModelVersion="120" StatementSubTreeCost="0.00596348" StatementText="SELECT ROW_NUMBER() OVER (ORDER BY someGroup, someOrder)&#xD;&#xA;FROM dbo.someTable" StatementType="SELECT" QueryHash="0x193176312402B8E7" QueryPlanHash="0x77F1D72C455025A4" 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 DegreeOfParallelism="1" CachedPlanSize="16" CompileTime="0" CompileCPU="0" CompileMemory="88">
            <OptimizerHardwareDependentProperties EstimatedAvailableMemoryGrant="131072" EstimatedPagesCached="65536" EstimatedAvailableDegreeOfParallelism="4" />
            <RelOp AvgRowSize="15" EstimateCPU="8E-05" EstimateIO="0" EstimateRebinds="0" EstimateRewinds="0" EstimatedExecutionMode="Row" EstimateRows="1000" LogicalOp="Compute Scalar" NodeId="0" Parallel="false" PhysicalOp="Sequence Project" EstimatedTotalSubtreeCost="0.00596348">
              <OutputList>
                <ColumnReference Column="Expr1002" />
              </OutputList>
              <SequenceProject>
                <DefinedValues>
                  <DefinedValue>
                    <ColumnReference Column="Expr1002" />
                    <ScalarOperator ScalarString="row_number">
                      <Sequence FunctionName="row_number" />
                    </ScalarOperator>
                  </DefinedValue>
                </DefinedValues>

                <!-- Segment operator completely removed from plan -->
                <!--<RelOp AvgRowSize="15" EstimateCPU="2E-05" EstimateIO="0" EstimateRebinds="0" EstimateRewinds="0" EstimatedExecutionMode="Row" EstimateRows="1000" LogicalOp="Segment" NodeId="1" Parallel="false" PhysicalOp="Segment" EstimatedTotalSubtreeCost="0.00588348">
                  <OutputList>
                    <ColumnReference Database="[tempdb]" Schema="[dbo]" Table="[someTable]" Column="someGroup" />
                    <ColumnReference Database="[tempdb]" Schema="[dbo]" Table="[someTable]" Column="someOrder" />
                    <ColumnReference Column="Segment1003" />
                  </OutputList>
                  <Segment>
                    <GroupBy />
                    <SegmentColumn>
                      <ColumnReference Column="Segment1003" />
                    </SegmentColumn>-->


                    <RelOp AvgRowSize="15" EstimateCPU="0.001257" EstimateIO="0.00460648" EstimateRebinds="0" EstimateRewinds="0" EstimatedExecutionMode="Row" EstimateRows="1000" LogicalOp="Clustered Index Scan" NodeId="0" Parallel="false" PhysicalOp="Clustered Index Scan" EstimatedTotalSubtreeCost="0.00586348" TableCardinality="1000">
                      <OutputList>
                        <ColumnReference Database="[tempdb]" Schema="[dbo]" Table="[someTable]" Column="someGroup" />
                        <ColumnReference Database="[tempdb]" Schema="[dbo]" Table="[someTable]" Column="someOrder" />
                      </OutputList>
                      <IndexScan Ordered="true" ScanDirection="FORWARD" ForcedIndex="false" ForceSeek="false" ForceScan="false" NoExpandHint="false" Storage="RowStore">
                        <DefinedValues>
                          <DefinedValue>
                            <ColumnReference Database="[tempdb]" Schema="[dbo]" Table="[someTable]" Column="someGroup" />
                          </DefinedValue>
                          <DefinedValue>
                            <ColumnReference Database="[tempdb]" Schema="[dbo]" Table="[someTable]" Column="someOrder" />
                          </DefinedValue>
                        </DefinedValues>
                        <Object Database="[tempdb]" Schema="[dbo]" Table="[someTable]" Index="[PK__someTabl__7CD03C8950FF62C1]" IndexKind="Clustered" Storage="RowStore" />
                      </IndexScan>
                    </RelOp>

                <!--</Segment>
                </RelOp>-->
              </SequenceProject>
            </RelOp>

          </QueryPlan>
        </StmtSimple>
      </Statements>
    </Batch>
  </BatchSequence>
</ShowPlanXML>' )

Découpez le plan XML du banc de test et enregistrez-le en tant que .sqlplan pour afficher le plan moins le segment.

PS Je ne passerais pas trop de temps à découper manuellement les plans SQL, comme si vous me connaissiez, vous saviez que je le considère comme un travail fastidieux et quelque chose que je ne ferais jamais. Oh attends!? :)

wBob
la source
Vous avez bien trop de temps libre ... Beau travail!
Mark Sinkinson
D'accord avec Mark. J'apprends des trucs que je ne pensais même pas demander. Merci! :)
Daniel Hutmacher