¿Cómo insinuar la unión de muchos a muchos en SQL Server?


9

Tengo 3 tablas "grandes" que se unen en un par de columnas (ambas int).

  • Table1 tiene ~ 200 millones de filas
  • Table2 tiene ~ 1.5 millones de filas
  • Table3 tiene ~ 6 millones de filas

Cada tabla tiene un índice agrupado en Key1, Key2y luego una columna más. Key1tiene baja cardinalidad y es muy sesgada. Siempre se hace referencia en la WHEREcláusula. Key2nunca se menciona en la WHEREcláusula Cada unión es de muchos a muchos.

El problema es con la estimación de cardinalidad. La estimación de salida de cada combinación se hace más pequeña en lugar de más grande . Esto da como resultado estimaciones finales de cientos bajos cuando el resultado real llega a millones.

¿Hay alguna forma de que yo pueda dar pistas al CE para hacer mejores estimaciones?

SELECT 1
FROM Table1 t1
     JOIN Table2 t2
       ON t1.Key1 = t2.Key1
          AND t1.Key2 = t2.Key2
     JOIN Table3 t3
       ON t1.Key1 = t3.Key1
          AND t1.Key2 = t3.Key2
WHERE t1.Key1 = 1;

Soluciones que he probado:

  • Crear estadísticas de varias columnas en Key1,Key2
  • Crear toneladas de estadísticas filtradas en Key1(Esto ayuda bastante, pero termino con miles de estadísticas creadas por el usuario en la base de datos).

Plan de ejecución enmascarado (perdón por el mal enmascaramiento)

En el caso que estoy viendo, el resultado tiene 9 millones de filas. El nuevo CE estima 180 filas; el legado CE estima 6100 filas.

Aquí hay un ejemplo reproducible:

DROP TABLE IF EXISTS #Table1, #Table2, #Table3;
CREATE TABLE #Table1 (Key1 INT NOT NULL, Key2 INT NOT NULL, T1Key3 INT NOT NULL, CONSTRAINT pk_t1 PRIMARY KEY CLUSTERED (Key1, Key2, T1Key3));
CREATE TABLE #Table2 (Key1 INT NOT NULL, Key2 INT NOT NULL, T2Key3 INT NOT NULL, CONSTRAINT pk_t2 PRIMARY KEY CLUSTERED (Key1, Key2, T2Key3));
CREATE TABLE #Table3 (Key1 INT NOT NULL, Key2 INT NOT NULL, T3Key3 INT NOT NULL, CONSTRAINT pk_t3 PRIMARY KEY CLUSTERED (Key1, Key2, T3Key3));

-- Table1 
WITH Numbers
     AS (SELECT TOP (1000000) Number = ROW_NUMBER() OVER(ORDER BY t1.number)
         FROM master..spt_values t1
              CROSS JOIN master..spt_values t2),
     DataSize (Key1, NumberOfRows)
     AS (SELECT 1, 2000 UNION
         SELECT 2, 10000 UNION
         SELECT 3, 25000 UNION
         SELECT 4, 50000 UNION
         SELECT 5, 200000)
INSERT INTO #Table1
SELECT Key1
     , Key2 = ROW_NUMBER() OVER (PARTITION BY Key1, T1Key3 ORDER BY Number)
     , T1Key3
FROM DataSize
     CROSS APPLY (SELECT TOP(NumberOfRows) 
                         Number
                       , T1Key3 = Number%(Key1*Key1) + 1 
                  FROM Numbers
                  ORDER BY Number) size;

-- Table2 (same Key1, Key2 values; smaller number of distinct third Key)
WITH Numbers
     AS (SELECT TOP (1000000) Number = ROW_NUMBER() OVER(ORDER BY t1.number)
         FROM master..spt_values t1
              CROSS JOIN master..spt_values t2)
INSERT INTO #Table2
SELECT DISTINCT 
       Key1
     , Key2
     , T2Key3
FROM #Table1
     CROSS APPLY (SELECT TOP (Key1*10) 
                         T2Key3 = Number
                  FROM Numbers
                  ORDER BY Number) size;

-- Table2 (same Key1, Key2 values; smallest number of distinct third Key)
WITH Numbers
     AS (SELECT TOP (1000000) Number = ROW_NUMBER() OVER(ORDER BY t1.number)
         FROM master..spt_values t1
              CROSS JOIN master..spt_values t2)
INSERT INTO #Table3
SELECT DISTINCT 
       Key1
     , Key2
     , T3Key3
FROM #Table1
     CROSS APPLY (SELECT TOP (Key1) 
                         T3Key3 = Number
                  FROM Numbers
                  ORDER BY Number) size;


DROP TABLE IF EXISTS #a;
SELECT col = 1 
INTO #a
FROM #Table1 t1
     JOIN #Table2 t2
       ON t1.Key1 = t2.Key1
          AND t1.Key2 = t2.Key2
WHERE t1.Key1 = 1;

DROP TABLE IF EXISTS #b;
SELECT col = 1 
INTO #b
FROM #Table1 t1
     JOIN #Table2 t2
       ON t1.Key1 = t2.Key1
          AND t1.Key2 = t2.Key2
     JOIN #Table3 t3
       ON t1.Key1 = t3.Key1
          AND t1.Key2 = t3.Key2
WHERE t1.Key1 = 1;

Respuestas:


5

Para ser claros, el optimizador ya sabe que es una unión de muchos a muchos. Si fuerza la combinación de combinaciones y mira un plan estimado, puede ver una propiedad para el operador de combinación que le indica si la combinación podría ser de muchos a muchos. El problema que debe resolver aquí es aumentar las estimaciones de cardinalidad, presumiblemente para que obtenga un plan de consulta más eficiente para la parte de la consulta que dejó de lado.

