I have a JDBC application that uses postgres INSERT INTO / ON CONFLICT to perform an upsert query. We would like to support H2 as well, but H2 uses the MERGE query instead. How would I go about translating the following query into H2's syntax? The H2 code will be in a separate library, which means that it doesn't have to work in both postgres and H2.
INSERT INTO dest_table(id,
field1,
field2,
field3,
field4)
VALUES (?, ?, ?, ?, ?)
on conflict (id) do update
set (field3,field3) = (excluded.field3, excluded.field4)
Of course, as soon as I posted to StackOverflow I figured it out. Here is my solution:
Table setup
drop table if exists characters;
CREATE TABLE characters (
id int not null,
first character varying(50),
last character varying(50));
insert into characters (id, first, last) values ( 1, 'scooby', 'doo' );
insert into characters (id, first, last) values ( 2, 'eric', 'cartman' );
insert into characters (id, first, last) values ( 3, 'luke', 'skywalker' );
select * from characters;
-------------------------
1,scooby,doo
2,eric,cartman
3,luke,skywalker
The update query looks like this (with hardcoded values, but it should work with placeholders as well)
Insert (no conflict):
merge into characters
using values (4, 'darth', 'vader') as incoming(id,first,last)
on characters.id = incoming.id
when not matched then
insert (id, first, last) values (incoming.id, incoming.first, incoming.last)
when matched then
update set first = incoming.first, last = incoming.last;
select * from characters;
-------------------------
1,scooby,doo
2,eric,cartman
3,luke,skywalker
4,darth,vader
Update (conflict on 'id', just updates darth vader to anikan skywalker):
merge into characters
using values (4, 'anikan', 'skywalker') as incoming(id,first,last)
on characters.id = incoming.id
when not matched then
insert (id, first, last) values (incoming.id, incoming.first, incoming.last)
when matched then
update set first = incoming.first, last = incoming.last;
select * from characters;
-------------------------
1,scooby,doo
2,eric,cartman
3,luke,skywalker
4,anikan,skywalker
Related
I use Postgres and I've integration app which write data to database. My column should not be null but my app send null value. I tried to set default value but query override this rule with null value. How can i handle this change without code.
My Column configuration looks like this.
If you won't or can't change the query in code, you have to use trigger
If you can change code structure and query:
If the column has a default value, then no need to send NULL value to query
-- Before change
insert into your_table (id, name, default_col) values
(1, 'name', null);
-- After change (remove null data)
insert into your_table (id, name) values
(1, 'name');
Or send default value in insert query
-- Before change
insert into your_table (id, name, default_col) values
(1, 'name', null);
-- After change (Use default keyboard)
insert into your_table (id, name, default_col) values
(1, 'name', default);
I am having trouble getting MERGE statements to work properly, and I have recently started to try to use checksums.
In the toy example below, I cannot get this row to insert (1, 'ANDREW', 334.3) that is sitting in the staging table.
DROP TABLE TEMP1
DROP TABLE TEMP1_STAGE
-- create table
CREATE TABLE TEMP1
(
[ID] INT,
[NAME] VARCHAR(55),
[SALARY] FLOAT,
[SCD] INT
)
-- create stage
CREATE TABLE TEMP1_STAGE
(
[ID] INT,
[NAME] VARCHAR(55),
[SALARY] FLOAT,
[SCD] INT
)
-- insert vals into stage
INSERT INTO TEMP1_STAGE (ID, NAME, SALARY)
VALUES
(1, 'ANDREW', 333.3),
(2, 'JOHN', 555.3),
(3, 'SARAH', 444.3)
-- insert stage table into main table
INSERT INTO TEMP1
SELECT *
FROM TEMP1_STAGE;
-- clean up stage table
TRUNCATE TABLE TEMP1_STAGE;
-- put some new values in the stage table
INSERT INTO TEMP1_STAGE (ID, NAME, SALARY)
VALUES
(1, 'ANDREW', 334.3),
(4, 'CARL', NULL)
-- CHECKSUMS
update TEMP1_STAGE
set SCD = binary_checksum(ID, NAME, SALARY);
update TEMP1
set SCD = binary_checksum(ID, NAME, SALARY);
-- run merge
MERGE TEMP1 AS TARGET
USING TEMP1_STAGE AS SOURCE
-- match
ON (SOURCE.[ID] = TARGET.[ID])
WHEN NOT MATCHED BY TARGET
THEN INSERT (
[ID], [NAME], [SALARY], [SCD]) VALUES (
SOURCE.[ID], SOURCE.[NAME], SOURCE.[SALARY], SOURCE.[SCD]);
-- the value: (1, 'ANDREW', 334.3) is not merged in
SELECT * FROM TEMP1;
How can I use the checksum to my advantage in the MERGE?
Your issue is that the NOT MATCHED condition is only considering the ID values specified in the ON condition.
If you want duplicate, but distinct records, include SCD to the ON condition.
If (more likely) your intent is that record ID = 1 be updated with the new SALARY, you will need to add a WHEN MATCHED AND SOURCE.SCD <> TARGET.SCD THEN UPDATE ... clause.
That said, the 32-bit int value returned by the `binary_checksum()' function is not sufficiently distinct to avoid collisions and unwanted missed updates. Take a look at HASHBYTES instead. See Binary_Checksum Vs HashBytes function.
Even that may not yield your intended performance gain. Assuming that you have to calculate the hash for all records in the staging table for each update cycle, you may find that it is simpler to just compare each potentially different field before the update. Something like:
WHEN MATCHED AND (SOURCE.NAME <> TARGET.NAME OR SOURCE.SALARY <> TARGET.SALARY)
THEN UPDATE ...
Even then, you need to be careful of potential NULL values and COLLATION. Both NULL <> 50000.00 and 'Andrew' <> 'ANDREW' may not give you the results you expect. It might be easiest and most reliable to just code WHEN MATCHED THEN UPDATE ....
Lastly, I suggest using DECIMAL instead of FLOAT for Salary.
I created a table which contains a column of string ARRAY type as:
CREATE TABLE test
(
id integer NOT NULL,
list text[] COLLATE pg_catalog."default",
CONSTRAINT test_pkey PRIMARY KEY (id)
)
I then added rows which contain various values for that array, including an empty array and missing data (null):
insert into test (id, list) values (1, array['one', 'two', 'three']);
insert into test (id, list) values (2, array['four']);
insert into test (id, list) values (3, array['']);
insert into test (id, list) values (4, array[]::text[]); // empty array
insert into test (id, list) values (5, null); // missing value
pgAdmin shows table like this:
I am trying to get a row which contains a null value ([null]) in the list column but:
select * from test where list = null;
...returns no rows and:
select * from test where list = '{}';
...returns row with id = 4.
How to write WHERE clause which would target NULL value for column of ARRAY type?
demo:db<>fiddle
... WHERE list IS NULL
select * from test where list IS null;
Like this:
select * from test where list IS NULL;
In postgres I have two tables like so
CREATE TABLE foo (
pkey SERIAL PRIMARY KEY,
name TEXT
);
CREATE TABLE bar (
pkey SERIAL PRIMARY KEY,
foo_fk INTEGER REFERENCES foo(pkey) NOT NULL,
other TEXT
);
What I want to do is to write a .sql script file that does the following
INSERT INTO foo(name) VALUES ('A') RETURNING pkey AS abc;
INSERT INTO bar(foo_fk,other) VALUES
(abc, 'other1'),
(abc, 'other2'),
(abc, 'other3');
which produces the error below in pgAdmin
Query result with 1 row discarded.
ERROR: column "abc" does not exist
LINE 3: (abc, 'other1'),
********** Error **********
ERROR: column "abc" does not exist
SQL state: 42703
Character: 122
Outside of a stored procedure how do a define a variable that I can use between statements? Is there some other syntax for being able to insert into bar with the pkey returned from the insert to foo.
You can combine the queries into one. Something like:
with foo_ins as (INSERT INTO foo(name)
VALUES ('A')
RETURNING pkey AS foo_id)
INSERT INTO bar(foo_fk,other)
SELECT foo_id, 'other1' FROM foo_ins
UNION ALL
SELECT foo_id, 'other2' FROM foo_ins
UNION ALL
SELECT foo_id, 'other3' FROM foo_ins;
Other option - use an anonymous PL/pgSQL block like:
DO $$
DECLARE foo_id INTEGER;
BEGIN
INSERT INTO foo(name)
VALUES ('A')
RETURNING pkey INTO foo_id;
INSERT INTO bar(foo_fk,other)
VALUES (foo_id, 'other1'),
(foo_id, 'other2'),
(foo_id, 'other3');
END$$;
You can use lastval() to ...
Return the value most recently returned by nextval in the current session.
This way you do not need to know the name of the seqence used.
INSERT INTO foo(name) VALUES ('A');
INSERT INTO bar(foo_fk,other) VALUES
(lastval(), 'other1')
, (lastval(), 'other2')
, (lastval(), 'other3')
;
This is safe because you control what you called last in your own session.
If you use a writable CTE as proposed by #Ihor, you can still use a short VALUES expression in the 2nd INSERT. Combine it with a CROSS JOIN (or append the CTE name after a comma (, ins) - same thing):
WITH ins AS (
INSERT INTO foo(name)
VALUES ('A')
RETURNING pkey
)
INSERT INTO bar(foo_fk, other)
SELECT ins.pkey, o.other
FROM (
VALUES
('other1'::text)
, ('other2')
, ('other3')
) o(other)
CROSS JOIN ins;
Another option is to use currval
INSERT INTO foo
(name)
VALUES
('A') ;
INSERT INTO bar
(foo_fk,other)
VALUES
(currval('foo_pkey_seq'), 'other1'),
(currval('foo_pkey_seq'), 'other2'),
(currval('foo_pkey_seq'), 'other3');
The automatically created sequence for serial columns is always named <table>_<column>_seq
Edit:
A more "robust" alternative is to use pg_get_serial_sequence as Igor pointed out.
INSERT INTO bar
(foo_fk,other)
VALUES
(currval(pg_get_serial_sequence('public.foo', 'pkey')), 'other1'),
(currval(pg_get_serial_sequence('public.foo', 'pkey')), 'other2'),
(currval(pg_get_serial_sequence('public.foo', 'pkey')), 'other3');
I have created table using this command successfully
create table Person(
first_name varchar(25) not null,
last_name varchar(25) not null,
persoin_id number not null,
birth_date date,
country varchar (25),
salary number);
and now I want to insert data into that table
insert into Person(persoin_id,first_name,last_name,salary,birth_date,country)
values(100,'dato','datuashvili',350,to_date('01/01/10','DD/MM/YY'),'georgia');
values(101,'irakli','oqruashvili',350,to_date('01/03/10','DD/MM/YY'),'georgia');
first row is inserted,but problem is with second line
1 rows inserted.
Error starting at line 10 in command:
values(101,'irakli','oqruashvili',350,to_date('01/03/10','DD/MM/YY'),'georgia')
Error report:
Unknown Command
Please help me to determine what is problem?thanks
If you are on a RDBMS that supports multi-rows inserts in one INSERT:
insert into Person(persoin_id,first_name,last_name,salary,birth_date,country)
values
(100,'dato','datuashvili',350,to_date('01/01/10','DD/MM/YY'),'georgia') ,
--- comma here ---^
(101,'irakli','oqruashvili',350,to_date('01/03/10','DD/MM/YY'),'georgia') ;
^--- no "values" here
If not (like Oracle), you'll have to issue two insert statements:
insert into Person(persoin_id,first_name,last_name,salary,birth_date,country)
values
(100,'dato','datuashvili',350,to_date('01/01/10','DD/MM/YY'),'georgia') ;
--- as it was here ---^
insert into Person(persoin_id,first_name,last_name,salary,birth_date,country)
values
(101,'irakli','oqruashvili',350,to_date('01/03/10','DD/MM/YY'),'georgia') ;
or use this approach:
insert into Person(persoin_id,first_name,last_name,salary,birth_date,country)
select
(100,'dato','datuashvili',350,to_date('01/01/10','DD/MM/YY'),'georgia')
from dual
union all select
(101,'irakli','oqruashvili',350,to_date('01/03/10','DD/MM/YY'),'georgia')
from dual
;
You will need to use 2 insert statement instead of one for 2 different sets of data...
insert into Person(persoin_id,first_name,last_name,salary,birth_date,country)
values(100,'dato','datuashvili',350,to_date('01/01/10','DD/MM/YY'),'georgia');
insert into Person(persoin_id,first_name,last_name,salary,birth_date,country)
values(101,'irakli','oqruashvili',350,to_date('01/03/10','DD/MM/YY'),'georgia')
You have a ; at the end of:
values(100,'dato','datuashvili',350,to_date('01/01/10','DD/MM/YY'),'georgia');
^
change it to , and also loose the values from the next line. You need just one values per insert.