访问每个单独标识符的最新行的正确方法?

ogr*_*ogr 4 sql postgresql indexing query-optimization greatest-n-per-group

core_message在 Postgres 中有一个表,有数百万行看起来像这样(简化):

??????????????????????????????????????????????????????????????????????????????????????????????????????????????????????
?    Colonne     ?           Type           ? Collationnement ? NULL-able ?                Par défaut                ?
??????????????????????????????????????????????????????????????????????????????????????????????????????????????????????
? id             ? integer                  ?                 ? not null  ? nextval('core_message_id_seq'::regclass) ?
? mmsi           ? integer                  ?                 ? not null  ?                                          ?
? time           ? timestamp with time zone ?                 ? not null  ?                                          ?
? point          ? geography(Point,4326)    ?                 ?           ?                                          ?
??????????????????????????????????????????????????????????????????????????????????????????????????????????????????????
Index:
    "core_message_pkey" PRIMARY KEY, btree (id)
    "core_message_uniq_mmsi_time" UNIQUE CONSTRAINT, btree (mmsi, "time")
    "core_messag_mmsi_b36d69_idx" btree (mmsi, "time" DESC)
    "core_message_point_id" gist (point)
Run Code Online (Sandbox Code Playgroud)

mmsi列是用于识别世界上船舶的唯一标识符。我正在尝试获取每个mmsi.

我可以得到这样的,例如:

SELECT a.* FROM core_message a
JOIN  (SELECT mmsi, max(time) AS time FROM core_message GROUP BY mmsi) b
       ON a.mmsi=b.mmsi and a.time=b.time;
Run Code Online (Sandbox Code Playgroud)

但这太慢了,2秒+。

所以我的解决方案是创建一个不同的表,只包含表的最新行(最多 100K+ 行core_message,称为LatestMessage.

每次必须将新行添加到core_message.

它工作正常,我能够在几毫秒内访问该表。但是我很想知道是否有更好的方法来仅使用一个表来实现这一目标并保持相同级别的数据访问性能。

ogr*_*ogr 6

这是本文中提到的查询的快速性能比较。

当前设置:

该表core_message有 10,904,283 行,有 60,740 行 in test_boats(或 60,740 不同 mmsi in core_message)。

我使用的是 PostgreSQL 11.5

使用仅索引扫描查询:

1)使用DISTINCT ON

SELECT DISTINCT ON (mmsi) mmsi 
FROM core_message;
Run Code Online (Sandbox Code Playgroud)

2)采用RECURSIVELATERAL

WITH RECURSIVE cte AS (
   (
   SELECT mmsi
   FROM   core_message
   ORDER  BY mmsi
   LIMIT  1
   )
   UNION ALL
   SELECT m.*
   FROM   cte c
   CROSS  JOIN LATERAL (
      SELECT mmsi
      FROM   core_message
      WHERE  mmsi > c.mmsi
      ORDER  BY mmsi
      LIMIT  1
      ) m
   )
TABLE cte;
Run Code Online (Sandbox Code Playgroud)

3)使用一个额外的表LATERAL

SELECT a.mmsi
FROM test_boats a
CROSS JOIN LATERAL(
    SELECT b.time
    FROM core_message b
    WHERE a.mmsi = b.mmsi
    ORDER BY b.time DESC
    LIMIT 1
) b;
Run Code Online (Sandbox Code Playgroud)

查询不使用仅索引扫描:

4)使用DISTINCT ON具有mmsi,time DESC INDEX

SELECT DISTINCT ON (mmsi) * 
FROM core_message 
ORDER BY mmsi, time desc;
Run Code Online (Sandbox Code Playgroud)

5)DISTINCT ON与向后使用mmsi,time UNIQUE CONSTRAINT

SELECT DISTINCT ON (mmsi) * 
FROM core_message 
ORDER BY mmsi desc, time desc;
Run Code Online (Sandbox Code Playgroud)

6)RECURSIVELATERAL和一起使用mmsi,time DESC INDEX

