为什么 distinct on in 子查询会损害 PostgreSQL 的性能?

Why does distinct on in subqueries hurt performance in PostgreSQL?

我有一个 table 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) 子句查询 table,我会受到严重的性能损失。

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 一个不唯一的索引列并且性能非常差。

逻辑差异

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

  • Allow null in unique column

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

Obviously, two rows are considered distinct if they differ in at least one column value. Null values are considered equal in this comparison.

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

  • Select first row in each GROUP BY group?

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

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

意外发现

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

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 产生影响。参见:

所以这可行:

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

但是切换idemail后同样不行。我在 fiddle:

添加了一些演示

db<>fiddle here

所以呢?

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

The real problem I'm having is that I'm trying to create a view that does distinct on an indexed column that isn't unique and the performance is very bad.

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

  • Optimize GROUP BY query to retrieve latest row per user