PostgreSQL Inserting into 2 tables? - postgresql

Working with a nodejs server, and I thought instead of having to do 2 method calls, with each separate function, I would call one query to add the 2 to the database. I've searched around abit on stack overflow for some questions regarding this, however they seem to not work.
My example would be that at the register phase, the user is inserted into the database, and then a sub user table called user_settings is added with a relational key to the user id of the created user.
One example that I tried but didn't work is this:
WITH ins1 AS (
INSERT INTO users(firstname, lastname)
VALUES ('foo', 'bar')
RETURNING id AS user_id
)
, ins2 AS (
INSERT INTO user_settings(user_id, adddetails)
SELECT user_id, 'foobar' FROM ins1
)
It doesn't seem to know the returning user_id when I try and put it in as a query.

Your CTE is missing a final statement. The second insert should be "outside" a CTE definition:
WITH ins1 AS (
INSERT INTO users(firstname, lastname)
VALUES ('foo', 'bar')
RETURNING id AS user_id
)
INSERT INTO user_settings(user_id, adddetails, "timestamp")
SELECT user_id, 'foobar', current_timetamp
FROM ins1;

Related

Coalesce sentence containing an insert into clause fails in PostgreSQL

This is my trivial test table,
create table test (
id int not null generated always as identity,
first_name. varchar,
primary key (id),
unique(first_name)
);
As an alternative to insert-into-on-conflict sentences, I was trying to use the coalesce laziness to execute a select whenever possible or an insert, only when select fails to find a row.
coalesce laziness is described in documentation. See https://www.postgresql.org/docs/current/functions-conditional.html
Like a CASE expression, COALESCE only evaluates the arguments that are needed to determine the result; that is, arguments to the right of the first non-null argument are not evaluated. This SQL-standard function provides capabilities similar to NVL and IFNULL, which are used in some other database systems.
I also want to get back the id value of the row, having being inserted or not.
I started with:
select coalesce (
(select id from test where first_name='carlos'),
(insert into test(first_name) values('carlos') returning id)
);
but an error syntax error at or near "into" was found.
See it on this other DBFiddle
https://www.db-fiddle.com/f/t7TVkoLTtWU17iaTAbEhDe/0
Then I tried:
select coalesce (
(select id from test where first_name='carlos'),
(with r as (
insert into test(first_name) values('carlos') returning id
) select id from r
)
);
Here I am getting a WITH clause containing a data-modifying statement must be at the top level error that I don't understand, as insert is the first and only sentence within the with.
I am testing this with DBFiddle and PostgreSQL 13. The source code can be found at
https://www.db-fiddle.com/f/hp8T1iQ8eS4wozDCBhBXDw/5
Different method: chained CTEs:
CREATE TABLE test
( id INTEGER NOT NULL GENERATED ALWAYS AS IDENTITY PRIMARY KEY
, first_name VARCHAR UNIQUE
);
WITH sel AS (
SELECT id FROM test WHERE first_name = 'carlos'
)
, ins AS (
INSERT INTO test(first_name)
SELECT 'carlos'
WHERE NOT EXISTS (SELECT 1 FROM test WHERE first_name = 'carlos')
RETURNING id
)
, omg AS (
SELECT id FROM sel
UNION ALL
SELECT id FROM ins
)
SELECT id
FROM omg
;
It seems that the returning value from the insert into clause is not equivalent in nature to the scalar query of a select clause. So I try encapsulating the insert into into an SQL function and it worked.
create or replace function insert_first_name(
_first_name varchar
) returns int
language sql as $$
insert into test (first_name)
values (_first_name)
returning id;
$$;
select coalesce (
(select id from test where first_name='carlos'),
(select insert_first_name('carlos'))
);
See it on https://www.db-fiddle.com/f/73rVXgqGfrG4VmjrAk6Z3i/2
This is a refinement on #wildplasser accepted answer. it avoids comparing first_name twice and uses coalesce instead of union all. Kind of an selsert in just one sentence.
with sel as (
select id from test where first_name = 'carlos'
)
, ins as (
insert into test(first_name)
select 'carlos'
where (select id from sel) is null
returning id
)
select coalesce (
(select id from sel),
(select id from ins)
);
See it at https://www.db-fiddle.com/f/goRh4TyAebTkEZFHk6WbtK/6

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.

Use returned value of INSERT ... RETURNING in multiple following inserts

