Как сделать индекс использования запроса JOIN? - программирование

Как сделать индекс использования запроса JOIN?

У меня есть две таблицы:

CREATE TABLE `articles` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `title` varchar(1000) DEFAULT NULL,
  `last_updated` datetime DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `last_updated` (`last_updated`),
) ENGINE=InnoDB AUTO_INCREMENT=799681 DEFAULT CHARSET=utf8 

CREATE TABLE `article_categories` (
  `article_id` int(11) NOT NULL DEFAULT '0',
  `category_id` int(11) NOT NULL DEFAULT '0',
  PRIMARY KEY (`article_id`,`category_id`),
  KEY `category_id` (`category_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 |

Это мой запрос:

SELECT a.*
FROM
    articles AS a,
    article_categories AS c
WHERE
    a.id = c.article_id
    AND c.category_id = 78
    AND a.comment_cnt > 0
    AND a.deleted = 0
ORDER BY a.last_updated
LIMIT 100, 20

И EXPLAIN для него:

*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: a
         type: index
possible_keys: PRIMARY
          key: last_updated
      key_len: 9
          ref: NULL
         rows: 2040
        Extra: Using where
*************************** 2. row ***************************
           id: 1
  select_type: SIMPLE
        table: c
         type: eq_ref
possible_keys: PRIMARY,fandom_id
          key: PRIMARY
      key_len: 8
          ref: db.a.id,const
         rows: 1
        Extra: Using index

Он использует полное сканирование индекса last_updated в первой таблице для сортировки, но не использует индекс y для объединения (type: index в объяснении). Это очень плохо для производительности и убивает весь сервер базы данных, так как это очень частый запрос.

Я попытался изменить порядок таблиц с STRAIGHT_JOIN, но это дает filesort, using_temporary, что еще хуже.

Есть ли способ сделать mysql использовать индекс для соединения и для сортировки в одно и то же время?

=== update ===

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

4b9b3361

Ответ 1

Если у вас много категорий, этот запрос не может быть эффективным. Ни один индекс не может охватывать сразу две таблицы в MySQL.

Вы должны выполнить денормализацию: добавьте last_updated, has_comments и deleted в article_categories:

CREATE TABLE `article_categories` (
  `article_id` int(11) NOT NULL DEFAULT '0',
  `category_id` int(11) NOT NULL DEFAULT '0',
  `last_updated` timestamp NOT NULL,
  `has_comments` boolean NOT NULL,
  `deleted` boolean NOT NULL,
  PRIMARY KEY (`article_id`,`category_id`),
  KEY `category_id` (`category_id`),
  KEY `ix_articlecategories_category_comments_deleted_updated` (category_id, has_comments, deleted, last_updated)
) ENGINE=InnoDB DEFAULT CHARSET=utf8

и запустите этот запрос:

SELECT  *
FROM    (
        SELECT  article_id
        FROM    article_categories
        WHERE   (category_id, has_comments, deleted) = (78, 1, 0)
        ORDER BY
                last_updated DESC
        LIMIT   100, 20
        ) q
JOIN    articles a
ON      a.id = q.article_id

Конечно, вы должны обновлять article_categories, а также всякий раз, когда вы обновляете соответствующие столбцы в article. Это можно сделать в триггере.

Обратите внимание, что столбец has_comments является логическим: это позволит использовать предикат равенства для сканирования одного диапазона по индексу.

Также обратите внимание, что LIMIT переходит в подзапрос. Это делает MySQL использовать поиск в конце строки, который он не использует по умолчанию. Посмотрите эту статью в своем блоге о том, почему они повышают производительность:

Если вы были на SQL Server, вы можете сделать индексируемое представление по вашему запросу, что по существу сделало бы денормализованную индексированную копию article_categories с дополнительными полями, автоматически поддерживаемыми сервером.

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

Ответ 2

Прежде чем перейти к конкретному запросу, важно понять, как работает индекс.

При соответствующей статистике этот запрос:

select * from foo where bar = 'bar'

... будет использовать индекс на foo(bar), если он будет избирательным. Это означает, что если bar = 'bar' составляет выбор большинства строк таблицы, это будет быстрее, чтобы просто прочитать таблицу и устранить строки, которые не применяются. Напротив, если bar = 'bar' означает только выбор нескольких строк, чтение индекса имеет смысл.

Предположим, что теперь мы бросаем в предложение order и у вас есть индексы на каждом из foo(bar) и foo(baz):

select * from foo where bar = 'bar' order by baz

Если bar = 'bar' является очень избирательным, он дешево захватывает все строки, которые соответствуют, и сортирует их в памяти. Если это вообще не выборочно, индекс на foo(baz) имеет мало смысла, потому что вы все равно получите всю таблицу: использование этого означало бы движение вперед и назад на дисковых страницах для чтения строк по порядку, что очень дорого.

Отбросить в ограничительном разделе, и foo(baz) может внезапно иметь смысл:

select * from foo where bar = 'bar' order by baz limit 10

Если bar = 'bar' является очень избирательным, он по-прежнему является хорошим вариантом. Если это вообще не выборочно, вы быстро найдете 10 подходящих строк, сканируя индекс на foo(baz) - вы можете прочитать 10 строк или 50, но вы скоро найдете 10 хороших.

Предположим, что последний запрос с индексами на foo(bar, baz) и foo(baz, bar). Индексы читаются слева направо. Один из них имеет очень хороший смысл для этого потенциального запроса, другой может вообще ничего не делать. Думайте о них так:

bar   baz    baz   bar
---------    ---------
bad   aaa    aaa   bad
bad   bbb    aaa   bar
bar   aaa    bbb   bad
bar   bbb    bbb   bar

Как вы можете видеть, индекс на foo(bar, baz) позволяет начать чтение с ('bar', 'aaa') и выборка строк по порядку с этой точки вперед.

Индекс на foo(baz, bar), напротив, дает строки, отсортированные по baz, независимо от того, что может быть bar. Если bar = 'bar' не является выборочным в качестве критерия, вы быстро столкнетесь с соответствующими строками для своего запроса, и в этом случае имеет смысл его использовать. Если он очень избирательный, вы можете закончить повторение строк строк до того, как найдете достаточно, чтобы соответствовать bar = 'bar' - он все равно может быть хорошим вариантом, но он оптимальным.

С учетом этого, вернемся к исходному запросу...

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

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

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

Без фильтра вашей категории параметры индекса достаточно очевидны: articles(last_updated); возможно, с столбцом счетчика справа, а удаленный флаг - влево.

С фильтром вашей категории все зависит от...

Если фильтр категории очень селективен, на самом деле имеет смысл выбрать все строки, находящиеся в этой категории, отсортировать их в памяти и выбрать верхние совпадающие строки.

Если ваш фильтр категории не является выборочным и дает почти статью, индекс в articles(last_update) имеет смысл: допустимые строки по всему месту, поэтому читайте строки по порядку, пока не найдете достаточно, чтобы соответствовать и voilà.

В более общем случае это просто смутно избирательно. Насколько мне известно, собранные статистические данные не слишком много смотрят на корреляции. Таким образом, у планировщика нет хорошего способа оценить, найдет ли он статьи с подходящей категорией достаточно быстро, чтобы прочитать последний индекс. Соединение и сортировка в памяти обычно будут дешевле, поэтому планировщик пойдет с этим.

Во всяком случае, у вас есть два варианта принудительного использования индекса.

Один из них - признать, что планировщик запросов не является идеальным и использовать подсказку:

http://dev.mysql.com/doc/refman/5.5/en/index-hints.html

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

Изменить: STRAIGHT_JOIN, как указано в работе DRap, с аналогичными предостережениями.

Другой заключается в том, чтобы сохранить дополнительный столбец для тегов часто выбранных статей (например, поле tinyint, которое установлено в 1, когда оно относится к вашей конкретной категории), а затем добавить индекс, например. articles(cat_78, last_updated). Поддерживайте его с помощью триггера, и все будет хорошо.

Ответ 3

Использование индекса без покрытия дорого. Для каждой строки любые непокрытые столбцы должны быть извлечены из базовой таблицы, используя первичный ключ. Поэтому я сначала попытался сделать индекс на articles покрытии. Это может помочь убедить оптимизатор запросов MySQL, что индекс полезен. Например:

KEY IX_Articles_last_updated (last_updated, id, title, comment_cnt, deleted),

Если это не помогает, вы можете играть с FORCE INDEX:

SELECT  a.*
FROM    article_categories AS c FORCE INDEX (IX_Articles_last_updated)
JOIN    articles AS a FORCE INDEX (PRIMARY)
ON      a.id = c.article_id
WHERE   c.category_id = 78
        AND a.comment_cnt > 0
        AND a.deleted = 0
ORDER BY 
        a.last_updated
LIMIT   100, 20

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

Ответ 4

Вы можете использовать влияние MySQL для использования KEYS или INDEXES

Для

  • Заказ или
  • Группирование, или
  • Регистрация

За дополнительной информацией следуйте этой ссылке. Я хотел использовать это для соединения (т.е. USE INDEX FOR JOIN (My_Index), но это не сработало, как ожидалось. Удаление части FOR JOIN значительно ускорило мой запрос - от более чем 3,5 часов до 1-2 секунд. Просто потому, что MySQL был принудительно для использования правильного индекса.

Ответ 5

Я бы имел следующие индексы:

таблица статей - INDEX (удаленный, last_updated, comment_cnt)

article_categories таблица - INDEX (article_id, category_id) - у вас уже есть этот индекс

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

SELECT STRAIGHT_JOIN
      a.*
   FROM
      articles AS a
         JOIN article_categories AS c
            ON a.id = c.article_id
            AND c.category_id = 78
   WHERE
          a.deleted = 0
      AND a.comment_cnt > 0
   ORDER BY 
      a.last_updated
   LIMIT 
      100, 20

В соответствии с комментарием/обратной связью, я бы рассмотрел реверсирование на основе набора, если записи категорий намного меньше оснований... таких как

SELECT STRAIGHT_JOIN
      a.*
   FROM
      article_categories AS c
         JOIN articles as a
            ON c.article_id = a.id
           AND a.deleted = 0
           AND a.Comment_cnt > 0
   WHERE
      c.category_id = 78
   ORDER BY 
      a.last_updated
   LIMIT 
      100, 20

В этом случае я бы обеспечил индекс в таблице статей

index - (id, deleted, last_updated)

Ответ 6

Прежде всего, я бы рекомендовал прочитать статью 3 способа использования индексов MySQL.

И теперь, когда вы знаете основы, вы можете оптимизировать этот конкретный запрос.

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

Еще одна важная вещь: перед заказом вы получаете все столбцы для всех фильтрованных строк из таблицы a, а затем пропускаете, вероятно, большинство из них. Гораздо эффективнее получить список требуемых идентификаторов строк и извлечь только те строки.

Для выполнения этой работы вам понадобится индекс покрытия (deleted, comment_cnt, last_updated) в таблице a, и теперь вы можете переписать запрос следующим образом:

SELECT *
FROM (
  SELECT a.id
  FROM articles AS a,
  JOIN article_categories AS c
    ON a.id = c.article_id AND c.category_id = 78
  WHERE a.comment_cnt > 0 AND a.deleted = 0
  ORDER BY a.last_updated
  LIMIT 100, 20
) as ids
JOIN articles USING (id);

P.S. Определение таблицы для таблицы a не содержит столбца comment_cnt;)