how can I translate a query to CTE? - postgresql

I'm still having trouble understanding how CTE works.
I'm looking to make an insert. In case of conflict I use the on conflict do nothing but I want it to return the id to me (for the success of the insert or the conflict)
WITH inserted AS (
INSERT INTO fiche(label)
VALUES ('label')
ON CONFLICT (label) DO NOTHING
RETURNING *
)
SELECT * FROM inserted
WHERE NOT EXISTS (SELECT 1 FROM inserted);

Note that
SELECT * FROM some_relation
WHERE NOT EXISTS (SELECT 1 FROM some_relation);
will always give you an empty result. Either some_relation is empty itself or if it is not empty SELECT 1 FROM some_relation is not empty and therefore NOT EXISTS ... always returns false and so no record is matching the WHERE clause.
What you want is to have the VALUES as a CTE. You can then reference the values from your INSERT statement and in a SELECT to compare those values to the result of the RETURNING clause.
WITH
vals AS (
VALUES ('label')
),
inserted AS (
INSERT INTO fiche(label)
SELECT * FROM vals
ON CONFLICT (label) DO NOTHING
RETURNING label, id
)
SELECT
vals.column1,
inserted.id
FROM vals
LEFT JOIN inserted ON vals.column1 = inserted.label
This should give you a row for each row in your VALUES clause and the second column will be NULL if it was not inserted due to a conflict or the inserted ID otherwise.

Related

Output Inserted.id equivalent in Postgres

