【问题标题】:Improve PostgreSQL set difference efficiency提高PostgreSQL集差效率
【发布时间】:2014-03-28 01:12:33
【问题描述】:

我正在尝试在 PostgreSQL 9.3 中进行一些设置操作。

我有两张桌子,为了简单起见,我们称它们为table_atable_b

create table table_a(id varchar primary key);
create table table_b(id varchar primary key);

我有一个简单的查询(在最简单的表述中,尽管它是实践中插入的来源):

(select id from table_a) except (select id from table_b);

在我开始使用 PostgreSQL 之前,我会做这样的操作:

set-diff table_a.csv table_b.csv > table_c.csv

set-diff 看起来大概像这样:

while (not eof(a)) and (not eof(b)):
  line_a <- peek_line(a)
  line_b <- peek_line(b)
  if line_a < line_b:
    output read_line(a)
  else if line_a == line_b:
    read_line(a)
  else:
    read_line(b)
while not eof(a):
  output read_line(a)

这根本不需要很长时间,内存需求微不足道,并且可以最大限度地有效利用顺序磁盘 I/O。这很重要,因为这台机器没有大量内存 - 它无法将所有数据都放入 RAM。

然而,PostgreSQL 提出了这种计划(来自一些实际的表):

                                    QUERY PLAN
----------------------------------------------------------------------------------
 SetOp Except  (cost=3184554.28..3238904.44 rows=9434298 width=51)
   ->  Sort  (cost=3184554.28..3211729.36 rows=10870032 width=51)
         Sort Key: "*SELECT* 1".id
         ->  Append  (cost=0.00..428039.64 rows=10870032 width=51)
               ->  Subquery Scan on "*SELECT* 1"  (cost=0.00..345707.96 rows=9434298 width=54)
                     ->  Seq Scan on table_a  (cost=0.00..251364.98 rows=9434298 width=54)
               ->  Subquery Scan on "*SELECT* 2"  (cost=0.00..82331.68 rows=1435734 width=32)
                     ->  Seq Scan on table_b  (cost=0.00..67974.34 rows=1435734 width=32)

查询时间过长 - 几分钟。

我确信 PostgreSQL 可以使用我上面概述的相同类型的合并策略,仅使用索引扫描,而不使用排序。相反,它似乎是连接两个表扫描并对它们进行排序,有点像这个命令行,虽然没有读取 table_b 两次:

sort table_a.csv table_b.csv table_b.csv | uniq -u

这涉及相当多的额外工作 - 例如,当并非所有内容都适合内存时,I/O 的一部分 log(n) 倍。

所涉及的列是 btree 索引的。从查询中选择的唯一列与已编入索引并正在合并的列相同。语言环境到处都是 C。

在我使用大量文本文件和一些自定义索引工具之前。我正在尝试使用数据库来获得额外的查询灵活性并避免维护自定义索引。然而性能令人震惊,以至于我正在考虑在数据库之外进行合并和大多数其他大规模更新操作,通过 csv 来回传输数据。

我错过了什么?

【问题讨论】:

  • 您可能希望将此问题迁移到 dba.stackexchange.com。

标签: sql database performance postgresql


【解决方案1】:

第一想法:

  • Plain EXCEPT 表示 EXCEPT DISTINCT,这意味着它会从结果中消除重复行。如果可以,请使用EXCEPT ALL,它应该更快。
  • 如果您还有其他选项,请不要使用 combining queries,众所周知它们很慢。
  • 从您的 EXPLAIN 看来,您似乎也应用了排序,这也需要更多时间(尤其是在组合查询时)。

我的9.2 上的结果:

EXCEPT

explain select id from table_a except (select id from table_b);

结果:

HashSetOp Except  (cost=0.00..947.00 rows=20000 width=5)
  ->  Append  (cost=0.00..872.00 rows=30000 width=5)
        ->  Subquery Scan on "*SELECT* 1"  (cost=0.00..563.00 rows=20000 width=5)
              ->  Seq Scan on table_a  (cost=0.00..363.00 rows=20000 width=5)
        ->  Subquery Scan on "*SELECT* 2"  (cost=0.00..309.00 rows=10000 width=4)
              ->  Seq Scan on table_b  (cost=0.00..209.00 rows=10000 width=4)

EXCEPTORDER BY

explain select id from table_a except (select id from table_b) order by id;

结果:

Sort  (cost=2375.77..2425.77 rows=20000 width=5)
  Sort Key: "*SELECT* 1".id
  ->  HashSetOp Except  (cost=0.00..947.00 rows=20000 width=5)
        ->  Append  (cost=0.00..872.00 rows=30000 width=5)
              ->  Subquery Scan on "*SELECT* 1"  (cost=0.00..563.00 rows=20000 width=5)
                    ->  Seq Scan on table_a  (cost=0.00..363.00 rows=20000 width=5)
              ->  Subquery Scan on "*SELECT* 2"  (cost=0.00..309.00 rows=10000 width=4)
                    ->  Seq Scan on table_b  (cost=0.00..209.00 rows=10000 width=4)

