How to join multiple tables when creating a CTE Recursive Query - postgresql

I am trying to create a recursive query that will take attributes from 3 different tables but I fail to JOIN the tables correctly and therefore I cannot select the desired attributes:
I have 3 tables:
tb_register
CREATE TABLE olympic.tb_register (
athlete_id CHARACTER(7) NOT NULL,
round_number INT NOT NULL,
discipline_id INT NOT NULL,
register_position INT DEFAULT NULL,
CONSTRAINT pk_register PRIMARY KEY (athlete_id, round_number, discipline_id),
CONSTRAINT fk_register_athlete FOREIGN KEY (athlete_id) REFERENCES olympic.tb_athlete (athlete_id),
CONSTRAINT fk_register_round FOREIGN KEY (discipline_id, round_number) REFERENCES olympic.tb_round (discipline_id, round_number)
);
athlete_id round_number discipline_id register_position
"1349038" 3 16 0
"1393212" 3 16 1
"1318621" 3 16 2
"1451030" 3 16 3
"1343607" 3 16 4
"1435826" 3 16 5
INSERT INTO olympic.tb_register(athlete_id, round_number, discipline_id, register_position) VALUES('1349038', 3, 16, 0);
INSERT INTO olympic.tb_register(athlete_id, round_number, discipline_id, register_position) VALUES('1393212', 3, 16, 1);
INSERT INTO olympic.tb_register(athlete_id, round_number, discipline_id, register_position) VALUES('1318621', 3, 16, 2);
INSERT INTO olympic.tb_register(athlete_id, round_number, discipline_id, register_position) VALUES('1451030', 3, 16, 3);
INSERT INTO olympic.tb_register(athlete_id, round_number, discipline_id, register_position) VALUES('1343607', 3, 16, 4);
INSERT INTO olympic.tb_register(athlete_id, round_number, discipline_id, register_position) VALUES('1435826', 3, 16, 5);
tb_athlete
CREATE TABLE olympic.tb_athlete (
athlete_id CHARACTER(7) NOT NULL,
name CHARACTER VARYING(50) NOT NULL,
country CHARACTER VARYING(3) NOT NULL,
substitute_id CHARACTER(7) DEFAULT NULL,
CONSTRAINT pk_athlete PRIMARY KEY (athlete_id),
CONSTRAINT fk_athlete_substitute FOREIGN KEY (substitute_id) REFERENCES olympic.tb_athlete (athlete_id)
);
athlete_id athlete_name country
"1349038" "AALERUD Katrine" "NOR"
"1393212" "ABASS Abobakr" "SUD"
"1451030" "ABDALLA Abubaker Haydar" "QAT"
"1444255" "ABDEL LATIF Radwa" "EGY"
INSERT INTO olympic.tb_athlete(athlete_id, name, country, substitute_id) VALUES('1346266','AALERUD Katrine','NOR',NULL);
INSERT INTO olympic.tb_athlete(athlete_id, name, country, substitute_id) VALUES('1344792','ABASS Abobakr','SUD',NULL);
INSERT INTO olympic.tb_athlete(athlete_id, name, country, substitute_id) VALUES('1328854','ABDALLA Abubaker Haydar','QAT',NULL);
INSERT INTO olympic.tb_athlete(athlete_id, name, country, substitute_id) VALUES('1306332','ABDEL LATIF Radwa','EGY',NULL);
tb_discipline
CREATE TABLE olympic.tb_discipline (
discipline_id INT NOT NULL,
name CHARACTER VARYING(50) NOT NULL,
inventor CHARACTER VARYING(50) NOT NULL,
type CHARACTER VARYING(10) NOT NULL,
object_type CHARACTER VARYING(20) DEFAULT NULL,
CONSTRAINT ck_discipline_type CHECK (type IN ('RUN', 'JUMP', 'THROW')),
CONSTRAINT pk_discipline PRIMARY KEY (discipline_id)
);
discipline_id discipline_name inventor type
16 "Triathlon" "Carina Melina" "RUN"
INSERT INTO olympic.tb_discipline(discipline_id, name, inventor, type, object_type) VALUES(16,'Triathlon','Carina Melina','RUN',null);
Desired output
I am trying to create a recursive query to obtain this result:
discipline_name, round_number, register_position, a_position
Triathlon, 3, 0, 0:AALERUD Katrine,
Triathlon, 3, 1, 0:AALERUD Katrine -> 1: ABASS Abobakr
Triathlon, 3, 2, 0:AALERUD Katrine -> 1: ABASS Abobakr -> ABDALLA Abubaker Haydar
where a_position is a list of athlete names with their respective position.
My code:
WITH RECURSIVE athlete_hierarchy AS (
SELECT
discipline_name,
round_number,
register_position,
CAST (athlete_name AS TEXT) AS a_position,
register_position AS first_athlete
FROM
olympic.tb_register
JOIN tb_discipline
ON discipline_id = tb_discipline.discipline_id
JOIN tb_athlete
ON athlete_id = tb_athlete.athlete_id;
WHERE
round_number IS 3
UNION ALL
SELECT
a.discipline_name,
a.round_number,
a.register_position,
CAST ( a.athlethe_name || ' -> ' || a2.a_position AS TEXT) AS a_position,
c2.first_athlete AS first_athlete
FROM
olympic.tb_register INNER JOIN athlete_hierarchy
ON (a.athlete_id = a2.athlete_id)
)
SELECT
discipline_name,
round_number,
register_position,
a_position,
first_athlete
FROM athlete_hierarchy;
Can anyone help me understand how to join 3 tables when creating a CTE Recursive Query?

