Outer Join / Union ?- t/sql query - sql

I need help with a complex t/sql query for a special report I'm writing.
I dont quite know how to articulate this problem; but I'll have a go:
Basicly, I have three tables as follows:
#PrimaryTable
Key - Is unique and may or may not exist
Description1 - Some field
Description2 - Some field which works as a heading, more on this later
#Subtable1
KeyFK - is NOT unique, links to Key in primary table. May or may not exist. May also exist multiple times per Key
Description - The field I want
#Subtable2
- Just like subtable1, but does not relate to subtable1 in any way
I want to join these tables to PrimaryTable as it would look with a UNION ALL, but without all the NULL values.
See this query for an example:
DECLARE #PrimaryTable AS table ("Key" int, Description1 varchar(32), Description2 varchar(32)); --Contains MAXIMUM one of each Key
DECLARE #Subtable_1 AS table ("KeyPK" int, SubDescription1 varchar(32)); --Can contain zero, one or more lines with same KeyPK
DECLARE #Subtable_2 AS table ("KeyPK" int, SubDescription2 varchar(32)); --Can contain zero, one or more lines with same KeyPK
INSERT INTO #PrimaryTable VALUES (1, 'Description', 'Heading');
INSERT INTO #PrimaryTable VALUES (2, 'Description', 'Heading');
INSERT INTO #PrimaryTable VALUES (3, 'Description', 'Heading');
INSERT INTO #PrimaryTable VALUES (5, 'Description', 'Heading');
INSERT INTO #PrimaryTable VALUES (6, 'Description', 'Heading');
INSERT INTO #PrimaryTable VALUES (7, 'Description', 'Heading');
INSERT INTO #PrimaryTable VALUES (8, 'Description', 'Heading');
INSERT INTO #Subtable_1 VALUES (1, 'Subdescription1_1');
INSERT INTO #Subtable_2 VALUES (2, 'Subdescription2_1');
INSERT INTO #Subtable_1 VALUES (3, 'Subdescription1_1');
INSERT INTO #Subtable_2 VALUES (3, 'Subdescription2_1');
INSERT INTO #Subtable_1 VALUES (4, 'Subdescription1_1');
INSERT INTO #Subtable_2 VALUES (4, 'Subdescription2_1');
INSERT INTO #Subtable_2 VALUES (4, 'Subdescription2_2');
INSERT INTO #Subtable_1 VALUES (5, 'Subdescription1_1');
INSERT INTO #Subtable_1 VALUES (5, 'Subdescription1_2');
INSERT INTO #Subtable_2 VALUES (5, 'Subdescription2_1');
INSERT INTO #Subtable_1 VALUES (6, 'Subdescription1_1');
INSERT INTO #Subtable_2 VALUES (6, 'Subdescription2_1');
INSERT INTO #Subtable_2 VALUES (6, 'Subdescription2_2');
INSERT INTO #Subtable_1 VALUES (7, 'Subdescription1_1');
INSERT INTO #Subtable_1 VALUES (7, 'Subdescription1_2');
I want the result to look as following:
/*
Key Description1 Description2 Subdescription1 Subdescription2
_________________________________________________________________________
1 Description Heading '' ''
1 Description '' Subdescription1_1 NULL
2 Description Heading '' ''
2 Description '' NULL Subdescription2_1
3 Description Heading '' ''
3 Description '' Subdescription1_1 Subdescription2_1
5 Description Heading '' ''
5 Description '' Subdescription1_1 Subdescription2_1
5 Description '' Subdescription1_2 NULL
6 Description Heading '' ''
6 Description '' Subdescription1_1 Subdescription2_1
6 Description '' NULL Subdescription2_2
7 Description Heading '' ''
7 Description '' Subdescription1_1 NULL
7 Description '' Subdescription1_2 NULL
8 Description Heading '' ''
*/
The Heading line with the empty descriptions is no problem; can easily be done using a union afterwards, but I dont know how to join these tables like this - Does anyone know how this could be done?
Edit:
I should have explained the report from the start:
I'm working on a very great DB-structure, but I have a very odd report requirement.
PrimaryTable is an actual table, which the report asks on
Subtable1 is table which explains different parts of primary table, this is real info.
Subtable2 is not a table, but a contract(a field from PrimaryTable) broken down to one row pr. line.
The report then wants all info from Primary table as a heading including some subtotals. Then there is a fold down option, which opens to one row pr. subtable2 description / line in contract. The amount of lines pr Key simply shows the length of each fold down.
As much as posible will be done by SQL for report answering times. - This will be a stored procedure which will generate a fast answering solution-based table

