UPSERT based on UNIQUE constraint with NULL values - sql

I have a Postgres table with a unique constraint on multiple columns, one of which can be NULL. I only every want to allow one record with NULL in that column for each combination.
create table my_table (
col1 int generated by default as identity primary key,
col2 int not null,
col3 real,
col4 int,
constraint ux_my_table_unique unique (col2, col3)
);
I have an upsert query that I want to update col4 when it encounters a record with the same values in col2, col3:
insert into my_table (col2, col3, col4) values (p_col2, p_col3, p_col4)
on conflict (col2, col3) do update set col4=excluded.col4;
But the conflict is not firing when col3 is NULL. I have read about using triggers. What is the best solution to get the conflict to fire please?

Postgres 15
... adds the clause NULLS NOT DISTINCT. Your case works out of the box now:
ALTER TABLE my_table
DROP CONSTRAINT IF EXISTS ux_my_table_unique
, ADD CONSTRAINT ux_my_table_unique UNIQUE NULLS NOT DISTINCT (col2, col3);
INSERT INTO my_table (col2, col3, col4)
VALUES (p_col2, p_col3, p_col4)
ON CONFLICT (col2, col3) DO UPDATE
SET col4 = EXCLUDED.col4;
See:
Create unique constraint with null columns
Postgres 14 or older
NULL values are not considered equal to each other and thus never trigger a UNIQUE violation. That means, your current table definition does not do what you say it should do. There can already be multiple rows with (col2, col3) = (1, NULL). ON CONFLICT never fires for col3 IS NULL in your current setup.
You can enforce your UNIQUE constraint with two partial UNIQUE indexes as also outlined here:
Create unique constraint with null columns
Applied to your case:
CREATE UNIQUE INDEX my_table_col2_uni_idx ON my_table (col2)
WHERE col3 IS NULL;
CREATE UNIQUE INDEX my_table_col2_col3_uni_idx ON my_table (col2, col3)
WHERE col3 IS NOT NULL;
But ON CONFLICT ... DO UPDATE can only be based on a single UNIQUE index or constraint. Only the ON CONFLICT DO NOTHING variant works as "catch-all". See:
How to use RETURNING with ON CONFLICT in PostgreSQL?
It would seem like what you want is currently impossible, but there is a ...
Perfect solution
With the two partial UNIQUE indexes in place, you can use the right statement based on the input value of col3:
WITH input(col2, col3, col4) AS (
VALUES
(3, NULL::real, 5) -- ①
, (3, 4, 5)
)
, upsert1 AS (
INSERT INTO my_table AS t(col2, col3, col4)
SELECT * FROM input WHERE col3 IS NOT NULL
ON CONFLICT (col2, col3) WHERE col3 IS NOT NULL -- matching index_predicate!
DO UPDATE
SET col4 = EXCLUDED.col4
WHERE t.col4 IS DISTINCT FROM EXCLUDED.col4 -- ②
)
INSERT INTO my_table AS t(col2, col3, col4)
SELECT * FROM input WHERE col3 IS NULL
ON CONFLICT (col2) WHERE col3 IS NULL -- matching index_predicate!
DO UPDATE SET col4 = EXCLUDED.col4
WHERE t.col4 IS DISTINCT FROM EXCLUDED.col4; -- ②
db<>fiddle here
Works in every case.
Even works for multiple input rows with an arbitrary mix of NULL and NOT NULL values for col3.
And doesn't even cost much more than the plain statement because each row only enters into one of the two UPSERTs.
This is one of those "Eurika!" queries where everything just clicks, against all odds. :)
① Note the explicit cast to ::real in the CTE input. This related answer explains why:
Casting NULL type when updating multiple rows
② The final WHERE clause is optional, but highly recommended. It would be a waste to go through with the UPDATE if it doesn't actually change anything. See:
How do I (or can I) SELECT DISTINCT on multiple columns?

