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 du projet de séquence effectue ensuite le calcul du numéro de ligne réel, en fonction de la sortie de la sortie de l'itérateur de 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 GroupBy
sur 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;
J'ai trouvé ce billet de blog de 6 ans mentionnant le même comportement.
Il semble que ROW_NUMBER()
inclut toujours un opérateur de segment, que PARTITION BY
est 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.
Selon showplan.xsd pour le plan d'exécution, GroupBy
apparaît sans les attributs minOccurs
ou maxOccurs
qui sont donc par défaut à [1..1] rendant l'élément obligatoire, pas nécessairement le contenu. L'élément enfant ColumnReference
de type (ColumnReferenceType
) a minOccurs
0 et maxOccurs
illimité [0 .. *], ce qui le rend facultatif , d'où l'élément vide autorisé. Si vous essayez manuellement de supprimer le GroupBy
et 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:
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.
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)
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 sauriez que je considère que cela prend du temps travail occupé et quelque chose que je ne ferais jamais. Oh attends!? :)