Оптимизировать запрос с помощью OFFSET на большой таблице

У меня есть стол

create table big_table (
id serial primary key,
-- other columns here
vote int
); 

Эта таблица очень большая, примерно 70 миллионов строк, мне нужно запросить:

SELECT * FROM big_table
ORDER BY vote [ASC|DESC], id [ASC|DESC]
OFFSET x LIMIT n  -- I need this for pagination

Как вы, возможно, знаете, когда x - большое число, такие запросы выполняются очень медленно.

Для оптимизации производительности добавил индексы:

create index vote_order_asc on big_table (vote asc, id asc);

а также

create index vote_order_desc on big_table (vote desc, id desc);

EXPLAIN показывает, что указанный выше запрос SELECT использует эти индексы, но в любом случае он очень медленный с большим смещением.

Что я могу сделать для оптимизации запросов с OFFSET в больших таблицах? Может быть, в PostgreSQL 9.5 или даже более новых версиях есть какие-то особенности? Я искал, но ничего не нашел.


person Oto Shavadze    schedule 05.12.2015    source источник
comment
почему бы не пагинация набора клавиш? blog.jooq .org / 2013/10/26 /   -  person Neil McGuigan    schedule 06.12.2015
comment
@NeilMcGuigan У этого метода есть некоторые недостатки, например, только предыдущая и следующая страницы, но, похоже, я был вынужден выбрать этот способ   -  person Oto Shavadze    schedule 06.12.2015


Ответы (2)


Большой OFFSET всегда будет медленным. Postgres должен упорядочить все строки и подсчитать видимые строки до вашего смещения. Чтобы пропустить все предыдущие строки напрямую, вы можете добавить индексированный row_number в таблицу (или создать _ 3_, включая указанный row_number) и работать с WHERE row_number > x вместо OFFSET x.

Однако этот подход имеет смысл только для данных, предназначенных только для чтения (или в основном). Реализовать то же самое для табличных данных, которые могут изменяться одновременно, является более сложной задачей. Вам необходимо начать с определения желаемого поведения точно.

Я предлагаю другой подход для разбивки на страницы:

SELECT *
FROM   big_table
WHERE  (vote, id) > (vote_x, id_x)  -- ROW values
ORDER  BY vote, id  -- needs to be deterministic
LIMIT  n;

Где vote_x и id_x - из последней строки предыдущей страницы (как для DESC, так и для ASC). Или с первого при навигации назад.

Сравнение значений строк поддерживается уже имеющимся у вас индексом - функцией, которая соответствует стандарту ISO SQL, но не каждая СУБД поддерживает ее.

CREATE INDEX vote_order_asc ON big_table (vote, id);

Или по убыванию:

SELECT *
FROM   big_table
WHERE  (vote, id) < (vote_x, id_x)  -- ROW values
ORDER  BY vote DESC, id DESC
LIMIT  n;

Можно использовать тот же индекс.
Я предлагаю вам объявить свои столбцы NOT NULL или познакомиться с конструкцией NULLS FIRST|LAST:

В частности, обратите внимание на две вещи:

  1. Значения ROW в предложении WHERE нельзя заменить разделенными полями-членами. WHERE (vote, id) > (vote_x, id_x) нельзя заменить на:

    WHERE  vote >= vote_x
    AND    id   > id_x

    Это исключит все строки с id <= id_x, в то время как мы хотим сделать это только для того же голосования, а не для следующего. Правильный перевод будет:

    WHERE (vote = vote_x AND id > id_x) OR vote > vote_x
    

    ... который не так хорошо работает с индексами и становится все более сложным для большего количества столбцов.

    Очевидно, было бы просто для одного столбца. Это особый случай, о котором я упоминал в самом начале.

  2. Этот метод не работает для смешанных направлений в ORDER BY, например:

    ORDER  BY vote ASC, id DESC
    

    По крайней мере, я не могу придумать общий способ реализовать это так же эффективно. Если хотя бы один из обоих столбцов является числовым типом, вы можете использовать функциональный индекс с инвертированным значением на (vote, (id * -1)) - и использовать то же выражение в ORDER BY:

    ORDER  BY vote ASC, (id * -1) ASC
    

Связанный:

В частности, обратите внимание на презентацию Маркуса Винанда, которую я связал с:

person Erwin Brandstetter    schedule 15.12.2015

Вы пробовали разделить таблицу на части?

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

http://sqlperformance.com/2013/09/sql-indexes/partitioning-benefits

person thepiyush13    schedule 05.12.2015
comment
Это для SQL Server, а не для Postgres - person a_horse_with_no_name; 06.12.2015
comment
PostgreSQL поддерживает базовое разделение таблиц: postgresql.org/docs/current/interactive/ ddl-partitioning.html - person thepiyush13; 06.12.2015