I'm trying to use a value returned by an INSERT ... RETURNING statement in multiple following INSERTs.
Say we have the following tables:
CREATE TABLE hosts (host_id SERIAL, name CHARACTER VARYING(20));
CREATE TABLE interfaces (interface_id SERIAL, host_id INTEGER, name CHARACTER VARYING(10), iface_ip INET);
INSERT INTO hosts (name) VALUES ('Host A'),('Host B');
What I want, is to insert a row in the first table (hosts), get the created host_id and then insert multiple rows into the second table (interfaces) with given values and the host_id from the first statement.
I found the following way, using a CTE and a SELECT with static values which works for me, but I'm pretty sure, that this is not the way to accomplish it...
WITH temp_table AS (
INSERT INTO hosts (name) VALUES ('Host C') RETURNING host_id AS last_hostid
), i1 AS (
INSERT INTO interfaces (host_id, name, iface_ip) SELECT last_hostid, 'eth0', '192.168.1.1' FROM temp_table
), i2 AS (
INSERT INTO interfaces (host_id, name, iface_ip) SELECT last_hostid, 'eth1', '192.168.2.1' FROM temp_table
), i3 AS (
INSERT INTO interfaces (host_id, name, iface_ip) SELECT last_hostid, 'eth2', '192.168.3.1' FROM temp_table
) SELECT 1;
I know that I can easily do this, by talking back to a webserver with say PHP, and then fill in the variable in the next statement. But I wanted to accomplish it without all the back and forth, solely in PostgreSQL. So, if there is a better way than mine (and I'm pretty sure of it) - any hints?
You can create one CTE with the rows you want to insert and then use that as the source for the actual insert:
WITH temp_table AS (
INSERT INTO hosts (name) VALUES ('Host C')
RETURNING host_id AS last_hostid
), new_data (name, iface_ip) AS (
values
('eth0', '192.168.1.1'::inet),
('eth1', '192.168.2.1'::inet),
('eth2', '192.168.3.1'::inet)
)
INSERT INTO interfaces (host_id, name, iface_ip)
SELECT last_hostid, nd.name, nd.iface_ip
FROM new_data as nd, temp_table;
The (implicit) cross join in the SELECT doesn't matter as temp_table only return a single row.

Update from inserted id

LOGIC: I want to create loyalty_customers when customers.user_id is null and update customers.user_id is loyalty_customers.id
I have a query and it's not working:
UPDATE customers
SET user_id = (
INSERT INTO loyalty_users (name) VALUES ('anonymous')) RETURNING id
)
WHERE user_id IS NULL;
Error like:
LINE 3: INSERT INTO loyalty_users
Look like INSERT can not be wrapped by any query, I tried this query and it's still not working:
SELECT id FROM (
INSERT INTO loyalty_users (name) VALUES ('anonymous')) RETURNING id);
However, stand-alone INSERT query work like a charm:
INSERT INTO loyalty_users (name) VALUES ('anonymous')) RETURNING id;
May be the right way to do this thing is:
WITH updated_customer AS (
UPDATE customers
SET name = 'anonymous'
WHERE user_id IS NULL
RETURNING user_id AS id
)
INSERT INTO loyalty_users SELECT * FROM updated_customer;
It's work for me, but if you have another idea, please comment in this question.

SELECT or INSERT a row in one command

