UPDATE PostgreSQL table with values from self

correlated-subquery, postgresql, sql, sql-update, window-functions

Solution

Rewrite 2022

I expect your added solution to perform poorly, as it's doing a of of unnecessary work. The following should be much faster.

The question and the added solution do not define which row to pick when there are multiple with the same `dob`. Typically you'll want a deterministic pick. This query picks the alphabetically first name from each group of peers with the same `dob`. Adapt to your needs.

UPDATE person p
SET    younger_sibling_name = y.name
     , younger_sibling_dob  = y.dob
FROM  (
   SELECT dob, name, lead(dob) OVER (ORDER BY dob) AS next_dob
   FROM  (
      SELECT DISTINCT ON (dob)
             dob, name
      FROM   person p
      ORDER  BY dob, name  -- ①
      ) sub
   ) y
WHERE  p.dob = y.next_dob;

db<>fiddle here - with extended test case

Works since at least Postgres 8.4.

Needs an index on `dob` to be fast, ideally a multicolumn index on `(dob, name)`.

Subquery `sub` passes over the whole table once and distills distinct rows per `dob`.

① I added `name` to `ORDER BY` as tiebreaker to pick the row with the alphabetically first name. Adapt to our needs.

In the outer `SELECT` add the next later `dob` (`next_dob`) to each row with `lead()` - simple now with distinct `dob`. Then join to that `next_dob` and the rest is simple.

If no younger person exists, no `UPDATE` happens and the columns stay `NULL`.

About `DISTINCT ON` and possibly faster query techniques for many duplicates:

- Select first row in each GROUP BY group?

- Optimize GROUP BY query to retrieve latest row per user

Taking `dob` and `name` from the same row guarantees we stay in sync. Multiple correlated subqueries would not offer this guarantee, and would be more expensive anyway.

Original answer

Still valid.

Old query 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;

Old sqlfiddle

In the CTE `cte` use the window function `dense_rank()` to get a rank without gaps according to the `dop` for every person.

Join `cte` to itself, but remove duplicates on `dob` from the second instance. Thereby everybody gets exactly one `UPDATE`. If more than one person share the same `dop`, the same one is selected as younger sibling for all persons on the next `dob`. I do this with:

   (SELECT DISTINCT ON (rnk) * FROM cte)

Add `ORDER BY rnk, ...` to this subquery to pick a particular person for every `dob`.

Old query 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;

Old sqlfiddle

This works, because aggregate functions are applied before window functions. And it should be very fast since both operations agree on the sort order.

Obviates the need for a later `DISTINCT` like in query 1.

Result is the same as query 1, exactly. Again, you can add more columns to `ORDER BY` to pick a particular person for every `dob`.

Problem

I am attempting to update multiple columns on a table with values from another row in the same table: ``` 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') ; ``` The task is to populate `younger_sibling_name` and `younger_sibling_dob` with the name and birthday of the person that is closest to them in age, but not older or the same age. I can set the younger sibling `dob` easily because this is the value that determines the record to use with a correlated subquery (I think this is an example of that?): ``` UPDATE person SET younger_sibling_dob = ( SELECT MAX(dob) FROM person AS sibling WHERE sibling.dob < person.dob); ``` I just can't see any way to get the `name`? The real query of this will run over about 1M rows in groups of 100-500 for each MAX selection so performance is a concern. Edit After trying many different approaches, I've decided on this one which I think is a good balance of being able to verify the data with the intermediate result, shows the intention of what the logic is, and performs adequately: ``` 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; ```

Original source

Related problems