我正在设计一个只读数据库,包含300,000个文档,大约50,000个不同的标签,每个文档平均有15个标签。现在,我唯一关心的查询是从给定的标签集中选择所有带有 no 标签的文档。我只对document_id
列感兴趣(结果中没有其他列)。
我的schema基本上是:
CREATE TABLE documents (
document_id SERIAL PRIMARY KEY,
title TEXT
);
CREATE TABLE tags (
tag_id SERIAL PRIMARY KEY,
name TEXT UNIQUE
);
CREATE TABLE documents_tags (
document_id INTEGER REFERENCES documents,
tag_id INTEGER REFERENCES tags,
PRIMARY KEY (document_id, tag_id)
);
我可以通过预先计算给定标签的文档集来用Python编写这个查询,这将问题减少到几个快速的集合操作:
In [17]: %timeit all_docs - (tags_to_docs[12345] | tags_to_docs[7654])
100 loops, best of 3: 13.7 ms per loop
然而,将set操作转换为Postgres并不那么快:
stuff=# SELECT document_id AS id FROM documents WHERE document_id NOT IN (
stuff(# SELECT documents_tags.document_id AS id FROM documents_tags
stuff(# WHERE documents_tags.tag_id IN (12345, 7654)
stuff(# );
document_id
---------------
...
Time: 201.476 ms
- 将
NOT IN
替换为EXCEPT
会使其更慢。 - 我在所有三个表中的
document_id
和tag_id
上都有btree索引,在(document_id, tag_id)
上还有一个。 - Postgres进程的默认内存限制已经显著增加,所以我不认为Postgres配置错误。
如何加快查询速度?有没有办法像我用Python那样预先计算之间的Map,或者我的想法是错误的?
下面是EXPLAIN ANALYZE
的结果:
EXPLAIN ANALYZE
SELECT document_id AS id FROM documents
WHERE document_id NOT IN (
SELECT documents_tags.documents_id AS id FROM documents_tags
WHERE documents_tags.tag_id IN (12345, 7654)
);
QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------------------------------------------------
Seq Scan on documents (cost=20280.27..38267.57 rows=83212 width=4) (actual time=176.760..300.214 rows=20036 loops=1)
Filter: (NOT (hashed SubPlan 1))
Rows Removed by Filter: 146388
SubPlan 1
-> Bitmap Heap Scan on documents_tags (cost=5344.61..19661.00 rows=247711 width=4) (actual time=32.964..89.514 rows=235093 loops=1)
Recheck Cond: (tag_id = ANY ('{12345,7654}'::integer[]))
Heap Blocks: exact=3300
-> Bitmap Index Scan on documents_tags__tag_id_index (cost=0.00..5282.68 rows=247711 width=0) (actual time=32.320..32.320 rows=243230 loops=1)
Index Cond: (tag_id = ANY ('{12345,7654}'::integer[]))
Planning time: 0.117 ms
Execution time: 303.289 ms
(11 rows)
Time: 303.790 ms
我从默认配置更改的唯一设置是:
shared_buffers = 5GB
temp_buffers = 128MB
work_mem = 512MB
effective_cache_size = 16GB
在64GB RAM的服务器上运行Postgres9.4.5。
2条答案
按热度按时间jexiocij1#
优化设置提高读性能
对于一个64 GB的服务器来说,你的内存设置似乎是合理的--除了
work_mem = 512MB
。这太高了。你的查询不是特别复杂,你的表也不是那么大。4.5百万行(300 k x 15)在简单的连接表
documents_tags
中应该占用~ 156 MB,PK另占96 MB。对于您的查询,您通常不需要读取整个表,只需读取索引的一小部分。对于像您这样的 "大多为只读",你应该看到***索引扫描***只在PK的索引上。你不需要那么多的work_mem
-这 * 可能 * 没有多大关系-除非你有很多并发查询。引用手册:...多个正在运行的会话可能同时执行这样的操作。因此,所使用的总内存可能是
work_mem
值的许多倍;在选择该值时必须记住这一事实。将
work_mem
设置得过高实际上可能会降低性能:我建议将
work_mem
减少到128 MB或更少,以避免可能的内存不足-除非您有其他需要更多内存的常见查询。还有其他几个Angular 可以优化读取性能:
关键问题:前导索引列
所有这些可能会有一点帮助。但关键问题是这样的:
300 k文档,2个要排除的标签。理想情况下,您的索引以
tag_id
作为*前导*列,document_id
作为第二列。如果索引仅针对(tag_id)
,则无法获得仅索引扫描。如果此查询是您唯一的用例,请按以下演示更改PK。或者更好:如果需要,您可以在
(tag_id, document_id)
上创建一个额外的普通索引-并将documents_tags
上的其他两个索引仅放在(tag_id)
和(document_id)
上。它们在两个多列索引上没有提供任何内容。剩余的2索引(与之前的3索引相反)更小,并且在各个方面都更上级。理由:同时,我建议也使用新的PK
CLUSTER
表,所有这些都在一个事务中,可能在本地有一些额外的maintenance_work_mem
:别忘了:
查询
查询本身是普通的。以下是4种标准技术:
NOT IN
是-引用我自己的话:仅适用于没有NULL值的小集合
您的使用案例:所有涉及的列
NOT NULL
和您的排除项列表非常短。您的原始查询是一个热门竞争者。NOT EXISTS
和LEFT JOIN / IS NULL
一直是热门的竞争者。在其他答案中都有建议。LEFT JOIN
必须是实际的LEFT [OUTER] JOIN
。EXCEPT ALL
最短,但通常没有那么快。1.
NOT IN
2.
NOT EXISTS
3.
LEFT JOIN
/IS NULL
4.
EXCEPT ALL
基准测试
去验证我的理论。
测试设置
请注意,由于我填充表的方式,
documents_tags
中的行在物理上被document_id
聚集-这可能也是您当前的情况。2016-08-06使用Postgres 9.5进行原始测试
在我的旧笔记本电脑与4 GB RAM。
对4个查询中的每个查询运行3次测试,每次最好运行5次以排除缓存影响。
***测试1:***使用
documents_tags_pkey
。索引 * 和 * 行的物理顺序对于我们的查询来说是 * 坏 * 的。***测试2:***按照建议在
(tag_id, document_id)
上重新创建PK。***测试三:***
CLUSTER
在新PK上,EXPLAIN ANALYZE
的执行时间(毫秒):结论
tag_id
-用于涉及 fewtag_id
和 manydocument_id
的查询。确切地说,
document_id
比tag_id
有更多不同的document_id
并不重要。这也可能是另一种情况。Btree索引基本上对任何顺序的列执行相同的操作。事实上,查询中最具选择性的 predicate 过滤tag_id
。这在前导索引列上更快。tag_id
的获胜查询是您的原始查询**NOT IN
**。NOT EXISTS
和LEFT JOIN / IS NULL
产生相同的查询计划。对于超过几十个ID,我希望这些可以更好地扩展...autovacuum
无法跟上,您将看到**(位图)索引扫描**。物理集群对于这些很重要。2023-04-11使用Postgres 15重新测试
在具有64 GB RAM的较新笔记本电脑上具有大量
work_mem
。我跳过了“测试2”,并添加了“测试1+”。这与测试1相同,但增加了
max_parallel_workers_per_gather
,以便使用3个并行工作者而不是2个。正如预期的那样,业绩普遍改善。
NOT IN
无法跟上,因为它带来了一些固有的优化障碍。现在明显的赢家是NOT EXISTS
或LEFT JOIN
/NOT NULL
,使用所有索引扫描。“测试1”和“测试1+”看起来比原来更好。那些使用了2个和3个工人,所以把数字乘以那个系数就可以得到实际的系统负载。
gzszwxb42#
使用外部连接,在连接上使用标记条件,仅保留未命中的连接以返回指定标记中 none 匹配的位置: