SQL: Multiple select statements in one query - sql

I want to select information from three SQL tables within one query.
An example could be the following setup.
tblFriends
id | idmother | dayBirth
--------------------------
1 | 1 | 09/09/21
2 | 2 | 09/09/21
3 | 3 | 11/09/21
4 | 3 | 11/09/21
5 | 4 | 07/09/21
... | ... | ...
tblMothers
id | name
---------------
1 | Alice
2 | Samantha
3 | Veronica
4 | Maria
... | ...
tblIsAssignedParty
idMother | codeParty | price
------------------------------
1 | 231 | 15
2 | 645 | 28
3 | 164 | 33
... | ... | ...
I want to have a query that gives me the following:
dayBirth | weekDay | totalFriendsForParty | totalFriendsForPartyPercent | totalFriendsNoParty | totalFriendsNoPartyPercent
-----------------------------------------------------------------------------------------------------------------------------
07/09/21 | Tuesday | 0 | 0 | 1 | 0.??
09/09/21 | Thursday | 2 | 0.?? | 0 | 0
11/09/21 | Saturday | 2 | 0.?? | 0 | 0
Note:
dayBirth = simply the day of birth; I need the friends grouped by this date
weekDay = dayBirth name
totalFriendsForParty = friends who will be attending the party; we know if the mother has a party assigned
totalFriendsForPartyPercent = Percentatge of friends, of the total number of friends who will attend the parties
totalFriendsNoParty = friends who will not attend the party; we know if the mother does not have a party assigned
totalFriendsNoPartyPercent = Percentatge of friends, of the total number of friends who will not attend the parties
I need the number of friends based on whether their mothers are at a party or not. I tried to multiple select statements in Single query but the following code didn't work:
SELECT
(SELECT distinct dayBirth, TO_CHAR(dayBirth, 'DAY') from tblFriends) as firstSecondColumn,
(SELECT dayBirth, count(*) from tblFriends
where idMother IN (
SELECT f.idMother
from tblFriends f
left join tblIsAssignedParty iap
on f.idMother = iap.idMother
where iap.codeParty is not null)
group by dayBirth) as thirdColumn,
(SELECT TRUNC(count(*) / count(thirdColumn.id) , 2) from tblFriends) as quarterColumn,
(SELECT dayBirth, count(*) from tblFriends
where idMother IN (
SELECT f.idMother
from tblFriends f
left join tblIsAssignedParty iap
on f.idMother = iap.idMother
where iap.codeParty is not null)
group by dayBirth) as fifthColumn,
(SELECT TRUNC(count(*) / count(fifthColumn.id) , 2) from tblFriends) as sixthColumn,
order by dayBirth
Any advice on this one? I try to learn, I do what I can :-(
Edit: I can't add inserts because it's a file upload, but I can add an approximation of table creation.
Create tables:
CREATE TABLE tblFriends
(
id NUMBER(*,0),
idMother CHAR(10 CHAR),
CONSTRAINT PK_FRIEND PRIMARY KEY (id, idMother),
CONSTRAINT FK_IDMOTHER FOREIGN KEY (idMother)
REFERENCES tblMothers (id),
dayBirth DATE CONSTRAINT NN_DAY NOT NULL
)
CREATE TABLE tblMothers
(
id CHAR(10 CHAR) CONSTRAINT PK_MOTHER PRIMARY KEY (id),
name VARCHAR2(20 CHAR) CONSTRAINT NN_MNAME NOT NULL
)
CREATE TABLE tblIsAssignedParty
(
idMother CHAR(10 CHAR),
codeParty CHAR(10 CHAR),
CONSTRAINT PK_ASSIGNED PRIMARY KEY (idMother, codeParty),
CONSTRAINT FK_ASSIGNEDMOTHER FOREIGN KEY (idMother)
REFERENCES tblMothers (id),
CONSTRAINT FK_ASSIGNEDPARTY FOREIGN KEY (codeParty)
REFERENCES tblParties (codeParty),
price DECIMAL(10,2)
)

You appear to want to LEFT JOIN the firends and party tables and then use conditional aggregation:
SELECT dayBirth,
TO_CHAR(dayBirth, 'FMDAY', 'NLS_DATE_LANGUAGE=English') AS day,
COUNT(p.idmother)
AS totalFriendsForParty,
COUNT(p.idmother) / COUNT(*) * 100
AS totalFriendsForPartyPercent,
COUNT(CASE WHEN p.idmother IS NULL THEN 1 END) AS totalFriendsNoParty,
COUNT(CASE WHEN p.idmother IS NULL THEN 1 END) / COUNT(*) * 100
AS totalFriendsNoPartyPercent
FROM tblFriends f
LEFT OUTER JOIN tblIsAssignedParty p
ON (f.idmother = p.idmother)
GROUP BY dayBirth
Which, for the sample data:
CREATE TABLE tblFriends (id, idmother, dayBirth) AS
SELECT 1, 1, DATE '2021-09-09' FROM DUAL UNION ALL
SELECT 2, 2, DATE '2021-09-09' FROM DUAL UNION ALL
SELECT 3, 3, DATE '2021-09-11' FROM DUAL UNION ALL
SELECT 4, 3, DATE '2021-09-11' FROM DUAL UNION ALL
SELECT 5, 4, DATE '2021-09-07' FROM DUAL;
CREATE TABLE tblIsAssignedParty (idMother, codeParty, price) AS
SELECT 1, 231, 15 FROM DUAL UNION ALL
SELECT 2, 645, 28 FROM DUAL UNION ALL
SELECT 3, 164, 33 FROM DUAL;
Outputs:
DAYBIRTH
DAY
TOTALFRIENDSFORPARTY
TOTALFRIENDSFORPARTYPERCENT
TOTALFRIENDSNOPARTY
TOTALFRIENDSNOPARTYPERCENT
09-SEP-21
THURSDAY
2
100
0
0
11-SEP-21
SATURDAY
2
100
0
0
07-SEP-21
TUESDAY
0
0
1
100
db<>fiddle here

Related

Oracle - Finding missing /non-joined records

I have an issue in Oracle 12 that is easiest explained with the traditional database design scenario of students, classes, and students taking classes called registrations. I understand this model well. I have a scenario where I need to get a COMPLETE list, of all students against ALL classes, and whether or not they are taking that class or not...
Lets use this table design here...
CREATE TABLE CLASSES
(CLASSID VARCHAR2(10) PRIMARY KEY,
CLASSNAME VARCHAR2(25),
INSTRUCTOR VARCHAR2(25) );
CREATE TABLE STUDENTS
(STUDENTID VARCHAR2(10) PRIMARY KEY,
STUDENTNAMENAME VARCHAR2(25)
STUDY_MAJOR VARCHAR2(25) );
CREATE TABLE REGISTRATION
(
CLASSID VARCHAR2(10 BYTE),
STUDENTID VARCHAR2(10 BYTE),
GRADE NUMBER(4,0),
CONSTRAINT "PK1" PRIMARY KEY ("CLASSID", "STUDENTID"),
CONSTRAINT "FK1" FOREIGN KEY ("CLASSID") REFERENCES "CLASSES" ("CLASSID") ENABLE,
CONSTRAINT "FK2" FOREIGN KEY ("STUDENTID") REFERENCES "EGR_MM"."STUDENTS" ("STUDENTID") ENABLE
) ;
So assume the following... 300 students, and 15 different classes... and the REGISTRATION table will show how many students taking how many classes... What I need is that info PLUS all the NON-TAKEN combinations... i.e. I need a report (SQL statement) that shows ALL possible combinations... i.e. 300 x 15, and then whether that row exists in the registration table...so for example, the output should look like this...
STUDENTID Class1_GRADE Class2_Grade Class3_Grade` Class4_Grade
101 A B Not Taking A
102 C Not Taking Not Taking Not Taking
****** THIS STUDENT NOT TAKING ANY CLASSES So NOT in the Registrations Table
103 Not Taking Not Taking Not Taking Not Taking
This would work as well, and I can probably do a PIVOT to get the above listing.
STUDENTID CLASSID GRADE
101 Class1 A
101 Class2 B
101 Class3 Not Taking
101 Class4 A
...
102 Class1 C
102 Class2 Not Taking
102 Class3 Not Taking
102 Class4 Not Taking
...
103 Class1 Not Taking // THIS STUDENT NOT TAKING ANY CLASSES
103 Class2 Not Taking
103 Class3 Not Taking
103 Class4 Not Taking
How do I fill in the missing data, i.e. the combination of students and classes NOT taken...?
CROSS JOIN the students and classes and then LEFT OUTER JOIN the registrations and then use COALESCE to get the Not taken value:
SELECT s.studentid,
c.classid,
COALESCE( TO_CHAR( r.grade ), 'Not taken' ) AS grade
FROM students s
CROSS JOIN classes c
LEFT OUTER JOIN registration r
ON ( s.studentid = r.studentid AND c.classid = r.classid )
Which, if you have the data:
INSERT INTO Classes
SELECT LEVEL,
'Class' || LEVEL,
'Instructor' || LEVEL
FROM DUAL
CONNECT BY LEVEL <= 3;
INSERT INTO Students
SELECT TO_CHAR( LEVEL, 'FM000' ),
'Student' || LEVEL,
'Major'
FROM DUAL
CONNECT BY LEVEL <= 5;
INSERT INTO Registration
SELECT 1, '001', 4 FROM DUAL UNION ALL
SELECT 1, '002', 2 FROM DUAL UNION ALL
SELECT 1, '003', 5 FROM DUAL UNION ALL
SELECT 2, '001', 3 FROM DUAL UNION ALL
SELECT 3, '001', 1 FROM DUAL;
Then it outputs:
STUDENTID | CLASSID | GRADE
:-------- | :------ | :--------
001 | 1 | 4
002 | 1 | 2
003 | 1 | 5
001 | 2 | 3
001 | 3 | 1
005 | 1 | Not taken
004 | 2 | Not taken
003 | 3 | Not taken
005 | 3 | Not taken
005 | 2 | Not taken
002 | 2 | Not taken
003 | 2 | Not taken
004 | 1 | Not taken
002 | 3 | Not taken
004 | 3 | Not taken
If you want to pivot it then:
SELECT *
FROM (
SELECT s.studentid,
c.classid,
COALESCE( TO_CHAR( r.grade ), 'Not taken' ) AS grade
FROM students s
CROSS JOIN classes c
LEFT OUTER JOIN registration r
ON ( s.studentid = r.studentid AND c.classid = r.classid )
)
PIVOT ( MAX( grade ) FOR classid IN (
1 AS Class1,
2 AS Class2,
3 AS Class3
) )
ORDER BY StudentID
Which outputs:
STUDENTID | CLASS1 | CLASS2 | CLASS3
:-------- | :-------- | :-------- | :--------
001 | 4 | 3 | 1
002 | 2 | Not taken | Not taken
003 | 5 | Not taken | Not taken
004 | Not taken | Not taken | Not taken
005 | Not taken | Not taken | Not taken
db<>fiddle here
This is just conditional aggregation:
select s.studentid,
max(case when r.classid = 1 then r.grade end) as class1_grade,
max(case when r.classid = 2 then r.grade end) as class2_grade,
. . .
from students s left join
registrations r
on r.studentid = s.studentid;
You do have to list the columns explicitly. To avoid that, you need dynamic SQL (execute immediate).
Getting the results with one grade per row is simpler. Use a cross join to generate the rows and a left join to bring in the values:
select s.studentid, c.classid, r.grade
from students s cross join
classes c left join
registrations r
on r.studentid = s.studentid and r.classid = c.classid;

Get records having the same value in 2 columns but a different value in a 3rd column

I am having trouble writing a query that will return all records where 2 columns have the same value but a different value in a 3rd column. I am looking for the records where the Item_Type and Location_ID are the same, but the Sub_Location_ID is different.
The table looks like this:
+---------+-----------+-------------+-----------------+
| Item_ID | Item_Type | Location_ID | Sub_Location_ID |
+---------+-----------+-------------+-----------------+
| 1 | 00001 | 20 | 78 |
| 2 | 00001 | 110 | 124 |
| 3 | 00001 | 110 | 124 |
| 4 | 00002 | 3 | 18 |
| 5 | 00002 | 3 | 25 |
+---------+-----------+-------------+-----------------+
The result I am trying to get would look like this:
+---------+-----------+-------------+-----------------+
| Item_ID | Item_Type | Location_ID | Sub_Location_ID |
+---------+-----------+-------------+-----------------+
| 4 | 00002 | 3 | 18 |
| 5 | 00002 | 3 | 25 |
+---------+-----------+-------------+-----------------+
I have been trying to use the following query:
SELECT *
FROM Table1
WHERE Item_Type IN (
SELECT Item_Type
FROM Table1
GROUP BY Item_Type
HAVING COUNT (DISTINCT Sub_Location_ID) > 1
)
But it returns all records with the same Item_Type and a different Sub_Location_ID, not all records with the same Item_Type AND Location_ID but a different Sub_Location_ID.
This should do the trick...
-- some test data...
IF OBJECT_ID('tempdb..#TestData', 'U') IS NOT NULL
BEGIN DROP TABLE #TestData; END;
CREATE TABLE #TestData (
Item_ID INT NOT NULL PRIMARY KEY,
Item_Type CHAR(5) NOT NULL,
Location_ID INT NOT NULL,
Sub_Location_ID INT NOT NULL
);
INSERT #TestData (Item_ID, Item_Type, Location_ID, Sub_Location_ID) VALUES
(1, '00001', 20, 78),
(2, '00001', 110, 124),
(3, '00001', 110, 124),
(4, '00002', 3, 18),
(5, '00002', 3, 25);
-- adding a covering index will eliminate the sort operation...
CREATE NONCLUSTERED INDEX ix_indexname ON #TestData (Item_Type, Location_ID, Sub_Location_ID, Item_ID);
-- the actual solution...
WITH
cte_count_group AS (
SELECT
td.Item_ID,
td.Item_Type,
td.Location_ID,
td.Sub_Location_ID,
cnt_grp_2 = COUNT(1) OVER (PARTITION BY td.Item_Type, td.Location_ID),
cnt_grp_3 = COUNT(1) OVER (PARTITION BY td.Item_Type, td.Location_ID, td.Sub_Location_ID)
FROM
#TestData td
)
SELECT
cg.Item_ID,
cg.Item_Type,
cg.Location_ID,
cg.Sub_Location_ID
FROM
cte_count_group cg
WHERE
cg.cnt_grp_2 > 1
AND cg.cnt_grp_3 < cg.cnt_grp_2;
You can use exists :
select t.*
from table t
where exists (select 1
from table t1
where t.Item_Type = t1.Item_Type and
t.Location_ID = t1.Location_ID and
t.Sub_Location_ID <> t1.Sub_Location_ID
);
Sql server has no vector IN so you can emulate it with a little trick. Assuming '#' is illegal char for Item_Type
SELECT *
FROM Table1
WHERE Item_Type+'#'+Cast(Location_ID as varchar(20)) IN (
SELECT Item_Type+'#'+Cast(Location_ID as varchar(20))
FROM Table1
GROUP BY Item_Type, Location_ID
HAVING COUNT (DISTINCT Sub_Location_ID) > 1
);
The downsize is the expression in WHERE is non-sargable
I think you can use exists:
select t1.*
from table1 t1
where exists (select 1
from table1 tt1
where tt1.Item_Type = t1.Item_Type and
tt1.Location_ID = t1.Location_ID and
tt1.Sub_Location_ID <> t1.Sub_Location_ID
);

Update table in Postgresql by grouping rows

I want to update a table by grouping (or combining) some rows together based on a certain criteria. I basically have this table currently (I want to group by 'id_number' and 'date' and sum 'count'):
Table: foo
---------------------------------------
| id_number | date | count |
---------------------------------------
| 1 | 2001 | 1 |
| 1 | 2001 | 2 |
| 1 | 2002 | 1 |
| 2 | 2001 | 6 |
| 2 | 2003 | 12 |
| 2 | 2003 | 2 |
---------------------------------------
And I want to get this:
Table: foo
---------------------------------------
| id_number | date | count |
---------------------------------------
| 1 | 2001 | 3 |
| 1 | 2002 | 1 |
| 2 | 2001 | 6 |
| 2 | 2003 | 14 |
---------------------------------------
I know that I can easily create a new table with the pertinent info. But how can I modify an existing table like this without making a "temp" table? (Note: I have nothing against using a temporary table, I'm just interested in seeing if I can do it this way)
If you want to delete rows you can add a primary key (for distinguish rows) and use two sentences, an UPDATE for the sum and a DELETE for obtain less rows.
You can do something like this:
create table foo (
id integer primary key,
id_number integer,
date integer,
count integer
);
insert into foo values
(1, 1 , 2001 , 1 ),
(2, 1 , 2001 , 2 ),
(3, 1 , 2002 , 1 ),
(4, 2 , 2001 , 6 ),
(5, 2 , 2003 , 12 ),
(6, 2 , 2003 , 2 );
select * from foo;
update foo
set count = count_sum
from (
select id, id_number, date,
sum(count) over (partition by id_number, date) as count_sum
from foo
) foo_added
where foo.id_number = foo_added.id_number
and foo.date = foo_added.date;
delete from foo
using (
select id, id_number, date,
row_number() over (partition by id_number, date order by id) as inner_order
from foo
) foo_ranked
where foo.id = foo_ranked.id
and foo_ranked.inner_order <> 1;
select * from foo;
You can try it here: http://rextester.com/PIL12447
With only one UPDATE
(but with a trigger) you can set a NULL value in count and trigger a DELETE in that case.
create table foo (
id integer primary key,
id_number integer,
date integer,
count integer
);
create function delete_if_count_is_null() returns trigger
language plpgsql as
$BODY$
begin
if new.count is null then
delete from foo
where id = new.id;
end if;
return new;
end;
$BODY$;
create trigger delete_if_count_is_null
after update on foo
for each row
execute procedure delete_if_count_is_null();
insert into foo values
(1, 1 , 2001 , 1 ),
(2, 1 , 2001 , 2 ),
(3, 1 , 2002 , 1 ),
(4, 2 , 2001 , 6 ),
(5, 2 , 2003 , 12 ),
(6, 2 , 2003 , 2 );
select * from foo;
update foo
set count = case when inner_order = 1 then count_sum else null end
from (
select id, id_number, date,
sum(count) over (partition by id_number, date) as count_sum,
row_number() over (partition by id_number, date order by id) as inner_order
from foo
) foo_added
where foo.id_number = foo_added.id_number
and foo.date = foo_added.date
and foo.id = foo_added.id;
select * from foo;
You can try it in: http://rextester.com/MWPRG10961

Postgresql select the two top towns

Have row like:
user | town 1 | town 2 | town 3 | town 4 | town 5 | town 6|
The towns all have integer values where town 3 and town 4 have the largest number
I want to select the two top towns for the user so the end result should be:
user | town 3 | town 4 |
This is the properly normalized model:
create table users (
user_id serial primary key,
user_name varchar(100)
);
create table town (
town_id serial primary key,
town_int int
);
create table user_town (
town_id int references town (town_id),
user_id int references users (user_id),
primary key (user_id, town_id)
);
insert into users (user_name) values ('John');
insert into town (town_int) values (1),(2),(3),(4),(5),(6);
insert into user_town (user_id, town_id) values (1,1),(1,2),(1,3),(1,4),(1,5),(1,6);
How to query it:
select user_id, user_name, town_id, town_int
from
user_town
inner join
users using (user_id)
inner join
town using (town_id)
where user_id = 1
order by town_int desc
limit 2
;
user_id | user_name | town_id | town_int
---------+-----------+---------+----------
1 | John | 6 | 6
1 | John | 5 | 5
Somtimes we have just what we have, poor designed legacy DB for example. Then
WITH t AS (
SELECT 100 as userid, 11 as town1, 23 as town2, 77 as town3, 14 as town4, 15 as town5, 16 as town6
UNION ALL
SELECT 101 as userid, 21 as town1, 235 as town2, 177 as town3, 24 as town4, 25 as town5, 26 as town6
)
SELECT userid, max(r.town) as top1, min(r.town) as top2
FROM t
CROSS JOIN LATERAL(
SELECT town1 as town FROM t t2 WHERE t2.userid=t.userid
UNION ALL
SELECT town2 FROM t t2 WHERE t2.userid=t.userid
UNION ALL
SELECT town3 FROM t t2 WHERE t2.userid=t.userid
UNION ALL
SELECT town4 FROM t t2 WHERE t2.userid=t.userid
UNION ALL
SELECT town5 FROM t t2 WHERE t2.userid=t.userid
UNION ALL
SELECT town6 FROM t t2 WHERE t2.userid=t.userid
ORDER BY town DESC LIMIT 2) AS r
GROUP BY userid;

Query for missing elements

I have a table with the following structure:
timestamp | name | value
0 | john | 5
1 | NULL | 3
8 | NULL | 12
12 | john | 3
33 | NULL | 4
54 | pete | 1
180 | NULL | 4
400 | john | 3
401 | NULL | 4
592 | anna | 2
Now what I am looking for is a query that will give me the sum of the values for each name, and treats the nulls in between (orderd by the timestamp) as the first non-null name down the list, as if the table were as follows:
timestamp | name | value
0 | john | 5
1 | john | 3
8 | john | 12
12 | john | 3
33 | pete | 4
54 | pete | 1
180 | john | 4
400 | john | 3
401 | anna | 4
592 | anna | 2
and I would query SUM(value), name from this table group by name. I have thought and tried, but I can't come up with a proper solution. I have looked at recursive common table expressions, and think the answer may lie in there, but I haven't been able to properly understand those.
These tables are just examples, and I don't know the timestamp values in advance.
Could someone give me a hand? Help would be very much appreciated.
With Inputs As
(
Select 0 As [timestamp], 'john' As Name, 5 As value
Union All Select 1, NULL, 3
Union All Select 8, NULL, 12
Union All Select 12, 'john', 3
Union All Select 33, NULL, 4
Union All Select 54, 'pete', 1
Union All Select 180, NULL, 4
Union All Select 400, 'john', 3
Union All Select 401, NULL, 4
Union All Select 592, 'anna', 2
)
, NamedInputs As
(
Select I.timestamp
, Coalesce (I.Name
, (
Select I3.Name
From Inputs As I3
Where I3.timestamp = (
Select Max(I2.timestamp)
From Inputs As I2
Where I2.timestamp < I.timestamp
And I2.Name Is not Null
)
)) As name
, I.value
From Inputs As I
)
Select NI.name, Sum(NI.Value) As Total
From NamedInputs As NI
Group By NI.name
Btw, what would be orders of magnitude faster than any query would be to first correct the data. I.e., update the name column to have the proper value, make it non-nullable and then run a simple Group By to get your totals.
Additional Solution
Select Coalesce(I.Name, I2.Name), Sum(I.value) As Total
From Inputs As I
Left Join (
Select I1.timestamp, MAX(I2.Timestamp) As LastNameTimestamp
From Inputs As I1
Left Join Inputs As I2
On I2.timestamp < I1.timestamp
And I2.Name Is Not Null
Group By I1.timestamp
) As Z
On Z.timestamp = I.timestamp
Left Join Inputs As I2
On I2.timestamp = Z.LastNameTimestamp
Group By Coalesce(I.Name, I2.Name)
You don't need CTE, just a simple subquery.
select t.timestamp, ISNULL(t.name, (
select top(1) i.name
from inputs i
where i.timestamp < t.timestamp
and i.name is not null
order by i.timestamp desc
)), t.value
from inputs t
And summing from here
select name, SUM(value) as totalValue
from
(
select t.timestamp, ISNULL(t.name, (
select top(1) i.name
from inputs i
where i.timestamp < t.timestamp
and i.name is not null
order by i.timestamp desc
)) as name, t.value
from inputs t
) N
group by name
I hope I'm not going to be embarassed by offering you this little recursive CTE query of mine as a solution to your problem.
;WITH
numbered_table AS (
SELECT
timestamp, name, value,
rownum = ROW_NUMBER() OVER (ORDER BY timestamp)
FROM your_table
),
filled_table AS (
SELECT
timestamp,
name,
value
FROM numbered_table
WHERE rownum = 1
UNION ALL
SELECT
nt.timestamp,
name = ISNULL(nt.name, ft.name),
nt.value
FROM numbered_table nt
INNER JOIN filled_table ft ON nt.rownum = ft.rownum + 1
)
SELECT *
FROM filled_table
/* or go ahead aggregating instead */