Create an index that guarantees uniqueness between union of two columns - postgresql

Consider the following table:
CREATE TABLE items (
ident text NOT NULL,
label_one text NOT NULL,
label_two text
);
Is there a way I could create a uniqueness constraint, on ident and either label_one and label_two?
So for example:
This insert would work:
INSERT INTO items (ident, label_one, label_two) VALUES ('foo', 'a', 'b')
But these inserts would fail:
INSERT INTO items (ident, label_one, label_two) VALUES ('foo', 'a', 'x')
It can't insert 'a' into label_one, Because label_one already has the value 'a'
INSERT INTO items (ident, label_one, label_two) VALUES ('foo', 'b', 'x')
It can't insert 'b' into label_one, Because label_two already has the value 'b'
INSERT INTO items (ident, label_one, label_two) VALUES ('foo', 'x', 'a')
It can't insert 'a' into label_two, Because label_one already has the value 'a'
INSERT INTO items (ident, label_one, label_two) VALUES ('foo', 'x', 'b')
It can't insert 'b' into label_two, Because label_two already has the value 'b'

This is basically what The Impaler said in a comment:
Create a secondary table
CREATE TABLE items_constraint (
ident text NOT NULL,
label text NOT NULL,
UNIQUE (ident, label)
);
create a before insert trigger on the original table (pseudo code)
insert into constraint(ident, label) values (new.ident, new.label_one);
if ( new.label_two is not null) {
insert into constraint(ident, label) values (new.ident, new.label_two);
}
Of course, all this smell of denormalized data and the problem would probably go away when the data is properly normalized.

Related

DB2: Insert new rows and ignore duplicates

I have about 100 rows to insert in a table - Some of them have already been inserted before and some of them have not
This is my insert that works fine if the primary key doesn't exist.. I'm going to run this 100 time with different values each time.. However, if the primary key exist, it fails and stop future commands to run.
How to ignore the failure and keep going or simply ignore duplicates?
INSERT INTO MY_TABLE
VALUES( 12342, 'fdbvdfb', 'svsdv', '5019 teR','' , 'saa', 'AL',35005 , 'C', 37, '0',368 , 'P', '2023-02-13', '2023-01-01', '2023-01-10', '2023-01-20','' , 'Test', 'Test', 'Test', 'JFK', '', null, 'Y', 'Y', '', '', '', '', '', '',2385 ,2 , '', 'N', '2023-01-16', '2023-01-20', '', NULL,NULL, NULL, NULL, 'Y', 'Test', 'Test', '', 'N', 'Test', '')
This is the error:
SQL0803N One or more values in the INSERT statement, UPDATE statement, or foreign key update caused by a DELETE statement are not valid because the primary key, unique constraint or unique index identified by "XPS01ME1" constrains
Insert IGNORE into throws:
The use of the reserved word "IGNORE" following "" is not valid.
If it can help I'm using WinSQL 10.0.157.697
You don't mention what platform and version of Db2, so I'll point you to the Linux/Unix/Windows (LUW) documentation for the MERGE statement...
Since I don't know your table or column names, I'll just give you an example with dummy names.
merge into MYTABLE as tgt
using (select *
from table( values(1,1,'CMW',5,1)
) tmp ( tblKey, fld1, fld2, fld3, fld4)
) as src
on src.tblKey = tgt.tblekey
when not matched then
insert ( tblKey, fld1, fld2, fld3, fld4)
values ( src.tblKey, src.fld1, src.fld2, src.fld3, src.fld4);
You're basically building a temporary table on the fly of one row
table( values(1,1,'CMW',5,1) ) tmp ( tblKey, fld1, fld2, fld3, fld4)
Then if there's no matching record via on src.tblKey = tgt.tblekey you do an insert.
Note that while you could do this 100 times, it is a much better performing solution to do all 100 rows at a time.
merge into MYTABLE as tgt
using (select *
from table( values (1,1,'CMW1',5,1)
, (2,11,'CMW2',50,11)
, (3,21,'CMW3',8,21)
-- , <more rows here>
) tmp ( tblKey, fld1, fld2, fld3, fld4)
) as src
on src.tblKey = tgt.tblekey
when not matched then
insert ( tblKey, fld1, fld2, fld3, fld4)
values ( src.tblKey, src.fld1, src.fld2, src.fld3, src.fld4);
Optionally, you could create an actual temporary table, insert the 100 rows (preferably in a single insert) and then use MERGE.
You may do it with a compound statement like below:
--#SET TERMINATOR #
CREATE TABLE MY_TABLE (ID INT NOT NULL PRIMARY KEY)#
BEGIN
DECLARE CONTINUE HANDLER FOR SQLSTATE '23505' BEGIN END;
INSERT INTO MY_TABLE (ID) VALUES (1);
END#
BEGIN
DECLARE CONTINUE HANDLER FOR SQLSTATE '23505' BEGIN END;
INSERT INTO MY_TABLE (ID) VALUES (1), (2), (3);
END#
BEGIN
DECLARE CONTINUE HANDLER FOR SQLSTATE '23505' BEGIN END;
INSERT INTO MY_TABLE (ID) VALUES (4);
END#
SELECT * FROM MY_TABLE#
ID
1
4
fiddle
This is how you ignore an error in db2 --
note, this is not the correct sqlstate for your problem -- replace with the one you need
DECLARE CONTINUE HANDLER FOR SQLSTATE '23505'
BEGIN -- ignore error for duplicate value
END;
Documented here
https://www.ibm.com/docs/en/db2-for-zos/12?topic=procedure-ignoring-condition-in-sql

