如何在 PostgreSQL 中高效查询版本化 rows/entities?

How do I efficiently query versioned rows/entities in PostgreSQL?

背景

我遇到过将给定实体的所有版本存储在我的 PostgreSQL 数据库中的情况。这是用两个 table 实现的;一个 table 存储实体的主键和 immutable 属性,第二个 table 存储实体的 mutable 属性。两个 tables 都是只插入的(由触发器强制执行)。

例子

这个概念可以很容易地用实体 User 来说明,存储在 useruser_details tables:

Table user:

id  timestamp
1   2018-04-10T12:00:00
2   2018-04-10T12:00:00

Table user_details:

id  user_id   username  first_name   last_name     timestamp
1   1         bob       Bob          Socks         2018-04-10T12:00:01
2   1         bob       Bobby        Socks         2018-04-10T12:00:02
3   2         alice     Alice        Jones         2018-04-10T12:00:03
4   1         bob       Bobbers      Socks         2018-04-10T12:00:04
5   2         alice     Alicia       Jones         2018-04-10T12:00:05

两个 'id' 列都定义为串行主键(严格递增)并且我在 user_details (user_id, id DESC).

上创建了一个索引

1 - 如何高效查询实体的最新版本?

给定一个用户 ID,我需要一种快速的方法来获取 user 中的 immutable 数据和 user_details 中的最新条目。哪种查询最适合此联接?

2 - 如何有效查询实体的版本 n 和 n-1?

我通过首先获取 XY 之间带有 timestamp 的所有行来生成时间间隔的审计日志,然后我获取插入的行及其前身(相同 user_id,最近的较低 id)并从中产生差异。在 XY 之间插入的行数通常很高,因此我需要有效地获取当前 + 先前对,即给定输入 user_details(5),我需要 select 连接 user(2) + user_details(5)user(2) + user_details(3)。哪种查询最适合此联接?

徒劳的尝试

到目前为止,我最好的结果是这些查询:

问题 1 的查询:

SELECT *
FROM "user" u
JOIN LATERAL (SELECT *
              FROM "user_details" ud
              WHERE u.id = ud.user_id
              ORDER BY id DESC
              LIMIT 1
       ) detail ON TRUE
WHERE u.id IN
      (...);

问题 2 的查询:

SELECT *
FROM "user" u
JOIN LATERAL (SELECT *
              FROM "user_details" ud
              WHERE u.id = ud.user_id
              AND ud.id IN (...)
              ORDER BY id DESC
              LIMIT 2) ud ON TRUE;

然而,这两个查询最终都使用嵌套循环(从EXPLAIN ANALYZE看到)并且当运行具有大量ID(5000+)时需要很长时间才能完成。

想法

我可以巧妙地使用 user_details (user_id, id DESC) 索引来首先创建我需要的 user_details id 的 CTE,然后在此基础上加入 user + user_details 吗?我可以创建某种功能索引吗?我是否需要在 user_details(或另一个 table)中维护一个 predecessor 列是否能够有效地查找这种类型的关系?

谢谢!

SQL Fiddle: http://www.sqlfiddle.com/#!17/5f5f0

解决方案

感谢 X 和 Y 将我推向正确的方向!我最终使用了@MichelMilezzi 为我的第一个问题建议的解决方案和为我的第二个问题改编的@RadimBača 解决方案:

WITH
cte_1 AS (SELECT id, user_id FROM "user_details" WHERE id IN (8999, 9999)),
cte_2 as (SELECT cte_1.id, cte_1.user_id, prev.id AS prev_id, row_number() OVER (PARTITION BY cte_1.id, cte_1.user_id ORDER BY prev.id DESC) AS rownum FROM "user_details" prev, cte_1 WHERE prev.user_id = cte_1.user_id AND prev.id < cte_1.id)
SELECT main.*, detail.*, cte_2.id AS __id, (detail.id <> cte_2.id) AS __is_predecessor FROM "user" main, "user_details" detail, cte_2
WHERE main.id = cte_2.user_id AND cte_2.rownum = 1 AND (detail.id = cte_2.id OR detail.id = cte_2.prev_id);

考虑使用 window 函数

SELECT *
FROM "user" u
JOIN
(
    SELECT row_number() over(partition by user_id order by id) rn,
           *
    FROM "user_details" ud
) t ON t.user_id = u.id
WHERE t.rn = 1

DEMO

此解决方案还允许您查询每组的所有 N 行或每组 N-th 行。

您可以使用 DISTINCT ON 检索用户的最新版本,如下所示:

SELECT 
    DISTINCT ON (u.id) 
    *
FROM
    "user" u
    JOIN user_details d ON (u.id = d.user_id)
WHERE
    d.id IN (100, 200, 300, 400, 500, 600, 700, 800, 900, 1000) 
ORDER BY 
    u.id,
    d.id DESC

引自docs

SELECT DISTINCT ON ( expression [, ...] ) keeps only the first row of each set of rows where the given expressions evaluate to equal. The DISTINCT ON expressions are interpreted using the same rules as for ORDER BY (see above). Note that the “first row” of each set is unpredictable unless ORDER BY is used to ensure that the desired row appears first.

Sql fiddle here.

要获得旧版本,您可以使用 @Radim 指出的 window function