为啥子查询中的 distinct on 会损害 PostgreSQL 的性能?

Posted

技术标签:

【中文标题】为啥子查询中的 distinct on 会损害 PostgreSQL 的性能?【英文标题】:Why does distinct on in subqueries hurt performance in PostgreSQL?为什么子查询中的 distinct on 会损害 PostgreSQL 的性能? 【发布时间】:2020-11-05 03:57:46 【问题描述】:

我有一个表 users,其中包含字段 idemailid 是主键,email 也是索引。

database> \d users
+-----------------------------+-----------------------------+-----------------------------------------------------+
| Column                      | Type                        | Modifiers                                           |
|-----------------------------+-----------------------------+-----------------------------------------------------|
| id                          | integer                     |  not null default nextval('users_id_seq'::regclass) |
| email                       | character varying           |                                                     |
+-----------------------------+-----------------------------+-----------------------------------------------------+
Indexes:
    "users_pkey" PRIMARY KEY, btree (id)
    "index_users_on_email" UNIQUE, btree (email)

如果我在子查询中使用 distinct on (email) 子句查询表,我会受到显着的性能损失。

database> explain (analyze, buffers)
   select
     id
   from (
     select distinct on (email)
       id
     from
       users
   ) as t
   where id = 123
+-----------------------------------------------------------------------------------------------------------------------------+
| QUERY PLAN                                                                                                                  |
|-----------------------------------------------------------------------------------------------------------------------------|
| Subquery Scan on t  (cost=8898.69..10077.84 rows=337 width=4) (actual time=221.133..250.782 rows=1 loops=1)                 |
|   Filter: (t.id = 123)                                                                                                      |
|   Rows Removed by Filter: 67379                                                                                             |
|   Buffers: shared hit=2824, temp read=288 written=289                                                                       |
|   ->  Unique  (cost=8898.69..9235.59 rows=67380 width=24) (actual time=221.121..247.582 rows=67380 loops=1)                 |
|         Buffers: shared hit=2824, temp read=288 written=289                                                                 |
|         ->  Sort  (cost=8898.69..9067.14 rows=67380 width=24) (actual time=221.120..239.573 rows=67380 loops=1)             |
|               Sort Key: users.email                                                                                         |
|               Sort Method: external merge  Disk: 2304kB                                                                     |
|               Buffers: shared hit=2824, temp read=288 written=289                                                           |
|               ->  Seq Scan on users  (cost=0.00..3494.80 rows=67380 width=24) (actual time=0.009..9.714 rows=67380 loops=1) |
|                     Buffers: shared hit=2821                                                                                |
| Planning Time: 0.243 ms                                                                                                     |
| Execution Time: 251.258 ms                                                                                                  |
+-----------------------------------------------------------------------------------------------------------------------------+

将此与distinct on (id) 进行比较,其成本不到上一个查询的千分之一。

database> explain (analyze, buffers)
   select
     id
   from (
     select distinct on (id)
       id
     from
       users
   ) as t
   where id = 123
+-----------------------------------------------------------------------------------------------------------------------------+
| QUERY PLAN                                                                                                                  |
|-----------------------------------------------------------------------------------------------------------------------------|
| Unique  (cost=0.29..8.31 rows=1 width=4) (actual time=0.021..0.022 rows=1 loops=1)                                          |
|   Buffers: shared hit=3                                                                                                     |
|   ->  Index Only Scan using users_pkey on users  (cost=0.29..8.31 rows=1 width=4) (actual time=0.020..0.020 rows=1 loops=1) |
|         Index Cond: (id = 123)                                                                                              |
|         Heap Fetches: 1                                                                                                     |
|         Buffers: shared hit=3                                                                                               |
| Planning Time: 0.090 ms                                                                                                     |
| Execution Time: 0.034 ms                                                                                                    |
+-----------------------------------------------------------------------------------------------------------------------------+

这是为什么?

我遇到的真正问题是我正在尝试创建一个视图,该视图执行 distinct on 一个不唯一的索引列并且性能非常糟糕。

【问题讨论】:

