Способ 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
последнего не удаленного комментария для темы .)