Here is the solution :
WITH RECURSIVE athlete_hierarchy (discipline_id, discipline_name, round_number, register_position, a_position) AS
(SELECT
d.discipline_id,
d.name,
r.round_number,
r.register_position,
r.register_position || ':' || CAST (a.name AS TEXT)
FROM tb_register AS r
JOIN tb_discipline AS d
ON r.discipline_id = d.discipline_id
JOIN tb_athlete AS a
ON r.athlete_id = a.athlete_id
WHERE r.round_number = 3
AND r.register_position = 0
UNION ALL
SELECT
ah.discipline_id,
ah.discipline_name,
ah.round_number,
r.register_position,
ah.a_position || ' -> ' || r.register_position || ':' || CAST (a.name AS TEXT)
FROM tb_register AS r
INNER JOIN athlete_hierarchy AS ah
ON ah.discipline_id = r.discipline_id
AND ah.round_number = r.round_number
AND ah.register_position = r.register_position - 1
INNER JOIN tb_athlete AS a
ON r.athlete_id = a.athlete_id
)
SELECT discipline_name, round_number, register_position, a_position
FROM athlete_hierarchy
ORDER BY register_position
and the test result from your data sample is in db<>fiddle.

Related

Join on multiple tables using distinct on

create table emp
(
emp_id serial primary key,
emp_no integer,
emp_ref_no character varying(15),
emp_class character varying(15)
);
create table emp_detail
(
emp_detail_id serial primary key,
emp_id integer,
class_no integer,
created_at timestamp without time zone,
constraint con_fk foreign key(emp_id) references emp(emp_id)
);
create table class_detail
(
class_id serial primary key,
emp_id integer,
class_no integer,
col1 JSONB,
created_at timestamp without time zone default now(),
constraint cd_fk foreign key(emp_id) references emp(emp_id)
);
INSERT INTO emp(
emp_no, emp_ref_no, emp_class)
VALUES ('548251', '2QcW', 'abc' );
INSERT INTO emp(
emp_no, emp_ref_no, emp_class)
VALUES ('548251', '2FQx', 'abc');
INSERT INTO emp(
emp_no, emp_ref_no, emp_class)
VALUES ('548251', '2yz', 'abc');
INSERT INTO emp_detail(
emp_id, class_no, created_at
)
VALUES ( 1, 2, '2018-05-04 11:00:00'
);
INSERT INTO emp_detail(
emp_id, class_no, created_at
)
VALUES ( 1, 1, '2018-04-04 11:00:00'
);
INSERT INTO emp_detail(
emp_id, class_no, created_at
)
VALUES ( 2, 1, '2018-05-10 11:00:00'
);
INSERT INTO emp_detail(
emp_id, class_no, created_at
)
VALUES ( 2, 2, '2018-02-01 11:00:00'
);
INSERT INTO emp_detail(
emp_id, class_no, created_at
)
VALUES ( 3, 2, '2018-02-01 11:00:00'
);
insert into class_detail(emp_id, class_no, col1, created_at) values(1,1,'{"Name":"Nik"}', '2018-02-01 10:00:00');
insert into class_detail(emp_id, class_no, col1, created_at) values(1,1,'{"Name":"Nik Anderson"}', '2018-03-01 10:00:00');
insert into class_detail(emp_id, class_no, col1, created_at) values(1,2,'{"Name":"James Anderson TST"}', '2018-03-15 10:00:00');
insert into class_detail(emp_id, class_no, col1, created_at) values(1,2,'{"Name":"Tim Paine ST"}', '2018-04-01 10:00:00');
I want to display corresponding emp_id, emp_no, emp_ref_no, class_no(the latest one from emp_detail table based on created at)along with all the columns of class_detail table. Class_detail table should show the latest corresponding record of the class no
The expected output which I would like to see is something like below :-
emp id | emp_no | emp_ref_no | class_no | class_id | class.col1 | class.created_at | class.created_by
1 | 548251 | 2QcW | 2 | 4 |{"Name":"Tim Paine ST"}|2018-04-01 10:00:00| NUlL
2 | 548251 | 2FQx | 1 | 2 |{"Name":"Nik Anderson"}|2018-03-01 10:00:00| NULL
3 | 548251 | 2yz | 2 | 4 |{"Name":"Tim Paine ST"}|2018-04-01 10:00:00| NULL
As I stated in the comments: It is exactly the same thing as in Inner join using distinct on. You simply have to add another join and another ORDER BY group (cd.created_at DESC)
demo:db<>fiddle
SELECT DISTINCT ON (ed.emp_id)
e.emp_id, e.emp_no, e.emp_ref_no, ed.class_no, cd.*
FROM
emp_detail ed
JOIN emp e ON e.emp_id = ed.emp_id
JOIN class_detail cd ON ed.class_no = cd.class_no
ORDER BY ed.emp_id, ed.created_at DESC, cd.created_at DESC
Note: I am not sure what the emp_id column in class_detail is for. It seems not well designed (this is also because it is always 1 in your example.) You should check whether you really need it.

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

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.