WITH RECURSIVE cte AS (
   (
   SELECT *
   FROM   core_message
   ORDER  BY mmsi , time DESC 
   LIMIT  1
   )
   UNION ALL
   SELECT m.*
   FROM   cte c
   CROSS  JOIN LATERAL (
      SELECT *
      FROM   core_message
      WHERE  mmsi > c.mmsi
      ORDER  BY mmsi , time DESC 
      LIMIT  1
      ) m
   )
TABLE cte;
Run Code Online (Sandbox Code Playgroud)

7) 使用RECURSIVEwithLATERAL和 back mmsi,time UNIQUE CONSTRAINT

WITH RECURSIVE cte AS (

   (

   SELECT *
   FROM   core_message
   ORDER  BY mmsi DESC , time DESC 
   LIMIT  1
   )
   UNION ALL
   SELECT m.*
   FROM   cte c
   CROSS  JOIN LATERAL (
      SELECT *
      FROM   core_message
      WHERE  mmsi < c.mmsi
      ORDER  BY mmsi DESC , time DESC 
      LIMIT  1
      ) m
   )
TABLE cte;
Run Code Online (Sandbox Code Playgroud)

8)使用一个额外的表LATERAL

SELECT b.*
FROM test_boats a
CROSS JOIN LATERAL(
    SELECT b.*
    FROM core_message b
    WHERE a.mmsi = b.mmsi
    ORDER BY b.time DESC
    LIMIT 1
) b;
Run Code Online (Sandbox Code Playgroud)

对最后一条消息使用专用表:

9)这是我的初始解决方案,使用仅包含最后一条消息的不同表。该表在新消息到达时填充,但也可以像这样创建:

CREATE TABLE core_shipinfos AS (
    WITH RECURSIVE cte AS (
       (
       SELECT *
       FROM   core_message
       ORDER  BY mmsi DESC , time DESC 
       LIMIT  1
       )
       UNION ALL
       SELECT m.*
       FROM   cte c
       CROSS  JOIN LATERAL (
          SELECT *
          FROM   core_message
          WHERE  mmsi < c.mmsi
          ORDER  BY mmsi DESC , time DESC 
          LIMIT  1
          ) m
       )
    TABLE cte);
Run Code Online (Sandbox Code Playgroud)

那么获取最新消息的请求就这么简单:

SELECT * FROM core_shipinfos;
Run Code Online (Sandbox Code Playgroud)

结果 :

多次查询的平均值(快速查询大约 5 个):

1) 9146 毫秒
2) 728 毫秒
3) 498 毫秒

4) 51488 毫秒
5) 54764 毫秒
6) 729 毫秒
7) 778 毫秒
8) 516 毫秒

9) 15 毫秒

结论:

我不会对专用表解决方案发表评论,并将保留到最后。

附加表 ( test_boats) 解决方案在这里绝对是赢家,但该RECURSIVE解决方案也非常有效。

DISTINCT ON使用仅索引扫描和不使用它的性能存在巨大差距,但对于其他高效查询,性能增益相当小。

这是有道理的,因为这些查询带来的主要改进是,它们不需要遍历整个core_message表,而只需要遍历mmsicore_message表大小 (10M+)相比明显更小 (60K+)的唯一子集

另外要注意的是,使用UNIQUE CONSTRAINTif I drop 的查询的性能似乎没有显着提高mmsi,time DESC INDEX。但是删除该索引当然会为我节省一些空间(该索引目前需要 328MB)

关于专用表解决方案:

存储在core_message表中的每条消息都携带位置信息(位置、速度、航向等)和船舶信息(名称、呼号、尺寸等),以及船舶标识符(mmsi)。

为了提供更多关于我实际尝试做的事情的背景知识:我正在实现一个后端来存储船舶通过AIS 协议发出的消息。

因此,我得到的每一个独特的 mmsi 都是通过这个协议得到的。它不是一个预定义的列表。它不断添加新的 MMSI,直到我让世界上的每艘船都使用 AIS。