If you can find a value that can never legally exist in col3 (make sure with a check constraint), you could use a unique index:
CREATE UNIQUE INDEX ON my_table (
col2,
coalesce(col3, -1.0)
);
and use that in your INSERT:
INSERT INTO my_table (col2, col3, col4)
VALUES (p_col2, p_col3, p_col4)
ON CONFLICT (col2, coalesce(col3, -1.0))
DO UPDATE SET col4 = excluded.col4;

Related

How can i define a CHECK CONSTRAINT to enforce the rule of minimum 3 instances of a value?

I have a table that looks like this:
Table1(col1,col2,col3)
I want to define a CHECK CONSTRAINT that enforce a rule that there must be a minimum of 3 instances of a value in a column.
Something like this:
ALTER TABLE Athletes_Participations
ADD CONSTRAINT Check_participations CHECK (count(AID) >= 3)
The error message that get is:
An aggregate may not appear in a computed column expression or check constraint.
this is what i've tried:
CREATE FUNCTION CHECKINSTANCES()
RETURNS int
AS
BEGIN
DECLARE #retval int
SELECT #retval = COUNT(AID) FROM tbl1
RETURN #retval
END;
GO
ALTER TABLE tbl1
ADD CONSTRAINT CHK_INS CHECK (dbo.CHECKINSTANCES() >= 3 );
GO
As has been mentioned in the comments, this is messy. You can't do this with a CONSTRAINT alone, which means you either need to use a TRIGGER or a scalar function. I dislike using scalar functions in CONSTRAINTs, they have been known to tank performance, so I'm using a TRIGGER here.
Note that either implementation comes with caveats. Firstly, if you are inserting new rows you cannot insert them one by one. This will fail (as the first row will fail the pseudo CONSTRAINT). You won't be able to do task like UPDATE 1 row to have the value of new value, and INSERT 2 new rows either (with the same new value) as when the first statement completes the error will be thrown (due to there only being 1 or 2 rows). You'd have to use "proxy rows" to get around that. I demonstrate this below.
All in all, I suggest a design rethink.
CREATE TABLE dbo.YourTable (col1 int NOT NULL,
col2 varchar(10) NOT NULL,
col3 date NOT NULL);
GO
CREATE TRIGGER dbo.YourTable_Min3Col1 ON dbo.YourTable
AFTER INSERT, UPDATE, DELETE
AS
BEGIN
SET NOCOUNT ON;
IF EXISTS (SELECT 1
FROM dbo.YourTable YT
WHERE YT.Col1 IN (SELECT i.col1
FROM inserted i
UNION ALL
SELECT d.col1
FROM deleted d)
GROUP BY Col1
HAVING COUNT(Col1) < 3)
--Use an error code appropriate for you
THROW 79845, N'A check rule in the trigger ''YourTable_Min3Col1'' has failed. Less than 3 instances of a value of in the column ''Col1'' exist. The statement has been aborted.', 16;
END;
GO
--Succeeds
INSERT INTO dbo.YourTable (col1,
col2,
col3)
VALUES(1,'abc',GETDATE()),
(1,'def',GETDATE()),
(1,'xyz',GETDATE());
GO
--Fails
INSERT INTO dbo.YourTable (col1,
col2,
col3)
VALUES(2,'abc',GETDATE()),
(2,'def',GETDATE());
GO
--Succeeds
INSERT INTO dbo.YourTable (col1,
col2,
col3)
VALUES(3,'abc',GETDATE()),
(3,'def',GETDATE()),
(3,'xyz',GETDATE()),
(3,'opq',GETDATE());
GO
--Fails, despite that at the end of the batch there would be 3 rows with a value of 4
INSERT INTO dbo.YourTable (col1,
col2,
col3)
VALUES(4,'abc',GETDATE()),
(4,'def',GETDATE());
UPDATE dbo.YourTable
SET col1 = 4
WHERE col1 = 3
AND col2 = 'opq';
GO
--Use a "proxy" row to enforce it works
INSERT INTO dbo.YourTable (col1,
col2,
col3)
VALUES(4,'abc',GETDATE()),
(4,'def',GETDATE()),
(4,'def','19000101');
UPDATE dbo.YourTable
SET col1 = 4
WHERE col1 = 3
AND col2 = 'opq';
DELETE
FROM dbo.YourTable
WHERE Col1 = 4
AND Col3 = '19000101';
GO
--Fails, would reduce below 3
DELETE
FROM dbo.YourTable
WHERE col1 = 1
AND col2 = 'xyz';
GO
SELECT *
FROM dbo.YourTable;
GO
DROP TABLE dbo.YourTable;
db<>fiddle