你强迫服务器做一个虚假的distinct,因为你要丢弃结果。在最后一种情况下,服务器能够检测到这一点,因为仅涉及唯一的 id 列,并且简单地丢弃了 DISTINCT 操作。在前一种情况下,它必须计算 distinct on 以找出返回了哪些 ID 值,然后才能过滤它们 这两个查询返回不同的结果。如果电子邮件的id 小于123,则第一个可能会返回no 结果。在第二种情况下,UNIQUE 索引上的DISTINCT 总是会返回索引值 我已经使用explain (analyze, buffers)更新了问题 【参考方案1】:

逻辑差异

idemail 两列都是 UNIQUE。但只有idNOT NULL。 (PRIMARY KEY 列始终是。)NULL 值不被视为相等,具有UNIQUE 约束(或索引)的列中允许多个NULL 值。那是根据标准SQL。见:

Allow null in unique column

DISTINCTDISTINCT ON 认为NULL 值相等。 The manual:

显然,如果两行至少不同 一列值。 在此比较中,空值被视为相等。

我的大胆强调。延伸阅读:

Select first row in each GROUP BY group?

在您的第二个查询中,distinct on (id) 是一个逻辑空操作:保证结果与没有DISTINCT ON 的结果相同。而且由于id = 123 上的外部SELECT 过滤器,Postgres 可以去除噪音并完成非常便宜的仅索引扫描。

另一方面,在您的第一个查询中,如果email IS NULL 有多个行,distinct on (email) 可能实际上会执行某些操作。然后 Postgres 必须根据给定的排序顺序选择第一个 id。由于没有ORDER BY,因此会导致任意选择。但是带有谓词where id = 123 的外部SELECT 可能取决于结果。整个查询在性质上与第一个完全不同 - 并且被设计破坏。

巧遇

除此之外,还有两个“幸运”的发现:

Sort Method: external merge  Disk: 2304kB

提及“磁盘”表示work_mem 不足。见:

Configuration parameter work_mem in PostgreSQL on Linux
          ->  Seq Scan on users  (cost=0.00..3494.80 rows=67380 

在我的测试中,我总是在这里进行索引扫描。表示您的设置存在臃肿的索引或其他问题。

有用的比较?

比较无济于事。我们可能会从中学到一些东西 将第一个查询与此查询进行比较 - 在​​切换 PK 和 UNIQUE 列的角色后:

select email
from  (select distinct on (id) email from users) t
where email = 'user123@foo.com';

或者通过比较第二个查询和这个 - 尝试使用 UNIQUE 列而不是 PK 列:

select email
from  (select distinct on (email) email from users) t
where email = 'user123@foo.com';

我们了解到 PK 和 UNIQUE 约束对查询计划没有不同的影响。 Postgres 不使用元信息来偷工减料。 PK 实际上会对GROUP BY 产生影响。见:

Unexpected behaviour for GROUP BY query

所以这行得通:

SELECT email
FROM  (
   SELECT email -- no aggregate required, because id = PK
   FROM   users
   GROUP  BY id  -- !
   ) t
WHERE email = 'user123@foo.com';

但是在切换idemail 之后同样不起作用。我在小提琴中添加了一些演示:

db小提琴here

那么?

这两个查询都是无意义的,原因不同。我看不出他们如何帮助您解决真正的问题:

我遇到的真正问题是,我正在尝试创建一个视图,该视图在不是唯一的的索引列上确实不同,并且性能非常糟糕。

我们需要查看您的真实查询 - 以及您设置的所有其他相关详细信息。可能有解决方案,但这可能远远超出了关于 SO 的问题的范围。考虑聘请顾问。或者考虑使用以下方法之一来优化性能:

Optimize GROUP BY query to retrieve latest row per user

【讨论】:

以上是关于为啥子查询中的 distinct on 会损害 PostgreSQL 的性能?的主要内容,如果未能解决你的问题,请参考以下文章

为啥子查询会在 group by 查询中抛出错误?

为啥子查询中的 OR 会使查询慢得多?

为啥子查询解决方法中的这个 ORDER BY 不能始终如一地工作?

django中的DISTINCT ON [重复]

为啥子 AppDomain 中的 ***Exception 会终止父 AppDomain?

为啥子进程中的管道有时会中断,有时不会?