I think you want something like this:
SELECT key, description1, description2, '', ''
FROM PrimaryTable
UNION
SELECT distinct p.description1, '', s1.subdescription1, s2.subdescription2
FROM PrimaryTable as p
LEFT JOIN Subtable1 as s1
ON p.key = s1.key
LEFT JOIN Subtable2 as s2
ON p.key = s2.key
I will also say that it looks like you have a pretty strange database structure. Is there any reason you have two "subdescription" tables?

This is close to your expected results (double-check where Key = 6 and also 8):
SELECT "Key",
P1.Description1, '' AS Description2,
S1.SubDescription1, S2.SubDescription2
FROM #PrimaryTable AS P1
LEFT OUTER JOIN #Subtable_1 AS S1
ON P1."Key" = S1."KeyPK"
LEFT OUTER JOIN #Subtable_2 AS S2
ON P1."Key" = S2."KeyPK"
UNION
SELECT "Key",
P1.Description1, P1.Description2,
'' AS SubDescription1, '' AS SubDescription2
FROM #PrimaryTable AS P1
ORDER
BY "Key", Description2 DESC;

Related

PostgreSQL bulk update

In my PostgreSQL database I have the following schema:
CREATE TABLE atc_codes (
id bigint NOT NULL,
name character varying,
atc_code character varying
);
INSERT INTO atc_codes (id, name, atc_code) VALUES (1, 'granisetron', 'A04AA02');
INSERT INTO atc_codes (id, name, atc_code) VALUES (2, '', 'A04AA02');
INSERT INTO atc_codes (id, name, atc_code) VALUES (3, '', 'A04AA02');
INSERT INTO atc_codes (id, name, atc_code) VALUES (4, 'metoclopramide', 'A03FA01');
INSERT INTO atc_codes (id, name, atc_code) VALUES (5, '', 'A03FA01');
INSERT INTO atc_codes (id, name, atc_code) VALUES (6, '', 'A03FA01');
SELECT * FROM atc_codes;
id
name
atc_code
1
granisetron
A04AA02
2
A04AA02
3
A04AA02
4
metoclopramide
A03FA01
5
A03FA01
6
A03FA01
View on DB Fiddle
Now I want to do the following things:
Update all records with act_code equal to A04AA02 to have granisetron value in the name column.
Update all records with act_code equal to A03FA01 to have metoclopramide value in the name column.
In the real database there will be much more scenarios like that so using something like CASE statement is impossible in that case.
Can I do that in one query instead of two?
I found the solution with using view:
WITH act_codes_name AS (
SELECT
name,
atc_code
FROM
atc_codes
WHERE
name IS NOT NULL
)
UPDATE
atc_codes
SET
name = act_codes_name.name
FROM act_codes_name
WHERE act_codes_name.atc_code = atc_codes.atc_code;
Yes you could use a CASE WHEN (standard SQL "switch case") in the SET clause:
UPDATE atc_codes
SET name = CASE
WHEN atc_code = 'A04AA02' THEN 'granisetron'
WHEN atc_code = 'A03FA01' THEN 'metoclopramide'
ELSE name
END
WHERE atc_code IN('A04AA02', 'A03FA01');
To avoid mistakes, I added a ELSE (equivalent to a default case) and a WHERE clause to prevent updating rows that don't match. Both do the same thing and I think you could just use one or the other.

postgres aggregate join matches to multiple array fields while creating views

