如何在 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分组的行,其中approved
是true
并且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 NULL
。 viewable_at
和 created_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(如果您 运行 查询 立即 创建后):
- Are regular VACUUM ANALYZE still recommended under 9.1?
- PostgreSQL partial index unused when created on a table with existing data
无论哪种方式,在支持后续查询的快照上可能支付创建一个或多个索引的费用。取决于数据和查询。
请注意,当前版本的 pgAdmin 1.20.0 不显示 MV 的索引。那是 already been fixed,正在等待下一个版本发布。
我正在 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
小于当前时间。 (在这种情况下,行 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
。
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 NULL
。 viewable_at
和 created_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(如果您 运行 查询 立即 创建后):
- Are regular VACUUM ANALYZE still recommended under 9.1?
- PostgreSQL partial index unused when created on a table with existing data
无论哪种方式,在支持后续查询的快照上可能支付创建一个或多个索引的费用。取决于数据和查询。
请注意,当前版本的 pgAdmin 1.20.0 不显示 MV 的索引。那是 already been fixed,正在等待下一个版本发布。