【问题标题】:UPDATE PostgreSQL table with values from self使用来自 self 的值更新 PostgreSQL 表
【发布时间】:2013-03-08 20:48:04
【问题描述】:

我正在尝试使用同一表中另一行的值更新表上的 多个列

CREATE TEMP TABLE person (
  pid INT
 ,name VARCHAR(40)
 ,dob DATE
 ,younger_sibling_name VARCHAR(40)
 ,younger_sibling_dob DATE
);

INSERT INTO person VALUES (pid, name, dob)
(1, 'John', '1980-01-05'),
(2, 'Jimmy', '1975-04-25'),
(3, 'Sarah', '2004-02-10'),
(4, 'Frank', '1934-12-12');

任务是在younger_sibling_nameyounger_sibling_dob 中填入与他们年龄最接近但不比他们大或同龄的人的姓名和出生日期。

我可以轻松设置弟弟dob,因为这是确定要与相关子查询一起使用的记录的值(我认为这是一个例子?):

UPDATE person SET younger_sibling_dob=(
SELECT MAX(dob)
FROM person AS sibling
WHERE sibling.dob < person.dob);

我只是看不到任何获取name的方法?
对于每个 MAX 选择,实际查询将运行大约 1M 记录,每组 100-500 条记录,因此性能是一个问题。

编辑:

在尝试了许多不同的方法后,我决定采用我认为不错的方法 能够用中间结果验证数据的平衡,表明 逻辑的意图,并充分执行:

WITH sibling AS (
  SELECT person.pid, sibling.dob, sibling.name,
         row_number() OVER (PARTITION BY person.pid
                            ORDER BY sibling.dob DESC) AS age_closeness
  FROM person
  JOIN person AS sibling ON sibling.dob < person.dob
)
UPDATE person
  SET younger_sibling_name = sibling.name
     ,younger_sibling_dob  = sibling.dob
FROM sibling
WHERE person.pid = sibling.pid
   AND sibling.age_closeness = 1;

SELECT * FROM person ORDER BY dob;

【问题讨论】:

  • 顺便说一句:如果您对性能如此着迷,您可以存储 sibling_id REFERENCES person(pid) 而不是重复的 {sibling_name,sibling_dob}

标签: sql postgresql sql-update correlated-subquery window-functions


【解决方案1】:

1) 找到 MAX() 总是可以重写为 NOT EXISTS (...)

UPDATE person dst
SET younger_sibling_name = src.name
        ,younger_sibling_dob = src.dob
FROM person src
WHERE src.dob < dst.dob
   OR src.dob = dst.dob AND src.pid < dst.pid
AND NOT EXISTS (
        SELECT * FROM person nx
        WHERE nx.dob < dst.dob
           OR nx.dob = dst.dob AND nx.pid < dst.pid
        AND nx.dob > src.dob
           OR nx.dob = src.dob AND nx.pid > src.pid
        );

2) 除了 rank() / row_number(),您还可以在 WINDOW 上使用 LAG() 函数:

UPDATE person dst
SET younger_sibling_name = src.name
        ,younger_sibling_dob = src.dob
FROM    (
        SELECT pid
        , LAG(name) OVER win AS name
        , LAG(dob) OVER win AS dob 
        FROM person
        WINDOW win AS (ORDER BY dob, pid)
        ) src
WHERE src.pid = dst.pid
        ;

两个版本都需要自连接子查询(或 CTE),因为 UPDATE 不允许窗口函数。

【讨论】:

    【解决方案2】:

    相关子查询因性能不佳而臭名昭著。对小桌子没关系,对大桌子很重要很多。改用其中之一,最好是第二个

    查询 1

    WITH cte AS (
       SELECT *, dense_rank() OVER (ORDER BY dob) AS drk
       FROM   person
        )
    UPDATE person p
    SET    younger_sibling_name = y.name
          ,younger_sibling_dob  = y.dob
    FROM   cte x
    JOIN   (SELECT DISTINCT ON (drk) * FROM cte) y ON y.drk = x.drk + 1
    WHERE  x.pid = p.pid;
    

    -> SQLfiddle (with extended test case)

    • CTE cte 中使用窗口函数dense_rank() 根据dop 为每个人获得一个没有差距的排名。

    • cte 加入自身,但从第二个实例中删除dob 上的重复项。因此,每个人都会得到一个UPDATE。如果多个人共享同一个dop同一个将被选为下一个dob 上所有人的弟弟。我这样做:

      (SELECT DISTINCT ON (rnk) * FROM cte)
      

      如果您想为每个 dob 选择一个特定的人,请添加 ORDER BY rnk, ...

    • 如果没有年轻人,则不会发生UPDATE,并且列将保持NULL

    • dobpid 上的索引加快了速度。

    查询 2

    WITH cte AS (
       SELECT dob, min(name) AS name
             ,row_number() OVER (ORDER BY dob) rn
       FROM   person p
       GROUP  BY dob
       )
    UPDATE person p
    SET    younger_sibling_name = y.name
          ,younger_sibling_dob  = y.dob
    FROM   cte x
    JOIN   cte y ON y.rn = x.rn + 1
    WHERE  x.dob = p.dob;
    

    -> SQLfiddle

    • 这可行,因为聚合函数应用在之前窗口函数。而且它应该非常快,因为两个操作都同意排序顺序。

    • 无需像查询 1 中那样稍后使用 DISTINCT

    • 结果与查询 1 完全相同。
      同样,您可以向 ORDER BY 添加更多列,以便为每个 dob 选择一个特定的人。

    • 只需要dob 上的索引即可。

    【讨论】:

    • 感谢这个替代实现。我缺乏关于窗口函数的知识使得这种方法非常有趣。我将对真实数据集进行一些示例查询,以了解这一点。
    • @Jeremy:重新访问时,我想我找到了一个更快的解决方案。
    • 虽然我的工作还没有完成,但这已经为我指明了正确的方向。为了帮助维护,我选择在 SELECT CTE 中进行简单的窗口函数计算,然后删除与我正在寻找的条件(排名、超出范围的最小值等)不匹配的记录。虽然没有你的方法那么快,但我会在 6 个月后更好地理解它,并且暂时足够快。
    • 顺便说一句:CTE 形成了计划障碍(它们永远不会与主查询合并/合并)如果可以通过合并主查询与其子查询来实现更好的计划,这可能会降低性能。 (例如我的max() as NOT EXISTS() 或joined subselect。)两者都将(给定正确的索引)只需要对表进行一次索引/有序扫描,而不是两次。 YMMV。
    【解决方案3】:

    要获取出生日期和姓名,您可以:

    update person
        set younger_sibling_dob = (select dob
                                   from person p2
                                   where s.dob < person.dob
                                   order by dob desc
                                   limit 1),
           younger_sibling_name = (select name
                                   from person p2
                                   where s.dob < person.dob
                                   order by dob desc
                                   limit 1)
    

    如果您在dob 上有索引,那么查询将运行得更快。

    【讨论】:

    • 通过使用update person from (...) 可以将两个子选择组合成一个,这可能更有效。
    • @a_horse_with_no_name:对。只是比这复杂一点,因为相关的子查询在功能上依赖于当前行。
    • 感谢您的方法。实际上,我确实在发布之前的测试中尝试过这个。不幸的是,查询的成本如此之高,以至于我从未完成执行以验证结果。
    猜你喜欢
    • 1970-01-01
    • 2018-11-22
    • 2021-12-27
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-04-23
    • 1970-01-01
    相关资源
    最近更新 更多