我正在设计一个大多数只读数据库,其中包含300,000个文档,大约有50,000个不同的标记,每个文档平均有15个标记。目前,我唯一关心的查询是从给定的一组标签中选择带有 no 标签的所有文档。我只对<!DOCTYPE html>
<html ng-app="app">
<head>
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.5.8/angular.min.js"></script>
</head>
<body ng-controller="MainCtrl">
<table>
<tr ng-repeat="user in users">
<td>
<input type="checkbox" ng-model="user.removed">
</td>
<td ng-bind="user.id"></td>
<td ng-bind="user.country"></td>
<td ng-bind="user.name"></td>
</tr>
</table>
<div ng-if="users.length">
<hr>
<button ng-click="removeUserData()">Remove User</button>
</div>
</body>
</html>
列感兴趣(结果中没有其他列)。
我的架构基本上是:
document_id
我可以通过预先计算给定标签的文档集来用Python编写此查询,从而将问题简化为一些快速设置操作:
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) );
将设置操作转换为Postgres不会那么快,但是:
In [17]: %timeit all_docs - (tags_to_docs[12345] | tags_to_docs[7654])
100 loops, best of 3: 13.7 ms per loop
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
的btree索引,tag_id
上有另一个索引。如何加快此查询?有没有办法像我用Python那样预先计算映射,或者我是否以错误的方式思考这个问题?
以下是(document_id, tag_id)
:
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
在具有64GB RAM的服务器上运行Postgres 9.4.5 。
答案 0 :(得分:5)
64GB服务器的内存设置似乎合理 - 除了work_mem = 512MB
。这很高。您的查询并不是特别复杂,您的表格也不是那么大。
简单联结表documents_tags
中的450万行(300k x 15)应占用~156 MB,PK另外占用96 MB。对于您的查询,您通常不需要阅读整个表格,只需阅读索引的一小部分内容。对于&#34; 大多数只读&#34; ,您应该看到 仅索引扫描 强>对PK的索引完全。您不需要几乎work_mem
- 可能无关紧要 - 除非您有许多并发查询。 Quoting the manual:
......几个正在运行的会话可能会同时执行此类操作。因此,使用的总内存可能是
work_mem
的值的许多倍;选择价值时,有必要牢记这一点。
设置work_mem
太高实际上可能会影响性能:
我建议将work_mem
减少到128 MB或更少以避免可能的内存不足 - 除非您有其他常见查询需要更多。您可以随时在本地设置更高的特殊查询。
还有其他几个角度可以优化读取性能:
所有这一切可能会有所帮助。但关键问题是:
PRIMARY KEY (document_id, tag_id)
300k文件,2个标签要排除。理想情况下,您的索引tag_id
为 前导 列,document_id
为第2列。只有(tag_id)
的索引,您无法获得仅索引扫描。如果此查询是您的唯一用例,请更改您的PK,如下所示。
或者甚至可能更好:如果你需要两者,你可以在(tag_id, document_id)
上创建一个额外的普通索引 - 并在documents_tags
和(tag_id)
上删除另外两个索引(document_id)
}。它们在两个多列索引上没有提供任何内容。剩余的 2 索引(与之前的 3 索引相对)在各方面都更小,更优越。理由:
在参与其中时,我建议使用新的PK CLUSTER
表,所有这些都在一个事务中,可能在本地有一些额外的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;
不要忘记:
ANALYZE documents_tags;
查询本身是普通的。以下是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)
);
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)
);
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;
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);
我在旧笔记本电脑上运行了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;
请注意,由于我填充表格的方式,documents_tags
中的行按document_id
进行物理聚类 - 这可能也是您当前的情况。
每次4次查询都会进行3次测试,每次最多5次,以排除缓存效果。
测试1: documents_tags_pkey
就像你拥有它一样。对于我们的查询,索引和行的物理顺序错误
测试2: 按照建议在(tag_id, document_id)
重新创建PK。
关于新PK的 测试3: CLUSTER
。
执行时间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
和许多>的查询em> document_id
。
准确地说, 重要的是,document_id
比tag_id
更明显。这也可能是另一回事。 Btree索引基本上对任何列的顺序执行相同的操作。事实上,您的查询中最具选择性的谓词会在tag_id
上过滤。并且在领先索引列上的速度更快。
tag_id
是您要删除的少数NOT IN
的获胜查询。
NOT EXISTS
和LEFT JOIN / IS NULL
会生成相同的查询计划。对于超过几十个ID,我希望这些ID可以更好地扩展......
在只读情况下,您只能看到 仅索引扫描 ,因此表中的行的物理顺序变得无关紧要。因此, test 3 没有带来任何进一步的改进。
如果发生对表的写入并且autovacuum无法跟上,您将看到(位图)索引扫描。物理聚类对于那些人来说非常重要。
答案 1 :(得分:1)
使用外部联接,在联接上使用标记条件,仅保留错过的联接以返回指定标记的 none 匹配的位置:
select d.id
from documents d
join documents_tags t on t.document_id = d.id
and t.tag_id in (12345, 7654)
where t.document_id is null