除了索引之外,如何在 PostgreSQL 中加快对 100m 行的查询?
Besides indexing, how to speed up this query on 100m rows in PostgreSQL?
背景
首先,让我知道这是否更适合 DBA StackExchange。很高兴把它移到那里。
我有一个数据集,db1_dummy
有大约 1 亿行汽车和摩托车保险索赔,我正准备进行统计分析。它在 PostgreSQL v13 中,我在本地 64 位 Windows 机器上 运行ning 并通过 DataGrip 访问。 db1_dummy
有 ~15 个变量,但对于这个问题只有 3 个相关。这是数据集的玩具版本:
+-------------------+------------+--+
|member_composite_id|service_date|id|
+-------------------+------------+--+
|eof81j4 |2010-01-12 |1 |
|eof81j4 |2010-06-03 |2 |
|eof81j4 |2011-01-06 |3 |
|eof81j4 |2011-05-21 |4 |
|j42roit |2015-11-29 |5 |
|j42roit |2015-11-29 |6 |
|j42roit |2015-11-29 |7 |
|p8ur0fq |2014-01-13 |8 |
|p8ur0fq |2014-01-13 |9 |
|p8ur0fq |2016-04-04 |10|
|vplhbun |2019-08-15 |11|
|vplhbun |2019-08-15 |12|
|vplhbun |2019-08-15 |13|
|akj3vie |2009-03-31 |14|
+-------------------+------------+--+
id
是唯一的(一个主键),正如你所看到的 member_composite_id
标识保单持有人并且可以有多个条目(一个保险保单持有人可以有多个索赔)。 service_date
只是保单持有人的车辆接受保险索赔服务的日期。
我需要将数据转换成某种格式以便 运行 我的分析,所有这些都是 R
中基于回归的生存分析实现(具有共享的 Cox 比例风险模型脆弱,如果有人感兴趣的话)。需要发生三件主要的事情:
service_date
需要转换为从 2009-01-01 开始计数的整数 - 换句话说,自 2009 年 1 月 1 日以来的天数。 service_date
需要重命名 service_date_2
.
- 需要创建一个新列
service_date_1
,它需要为每一行包含以下两个内容之一:如果该行是第一行,则单元格应为 0 member_composite_id
,或者,如果它不是第一个,它应该包含 service_date_2
的值 member_composite_id
的前一行。
- 由于
service_date_1
和service_date_2
之间的间隔(差)不能为零,在这种情况下应该从service_date_1
中减去少量(0.1)。
这听起来可能令人困惑,所以让我来展示一下。这是我需要的数据集:
+--+-------------------+--------------+--------------+
|id|member_composite_id|service_date_1|service_date_2|
+--+-------------------+--------------+--------------+
|1 |eof81j4 |0 |376 |
|2 |eof81j4 |376 |518 |
|3 |eof81j4 |518 |735 |
|4 |eof81j4 |735 |870 |
|5 |j42roit |0 |2523 |
|6 |j42roit |2522.9 |2523 |
|7 |j42roit |2522.9 |2523 |
|8 |p8ur0fq |0 |1838 |
|9 |p8ur0fq |1837.9 |1838 |
|10|p8ur0fq |1838 |2650 |
|11|vplhbun |0 |3878 |
|12|vplhbun |3877.9 |3878 |
|13|vplhbun |3877.9 |3878 |
|14|akj3vie |0 |89 |
+--+-------------------+--------------+--------------+
好消息:我有一个可以执行此操作的查询——事实上,这个查询输出了上面的输出。这是查询:
CREATE TABLE db1_dummy_2 AS
SELECT
d1.id
, d1.member_composite_id
,
CASE
WHEN (COALESCE(MAX(d2.service_date)::TEXT,'') = '') THEN 0
WHEN (MAX(d2.service_date) - '2009-01-01'::DATE = d1.service_date - '2009-01-01'::DATE) THEN d1.service_date - '2009-01-01'::DATE - 0.1
ELSE MAX(d2.service_date) - '2009-01-01'::DATE
END service_date_1
, d1.service_date - '2009-01-01'::DATE service_date_2
FROM db1_dummy d1
LEFT JOIN db1_dummy d2
ON d2.member_composite_id = d1.member_composite_id
AND d2.service_date <= d1.service_date
AND d2.id < d1.id
GROUP BY
d1.id
, d1.member_composite_id
, d1.service_date
ORDER BY
d1.id;
问题
坏消息是,虽然此查询 运行 在我在这里为大家提供的虚拟数据集上非常快速,但在约 1 亿行的“真实”数据集上却需要很长时间。我已经等了 9.5 个小时才让这东西完成工作,但运气为零。
我的问题主要是:有没有更快的方法来完成我要求 Postgres 做的事情?
我试过的
无论如何我都不是数据库天才,所以我在这里想到的最好办法是索引查询中使用的变量:
create index index_member_comp_id on db1_dummy(member_composite_id)
等等 id
也是如此。但它似乎并没有在时间上产生影响。我不确定如何在 Postgres 中对代码进行基准测试,但如果我在 10 小时后无法获得对 运行 的查询,这就有点没有意义了。我还考虑过修剪数据集中的一些变量(我不需要分析的变量),但这只会让我从 ~15 列减少到 ~11。
我在上面的查询中得到了外部帮助,但他们(目前)也不确定如何解决这个问题。所以我决定看看 SO 上的研究人员是否有任何想法。在此先感谢您的帮助。
编辑
根据 Laurenz 的要求,这里是 EXPLAIN
的输出,基于我在此处为您提供的查询版本:
+-------------------------------------------------------------------------------------+
|QUERY PLAN |
+-------------------------------------------------------------------------------------+
|GroupAggregate (cost=2.98..3.72 rows=14 width=76) |
| Group Key: d1.id |
| -> Sort (cost=2.98..3.02 rows=14 width=44) |
| Sort Key: d1.id |
| -> Hash Left Join (cost=1.32..2.72 rows=14 width=44) |
| Hash Cond: (d1.member_composite_id = d2.member_composite_id) |
| Join Filter: ((d2.service_date <= d1.service_date) AND (d2.id < d1.id))|
| -> Seq Scan on db1_dummy d1 (cost=0.00..1.14 rows=14 width=40) |
| -> Hash (cost=1.14..1.14 rows=14 width=40) |
| -> Seq Scan on db1_dummy d2 (cost=0.00..1.14 rows=14 width=40) |
+-------------------------------------------------------------------------------------+
您的查询是真正的服务器杀手(*)。使用window函数lag()
.
select
id,
member_composite_id,
case service_date_1
when service_date_2 then service_date_1- .1
else service_date_1
end as service_date_1,
service_date_2
from (
select
id,
member_composite_id,
lag(service_date, 1, '2009-01-01') over w - '2009-01-01' as service_date_1,
service_date - '2009-01-01' as service_date_2
from db1_dummy
window w as (partition by member_composite_id order by id)
) main_query
order by id
在运行查询之前创建索引
create index on db1_dummy(member_composite_id, id)
阅读文档:
(*) 查询为每个 member_composite_id
生成几个额外的记录。在最坏的情况下,这是笛卡尔积的一半。所以在服务器可以分组和计算聚合之前,它必须创建大约几亿行。我的笔记本电脑无法忍受,服务器 运行 在具有一百万行的 table 上内存不足。自连接总是可疑的,尤其是在大型 tables.
上
背景
首先,让我知道这是否更适合 DBA StackExchange。很高兴把它移到那里。
我有一个数据集,db1_dummy
有大约 1 亿行汽车和摩托车保险索赔,我正准备进行统计分析。它在 PostgreSQL v13 中,我在本地 64 位 Windows 机器上 运行ning 并通过 DataGrip 访问。 db1_dummy
有 ~15 个变量,但对于这个问题只有 3 个相关。这是数据集的玩具版本:
+-------------------+------------+--+
|member_composite_id|service_date|id|
+-------------------+------------+--+
|eof81j4 |2010-01-12 |1 |
|eof81j4 |2010-06-03 |2 |
|eof81j4 |2011-01-06 |3 |
|eof81j4 |2011-05-21 |4 |
|j42roit |2015-11-29 |5 |
|j42roit |2015-11-29 |6 |
|j42roit |2015-11-29 |7 |
|p8ur0fq |2014-01-13 |8 |
|p8ur0fq |2014-01-13 |9 |
|p8ur0fq |2016-04-04 |10|
|vplhbun |2019-08-15 |11|
|vplhbun |2019-08-15 |12|
|vplhbun |2019-08-15 |13|
|akj3vie |2009-03-31 |14|
+-------------------+------------+--+
id
是唯一的(一个主键),正如你所看到的 member_composite_id
标识保单持有人并且可以有多个条目(一个保险保单持有人可以有多个索赔)。 service_date
只是保单持有人的车辆接受保险索赔服务的日期。
我需要将数据转换成某种格式以便 运行 我的分析,所有这些都是 R
中基于回归的生存分析实现(具有共享的 Cox 比例风险模型脆弱,如果有人感兴趣的话)。需要发生三件主要的事情:
service_date
需要转换为从 2009-01-01 开始计数的整数 - 换句话说,自 2009 年 1 月 1 日以来的天数。service_date
需要重命名service_date_2
.- 需要创建一个新列
service_date_1
,它需要为每一行包含以下两个内容之一:如果该行是第一行,则单元格应为 0member_composite_id
,或者,如果它不是第一个,它应该包含service_date_2
的值member_composite_id
的前一行。 - 由于
service_date_1
和service_date_2
之间的间隔(差)不能为零,在这种情况下应该从service_date_1
中减去少量(0.1)。
这听起来可能令人困惑,所以让我来展示一下。这是我需要的数据集:
+--+-------------------+--------------+--------------+
|id|member_composite_id|service_date_1|service_date_2|
+--+-------------------+--------------+--------------+
|1 |eof81j4 |0 |376 |
|2 |eof81j4 |376 |518 |
|3 |eof81j4 |518 |735 |
|4 |eof81j4 |735 |870 |
|5 |j42roit |0 |2523 |
|6 |j42roit |2522.9 |2523 |
|7 |j42roit |2522.9 |2523 |
|8 |p8ur0fq |0 |1838 |
|9 |p8ur0fq |1837.9 |1838 |
|10|p8ur0fq |1838 |2650 |
|11|vplhbun |0 |3878 |
|12|vplhbun |3877.9 |3878 |
|13|vplhbun |3877.9 |3878 |
|14|akj3vie |0 |89 |
+--+-------------------+--------------+--------------+
好消息:我有一个可以执行此操作的查询——事实上,这个查询输出了上面的输出。这是查询:
CREATE TABLE db1_dummy_2 AS
SELECT
d1.id
, d1.member_composite_id
,
CASE
WHEN (COALESCE(MAX(d2.service_date)::TEXT,'') = '') THEN 0
WHEN (MAX(d2.service_date) - '2009-01-01'::DATE = d1.service_date - '2009-01-01'::DATE) THEN d1.service_date - '2009-01-01'::DATE - 0.1
ELSE MAX(d2.service_date) - '2009-01-01'::DATE
END service_date_1
, d1.service_date - '2009-01-01'::DATE service_date_2
FROM db1_dummy d1
LEFT JOIN db1_dummy d2
ON d2.member_composite_id = d1.member_composite_id
AND d2.service_date <= d1.service_date
AND d2.id < d1.id
GROUP BY
d1.id
, d1.member_composite_id
, d1.service_date
ORDER BY
d1.id;
问题
坏消息是,虽然此查询 运行 在我在这里为大家提供的虚拟数据集上非常快速,但在约 1 亿行的“真实”数据集上却需要很长时间。我已经等了 9.5 个小时才让这东西完成工作,但运气为零。
我的问题主要是:有没有更快的方法来完成我要求 Postgres 做的事情?
我试过的
无论如何我都不是数据库天才,所以我在这里想到的最好办法是索引查询中使用的变量:
create index index_member_comp_id on db1_dummy(member_composite_id)
等等 id
也是如此。但它似乎并没有在时间上产生影响。我不确定如何在 Postgres 中对代码进行基准测试,但如果我在 10 小时后无法获得对 运行 的查询,这就有点没有意义了。我还考虑过修剪数据集中的一些变量(我不需要分析的变量),但这只会让我从 ~15 列减少到 ~11。
我在上面的查询中得到了外部帮助,但他们(目前)也不确定如何解决这个问题。所以我决定看看 SO 上的研究人员是否有任何想法。在此先感谢您的帮助。
编辑
根据 Laurenz 的要求,这里是 EXPLAIN
的输出,基于我在此处为您提供的查询版本:
+-------------------------------------------------------------------------------------+
|QUERY PLAN |
+-------------------------------------------------------------------------------------+
|GroupAggregate (cost=2.98..3.72 rows=14 width=76) |
| Group Key: d1.id |
| -> Sort (cost=2.98..3.02 rows=14 width=44) |
| Sort Key: d1.id |
| -> Hash Left Join (cost=1.32..2.72 rows=14 width=44) |
| Hash Cond: (d1.member_composite_id = d2.member_composite_id) |
| Join Filter: ((d2.service_date <= d1.service_date) AND (d2.id < d1.id))|
| -> Seq Scan on db1_dummy d1 (cost=0.00..1.14 rows=14 width=40) |
| -> Hash (cost=1.14..1.14 rows=14 width=40) |
| -> Seq Scan on db1_dummy d2 (cost=0.00..1.14 rows=14 width=40) |
+-------------------------------------------------------------------------------------+
您的查询是真正的服务器杀手(*)。使用window函数lag()
.
select
id,
member_composite_id,
case service_date_1
when service_date_2 then service_date_1- .1
else service_date_1
end as service_date_1,
service_date_2
from (
select
id,
member_composite_id,
lag(service_date, 1, '2009-01-01') over w - '2009-01-01' as service_date_1,
service_date - '2009-01-01' as service_date_2
from db1_dummy
window w as (partition by member_composite_id order by id)
) main_query
order by id
在运行查询之前创建索引
create index on db1_dummy(member_composite_id, id)
阅读文档:
(*) 查询为每个 member_composite_id
生成几个额外的记录。在最坏的情况下,这是笛卡尔积的一半。所以在服务器可以分组和计算聚合之前,它必须创建大约几亿行。我的笔记本电脑无法忍受,服务器 运行 在具有一百万行的 table 上内存不足。自连接总是可疑的,尤其是在大型 tables.