How create a column that increase according to the value of another column - postgresql

I have a table where I want to put all the information about articles use, and I need to create a column with autoincrement, where the ID can have the same value if the field (tipo) have another value, unique for this particular ID. For example:
ID / TIPO
1 / AJE -- Is Ok
1 / AJS -- Is Ok (because this Tipo is AJS, different from AJE)
1 / SI -- Is Ok
2 / AJE -- Is Ok (New ID)
2 / AJE -- Is Wrong, because ID=2, TIPO=AJE already exist.
I've done the unique sentence:
ALTER TABLE public.art_movimientos
ADD CONSTRAINT uk_mov UNIQUE (id,tipo) USING INDEX TABLESPACE sistema_index;
But how I can create the autoincrement covering two columns?
My table Code:
CREATE TABLE public.art_movimientos
(
id integer NOT NULL DEFAULT nextval('seq_art_movimientos'::regclass),
tipo character(3) NOT NULL, -- Tipos de Valores:...
documento integer,
fecha_doc date[] NOT NULL,
fecha_mov date[] NOT NULL,
calmacen integer NOT NULL,
status character(13) NOT NULL DEFAULT 'PENDIENTE'::bpchar, -- PENDIENTE...
mes integer NOT NULL,
"año" integer NOT NULL,
donado integer NOT NULL DEFAULT 0
)
WITH (
OIDS=FALSE
);

You can manage this situation by using an before insert trigger, mimicking the behaviour statedby #dhke:
CREATE TABLE art_movimientos
(
id integer NOT NULL DEFAULT NULL, -- You don't want a serial, nor a default
tipo character(3) NOT NULL, -- Tipos de Valores:...
documento integer,
fecha_doc date[] NOT NULL,
fecha_mov date[] NOT NULL,
calmacen integer NOT NULL,
status character(13) NOT NULL DEFAULT 'PENDIENTE'::bpchar, -- PENDIENTE...
mes integer NOT NULL,
"año" integer NOT NULL,
donado integer NOT NULL DEFAULT 0,
/* You have actually a 2-column Primary Key */
PRIMARY KEY (tipo, id)
);
-- Create a trigger function to generate 'id'
CREATE FUNCTION art_movimientos_insert_trigger()
RETURNS trigger
AS
$$
BEGIN
/* Compute "id", as the following id for a certain "tipo" */
new.id = coalesce(
(SELECT max(id) + 1
FROM art_movimientos a
WHERE a.tipo = new.tipo), 1);
return new;
END
$$
LANGUAGE 'plpgsql'
VOLATILE ;
-- This trigger will be called whenever a new row is inserted, and "id" is
-- not specified (i.e.: it defaults to null), or is specified as null
CREATE TRIGGER art_movimientos_ins_trg
BEFORE INSERT
ON art_movimientos
FOR EACH ROW
WHEN (new.id IS NULL)
EXECUTE PROCEDURE art_movimientos_insert_trigger();
You can then insert the following rows (without specifying the id column):
INSERT INTO art_movimientos
(tipo, documento, fecha_doc, fecha_mov, calmacen, mes, "año")
VALUES
('AJE', 1, array['20170128'::date], array['20170128'::date], 1, 1, 2017),
('AJS', 2, array['20170128'::date], array['20170128'::date], 1, 1, 2017),
('SI', 3, array['20170128'::date], array['20170128'::date], 1, 1, 2017),
('AJE', 4, array['20170128'::date], array['20170128'::date], 1, 1, 2017),
('AJE', 5, array['20170128'::date], array['20170128'::date], 1, 1, 2017) ;
... and see that you get what you intended:
SELECT
id, tipo
FROM
art_movimientos
ORDER BY
documento ;
| id | tipo |
|----|------|
| 1 | AJE |
| 1 | AJS |
| 1 | SI |
| 2 | AJE |
| 3 | AJE |
You can check everything a SQLFiddle (which is a bit picky about PL/pgSQL functions and semicolons).
Side note: There can be a few corner cases where this procedure might fail within a transaction because of deadlocks and/or race conditions, if other transactions are also inserting data into the same table at the same time. So, your overall code should be able to handle aborted transactions, and retry them or show an error to the user.

Related

PostgreSQL Update TRIGGER Fires Multiple Times When Just 1 Row Updated