I have the following schema + data:
create table org_users (
id character varying (255),
settings_id character varying (255) -- fk: settings.id
);
create table settings (
id character varying (255), -- primary key settings_id
perdiem_settings character varying (255), -- jsonised fk to perdiems.id
floor_settings character varying (255) -- jsonised fk to floors.id
);
create table perdiems (
id integer, -- primary key
name character varying(255)
);
create table floors (
id integer, -- primary key
name character varying (255)
);
insert into perdiems (id, name) values (1, 'perdiem 1');
insert into perdiems (id, name) values (2, 'perdiem 2');
insert into perdiems (id, name) values (3, 'perdiem 3');
insert into floors (id, name) values (1, 'floor 1');
insert into floors (id, name) values (2, 'floor 2');
insert into floors (id, name) values (3, 'floor 3');
insert into settings (id, perdiem_settings, floor_settings) values ('setting1', '{"allowed_per_diem_ids":[1, 2]}', '{"allowed_floor_ids":[1]}');
insert into settings (id, perdiem_settings, floor_settings) values ('setting2', '{"allowed_per_diem_ids":[2, 3]}', '{"allowed_floor_ids":[1, 2]}');
insert into settings (id, perdiem_settings, floor_settings) values ('setting3', '{"allowed_per_diem_ids":[3, 1]}', '{"allowed_floor_ids":[1, 2, 3]}');
insert into org_users (id, settings_id) values ('user1', 'setting1');
insert into org_users (id, settings_id) values ('user2', 'setting2');
insert into org_users (id, settings_id) values ('user3', 'setting3');
Now I want to create a view which will have aggregates from each of the other table, into an array field of its own. To explain with an example, the view that I want should be like:
org_user_id | settings_id | perdiems | floors
--------------------------------------------------------------------------------------------
user1 | setting1 | ['perdiem 1', 'perdiem 2'] | ['floor 1']
user2 | setting2 | ['perdiem 2', 'perdiem 3'] | ['floor 1', 'floor 2']
user3 | setting3 | ['perdiem 3', 'perdiem 1'] | ['floor 1', 'floor 2', 'floor 3']
This question is somewhat related to postgres aggregate join matches to an array field which deals with creating array fields out of join matches. However, here I want to create multiple array fields in a single view and so using a GROUP BY clause will not be feasible iiuc.
The query that I tried is:
CREATE OR REPLACE VIEW users_settings_view AS
SELECT ou.id AS org_user_id, <other fields...>
FROM org_users ou
LEFT JOIN settings pdr_s ON pdr_s.id = ou.settings_id
LEFT JOIN perdiems pdr ON pdr.id = ANY (SELECT json_array_elements(perdiem_settings::JSON->'allowed_per_diem_ids')::text::int FROM settings)
which creates duplicate records for each of the matching perdiem because of the join and not creating an array. Even if I crate an array as mentioned in the other stackoverflow question, it won't work if I have multiple string arrays as part of the view for different columns. Any way I can get multiple join matches to multiple array fields in a single view ?
This will give you the result.
select ou.id, array_agg( DISTINCT pd.name ),
array_agg( DISTINCT f.name )
from org_users ou join settings s on ou.settings_id = s.id
cross join lateral
json_array_elements_text(((s.perdiem_settings)::json->'allowed_per_diem_ids')::json)
as jp(perdiem) join
perdiems pd
on pd.id = jp.perdiem::int
cross join lateral
json_array_elements_text(((s.floor_settings)::json->'allowed_floor_ids')::json)
as js(floor) join
floors f
on f.id = js.floor::int
GROUP BY ou.id;
Demo
Edit
For cases of NULL settings, you may use a separate UNION ALL
select id , ARRAY[NULL] as perdiems ,ARRAY[NULL] as floors FROM org_users
WHERE settings_id IS NULL
UNION ALL
(
-- The above query --
) ORDER BY id;
Demo2

Passing a parameter value for like