how we can Create Primary Keys in a table

How can I create a Primary Key in a table. I used the following command to create one, but the key doesn't appear to be enforced:
ALTER TABLE tablename ADD PRIMARY KEY (column1,column3);
Snowflake supports defining and maintaining constraints, but does not enforce them, except for NOT NULL constraints, which are always enforced.
https://docs.snowflake.com/en/sql-reference/constraints-overview.html#supported-constraint-types
Although the primary key is not enforced in Snowflake, you can use the MERGE statement to insert the data to enforce uniqueness of that key. This approach requires making sure that the source data set does not include duplicates, by using for example QUALIFY function.
Example:
CREATE TABLE IF NOT EXISTS test (
col1 VARCHAR(50) NOT NULL,
col2 VARCHAR(50),
col3 VARCHAR(50) NOT NULL,
constraint PK_test primary key (col1, col3) not enforced
);
MERGE INTO TEST AS tgt
USING (
SELECT COL1, COL2, COL3 FROM (
SELECT 'a' COL1, 'b' COL2, 'a' COL3
UNION
SELECT 'a' COL1, 'c' COL2, 'a' COL3
UNION
SELECT 'a' COL1, 'd' COL2, 'a' COL3)
QUALIFY row_number() over (partition by COL1, COL3 order by COL2) = 1
)AS src
ON (tgt.COL1=src.COL1 AND tgt.COL3=src.COL3)
WHEN NOT MATCHED
THEN INSERT (COL1, COL2, COL3)
VALUES (src.COL1, src.COL2, src.COL3);
Only the row a,b,a was inserted.

Query for Merge values from 3 column and merge as Primary Key in Oracle

In one table few columns are there and in 3 columns i want to merge values from these 3 columns and generate as Primary key after merging these 3 values.
Col1 having Datatype length 4, While col2 & col3 having datatype length 5 & 3 respectively. In col2 & col3 if any values are less than the maximum length then use LPAD with 0 and then after Merge into the Primary Key.
Ex- If col1 = 1234, col2 = 142, col3 = 32 then after merging It should be like "123400142032" as Primary Key.
You probably need something like this.
CREATE TABLE yourtable
(
col1 NUMBER,
col2 NUMBER,
col3 NUMBER
);
ALTER TABLE yourtable ADD (ID NUMBER GENERATED ALWAYS AS ( LPAD(col1,4,0)||LPAD(col2,5,0)||LPAD(col3,3,0) ) );
ALTER TABLE yourtable ADD CONSTRAINT t_test_pk PRIMARY KEY (ID) USING INDEX;
You may then insert only 3 columns and the column id gets automatically populated with a number such as 123400142032.
INSERT INTO yourtable (col1, col2, col3)
VALUES (1234, 142, 32);
Note : The create table script is just for understanding. You may not need it since you already have an existing table.
The virtual column syntax GENERATED .. AS works only in 11g and above. For lower versions, you may require a before insert trigger and a sequence.
with lp as
(select max(length(employee_id)) mp from employee),
llp as
(select max(length(first_name)) mf from employee)
select lp.*,lpad(employee_id,lp.mp,'0'),llp.*, lpad(first_name,llp.mf,'0'),
employee_id||lpad(employee_id,lp.mp,'0')||lpad(first_name,llp.mf,'0')from lp,llp,employee;
note: here employee_id and first_name are the columns, you can assume it as column1 and column2..

SQL Check constraint on column referencing other columns