PostgreSQL transform value from jsonb column to other column

I have a PostgreSQL database v10 with the following data:
CREATE TABLE test (
id INT,
custom_fields jsonb not null default '{}'::jsonb,
guest_profile_id character varying(100)
);
INSERT INTO test (id, custom_fields) VALUES (1, '[{"protelSurname": "Smith", "servicio_tags": ["protel-info"], "protelUniqueID": "[{\"ID\":\"Test1-ID\",\"Type\":\"21\",\"ID_Context\":\"GHA\"}{\"ID\":\"4842148\",\"Type\":\"1\",\"ID_Context\":\"protelIO\"}]", "protelGivenName": "Seth"}, {"value": "Test", "display_name": "Traces", "servicio_tags": ["trace"]}, {...}]');
INSERT INTO test (id, custom_fields) VALUES (2, '[{"protelSurname": "Smith", "servicio_tags": ["protel-info"], "protelUniqueID": "[{\"ID\":\"Test2-ID\",\"Type\":\"21\",\"ID_Context\":\"GHA\"},{\"ID\":\"4842148\",\"Type\":\"1\",\"ID_Context\":\"protelIO\"}]", "protelGivenName": "Seth"}, {"value": "Test2", "display_name": "Traces", "servicio_tags": ["trace"]}, {...}]');
INSERT INTO test (id, custom_fields) VALUES (3, '[{"value": "Test3-ID", "display_name": "Test", "servicio_tags": ["person-name"]}, {...}]');
INSERT INTO test (id, custom_fields) VALUES (4, '[{"value": "Test4-ID", "display_name": "Test", "servicio_tags": ["profile-id"]}, {...}]');
There are way more records in the real table.
Goal: I want to transfer the TestX-ID values into the column guest_profile_id in the same row. And only those values not the other JSONB objects or values etc.
My try:
do $$
declare
colvar varchar;
begin
select x ->> 'ID' from (select jsonb_array_elements(f) from (
select (field ->>'protelUniqueID')::jsonb f
FROM guest_group gg,
lateral jsonb_array_elements(custom_fields) AS field
WHERE value #> '{"servicio_tags": ["protel-info"]}'::jsonb
) d(f)) dd(x)
where x->>'ID_Context'='protelIO'
into colvar;
raise notice 'colvar: %', colvar;
end
$$;
execute format('UPDATE guest_group SET guest_profile_id = %s, colvar);
My Result: It only takes Test1-ID and stores it in all rows in the guest_profile_id column.
My Problem: I want to store each TestX-ID in the custom_fields column into the guest_profile_id column in the same row.
My assumption: I need to add a loop to this query. If the query up there does not find any value, the loop should try the next query: e.g.:
SELECT field ->>'value'
FROM guest_group gg
cross join lateral jsonb_array_elements(custom_fields) AS field
WHERE value #> '{"servicio_tags": ["profile-id"]}'::jsonb
And then the next:
SELECT field ->>'value'
FROM guest_group gg
cross join lateral jsonb_array_elements(custom_fields) AS field
WHERE value #> '{"servicio_tags": ["person-name"]}'::jsonb
When all TestX-ID values are copied into the guest_profile_id column in the same row, the goal is reached.
How can I put all this together? Thanks a lot for the help.
I want to store each TestX-ID in the custom_fields column into the guest_profile_id column in the same row.
No need for PL/PGSQL, loops or dynamic sql. Just use a single query of the form
UPDATE guest_group
SET guest_profile_id = (/* complex expression */);
In your case, with that complex expression it amounts to
UPDATE guest_group
SET guest_profile_id = (
SELECT x ->> 'ID'
FROM jsonb_array_elements(custom_fields) AS field,
jsonb_array_elements(field ->> 'protelUniqueID') AS dd(x)
WHERE value #> '{"servicio_tags": ["protel-info"]}'::jsonb
AND x->>'ID_Context' = 'protelIO'
);
If the query up there does not find any value, it should try the next query
You can use the COALESCE function for that, or add some OR conditions to your query, or even use a UNION. Alternatively, add a WHERE guest_profile_id IS NULL to the update statement to exclude those rows that already have a value, and do multiple successive updates.

How to create a unique index for a subset of rows in a db2 table

I am trying to create a unique index for a subset of data in a particular table. The existing data is something like this -
But the actual data should look like this -
The subset of rows will be the rows with the condition status as A or B. For these set of rows, the unique_id and amount value combination should be unique.
The DB2 version been used here is 9.7 on a windows server. Is partial index or conditional index possible in DB2?
New table
create or replace function generate_unique_det()
returns varchar(13) for bit data
deterministic
no external action
contains sql
return generate_unique();
create table test_unique (
unique_id int not null
, status char(1) not null
, amount int not null
, status2 varchar(13) for bit data not null generated always as
(case when status in ('A', 'B') then '' else generate_unique_det() end)
) in userspace1;
create unique index test_unique1 on test_unique (unique_id, amount, status2);
insert into test_unique (unique_id, status, amount)
values
(1234, 'A', 400)
--, (1234, 'B', 400)
, (1234, 'Z', 400)
, (1234, 'Z', 400);
The standard generate_unique function is not deterministic.
Such functions are not allowed in the generated always clause.
This is why we create our own function based on the standard one.
The problem with such a "fake" function could be, if Db2 not actually called this function for each updated / inserted row during a multi-row change operation (why to call the deterministic function multiple times on the same set of parameters, if it's enough to do it once and reuse the result for other affected rows afterwards). But it works in reality - Db2 does call such a function for every affected row, which is desired in our case.
You are not able to insert the commented out row in the last statement.
Existing table
set integrity for test_unique off;
alter table test_unique add
status2 varchar(13) for bit data not null
generated always as (case when status in ('A', 'B') then '' else generate_unique_det() end);
set integrity for test_unique immediate checked force generated;
-- If you need to save the rows violated future unique index
create table test_unique_exc like test_unique in userspace1;
-- If you don't need to save the the rows violated future unique index,
-- then just run the inner DELETE statement only,
-- which just removes these rows.
-- The whole statement inserts the deleted rows into the "exception table".
with d as (
select unique_id, status, amount, status2
from old table (
delete from (select t.*, rownumber() over (partition by unique_id, amount, status2) rn_ from test_unique t) where rn_>1
)
)
select count(1)
from new table (
insert into test_unique_exc select * from d
);
create unique index test_unique1 on test_unique (unique_id, amount, status2);

How to load data as nested JSONB from non-JSONB postgres tables

I'm trying to construct an object for use from my postgres backend. The tables in question look something like this:
We have some Things that essentially act as rows for a matrix where the columns are Field_Columns. Field_Values are filled cells.
Create Table Platform_User (
serial id PRIMARY KEY
)
Create Table Things (
serial id PRIMARY KEY,
INTEGER user_id REFERENCES Platform_User(id)
)
Create Table Field_Columns (
serial id PRIMARY KEY,
TEXT name,
)
Create Table Field_Values (
INTEGER field_column_id REFERENCES Field_Columns(id),
INTEGER thing_id REFERENCES Things(id)
TEXT content,
PRIMARY_KEY(field_column_id, thing_id)
)
This would be simple if I were trying to load just the Field_Values for a single Thing as JSON, which would look like this:
SELECT JSONB_OBJECT(
ARRAY(
SELECT name
FROM Field_Columns
ORDER BY Field_Columns.id
),
ARRAY(
SELECT Field_Values.content
FROM Fields_Columns
LEFT JOIN Field_Values ON Field_Values.field_column_id = Field_Columns.id
AND Field_Values.thing_id = Things.id
ORDER BY Field_Columns.id)
)
)
FROM Things
WHERE Thing.id = $1
however, I'd like to construct the JSON object to look like this when returned. I want to get an object of all the Fields:Field_Values objects for the Things that a user owns
{
14:
{
'first field':'asdf',
'other field':''
}
25:
{
'first field':'qwer',
'other field':'dfgdsfg'
}
43:
{
'first field':'',
'other field':''
}
}
My efforts to construct this query look like this, but I'm running into the problem where the JSONB object function doesn't want to construct an object where the value of the field is an object itself
SELECT (
JSONB_OBJECT(
ARRAY(SELECT Things.id::TEXT
FROM Things
WHERE Things.user_id = $2
ORDER BY Things.id
),
ARRAY(SELECT JSONB_OBJECT(
ARRAY(
SELECT name
FROM Field_Columns
ORDER BY Field_Columns.id),
ARRAY(
SELECT Field_Values.content
FROM Field_Columns
LEFT JOIN Field_Values ON Field_Values.field_column_Id = Field_Columns.id
AND Field_Values.thing_id = Things.id
ORDER BY Field_Columns.id)
)
FROM Things
WHERE Things.user_id = $2
ORDER BY Things.id
)
)
) AS thing_fields
The specific error I get is function jsonb_object(text[], jsonb[]) does not exist. Is there a way to do this that doesn't involve copious text conversions and nonsense like that? Or will I just need to abandon trying to sort my data in the query and do it in my code instead.
Your DDL scripts are syntactically incorrect so I created these for you:
create table platform_users (
id int8 PRIMARY KEY
);
create table things (
id int8 PRIMARY KEY,
user_id int8 REFERENCES platform_users(id)
);
create table field_columns (
id int8 PRIMARY KEY,
name text
);
create table field_values (
field_column_id int8 REFERENCES field_columns(id),
thing_id int8 REFERENCES things(id),
content text,
PRIMARY KEY(field_column_id, thing_id)
);
I also created some scripts to populate the db:
insert into platform_users(id) values (1);
insert into platform_users(id) values (2);
insert into platform_users(id) values (3);
insert into platform_users(id) values (4);
insert into platform_users(id) values (5);
insert into things(id, user_id) values(1, 1);
insert into things(id, user_id) values(2, 1);
insert into things(id, user_id) values(3, 2);
insert into things(id, user_id) values(4, 2);
insert into field_columns(id, name) values(1, 'col1');
insert into field_columns(id, name) values(2, 'col2');
insert into field_values(field_column_id, thing_id, content) values(1, 1, 'thing1 val1');
insert into field_values(field_column_id, thing_id, content) values(2, 1, 'thing1 val2');
insert into field_values(field_column_id, thing_id, content) values(1, 2, 'thing2 val1');
insert into field_values(field_column_id, thing_id, content) values(2, 2, 'thing2 val2');
Please include such scripts next time when you ask for help, and make sure that your scripts are correct. This will reduce the work needed to answer your question.
You can get your jsonb value by aggregating the key value pairs with jsonb_object_agg
select
t.id,
jsonb_object_agg(fc.name, fv.content)
from
things t inner join
field_values fv on fv.thing_id = t.id inner join
field_columns fc on fv.field_column_id = fc.id
group by 1
The results looking like this:
thing_id;jsonb_value
1;"{"col1": "thing1 val1", "col2": "thing1 val2"}"
2;"{"col1": "thing2 val1", "col2": "thing2 val2"}"

PostgreSQL 9.5 UPSERT in rule

I have an INSERT rule in an updatable view system, for which I would like to realize an UPSERT, such as :
CREATE OR REPLACE RULE _insert AS
ON INSERT TO vue_pays_gex.bals
DO INSTEAD (
INSERT INTO geo_pays_gex.voie(name, code, district) VALUES (new.name, new.code, new.district)
ON CONFLICT DO NOTHING;
But my since there can be many different combinations of these three columns, I don't think I can set a CONSTRAINT including them all (although I may be missing a point of understanding in the SQL logics), hence nullifying the ON CONFLIT DO NOTHING part.
The ideal solution would seem to be the use of an EXCEPT, but it only works in an INSERT INTO SELECT statement. Is there a way to use an INSERT INTO SELECT statement referring to the newly inserted row? Something like FROM new.bals (in my case)?
If not I could imagine a WHERE NOT EXISTS condition, but the same problem than before arises.
I'm guessing it is a rather common SQL need, but cannot find how to solve it. Any idea?
EDIT :
As requested, here is the table definition :
CREATE TABLE geo_pays_gex.voie
(
id_voie serial NOT NULL,
name character varying(50),
code character varying(15),
district character varying(50),
CONSTRAINT prk_constraint_voie PRIMARY KEY (id_voie),
CONSTRAINT voie_unique_key UNIQUE (name, code, district)
);
How do you define uniqueness? If it is the combination of name + code + district, then just add a constraint UNIQUE(name, code, district) on the table geo_pays_gex.voie. The 3, together, must be unique... but you can have several time the same name, or code, or district.
See it at http://rextester.com/EWR73154
EDIT ***
Since you can have Nulls and want to treat them as a unique value, you can replace the constraint creation by a unique index that replace the nulls
CREATE UNIQUE INDEX
voie_uniq ON voie
(COALESCE(name,''), code, COALESCE(district,''));
In addition to #JGH's answer.
INSERT in rule for INSERT will lead to infinity recursion (Postgres 9.6).
Full (NOT)runnable example:
CREATE SCHEMA ttest;
CREATE TABLE ttest.table_1 (
id bigserial
CONSTRAINT pk_table_1 PRIMARY KEY,
col_1 text,
col_2 text
);
CREATE OR REPLACE RULE table_1_always_upsert AS
ON INSERT TO ttest.table_1
DO INSTEAD (
INSERT INTO ttest.table_1(id, col_1, col_2)
VALUES (new.id, new.col_1, new.col_2)
ON CONFLICT ON CONSTRAINT pk_table_1
DO UPDATE
SET col_1 = new.col_1,
col_2 = new.col_2
);
INSERT INTO ttest.table_1(id, col_1, col_2) -- will result error: infinity recursion in rules
VALUES (1, 'One', 'A'),
(2, 'Two', 'B');
INSERT INTO ttest.table_1(id, col_1, col_2)
VALUES (1, 'One_updated', 'A_updated'),
(2, 'Two_updated', 'B_updated'),
(3, 'Three_inserted', 'C_inserted');
SELECT *
FROM ttest.table_1;