除了索引之外,如何在 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 比例风险模型脆弱,如果有人感兴趣的话)。需要发生三件主要的事情:

  1. service_date 需要转换为从 2009-01-01 开始计数的整数 - 换句话说,自 2009 年 1 月 1 日以来的天数。 service_date 需要重命名 service_date_2.
  2. 需要创建一个新列 service_date_1,它需要为每一行包含以下两个内容之一:如果该行是第一行,则单元格应为 0 member_composite_id,或者,如果它不是第一个,它应该包含 service_date_2 的值 member_composite_id 的前一行。
  3. 由于service_date_1service_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.