PostgreSQL: Iterate through a tables rows with for loop, retrieve column value based on current row

I have the following 2 tables
CREATE TABLE salesperson_t (
salespersonid numeric(4,0) NOT NULL,
salespersonname character varying(25),
salespersontelephone character varying(50),
salespersonfax character varying(50),
salespersonaddress character varying(30),
salespersoncity character varying(20),
salespersonstate character(2),
salespersonzip character varying(20),
salesterritoryid numeric(4,0),
CONSTRAINT salesperson_pk PRIMARY KEY (salespersonid)
);
INSERT INTO salesperson_t VALUES (1, 'Doug Henny', '8134445555', NULL, NULL, NULL, NULL, NULL, 2);
INSERT INTO salesperson_t VALUES (2, 'Robert Lewis', '8139264006', NULL, '124 Deerfield', 'Lutz', 'FL', '33549', 13);
INSERT INTO salesperson_t VALUES (3, 'William Strong', '3153821212', NULL, '787 Syracuse Lane', 'Syracuse', 'NY', '33240', 3);
INSERT INTO salesperson_t VALUES (4, 'Julie Dawson', '4355346677', NULL, NULL, NULL, NULL, NULL, 4);
INSERT INTO salesperson_t VALUES (5, 'Jacob Winslow', '2238973498', NULL, NULL, NULL, NULL, NULL, 5);
INSERT INTO salesperson_t VALUES (6, 'Pepe Lepue', NULL, NULL, NULL, 'Platsburg', 'NY', NULL, 13);
INSERT INTO salesperson_t VALUES (8, 'Fred Flinstone', NULL, NULL, '1 Rock Lane', 'Bedrock', 'Ca', '99999', 2);
INSERT INTO salesperson_t VALUES (9, 'Mary James', '3035555454', NULL, '9 Red Line', 'Denver', 'CO', '55555', 4);
INSERT INTO salesperson_t VALUES (10, 'Mary Smithson', '4075555555', NULL, '4585 Maple Dr', 'Orlando', 'FL', '32826', 15);
CREATE TABLE territory2_t (
territoryid numeric(4,0),
territoryname character varying(50),
total_sales_person integer,
CONSTRAINT territory2_t_pk PRIMARY KEY (territoryid)
);
INSERT INTO territory2_t VALUES (1, 'SouthEast', NULL);
INSERT INTO territory2_t VALUES (2, 'SouthWest', NULL);
INSERT INTO territory2_t VALUES (3, 'NorthEast', NULL);
INSERT INTO territory2_t VALUES (4, 'NorthWest', NULL);
INSERT INTO territory2_t VALUES (5, 'Central', NULL);
INSERT INTO territory2_t VALUES (6, 'Alaska', NULL);
INSERT INTO territory2_t VALUES (12, 'Hawaii', NULL);
INSERT INTO territory2_t VALUES (13, 'Colorado', NULL);
INSERT INTO territory2_t VALUES (15, 'Arizona', NULL);
I have the following pseudo code:
DO $$
DECLARE
-- currentRow [relevant datatype];
BEGIN
FOR counter IN 1..(SELECT count(*)FROM territory2_t) LOOP -- There are 13 total rows
-- **assign currentRow to counter**
RAISE NOTICE 'Counter: %', counter; -- debugging purposes
UPDATE terriory2_t
SET total_sales_person = ((SELECT count(*)
FROM salesperson_t
WHERE salesterritoryid = currentRow.territoryid)*1) -- *1 is for debuggin puporses
WHERE territoryid = currentRow.territoryid;
-- **increase currentRow by 1**
END LOOP;
END; $$
It's purpose is count how many rows in the table (salesperson) have the 'territoryid' of the the currentRows->'territory2.territoryid', and then assign that quantity to currentRows->territory2.total_sales_person.
You don't need a loop or even a function for this.
What you want to do can be done in a single update statement because the total count per territory can be calculated with a single aggregation:
SELECT salesterritoryid, count(*) as total_count
FROM salesperson_t
group by salesterritoryid
This can then be used as the source to update the territory table:
UPDATE territory2_t
SET total_sales_person = t.total_count
FROM (
SELECT salesterritoryid, count(*) as total_count
FROM salesperson_t
group by salesterritoryid
) t
WHERE territoryid = t.salesterritoryid;
An alternative that might be easier to understand but will be slower for larger tables is an update with a co-related sub-query
UPDATE territory2_t tg
SET total_sales_person = (select count(*)
from salesperson_t sp
where sp.salesterritoryid = tg.territoryid);
There is a slight difference between the first and second update: the second one will update the total_sales_person to 0 (zero) for those territories where there is no salesperson at all. The first one will only update the count for territories that are actually present in the salesperson table.
Unrelated, but: having a "type identifying" prefix or suffix for an identifier is usually useless and doesn't really help at all. See a related discussion on dba.stackexchange

