SQL Server no puede utilizar el índice en bijection simple

11

Este es otro enigma del optimizador de consultas.

Tal vez solo estoy sobreestimando los optimizadores de consultas, o tal vez me falta algo, así que lo estoy publicando.

Tengo una mesa simple

CREATE TABLE [dbo].[MyEntities](
  [Id] [uniqueidentifier] NOT NULL,
  [Number] [int] NOT NULL,
  CONSTRAINT [PK_dbo.MyEntities] PRIMARY KEY CLUSTERED ([Id])
)

CREATE NONCLUSTERED INDEX [IX_Number] ON [dbo].[MyEntities] ([Number])

con un índice y unos miles de filas allí, Numberdistribuidos uniformemente en los valores 0, 1 y 2.

Ahora esta consulta:

SELECT * FROM
    (SELECT
        [Extent1].[Number] AS [Number],
        CASE
        WHEN (0 = [Extent1].[Number]) THEN 'one'
        WHEN (1 = [Extent1].[Number]) THEN 'two'
        WHEN (2 = [Extent1].[Number]) THEN 'three'
        ELSE '?'
        END AS [Name]
        FROM [dbo].[MyEntities] AS [Extent1]
        ) P
WHERE P.Number = 0;

busca un índice IX_Numbercomo cabría esperar.

Si la cláusula where es

WHERE P.Name = 'one';

Sin embargo, se convierte en un escaneo.

La cláusula del caso es obviamente una biyección, por lo que en teoría una optimización debería ser posible deducir el primer plan de consulta de la segunda consulta.

Tampoco es puramente académico: la consulta se inspira en la traducción de valores de enumeración a sus respectivos nombres descriptivos.

Me gustaría saber de alguien que sepa qué se puede esperar de los optimizadores de consultas (y específicamente el que está en Sql Server): ¿Simplemente estoy esperando demasiado?

Pregunto, ya que tenía casos antes, donde una ligera variación de una consulta haría que una optimización saliera a la luz de repente.

Estoy usando Sql Server 2016 Developer Edition.

John
fuente

Respuestas:

18

¿Simplemente estoy esperando demasiado?

Si. Al menos en las versiones actuales del producto.

SQL Server no machacar a la CASEdeclaración y realizar ingeniería inversa a descubrir que si el resultado de la columna calculada es 'one'entonces [Extent1].[Number]debe ser 0.

Debe asegurarse de escribir sus predicados para ser sargable. Lo que casi siempre implica estar en la forma. basetable_column_name comparison_operator expression.

Incluso las desviaciones menores rompen la sargabilidad.

WHERE P.Number + 0 = 0;

tampoco usaría una búsqueda de índice aunque sea aún más sencillo de simplificar que la CASEexpresión.

Si desea buscar un nombre de cadena y obtener un número de búsqueda, necesitaría una tabla de mapeo con los nombres y números y unirse a ella en la consulta, entonces el plan podría tener una búsqueda en la tabla de mapeo seguida de una búsqueda correlacionada en [dbo].[MyEntities]el número de regresar de la primera búsqueda.

Martin Smith
fuente
6

No proyecte su enumeración como una declaración de caso. Proyectarlo como una tabla derivada así:

SELECT * FROM
   (SELECT
      [Extent1].[Number] AS [Number],
      enum.Name
   FROM
      [dbo].[MyEntities] AS [Extent1]
      LEFT JOIN (VALUES
         (0, 'one'),
         (1, 'two'),
         (2, 'three')
      ) enum (Number, Name)
         ON Extent1.Number = enum.Number
   ) P
WHERE
   P.Name = 'one';

Sospecho que obtendrás mejores resultados. (No convertí el Nombre a ?cuando faltaba porque esto probablemente interferiría con posibles ganancias de rendimiento. Sin embargo, podría mover la WHEREcláusula dentro de la consulta externa para colocar el predicado en la enumtabla, o podría devolver dos columnas del consulta interna, una para el predicado y otra para visualización, donde está el predicado NULLcuando no hay un valor de enumeración coincidente).

