优化行排除查询

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编写此查询,从而将问题简化为一些快速设置操作:

In [17]: %timeit all_docs - (tags_to_docs[12345] | tags_to_docs[7654])
100 loops, best of 3: 13.7 ms per loop
Run Code Online (Sandbox Code Playgroud)

然而,将设置操作转换为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 INEXCEPT使得它更慢.
  • 我对B树索引document_idtag_id所有三个表和另一位在(document_id, tag_id).
  • Postgres进程的默认内存限制已显着增加,因此我认为Postgres配置错误.

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

Erw*_*ter 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 会是最短的,但通常不会那么快.

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

  • 几个tag_id要排除的获胜查询是您原来的NOT IN.

  • NOT EXISTSLEFT JOIN / IS NULL导致相同的查询计划.对于超过几十个ID,我希望这些ID可以更好地扩展......

  • 在只读情况下,您将独占地看到仅索引扫描,因此中行的物理顺序变得无关紧要.因此,测试3没有带来任何进一步的改进.

  • 如果发生对表的写入并且autovacuum无法跟上,您将看到(位图)索引扫描.物理聚类对于那些人来说非常重要.