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
Related
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.
how do I insert the std_id value and sub_id value in the student_subject table
insert into student_subjects(student_id,subject_id)
values(std_id,(select id from subjects
where guid in
(select * from
unnest(string_to_array(subjects_colls,',')::uuid[])))::int);
ERROR: more than one row returned by a subquery used as an expression
Get rid of the values clause and use the SELECT directly as the source for the INSERT statement:
You also don't need to unnest your array, using = any() will be a bit more efficient (although I would recommend you do not pass comma separated strings, but an array of uuid directly)
insert into student_subjects(student_id,subject_id)
select std_id, s.id
from subjects s
where guid = any(string_to_array(subjects_colls,',')::uuid[])
I assume this is part of a procedure or function and std_id and subjects_colls are parameters passed to it.
I have the following table:
RecordID
Name
Col1
Col2
....
ColN
The RecordID is BIGINT PRIMARY KEY CLUSTERED IDENTITY(1,1) and RecordID and Name are initialized. The other columns are NULLs.
I have a function which returns information about the other columns by Name.
To initialized my table I use the following algorithm:
Create a LOOP
Get a row, select its Name value
Execute the function using the selected name, and store its result
in temp variables
Insert the temp variables in the table
Move to the next record
Is there a way to do this without looping?
Cross apply was basically built for this
SELECT D.deptid, D.deptname, D.deptmgrid
,ST.empid, ST.empname, ST.mgrid
FROM Departments AS D
CROSS APPLY fn_getsubtree(D.deptmgrid) AS ST;
Using APPLY
UPDATE some_table
SET some_row = another_row,
some_row2 = another_row/2
FROM some_table st
CROSS APPLY
(SELECT TOP 1 another_row FROM another_table at WHERE at.shared_id=st.shared_id)
WHERE ...
using cross apply in an update statement
You can simply say the following if you already have the records in the table.
UPDATE MyTable
SET
col1 = dbo.col1Method(Name),
col2 = dbo.col2Method(Name),
...
While inserting new records, assuming RecordID is auto-generated, you can say
INSERT INTO MyTable(Name, Col1, Col2, ...)
VALUES(#Name, dbo.col1Method(#Name), dbo.col2Method(#name), ...)
where #Name contains the value for the Name column.
Say I have a table with an identity field. I want to insert a record in it if it doesn't already exist. In the below example, I check if the value stored in #Field1 already exists in the table. If not, I insert a new record:
Definition of the table:
MyTable (MyTableId int Identity not null, Field1 int not null, Field2 int not null)
This is how I check if the value already exists and insert it if necessary
merge MyTable as t
using (#Field1, #Field2) as s (Field1,Field2)
on (t.Field1=s.Field1)
when not matched then
insert (Field1,Field2) values (s.Field1,s.Field2);
Getting the identity value when the record didn't already exist in the table can be done by adding:
output Inserted.MyTableId
but what if the record was already in the table (ie if there was a match)?
The only way I found is to query the table after executing the Merge statement:
select MyTableId from MyTable where Field1=#Field1
Is there a way to get the identity value directly from the Merge?
In the case when the record already exists, you can store the matched id into a variable like this:
DECLARE #MatchedId INTEGER;
MERGE MyTable as t
....
....
WHEN MATCHED THEN
UPDATE SET #MatchedId = t.MyTableId;
UPDATE:
Here's a full example. This demonstrates one way:
DECLARE #UpdateVariable bit
DECLARE #ChangeResult TABLE (ChangeType VARCHAR(10), Id INTEGER)
DECLARE #Data TABLE (Id integer IDENTITY(1,1), Val VARCHAR(10))
INSERT #Data ([Val]) VALUES ('A');
MERGE #data AS TARGET
USING (SELECT 'A' AS Val UNION ALL SELECT 'B' AS Val) AS SOURCE ON TARGET.Val = SOURCE.Val
WHEN NOT MATCHED THEN
INSERT ([Val])
VALUES (SOURCE.Val)
WHEN MATCHED THEN
UPDATE SET #UpdateVariable = 1
OUTPUT $action, inserted.Id INTO #ChangeResult;
SELECT * FROM #data
SELECT * FROM #ChangeResult
Points to note are:
$action will give you what type of action was performed for a row (INSERT, UPDATE, DELETE)
#ChangeResult table will hold the info as to what types of changes were made
for the WHEN MATCHED case, I am basically setting a dummy variable. This doesn't serve any purpose here other than to ensure the UPDATE path gets hit to generate the UPDATE row in the output. i.e. that #UpdateVariable is not used for anything else. If you actually wanted to update the existing row, then you'd put a proper UPDATE in here, but in the case where you don't want to actually UPDATE the existing row, then this "dummy" update seems to be required.
Here is an alternative and slightly simpler approach (in my opinion):
DECLARE #Id [int];
MERGE INTO [MyTable] AS [t]
USING (VALUES
(#FieldA, #FieldB)
)
AS [x] (FieldA, FieldB)
ON [t].[FieldA] = [x].[FieldA]
AND [t].[FieldB] = [x].[FieldB]
WHEN NOT MATCHED BY TARGET THEN
INSERT (FieldA, FieldB)
VALUES (FieldA, FieldB)
WHEN MATCHED THEN
UPDATE SET #Id = [t].[Id]
IF #Id IS NULL
BEGIN
SET #Id = CAST(SCOPE_IDENTITY() as [int]);
END
SELECT #Id;
If the merge statement resulted in a match then #Id will be set to the identity of the matching row. In the event of no match, the new row will have been inserted with its new identity ready to be selected from SCOPE_IDENTITY().
Here other alternative:
DECLARE #FakeVar BIT
MERGE MyTable AS T
USING (VALUES(#Field1, #Field2)) AS S (Field1, Field2)
ON (T.Field1 = S.Field1)
WHEN NOT MATCHED THEN
INSERT (Field1, Field2)
VALUES (S.Field1, T.Field2)
WHEN MATCHED THEN
UPDATE SET #FakeVar = NULL -- do nothing, only force "an update" to ensure output on updates
OUTPUT INSERTED.MyTableId ;
If you check the OUPUT doc
INSERTED
Is a column prefix that specifies the value added by the insert or update operation.
you only need to do "something" on the update set clause
When a Merge has existing insert and an update code, the update most likely requires a primary key id. That value is generally passed to the stored procedure as a separate variable. Since that variable is null on a insert and has a value on an update, it is no issue to get the last insert with Ident_Current targeting the insert table.
Example to Merge into a Project table
Merge info.Project as Target
...
on #projectId = TARGET.ProjectId
When not matched then
... -- Insert
When matched then
... -- Update
;
if (#projectId is null)
set #projectId = IDENT_CURRENT('info.Project')
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.