sqlpostgresqlcommon-table-expressionupsertsql-merge

How to avoid "ON CONFLICT DO UPDATE command cannot affect row a second time" error in WITH statement


I have two tables: demo at db<>fiddle

  1. Table keywords has two columns id and v (which hold the keyword's value)
    create table keywords(
      id int generated always as identity primary key
     ,v text unique);
    
  2. Table main has, among others, two columns that are each a foreign key into the keywords table's id, named key1_id and key2_id
    create table main(
      id int generated always as identity primary key
     ,key1_id int references keywords(id)
     ,key2_id int references keywords(id));
    

Now, I want to insert pairs of keywords (key1 and key2 as $1 and $2) into the main table, like this:

WITH key1 AS (INSERT INTO keywords (v) 
              VALUES ($1) 
              ON CONFLICT (v) DO UPDATE
              SET v = EXCLUDED.v
              RETURNING id),
     key2 AS (INSERT INTO keywords (v) 
              VALUES ($2) 
              ON CONFLICT (v) DO UPDATE
              SET v = EXCLUDED.v
              RETURNING id)
INSERT INTO main (key1_id, key2_id)
SELECT key1.id, key2.id
FROM key1, key2
RETURNING id

Basically, the two keys often have recurring values, so I use the keywords table to keep a unique set of them (mostly for storage optimization, as I have millions of rows with them).

But if $1 and $2 have identical values, I get this error, whereas there's no issue if they're different:

pg_query_params(): Query failed: ERROR: ON CONFLICT DO UPDATE command cannot affect row a second time
HINT: Ensure that no rows proposed for insertion within the same command have duplicate constrained values.

The goal is that both key1_id and key2_id point to the correct row in keywords based on the values passed as $1 and $2, even if they're both the same.

How do I modify the SQL statement (ideally, it should remain a single one) so that I can insert these keys without getting the error?

I am using Postgresql 16.9.


Solution

  • You can do exactly what the hint suggests:

    Ensure that no rows proposed for insertion within the same command have duplicate constrained values.

    Reduced to a single insert, with a union making sure duplicate insert never happens and coalesce() just repeating key1.id in that case:
    demo at db<>fiddle

    WITH keys AS (INSERT INTO keywords (v) 
                  SELECT $1
                  UNION--deduplicates
                  SELECT $2
                  ON CONFLICT (v) DO UPDATE
                  SET v = EXCLUDED.v
                  RETURNING id)
    INSERT INTO main ( key1_id, key2_id )
    SELECT min(id), max(id)
    FROM keys
    RETURNING id;