JOINORDER BY

explain select table_a.id from table_a
left outer join table_b on table_a.id = table_b.id
where table_b.id is null order by table_a.id;

explain select id from table_a
where not exists (select * from table_b where table_b.id = table_a.id) order by id;

结果(相同):

Merge Anti Join  (cost=0.57..1213.57 rows=10000 width=5)
  Merge Cond: ((table_a.id)::text = (table_b.id)::text)
  ->  Index Only Scan using table_a_pkey on table_a  (cost=0.29..688.29 rows=20000 width=5)
  ->  Index Only Scan using table_b_pkey on table_b  (cost=0.29..350.29 rows=10000 width=4)

NOT INORDER BY

explain select id from table_a where id not in (select id from table_b) order by id;

结果(我的获胜者):

Seq Scan on table_a  (cost=234.00..647.00 rows=10000 width=5)
  Filter: (NOT (hashed SubPlan 1))
  SubPlan 1
    ->  Seq Scan on table_b  (cost=0.00..209.00 rows=10000 width=4)

二手

create table table_a(id varchar primary key, rnd float default random());
create table table_b(id varchar primary key, rnd float default random());

do language plpgsql $$
begin
    for i in 1 .. 10000 loop
        insert into table_a(id) values (i);
        insert into table_b(id) values (i);
    end loop;
    for i in 10001 .. 20000 loop
        insert into table_a(id) values (i);
    end loop;
end;
$$;

【讨论】:

  • 不错。您也可以添加NOT EXISTS 版本。
  • 就在Anti JOIN下面,因为他们有相同的计划。
  • 糟糕,我错过了 ;)
【解决方案2】:

这些变种是怎么做的?

select id
from table_a a
where not exists (select 1 from table_b b where b.id = a.id);

或:

select id
from table_a left outer join
     table_b b
     on a.id = b.id
where b.id is null;

如果这些性能更好,只是优化except 的努力不如语言的其他组件那么多。

【讨论】:

  • 我昨晚尝试了这两种变体(我发现not innot exists 相比有多慢,以及它背后奇怪的 SQL 标准空处理),我觉得我有点狡猾与左外连接版本。两者有相同的计划,大约merge_anti_join(merge(external_sort(table_a), materialize(sort(table_b)))),但运行时间没有改善。
  • @BarryKelly 。 . .这是令人惊讶的。我认为这些会更好地利用索引。
  • 之前我为 table_a 添加了一个整数键和一个唯一索引。使用存储过程和游标对行进行编号需要几个小时,而同时插入的还有几百万行(因此索引列有很多空值)。我提到这一点是因为对整数的直接相等查找现在使用全表扫描,即使该列存在唯一索引。我已经运行了分析、真空等,删除并重新创建了索引,但没有区别。很奇怪,类似于stackoverflow.com/q/15965785/3712
  • 我应该补充一下,我尝试使用set enable_seqscan=false,但它仍然不会使用索引。有点无聊。
【解决方案3】:

你用吸尘器打扫你的桌子了吗? pg_class 中的 relallvisible 是什么?如果索引扫描还必须访问每行的表,则它是不利的,因为那是分散的IO。

使用真空表,我得到两个仅索引扫描之间的合并连接。

我认为您对数据库的功能抱有不切实际的期望。您始终可以编写自己的代码来比数据库更快地执行任何给定的操作,尤其是如果您在此过程中忽略 ACID。

例如,如果您保持数据排序,那么您可以避免进行排序(前提是您只需要一个键进行排序)。但是你不能再进行有效的更新或插入。

【讨论】:

  • 我已经清理了桌子。被连接的列是唯一被选中的并且被索引的列,所以没有访问表。我不认为我的期望太不切实际;我预计乘数会持续下降,磁盘和内存使用量会激增。但是,我没想到会及时出现 1000 倍的爆炸,这与我在实践中看到的很接近,并且在很大程度上不是数据库应该能够使用可用的信息来做的事情。我的 postgres 实例中似乎有些问题。我现在不在数据库回答您的元数据问题。
  • @BarryKelly 只要表是真空的并且 relallvisible 很高,我就会得到仅索引扫描之间的合并连接。我不知道你为什么没有得到它们。您可以发布一个脚本来生成演示这一点的虚拟数据吗? PostgreSQL 不会在索引中存储可见性信息,因此它必须访问表,除非为页面设置了 allvisible。如果我在数据太大而无法放入 RAM 并且未设置 allvisible 时欺骗它进行索引合并,那么性能将是灾难性的。
  • @BarryKelly 如果你炸毁了空间,你可以很容易地越过工作集不再适合 RAM 的边界。在某些情况下,这肯定会导致灾难性的减速,但我怀疑你真的看到了 1000 倍。从仅索引的合并反连接扫描到 SetOp-Sort-Append 时,我的速度降低了大约 5 倍
猜你喜欢
  • 1970-01-01
  • 2013-01-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-11-09
  • 2013-08-27
  • 2016-07-11
相关资源
最近更新 更多