在这种情况下,将船舶信息作为收到的最后一条消息的专用表是有意义的。

我可以避免使用我们在RECURSIVE解决方案中看到的这样的表,但是……专用表仍然比这个RECURSIVE解决方案快 50 倍。

该专用表实际上与该test_boat表类似,包含的信息不仅仅是mmsi字段。事实上,拥有一个mmsi只有字段的表或一个包含表的每个最后信息的core_message表都会给我的应用程序增加相同的复杂性。

最后,我想我会选择这张专用桌子。它会给我无与伦比的速度,我仍然有可能在 上使用这个LATERAL技巧core_message,这会给我更多的灵活性。


ogr*_*ogr 3

这个答案似乎与这里的答案相悖DISTINCT ON,但它也提到了这一点:

对于每个客户的许多行(列中的基数较低 customer),松散索引扫描(又名“跳过扫描”)会(更)高效,但这在 Postgres 12 之前还没有实现。(仅索引扫描的实现位于Postgres 13 的开发。请参阅此处此处。)
目前,有更快的查询技术可以替代它。特别是如果您有一个单独的表来保存唯一的客户,这是典型的用例。但如果你不这样做:

使用这个另一个很好的答案,我找到了一种方法来保持与使用不同表相同的性能LATERAL。通过使用新表,test_boats我可以执行以下操作:

 CREATE TABLE test_boats AS (select distinct on (mmsi) mmsi from core_message);
Run Code Online (Sandbox Code Playgroud)

此表创建需要 40 多秒,这与此处其他答案所花费的时间非常相似。

然后,在以下人员的帮助下LATERAL

SELECT a.mmsi, b.time
FROM test_boats a
CROSS JOIN LATERAL(
    SELECT b.time
    FROM core_message b
    WHERE a.mmsi = b.mmsi
    ORDER BY b.time DESC
    LIMIT 1
) b LIMIT 10;
Run Code Online (Sandbox Code Playgroud)

这速度非常快,1 毫秒以上。

这将需要修改我的程序逻辑并使用稍微复杂一点的查询,但我认为我可以忍受。

对于无需创建新表的快速解决方案,请查看下面@ErwinBrandstetter 的答案


更新:我觉得这个问题还没有得到完全解答,因为目前还不清楚为什么提出的其他解决方案在这里表现不佳。

我尝试了这里提到的基准。起初,DISTINCT ON如果您在我的计算机上执行基准测试中提出的请求:+/- 30ms,那么该方法似乎足够快。但这是因为该请求使用仅索引扫描。如果您包含索引中没有的字段,some_column则在基准测试的情况下,性能将下降至 +/- 100 毫秒。

性能还没有大幅下降。这就是为什么我们需要一个具有更大数据集的基准。与我的情况类似:40K 客户和 800 万行。这里

让我们用这个新表再试一次DISTINCT ON

SELECT DISTINCT ON (customer_id) id, customer_id, total 
FROM purchases_more 
ORDER BY customer_id, total DESC, id;
Run Code Online (Sandbox Code Playgroud)

这大约需要 1.5 秒才能完成。

SELECT DISTINCT ON (customer_id) *
FROM purchases_more 
ORDER BY customer_id, total DESC, id;
Run Code Online (Sandbox Code Playgroud)

这大约需要 35 秒才能完成。

现在,回到我上面的第一个解决方案。它使用仅索引扫描和 a LIMIT,这就是它速度极快的原因之一。如果我重新设计该查询以不使用仅索引扫描并转储限制:

SELECT b.*
FROM test_boats a
CROSS JOIN LATERAL(
    SELECT b.*
    FROM core_message b
    WHERE a.mmsi = b.mmsi
    ORDER BY b.time DESC
    LIMIT 1
) b;
Run Code Online (Sandbox Code Playgroud)

这大约需要 500 毫秒,仍然相当快。

有关更深入的基准测试,请参阅下面我的其他答案。