Adding an additional column to SQL UNION SELECT - sql

Suppose I have the following mapped and normalized tables;
Group User Contact BelongsTo
+-------+------+------+ +-------+------+------+ +-------+------+------+ +-------+------+
| gID| name| col3| | uID| fname| sname| | cID| name| col3| | accID| gID|
+-------+------+------+ +-------+------+------+ +-------+------+------+ +-------+------+
|1 |ABC |? | |1 |JJ |BB | |4 |ABCD |? | |1 |2 |
+-------+------+------+ +-------+------+------+ +-------+------+------+ +-------+------+
|2 |BCD |? | |2 |CC |LL | |5 |BCDE |? | |3 |2 |
+-------+------+------+ +-------+------+------+ +-------+------+------+ +-------+------+
|3 |DEF |? | |3 |RR |NN | |6 |CDEF |? | |5 |3 |
+-------+------+------+ +-------+------+------+ +-------+------+------+ +-------+------+
Using EERM, User and Contact are subclasses of "Account" superclass. (not shown) An account can belong to many groups, thus "BelongsTo" table records the M:N relationship between the Accounts and Group membership.
I would like an SQL statement which will allow me to query all the users and contacts that have membership in a Group matching conditions as follows:
SELECT
tc."cID" AS "accID",
tc."name" AS "accName",
tbt."gID"
FROM "tblContact" tc
INNER JOIN "tblBelongsTo" tbt
ON tbt."accID" = tc."cID"
UNION SELECT
tu."uID" AS "accID",
CONCAT (tu."fname", ' ', tu."sname") AS "accName",
tbt."gID"
FROM "tblUser" tu
INNER JOIN "tblBelongsTo" tbt
ON tbt."accID" = tu."uID"
ORDER BY "accID" ASC;
The above works, I have combined UNION SELECT in the query as the number of columns match either side when I CONCAT the forename and surname together. Resulting in a global "account_name" & "account_id" column.
My question is this: How would I go about adding an extra column so that I can see what the group name is?
ie from this:
Result
+-------+-------+------+
| accID|accName| gID|
+-------+-------+------+
|1 |JJBB |2 |
+-------+-------+------+
|3 |RRNN |2 |
+-------+-------+------+
|5 |BCDE |3 |
+-------+-------+------+
to this:
Result (2)
+-------+-------+------+------+
| accID|accName| gID| name|
+-------+-------+------+------+
|1 |JJBB |2 | BCD|
+-------+-------+------+------+
|3 |RRNN |2 | BCD|
+-------+-------+------+------+
|5 |BCDE |3 | DEF|
+-------+-------+------+------+
It seems everything I have tried causes the UNION SELECT to break (because of unmatched column). Likewise, I had little luck in combining sub-queries. I am probably missing something very obvious...
Thanks in advance.

You can join it to the group table to get the name.
select x.*, g.name
from group g join
(
SELECT
tc."cID" AS "accID",
tc."name" AS "accName",
tbt."gID"
FROM "tblContact" tc
INNER JOIN "tblBelongsTo" tbt
ON tbt."accID" = tc."cID"
UNION
SELECT
tu."uID" AS "accID",
CONCAT (tu."fname", ' ', tu."sname") AS "accName",
tbt."gID"
FROM "tblUser" tu
INNER JOIN "tblBelongsTo" tbt
ON tbt."accID" = tu."uID"
) x on x.gid = g.gid
order by accid;

Related

In PostgreSQL, conditionally count rows