Lo primero que intentaría es poner los resultados de la unión desde Object3y Object5en una tabla temporal. Para el plan que publicó, es solo una columna en 51393 filas, por lo que apenas debería ocupar espacio en tempdb. Puede recopilar estadísticas completas en la tabla temporal y eso solo podría ser suficiente para obtener una estimación de cardinalidad final lo suficientemente precisa. Recopilar estadísticas completas también Object1puede ayudar. Las estimaciones de cardinalidad a menudo empeoran a medida que avanza desde un plan de derecha a izquierda.

Si eso no funciona, puede probar la ENABLE_QUERY_OPTIMIZER_HOTFIXESsugerencia de consulta si aún no la tiene habilitada a nivel de la base de datos o del servidor. Microsoft bloquea las correcciones de rendimiento que afectan el plan para SQL Server 2016 detrás de esa configuración. Algunos de ellos se relacionan con estimaciones de cardinalidad, por lo que quizás tenga suerte y una de las soluciones lo ayudará con su consulta. También puede intentar usar el estimador de cardinalidad heredado con una FORCE_LEGACY_CARDINALITY_ESTIMATIONsugerencia de consulta. Ciertos conjuntos de datos pueden obtener mejores estimaciones con el CE heredado.

Como último recurso, puede aumentar manualmente la estimación de cardinalidad por el factor que desee utilizando la MANY()función de Adam Machanic . Lo hablo en otra respuesta, pero parece que el enlace está muerto. Si te interesa, puedo intentar desenterrar algo.


La make_parallelfunción de Adam se acostumbra a ayudar a mitigar el problema. Voy a echar un vistazo many. Parece una curita bastante asquerosa.
Steven Hibble

2

Las estadísticas de SQL Server solo contienen un histograma para la columna inicial del objeto de estadísticas. Por lo tanto, puede crear estadísticas filtradas que proporcionen un histograma de valores para Key2, pero solo entre filas con Key1 = 1. La creación de estas estadísticas filtradas en cada tabla corrige las estimaciones y conduce al comportamiento que espera para la consulta de prueba: cada nueva unión no afecta la estimación de cardinalidad final (confirmada en SQL 2016 SP1 y SQL 2017).

-- Note: Add "WITH FULLSCAN" to each if you want a perfect 20,000 row estimate
CREATE STATISTICS st_#Table1 ON #Table1 (Key2) WHERE Key1 = 1
CREATE STATISTICS st_#Table2 ON #Table2 (Key2) WHERE Key1 = 1
CREATE STATISTICS st_#Table3 ON #Table3 (Key2) WHERE Key1 = 1

Sin estas estadísticas filtradas, SQL Server adoptará un enfoque más heurístico para estimar la cardinalidad de su unión. El siguiente documento contiene buenas descripciones de alto nivel de algunas de las heurísticas que utiliza SQL Server: Optimización de sus planes de consulta con el Estimador de cardinalidad de SQL Server 2014 .

Por ejemplo, agregar la USE HINT('ASSUME_JOIN_PREDICATE_DEPENDS_ON_FILTERS')sugerencia a su consulta cambiará la heurística de contención de la unión para asumir cierta correlación (en lugar de independencia) entre el Key1predicado y el Key2predicado de unión, lo que puede ser beneficioso para su consulta. Para la consulta de prueba final, esta sugerencia aumenta la estimación de cardinalidad de 1,175a 7,551, pero todavía está bastante por debajo de la 20,000estimación de fila correcta producida con las estadísticas filtradas.

Otro enfoque que hemos usado en situaciones similares es extraer el subconjunto relevante de los datos en tablas #temp. Especialmente ahora que las versiones más nuevas de SQL Server ya no escriben ansiosamente tablas #temp en el disco , hemos tenido buenos resultados con este enfoque. Su descripción de su unión de muchos a muchos implica que cada tabla de #temp individual en su caso sería relativamente pequeña (o al menos más pequeña que el conjunto de resultados final), por lo que vale la pena probar este enfoque.

DROP TABLE IF EXISTS #Table1_extract, #Table2_extract, #Table3_extract, #c
-- Extract only the subset of rows that match the filter predicate
-- (Or better yet, extract only the subset of columns you need!)
SELECT * INTO #Table1_extract FROM #Table1 WHERE Key1 = 1
SELECT * INTO #Table2_extract FROM #Table2 WHERE Key1 = 1
SELECT * INTO #Table3_extract FROM #Table3 WHERE Key1 = 1
-- Now perform the join on those extracts, removing the filter predicate
SELECT col = 1
INTO #c 
FROM #Table1_extract t1
JOIN #Table2_extract t2
    ON t1.Key2 = t2.Key2
JOIN #Table3_extract t3
    ON t1.Key2 = t3.Key2

Usamos estadísticas filtradas ampliamente, pero las hacemos una por Key1valor en cada tabla. Ahora tenemos miles de ellos.
Steven Hibble

2
@StevenHibble Buen punto de que miles de estadísticas filtradas podrían dificultar la administración. (También hemos visto que afecta negativamente el tiempo de compilación del plan). Puede que no se ajuste a su caso de uso, pero también agregué otro enfoque de tabla #temp que hemos usado con éxito varias veces.
Geoff Patterson

-1

Un alcance No hay una base real aparte de intentarlo.

SELECT 1
FROM Table1 t1
     JOIN Table2 t2
       ON t1.Key2 = t2.Key2
      AND t1.Key1 = 1
      AND t2.Key1 = 1
     JOIN Table3 t3
       ON t2.Key2 = t3.Key2
      AND t3.Key1 = 1;
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.