Лучший способ сделать SELECT с GROUP BY - PullRequest
2 голосов
/ 27 марта 2010

Привет, я написал запрос, который работает:

SELECT `comments`.* FROM `comments` 
RIGHT JOIN (SELECT MAX( id ) AS id, core_id, topic_id 
FROM comments GROUP BY core_id, topic_id order by id desc) comm 
ON comm.id = comments.id LIMIT 10

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

Спасибо

1 Ответ

5 голосов
/ 27 марта 2010

Способ 1 - улучшение исходного запроса

Я почти уверен, что в этом случае INNER JOIN будет достаточно , нет никаких оснований для RIGHT JOIN (если id существует в comm, он также будет существовать в comments). INNER JOIN s может привести к лучшей производительности .

Более того, вы действительно хотите вставить LIMIT 10 внутрь comm (кстати, оставив его вместе с ORDER BY):

  • за одну, , а не , если вместе хранить LIMIT 10 и ORDER BY, то не даст вам десять самых последних опубликованных тем (при заказе подзапроса comm не обязательно сохраняться в конечном результате, который вы LIMIT ing.)
  • Кроме того, применение LIMIT внутри самого внутреннего агрегированного подзапроса побудит оптимизаторов на основе затрат отдавать предпочтение вложенным циклам (точнее, 10) над хешем или Объединение объединяет (10 вложенных циклов являются самыми быстрыми для любого comments стола респектабельного размера.)

Итак, ваш запрос должен быть переписан как:

SELECT `comments`.* FROM `comments` 
INNER JOIN (
 SELECT MAX( id ) AS id, core_id, topic_id 
 FROM comments
 GROUP BY core_id, topic_id
 ORDER BY id DESC
 LIMIT 10
) comm 
ON comm.id = comments.id
ORDER BY comments.id

Наконец, используйте EXPLAIN, чтобы увидеть, что делает запрос. Не забудьте проверить, что вы создали индекс для comments.id, чтобы помочь с вложенными циклами JOIN.

Метод 2 - другой подход

Обратите внимание, что хотя приведенный выше запрос все еще может быть быстрее, чем ваш исходный запрос, самый внутренний подзапрос comm может все же оказаться существенным узким местом , если он приведет к полному сканированию таблицы comments. Это действительно зависит от того, насколько умна база данных, когда она видит GROUP BY, ORDER BY и LIMIT вместе.

Если EXPLAIN указывает, что подзапрос выполняет сканирование таблицы, тогда вы можете попробовать комбинацию SQL и логики уровня приложения , чтобы получить наилучшую производительность , предполагая, что вы поняли вашу правильно, и вы хотите определить десять последних комментариев, опубликованных в десяти различных темах :

# pseudo-code
core_topics_map = { }
command = "SELECT * FROM comments ORDER BY id DESC;"
command.execute
# iterate over the result set, betting that we will be able to break
#  early, bringing only a handful of rows over from the database server
while command.fetch_next_row do
  # have we identified our 10 most recent topics?
  if core_topics_map.size >= 10 then
    command.close
    break
  else
    core_topic_key = pair(command.field('core_id'), command.field('topic_id'))
    if not defined?(core_topics_map[core_topic_key]) then
      core_topics_map[core_topic_key] = command.field('id')
    end
  end
done
# sort our 10 topics in reverse chronological order
sort_by_values core_topics_map

В большинстве случаев (т. Е. При условии, что драйвер базы данных вашего приложения не пытается буферизовать все строки в памяти для вас перед возвратом управления из execute), приведенное выше будет получать только несколько строк, всегда используя индекс без сканирования таблицы.

Метод 3 - гибридный подход

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

Поэтому я могу переписать самый внутренний запрос, чтобы он был намного, гораздо более избирательным с использованием дополнительного условия, WHERE id >= :last_lowest_id:

SELECT `comments`.* FROM `comments` 
INNER JOIN (
 SELECT MAX( id ) AS id, core_id, topic_id 
 FROM comments
 WHERE id >= :last_lowest_id
 GROUP BY core_id, topic_id
 ORDER BY id DESC
 LIMIT 10
) comm 
ON comm.id = comments.id
ORDER BY comments.id

Когда вы запускаете запрос в первый раз, используйте 0 для :last_lowest_id. Запрос вернет до 10 строк в порядке убывания. Внутри вашего приложения отложите в сторону id последней строки и повторно используйте его значение как :last_lowest_id при следующем запуске запроса и повторите (опять же, отложите в сторону id последняя строка, возвращаемая последним запросом и т. д.) Это по существу сделает запрос инкрементным и чрезвычайно быстрым .

Пример:

  • первый запрос запускается с :last_lowest_id, установленным на 0
    • возвращает 10 строк с идентификаторами: 129, 100, 99, 88, 83, 79, 78, 75, 73, 70
    • сохранить 70
  • второй запрос выполняется с :last_lowest_id, установленным на 70
    • возвращает 10 строк с идентификаторами: 130, 129, 100, 99, 88, 83, 79, 78, 75, 73
    • сохранить 73
  • и т.д.

Метод 4 - еще один подход

Если вы планируете выполнять SELECT ... ORDER BY id DESC LIMIT 10 гораздо чаще, чем INSERT с, в таблицу comments, попробуйте добавить немного больше работы в INSERT, чтобы сделать SELECT быстрее. Таким образом, вы можете добавить индексированный столбец updated_at в свою таблицу topics и т. Д., И всякий раз, когда вы INSERT оставляете комментарий в таблицу comments, подумайте также об изменении значения updated_at соответствующей темы до NOW(). Затем вы можете легко выбрать 10 последних обновленных тем (простое и короткое сканирование индекса на updated_at, возвращающее 10 строк), внутреннее соединение с таблицей comments, чтобы получить MAX(id) для этих 10 тем (бесконечно более эффективно, чем получить MAX(id) для всех тем, прежде чем выбрать десять самых больших, как в оригинале и методе 1), затем снова выполнить внутреннее объединение на comments, чтобы получить остальные значения столбцов для этих 10.

Я ожидаю, что общая эффективность метода 4 будет сопоставима с методами 2 и 3. Метод 4 придется использовать, если вам нужно получить произвольные темы (например, разбив их на страницы, LIMIT 10 OFFSET 50) или если темы или комментарии могут быть удалено (не требуется никаких изменений для поддержки удаления темы; для правильной поддержки удаления комментариев необходимо обновить updated_at темы для обоих комментариев INSERT и DELETE со значением created_at последнего не удаленного комментария для темы .)

...