Sql Server ne parvient pas à utiliser l'index sur une simple bijection

11

Ceci est une autre énigme d'optimiseur de requête.

Peut-être que je surestime simplement les optimiseurs de requête, ou peut-être qu'il me manque quelque chose - je le mets donc là.

J'ai une table 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])

avec un index et quelques milliers de lignes, Numberréparties uniformément dans les valeurs 0, 1 et 2.

Maintenant, cette requête:

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;

recherche un indice IX_Numbercomme on pourrait s'y attendre.

Si la clause where est

WHERE P.Name = 'one';

cependant, cela devient un scan.

La clause de cas est évidemment une bijection, donc en théorie une optimisation devrait être possible pour déduire le premier plan de requête de la seconde requête.

Ce n'est pas non plus purement académique: la requête est inspirée de la traduction des valeurs d'énumération en leurs noms conviviaux respectifs.

J'aimerais entendre quelqu'un qui sait ce que l'on peut attendre des optimiseurs de requêtes (et en particulier celui de Sql Server): est-ce que j'attends simplement trop?

Je demande comme j'avais des cas auparavant où une légère variation d'une requête ferait soudainement apparaître une optimisation.

J'utilise Sql Server 2016 Developer Edition.

John
la source

Réponses:

18

Suis-je tout simplement trop attendre?

Oui. Au moins dans les versions actuelles du produit.

SQL Server ne séparera pas l' CASEinstruction et ne fera pas de rétro-ingénierie pour découvrir que si le résultat de la colonne calculée est 'one'alors [Extent1].[Number]doit être 0.

Vous devez vous assurer que vous écrivez vos prédicats pour être sargable. Ce qui implique presque toujours que ce soit dans la forme. basetable_column_name comparison_operator expression.

Même des écarts mineurs brisent la sargabilité.

WHERE P.Number + 0 = 0;

n'utiliserait pas non plus une recherche d'index même s'il est encore plus simple à simplifier que l' CASEexpression.

Si vous souhaitez rechercher un nom de chaîne et obtenir une recherche sur un numéro, vous auriez besoin d'une table de mappage avec les noms et les numéros et la rejoindre dans la requête, le plan pourrait avoir une recherche sur la table de mappage suivie d'une recherche corrélée sur [dbo].[MyEntities]le nombre de retour de la première recherche.

Martin Smith
la source
6

Ne projetez pas votre énumération comme une déclaration de cas. Projetez-le comme une table dérivée comme ceci:

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

Je suppose que vous obtiendrez de meilleurs résultats. (Je n'ai pas converti le nom en ?manquant, car cela pourrait probablement interférer avec les gains de performances possibles. Cependant, vous pouvez déplacer la WHEREclause à l'intérieur de la requête externe afin de placer le prédicat sur la enumtable, ou vous pouvez renvoyer deux colonnes de la requête interne, une pour le prédicat et une pour l'affichage, où se trouve le prédicat NULLlorsqu'il n'y a pas de valeur d'énumération correspondante.)

Je suppose, cependant, qu'en raison de cela [Extent1], vous utilisez un ORM tel que Entity Framework ou Linq-To-SQL. Je ne peux pas vous guider comment réaliser une telle projection en natif, mais vous pouvez utiliser une technique différente.

Dans un de mes projets, j'ai reflété des valeurs d'énumération de code dans de vraies tables de la base de données, via une classe de construction personnalisée qui a fusionné les valeurs d'énumération dans la base de données. (Vous devez respecter la règle selon laquelle vous devez lister explicitement vos valeurs d'énumération, vous ne pouvez jamais en supprimer sans revoir vos tables, et vous ne pouvez jamais, jamais les changer, même si vous devez déjà en observer au moins une partie avec votre configuration actuelle) .

Maintenant, j'utilisais un énumérable d'une Identifierclasse de base qui a de nombreuses sous-classes concrètes différentes, mais il n'y a aucune raison que cela ne puisse pas être fait avec une énumération vanille simple. Voici un exemple d'utilisation:

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

Vous pouvez voir que j'ai transmis toutes les informations nécessaires afin d'écrire et de lire les valeurs de la base de données. (J'ai eu une situation où la demande actuelle peut ne pas contenir toutes les valeurs existantes, donc je devais renvoyer tout élément supplémentaire de la base de données ainsi que l'ensemble actuellement chargé. J'ai également laissé la base de données attribuer des ID, bien que pour une énumération, vous ne le feriez probablement pas veulent que.)

L'idée est qu'une fois que vous avez une table qui n'est lue / écrite qu'une seule fois au démarrage et qui aura de manière fiable toutes les valeurs d'énumération, vous vous y joindrez simplement comme n'importe quelle autre table, et les performances devraient être bonnes.

J'espère que ces idées vous suffisent pour faire une amélioration.

ErikE
la source
Oui, j'utilise EntityFramework et c'est là que la solution devrait vraiment se trouver dans un monde optimal. Avant cela, votre suggestion est l'une des meilleures solutions de contournement, je crois.
John
5

J'interprète la question comme si vous êtes intéressé par les optimiseurs en général, mais avec un intérêt particulier pour SQL Server. J'ai testé votre scénario avec 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"

L'optimiseur dans DB2 réécrit la deuxième requête dans la première:

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)

