Sql Server не может использовать индекс при простом бижекции

11

Это еще одна головоломка оптимизатора запросов.

Может быть, я просто переоцениваю оптимизаторы запросов, или, может быть, я что-то упустил - поэтому я выкладываю это туда.

У меня простой стол

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

с индексом и несколькими тысячами строк, Numberравномерно распределенными в значениях 0, 1 и 2.

Теперь этот запрос:

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;

Индекс ищет, IX_Numberкак и следовало ожидать.

Если пункт где

WHERE P.Name = 'one';

однако, это становится сканированием.

Кейс-кейс, очевидно, является биекцией, поэтому в теории должна быть возможность оптимизации, чтобы вычесть первый план запроса из второго запроса.

Это также не чисто академический: запрос основан на переводе значений enum в их дружественные имена.

Я хотел бы услышать от кого-то, кто знает, чего можно ожидать от оптимизаторов запросов (и в частности от Sql Server): я просто слишком многого ожидаю?

Я спрашиваю, как у меня были случаи, когда некоторые небольшие изменения в запросе внезапно выявляли оптимизацию.

Я использую Sql Server 2016 Developer Edition.

Джон
источник

Ответы:

18

Я просто ожидаю слишком многого?

Да. По крайней мере, в текущих версиях продукта.

SQL Server не будет разбирать CASEоператор и перепроектировать его, чтобы обнаружить, что если результат вычисляемого столбца будет, 'one'то [Extent1].[Number]должен быть 0.

Вы должны убедиться, что вы пишете свои предикаты, чтобы быть саркастичным. Который почти всегда вовлекает это быть в форме. basetable_column_name comparison_operator expression,

Даже незначительные отклонения нарушают проходимость.

WHERE P.Number + 0 = 0;

также не будет использовать поиск по индексу, даже если его проще упростить, чем CASEвыражение.

Если вы хотите выполнить поиск по имени строки и получить искомый номер, вам понадобится таблица сопоставления с именами и номерами и присоединиться к ней в запросе, тогда план может иметь поиск в таблице сопоставления с последующим коррелированным поиском. на [dbo].[MyEntities]с номером вернулся из первого искать.

Мартин Смит
источник
6

Не проектируйте свое перечисление как заявление случая. Спроектируйте его как производную таблицу следующим образом:

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

Я подозреваю, что вы получите лучшие результаты. (Я не преобразовывал имя в ?пропущенное, потому что это, вероятно, помешало бы возможному повышению производительности. Однако вы можете переместить WHEREпредложение внутри внешнего запроса, чтобы поместить предикат в enumтаблицу, или вы можете вернуть два столбца из внутренний запрос, один для предиката и один для отображения, где предикат - это NULLкогда нет подходящего значения перечисления.)

Я предполагаю, однако, что из-за этого [Extent1]вы используете ORM, такой как Entity Framework или Linq-To-SQL. Я не могу подсказать вам, как выполнить такую ​​проекцию изначально, но вы могли бы использовать другую технику.

В одном из моих проектов я отражал значения перечисления кода в реальных таблицах в базе данных с помощью специального класса сборки, который объединял значения перечисления в базе данных. (Вы должны соблюдать правило, согласно которому вы должны явно перечислить значения перечисления, никогда не можете удалять их, не просматривая таблицы, и никогда не сможете их изменить, хотя вам уже нужно соблюдать хотя бы некоторые из них при текущей настройке) ,

Теперь я использовал перечислимый Identifierбазовый класс, который имеет много разных конкретных подклассов, но нет никаких причин, по которым это невозможно сделать с простым перечислением vanilla. Вот пример использования:

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

Вы можете видеть, что я передал всю необходимую информацию для записи и чтения значений базы данных. (У меня была ситуация, когда текущий запрос может содержать не все существующие значения, поэтому необходимо было возвращать любые дополнительные данные из базы данных, а также из загруженного в данный момент набора. Я также позволил базе данных назначать идентификаторы, хотя для перечисления вы, вероятно, не хочу это.)

Идея состоит в том, что если у вас есть таблица, которая при запуске читается / записывается только один раз, и в которой будут надежно указаны все значения перечисления, вы просто присоединяетесь к ней, как и любая другая таблица, и производительность должна быть хорошей.

Я надеюсь, что этих идей достаточно для улучшения.

