На прошлой неделе я написал в блоге о том, почему важно контролировать свой SQL , так как написание хорошего SQL помогает снизить эксплуатационные расходы. Это верно во многих отношениях, и сегодня мы рассмотрим другой способ написания хорошего, высокопроизводительного SQL: использование «метода поиска».
Медленный сдвиг
Чтобы понять метод поиска, давайте сначала разберемся, какую проблему он решает: предложения SQL OFFSET работают медленно . Они медленные по простой причине. Чтобы достичь высокого смещения из набора результатов, все предыдущие записи должны быть пропущены и подсчитаны. Хотя запрос без OFFSET
может быть очень быстрым (с использованием синтаксиса MySQL):
SELECT first_name, last_name, score FROM players WHERE game_id = 42 ORDER BY score DESC LIMIT 10;
Переход на страницу с номером 10’000 будет намного медленнее:
SELECT first_name, last_name, score FROM players WHERE game_id = 42 ORDER BY score DESC LIMIT 10 OFFSET 100000;
Даже если кортеж (game_id, score)
будет проиндексирован, нам придется проследить весь индекс, чтобы подсчитать, сколько записей мы уже пропустили. Хотя эта проблема может быть несколько уменьшена путем трюка , связанного players
с производной таблицей, существует альтернативный, гораздо более быстрый подход к решению подкачки: метод поиска.
Метод поиска
Хотя не совсем ясно, кто изначально изобрел метод поиска ( некоторые также называют его «подкачкой ключей» ), очень заметным сторонником этого является Маркус Винанд . Он описывает метод поиска в своем блоге (и в своей книге ):
http://use-the-index-luke.com/sql/partial-results/fetch-next-page
По сути, метод поиска не пропускает записи перед смещением, но пропускает записи до последней выбранной записи. Подумайте о поиске в Google. С точки зрения удобства использования вы вряд ли пропустите ровно 100 000 записей. В основном вы хотите перейти на следующую страницу, а затем снова на следующую страницу, то есть сразу после последней выбранной записи / результата поиска. Возьмите следующие 10 лучших игроков (поддельные имена, созданные с помощью генератора имен ):
first_name | last_name | score ------------------------------ Mary | Paige | 1098 Tracey | Howard | 1087 Jasmine | Butler | 1053 Zoe | Piper | 1002 Leonard | Peters | 983 Jonathan | Hart | 978 Adam | Morrison | 976 Amanda | Gibson | 967 Alison | Wright | 958 Jack | Harris | 949
Выше приведены первые 10 игроков, упорядоченных по счету. Это может быть достигнуто довольно быстро, используя LIMIT 10
только. Теперь, когда OFFSET 10
вы переходите на следующую страницу, вы можете либо просто использовать предложение, либо пропустить всех пользователей с баллом выше, чем 949
:
SELECT first_name, last_name, score FROM players WHERE game_id = 42 -- Let's call this the "seek predicate" AND score < 949 ORDER BY score DESC LIMIT 10;
Это даст вам игроков на следующей странице:
first_name | last_name | score ------------------------------ William | Fraser | 947 Claire | King | 945 Jessica | McDonald | 932 ... | ... | ...
Обратите внимание, что предыдущий запрос предполагает score
уникальность players
таблицы, что, конечно, маловероятно. Если бы Уильям Фрейзер также имел 949
очки, как и Джек Харрис, последний игрок на первой странице, он был бы «потерян между страницами». Поэтому важно создать однозначное предложение ORDER BY и «предикат поиска», добавив дополнительный уникальный столбец:
SELECT player_id, first_name, last_name, score FROM players WHERE game_id = 42 -- assuming 15 is Jack Harris's player_id AND (score, player_id) < (949, 15) ORDER BY score DESC, player_id DESC LIMIT 10;
Теперь «предикат поиска» зависит от ORDER BY
условия. Вот несколько возможных альтернативных конфигураций:
-- "consistent" ASC and DESC correspond to > and < AND (score, player_id) > (949, 15) ORDER BY score ASC, player_id ASC -- "mixed" ASC and DESC complicate things a bit AND ((score < 949) OR (score = 949 AND player_id > 15)) ORDER BY score DESC, player_id ASC -- The above might be further performance-tweaked AND (score <= 949) AND ((score < 949) OR (score = 949 AND player_id > 15)) ORDER BY score DESC, player_id ASC
Если столбцы в ORDER BY
предложении являются обнуляемым, NULLS FIRST
и NULLS LAST
может применяться и в дальнейшем усложнит «искать предикат».
Как это лучше, чем OFFSET?
Метод поиска позволяет избежать дорогостоящих операций «пропустить и сосчитать», заменив их простым сканированием диапазона по индексу, который может охватывать «предикат поиска». Так как вы в любом случае применяете ORDER BY к столбцам «предиката поиска», вы, возможно, уже решили соответствующим образом их проиндексировать .
Хотя метод поиска не улучшает запросы для небольших номеров страниц, выборка больших номеров страниц выполняется значительно быстрее, что доказано в этом замечательном тесте:
Воспроизводится с сайта use-the-index-luke.com с разрешения Маркуса Винанда.
Более интересные отзывы на эту тему можно найти в этой ветке reddit.com , в которую даже сам Том Кайт добавил пару замечаний .
Побочный эффект метода поиска
Побочным эффектом метода поиска является тот факт, что подкачка более «стабильна». Когда вы собираетесь отобразить страницу 2, и новый игрок тем временем достиг страницы 1, или если какой-либо игрок полностью удален, вы все равно будете отображать тех же игроков на странице 2. Другими словами, при использовании метода поиска , нет гарантии, что первый игрок на странице 2 имеет ранг 11.
Это может или не может быть желательным. Это может быть неуместно на странице 10’000, хотя.
Поддержка jOOQ 3.3 для метода поиска
В готовящемся выпуске jOOQ 3.3 ( выход которого запланирован на конец 2013 г.) будет включена поддержка метода поиска на уровне API SQL DSL. В дополнение к существующей поддержке LIMIT .. OFFSET в jOO, можно указать «предикат поиска» через синтетическое SEEK
предложение (аналогично синтетическому DIVIDE BY
предложению jOOQ ):
DSL.using(configuration) .select(PLAYERS.PLAYER_ID, PLAYERS.FIRST_NAME, PLAYERS.LAST_NAME, PLAYERS.SCORE) .from(PLAYERS) .where(PLAYERS.GAME_ID.eq(42)) .orderBy(PLAYERS.SCORE.desc(), PLAYERS.PLAYER_ID.asc()) .seek(949, 15) // (!) .limit(10) .fetch();
Вместо того чтобы явно формулировать «предикат поиска», просто передайте последнюю запись из предыдущего запроса, и jOOQ увидит, что все записи до и включая эту запись пропущены, учитывая ORDER BY
предложение.
Это выглядит намного более читабельным, чем реальный SQL, потому что «предикат поиска» ближе к ORDER BY
предложению, к которому он принадлежит. Кроме того, здесь применяется стандартная безопасность типов значений jOOQ , помогая вам найти правильную степень / арность и типы данных для вашего SEEK
предложения. В приведенном выше примере следующие вызовы методов не будут компилироваться в Java:
// Not enough arguments in seek() .orderBy(PLAYERS.SCORE.desc(), PLAYERS.PLAYER_ID.asc()) .seek(949) // Wrong argument types in seek() .orderBy(PLAYERS.SCORE.desc(), PLAYERS.PLAYER_ID.asc()) .seek(949, "abc")
Начало работы с методом поиска
Благодаря встроенной поддержке API для SEEK
предложения вы можете снова получить контроль над своим SQL и довольно легко реализовать высокопроизводительный SQL. Ранние пользователи уже могут поиграть с текущим состоянием jOOQ 3.3.0 Open Source Edition , которое доступно на GitHub .
И даже если вы не используете jOOQ, попробуйте метод поиска. Впоследствии у вас может быть гораздо более быстрое приложение!