I'm using PostgreSQL 9.0 and I have a table with just an artificial key (auto-incrementing sequence) and another unique key. (Yes, there is a reason for this table. :)) I want to look up an ID by the other key or, if it doesn't exist, insert it:
SELECT id
FROM mytable
WHERE other_key = 'SOMETHING'
Then, if no match:
INSERT INTO mytable (other_key)
VALUES ('SOMETHING')
RETURNING id
The question: is it possible to save a round-trip to the DB by doing both of these in one statement? I can insert the row if it doesn't exist like this:
INSERT INTO mytable (other_key)
SELECT 'SOMETHING'
WHERE NOT EXISTS (SELECT * FROM mytable WHERE other_key = 'SOMETHING')
RETURNING id
... but that doesn't give the ID of an existing row. Any ideas? There is a unique constraint on other_key, if that helps.
Have you tried to union it?
Edit - this requires Postgres 9.1:
create table mytable (id serial primary key, other_key varchar not null unique);
WITH new_row AS (
INSERT INTO mytable (other_key)
SELECT 'SOMETHING'
WHERE NOT EXISTS (SELECT * FROM mytable WHERE other_key = 'SOMETHING')
RETURNING *
)
SELECT * FROM new_row
UNION
SELECT * FROM mytable WHERE other_key = 'SOMETHING';
results in:
id | other_key
----+-----------
1 | SOMETHING
(1 row)
No, there is no special SQL syntax that allows you to do select or insert. You can do what Ilia mentions and create a sproc, which means it will not do a round trip fromt he client to server, but it will still result in two queries (three actually, if you count the sproc itself).
using 9.5 i successfully tried this
based on Denis de Bernardy's answer
only 1 parameter
no union
no stored procedure
atomic, thus no concurrency problems (i think...)
The Query:
WITH neworexisting AS (
INSERT INTO mytable(other_key) VALUES('hello 2')
ON CONFLICT(other_key) DO UPDATE SET existed=true -- need some update to return sth
RETURNING *
)
SELECT * FROM neworexisting
first call:
id|other_key|created |existed|
--|---------|-------------------|-------|
6|hello 1 |2019-09-11 11:39:29|false |
second call:
id|other_key|created |existed|
--|---------|-------------------|-------|
6|hello 1 |2019-09-11 11:39:29|true |
First create your table ;-)
CREATE TABLE mytable (
id serial NOT NULL,
other_key text NOT NULL,
created timestamptz NOT NULL DEFAULT now(),
existed bool NOT NULL DEFAULT false,
CONSTRAINT mytable_pk PRIMARY KEY (id),
CONSTRAINT mytable_uniq UNIQUE (other_key) --needed for on conflict
);
you can use a stored procedure
IF (SELECT id FROM mytable WHERE other_key = 'SOMETHING' LIMIT 1) < 0 THEN
INSERT INTO mytable (other_key) VALUES ('SOMETHING')
END IF
I have an alternative to Denis answer, that I think is less database-intensive, although a bit more complex:
create table mytable (id serial primary key, other_key varchar not null unique);
WITH table_sel AS (
SELECT id
FROM mytable
WHERE other_key = 'test'
UNION
SELECT NULL AS id
ORDER BY id NULLS LAST
LIMIT 1
), table_ins AS (
INSERT INTO mytable (id, other_key)
SELECT
COALESCE(id, NEXTVAL('mytable_id_seq'::REGCLASS)),
'test'
FROM table_sel
ON CONFLICT (id) DO NOTHING
RETURNING id
)
SELECT * FROM table_ins
UNION ALL
SELECT * FROM table_sel
WHERE id IS NOT NULL;
In table_sel CTE I'm looking for the right row. If I don't find it, I assure that table_sel returns at least one row, with a union with a SELECT NULL.
In table_ins CTE I try to insert the same row I was looking for earlier. COALESCE(id, NEXTVAL('mytable_id_seq'::REGCLASS)) is saying: id could be defined, if so, use it; whereas if id is null, increment the sequence on id and use this new value to insert a row. The ON CONFLICT clause assure
that if id is already in mytable I don't insert anything.
At the end I put everything together with a UNION between table_ins and table_sel, so that I'm sure to take my sweet id value and execute both CTE.
This query needs to search for the value other_key only once, and is a "search this value" not a "check if this value not exists in the table", that is very heavy; in Denis alternative you use other_key in both types of searches. In my query you "check if a value not exists" only on id that is a integer primary key, that, for construction, is fast.
Minor tweak a decade late to Denis's excellent answer:
-- Create the table with a unique constraint
CREATE TABLE mytable (
id serial PRIMARY KEY
, other_key varchar NOT NULL UNIQUE
);
WITH new_row AS (
-- Only insert when we don't find anything, avoiding a table lock if
-- possible.
INSERT INTO mytable ( other_key )
SELECT 'SOMETHING'
WHERE NOT EXISTS (
SELECT *
FROM mytable
WHERE other_key = 'SOMETHING'
)
RETURNING *
)
(
-- This comes first in the UNION ALL since it'll almost certainly be
-- in the query cache. Marginally slower for the insert case, but also
-- marginally faster for the much more common read-only case.
SELECT *
FROM mytable
WHERE other_key = 'SOMETHING'
-- Don't check for duplicates to be removed
UNION ALL
-- If we reach this point in iteration, we needed to do the INSERT and
-- lock after all.
SELECT *
FROM new_row
) LIMIT 1 -- Just return whatever comes first in the results and allow
-- the query engine to cut processing short for the INSERT
-- calculation.
;
The UNION ALL tells the planner it doesn't have to collect results for de-duplication. The LIMIT 1 at the end allows the planner to short-circuit further processing/iteration once it knows there's an answer available.
NOTE: There is a race condition present here and in the original answer. If the entry does not already exist, the INSERT will fail with a unique constraint violation. The error can be suppressed with ON CONFLICT DO NOTHING, but the query will return an empty set instead of the new row. This is a difficult problem because getting that info from another transaction would violate the I in ACID.