ErikE
источник
Да, я использую EntityFramework и там, где решение действительно должно быть в оптимальном мире. До того, как это произойдет, ваше предложение является одним из лучших обходных путей, я считаю.
Джон
5

Я интерпретирую вопрос как то, что вы заинтересованы в оптимизаторах в целом, но с особым интересом к SQL Server. Я проверил ваш сценарий с 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"

Оптимизатор в DB2 переписывает второй запрос на первый:

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)

План выглядит так:

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

Я не знаю много о других оптимизаторах, но у меня такое ощущение, что оптимизатор DB2 считается довольно хорошим даже среди конкурентов.

Леннарт
источник
Это захватывающе. Можете ли вы пролить свет на то, откуда происходит «оптимизированное утверждение»? Сам db2 возвращает это вам? - Кроме того, у меня проблемы с чтением плана. Я так понимаю "IXSCAN" не означает сканирование индекса в этом случае?
Джон
1
Вы можете сказать DB2, чтобы объяснить для вас заявление. Собранная информация хранится в виде набора таблиц, и вы можете использовать визуальное объяснение или, как в этом случае, утилиту db2exfmt (или создать свою собственную утилиту). Кроме того, вы можете отслеживать выписку и сравнивать предполагаемое количество элементов в плане с фактическим планом. В этом плане мы можем видеть, что это действительно индексное сканирование (IXSCAN), и предполагаемый результат этого оператора составляет 3334 строки. Это плохо в SQL-сервере? Он знает ключ запуска и ключ остановки, поэтому он сканирует только соответствующие строки в DB2.
Леннарт
Таким образом, то, что он называет сканированием, включает поиск, и, честно говоря, эквивалентные объяснения плана Sql Server также иногда называют чем-то сканированием, которое включает поиск, а в других случаях это называется поиском. Мне всегда нужно смотреть на количество строк, чтобы понять, что к чему. Поскольку в выводе db2 явно есть 3334, он, безусловно, делает то, на что я надеялся. Очень интересно.
Джон
Да, я тоже иногда путаюсь. Нужно взглянуть на более подробную информацию для каждого оператора, чтобы действительно понять, что происходит.
Леннарт
0

В этом конкретном запросе даже глупо иметь CASEзаявление. Вы переходите к одному конкретному случаю! Возможно, это просто деталь конкретного примера запроса, который вы дали, но если нет, вы можете написать этот запрос, чтобы получить эквивалентные результаты:

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

Это даст вам точно такой же набор результатов, и, поскольку вы в CASEлюбом случае уже жестко кодируете значения в выражении, вы не теряете здесь никакой возможности сопровождения.

jpmc26
источник
1
Я думаю, что вы упускаете суть - это генерируемый SQL из серверной базы кода, которая работает с перечислениями через их строковые представления. Код, который проецирует SQL, применяет насилие к запросу. Я уверен, что спрашивающий, если бы он сам писал SQL, смог бы написать лучший запрос. Таким образом, вообще не глупо иметь CASEутверждение, потому что ORM делают подобные вещи. Что глупого в том, что вы не узнали этих простых аспектов проблемы ... (как это для того, чтобы косвенно называться безмозглым?)
ErikE
@ErikE Все еще немного глупо, поскольку вы можете просто использовать числовое значение перечисления, в любом случае предполагая C #. (Довольно безопасное предположение, учитывая, что мы говорим о SQL Server.)
jpmc26
Но вы понятия не имеете, что такое сценарий использования. Возможно, было бы огромным изменением перейти на числовое значение. Возможно, перечисления были преобразованы в существующую гигантскую кодовую базу. Критика без знания - это смешно.
ErikE
@ErikE Если это смешно, то зачем ты это делаешь? =) Я только ответил, чтобы указать, что если сценарий использования такой же простой, как пример в вопросе (который четко указан в предисловии к моему ответу), CASEутверждение может быть полностью исключено без недостатка. Из конечно там могут быть неизвестные факторы, но они не определено.
jpmc26
Я не возражаю против фактических частей вашего ответа, только части, которые субъективно характеризуют. Что касается того, критикую ли я без знания, у меня есть все уши, чтобы понять любой способ, которым я не смог использовать скрупулезно чистую логику или сделал предположения, которые явно ложны ...
ErikE