MySQL не использует индекс при простом условии ИЛИ - PullRequest
2 голосов
/ 21 октября 2019

Я столкнулся с давней проблемой MySQL, отказывающейся использовать индекс для, казалось бы, базовых вещей. Запрос:

SELECT c.*
FROM app_comments c
LEFT JOIN app_comments reply_c ON c.reply_to = reply_c.id
WHERE (c.external_id = '840774' AND c.external_context = 'deals')
 OR (reply_c.external_id = '840774' AND reply_c.external_context = 'deals')
ORDER BY c.reply_to ASC, c.date ASC

ОБЪЯСНЕНИЕ:

id  select_type table   type    possible_keys   key key_len ref rows    Extra
1   SIMPLE  c   ALL external_context,external_id,idx_app_comments_externals NULL    NULL    NULL    903507  Using filesort
1   SIMPLE  reply_c eq_ref  PRIMARY PRIMARY 4   altero_full.c.reply_to  1   Using where

Есть индексы отдельно для external_id и external_context, и я также попытался добавить составной индекс (idx_app_comments_externals), но это совсем не помогло.

Запрос выполняется в производственном процессе за 4-6 секунд (> 1 м записей), но при удалении части ИЛИ условия WHERE уменьшается до 0,05 с (он все еще используетхотя сортировка файлов). Очевидно, что индексы здесь не работают, но я понятия не имею, почему. Кто-нибудь может объяснить это?

PS Мы используем MariaDB 10.3.18, может быть, здесь ошибка?

Ответы [ 2 ]

2 голосов
/ 21 октября 2019

MySQL (и MariaDB) не могут оптимизировать условия OR для разных столбцов или таблиц. Обратите внимание, что в контексте плана запроса c и reply_c считаются разными таблицами. Эти запросы обычно оптимизируются «вручную» с помощью операторов UNION, которые часто содержат много дублирования кода. Но в вашем случае и с совсем недавней версией, которая поддерживает CTE ( Common Table Expressions ), вы можете избежать большинства из них:

WITH p AS (
    SELECT *
    FROM app_comments
    WHERE external_id      = '840774'
      AND external_context = 'deals'
)
SELECT * FROM p
UNION DISTINCT
SELECT c.* FROM p JOIN app_comments c ON c.reply_to = p.id
ORDER BY reply_to ASC, date ASC

Хорошие показатели для этого запроса будут составнымиодин на (external_id, external_context) (в любом порядке) и отдельный на (reply_to).

Хотя вы не избежите "файловой сортировки", но это не должно быть проблемой, когда данные фильтруются вмаленький набор.

2 голосов
/ 21 октября 2019

С помощью предикатов равенства в столбцах external_id и external_context в предложении WHERE MySQL может эффективно использовать индекс ... когда эти предикаты указывают подмножество строк, которые могут удовлетворить запрос.

Но с добавлением OR к предложению WHERE теперь строки, возвращаемые из c, имеют , а не , ограниченные значениями external_id и external_content. Теперь возможно, что строки с другими значениями этих столбцов могут быть возвращены;строк с любыми значениями этих столбцов.

И это сводит на нет большое преимущество использования операции сканирования диапазона индекса ... очень быстро устраняя обширные полосы строк изсчитаетсяДа, сканирование диапазона индекса используется для быстрого поиска строк. Это правда. Но суть в том, что операция сканирования диапазона использует индекс для быстрого обхода миллионов и миллионов строк, которые невозможно вернуть.


Это не характерно для MariaDB 10.3. Мы будем наблюдать такое же поведение в MariaDB 10.2, MySQL 5.7, MySQL 5.6.


Я подвергаю сомнению операцию соединения: необходимо ли возвращать несколько копий строкс c при наличии нескольких совпадающих строк из reply_c? Или же спецификация просто возвращает отдельные строки из c?


Мы можем рассматривать требуемый набор результатов как две части.

1) строки из app_contents с равенствомПредикаты external_id и external_context

  SELECT c.*
    FROM app_comments c
   WHERE c.external_id       = '840774'
     AND c.external_context  = 'deals'
   ORDER
      BY c.external_id
       , c.external_context
       , c.reply_to
       , c.date

Для оптимальной производительности (исключая рассмотрение индекса покрытия из-за * в списке SELECT), такой индекс может использоваться для удовлетворения обоихоперация сканирования диапазона и порядок (исключая операцию использования файловой сортировки)

   ... ON app_comments (external_id, external_context, reply_to, date)

2) Вторая часть результата - это reply_to строк, связанных с соответствующими строками

  SELECT d.*
    FROM app_comments d
    JOIN app_comments e
      ON e.id = d.reply_to
   WHERE e.external_id       = '840774'
     AND e.external_context  = 'deals'
   ORDER
      BY d.reply_to
       , d.date

Тот же индекс, рекомендованный ранее, можно использовать для доступа к строкам в e (операция сканирования диапазона). В идеале этот индекс также должен включать столбец id. Нашим лучшим вариантом, вероятно, является изменение индекса для включения столбца id после date

   ... ON app_comments (external_id, external_context, reply_to, date, id)

Или, для эквивалентной производительности, за счет дополнительного индекса, мы могли бы определить индекс следующим образом:

   ... ON app_comments (external_id, external_context, id)

Для доступа к строкам из d со сканированием диапазона нам, вероятно, понадобится индекс:

   ... ON app_comments (reply_to, date)

Мы можем объединить два набора с UNION ALLоператор набора;но есть вероятность того, что одна и та же строка будет возвращена обоими запросами. Оператор UNION заставит уникальную сортировку удалить дублирующиеся строки. Или мы могли бы добавить условие ко второму запросу, чтобы исключить строки, которые будут возвращены первым запросом.

  SELECT d.*
    FROM app_comments d
    JOIN app_comments e
      ON e.id = d.reply_to
   WHERE e.external_id       = '840774'
     AND e.external_context  = 'deals'
  HAVING NOT ( d.external_id      <=> '840774'
           AND d.external_context <=> 'deals'
             )
   ORDER
      BY d.reply_to
       , d.date

Объединяя две части, оберните каждую часть в набор символов, добавьте UNIONВСЕ оператор set и оператор ORDER BY в конце (вне паренов), что-то вроде этого:

(
  SELECT c.*
    FROM app_comments c
   WHERE c.external_id       = '840774'
     AND c.external_context  = 'deals'
   ORDER
      BY c.external_id
       , c.external_context
       , c.reply_to
       , c.date
)
UNION ALL
(
  SELECT d.*
    FROM app_comments d
    JOIN app_comments e
      ON e.id = d.reply_to
   WHERE e.external_id       = '840774'
     AND e.external_context  = 'deals'
  HAVING NOT ( d.external_id      <=> '840774'
           AND d.external_context <=> 'deals'
             )
   ORDER
      BY d.reply_to
       , d.date
)
ORDER BY `reply_to`, `date`

Для этого потребуется операция «Использование файловой сортировки» над объединенным множеством, но теперь мы получилидействительно хороший способ получить хороший план выполнения для каждой части.


У меня все еще остается вопрос о том, сколько строк мы должны вернуть, если имеется несколько совпадающих строк response_to.

...