sql - 使用主键连接大表

标签 sql postgresql join

mydat 表有大约 48.3M 条定义的记录:

┌────────────────┬──────────────┬───────────┐
│     Column     │     Type     │ Modifiers │
├────────────────┼──────────────┼───────────┤
│ id             │ bigint       │ not null  │
│ dt             │ integer      │ not null  │
│ data           │ real         │           │
└────────────────┴──────────────┴───────────┘
Indexes:
    "mydat_pkey" PRIMARY KEY, btree (id, dt)

对于每个由 id 标识的对象,大约有 40 条记录,由 dt 时间字段表示。目标是检查连续记录之间的变化模式,实现是根据每个 iddt 将每个记录与其下一个记录连接起来。查询如下:

SELECT *
  FROM mydat AS dat1
  JOIN mydat AS dat2
    ON dat1.id = dat2.id
   AND dat1.dt = dat2.dt - 1;

查询计划如下。 Merge-Join 被使用了,它运行了很长时间。此外,我们还可以看到行数 427198811 被严重高估了。似乎 postgresql 没有考虑 (id,dt) 的唯一性。

┌───────────────────────────────────────────────────────────────────────────────────────────────┐
│                                          QUERY PLAN                                           │
├───────────────────────────────────────────────────────────────────────────────────────────────┤
│ Merge Join  (cost=19919125.46..25466155.03 rows=247144681 width=222)                          │
│   Merge Cond: ((dat1.id = dat2.id) AND (dat1.dt = ((dat2.dt - 1))))                           │
│   ->  Sort  (cost=9959562.73..10080389.92 rows=48330876 width=111)                            │
│         Sort Key: dat1.id, dat1.dt                                                            │
│         ->  Seq Scan on mydat dat1  (cost=0.00..982694.76 rows=48330876 width=111)            │
│   ->  Materialize  (cost=9959562.73..10201217.11 rows=48330876 width=111)                     │
│         ->  Sort  (cost=9959562.73..10080389.92 rows=48330876 width=111)                      │
│               Sort Key: dat2.id, ((dat2.dt - 1))                                              │
│               ->  Seq Scan on mydat dat2  (cost=0.00..982694.76 rows=48330876 width=111)      │
└───────────────────────────────────────────────────────────────────────────────────────────────┘

出于好奇,这是一个简单的 mydat 与自身的连接:

SELECT *
  FROM mydat AS dat1
  JOIN mydat AS dat2
    ON dat1.id = dat2.id
   AND dat1.dt = dat2.dt;

查询计划类似:

┌───────────────────────────────────────────────────────────────────────────────────────────────┐
│                                          QUERY PLAN                                           │
├───────────────────────────────────────────────────────────────────────────────────────────────┤
│ Merge Join  (cost=19919125.46..27878413.41 rows=427198811 width=222)                          │
│   Merge Cond: ((dat1.id = dat2.id) AND (dat1.dt = dat2.dt))                                   │
│   ->  Sort  (cost=9959562.73..10080389.92 rows=48330876 width=111)                            │
│         Sort Key: dat1.id, dat1.dt                                                            │
│         ->  Seq Scan on act_2003q1 dat1  (cost=0.00..982694.76 rows=48330876 width=111)       │
│   ->  Materialize  (cost=9959562.73..10201217.11 rows=48330876 width=111)                     │
│         ->  Sort  (cost=9959562.73..10080389.92 rows=48330876 width=111)                      │
│               Sort Key: dat2.id, dat2.dt                                                      │
│               ->  Seq Scan on act_2003q1 dat2  (cost=0.00..982694.76 rows=48330876 width=111) │
└───────────────────────────────────────────────────────────────────────────────────────────────┘

这样的查询计划让我百思不得其解。在这里,我的问题是这些用例的最佳实践是什么?谢谢。

以上查询在 Windows 上的 Postgresql 9.5.3 和 Linux 上的 9.4.6 上进行了测试:结果相似。


根据@Erwin 的建议,测试了窗口函数的延迟,结果比最初的合并连接方法好得多:511524ms 完成查询。正如 Erwin 指出的那样,查询与原始查询并不完全相同。特别是如果 dt 字段中存在空白,那么一些记录将是不需要的。

这是一个示例,我发现表分区是有益的,因为我使用的数据集比上面给出的示例大。问题的底线是 postgresql 使用磁盘对所有记录进行排序,并且对于这两个查询根本不使用索引。

最佳答案

如果您的实际目标是在每个结果行中保留 data 的先前值,则使用 window function :

SELECT *, lag(data) OVER (PARTITION BY id ORDER BY dt) AS last_data
FROM   mydat;

区别:没有前导的行仍然包括在内。你可能想要也可能不想要那个。要排除,请使用子查询:

SELECT *
FROM (
   SELECT *, lag(data) OVER (PARTITION BY id ORDER BY dt) AS last_data
   FROM   mydat
   ) t
WHERE  last_data IS NOT NULL;

剩下的极端情况是这样的:如果 data 可以为 NULL,我们无法区分真正的 NULL 值和“未找到”。因此,为“未找到”情况使用不同的不可能默认值,如下所示:

SELECT *
FROM (
   SELECT *, lag(data, 1, '-infinity') OVER (PARTITION BY id ORDER BY dt) AS last_data
   FROM   mydat
   ) t
WHERE  last_data IS DISTINCT FROM '-infinity';

这些查询中的每一个都只需要单个顺序扫描

关于sql - 使用主键连接大表,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/38646283/

相关文章:

postgresql - 没有唯一约束与引用表的给定键匹配

postgresql - 如何按计划更新Elasticsearch中的数据?

SQLite 按另一个表中的条件查询顺序?

java - 我应该在新应用程序中使用 Postgres 的 bigserial 记录吗?

sql - 在 sql 中选择将表与其自身连接的不同对

sql - 游标的替代品

java - 带有 Hibernate 的 Postgres jsonb

mysql - 连接 3 个表 MySQL

postgresql - 如何在 PostgreSql 中连接 2 个表而不重复值

用于在 derby 数据库中限制记录组的 sql