Sin embargo, supongo que debido a eso [Extent1], está utilizando un ORM como Entity Framework o Linq-To-SQL. No puedo guiarte cómo lograr tal proyección de forma nativa, pero podrías usar una técnica diferente.

En un proyecto mío, reflejé valores de enumeración de código en tablas reales en la base de datos, a través de una clase de compilación personalizada que fusionó los valores de enumeración en la base de datos. (Debe respetar la regla de que debe enumerar explícitamente sus valores de enumeración, nunca puede eliminar ninguno sin revisar sus tablas, y nunca, nunca puede cambiarlos, aunque ya debe observar al menos algo de esto con su configuración actual) .

Ahora, estaba usando un enumerable de una Identifierclase base que tiene muchas subclases concretas diferentes, pero no hay razón para que no se pueda hacer con una simple enumeración de vainilla. Aquí hay un ejemplo de uso:

new EnumOrIdentifierProjector<CodeClassOrEnum, PrivateDbDtoObject>(
   _sqlConnector.Connection,
   "dbo.TableName",
   "PrimaryKeyId",
   "NameColumnName",
   dtoObject => dtoObject.PrimaryKeyId,
   dtoObject => dtoObject.NameField,
   EnumerableOfIdentifierOrTypeOfEnum
)
   .Populate();

Puede ver que pasé toda la información necesaria para escribir y leer los valores de la base de datos. (Tuve una situación en la que la solicitud actual puede no contener todos los valores existentes, por lo que necesitaba devolver cualquier valor adicional de la base de datos, así como el conjunto actualmente cargado. También dejé que la base de datos asigne ID, aunque para una enumeración probablemente no lo haría quiero eso.)

La idea es que una vez que tenga una tabla que se lea / escriba solo una vez en el inicio que tendrá todos los valores de enumeración de manera confiable, simplemente se una como cualquier otra tabla, y el rendimiento debería ser bueno.

Espero que estas ideas sean suficientes para que puedas mejorar.

ErikE
fuente
Sí, uso EntityFramework y ahí es donde la solución realmente debería estar en un mundo óptimo. Antes de que eso suceda, su sugerencia es una de las mejores soluciones que creo.
John
5

Interpreto la pregunta como si estuviera interesado en los optimizadores en general, pero con un interés especial para SQL Server. Probé su escenario con db2 LUW V11.1:

]$ db2 "create table myentities ( id int not null, number int not null )"
]$ db2 "create index ix_number on myentities (number)"
]$ db2 "insert into myentities (id, number) with t(n) as ( values 0 union all select n+1 from t where n<10000) select n, mod(n,3) from t"

El optimizador en DB2 reescribe la segunda consulta a la primera:

Original Statement:
------------------
SELECT 
  * 
FROM 
  (SELECT 
     number,

   CASE 
   WHEN (0 = Number) 
   THEN 'one' 
   WHEN (1 = Number) 
   THEN 'two' 
   WHEN (2 = Number) 
   THEN 'three' 
   ELSE '?' END AS Name 
   FROM 
     MyEntities
  ) P 
WHERE 
  P.name = 'one'


Optimized Statement:
-------------------
SELECT 
  Q1.NUMBER AS "NUMBER",

CASE 
WHEN (0 = Q1.NUMBER) 
THEN 'one' 
WHEN (1 = Q1.NUMBER) 
THEN 'two' 
WHEN (2 = Q1.NUMBER) 
THEN 'three' 
ELSE '?' END AS "NAME" 
FROM 
  LELLE.MYENTITIES AS Q1 
WHERE 
  (0 = Q1.NUMBER)

El plan se ve así:

Access Plan:
-----------
        Total Cost:             33.5483
        Query Degree:           1


      Rows 
     RETURN
     (   1)
      Cost 
       I/O 
       |
      3334 
     IXSCAN
     (   2)
     33.1861 
     4.66713 
       |
      10001 
 INDEX: LELLE   
    IX_NUMBER
       Q1