create table ter (
ID number,
category varchar2(250 byte),
name varchar2(250 byte)
);
insert into ter values (1, 'amd-visual theraphy','john');
insert into ter values (1, 'amd-visual theraphy','mike');
insert into ter values (2, 'amd-autmatic theraphy','mike');
insert into ter values (3, 'amd-autmatic theraphy','drane');
insert into ter values (3, 'cmd autmatic theraphy', 'traverse');
insert into ter values (3, 'amd-dramatic theraphy','drane');
insert into ter values (3, 'cmd-dropertic theraphy', 'traverse');
insert into ter values (5,'qwd-dropertic visual-theraphy','drones');
insert into ter values (5,'qwd-aromatic-theraphy','drones');
insert into ter values (3, 'other', 'traverse');
insert into ter values (3, 'other', 'traverse');
1: &category is null display all records
2: &category is not null and if i enter category =visual,autmatic then display respectively
3 &category is not null and if i enter category =dramatic it should display dramtic and dropertic ignoring other results;
as dramatic and dropertic is the requirement where the user want to see and ignoring the if partial value matches with visual and autmatic contains
how to obtain this result
this is what one gave the solution to me but still i want to ignore the visual containing dropetic
select *
from ter t cross join (select '&category' as my_categ from dual) m
where m.my_categ is null
or m.my_categ in ('visual', 'autmatic', 'dramtic') and t.category like '%'
|| m.my_categ || '%'
or m.my_categ = 'dramtic' and t.category like '%dropertic%' ;
I'd change your data model a bit.
Instead of saving categories as a space separated string, i'd better move them to separate table.
create table category(
ter_id number,
name varchar(250 bytes)
);
where ter_id is the same as ID in your ter column.
This table should contain records like:
insert into category values (1, 'amd-visual');
insert into category values (1, 'therapy');
-- etc.
Implying above your query will be something like:
select t.*, c.*
from ter t, category c
where t.id = c.ter_id
and category in ('therapy', 'visual');
-- etc...
If you really need to satisfy criteria:
if category is null display all records
it's easy to implement just adding
record with empty category name to table category:
insert into category values (1, null);
insert into category values (2, null);
-- etc.

Collecting entity attribute values with null for missing attributes in SQL

I have some entities in a table and their attributes and values in an other. I would like to create a select where I can see the value a specific attribute for every entity or null if that attribute is missing. How can I do this using standard SQL?
This is the setup:
create table person (id int not null, nick varchar(32) not null);
insert into person (id, nick) values (1, 'John');
insert into person (id, nick) values (2, 'Peter');
create table req_attributes (name varchar(32));
create table person_attributes (id int not null,
person_id int not null,
attribute varchar(32) not null,
value varchar(64) not null);
insert into person_attributes values (1, 1, 'age', '21');
insert into person_attributes values (2, 1, 'hair', 'brown');
insert into person_attributes values (3, 2, 'age', '32');
insert into person_attributes values (4, 2, 'music', 'jazz');
And this is my current select statement:
select * from person join person_attributes on
person.id = person_attributes.person_id
where attribute = 'hair';
Obviously Peter is not in the result set because we have no information about his hair. I would like to get him into the result set as well, but with null value.
The best would be if the result set was like
Person, Hair color
John, brown
Peter, null
I would like to avoid subqueries if possible, but if it is impossible to do with joins then they are welcome.
An outer join will do this:
select p.*, pa.value
from person p
left join person_attributes pa
on p.id = pa.person_id
and pa.attribute = 'hair';
Note that the condition for the "outer joined" table needs to go into the JOIN clause, not into the where clause. If the condition was in the where clause it would effectively turn the outer join into an inner join. This is because pa.attribute would be null due to the outer join, and the where would not match the null value thus eliminating all the rows that should actually stay in the result.
SQFiddle based on your example: http://sqlfiddle.com/#!12/d0342/1

SQL Query problems exist

