Улучшение производительности OFFSET в PostgreSQL - PullRequest
36 голосов
/ 08 июля 2011

У меня есть таблица, в которой я выполняю ORDER BY перед LIMIT и OFFSET для разбивки на страницы.

Добавление индекса для столбца ORDER BY существенно влияет на производительность (при использовании в комбинациис небольшим ПРЕДЕЛОМ).В таблице строк на 500 000 я увидел улучшение в 10 000 раз при добавлении индекса, если был небольшой предел.

Однако индекс не влияет на большие смещения (то есть на более поздних страницах в моей пагинации).Это понятно: индекс b-дерева упрощает итерацию по порядку с самого начала, но не позволяет найти n-й элемент.

Кажется, что это поможет * подсчитанный индекс b-дерева , но я не в курсе их поддержки в PostgreSQL.Есть ли другое решение?Кажется, что оптимизация для больших OFFSET (особенно в случаях использования нумерации страниц) не так уж необычна.

К сожалению, руководство PostgreSQL просто говорит: «Строки, пропущенные предложением OFFSET, все еще должны быть вычислены внутри серверапоэтому большое смещение может быть неэффективным. "

Ответы [ 5 ]

36 голосов
/ 08 июля 2011

Возможно, вы захотите вычислить индекс.

Давайте создадим таблицу:

create table sales(day date, amount real);

И заполните его случайным материалом:

insert into sales 
    select current_date + s.a as day, random()*100 as amount
    from generate_series(1,20);

Индексируйте это днем, ничего особенного здесь:

create index sales_by_day on sales(day);

Создать функцию позиционирования строки. Есть и другие подходы, этот самый простой:

create or replace function sales_pos (date) returns bigint 
   as 'select count(day) from sales where day <= $1;' 
   language sql immutable;

Проверьте, работает ли он (но не называйте его так для больших наборов данных):

select sales_pos(day), day, amount from sales;

     sales_pos |    day     |  amount  
    -----------+------------+----------
             1 | 2011-07-08 |  41.6135
             2 | 2011-07-09 |  19.0663
             3 | 2011-07-10 |  12.3715
    ..................

Теперь сложная часть: добавьте еще один индекс, рассчитанный по значениям функции sales_pos:

create index sales_by_pos on sales using btree(sales_pos(day));

Вот как вы это используете. 5 - это ваше «смещение», 10 - это «предел»:

select * from sales where sales_pos(day) >= 5 and sales_pos(day) < 5+10;

        day     | amount  
    ------------+---------
     2011-07-12 | 94.3042
     2011-07-13 | 12.9532
     2011-07-14 | 74.7261
    ...............

Это быстро, потому что когда вы называете это так, Postgres использует предварительно рассчитанные значения из индекса:

explain select * from sales 
  where sales_pos(day) >= 5 and sales_pos(day) < 5+10;

                                    QUERY PLAN                                
    --------------------------------------------------------------------------
     Index Scan using sales_by_pos on sales  (cost=0.50..8.77 rows=1 width=8)
       Index Cond: ((sales_pos(day) >= 5) AND (sales_pos(day) < 15))

Надеюсь, это поможет.

3 голосов
/ 08 июля 2011

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

SELECT *
FROM massive_table
WHERE id IN (
    SELECT id
    FROM massive_table
    WHERE ...
    LIMIT 50
    OFFSET 500000
);

Преимущество здесь в том, что, хотя он все еще должен рассчитать правильное упорядочение всего, он не упорядочивает всю строку-только столбец id .

2 голосов
/ 08 июля 2011

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

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

Но все равно.,.

Поскольку код вашего приложения знает, какие упорядоченные значения уже видны, он должен иметь возможность уменьшить набор результатов и уменьшить смещение, исключив эти значения в предложении WHERE.Предполагая, что вы заказываете один столбец и он сортируется по возрастанию, код вашего приложения может сохранить последнее значение на странице, а затем добавить AND your-ordered-column-name > last-value-seen к предложению WHERE некоторым подходящим способом.

1 голос
/ 16 ноября 2018

Вместо использования OFFSET очень эффективным приемом является использование временной таблицы:

CREATE  TEMPORARY TABLE just_index AS
SELECT ROW_NUMBER() OVER (ORDER BY myID), myID
FROM mytable;

Для 10 000 000 строк требуется около 10 с.Затем вы хотите использовать SELECT или UPDATE вашей таблицы, просто:

SELECT * FROM mytable INNER JOIN (SELECT just_index.myId FROM just_index WHERE row_number >= *your offset* LIMIT 1000000) indexes ON mytable.myID = indexes.myID

Фильтрация mytable только с just_index более эффективна (в моем случае) с INNER JOIN, чем с WHERE myID IN (SELECT ...)

Таким образом, вам не нужно сохранять последнее значение myId, вы просто заменяете смещение предложением WHERE, в котором используются индексы

1 голос
/ 01 ноября 2013

недавно я работал над такой проблемой и написал блог о том, как решить эту проблему.очень похоже, я надеюсь быть полезным для любого.я использую ленивый подход списка с частичным adquisition.я заменил предел и смещение или нумерацию запроса на нумерацию страниц вручную.В моем примере select возвращает 10 миллионов записей, я получаю их и вставляю в «временную таблицу»:

create or replace function load_records ()
returns VOID as $$
BEGIN
drop sequence if exists temp_seq;
create temp sequence temp_seq;
insert into tmp_table
SELECT linea.*
FROM
(
select nextval('temp_seq') as ROWNUM,* from table1 t1
 join table2 t2 on (t2.fieldpk = t1.fieldpk)
 join table3 t3 on (t3.fieldpk = t2.fieldpk)
) linea;
END;
$$ language plpgsql;

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

select * from tmp_table where counterrow >= 9000000 and counterrow <= 9025000

С точки зрения Java, я реализовал эту нумерацию страниц посредством частичной рекламы с ленивым списком.это список, который выходит из списка Abstract и реализует метод get ().Метод get может использовать интерфейс доступа к данным для продолжения получения следующего набора данных и освобождения кучи памяти:

@Override
public E get(int index) {
  if (bufferParcial.size() <= (index - lastIndexRoulette))
  {
    lastIndexRoulette = index;
    bufferParcial.removeAll(bufferParcial);
    bufferParcial = new ArrayList<E>();
        bufferParcial.addAll(daoInterface.getBufferParcial());
    if (bufferParcial.isEmpty())
    {
        return null;
    }

  }
  return bufferParcial.get(index - lastIndexRoulette);<br>
}

, с другой стороны, интерфейс доступа к данным использует запрос для разбивки на страницы и реализует один метод для последовательной итерациикаждые 25000 записей для завершения всего этого.

результаты для этого подхода можно увидеть здесь http://www.arquitecturaysoftware.co/2013/10/laboratorio-1-iterar-millones-de.html

...