use*_*188 5 sql postgresql indexing performance postgresql-performance
我正在设计一个大多数只读数据库,其中包含300,000个文档,大约有50,000个不同的标记,每个文档平均有15个标记.目前,我唯一关心的查询是从给定的一组标签中选择没有标签的所有文档.我只对document_id列感兴趣(结果中没有其他列).
我的架构基本上是:
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)
);
Run Code Online (Sandbox Code Playgroud)
我可以通过预先计算给定标记的文档集来用Python编写此查询,从而将问题简化为一些快速设置操作:
Run Code Online (Sandbox Code Playgroud)In [17]: %timeit all_docs - (tags_to_docs[12345] | tags_to_docs[7654]) 100 loops, best of 3: 13.7 ms per loop
然而,将设置操作转换为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
Run Code Online (Sandbox Code Playgroud)
NOT IN用EXCEPT使得它更慢.document_id和tag_id所有三个表和另一位在(document_id, tag_id).如何加快此查询?有没有办法像我用Python那样预先计算映射,或者我是否以错误的方式思考这个问题?
这是一个结果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
Run Code Online (Sandbox Code Playgroud)
我从默认配置更改的唯一设置是:
shared_buffers = 5GB
temp_buffers = 128MB
work_mem = 512MB
effective_cache_size = 16GB
Run Code Online (Sandbox Code Playgroud)
在具有64GB RAM的服务器上运行Postgres 9.4.5.
对于64GB服务器,你的内存设置似乎合理 - 除了可能work_mem = 512MB.那很高.您的查询并不是特别复杂,您的表格也不是那么大.
简单连接表中的450万行(300k x 15)documents_tags应占用~156 MB,PK另外占用96 MB.对于您的查询,您通常不需要读取整个表,只需读取索引的一小部分.对于" 大多数只读 ",你应该只看到PK的索引上的仅索引扫描.你不需要那么多work_mem- 这可能并不重要 - 除非你有很多并发查询.引用手册:
......几个运行会话可能同时进行此类操作.因此,使用的总内存可能是其值的许多倍
work_mem; 在选择价值时,有必要记住这一事实.
设置work_mem过高实际上可能会影响性能:
我建议减少work_mem到128 MB或更少,以避免可能的内存饥饿 - 除非你有其他需要更多的常见查询.您可以随时在本地设置更高的特殊查询.
还有其他几个角度可以优化读取性能:
所有这些可能会有所帮助.但关键问题是:
PRIMARY KEY (document_id, tag_id)Run Code Online (Sandbox Code Playgroud)
300k文件,2个标签排除.理想情况下,您有一个索引tag_id作为前导列和document_id第二列.只有索引(tag_id)才能获得仅索引扫描.如果此查询是您的唯一用例,请更改您的PK,如下所示.
或者甚至可能更好:(tag_id, document_id)如果你需要两者,你可以创建一个额外的普通索引- 然后将其他两个索引放在documents_tagson (tag_id)和(document_id).它们在两个多列索引上没有提供任何内容.剩余的2个索引(与之前的3个索引相对)在各方面都较小且优越.理由:
在此期间,我建议CLUSTER使用新PK的表,所有这些都在一个事务中,可能还有一些额外的maintenance_work_mem本地:
BEGIN;
SET LOCAL maintenance_work_mem = '256MB';
ALTER TABLE documents_tags
DROP CONSTRAINT documents_tags_pkey
, ADD PRIMARY KEY (tag_id, document_id); -- tag_id first.
CLUSTER documents_tags USING documents_tags_pkey;
COMMIT;Run Code Online (Sandbox Code Playgroud)
别忘了:
ANALYZE documents_tags;
Run Code Online (Sandbox Code Playgroud)
查询本身是普通的.以下是4种标准技术:
NOT IN 是 - 引用自己:
仅适用于没有NULL值的小集
您的用例完全是:所有涉及的列NOT NULL和您排除的项目列表都很短.您的原始查询是一个热门竞争者.
NOT EXISTS而且LEFT JOIN / IS NULL总是热门的竞争者.其他答案都提出了这两点.LEFT JOIN但必须是实际的LEFT [OUTER] JOIN.
EXCEPT ALL 会是最短的,但通常不会那么快.
SELECT document_id
FROM documents d
WHERE document_id NOT IN (
SELECT document_id -- no need for column alias, only value is relevant
FROM documents_tags
WHERE tag_id IN (12345, 7654)
);
Run Code Online (Sandbox Code Playgroud)
2.不存在
SELECT document_id
FROM documents d
WHERE NOT EXISTS (
SELECT 1
FROM documents_tags
WHERE document_id = d.document_id
AND tag_id IN (12345, 7654)
);
Run Code Online (Sandbox Code Playgroud)
3. LEFT JOIN/IS为空
SELECT d.document_id
FROM documents d
LEFT JOIN documents_tags dt ON dt.document_id = d.document_id
AND dt.tag_id IN (12345, 7654)
WHERE dt.document_id IS NULL;Run Code Online (Sandbox Code Playgroud)
4.除了所有
SELECT document_id
FROM documents
EXCEPT ALL -- ALL, to keep duplicate rows and make it faster
SELECT document_id
FROM documents_tags
WHERE tag_id IN (12345, 7654);
Run Code Online (Sandbox Code Playgroud)
我在旧笔记本电脑上运行了4 GB RAM和Postgres 9.5.3的快速基准测试,以便对我的理论进行测试:
SET random_page_cost = 1.1;
SET work_mem = '128MB';
CREATE SCHEMA temp;
SET search_path = temp, public;
CREATE TABLE documents (
document_id serial PRIMARY KEY,
title text
);
-- CREATE TABLE tags ( ... -- actually irrelevant for this query
CREATE TABLE documents_tags (
document_id integer REFERENCES documents,
tag_id integer -- REFERENCES tags -- irrelevant for test
-- no PK yet, to test seq scan
-- it's also faster to create the PK after filling the big table
);
INSERT INTO documents (title)
SELECT 'some dummy title ' || g
FROM generate_series(1, 300000) g;
INSERT INTO documents_tags(document_id, tag_id)
SELECT i.*
FROM documents d
CROSS JOIN LATERAL (
SELECT DISTINCT d.document_id, ceil(random() * 50000)::int
FROM generate_series (1,15)) i;
ALTER TABLE documents_tags ADD PRIMARY KEY (document_id, tag_id); -- your current index
ANALYZE documents_tags;
ANALYZE documents;
Run Code Online (Sandbox Code Playgroud)
请注意,由于我填充表格的方式,行中documents_tags的行是物理聚集的document_id- 这可能也是您当前的情况.
对4个查询中的每个查询进行3次测试,每次最好5次,以排除缓存效果.
测试1:有了documents_tags_pkey像你有它.行的索引和物理顺序对我们的查询不利.
测试2:(tag_id, document_id)像建议的那样重新创建PK.
测试3: CLUSTER关于新的PK.执行时间EXPLAIN ANALYZE以毫秒为单位:
time in ms | Test 1 | Test 2 | Test 3 1. NOT IN | 654 | 70 | 71 -- winner! 2. NOT EXISTS | 684 | 103 | 97 3. LEFT JOIN | 685 | 98 | 99 4. EXCEPT ALL | 833 | 255 | 250
关键元素是带有前导tag_id的正确索引 - 用于涉及少量 tag_id和多个的 查询document_id.
准确地说,这不是重要的是,有更多不同的document_id比tag_id.这也可能是另一回事.Btree索引基本上对任何列的顺序执行相同的操作.事实上,查询中最具选择性的谓词会过滤掉tag_id.这在领先的索引列上更快.
几个tag_id要排除的获胜查询是您原来的NOT IN.
NOT EXISTS并LEFT JOIN / IS NULL导致相同的查询计划.对于超过几十个ID,我希望这些ID可以更好地扩展......
在只读情况下,您将独占地看到仅索引扫描,因此表中行的物理顺序变得无关紧要.因此,测试3没有带来任何进一步的改进.
如果发生对表的写入并且autovacuum无法跟上,您将看到(位图)索引扫描.物理聚类对于那些人来说非常重要.