Sql Server falha ao usar o índice na simples bijeção

11

Esse é outro enigma do otimizador de consulta.

Talvez eu esteja apenas superestimando os otimizadores de consulta ou talvez esteja perdendo alguma coisa - por isso estou publicando isso.

Eu tenho uma mesa simples

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

com um índice e algumas milhares de linhas, Numbersendo distribuídos igualmente nos valores 0, 1 e 2.

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

um índice procura IX_Numbercomo seria de esperar.

Se a cláusula where for

WHERE P.Name = 'one';

no entanto, torna-se uma varredura.

A cláusula de caso é obviamente uma bijeção, portanto, em teoria, uma otimização deve ser possível deduzir o primeiro plano de consulta da segunda consulta.

Também não é puramente acadêmico: a consulta é inspirada na tradução de valores enum para seus respectivos nomes amigáveis.

Eu gostaria de ouvir de alguém que sabe o que pode ser esperado dos otimizadores de consulta (e especificamente do Sql Server): Estou simplesmente esperando demais?

Estou perguntando como tive casos anteriores em que uma ligeira variação de uma consulta faria uma otimização aparecer de repente.

Estou usando o Sql Server 2016 Developer Edition.

John
fonte

Respostas:

18

Estou simplesmente esperando demais?

Sim. Pelo menos nas versões atuais do produto.

O SQL Server não separará a CASEinstrução e fará a engenharia reversa para descobrir que, se o resultado da coluna calculada for 'one', [Extent1].[Number]deve ser 0.

Você precisa certificar-se de escrever seus predicados para serem sargáveis. O que quase sempre envolve estar na forma. basetable_column_name comparison_operator expression.

Mesmo pequenos desvios quebram a sargabilidade.

WHERE P.Number + 0 = 0;

não usaria uma busca de índice, mesmo que seja ainda mais simples de simplificar do que a CASEexpressão.

Se você deseja procurar um nome de sequência e obter um número, você precisaria de uma tabela de mapeamento com os nomes e números e se unir a ela na consulta, então o plano pode ter uma pesquisa na tabela de mapeamento seguida por uma pesquisa correlacionada on [dbo].[MyEntities]com o número retornado do primeiro procurar.

Martin Smith
fonte
6

Não projete seu enum como uma declaração de caso. Projete-o como uma tabela derivada da seguinte forma:

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

Eu suspeito que você obterá melhores resultados. (Não converti o Nome para ?quando estava ausente, pois isso provavelmente interferiria em possíveis ganhos de desempenho. No entanto, você pode mover a WHEREcláusula dentro da consulta externa para colocar o predicado na enumtabela ou retornar duas colunas do consulta interna, uma para o predicado e outra para exibição, onde o predicado está NULLquando não há valor de enum correspondente.)

No entanto, acho que, devido a isso [Extent1], você está usando um ORM como o Entity Framework ou o Linq-To-SQL. Não posso orientá-lo sobre como realizar essa projeção nativamente, mas você poderia usar uma técnica diferente.

Em um projeto meu, refleti valores de enumeração de código em tabelas reais no banco de dados, através de uma classe de construção personalizada que mesclou os valores de enumeração no banco de dados. (Você precisa respeitar a regra de que deve listar explicitamente seus valores de enumeração, nunca pode excluir nenhum sem revisar suas tabelas e nunca pode alterá-los, embora você já precise observar pelo menos parte disso na configuração atual) .

Agora, eu estava usando um enumerável de uma Identifierclasse base que possui muitas subclasses concretas diferentes, mas não há razão para que isso não possa ser feito com um enum de baunilha comum. Aqui está um exemplo de uso:

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

Você pode ver que eu passei todas as informações necessárias para escrever e ler os valores do banco de dados. (Eu tive uma situação em que a solicitação atual pode não conter todos os valores existentes, portanto, é necessário retornar qualquer adicional do banco de dados e do conjunto carregado no momento. Também deixei o banco de dados atribuir IDs, embora para uma enumeração você provavelmente não quero isso.)

A idéia é que, quando você tiver uma tabela que seja lida / gravada apenas uma vez na inicialização e que tenha todos os valores de enum de maneira confiável, basta associá-la como qualquer outra tabela, e o desempenho deverá ser bom.

Espero que essas idéias sejam suficientes para você fazer uma melhoria.

