优化行排除查询

时间:2016-08-06 19:20:59

标签: sql performance postgresql indexing postgresql-performance

我正在设计一个大多数只读数据库,其中包含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会让它更慢。
  • 我在所有三个表中都有EXCEPTdocument_id的btree索引,tag_id上有另一个索引。
  • Postgres进程的默认内存限制已显着增加,因此我认为Postgres配置错误。

如何加快此查询?有没有办法像我用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

2 个答案:

答案 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 EXISTSLEFT JOIN / IS NULL始终是热门竞争者。其他答案都提出了这两点。不过,LEFT JOIN必须是实际的LEFT [OUTER] JOIN

EXCEPT ALL最短,但通常不会那么快。

1.不在
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_idtag_id更明显。这也可能是另一回事。 Btree索引基本上对任何列的顺序执行相同的操作。事实上,您的查询中最具选择性的谓词会在tag_id上过滤。并且在领先索引列上的速度更快。

  • tag_id 是您要删除的少数NOT IN的获胜查询。

  • NOT EXISTSLEFT 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