Сервер Sql не вдається використовувати індекс для простого біекціонування


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): Я просто очікую занадто багато?

Я запитую, як у мене були випадки, коли деякі незначні зміни запиту зробили б оптимізацію раптом.

Я використовую Sql Server 2016 для розробників.

Відповіді:


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. Я не можу орієнтувати вас, як реально виконати таку проекцію, але ви можете використовувати іншу техніку.

В одному з моїх проектів я відображав значення перерахунку коду в реальних таблицях бази даних, через клас побудови на замовлення, який об'єднав значення enum у базу даних. (Ви повинні дотримуватися правила, згідно з яким ви повинні чітко перелічити свої значення enum. Ніколи не можна видалити жодну, не переглядаючи ваші таблиці, і ніколи, ніколи не змінювати їх, хоча вам вже доводиться дотримуватися принаймні дещо з цього при вашому поточному налаштуванні) .

Тепер я використовував безліч Identifierбазового класу, який містить багато різних конкретних підкласів, але немає причин, щоб це не було можливо зробити з простою ванільною перерахункою. Ось приклад використання:

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 Досі нерозумно, оскільки ти можеш просто використовувати числове значення перерахунку, припускаючи C # у будь-якому випадку. (Досить безпечне припущення, враховуючи, що ми говоримо про SQL Server.)
jpmc26

Але ви не маєте поняття, що таке випадок використання. Можливо, це було б величезною зміною для переходу на числове значення. Можливо, перерахунки були модернізовані в існуючу базу кодів-гігантів. Критикувати без знань смішно.
ErikE

@ErikE Якщо це смішно, то чому ти це робиш? =) Я лише відповів, щоб зазначити, що якщо випадок використання настільки простий, як приклад у запитанні (який чітко вказаний у передмові моєї відповіді), то CASEтвердження можна усунути повністю без недоліку. З звичайно там можуть бути невідомі фактори, але вони не визначено.
jpmc26

Я не заперечую проти фактичних частин вашої відповіді, лише частин, які суб'єктивно характеризують. Щодо того, чи я критикую без знань, я на всі вуха розумію будь-який спосіб, в якому я не встиг використати скрупульозно чисту логіку або зробив припущення, які явно неправдиві ...
ErikE
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.