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();

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

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

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


Да, я использую 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любом случае уже жестко кодируете значения в выражении, вы не теряете здесь никакой возможности сопровождения.


1
Я думаю, что вы упускаете суть - это генерируемый SQL из серверной базы кода, которая работает с перечислениями через их строковые представления. Код, который проецирует SQL, применяет насилие к запросу. Я уверен, что спрашивающий, если бы он сам писал SQL, смог бы написать лучший запрос. Таким образом, вообще не глупо иметь CASEутверждение, потому что ORM делают подобные вещи. Что глупого в том, что вы не узнали этих простых аспектов проблемы ... (как это для того, чтобы косвенно называться безмозглым?)
ErikE

@ErikE Все еще немного глупо, поскольку вы можете просто использовать числовое значение перечисления, в любом случае предполагая C #. (Довольно безопасное предположение, учитывая, что мы говорим о SQL Server.)
jpmc26

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

@ErikE Если это смешно, то зачем ты это делаешь? =) Я только ответил, чтобы указать, что если сценарий использования такой же простой, как пример в вопросе (который четко указан в предисловии к моему ответу), CASEутверждение может быть полностью исключено без недостатка. Из конечно там могут быть неизвестные факторы, но они не определено.
jpmc26

Я не возражаю против фактических частей вашего ответа, только части, которые субъективно характеризуют. Что касается того, критикую ли я без знания, у меня есть все уши, чтобы понять любой способ, которым я не смог использовать скрупулезно чистую логику или сделал предположения, которые явно ложны ...
ErikE
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.