I'm having a lot of troubles with the last query I need and I think it's a level out of my league so any help is appreciated.
The tables:
CREATE TABLE Recipe
(
nrecipe integer,
name varchar(255),
primary key (nrecipe)
);
CREATE TABLE Food
(
designation varchar(255) unique,
quantity integer,
primary key (designation)
);
CREATE TABLE Contains
(
nrecipe integer,
designation varchar(255),
quantity integer,
primary key (nrecipe, designation),
foreign key (nrecipe) references Recepie (nrecipe),
foreign key (designation) references Food (designation)
);
Quantity in Food table is the quantity stored in warehouse.
Quantity in Contains is the amount needed of a food element to use in recipe.
Quantity in Food table and Contains differ from each other.
The query:
I want to know the names of ALL recipes that are possible to be done with the food stored in warehouse.
It requires that the quantity of every element of food in warehouse is bigger than the quantity needed for the recipe.
EDIT: also, it shouldn't show a recipe's name if there is nothing referring to it on Contains table.
To make it easier to understand, I'll give some data:
INSERT INTO Recipe VALUES ('01', 'Steak with potatos and water');
INSERT INTO Recipe VALUES ('02', 'Rice and ice tea');
INSERT INTO Recipe VALUES ('03', 'Potatos and shrimp');
INSERT INTO Recipe VALUES ('04', 'Water');
INSERT INTO Recipe VALUES ('05', 'Steak with rice');
INSERT INTO Recipe VALUES ('06', 'Steak with spaguetti');
INSERT INTO Recipe VALUES ('07', 'Potatos with rice');
INSERT INTO Food VALUES ('Water', 5);
INSERT INTO Food VALUES ('Ice tea', 10);
INSERT INTO Food VALUES ('Steak', 30);
INSERT INTO Food VALUES ('Potatos', 20);
INSERT INTO Food VALUES ('Rice', 50);
INSERT INTO Food VALUES ('Shrimp', 5);
INSERT INTO Food VALUES ('Spaguetti', 5);
INSERT INTO Contains VALUES ('01', 'Steak', 1);
INSERT INTO Contains VALUES ('01', 'Potatos', 15);
INSERT INTO Contains VALUES ('01', 'Water', 10);
INSERT INTO Contains VALUES ('02', 'Rice', 5);
INSERT INTO Contains VALUES ('02', 'Ice tea', 8);
INSERT INTO Contains VALUES ('03', 'Potatos', 1);
INSERT INTO Contains VALUES ('03', 'Shrimp', 10);
INSERT INTO Contains VALUES ('04', 'Water', 20);
INSERT INTO Contains VALUES ('05', 'Steak', 1);
INSERT INTO Contains VALUES ('05', 'Rice', 20);
INSERT INTO Contains VALUES ('06', 'Steak', 1);
INSERT INTO Contains VALUES ('06', 'Spaguetti', 10);
The outcome expected from the query is:
Rice and ice tea
Steak with rice
Since it's the only two recipes with enough quantity in warehouse.
EDIT: potatoes with rice shouldn't appear as it is a recipe but isn't in contains list
Thanks for input and time. Any help is welcome :)
I'd use >= ALL operator :
SELECT name
FROM Recipe R
WHERE 0 >= ALL (SELECT C.quantity - F.quantity
FROM Food F
INNER JOIN Contains C
USING (designation)
WHERE C.nrecipe = R.nrecipe);
The correct spelling is recipe, and you used different names for some columns (recepie, nrecipe, nrecepie) so I changed it. Note that instead of using a varchar primary key, you should use a numeric one.
Edit:
SELECT name
FROM Recipe R
WHERE 0 >= ALL (SELECT C.quantity - F.quantity
FROM Food F
INNER JOIN Contains C
USING (designation)
WHERE C.nrecipe = R.nrecipe)
AND EXISTS(SELECT NULL
FROM Contains C
WHERE C.nrecipe = R.nrecipe);
This is in SQL Server because that is what I have:
select
r.name
from
Recepie r
where
not exists
(
select 1
from
[Contains] c
where
c.nrecipe = r.nrecepie and
not exists
(
select 1
from
Food f
where
f.designation = c.designation and
f.quantity >= c.quantity
)
)
Which in plain language is "Get me all recipes where there are no ingredients of insufficient quantity"