为什么 distinct on in 子查询会损害 PostgreSQL 的性能?
Why does distinct on in subqueries hurt performance in PostgreSQL?
我有一个 table users
字段 id
和 email
。 id
是主键,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
一个不唯一的索引列并且性能非常差。
逻辑差异
id
和 email
两列都是 UNIQUE
。但只有id
是NOT NULL
。 (PRIMARY KEY
列始终是。)NULL
值不被视为相等,在具有 UNIQUE
约束(或索引)的列中允许多个 NULL
值。这是根据标准 SQL。参见:
- Allow null in unique column
但是 DISTINCT
或 DISTINCT 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 NULL
,distinct 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';
但是切换id
和email
后同样不行。我在 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
我有一个 table users
字段 id
和 email
。 id
是主键,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
一个不唯一的索引列并且性能非常差。
逻辑差异
id
和 email
两列都是 UNIQUE
。但只有id
是NOT NULL
。 (PRIMARY KEY
列始终是。)NULL
值不被视为相等,在具有 UNIQUE
约束(或索引)的列中允许多个 NULL
值。这是根据标准 SQL。参见:
- Allow null in unique column
但是 DISTINCT
或 DISTINCT 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 NULL
,distinct 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';
但是切换id
和email
后同样不行。我在 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