Crear una guía de plan para el resultado CTE de caché (spool perezoso)


19

Normalmente creo guías de plan construyendo primero una consulta que usa el plan correcto y copiándola en una consulta similar que no lo hace. Sin embargo, eso a veces es complicado, especialmente si la consulta no es exactamente la misma. ¿Cuál es la forma correcta de crear guías de plan desde cero?

SQLKiwi ha mencionado la elaboración de planes en SSIS, ¿hay alguna forma o herramienta útil para ayudar a diseñar un buen plan para SQL Server?

La instancia específica en cuestión es este 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;

¿Hay ALGUNA forma de hacer que el resultado arroje exactamente 3 guidsy no más? Espero poder responder mejor a las preguntas en el futuro al incluir guías de plan con consultas de tipo CTE a las que se hace referencia varias veces para superar algunas peculiaridades de SQL Server CTE.


Respuestas:


14

¿Hay ALGUNA forma de hacer que el resultado tenga exactamente 3 guías distintas y nada más? Espero poder responder mejor a las preguntas en el futuro al incluir guías de plan con consultas de tipo CTE a las que se hace referencia varias veces para superar algunas peculiaridades de SQL Server CTE.

Hoy no. Las expresiones de tabla común (CTE) no recursivas se tratan como definiciones de vista en línea y se expanden en el árbol de consulta lógica en cada lugar al que se hace referencia (al igual que las definiciones de vista regulares) antes de la optimización. El árbol lógico para su consulta es:

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

Observe los dos anclajes de vista y las seis llamadas a la función intrínseca newidantes de que comience la optimización. Sin embargo, muchas personas consideran que el optimizador debería poder identificar que los subárboles expandidos eran originalmente un solo objeto referenciado y simplificar en consecuencia. También ha habido varias solicitudes de Connect para permitir la materialización explícita de una tabla CTE o derivada.

Una implementación más general haría que el optimizador considerara materializar expresiones comunes arbitrarias para mejorar el rendimiento ( CASEcon una subconsulta es otro ejemplo donde pueden ocurrir problemas hoy en día). Microsoft Research publicó un documento (PDF) sobre eso en 2007, aunque sigue sin implementarse hasta la fecha. Por el momento, estamos limitados a la materialización explícita usando cosas como variables de tabla y tablas temporales.

SQLKiwi ha mencionado la elaboración de planes en SSIS, ¿hay alguna forma o herramienta útil para ayudar a diseñar un buen plan para SQL Server?

Esto fue solo una ilusión de mi parte, y fue mucho más allá de la idea de modificar las guías del plan. Es posible, en principio, escribir una herramienta para manipular directamente el XML del plan de presentación, pero sin una instrumentación optimizadora específica, usar la herramienta probablemente sería una experiencia frustrante para el usuario (y el desarrollador piensa en ello).

En el contexto particular de esta pregunta, dicha herramienta aún sería incapaz de materializar el contenido de CTE de una manera que podría ser utilizada por múltiples consumidores (para alimentar ambas entradas a la unión cruzada en este caso). El optimizador y el motor de ejecución son compatibles con los carretes de múltiples consumidores, pero solo para fines específicos, ninguno de los cuales podría aplicarse a este ejemplo en particular.

Si bien no estoy seguro, tengo el presentimiento de que se puede seguir RelOps (Nested Loop, Lazy Spool) incluso si la consulta no es exactamente la misma que el plan, por ejemplo, si agregó 4 y 5 al CTE , sigue utilizando el mismo plan (aparentemente probado en SQL Server 2012 RTM Express).

Aquí hay una cantidad razonable de flexibilidad. La forma amplia del plan XML se utiliza para guiar la búsqueda de un plan final (aunque muchos atributos se ignoran por completo, por ejemplo, el tipo de partición en los intercambios) y las reglas de búsqueda normales también se relajan considerablemente. Por ejemplo, se desactiva la poda temprana de alternativas basadas en consideraciones de costos, se permite la introducción explícita de combinaciones cruzadas y se ignoran las operaciones escalares.

Hay demasiados detalles para profundizar, pero no se puede forzar la colocación de Filtros y Calcular escalares, y los predicados del formulario column = valuese generalizan para que un plan que contenga X = 1o X = @Xse pueda aplicar a una consulta que contenga X = 502o X = @Y. Esta flexibilidad particular puede ayudar enormemente a encontrar un plan natural para forzar.

En el ejemplo específico, constante Union All siempre se puede implementar como una exploración constante; el número de entradas a la Unión Todos no importa.


3

No hay forma (versiones de SQL Server hasta 2012) de reutilizar un solo spool para ambas ocurrencias del CTE. Los detalles se pueden encontrar en la respuesta de SQLKiwi. Más abajo hay dos formas de materializar el CTE dos veces, lo cual es inevitable por la naturaleza de la consulta. Ambas opciones resultan en un recuento de guid neto distinto de 6.

El enlace del comentario de Martin al sitio de Quassnoi en un blog sobre el plan para guiar a un CTE fue una inspiración parcial para esta pregunta. Describe una forma de materializar un CTE con el propósito de una subconsulta correlacionada, a la que se hace referencia solo una vez, aunque la correlación puede hacer que se evalúe varias veces. Eso no se aplica a la consulta en la pregunta.