List ranges and the total count based on condition

Table Schema
CREATE TABLE [dbo].[TblMaster](
[SID] [int] IDENTITY(1,1) NOT NULL Primary Key,
[VID] [int] NOT NULL,
[CreatedDate] [datetime] default (getdate()) NOT NULL,
[CharToAdd] [varchar](10) NULL,
[Start] [int] NOT NULL,
[End] [int] NOT NULL
)
GO
CREATE TABLE [dbo].[TblDetails](
[DetailsID] [int] IDENTITY(1,1) NOT NULL Primary Key,
[SID] [int] NOT NULL,
[Sno] [int] NOT NULL,
[ConcatenatedText] [varchar](20) NOT NULL,
[isIssued] [bit] default (0) NOT NULL,
[isUsed] [bit] default (0) NOT NULL
)
GO
Sample Data:
Insert into dbo.TblMaster Values (1,default, 'CA', 1, 5)
Insert into dbo.TblMaster Values (1,default, 'PA', 1, 5)
GO
Insert into dbo.TblDetails values(1, 1, 'CA1', 0,0)
Insert into dbo.TblDetails values(1, 2, 'CA2', 0,0)
Insert into dbo.TblDetails values(1, 3, 'CA3', 0,0)
Insert into dbo.TblDetails values(1, 4, 'CA4', 1,0)
Insert into dbo.TblDetails values(1, 5, 'CA5', 0,0)
Insert into dbo.TblDetails values(2, 1, 'PA1', 0,0)
Insert into dbo.TblDetails values(2, 2, 'PA2', 0,0)
Insert into dbo.TblDetails values(2, 3, 'PA3', 1,0)
Insert into dbo.TblDetails values(2, 4, 'PA4', 0,0)
Insert into dbo.TblDetails values(2, 5, 'PA5', 0,0)
Insert into dbo.TblDetails values(3, 1, '1', 0,0)
Insert into dbo.TblDetails values(3, 2, '2', 1,0)
Insert into dbo.TblDetails values(3, 3, '3', 1,0)
Insert into dbo.TblDetails values(3, 4, '4', 0,0)
Insert into dbo.TblDetails values(3, 5, '5', 0,0)
GO
Expected Output:
Query I have built as of now:
Declare #VID INT = 1
;WITH Tmp as
(
SELECT
TM.CharToAdd as Prefix,
sno,
sno - ROW_NUMBER() OVER(ORDER BY sno) as grp
FROM dbo.TblDetails TD
LEFT JOIN dbo.TblMaster TM on TM.[SID] = TD.[SID]
WHERE isIssued = 0 and isUsed = 0
AND TM.VID = #VID
)
SELECT Prefix,
MIN(sno) as RangeStart,
MAX(sno) as RangeEnd,
COUNT(*) as [Count]
FROM Tmp
GROUP BY grp, Prefix
In the TblDetails table want to find the range of available values and its total counts from all records whose bit columns are 0. If bit column is 1 then it means it is already used so I am trying to skip it and list rest as available records. Doubtful whether am I explaining the problem statement well so have provided the sample data and expected output for better understanding. I did try doing some recursive function but the result isn't matching the expected output. So looking for help to resolve this.
You were very close...
CODE
Declare #VID INT = 1
;with cte as(
select
m.CHarToAdd,
d.sno,
d.sno - ROW_NUMBER() OVER(partition by m.CharToAdd ORDER BY sno) as grp
from
TblMaster m
inner join
TblDetails d on
d.sid = m.sid
where
d.isIssued = 0 and d.isUsed = 0 and m.vid = #VID)
select
CharToAdd,
min(sno) as Start,
max(sno) as [End],
(max(sno) - min(sno) + 1) as [Count]
from cte
group by
CHarToAdd, grp
order by
CHarToAdd
RESULTS
CharToAdd Start End Count
CA 1 3 3
CA 5 5 1
PA 1 2 2
PA 4 5 2

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;