Как хранить и запрашивать версию одного и того же документа в PostgreSQL?

Я храню версии документа в PostgreSQL 9.4. Каждый раз, когда пользователь создает новую версию, он вставляет строку, чтобы я мог отслеживать все изменения с течением времени. Каждая строка разделяет столбец reference_id с предыдущими строками. Некоторые строки утверждаются, а некоторые остаются черновиками. Каждая строка также имеет viewable_at время.

id | reference_id | approved | viewable_at         | created_on | content
1  | 1            | true     | 2015-07-15 00:00:00 | 2015-07-13 | Hello
2  | 1            | true     | 2015-07-15 11:00:00 | 2015-07-14 | Guten Tag
3  | 1            | false    | 2015-07-15 17:00:00 | 2015-07-15 | Grüß Gott

Наиболее частый запрос — получить строки, сгруппированные по reference_id, где approved равно true, а viewable_at меньше текущего времени. (В этом случае в результаты будет включена строка с идентификатором 2)

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

SELECT DISTINCT ON (reference_id) reference_id, id, approved, viewable_at, content 
FROM documents 
WHERE approved = true AND viewable_at <= '2015-07-15 13:00:00' 
ORDER BY reference_id, created_at DESC`

У меня есть индекс для reference_id и многоколоночный индекс для утвержденных и viewable_at.

Всего 15 000 строк, на моем локальном компьютере это все еще составляет в среднем несколько сотен миллисекунд (140–200). Я подозреваю, что отчетливый звонок или заказ могут замедлять его.

Как наиболее эффективно хранить эту информацию, чтобы запросы SELECT были наиболее производительными?

Результат EXPLAIN (БУФЕРЫ, АНАЛИЗ):

                                                              QUERY PLAN                                                                
-----------------------------------------------------------------------------------------------------------------------------------------
Unique  (cost=6668.86..6730.36 rows=144 width=541) (actual time=89.862..99.613 rows=145 loops=1)
  Buffers: shared hit=2651, temp read=938 written=938
  ->  Sort  (cost=6668.86..6699.61 rows=12300 width=541) (actual time=89.861..97.796 rows=13184 loops=1)
        Sort Key: reference_id, created_at
        Sort Method: external merge  Disk: 7488kB
        Buffers: shared hit=2651, temp read=938 written=938
        ->  Seq Scan on documents  (cost=0.00..2847.80 rows=12300 width=541) (actual time=0.049..40.579 rows=13184 loops=1)
              Filter: (approved AND (viewable_at < '2015-07-20 06:46:55.222798'::timestamp without time zone))
              Rows Removed by Filter: 2560
              Buffers: shared hit=2651
Planning time: 0.218 ms
Execution time: 178.583 ms
(12 rows)

Примечания по использованию документа:

Документы редактируются вручную, и мы еще не сохраняем документы автоматически каждые X секунд или что-то в этом роде, поэтому объем будет достаточно низким. На данный момент существует в среднем 7 версий и в среднем только 2 утвержденных версии на каждый reference_id. (~30%)

Что касается минимальных и максимальных значений, подавляющее большинство документов будет иметь 1 или 2 версии, и маловероятно, что какой-либо документ будет иметь более 30 или 40. Существует процесс сборки мусора для очистки неутвержденных версий старше недели, поэтому общее количество версий должно оставаться довольно низким.

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


person Jeremy Baker    schedule 20.07.2015    source источник
comment
Можете ли вы опубликовать результаты ANALYZE, а также дать оценку количества редакций отдельного документа и процент утвержденных редакций.   -  person Gary    schedule 20.07.2015
comment
Вам действительно нужно получить все документы, которые старше заданного времени? Если ваш SELECT не очень избирательный (не каламбур), никакая индексация не поможет...   -  person Branko Dimitrijevic    schedule 20.07.2015
comment
@Gary Поскольку это связано с производительностью, EXPLAIN (BUFFERS, ANALYZE) было бы предпочтительнее. Обычный ANALYZE не содержит много полезной информации.   -  person Craig Ringer    schedule 20.07.2015
comment
@ Гэри Спасибо. Я добавил дополнительный контекст.   -  person Jeremy Baker    schedule 20.07.2015
comment
@BrankoDimitrijevic Я добавил примечание об использовании лимита/смещения и области действия моих запросов. Спасибо за хороший вопрос.   -  person Jeremy Baker    schedule 21.07.2015
comment
@CraigRinger Спасибо! Я не знал о БУФЕРАХ. Это уже полезно. Я разместил вывод выше.   -  person Jeremy Baker    schedule 21.07.2015
comment
explain.depesz.com/s/jAb   -  person Craig Ringer    schedule 21.07.2015
comment
@CraigRinger эй. Это потрясающе. Изучаю сейчас..   -  person Jeremy Baker    schedule 21.07.2015
comment
@CraigRinger Спасибо. Это помогло мне обнаружить, что индекс для reference_id и created_at значительно улучшил время моего запроса. Мне все еще любопытно, является ли этот отдельный запрос правильным путем, но это огромное улучшение.   -  person Jeremy Baker    schedule 21.07.2015
comment
Имейте в виду, что офсет имеет свои проблемы. Для лучшего способа см. здесь.   -  person Branko Dimitrijevic    schedule 21.07.2015


Ответы (2)


@Craig уже охватывает большинство вариантов ускорения этого запроса. Больше work_mem за сессию, пожалуй, самый эффективный пункт.

С:

Существует процесс сборки мусора для очистки неутвержденных версий старше недели.

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

подавляющее большинство документов будет иметь 1 или 2 версии

У вас уже есть лучшая техника запросов с DISTINCT ON:

С ростом числа версий другие методы были бы более эффективными:

Единственный слегка нетрадиционный элемент в вашем запросе состоит в том, что предикат находится на viewable_at, но затем вы берете строку с последним created_at, поэтому ваш индекс на самом деле будет таким:

(reference_id, viewable_at ASC, created_at DESC) WHERE (approved)

Предположим, что все столбцы определены NOT NULL. Чередующийся порядок сортировки между viewable_at и created_at важен. Опять же, хотя у вас так мало строк на reference_id, я не ожидаю, что какой-либо индекс будет полезен. Вся таблица должна быть прочитана в любом случае, последовательное сканирование будет примерно таким же быстрым. Дополнительные затраты на обслуживание индекса могут даже перевесить его преимущества.

Однако, поскольку:

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

У меня есть еще одно предложение: создайте MATERIALIZED VIEW. из вашего запроса, предоставляя вам снимок вашего проекта на данный момент времени. Если место на диске не является проблемой, а моментальный снимок можно использовать повторно, вы можете даже собрать пару из них, чтобы остаться с ними:

CREATE MATERIALIZED VIEW doc_20150715_1300 AS
SELECT DISTINCT ON (reference_id)
       reference_id, id, approved, viewable_at, content 
FROM   documents 
WHERE  approved  -- simpler expression for boolean column
AND    viewable_at <= '2015-07-15 13:00:00' 
ORDER  BY reference_id, created_at DESC;

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

CREATE TEMP TABLE doc_20150715_1300 AS ...;

ANALYZE doc_20150715_1300;

Обязательно запустите ANALYZE для временной таблицы (а также для MV, если вы запускаете запросы сразу после его создания):

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

Обратите внимание: текущая версия 1.20.0 pgAdmin не отображает индексы для MV. Это уже исправлено и ожидает выхода следующей версии.

person Erwin Brandstetter    schedule 21.07.2015
comment
Спасибо, Эрвин. Вчера вечером я экспериментировал с материализованными представлениями, и мне очень понравилось их исполнение. Сложная часть (и почему я не думаю, что могу использовать материализованное представление) заключается в том, что для запроса необходимо указать время viewable_at. Возможно, временная таблица решит эту проблему? - person Jeremy Baker; 21.07.2015
comment
@JeremyBaker: Если viewable_at отличается для каждого запроса, то ни MV, ни временная таблица не являются хорошими вариантами. Это имеет смысл только для нескольких запросов, основанных на одном снимке. - person Erwin Brandstetter; 23.07.2015

Глядя на ваш вывод объяснения, похоже, что вы извлекаете большую часть содержимого таблицы documents, поэтому разумно выполняет последовательное сканирование. Ваши оценки количества строк разумны, здесь, похоже, нет проблем со статистикой.

Он выполняет внешнюю сортировку слиянием на диске, поэтому вы можете увидеть значительное увеличение производительности за счет увеличения work_mem в сеансе, например.

SET work_mem = '12MB'

Возможно, будет полезен индекс (reference_id ASC, created_at DESC) WHERE (approved), поскольку он позволит извлекать результаты в требуемом порядке.

Вы также можете поэкспериментировать с добавлением viewable_at к индексу. Я думаю, что это может быть последняя колонка, но я не уверен. Или даже превратить его в покрывающий индекс, добавив viewable_at, id, content и исключив ненужный столбец approved из набора результатов. Это может разрешить сканирование только индекса, хотя с участием DISTINCT ON я не уверен.

person Craig Ringer    schedule 21.07.2015