I am new to PostgreSQL and trying to convert mssql scripts to Postgres.
For Merge statement, we can use insert on conflict update or do nothing but am using the below statement, not sure whether it is the correct way.
MSSQL code:
Declare #tab2(New_Id int not null, Old_Id int not null)
MERGE Tab1 as Target
USING (select * from Tab1
WHERE ColumnId = #ID) as Source on 0 = 1
when not matched by Target then
INSERT
(ColumnId
,Col1
,Col2
,Col3
)
VALUES (Source.ColumnId
,Source.Col1
,Source.Col2
,Source.Col3
)
OUTPUT INSERTED.Id, Source.Id into #tab2(New_Id, Old_Id);
Postgres Code:
Create temp table tab2(New_Id int not null, Old_Id int not null)
With source as( select * from Tab1
WHERE ColumnId = ID)
Insert into Tab1(ColumnId
,Col1
,Col2
,Col3
)
select Source.ColumnId
,Source.Col1
,Source.Col2
,Source.Col3
from source
My query is how to convert OUTPUT INSERTED.Id in postgres.I need this id to insert records in another table (lets say as child tables based on Inserted values in Tab1)
In PostgreSQL's INSERT statements you can choose what the query should return. From the docs on INSERT:
The optional RETURNING clause causes INSERT to compute and return value(s) based on each row actually inserted (or updated, if an ON CONFLICT DO UPDATE clause was used). This is primarily useful for obtaining values that were supplied by defaults, such as a serial sequence number. However, any expression using the table's columns is allowed. The syntax of the RETURNING list is identical to that of the output list of SELECT. Only rows that were successfully inserted or updated will be returned.
Example (shortened form of your query):
WITH [...] INSERT INTO Tab1 ([...]) SELECT [...] FROM [...] RETURNING Tab1.id

CTE based insert of multiple rows into "one-per-group" table violates unique index

I have a table where only one row per group can be true.
This is enforced by a partial unique index (which can't be deferred).
CREATE TABLE test
(
id SERIAL PRIMARY KEY,
my_group INTEGER,
last BOOLEAN DEFAULT TRUE
);
CREATE UNIQUE INDEX "test.last" ON test (my_group) WHERE last;
INSERT INTO test (my_group)
VALUES (1), (2);
I'm trying to insert a new row into this table that shall replace the "last" element of the corresponding group. I also want to accomplish this in a single statement.
With some CTE trickery I'm able to do this: link to Fiddle
-- the statement is structured this way to closely resemble my actual usecase
WITH
new_data AS (
VALUES (1)
),
uncheck_old_last AS (
UPDATE test
SET last = FALSE
WHERE last AND my_group in (SELECT * FROM new_data)
RETURNING TRUE
)
INSERT INTO test (my_group)
SELECT *
FROM new_data
WHERE COALESCE((SELECT * FROM uncheck_old_last LIMIT 1), true);
So far so good, the insert happens... no conflicts.
I don't quite understand why this is working as from my understanding all CTEs should read the same initial DB state and can't see the changes made by other CTEs
The problem is now that I get a unique violation when I try to do the same with multiple rows at once: Link to Fiddle
-- the statement is structured this way to closely resemble my actual usecase
WITH
new_data AS (
VALUES (1), (2) -- <- difference to above query
),
uncheck_old_last AS (
UPDATE test
SET last = FALSE
WHERE last AND my_group in (SELECT * FROM new_data)
RETURNING TRUE
)
INSERT INTO test (my_group)
SELECT *
FROM new_data
WHERE COALESCE((SELECT * FROM uncheck_old_last LIMIT 1), true);
-- Schema Error: error: duplicate key value violates unique constraint "test.last"
Is there any way to insert multiple rows with one statement /Can someone explain to me why the first query is working and the second isn't?
This was caused by PostgreSQL simplifying my always true clause:
WHERE COALESCE((SELECT * FROM uncheck_old_last LIMIT 1), true)
was supposed to create a dependency between the main query and the CTE to enforce execution order from the main query's point of view.
It broke with more than one entry because the limit 1 allowed PostgreSQL to ignore the second row, as only one was required for evaluation.
I fixed it by comparing COUNT(*) > -1 instead:
COALESCE((SELECT COUNT(*) FROM uncheck_old_last) > -1, true)

Return fields from query used for INSERT values

When using a SELECT query to produce values for an INSERT like this:
INSERT INTO some_table (
foo,
bar
)
SELECT
at.foo,
at.bar
FROM another_table at;
How to return the fields from the SELECT query in the RETURNING clause along with the newly INSERTED rows? Something like:
INSERT INTO some_table AS t (
foo,
bar
)
SELECT
at.foo,
at.bar
FROM another_table at
RETURNING t.id, at.id;
I'm sure I've done this before or I've overlooked something really obvious.
Is it possible to do this? If so, how?
demo: db<>fiddle
WITH selecting AS (
SELECT id, x FROM a
), inserting AS (
INSERT INTO b AS b (y)
SELECT x FROM selecting
RETURNING id, y
)
SELECT
i.id,
s.id
FROM
inserting i
JOIN
selecting s
ON i.y = s.x
You can try a CTE approach. First selecting all relevant data, store the result internally. After that do the INSERT statement with these data, store the RETURNING values internally. After all you can you all saved data, combine them with a join and print out what you want.
It is not possible to return the fields from the query that provides the values for an INSERT. You can only return fields from the newly inserted rows.
An UPDATE can return fields from the updated rows as well as from the from list of the query.

Insert (if not present) and return id

I have a unique column. I want to insert a row if it's not already there, and then return the id of that row.
INSERT INTO t(a) VALUES ('a') ON CONFLICT DO NOTHING RETURNING t.id;
returns nothing at all. Here's a fiddle.
I'm looking for how to get 1 each time, whether 'a' was newly inserted or not.
with i as (
INSERT INTO t(a) VALUES ('a') ON CONFLICT (a) DO NOTHING RETURNING id
)
select id from i
union all
select id from t where a = 'a'
limit 1

upsert and sub select

i have a upsert statement (http://www.the-art-of-web.com/sql/upsert/) doing an insert whenever a row with id does not exist and updating the column when the row exists :
WITH upsert AS
(UPDATE foo SET counter=counter+1 WHERE id='bar' RETURNING *)
INSERT INTO foo(id, counter) SELECT 'bar', 0
WHERE NOT EXISTS (SELECT * FROM upsert) RETURNING counter;
id is the primary key column (as expected). until here everything works fine.
but there is a 3rd column 'position' which can be used for custom ordering.
in case of an update i want to keep the current value.
but the insert statement needs an additional subquery returning the lowest possible position not in use:
WITH upsert AS
(UPDATE foo SET counter=counter+1 WHERE id='bar' RETURNING *)
INSERT INTO foo(id, counter, position) SELECT 'bar', 0, MIN( position)-1 from foo
WHERE NOT EXISTS (SELECT * FROM upsert) RETURNING counter;
using this statement i get an error
ERROR: duplicate key value violates unique constraint "id"
whats wrong here ?
The problem is that MIN() applied to 0 row returns one row (with a NULL value)
Example:
test=> select min(1) where false;
min
-----
(1 row)
This differs from the same WHERE clause without min()
test=> select 1 where false;
?column?
----------
(0 rows)
So when using MIN() in the subquery feeding the INSERT, it will insert a new row even when the WHERE clause evaluates to false, which defeats the logic of this UPSERT.
I think this can be worked around by introducing another subquery:
WITH upsert AS
(UPDATE foo SET counter=counter+1 WHERE id='bar' RETURNING *)
INSERT INTO foo(id, counter, position)
SELECT * FROM (SELECT 'bar', 0, MIN( position)-1 from foo) s
WHERE NOT EXISTS (SELECT * FROM upsert)
RETURNING counter;
Note however that cramming this into a single SQL statement does not confer any guarantee of systematic success when run concurrently.
See for more:
How do I do an UPSERT (MERGE, INSERT … ON DUPLICATE UPDATE) in PostgreSQL?