Can't figure out how to full join two queries with different values in both tables - sql

I have two queries that look like this:
SELECT
sem.Sem_Jahr,
sem.Sem_KW,
COUNT(*) AS Seminars,
bearb.MA_ID
FROM acc_seminar.t_Seminar sem
JOIN acc_seminar.t_Seminar_Thema semth ON sem.Sem_SemTh_ID = semth.SemTh_ID
JOIN acc_ma.t_Mitarbeiter bearb ON sem.Sem_Berb_MA_ID = bearb.MA_ID
WHERE sem.Sem_Sto != 1 AND semth.SemTh_Typ = 7 AND sem.Sem_Jahr = #Jahr and MA_ID = 372
GROUP BY bearb.MA_ID, sem.Sem_KW, sem.Sem_Jahr
the second query is exactly the same, except the condition is WHERE sem.Sem_Sto != 1 AND semth.SemTh_Typ = 7 AND sem.Sem_Jahr = #Jahr and MA_ID = 372
KW refers to week
I want to show results from both queries, combined. The problem is that the first query may have seminar count value for KW 2, but the second one would have NULL. The problem is, I can't figure out how to join them to get the following desired result:
KW | Seminars from query 1 | Seminars from query 2
----------------------------------------------------
2 | NULL | 5
3 | 8 | NULL
4 | 1 | 4
What I tried:
I tried just putting UNION between these two, but then I only get results from first query.
I also tried to write first query normally and then doing a FULL OUTER JOIN with second query as subquery in JOIN, but then I get results for the first query and results from second query only where the week matches with row from first query.
This whole request seems so banal to me, but I just can't figure it out, it doesn't click in my head on how to join them. Any suggestions?

Alway aim for a minimal, reproducable example. My sample data has way less joins, but should still show your issue and possible solutions.
Sample data
create table data
(
year int,
week int,
flag bit
);
insert into data (year, week, flag) values
(2021, 1, 0),
(2021, 1, 1),
(2021, 1, 1),
(2021, 2, 0),
(2021, 2, 0),
(2021, 2, 0),
(2021, 2, 0),
(2021, 3, 1);
Issue reproduction
Second query as subquery:
select coalesce(f.year, t.year) as year,
coalesce(f.week, t.week) as week,
count(1) as countFalse,
t.countTrue
from data f
full join ( select d.year,
d.week,
count(1) as countTrue
from data d
where d.flag = 1
group by d.year,
d.week ) t
on t.year = f.year
and t.week = f.week
where f.flag = 0 --> issue: week 3 not available for flag = 0, results limited...
group by f.year,
t.year,
f.week,
t.week,
t.countTrue
order by f.year,
f.week;
Result missing week = 3:
year week countFalse countTrue
---- ---- ---------- ---------
2021 1 1 2
2021 2 4 null
Solution 1
Isolate both queries in common table expressions (cte_false, cte_true) and join them without where clause in final select.
with cte_false as
(
select d.year,
d.week,
count(1) as countFalse
from data d
where d.flag = 0
group by d.year,
d.week
),
cte_true as
(
select d.year,
d.week,
count(1) as countTrue
from data d
where d.flag = 1
group by d.year,
d.week
)
select coalesce(f.year, t.year) as year,
coalesce(f.week, t.week) as week,
f.countFalse,
t.countTrue
from cte_false f
full join cte_true t
on t.year = f.year
and t.week = f.week;
Solution 2
Perform all calculations first (cte_count), then use pivot to transform the data.
with cte_count as
(
select d.year,
d.week,
d.flag,
count(1) as countFlag
from data d
group by d.year,
d.week,
d.flag
)
select piv.year,
piv.week,
piv.[0] as countFalse,
piv.[1] as countTrue
from cte_count cc
pivot (max(cc.countFlag) for cc.flag in ([0], [1])) piv;
Result
year week countFalse countTrue
---- ---- ---------- ---------
2021 1 1 2
2021 2 4 null
2021 3 null 1
Fiddle to see things in action.