Source Table :-
CREATE TABLE schema1.Source_Table
(
Source_Table_id serial NOT NULL,
current_status_id smallint NOT NULL,
current_status_reason varchar(200) NULL,
requestor_id integer NOT NULL,
approver_id integer NULL,
last_upd_user_id integer NOT NULL,
last_upd_date_time timestamp without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
CONSTRAINT PK_Source_Table PRIMARY KEY (Source_Table_id)
)
WITH OIDS;
Destination Table (Audit History Purpose) :-
CREATE TABLE schema2.Destination_Table
(
type_id smallint NOT NULL,
id integer NOT NULL,
state_id smallint NOT NULL,
state_reason varchar(200) NULL,
requestor_id integer NOT NULL,
approver_id integer NULL,
upd_by_user_id integer NOT NULL,
upd_by_user_type smallint NOT NULL,
upd_date_time timestamp without time zone NOT NULL
)
WITH OIDS;
After Update for each Row Trigger on the Source Table :-
CREATE TRIGGER trg_upd_Source_Table
AFTER UPDATE of current_status_id
ON schema1.Source_Table
FOR EACH ROW
WHEN (OLD.current_status_id IS DISTINCT FROM NEW.current_status_id)
EXECUTE PROCEDURE schema1.Source_Table_hist();
Trigger Function for the After Update for each Row Trigger above :-
CREATE OR REPLACE FUNCTION schema1.Source_Table_hist()
RETURNS TRIGGER
LANGUAGE PLPGSQL
AS $$
BEGIN
INSERT INTO schema2.Destination_Table
(type_id, id, state_id, state_reason, requestor_id, approver_id, upd_by_user_id,
upd_by_user_type, upd_date_time)
SELECT 1, OLD.Source_Table_id, OLD.current_status_id, OLD.current_status_reason,
OLD.requestor_id, OLD.approver_id, OLD.last_upd_user_id, 1, OLD.last_upd_date_time
from schema1.Source_Table
where OLD.current_status_id IS DISTINCT FROM NEW.current_status_id;
RETURN NULL;
END;
$$
There are already 8 rows in schema1.Source_Table table with the unique primary key Source_Table_id.
When I update just 1 row of this table as below using the primary key, it inserts 8 rows (1 original and 7 duplicates) into the schema2.Destination_Table table instead of just 1 row.
update schema1.Source_Table
set current_status_id = 4
where Source_Table_id = 9;
The issue here is :-
Why the trigger is firing for 8 times (which is equals to the total number of rows in the table on which this trigger is created) when only 1 row of that table is updated.
Expected Behavior :-
The Trigger should fire only once followed by inserting 1 row in the destination audit table when just 1 row is updated in the source table on which the trigger is created.
How to solve this issue ?
The trigger isn't firing multiple times, your query is inserting a row into the hist table for every row in the source table:
INSERT INTO schema2.Destination_Table
(type_id, id, state_id, state_reason, requestor_id, approver_id, upd_by_user_id,
upd_by_user_type, upd_date_time)
SELECT 1, OLD.Source_Table_id, OLD.current_status_id, OLD.current_status_reason,
OLD.requestor_id, OLD.approver_id, OLD.last_upd_user_id, 1, OLD.last_upd_date_time
from schema1.Source_Table
where OLD.current_status_id IS DISTINCT FROM NEW.current_status_id;
RETURN NULL;
I don't think you need that from clause.
The problem is in the WHERE condition:
where OLD.current_status_id IS DISTINCT FROM NEW.current_status_id
This condition is known to be true from the WHEN condition on the trigger. As it is the only WHERE effective it is the same no WHERE condition at all, there fore all roes are processed by the insert. Suggest
where current_status_id = OLD.current_status_id

PostgreSQL recursive parent/child query