No sé mucho sobre otros optimizadores, pero tengo la sensación de que el optimizador de DB2 se considera bastante bueno incluso entre los competidores.

Lennart
fuente
Eso es emocionante. ¿Puede arrojar algo de luz sobre el origen de la "declaración optimizada"? ¿Db2 mismo te lo devuelve? - Además, tengo problemas para leer el plan. Supongo que "IXSCAN" no significa exploración de índice en este caso.
John
1
Puede decirle a DB2 que le explique una declaración. La información recopilada se almacena en un conjunto de tablas, y puede usar la explicación visual o, en este caso, la utilidad db2exfmt (o crear su propia utilidad). Además, puede monitorear una declaración y comparar la cardinalidad estimada en el plan con el plan real. En este plan podemos ver que de hecho es un indexscan (IXSCAN) y la salida estimada de este operador es de 3334 filas. ¿Es esto malo en el servidor SQL? Conoce la tecla de inicio y la tecla de parada, por lo que solo escanea las filas relevantes en DB2.
Lennart
Entonces, lo que llama exploración implica búsqueda, y para ser honesto, las explicaciones del plan equivalente de SQL Server también a veces llaman a algo exploración que implica búsqueda, y otras veces lo llaman búsqueda. Siempre necesito mirar el recuento de filas para comprender qué es qué. Dado que claramente hay un 3334 en la salida de db2, seguro que hace lo que esperaba. Muy interesante.
John
Sí, a veces también me resulta confuso. Hay que mirar la información más detallada para cada operador para comprender realmente lo que está sucediendo.
Lennart
0

En esta consulta en particular, es bastante tonto incluso tener una CASEdeclaración. ¡Estás filtrando a un caso en particular! Quizás esto sea solo un detalle de la consulta de ejemplo particular que ha dado, pero si no, puede escribir esta consulta para obtener resultados equivalentes:

SELECT
    [Extent1].[Number] AS [Number],
    'one' AS [Name]
FROM [dbo].[MyEntities] AS [Extent1]
WHERE [Extent1].[Number] = 0;

Esto le dará exactamente el mismo conjunto de resultados, y dado que de CASEtodos modos ya está codificando valores en una declaración, no está perdiendo ninguna capacidad de mantenimiento aquí.

jpmc26
fuente
1
Creo que te estás perdiendo el punto: esto se genera SQL a partir de una base de código de fondo que funciona con enumeraciones a través de sus representaciones de cadena. El código que proyecta el SQL está afectando la consulta. Estoy seguro de que el autor de la pregunta, si él mismo estuviera escribiendo el SQL, podría escribir una mejor consulta. Por lo tanto, no es tonto tener una CASEdeclaración en absoluto, porque los ORM hacen ese tipo de cosas. Lo tonto es que no reconociste estas simples facetas del problema ... (¿cómo es eso de ser indirectamente llamado sin cerebro?)
ErikE
@ErikE Todavía es un poco tonto, ya que puede usar el valor numérico de la enumeración, suponiendo que C # de todos modos. (Una suposición bastante segura dado que estamos hablando de SQL Server.)
jpmc26
Pero no tienes idea de cuál es el caso de uso. Tal vez sería un gran cambio cambiar al valor numérico. Tal vez las enumeraciones se actualizaron en una base de código gigante existente. Criticar sin conocimiento es ridículo.
ErikE
@ErikE Si es ridículo, ¿por qué lo haces? =) Solo respondí para señalar que si el caso de uso es tan simple como el ejemplo en la pregunta (que se especifica claramente en el prefacio de mi respuesta), la CASEdeclaración se puede eliminar por completo sin inconvenientes. Por supuesto , puede haber factores desconocidos, pero no están especificados.
jpmc26
No tengo objeciones a las partes objetivas de su respuesta, solo a las partes que caracterizan subjetivamente. En cuanto a si estoy criticando sin conocimiento, soy todo oídos para comprender cualquier forma en que no haya podido usar una lógica escrupulosamente limpia o haya hecho suposiciones que son demostrablemente falsas ...
ErikE