Background
I'm a novice Postgres user running a local server on a Windows 10 machine. I've got a dataset g that looks like this:
+--+---------+----------------+
|id|treatment|outcome_category|
+--+---------+----------------+
|a |1 |cardiovascular |
|a |0 |cardiovascular |
|b |0 |metabolic |
|b |0 |sensory |
|c |1 |NULL |
|c |0 |cardiovascular |
|c |1 |sensory |
|d |1 |NULL |
|d |0 |cns |
+--+---------+----------------+
The Problem
I'd like to get a count of outcome_category by outcome_category for those id who are "ever treated" -- defined as "id's who have any row where treatment=1".
Here's the desired result:
+----------------+---------+
|outcome_category| count |
+----------------+---------+
|cardiovascular | 3 |
|sensory | 1 |
|cns | 1 |
+----------------+---------+
It would be fine if the result had to contain metabolic, like so:
+----------------+---------+
|outcome_category|treatment|
+----------------+---------+
|cardiovascular | 3 |
|metabolic | 0 |
|sensory | 1 |
|cns | 1 |
+----------------+---------+
Obviously I don't need the rows to be in any particular order, though descending would be nice.
What I've tried
Here's a query I've written:
select treatment, outcome_category, sum(outcome_ct)
from (select max(treatment) as treatment,
outcome_category,
count(outcome_category) as outcome_ct
from g
group by outcome_category) as sub
group by outcome_category, sub.treatment;
But it's a mishmash result:
+---------+----------------+---+
|treatment|outcome_category|sum|
+---------+----------------+---+
|1 |cardiovascular |3 |
|1 |sensory |2 |
|0 |metabolic |1 |
|1 |NULL |0 |
|0 |cns |1 |
+---------+----------------+---+
I'm trying to identify the "ever exposed" id's using that first line in the subquery: select max(treatment) as treatment. But I'm not quite getting at the rest of it.
EDIT
I realized that the toy dataset g I originally gave you above doesn't correspond to the idiosyncrasies of my real dataset. I've updated g to reflect that many id's who are "ever treated" won't have a non-null outcome_category next to a row with treatment=1.
Interesting little problem. You can do:
select
outcome_category,
count(x.id) as count
from g
left join (
select distinct id from g where treatment = 1
) x on x.id = g.id
where outcome_category is not null
group by outcome_category
order by count desc
Result:
outcome_category count
----------------- -----
cardiovascular 3
sensory 1
cns 1
metabolic 0
See running example at db<>fiddle.
This would appear to be just a simple aggregation,
select outcome_category, Count(*) count
from t
where treatment=1
group by outcome_category
order by Count(*) desc
Demo fiddle

In SQL, query a table by transposing column results

Background
Forgive the title of this question, as I'm not really sure how to describe what I'm trying to do.
I have a SQL table, d, that looks like this:
+--+---+------------+------------+
|id|sex|event_type_1|event_type_2|
+--+---+------------+------------+
|a |m |1 |1 |
|b |f |0 |1 |
|c |f |1 |0 |
|d |m |0 |1 |
+--+---+------------+------------+
The Problem
I'm trying to write a query that yields the following summary of counts of event_type_1 and event_type_2 cut (grouped?) by sex:
+-------------+-----+-----+
| | m | f |
+-------------+-----+-----+
|event_type_1 | 1 | 1 |
+-------------+-----+-----+
|event_type_2 | 2 | 1 |
+-------------+-----+-----+
The thing is, this seems to involve some kind of transposition of the 2 event_type columns into rows of the query result that I'm not familiar with as a novice SQL user.
What I've tried
I've so far come up with the following query:
SELECT event_type_1, event_type_2, count(sex)
FROM d
group by event_type_1, event_type_2
But that only gives me this:
+------------+------------+-----+
|event_type_1|event_type_2|count|
+------------+------------+-----+
|1 |1 |1 |
|1 |0 |1 |
|0 |1 |2 |
+------------+------------+-----+
You can use a lateral join to unpivot the data. Then use conditional aggregate to calculate m and f:
select v.which,
count(*) filter (where d.sex = 'm') as m,
count(*) filter (where d.sex = 'f') as f
from d cross join lateral
(values (d.event_type_1, 'event_type_1'),
(d.event_type_2, 'event_type_2')
) v(val, which)
where v.val = 1
group by v.which;
Here is a db<>fiddle.

SQL COUNT ignoring a column

I have a doubt on a SQL query:
I have the following result from a query:
select distinct eb.event_type_id, eb.status from eid.event_backlog eb order by 1
|event_type_id|status |
|-------------|----------|
|1 |SUCCESS |
|2 |SUCCESS |
|2 |ERROR |
|3 |SUCCESS |
|3 |ERROR |
|4 |SUCCESS |
i would like to obtain this result doing a distinct on the status:
|event_type_id|count |
|-------------|-------|
|1 |1 |
|2 |2 |
|3 |2 |
|4 |1 |
but the only way that I see to obtain this result is doing the following query:
select
eb.event_type_id,
count(1)
from
(
select
distinct eb.event_type_id, eb.status
from
eid.event_backlog eb
order by
1) eb
group by
eb.event_type_id
I don't like to use an nestled query, there is another way to obtain what i want?
Simply count(distinct eb.status), i.e.
select
eb.event_type_id,
count(distinct eb.status)
from eid.event_backlog eb
group by
eb.event_type_id

SQL Group by main category in self-referenced table