I want to limit a column that it can only have a value when another column has a value.
example: (this doesn't work)
create table testConstraint (
col1 int not null identity(1, 1) primary key,
col2 int,
col3 int check (col2 is not null),
col4 int)
This is not possible because he cannot reference another column.
Error:
Column CHECK constraint for column 'col3' references another column,
table 'testConstraint'.
Another try was: (also doesn't work)
create table testConstraint (
col1 int not null identity(1, 1) primary key,
col2 int,
col3 int,
col4 int)
GO
alter table testConstraint add constraint ck_columnNotNull check (case when col2 is null then col3 is null end)
GO
Anyone have an idea how this would be possible with a constraint?
You can write a trigger.
Also, you can try this
(1)
ALTER TABLE TestConstraint ADD CONSTRAINT
CK_TestConstraint CHECK (NOT ( (col3 is not null) and (col2 is null) ))
GO
or this
(2)
ALTER TABLE TestConstraint ADD CONSTRAINT
CK_TestConstraint CHECK
(
((col3 is not null) and (col2 is not null)) or
((col3 is null) and (col2 is null))
)
GO
depending on what exactly you need.
I just tested it and it works OK, I think.
insert into
TestConstraint
(col2, col3, col4)
values
(null, 1, 2)
-- ERROR
insert into
TestConstraint
(col2, col3, col4)
values
(1, 1, 2)
-- OK
ALTER TABLE testConstraint
ADD CONSTRAINT ck_columnNotNull
CHECK ( 1 = CASE
WHEN col2 IS NULL AND col3 IS NULL THEN 1
WHEN col2 IS NOT NULL AND col3 IS NOT NULL THEN 1
ELSE 0
END)
Only simple logic is required, plus it needs (as per your second attempt) to be a table check constraint, so you can't declare it inline with the declaration of col3:
create table testConstraint (
col1 int not null identity(1, 1) primary key,
col2 int,
col3 int,
col4 int)
GO
alter table testConstraint add constraint ck_columnNotNull check (
col3 is null
or col2 is not null
)
GO
If col3 is null, then we don't care what the value of col2 is. Conversely, if it's not NULL, then we do want to enforce the col2 isn't null. That's what the two sides of the or effectively give us.

How to block wrong inserts and updates into one table?

Situation is as below. I have a table like here:
Col1 Col2 Col3 Col4 Col5 Col6
a b c 1 e 1
a b c 3 l 1
a b c 1 e 0
a b f 1 f 1
The idea is that I cant update existing data or add new row
which has combination a b c 1 ? 1.
I have to block adding 1 in last column if there
is already some combination of cols 1-3, but I can still may
add same combination with 0 in col 6.
For more complex logic, you use triggers: trigger gets triggered at specified time (insert, update, delete) - before (use word "for") or after.
Your example - trigger that does not allow inserting given combination of columns:
create table MyTable (
Col1 char, Col2 char, Col3 char, Col4 int, Col5 char, Col6 int
);
insert into MyTable (Col1, Col2, Col3, Col4, Col5, Col6)
values ('a', 'b', 'c', 1, 'e', '1');
insert into MyTable (Col1, Col2, Col3, Col4, Col5, Col6)
values ('a', 'b', 'f', 1, 'e', '1');
create trigger [dbo].[trigger_MyTable] on MyTable for insert as
begin
if 0 < (select count(*) from inserted where
Col1='a' and Col2='b' and Col3='c' and Col4=1 and Col6=1)
begin
raiserror 50009 'wrong insert, no way!'
rollback transaction
return
end
end;
-- now it fails
insert into MyTable (Col1, Col2, Col3, Col4, Col5, Col6)
values ('a', 'b', 'c', 1, 'e', '1');
Of course, you can create trigger for update (use "inserted" table), or for delete (use "deleted" table).
You might do other actions (like inserting another row in another table), ...
Give a MSDN try as well: http://msdn.microsoft.com/en-us/library/ms189799(v=SQL.90).aspx
Unless I misunderstood you requirements I think you want a unique constraint across all columns except col5.
Alter Table Foo
ADD CONSTRAINT ak_orf UNIQUE
(Col1, Col2 ,Col3 ,Col4 ,Col6),