ROW_NUMBER () sin PARTITION BY todavía genera el iterador de segmento

11

Estoy escribiendo en una próxima publicación mía del blog sobre las funciones de clasificación y ventana agregada, específicamente los iteradores de Segmento y Proyecto de secuencia. La forma en que lo entiendo es que Segmento identifica filas en una secuencia que constituyen el final / principio de un grupo, por lo que la siguiente consulta:

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

Utilizará Segmento para saber cuándo una fila pertenece a un grupo diferente que no sea la fila anterior. El iterador del Proyecto de secuencia realiza el cálculo del número de fila real, en función de la salida de la salida del iterador de segmento.

Pero la siguiente consulta, usando esa lógica, no debería tener que incluir un Segmento, porque no hay expresión de partición.

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

Sin embargo, cuando pruebo esta hipótesis, ambas consultas utilizan un operador de segmento. La única diferencia es que la segunda consulta no necesita un GroupByen el segmento. ¿No elimina eso la necesidad de un segmento en primer lugar?

Ejemplo

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
fuente
1
Aunque no hay una expresión de partición, supongo que técnicamente aún está dividiendo el conjunto de resultados en particiones, aunque solo una en este caso.
Mark Sinkinson
El QP muestra un vacío, <GroupBy />por lo que el segmento realmente no hace nada, casi envía la columna del segmento al operador del Proyecto de secuencia. La razón para que el operador del segmento esté allí podría ser que el operador del Proyecto de secuencia necesita ese valor para hacer su trabajo.
Mikael Eriksson
Esa es mi teoría también. Pero el optimizador generalmente elimina este tipo de operadores innecesarios, en mi opinión ..
Daniel Hutmacher 02 de

Respuestas:

12

Encontré esta publicación de blog de 6 años que menciona el mismo comportamiento.

Parece que ROW_NUMBER()siempre incluye un operador de segmento, PARTITION BYse use o no. Si tuviera que adivinar, diría que esto se debe a que facilita la creación de un plan de consulta en el motor.

Si el segmento se necesita en la mayoría de los casos, y en los casos en que no se necesita es esencialmente una no operación de costo cero, es mucho más simple incluirlo siempre en el plan cuando se utiliza una función de ventana.

JNK
fuente
11

De acuerdo con el showplan.xsd para el plan de ejecución, GroupByaparece sin minOccurso maxOccursatributos que, por lo tanto, predeterminan a [1..1] haciendo que el elemento sea obligatorio, no necesariamente contenido. El elemento secundario ColumnReferencede tipo ( ColumnReferenceType) tiene minOccurs0 y maxOccurs[0 .. *] ilimitado, lo que lo hace opcional , de ahí el elemento vacío permitido. Si intenta eliminar manualmente GroupByy forzar el plan, obtendrá el error esperado:

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].

Curiosamente, descubrí que puede eliminar manualmente el operador Segmento para obtener un plan válido para forzar que se ve así:

ingrese la descripción de la imagen aquí

Sin embargo, cuando ejecuta con ese plan (usando OPTION ( USE PLAN ... )), el Operador de Segmento reaparece mágicamente. Solo muestra que el optimizador solo toma los planes XML como una guía aproximada.

Mi plataforma de prueba:

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>' )

Recorte el plan XML del equipo de prueba y guárdelo como un plan .sql para ver el plan menos el segmento.

PD: No pasaría demasiado tiempo cortando planes SQL manualmente, como si supieras que lo sabrías, lo considero como un trabajo ocupado que consume mucho tiempo y algo que nunca haría. Oh espera !? :)

wBob
fuente
Tienes demasiado tiempo en tus manos ... ¡Buen trabajo!
Mark Sinkinson
De acuerdo con Mark. Estoy aprendiendo cosas que ni siquiera pensé pedir. ¡Gracias! :)
Daniel Hutmacher