如何在PostgreSQL中有效地查询版本化的行/实体?

时间:2018-04-11 12:59:53

标签: sql postgresql greatest-n-per-group audit

背景

我的情况是我在PostgreSQL数据库中存储给定实体的所有版本。这是用两个表实现的;一个表存储实体的主键和不可变属性,另一个表存储实体的可变属性。两个表都是仅插入的(由触发器强制执行)。

实施例

可以使用存储在Useruser表中的实体user_details轻松说明该概念:

user

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

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中的不可变数据以及user_details中的最新条目。哪种查询最适合此加入?

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

我通过首先在 X Y 之间获取timestamp的所有行来生成时间间隔的审核日志,然后我获取插入的行及其前身(相同的user_id,最接近的id)并从这些中产生差异。在 X Y 之间插入的行数通常很高,因此我需要有效地获取当前+先前的对,即给定输入user_details(5),我需要选择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?我可以创建某种功能索引吗?我是否需要在predecessor(或其他表格)中维护user_details列才能有效地查找此类型的关系?

谢谢!

SQL小提琴: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);

2 个答案:

答案 0 :(得分:1)

您可以使用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(表达式[,...])仅保留第一行   给定表达式求值等于的每组行。该   DISTINCT ON表达式使用与之相同的规则进行解释   ORDER BY(见上文)。请注意,每组的“第一行”是   不可预测,除非使用ORDER BY来确保所需的行   首先出现。

Sql fiddle here

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

答案 1 :(得分:0)

考虑使用窗口函数

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行。