Il server SQL non riesce a utilizzare l'indice su bijection semplice


11

Questo è un altro enigma di Query Optimizer.

Forse sto solo sopravvalutando gli ottimizzatori di query o forse mi sto perdendo qualcosa, quindi lo sto pubblicando.

Ho un tavolo semplice

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 indice e alcune migliaia di righe, Numberdistribuite uniformemente nei valori 0, 1 e 2.

Ora questa query:

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;

un indice cerca IX_Numbercome ci si aspetterebbe.

Se la clausola where è

WHERE P.Name = 'one';

tuttavia, diventa una scansione.

La clausola case è ovviamente una biiezione, quindi in teoria dovrebbe essere possibile un'ottimizzazione per dedurre il primo piano di query dalla seconda query.

Inoltre non è puramente accademico: la query si ispira traducendo i valori enum nei loro rispettivi nomi amichevoli.

Mi piacerebbe avere notizie da qualcuno che sa cosa ci si può aspettare da Query Optimizer (e in particolare quello nel Server SQL): mi aspetto semplicemente troppo?

Sto chiedendo come ho avuto casi prima in cui qualche leggera variazione di una query avrebbe fatto emergere improvvisamente un'ottimizzazione.

Sto usando Sql Server 2016 Developer Edition.

Risposte:


18

Mi aspetto semplicemente troppo?

Sì. Almeno nelle versioni attuali del prodotto.

SQL Server non prenderà parte il CASEcomunicato e reverse engineering è di scoprire che, se il risultato della colonna calcolata è 'one'quindi [Extent1].[Number]necessario essere 0.

Devi assicurarti di scrivere i tuoi predicati per essere sargable. Che quasi sempre implica che sia nella forma. basetable_column_name comparison_operator expression.

Anche le deviazioni minori infrangono la sargability.

WHERE P.Number + 0 = 0;

non utilizzerebbe neanche una ricerca dell'indice, anche se è ancora più semplice semplificare CASEdell'espressione.

Se si desidera cercare un nome di stringa e ottenere un numero di ricerca, è necessaria una tabella di mappatura con nomi e numeri e unirsi ad esso nella query, quindi il piano potrebbe avere una ricerca sulla tabella di mappatura seguita da una ricerca correlata avanti [dbo].[MyEntities]con il numero restituito dalla prima ricerca.


6

Non proiettare il tuo enum come una dichiarazione di caso. Proiettalo come una tabella derivata in questo modo:

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