I need to get a list of total sales grouped by the main category and Seller. Note that there can be sales on the main category (this is the best example I can think of at the moment).
Source table
+--------------------------------------+
|ID |Name |Seller|Qty|ParentID|
+--------------------------------------+
|10 |Egg |John |5 |NULL |
|10 |Egg |Anna |2 |NULL |
|10-01|Egg - Small |John |3 |10 |
|10-01|Egg - Small |Anna |4 |10 |
|10-02|Egg - Medium|John |2 |10 |
|10-02|Egg - Medium|Bob |11 |10 |
|10-03|Egg - Large |Anna |7 |10 |
+--------------------------------------+
Desired output
+------------------+
|ID|Name|Seller|Qty|
+------------------+
|10|Egg |John |10 | <- SUM of all sales John has made for any type of egg
|10|Egg |Anna |13 |
|10|Egg |Bob |11 |
+------------------+
I'm getting close with this query, but if someone has not made a sale on the main category, they will get the wrong Name when I use MIN(Name).
Current query
SELECT
SUBSTRING(t1.ID, 1, 2) AS 'ID',
MIN(t1.Name) AS 'Name',
t1.Seller,
SUM(t1.Qty) AS 'Qty'
FROM EggTest t1
GROUP BY
SUBSTRING(t1.ID, 1, 2),
t1.Seller
Current output
+--------------------------+
|ID|Name |Seller|Qty|
+--------------------------+
|10|Egg |Anna |13 |
|10|Egg - Medium|Bob |11 | <- Bob has not made sales on the main category
|10|Egg |John |10 |
+--------------------------+
EDIT: Seeing that multiple answers have already suggested SUBSTRING(Name, 1, 3) it will not work for me. Name does not always start with "Egg".
Update:
Now trying this query:
WITH report AS(
SELECT
ID = CASE WHEN s.ParentID IS NOT NULL THEN s.ParentID ELSE s.ID END,
Name = CASE WHEN s.ParentID IS NOT NULL THEN p.Name ELSE s.Name END,
s.Seller,
s.Qty
FROM EggTest s
LEFT JOIN EggTest p ON p.ID = s.ParentID
)
SELECT ID, Name, Seller, SUM(Qty) AS 'Total'
FROM report
GROUP BY ID, Name, Seller;
But I am getting this strange result:
+--------------------+
|ID|Name|Seller|Total|
+--------------------+
|10|Egg |Anna |24 | <- Wrong (Should be 13)
|10|Egg |Bob |22 | <- Wrong (Should be 11)
|10|Egg |John |15 | <- Correct(!!)
+--------------------+
In the report-table I'm getting some duplicates:
+------------------+
|ID|Name|Seller|Qty|
+------------------+
|10|Egg |John |5 |
|10|Egg |Anna |2 |
|10|Egg |John |3 |
|10|Egg |John |3 |
|10|Egg |Anna |4 |
|10|Egg |Anna |4 |
|10|Egg |John |2 |
|10|Egg |John |2 |
|10|Egg |Anna |7 |
|10|Egg |Anna |7 |
|10|Egg |Bob |11 |
|10|Egg |Bob |11 |
+------------------+
I will consider the source table name as [Sales]
You can use the following
with report as(
select ID = case when s.ParentID is not null then s.ParentID else s.ID end,
Name= case when s.ParentID is not null then p.Name else s.Name end,
s.Seller,
s.Qty
from Sales s
left join Sales p on p.ID = s.ParentID and p.Seller = s.Seller
)
select ID,Name,Seller,sum(Qty) as Qty
from report
group by ID,Name,Seller
Here a demo using Distinct
Here a demo by including the Seller in the left join , which will give you Name of the item for Seller Bob as NULL, the left join should work if you have correct data integrity which means separate table for the Items and Categories
replying on your last comment, here a demo how to make your data clear
Hope this will help you
Try this query. If you need explanation, ask :) But it's rather simple query :)
SELECT MAX(SUBSTRING(ID, 1, 2)) AS ID,
SUBSTRING(Name, 1, 3) AS Name,
Seller,
SUM(Qty) AS Qty
FROM TABLE_NAME
GROUP BY Seller, SUBSTRING(Name, 1, 3)
I'm not sure if ID is always in the format nn[-nn] and if Name can handle other stuffs than eggs...
This shoud works in any case:
;with
m as (
select *, nullif(charindex('-', ID), 0) div_id, nullif(charindex(' - ', name), 0) div_cat
from EggTest
),
c as (
select *,
SUBSTRING(ID, 1, isnull(div_id-1, 1000)) main_ID,
SUBSTRING(name, 1, isnull(div_cat-1, 1000)) main_cat,
nullif(SUBSTRING(name, isnull(div_cat, 1000)+2, 1000), '') sub_cat
from m
)
select main_ID ID, main_cat [Name], Seller, sum(qty) Qty
from c
group by main_ID, main_cat, seller
Outputs:
ID Name Seller Qty
10 Egg Anna 13
10 Egg Bob 11
10 Egg John 10

Complicated min/max multi-table query