I'm having some trouble working out the PostgreSQL documentation for recursive queries, and wonder if anyone might be able to offer a suggestion for the following.
Here's the data:
Table "public.subjects"
Column | Type | Collation | Nullable | Default
-------------------+-----------------------------+-----------+----------+--------------------------------------
id | bigint | | not null | nextval('subjects_id_seq'::regclass)
name | character varying | | |
Table "public.subject_associations"
Column | Type | Collation | Nullable | Default
------------+-----------------------------+-----------+----------+--------------------------------------------------
id | bigint | | not null | nextval('subject_associations_id_seq'::regclass)
parent_id | integer | | |
child_id | integer | | |
Here, a "subject" may have many parents and many children. Of course, at the top level a subject has no parents and at the bottom no children. For example:
parent_id | child_id
------------+------------
2 | 3
1 | 4
1 | 3
4 | 8
4 | 5
5 | 6
6 | 7
What I'm looking for is starting with a child_id to get all the ancestors, and with a parent_id, all the descendants. Therefore:
parent_id 1 -> children 3, 4, 5, 6, 7, 8
parent_id 2 -> children 3
child_id 3 -> parents 1, 2
child_id 4 -> parents 1
child_id 7 -> parents 6, 5, 4, 1
Though there seem to be a lot of examples of similar things about I'm having trouble making sense of them, so any suggestions I can try out would be welcome.
To get all children for subject 1, you can use
WITH RECURSIVE c AS (
SELECT 1 AS id
UNION ALL
SELECT sa.child_id
FROM subject_associations AS sa
JOIN c ON c.id = sa. parent_id
)
SELECT id FROM c;
CREATE OR REPLACE FUNCTION func_finddescendants(start_id integer)
RETURNS SETOF subject_associations
AS $$
DECLARE
BEGIN
RETURN QUERY
WITH RECURSIVE t
AS
(
SELECT *
FROM subject_associations sa
WHERE sa.id = start_id
UNION ALL
SELECT next.*
FROM t prev
JOIN subject_associations next ON (next.parentid = prev.id)
)
SELECT * FROM t;
END;
$$ LANGUAGE PLPGSQL;
Try this
--- Table
-- DROP SEQUENCE public.data_id_seq;
CREATE SEQUENCE "data_id_seq"
INCREMENT 1
MINVALUE 1
MAXVALUE 9223372036854775807
START 1
CACHE 1;
ALTER TABLE public.data_id_seq
OWNER TO postgres;
CREATE TABLE public.data
(
id integer NOT NULL DEFAULT nextval('data_id_seq'::regclass),
name character varying(50) NOT NULL,
label character varying(50) NOT NULL,
parent_id integer NOT NULL,
CONSTRAINT data_pkey PRIMARY KEY (id),
CONSTRAINT data_name_parent_id_unique UNIQUE (name, parent_id)
)
WITH (
OIDS=FALSE
);
INSERT INTO public.data(id, name, label, parent_id) VALUES (1,'animal','Animal',0);
INSERT INTO public.data(id, name, label, parent_id) VALUES (5,'birds','Birds',1);
INSERT INTO public.data(id, name, label, parent_id) VALUES (6,'fish','Fish',1);
INSERT INTO public.data(id, name, label, parent_id) VALUES (7,'parrot','Parrot',5);
INSERT INTO public.data(id, name, label, parent_id) VALUES (8,'barb','Barb',6);
--- Function
CREATE OR REPLACE FUNCTION public.get_all_children_of_parent(use_parent integer) RETURNS integer[] AS
$BODY$
DECLARE
process_parents INT4[] := ARRAY[ use_parent ];
children INT4[] := '{}';
new_children INT4[];
BEGIN
WHILE ( array_upper( process_parents, 1 ) IS NOT NULL ) LOOP
new_children := ARRAY( SELECT id FROM data WHERE parent_id = ANY( process_parents ) AND id <> ALL( children ) );
children := children || new_children;
process_parents := new_children;
END LOOP;
RETURN children;
END;
$BODY$
LANGUAGE plpgsql VOLATILE COST 100;
ALTER FUNCTION public.get_all_children_of_parent(integer) OWNER TO postgres
--- Test
SELECT * FROM data WHERE id = any(get_all_children_of_parent(1))
SELECT * FROM data WHERE id = any(get_all_children_of_parent(5))
SELECT * FROM data WHERE id = any(get_all_children_of_parent(6))

Look for a unique value using two variables

