如何在 PostgreSQL 中存储和查询同一文档的版本?

How to store and query version of same document in 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分组的行,其中approvedtrue并且viewable_at小于当前时间。 (在这种情况下,行 ID 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 上有一个索引,在 approved 和 viewable_at 上有一个多列索引。

只有 15,000 行,它在我的本地计算机上仍然平均为几百毫秒 (140 - 200)。我怀疑不同的调用或排序可能会减慢它的速度。

存储此信息的最有效方法是什么,以便 SELECT 查询性能最高?

EXPLAIN (BUFFERS, ANALYZE) 的结果:

                                                              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 个版本每个 reference_id 平均只有 2 个批准版本。 (~30%)

在最小和最大方面,绝大多数文档将有 1 或 2 个版本,似乎任何文档都不会超过 30 或 40。有一个垃圾收集过程来清除未批准的旧版本不到一周,所以版本总数应该保持在相当低的水平。

为了检索和实际使用,我可以在查询中使用限制/偏移量,但在我的测试中这并没有太大的不同。理想情况下,这是一个填充视图或其他内容的基本查询,以便我可以在这些结果之上执行其他查询,但我不完全确定这将如何影响结果性能并且愿意接受建议。我的印象是,如果我能让这个存储/查询尽可能简单/快速,那么从这一点开始的所有其他查询都可以得到改进,但很可能我错了,每个查询都需要更多的独立思考。

查看您的解释输出,您似乎正在获取 documents table 中的大部分内容,因此它明智地进行了顺序扫描。您的行数估计是合理的,这里似乎没有任何统计问题。

它在磁盘上执行外部合并排序,因此您可能会看到通过增加会话中的 work_mem 显着提高性能,例如

SET work_mem = '12MB'

(reference_id ASC, created_at DESC) WHERE (approved) 上的索引可能会有用,因为它将允许按要求的顺序获取结果。

您也可以尝试将 viewable_at 添加到索引中。我认为它可能必须是最后一列,但我不确定。或者甚至通过附加 viewable_at, id, content 并从结果集中省略不必要的 approved 列使其成为覆盖索引。这可能允许仅索引扫描,但我不确定是否涉及 DISTINCT ON

大多数选项可使此查询更快。 More work_mem for the session 可能是最有效的项目。

开始于:

There is a garbage collection process to clean out unapproved versions older than a week

不包括未经批准的版本的部分索引不会太多。 如果你使用索引,你仍然会排除那些不相关的行。
由于您似乎 每个 reference_id:

的版本很少

the vast majority of documents will have 1 or 2 versions

您已经拥有了最好的查询技术 DISTINCT ON:

  • Select first row in each GROUP BY group?

随着版本越来越多,其他技术越来越优越:

  • Optimize GROUP BY query to retrieve latest record per user

您的查询中唯一稍微不合常规的元素是谓词在viewable_at上,但您随后使用最新的created_at行,这就是为什么您的索引实际上是:

(reference_id, <b>viewable_at ASC</b>, created_at <b>DESC</b>) WHERE (approved)

假设所有列都是 defined NOT NULLviewable_atcreated_at 之间的交替排序顺序很重要。再一次,虽然每个 reference_id 的行数很少,但我不认为 any 索引会有多大用处。无论如何都必须读取整个 table,顺序扫描的速度差不多。索引增加的维护成本甚至可能超过其收益。

但是,由于:

Ideally this is a base query that populates a view or something so that I can do additional queries on top of these results

我还有一个建议:根据您的查询创建一个 MATERIALIZED VIEW,为您提供给定时间点的项目快照。如果磁盘 space 不是问题并且快照可能会被重复使用,您甚至可以收集其中的几个来保存:

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;

或者,如果所有其他查询 在同一会话中发生 ,请改用临时 table(它会在会话结束时自动终止):

CREATE TEMP TABLE doc_20150715_1300 AS ...;

ANALYZE doc_20150715_1300;

请务必 运行 ANALYZE 临时 table(如果您 运行 查询 立即 创建后):

无论哪种方式,在支持后续查询的快照上可能支付创建一个或多个索引的费用。取决于数据和查询。

请注意,当前版本的 pgAdmin 1.20.0 不显示 MV 的索引。那是 already been fixed,正在等待下一个版本发布。