Sospetto che otterrai risultati migliori. (Non ho convertito il nome in ?quando mancante perché questo probabilmente interferirebbe con possibili aumenti delle prestazioni. Tuttavia, è possibile spostare la WHEREclausola all'interno della query esterna per mettere il predicato sulla enumtabella o è possibile restituire due colonne dal query interna, una per il predicato e una per la visualizzazione, dove si trova quello del predicato NULLquando non esiste un valore enum corrispondente.)

Immagino, tuttavia, che a causa di ciò [Extent1], stai usando un ORM come Entity Framework o Linq-To-SQL. Non posso guidarti su come realizzare tale proiezione in modo nativo, ma potresti usare una tecnica diversa.

In un mio progetto, ho riflesso i valori enum del codice in tabelle reali nel database, attraverso una classe di generazione personalizzata che ha unito i valori enum nel database. (Devi rispettare la regola che devi elencare esplicitamente i tuoi valori enum, non puoi mai cancellarli senza rivedere le tue tabelle e non puoi mai, mai modificarli, anche se devi già osservare almeno un po 'di questo con la tua configurazione attuale) .

Ora, stavo usando un elenco di una Identifierclasse base che ha molte sottoclassi concrete diverse, ma non c'è motivo per cui non si potesse fare con un semplice enum vanilla. Ecco un esempio di utilizzo:

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

Potete vedere che ho passato tutte le informazioni necessarie per scrivere e leggere i valori del database. (Ho avuto una situazione in cui la richiesta corrente potrebbe non contenere tutti i valori esistenti, quindi era necessario restituire qualsiasi ulteriore dal database così come il set attualmente caricato. Ho anche lasciato che il database assegnasse gli ID, anche se per un enum probabilmente non lo faresti voglio quello.)

L'idea è che una volta che hai una tabella che viene letta / scritta una sola volta all'avvio che avrà in modo affidabile tutti i valori enum, ti unisci semplicemente ad essa come qualsiasi altra tabella e le prestazioni dovrebbero essere buone.

Spero che queste idee siano sufficienti per farti migliorare.


Sì, utilizzo EntityFramework e lì è dove la soluzione dovrebbe davvero essere in un mondo ottimale. Prima che ciò accada, il tuo suggerimento è uno dei migliori soluzioni alternative credo.
Giovanni

5

Interpreto la domanda secondo cui sei interessato agli ottimizzatori in generale, ma con un interesse speciale per SQL Server. Ho testato il tuo scenario 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"

L'ottimizzatore in DB2 riscrive la seconda query nella prima:

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)

Il piano è simile a:

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

Non so molto di altri ottimizzatori, ma ho la sensazione che l'ottimizzatore DB2 sia considerato abbastanza buono anche tra i concorrenti.


È eccitante. Puoi far luce su da dove viene la "dichiarazione ottimizzata"? Db2 stesso te lo restituisce? - Inoltre, ho difficoltà a leggere il piano. Lo prendo "IXSCAN" non significa scansione indice in questo caso?
Giovanni

1
Puoi dire a DB2 di spiegare una dichiarazione per te. Le informazioni raccolte sono memorizzate in un set di tabelle e puoi usare visual spieg o come in questo caso l'utilità db2exfmt (o creare il tuo util). Inoltre, è possibile monitorare un'istruzione e confrontare la cardinalità stimata nel piano con il piano effettivo. In questo piano possiamo vedere che si tratta effettivamente di un indice (IXSCAN) e l'output stimato da questo operatore è di 3334 righe. Questo è male in SQL Server? Conosce la chiave di avvio e la chiave di arresto, quindi scansiona solo le righe rilevanti in DB2.
Lennart,

Quindi ciò che chiama scan implica la ricerca e, ad essere onesti, le spiegazioni del piano equivalente di Sql Server a volte chiamano qualcosa una scansione che implica la ricerca, e altre volte lo chiama una ricerca. Ho sempre bisogno di guardare il conteggio delle righe per capire cosa è cosa. Dal momento che c'è chiaramente un 3334 nell'output di db2, fa sicuramente quello che speravo. Molto interessante.
Giovanni,

Sì, lo trovo anche confuso a volte. Bisogna guardare le informazioni più dettagliate per ogni operatore per capire veramente cosa sta succedendo.
Lennart,

0

In questa particolare query, è abbastanza sciocco persino avere una CASEdichiarazione. Stai filtrando fino a un caso particolare! Forse questo è solo un dettaglio della particolare query di esempio che hai dato, ma in caso contrario, puoi scrivere questa query per ottenere risultati equivalenti:

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

Questo ti darà esattamente lo stesso set di risultati e dato che stai già programmando con forza valori in una CASEdichiarazione, non stai perdendo alcuna manutenibilità qui.


1
Penso che manchi il punto: questo è generato da SQL da una base di codice back-end che funziona con gli enum tramite le loro rappresentazioni di stringa. Il codice che proietta l'SQL sta violando la query. Sono sicuro che il richiedente, se stesse scrivendo lo stesso SQL, sarebbe in grado di scrivere una query migliore. Pertanto, non è sciocco avere una CASEdichiarazione, perché gli ORM fanno questo genere di cose. La cosa sciocca è che non hai riconosciuto queste semplici sfaccettature del problema ... (come è che per essere indirettamente chiamato senza cervello?)
ErikE

@ErikE Ancora un po 'sciocco, dato che puoi semplicemente usare il valore numerico dell'enum, assumendo C # comunque. (Un presupposto abbastanza sicuro dato che stiamo parlando di SQL Server.)
jpmc26

Ma non hai idea di quale sia il caso d'uso. Forse sarebbe un grande cambiamento passare al valore numerico. Forse gli enum sono stati trasformati in una base di codice gigante esistente. Criticare senza conoscenza è ridicolo.
ErikE

@ErikE Se è ridicolo, allora perché lo fai? =) Ho solo risposto per sottolineare che se il caso d'uso è semplice come nell'esempio nella domanda (che è chiaramente specificato nella prefazione della mia risposta), l' CASEaffermazione può essere eliminata del tutto senza inconvenienti. Di Certo ci potrebbe essere incognite, ma sono non specificato.
jpmc26,

Non ho obiezioni alle parti fattuali della tua risposta, solo alle parti che caratterizzano soggettivamente. Per quanto riguarda il fatto che io stia criticando senza conoscenza, sono tutti orecchi per capire in che modo non sono riuscito a usare una logica scrupolosamente pulita o ho fatto ipotesi che sono manifestamente false ...
ErikE
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.