Opción 1 - Guía del plan

Tomando pistas de la respuesta de SQLKiwi, he reducido la guía a un mínimo que todavía hará el trabajo, por ejemplo, los ConstantScannodos solo enumeran 2 operadores escalares que pueden expandirse lo suficiente a cualquier número.

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

Opción 2 - Escaneo remoto

Al aumentar el gasto de la consulta e introducir una exploración remota, el resultado se materializa.

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

Con toda seriedad, no puede cortar planes de ejecución xml desde cero. Crearlos usando SSIS es ciencia ficción. Sí, todo es XML, pero son de universos diferentes. Al mirar el blog de Paul sobre ese tema , está diciendo "mucho en la forma en que SSIS permite ...", ¿entonces posiblemente ha entendido mal? No creo que esté diciendo "use SSIS para crear planes", sino "no sería genial poder crear planes usando una interfaz de arrastrar y soltar como SSIS". Tal vez, para una consulta muy simple, podría manejar esto, pero es una exageración, posiblemente incluso una pérdida de tiempo. Trabajo ocupado, se podría decir.

Si estoy creando un plan para una sugerencia o guía de plan USE PLAN, tengo un par de enfoques. Por ejemplo, podría eliminar registros de las tablas (por ejemplo, en una copia de la base de datos) para influir en las estadísticas y alentar al optimizador a tomar una decisión diferente. También he usado variables de tabla en lugar de todas las tablas en la consulta, por lo que el optimizador cree que cada tabla contiene 1 registro. Luego, en el plan generado, reemplace todas las variables de la tabla con los nombres de la tabla original e intercambie como el plan. Otra opción sería utilizar la opción WITH STATS_STREAM de ACTUALIZAR ESTADÍSTICAS para falsificar estadísticas, que es el método utilizado para clonar copias de bases de datos solo estadísticas.

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

He pasado algún tiempo jugando con los planes de ejecución xml en el pasado y he descubierto que al final, SQL simplemente dice "No estoy usando eso" y ejecuta la consulta como quiere de todos modos.

Para su ejemplo específico, estoy seguro de que sabe que puede usar set rowcount 3 o TOP 3 en la consulta para obtener ese resultado, pero supongo que ese no es su punto. La respuesta correcta sería realmente: usar una tabla temporal. Votaría que:) No sería una respuesta correcta "pasar horas, incluso días, cortando su propio plan de ejecución XML personalizado en el que intenta engañar al optimizador para que haga un spool perezoso para el CTE que de todos modos podría no funcionar, parecería inteligente pero también sería imposible de mantener ".

No intento ser poco constructivo allí, solo mi opinión, espero que ayude.


En serio, ¿los planes XML son ignorables? ¿Pensé que ese era el punto? Si no son válidos, debería arrojar.
crokusek

Me refería al evento fallido de la Guía de planes.
wBob el

2

¿Hay alguna manera ...

Finalmente en SQL 2016 CTP 3.0 hay una forma, tipo de:)

Usando el indicador de seguimiento y los eventos extendidos como detalla Dmitry Pilugin aquí , puede extraer (de forma algo arbitraria) tres guías únicas de las etapas intermedias de la ejecución de la consulta.

Nota: este código NO está destinado a la producción o al uso serio con respecto al forzamiento del plan CTE, simplemente una mirada alegre a un nuevo indicador de rastreo y una forma diferente de hacer las cosas:

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

Probado en la versión (CTP3.2) - 13.0.900.73 (x64), solo por diversión.


1

Encontré que traceflag 8649 (plan paralelo de fuerza) indujo este comportamiento para la columna de guía izquierda en mis instancias de 2008, R2 y 2012. No necesitaba usar la bandera en SQL 2005 donde el CTE se comportó correctamente. Intenté usar el plan generado en SQL 2005 en las instancias superiores, pero no valió.

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 )

Ya sea usando la pista, usando una guía de plan que incluya la pista o usando el plan generado por la consulta con la pista activada en un PLAN DE USO, etc. todo funcionó. cte newid


Gracias por intentarlo de nuevo. La consulta no se ve diferente con o sin esa marca de seguimiento en 2008/2012. No estoy seguro de si son mis instancias de SQL Server o lo que está tratando de mostrar. Todavía veo 18 guías. ¿Que ves?
孔夫子

3 guías distintas en el lado izquierdo (columna de guía), cada una repitiendo tres veces. 9 guías únicas en el lado derecho (columna guidb), por lo que al menos el bit izquierdo se comporta como quieres jajaja. Agregué una imagen a otra respuesta para aclarar un poco. Pasos pequeños. También debo señalar en SQL 2005, obtengo 6 guías únicas, 3 a la izquierda, 3 a la derecha.
wBob

También noté que al eliminar el 'todo' también se obtienen 6 guías únicas, 3 por cada lado.
wBob

Puede hacer que el traceflag no funcione al tener el servidor maxdop 1.
wBob
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.