I need to implement the ISSUES column by using REFERENCE and LOCALISATION variables for each rows with unique values stocked on Table_issues_Localisation.
The problem is, those two variables make a two dimension table, so I have to dynamically select the right column of LOCALISATION.
Here is an explanation of what I need to do with an image. I'm sorry for posting an image but I think this is way more understandable.
I tried to make a query to update row by row the Table_Observation.ISSUES column with informations stocked on variable columns (=SELECT(SELECT))) of Table_issues_Localisation.
On Table_Observation.ROW_NUMBER column indicates the number of each rows. It is used for the loop.
DO $$
DECLARE
my_variable TEXT;
BEGIN
FOR i IN 1..35 LOOP
my_variable = SELECT((SELECT LOCALISATION FROM Table_Observation WHERE Table_Observation.ROW_NUMBER = i) FROM Table__issues_Localisation ON Table_Observation.REFERENCE = Table__issues_Localisation.REFERENCE)
UPDATE Table_Observation
SET ISSUES = my_variable
WHERE Table_Observation.ROW_NUMBER = i
END LOOP;
END;
$$
Postgres v 9.4
I hope I'm clear enough,
You don't need PL/pgSQL or a loop for this. You can do that with a single update statement:
update observation o
set issues = row_to_json(il) ->> o.localisation
from issues_localisation il
where il.reference = o.reference;
This requires that the values in observation.localisation exactly map to the column names in issues_localisation.
With the following test data:
create table observation
(
rn integer primary key,
reference integer,
localisation text,
issues text
);
create table issues_localisation
(
reference integer,
issues_12 text,
issues_17 text,
issues_27 text,
issues_34 text
);
insert into observation (rn, reference, localisation)
values
(1, 27568, 'issues_27'),
(2, 6492, 'issues_34'),
(3, 1529, 'issues_34'),
(4, 1529, 'issues_34'),
(5, 709, 'issues_12');
insert into issues_localisation (reference, issues_12, issues_17, issues_27, issues_34)
values
(29, 'FB', 'FB', 'TFB', null),
(506, 'M', null, 'M', null),
(709, 'TF', null, null, null),
(1234, null, 'TF', 'TF', null),
(1529, 'FB', 'FB', 'FB', 'M'),
(3548, null, 'M', null, null),
(6492, 'FB', 'FB', 'FB', null),
(18210, 'TFB', null, 'TFB', 'TFB'),
(27568, 'TF', null, 'TF', 'TF');
The update will result in this data in the table observation:
rn | reference | localisation | issues
---+-----------+--------------+-------
1 | 27568 | issues_27 | TF
2 | 6492 | issues_34 |
3 | 1529 | issues_34 | M
4 | 1529 | issues_34 | M
5 | 709 | issues_12 | TF
Online example: http://rextester.com/OCGFM81609
For your next question you should supply the sample data (and the expected output) the way I did in my answer.
I removed the completely useless prefix table_ from the table names as well. That is horrible naming convention.
And here is an (unfinished, still need to execute) example of dynamic sql:
CREATE FUNCTION bagger (_name text) RETURNS text
AS
$func$
DECLARE upd text;
BEGIN
upd := format('
UPDATE observation dst
SET issues = src.%I
FROM issues_localisation src
WHERE src.reference = dst.reference
AND dst.localisation = %L
;', _name, _name);
-- RAISE NOTICE upd;
RETURN upd;
END
$func$
LANGUAGE plpgsql
;
-- SELECT bagger('issues_12' );
WITH loc AS (
SELECT DISTINCT localisation AS loc
FROM observation
)
SELECT bagger( loc.loc)
FROM loc
;

PostgreSQL: How to create a foreign key on a table with inheritance

I'm trying to create the following tables structure:
create table base_auditoria (
ip_usuario_aud character varying(30) not null,
id_usuarioa_aud integer not null
);
create table pessoa (
id serial not null,
nome character varying not null,
constraint pk__pessoa__id primary key (id)
)
inherits (base_auditoria);
create table pessoa_fisica (
cpf character varying(11) not null,
nome_mae character varying not null
)
inherits (pessoa);
create table usuario (
id serial not null,
id_pessoa integer not null,
email character varying(500) not null,
constraint pk_usuario_id primary key (id)
)
inherits (base_auditoria);
After creating them, insert only one record in the table "usuario" and only one record in the table "pessoa_fisica":
INSERT INTO pessoa_fisica (cpf, nome_mae, nome, ip_usuario_aud, id_usuario_aud)
VALUES ('cpf 001', 'nome mae 001', 'pessoa.nome 001', '255.255.255.255', 0);
INSERT INTO usuario (id_pessoa, email, ip_usuario_aud, id_usuario_aud)
VALUES (
(SELECT id FROM pessoa_fisica WHERE cpf = 'cpf 001' LIMIT 1),
'test#test', '0.0.0.0', 0
);
So I update the two records to contain the correct identifiers that are foreign keys:
UPDATE usuario SET id_usuario_aud = id;
UPDATE pessoa_fisica SET id_usuario_aud = (SELECT id FROM usuario LIMIT 1);
When I run the SELECT, I get the expected answers:
db=# SELECT * FROM usuario;
ip_usuario_aud | id_usuario_aud | id | id_pessoa | email
----------------+----------------+----+-----------+-----------
0.0.0.0 | 1 | 1 | 1 | test#test
(1 registro)
db=# SELECT * FROM pessoa_fisica
ip_usuario_aud | id_usuario_aud | id | nome | cpf | nome_mae
-----------------+----------------+----+-----------------+---------+--------------
255.255.255.255 | 1 | 1 | pessoa.nome 001 | cpf 001 | nome mae 001
(1 registro)
ab=# SELECT * FROM pessoa;
ip_usuario_aud | id_usuario_aud | id | nome
-----------------+----------------+----+-----------------
255.255.255.255 | 1 | 1 | pessoa.nome 001
(1 registro)
ab=# SELECT * FROM ONLY pessoa;
ip_usuario_aud | id_usuario_aud | id | nome
----------------+----------------+----+------
(0 registro)
When I try to add a FOREIGN KEY constraint with the following command, I get the error:
alter table usuario
add constraint fk_1
foreign key (id_pessoa)
references pessoa (id)
match simple on update no action on delete no action;
ERROR: insert or update on table "usuario" violates foreign key constraint
"fk_1" DETAIL: Key (id_pessoa) = (1) is not present in the table
"pessoa".
How can I solve this problem?
Solution 1: Remove inheritance and work with foreign key relationships only;
Solution 2: Create a trigger to copy the table data "pessoa_fisica" to "pessoa" with the command:
CREATE OR REPLACE FUNCTION fn_tg_1() RETURNS trigger AS
$BODY$
BEGIN
IF tg_op = 'INSERT' THEN
INSERT INTO pessoa
SELECT ip_usuario_aud, id_usuario_aud, id, nome
FROM pessoa_fisica
WHERE id = new.id;
ELSIF tg_op = 'UPDATE' THEN
UPDATE pessoa SET
ip_usuario_aud = new.ip_usuario_aud,
id_usuario_aud = new.id_usuario_aud,
nome = new.nome
WHERE id = new.id;
ELSIF tg_op = 'DELETE' THEN
DELETE FROM pessoa WHERE id = new.id;
END IF;
END
$BODY$
LANGUAGE plpgsql VOLATILE COST 100;
CREATE TRIGGER tg_1
BEFORE INSERT OR UPDATE OR DELETE
ON pessoa_fisica FOR EACH ROW
EXECUTE PROCEDURE fn_tg_1();
Thus the foreign key "fk_1" can be created.
Is there any other alternative?

Efficiently select the most specific result from a table

I have a table roughly as follows:
CREATE TABLE t_table (
f_userid BIGINT NOT NULL
,f_groupaid BIGINT
,f_groupbid BIGINT
,f_groupcid BIGINT
,f_itemid BIGINT
,f_value TEXT
);
The groups are orthogonal, so no hierarchy can be implied beyond the fact that every entry in the table will have a user ID. There is no uniqueness in any of the columns.
So for example a simple setup might be:
INSERT INTO t_table VALUES (1, NULL, NULL, NULL, NULL, 'Value for anything by user 1');
INSERT INTO t_table VALUES (1, 5, 2, NULL, NULL, 'Value for anything by user 1 in groupA 5 groupB 2');
INSERT INTO t_table VALUES (1, 4, NULL, 1, NULL, 'Value for anything by user 1 in groupA 5 and groupC 1');
INSERT INTO t_table VALUES (2, NULL, NULL, NULL, NULL, 'Value for anything by user 2');
INSERT INTO t_table VALUES (2, 1, NULL, NULL, NULL, 'Value for anything by user 2 in groupA 1');
INSERT INTO t_table VALUES (2, 1, 3, 4, 5, 'Value for item 5 by user 2 in groupA 1 and groupB 3 and groupC 4');
For any given set of user/groupA/groupB/groupC/item I want to be able to obtain the most specific item in the table that applies. If any of the given set are NULL then it can only match relevant columns in the table which contain NULL. For example:
// Exact match
SELECT MostSpecific(1, NULL, NULL, NULL, NULL) => "Value for anything by user 1"
// Match the second entry because groupC and item were not specified in the table and the other items matched
SELECT MostSpecific(1, 5, 2, 3, NULL) => "Value for anything by user 1 in groupA 5 groupB 2"
// Does not match the second entry because groupA is NULL in the query and set in the table
SELECT MostSpecific(1, NULL, 2, 3, 4) => "Value for anything by user 1"
The obvious approach here is for the stored procedure to work through the parameters and find out which are NULL and not, and then call the appropriate SELECT statement. But this seems very inefficient. IS there a better way of doing this?
This should do it, just filter out any non matching rows using a WHERE, then rank the remaining rows by how well they match. If any column doesn't match, the whole bop expression will result in NULL, so we filter that out in an outer query where we also order by match and limit the result to only the single best match.
CREATE FUNCTION MostSpecific(BIGINT, BIGINT, BIGINT, BIGINT, BIGINT)
RETURNS TABLE(f_userid BIGINT, f_groupaid BIGINT, f_groupbid BIGINT, f_groupcid BIGINT, f_itemid BIGINT, f_value TEXT) AS
'WITH cte AS (
SELECT *,
CASE WHEN f_groupaid IS NULL THEN 0 WHEN f_groupaid = $2 THEN 1 END +
CASE WHEN f_groupbid IS NULL THEN 0 WHEN f_groupbid = $3 THEN 1 END +
CASE WHEN f_groupcid IS NULL THEN 0 WHEN f_groupcid = $4 THEN 1 END +
CASE WHEN f_itemid IS NULL THEN 0 WHEN f_itemid = $5 THEN 1 END bop
FROM t_table
WHERE f_userid = $1
AND (f_groupaid IS NULL OR f_groupaid = $2)
AND (f_groupbid IS NULL OR f_groupbid = $3)
AND (f_groupcid IS NULL OR f_groupcid = $4)
AND (f_itemid IS NULL OR f_itemid = $5)
)
SELECT f_userid, f_groupaid, f_groupbid, f_groupcid, f_itemid, f_value FROM cte
WHERE bop IS NOT NULL
ORDER BY bop DESC
LIMIT 1'
LANGUAGE SQL
//
An SQLfiddle to test with.
Try something like:
select *
from t_table t
where f_userid = $p_userid
and (t.f_groupaid is not distinct from $p_groupaid or t.f_groupaid is null) --null in f_groupaid matches both null and not null values
and (t.f_groupbid is not distinct from $p_groupbid or t.f_groupbid is null)
and (t.f_groupcid is not distinct from $p_groupcid or t.f_groupcid is null)
order by (t.f_groupaid is not distinct from $p_groupaid)::int -- order by count of matches
+(t.f_groupbid is not distinct from $p_groupbid)::int
+(t.f_groupcid is not distinct from $p_groupcid)::int desc
limit 1;
It will give you the best match on groups.
A is not distinct from B fill return true if A and B are equal or both null.
::int means cast ( as int). Casting boolean true to int will give 1 (You can not add boolean values directly).
SQL Fiddle
create or replace function mostSpecific(
p_userid bigint,
p_groupaid bigint,
p_groupbid bigint,
p_groupcid bigint,
p_itemid bigint
) returns t_table as $body$
select *
from t_table
order by
(p_userid is not distinct from f_userid or f_userid is null)::integer
+
(p_groupaid is not distinct from f_groupaid or f_userid is null)::integer
+
(p_groupbid is not distinct from f_groupbid or f_userid is null)::integer
+
(p_groupcid is not distinct from f_groupcid or f_userid is null)::integer
+
(p_itemid is not distinct from f_itemid or f_userid is null)::integer
desc
limit 1
;
$body$ language sql;