You can do this using conditional aggregation:
SELECT sem.Sem_Jahr,
sem.Sem_KW,
SUM(CASE WHEN sem.Sem_Sto <> 1 AND semth.SemTh_Typ = 7 AND sem.Sem_Jahr = #Jahr and MA_ID = 372 THEN 1 ELSE 0 END) AS Seminars,
SUM( <whatever the second condition is> THEN 1 ELSE 0 END),
bearb.MA_ID
FROM acc_seminar.t_Seminar sem JOIN
acc_seminar.t_Seminar_Thema semth
ON sem.Sem_SemTh_ID = semth.SemTh_ID JOIN
acc_ma.t_Mitarbeiter bearb
ON sem.Sem_Berb_MA_ID = bearb.MA_ID
GROUP BY bearb.MA_ID, sem.Sem_KW, sem.Sem_Jahr;
If I speculate that the difference is one of the columns, such as semth.SemTh_Typ = 8, then this can be simplified by moving common conditions to the WHERE clause:
SELECT sem.Sem_Jahr,
sem.Sem_KW,
SUM(CASE WHEN semth.SemTh_Typ = 7 THEN 1 ELSE 0 END) AS Seminars,
SUM(CASE WHEN semth.SemTh_Typ = 8 THEN 1 ELSE 0 END),
bearb.MA_ID
FROM acc_seminar.t_Seminar sem JOIN
acc_seminar.t_Seminar_Thema semth
ON sem.Sem_SemTh_ID = semth.SemTh_ID JOIN
acc_ma.t_Mitarbeiter bearb
ON sem.Sem_Berb_MA_ID = bearb.MA_ID
WHERE sem.Sem_Sto <> 1 AND
semth.SemTh_Typ IN (7, 8) AND
sem.Sem_Jahr = #Jahr AND
MA_ID = 372
GROUP BY bearb.MA_ID, sem.Sem_KW, sem.Sem_Jahr;

Related

How to extract and pivot in sql

I have tables like following
I treid to sum score in pivoted style..
product date score
A 2020/8/1 1
B 2018/8/1 2
B 2018/9/1 1
C 2017/9/1 2
I'd like to transform them to the following pivotedone.
The index is YEAR(t.date) and columns = product
date A B C
2017 0 0 2
2018 0 3 0
2019 0 0 0
2020 1 0 0
Are there any effective way to achieve this?
Thanks
We can handle this by joining a calendar table containing all years of interest to your current table, aggregating by year, and then using conditional aggregation to find the sum of scores for each product.
WITH years AS (
SELECT 2017 AS year FROM dual UNION ALL
SELECT 2018 FROM dual UNION ALL
SELECT 2019 FROM dual UNION ALL
SELECT 2020 FROM dual
)
SELECT
y.year,
SUM(CASE WHEN t.product = 'A' THEN t.score ELSE 0 END) AS A,
SUM(CASE WHEN t.product = 'B' THEN t.score ELSE 0 END) AS B,
SUM(CASE WHEN t.product = 'C' THEN t.score ELSE 0 END) AS C
FROM years y
LEFT JOIN yourTable t
ON y.year = EXTRACT(YEAR FROM t."date")
GROUP BY
y.year
ORDER BY
y.year;
Demo
One option would be using PIVOT Clause after determining the year range, and joining outerly with your data source and setting the null scores as zeroes :
WITH years AS
(SELECT MIN(EXTRACT(year from "date")) AS min_year,
MAX(EXTRACT(year from "date")) AS max_year
FROM tab)
SELECT year, NVL(A,0) AS A, NVL(B,0) AS B, NVL(C,0) AS C
FROM (SELECT l.year, product, SUM(score) AS score
FROM tab t --> your original data source
RIGHT JOIN (SELECT level + min_year - 1 AS year
FROM years
CONNECT BY level BETWEEN 1 AND max_year - min_year + 1) l
ON l.year = EXTRACT(year from "date")
GROUP BY l.year, product)
PIVOT (SUM(score) FOR product IN('A' AS "A", 'B' AS "B", 'C' AS "C"))
ORDER BY year;
YEAR A B C
---- - - -
2017 0 0 2
2018 0 3 0
2019 0 0 0
2020 1 0 0
Demo

Oracle SQL: How to select only ID‘s which are member in specific groups?

I want to select only those ID‘s which are in specific groups.
For example:
ID GroupID
1 11
1 12
2 11
2 12
2 13
Here I want to select the ID's which are in the groups 11 and 12 but in no other groups.
So the result should show just the ID 1 and not 2.
Can someone provide a SQL for that?
I tried it with
SELECT ID FROM table
WHERE GroupID = 11 AND GroupID = 12 AND GroupID != 13;
But that didn't work.
You can use aggregation:
select id
from mytable
group by id
having min(groupID) = 11 and max(groupID) = 12
This having condition ensures that the given id belongs to groupIDs 11 and 12, and to no other group. This works because 11 and 12 are sequential numbers.
Other options: if you want ids that belong to group 11 or 12 (not necessarily both), and to no other group, then:
having sum(case when groupId in (11, 12) then 1 end) = count(*)
If numbers are not sequential, and you want ids in both groups (necessarily) and in no other group:
having
max(case when groupID = 11 then 1 end) = 1
and max(case when groupID = 12 then 1 end) = 1
and max(case when groupID in (11, 12) then 0 else 1 end) = 0
SELECT t.id FROM table t
where exists(
SELECT * FROM table
where group = 11
and t.id = id
)
and exists(
SELECT * FROM table
where group = 12
and t.id = id
)
and not exists(
SELECT * FROM table
where group = 13
and t.id = id
)
group by t.id
One method is conditional aggregation:
select id
from t
group by id
having sum(case when groupid = 1 then 1 else 0 end) > 0 and
sum(case when groupid = 2 then 1 else 0 end) > 0 and
sum(case when groupid in (1, 2) then 1 else 0 end) = 0 ;
You can use GROUP BY with HAVING and a conditional COUNT:
SELECT id
FROM table_name
GROUP BY ID
HAVING COUNT( CASE Group_ID WHEN 11 THEN 1 END ) > 0
AND COUNT( CASE Group_ID WHEN 12 THEN 1 END ) > 0
AND COUNT( CASE WHEN Group_ID NOT IN ( 11, 12 ) THEN 1 END ) = 0
Or you can use collections:
CREATE TYPE int_list IS TABLE OF NUMBER(8,0);
and:
SELECT id
FROM table_name
GROUP BY id
HAVING int_list( 11, 12 ) SUBMULTISET OF CAST( COLLECT( group_id ) AS int_list )
AND CARDINALITY( CAST( COLLECT( group_id ) AS int_list )
MULTISET EXCEPT int_list( 11, 12 ) ) = 0
(Using collections has the advantage that you can pass the collection of required values as a single bind parameter whereas using conditional aggregation is probably going to require dynamic SQL if you want to pass a variable number of items to the query.)
Both output:
| ID |
| -: |
| 1 |
db<>fiddle here
Use joins:
SELECT DISTINCT c11.ID
FROM (SELECT ID FROM WORK_TABLE WHERE GROUPID = 11) c11
INNER JOIN (SELECT ID FROM WORK_TABLE WHERE GROUPID = 12) c12
ON c12.ID = c11.ID
LEFT OUTER JOIN (SELECT ID FROM WORK_TABLE WHERE GROUPID NOT IN (11, 12)) co
ON co.ID = c11.ID
WHERE co.ID IS NULL;
The INNER JOIN between the first two subqueries ensures that rows exist for both GROUPID 11 and 12, and the LEFT OUTER JOIN and WHERE verify that there are no rows for any other GROUPIDs.
dbfiddle here

Find all records that match a GROUP BY result HAVING count > 1 in SQLite

The GROUP BY and HAVING isn't the hard part. This query results the summary:
SELECT date, account, amount, COUNT(1) AS num
FROM "transactions"
GROUP BY date, account, amount
HAVING num > 1
Something like:
date account amount num
2011-02-07 580416690 -6.4 2
2011-07-19 -50.0 2
2011-08-29 2445588 -22.0 2
2011-12-16 265113334 -0.1 3
But I dont want the summary (4 records). I want all the relevant records (so 2 + 2 + 2 + 3 = 9 records). If the GROUP BY was on 1 column, that wouldn't be hard either, but with 3 columns...
How do I get the actual records with those values? 1 query must be possible. Do I need 3 subqueries?
One way to do it is to join back to transactions
SELECT *
FROM transactions t JOIN
(
SELECT date, account, amount
FROM transactions
GROUP BY date, account, amount
HAVING COUNT(*) > 1
) d
ON (t.date = d.date
AND t.account = d.account
AND t.amount = d.amount) OR
(t.date = d.date
AND t.account IS NULL AND d.account IS NULL
AND t.amount = d.amount)
Here is a SQLFiddle demo

How to transpose recordset columns into rows

I have a query whose code looks like this:
SELECT DocumentID, ComplexSubquery1 ... ComplexSubquery5
FROM Document
WHERE ...
ComplexSubquery are all numerical fields that are calculated using, duh, complex subqueries.
I would like to use this query as a subquery to a query that generates a summary like the following one:
Field DocumentCount Total
1 dc1 s1
2 dc2 s2
3 dc3 s3
4 dc4 s4
5 dc5 s5
Where:
dc<n> = SUM(CASE WHEN ComplexSubquery<n> > 0 THEN 1 END)
s <n> = SUM(CASE WHEN Field = n THEN ComplexSubquery<n> END)
How could I do that in SQL Server?
NOTE: I know I could avoid the problem by discarding the original query and using unions:
SELECT '1' AS TypeID,
SUM(CASE WHEN ComplexSubquery1 > 0 THEN 1 END) AS DocumentCount
SUM(ComplexSubquery1) AS Total
FROM (SELECT DocumentID, BLARGH ... AS ComplexSubquery1) T
UNION ALL
SELECT '2' AS TypeID,
SUM(CASE WHEN ComplexSubquery2 > 0 THEN 1 END) AS DocumentCount
SUM(ComplexSubquery2) AS Total
FROM (SELECT DocumentID, BLARGH ... AS ComplexSubquery2) T
UNION ALL
...
But I want to avoid this route, because redundant code makes my eyes bleed. (Besides, there is a real possibility that the number of complex subqueries grow in the future.)
WITH Document(DocumentID, Field) As
(
SELECT 1, 1 union all
SELECT 2, 1 union all
SELECT 3, 2 union all
SELECT 4, 3 union all
SELECT 5, 4 union all
SELECT 6, 5 union all
SELECT 7, 5
), CTE AS
(
SELECT DocumentID,
Field,
(select 10) As ComplexSubquery1,
(select 20) as ComplexSubquery2,
(select 30) As ComplexSubquery3,
(select 40) as ComplexSubquery4,
(select 50) as ComplexSubquery5
FROM Document
)
SELECT Field,
SUM(CASE WHEN RIGHT(Query,1) = Field AND QueryValue > 1 THEN 1 END ) AS DocumentCount,
SUM(CASE WHEN RIGHT(Query,1) = Field THEN QueryValue END ) AS Total
FROM CTE
UNPIVOT (QueryValue FOR Query IN
(ComplexSubquery1, ComplexSubquery2, ComplexSubquery3,
ComplexSubquery4, ComplexSubquery5)
)AS unpvt
GROUP BY Field
Returns
Field DocumentCount Total
----------- ------------- -----------
1 2 20
2 1 20
3 1 30
4 1 40
5 2 100
I'm not 100% positive from your example, but perhaps the PIVOT operator will help you out here? I think if you selected your original query into a temporary table, you could pivot on the document ID and get the sums for the other queries.
I don't have much experience with it though, so I'm not sure how complex you can get with your subqueries - you might have to break it down.

SQL query with join, sum, group by, etc

I'm trying to build a report that will look like this:
jan feb mar apr may jun jul ago sep oct nov dec
food 0 1 1 2 0 0 3 1 0 0 1 1
car 1 0 0 0 1 2 1 0 1 2 3 4
home 0 0 1 2 2 2 5 1 2 4 0 0
other 0 0 0 0 0 0 0 0 0 0 0 0
I have two tables: t_item and t_value. t_item has 2 columns: itemID and itemName. t_value has 3 columns: itemID, value, date.
With the following query I can generate a list with all the itens, even with the empty ones.
SELECT t_item.itemID, ISNULL(SUM(t_value.value), 0) AS value
FROM t_value RIGHT OUTER JOIN t_item ON t_value.itemID = t_item.itemID
GROUP BY t_item.itemID
But, if I try to include a MONTH column (as follows) the result will show only the items with values...
SELECT t_item.itemID, ISNULL(SUM(t_value.value), 0) AS value, MONTH(date) AS date
FROM t_value RIGHT OUTER JOIN t_item ON t_value.itemID = t_item.itemID
GROUP BY t_item.itemID, MONTH(date)
Is it possible to do it? How do I include into the results the itens with no values and group then by month?
TIA,
Bob
WITH calendar(mon) AS
(
SELECT 1
UNION ALL
SELECT mon + 1
FROM calendar
WHERE mon < 12
)
SELECT itemID, mon, SUM(value)
FROM calendar c, t_item i
LEFT OUTER JOIN
t_value v
ON v.itemID = i.itemID
AND MONTH(date) = mon
GROUP BY
i.itemID, mon
For the "holes" in your data you need a filler table. Join this table with a full outer join to the fact table on month.
month
------
month --values jan through dec
For the formating you have a couple options.
In your reporting tool use the cross tab or matrix function.
In SQL use the CASE function.
In SQL use the Pivot function.
Are you using a reporting tool with crosstab like ability?
If not, you can create a sum column for each month. so your resultset would actually look like that report sample.
SELECT t_item.itemID,
--ISNULL(SUM(t_value.value), 0) AS value,
sum(case when MONTH(date) = 1 then t_value.value else 0 end) AS m1_sum,
sum(case when MONTH(date) = 2 then t_value.value else 0 end) AS m2_sum,
sum(case when MONTH(date) = 3 then t_value.value else 0 end) AS m3_sum,
--etc
FROM t_value RIGHT OUTER JOIN t_item ON t_value.itemID = t_item.itemID
GROUP BY t_item.itemID
Here's an example:
create table #months (value int, name varchar(12))
create table #items (value int, name varchar(24))
create table #sales (month int, item int, sales int)
insert into #months values (1, 'jan')
insert into #months values (2, 'feb')
insert into #months values (3, 'mar')
insert into #items values (1, 'apple')
insert into #items values (2, 'pear')
insert into #items values (3, 'nut')
insert into #sales values (1,1,12)
insert into #sales values (2,2,3)
insert into #sales values (2,2,5)
insert into #sales values (3,3,7)
You can query it using a PIVOT table, like:
select *
from (
select
item = #items.name
, month = #months.name
, sales = isnull(sum(#sales.sales),0)
from #months
cross join #items
left join #sales on #months.value = #sales.month
and #items.value = #sales.item
group by #months.name, #items.name
) vw
pivot (sum(sales) for month in ([jan],[feb],[mar])) as PivotTable
Or as an alternative, a regular query:
select
item = #items.name
, jan = sum(case when #sales.month = 1 then sales else 0 end)
, feb = sum(case when #sales.month = 2 then sales else 0 end)
, mar = sum(case when #sales.month = 3 then sales else 0 end)
from #items
left join #sales on #items.value = #sales.item
group by #items.name
Both result in:
item jan feb mar
apple 12 0 0
nut 0 0 7
pear 0 8 0
In the first example, the "cross join" ensures all months and values are present. They're then "left joined", so even the rows with no values are displayed.
The IsNull() is just so that it displays 0 instead of NULL for a month in which that particular item was not sold.