I need to get the min and max score of group ids, but only if they are enabled:
cdu_group_sl: cdu_group_cc: cdu_group_ph:
-------------------- -------------------- --------------------
|id |name |enabled | |id |name |enabled | |id |name |enabled |
-------------------- -------------------- --------------------
|1 |sl_1 |1 | |1 |cc_1 |1 | |1 |ph_1 |0 |
|2 |sl_3 |1 | |2 |cc_2 |0 | |2 |ph_2 |1 |
|3 |sl_4 |1 | |3 |cc_3 |1 | |3 |ph_3 |1 |
-------------------- -------------------- --------------------
Scores are found in a separate table:
cdu_user_progress
----------------------------------
|id |group_type |group_id |score |
----------------------------------
|1 |sl |1 |50 |
|1 |cc |1 |10 |
|1 |ph |1 |20 |
|1 |sl |2 |80 |
|1 |sl |3 |20 |
|1 |cc |3 |30 |
|1 |sl |1 |40 |
|1 |ph |1 |50 |
|1 |cc |1 |40 |
|1 |ph |2 |90 |
----------------------------------
I need to get a max and min score for each type of group for only enabled groups (for each type):
---------------------------------------------
|group_type |group_id |min_score |max_score |
---------------------------------------------
|sl |1 |40 |50 |
|sl |2 |80 |80 |
|sl |3 |20 |20 |
|cc |1 |10 |40 |
|cc |3 |30 |30 |
|ph |1 |20 |50 |
|ph |2 |90 |90 |
---------------------------------------------
Any idea what the query might be??? So far I have:
SELECT * FROM cdu_user_progress
JOIN cdu_group_sl ON (cdu_group_sl.id = cdu_user_progress.group_id AND cdu_user_progress.group_type = 'sl')
JOIN cdu_group_cc ON (cdu_group_cc.id = cdu_user_progress.group_id AND cdu_user_progress.group_type = 'cc')
JOIN cdu_group_ph ON (cdu_group_ph.id = cdu_user_progress.group_id AND cdu_user_progress.group_type = 'ph')
WHERE cdu_user_progress.uid = $student->uid
AND (cdu_user_progress.group_type = 'sl' AND cdu_group_sl.enabled = 1)
AND (cdu_user_progress.group_type = 'cc' AND cdu_group_cc.enabled = 1)
AND (cdu_user_progress.group_type = 'ph' AND cdu_group_ph.enabled = 1)
Probably completely wrong...
what about using a union to pick the groups you are interested in - something like:
select group_type, group_id min(score) min_score, max(score) max_score
from (
select id, 'sl' grp from cdu_group_sl where enabled = 1
union all
select id, 'cc' from cdu_group_cc where enabled = 1
union all
select id, 'ph' from cdu_group_ph where enabled = 1
) grps join cdu_user_progress scr
on grps.id = scr.group_id and grps.grp = scr.group_type
group by scr.group_type, scr.group_id
The following is probably the fastest way to do this query. To optimize this, you should have an index on group_id, enabled on each of the three "sl", "cc", and "ph" tables:
select cup.*
from cdu_user_progress cup
where (cup.group_type = 'sl' and
exists (select 1
from cdu_group_sl sl
where sl.id = cup.group_id and
sl.enabled = 1
)
) or
(cup.group_type = 'cc' and
exists (select 1
from cdu_group_cc cc
where cc.id = cup.group_id and
cc.enabled = 1
)
) or
(cup.group_type = 'ph' and
exists (select 1
from cdu_group_ph ph
where ph.id = cup.group_id and
ph.enabled = 1
)
)
As a note, having three tables with the same structure is usually a sign of a poor database schema. These three tables should probably be combined into a single table, which would make this query much easier to write.
If you are just starting up this project, I would recommend refining your data structure. Based on what you showed, you could benefit from only one cdu_groups table with a reference to a new cdu_group_types table, and removing the group_type column from cdu_user_progress.
If this is an established project, where changing the structure would be too disruptive... then one of the other answers showing a query would be a better/easier fit.
Otherwise, you could simplify things with restructured tables and end up with a query like:
SELECT group_type,
group_id,
MIN(score) as min_score,
MAX(score) as max_score
FROM cdu_user_progress c
INNER JOIN cdu_groups g
ON c.group_id=g.id
INNER JOIN cdu_group_types t
ON g.group_type_id=t.id
WHERE enabled=1
GROUP BY group_type, group_id
This is shown, with expected results, in this SQLFiddle. With this structure you can add new group types as you want (and also cut down on amount of tables and joins). Tables would be (simplified in this code below, no FKs or anything):
CREATE TABLE cdu_user_progress
(id INT, group_id INT, score INT)
CREATE TABLE cdu_group_types
(id INT, group_type VARCHAR(3))
CREATE TABLE cdu_groups
(id INT, group_type_id INT, name VARCHAR(10), enabled BIT NOT NULL DEFAULT 1)
Granted moving data to a new structure may be a pain or not reasonable... but wanted to throw this out there as a possibility or just something to chew on.