ErikE
fonte
Sim, eu uso o EntityFramework e é aí que a solução realmente deve estar em um mundo ideal. Antes que isso aconteça, sua sugestão é uma das melhores soluções alternativas que acredito.
John
5

Interpreto a pergunta como se você estivesse interessado em otimizadores em geral, mas com um interesse especial pelo SQL Server. Testei seu cenário com o 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"

O otimizador no DB2 reescreve a segunda consulta na primeira:

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)

O plano se parece com:

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

Não sei muito sobre outros otimizadores, mas sinto que o otimizador do DB2 é considerado muito bom, mesmo entre os concorrentes.

Lennart
fonte
Isso é empolgante. Você pode esclarecer de onde vem a "declaração otimizada"? O próprio db2 devolve isso para você? - Além disso, tenho problemas para ler o plano. Acho que "IXSCAN" não significa varredura de índice neste caso?
John
1
Você pode dizer ao DB2 para explicar uma declaração para você. As informações coletadas são armazenadas em um conjunto de tabelas e você pode usar a explicação visual ou, nesse caso, o utilitário db2exfmt (ou criar seu próprio utilitário). Além disso, você pode monitorar uma declaração e comparar a cardinalidade estimada no plano com o plano real. Nesse plano, podemos ver que, de fato, é uma indexscan (IXSCAN) e a saída estimada desse operador é de 3334 linhas. Isso é ruim no servidor SQL? Ele conhece a tecla start e a tecla stop, portanto, apenas varre as linhas relevantes no DB2.
Lennart
Portanto, o que chama de varredura envolve a busca e, para ser honesto, as explicações equivalentes do plano do Sql Server às vezes também chamam algo de varredura que envolve a busca, e outras vezes o chamam de busca. Eu sempre preciso olhar para a contagem de linhas para entender o que é o quê. Como há claramente uma saída de 3334 na db2, com certeza faz o que eu estava esperando. Muito interessante.
John
Sim, às vezes também acho confuso. É preciso olhar para as informações mais detalhadas de cada operador para realmente entender o que está acontecendo.
Lennart
0

Nesta consulta em particular, é muito bobo até ter uma CASEdeclaração. Você está filtrando para um caso específico! Talvez este seja apenas um detalhe da consulta de exemplo específica que você forneceu, mas, se não, você pode escrever essa consulta para obter resultados equivalentes:

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

Isso fornecerá exatamente o mesmo conjunto de resultados e, como você já está codificando os valores em uma CASEinstrução de qualquer maneira, não estará perdendo nenhuma manutenção aqui.

jpmc26
fonte
1
Eu acho que você está perdendo o ponto - isso é gerado SQL a partir de uma base de código de back-end que funciona com enumerações por meio de suas representações de string. O código que está projetando o SQL está violando a consulta. Tenho certeza de que o solicitante, se ele estivesse escrevendo o próprio SQL, seria capaz de escrever uma consulta melhor. Portanto, não é bobagem ter uma CASEdeclaração, porque as ORMs fazem esse tipo de coisa. Que bobagem é que você não reconhecer essas facetas simples do problema ... (como é que para ser indiretamente chamado desmiolado?)
ErikE
@ ErikE Ainda meio bobo, já que você pode usar o valor numérico da enumeração, assumindo C # de qualquer maneira. (A suposição bastante segura, uma vez que estamos falando de SQL Server.)
jpmc26
Mas você não tem idéia do que é o caso de uso. Talvez seja uma grande mudança mudar para o valor numérico. Talvez as enums tenham sido adaptadas a uma base de código gigante existente. Criticar sem conhecimento é ridículo.
ErikE
@ErikE Se é ridículo, por que você está fazendo isso? =) Eu apenas respondi para salientar que, se o caso de uso for tão simples quanto o exemplo da pergunta (que está claramente especificado no prefácio da minha resposta), a CASEdeclaração poderá ser eliminada totalmente sem inconvenientes. Claro que pode haver fatores desconhecidos, mas eles não são especificados.
precisa saber é
Não tenho objeções às partes factuais da sua resposta, apenas às partes que subjetivamente caracterizam. Quanto a saber se eu estou criticando sem conhecimento, eu sou todo ouvidos para entender qualquer maneira em que eu não conseguiram usar a lógica escrupulosamente limpa ou fizeram suposições que são comprovadamente falsa ...
ErikE