Le plan ressemble à:

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

Je ne connais pas grand-chose aux autres optimiseurs, mais j'ai l'impression que l'optimiseur DB2 est considéré comme assez bon, même parmi les concurrents.

Lennart
la source
C'est excitant. Pouvez-vous nous éclairer sur l'origine de la «déclaration optimisée»? Est-ce que db2 lui-même vous le rend? - De plus, j'ai du mal à lire le plan. Je suppose que "IXSCAN" ne signifie pas un scan d'index dans ce cas?
John
1
Vous pouvez demander à DB2 de vous expliquer une déclaration. Les informations collectées sont stockées dans un ensemble de tableaux, et vous pouvez soit utiliser l'explication visuelle ou comme dans ce cas l'utilitaire db2exfmt (ou créer votre propre util). De plus, vous pouvez surveiller une déclaration et comparer la cardinalité estimée dans le plan avec le plan réel. Dans ce plan, nous pouvons voir qu'il s'agit bien d'un indexscan (IXSCAN) et la sortie estimée de cet opérateur est de 3334 lignes. Est-ce mauvais dans le serveur SQL? Il connaît la clé de démarrage et la touche d'arrêt, il analyse donc uniquement les lignes pertinentes dans DB2.
Lennart
Donc, ce qu'il appelle l'analyse implique la recherche et, pour être honnête, les explications équivalentes du plan de Sql Server appellent parfois quelque chose une analyse qui implique la recherche, et d'autres fois, il l'appelle une recherche. J'ai toujours besoin de regarder le nombre de lignes pour comprendre quoi. Puisqu'il y a clairement un 3334 dans la sortie de db2, il fait bien ce que j'espérais. Très intéressant.
John
Oui, je trouve aussi cela parfois déroutant. Il faut regarder les informations plus détaillées pour chaque opérateur pour vraiment comprendre ce qui se passe.
Lennart
0

Dans cette requête particulière, il est assez stupide d'avoir même une CASEdéclaration. Vous filtrez vers un cas particulier! Peut-être que ce n'est qu'un détail de l'exemple de requête particulier que vous avez donné, mais sinon, vous pouvez écrire cette requête pour obtenir des résultats équivalents:

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

Cela vous donnera exactement le même jeu de résultats, et puisque vous êtes déjà en train de coder en dur des valeurs dans une CASEinstruction, vous ne perdez aucune maintenance ici.

jpmc26
la source
1
Je pense que vous manquez le point - c'est du SQL généré à partir d'une base de code principale qui fonctionne avec des énumérations via leurs représentations de chaînes. Le code qui projette le SQL fait la violence de la requête. Je suis sûr que le demandeur, s'il écrivait lui-même le SQL, serait en mesure d'écrire une meilleure requête. Ainsi, ce n'est pas idiot d'avoir une CASEdéclaration du tout, parce que les ORM font ce genre de chose. Ce qui est stupide, c'est que vous n'avez pas reconnu ces facettes simples du problème ... (comment ça s'appelle indirectement sans cervelle?)
ErikE
@ErikE Encore un peu idiot, car vous pouvez simplement utiliser la valeur numérique de l'énumération, en supposant que C # de toute façon. (Une hypothèse assez sûre étant donné que nous parlons de SQL Server.)
jpmc26
Mais vous n'avez aucune idée de ce qu'est le cas d'utilisation. Ce serait peut-être un énorme changement de passer à la valeur numérique. Peut-être que les énumérations ont été mises à niveau dans une base de code géante existante. Critiquer sans connaissance est ridicule.
ErikE
@ErikE Si c'est ridicule, pourquoi le faites-vous? =) J'ai seulement répondu pour souligner que si le cas d'utilisation est aussi simple que l'exemple de la question (qui est clairement spécifié dans la préface de ma réponse), l' CASEénoncé peut être entièrement éliminé sans inconvénient. Bien sûr, il peut y avoir des facteurs inconnus, mais ils ne sont pas spécifiés.
jpmc26
Je n'ai pas d'objection aux parties factuelles de votre réponse, juste aux parties qui caractérisent subjectivement. Quant à savoir si je critique sans le savoir, j'ai toutes les oreilles pour comprendre de quelle manière je n'ai pas utilisé une logique scrupuleusement propre ou j'ai fait des hypothèses qui sont manifestement fausses ...
ErikE