dynamic pivot for resultset - sql

I'm looking for an efficient way to build the following resultset. Someone has an idea how I could get started.
I have no idea to start. Reading something about pivot but dont know.
The Problem is that the rows in Table are unlimted, means that i dont know how many column must be show.
Table1
--------------
|ID1 | Name1|
--------------
| 1 | Name1|
| 2 | Name2|
| 3 | Name3|
Table 2
--------------------
|ID2 | Name2| Use |
--------------------
| 1 | xyz1 |True |
| 2 | xyz2 |False|
| 3 | xyz3 |True |
| 4 | xyz4 |True |
Table3
--------------------
|ID_3|FK_ID1 |FK_ID2|
| 1 | 1 | 1 |
| 3 | 3 | 1 |
| 4 | 1 | 3 |
| 5 | 2 | 3 |
Resultset
---------------------------------
ID1 | Name1 | xyz1| xyz3 | xyz4 |
--------------------------
1 | Name1 | True|True | False|
2 | Name2 |False| True | False|
3 | Name3 | True|False | False|

To pivot over a fixed list of columns, you can do conditional aggregation:
select
t1.id1,
t1.name1,
max(case when t2.name2 = 'xyz1' then t2.use else 'False' end) xyz1,
max(case when t2.name2 = 'xyz3' then t2.use else 'False' end) xyz3,
max(case when t2.name2 = 'xyz4' then t2.use else 'False' end) xyz4
from table1 t1
inner join table3 t3 on t3.fk_id1 = t1.id1
inner join table2 t2 on t2.id2 = t3.fk_id2
where t2.name2 in ('xyz1', 'xyz3', 'xyz4')
group by t1.id1, t1.name1

This looks like join and aggregation:
select t1.id, t1.name,
max(case when t2.name2 = 'xyz1' then t2.use else 'false' end) as xyz1,
max(case when t2.name2 = 'xyz2' then t2.use else 'false' end) as xyz2,
max(case when t2.name2 = 'xyz3' then t2.use else 'false' end) as xyz3
from table3 t3 join
table1 t1
on t3.fk_id1 = t1.id join
table2 t2
on t3.fk_id2 = t2.id
group by t1.id, t1.name;

Related

Take the row after the specific row

I have the table, where I need to take the next row after the row which has course 'TA' and flag = 1. For this I created the column rnum (OVER DATE) which may help for finding it
| student | date | course | flag | rnum |
| ------- | ----- | ----------- | ---- | ---- |
| 1 | 17:00 | Math | null | 1 |
| 1 | 17:10 | Python | null | 2 |
| 1 | 17:15 | TA | 1 | 3 |
| 1 | 17:20 | English | null | 4 |
| 1 | 17:35 | Geography | null | 5 |
| 2 | 16:10 | English | null | 1 |
| 2 | 16:20 | TA | 1 | 2 |
| 2 | 16:30 | SQL | null | 3 |
| 2 | 16:40 | Python | null | 4 |
| 3 | 19:05 | English | null | 1 |
| 3 | 19:20 | Literachure | null | 2 |
| 3 | 19:30 | TA | null | 3 |
| 3 | 19:40 | Python | null | 4 |
| 3 | 19:50 | Python | null | 5 |
As a result I should have:
| student | date | course | flag | rnum |
| ------- | ----- | ------- | ---- | ---- |
| 1 | 17:20 | English | null | 4 |
| 2 | 16:30 | SQL | null | 3 |
There are many ways to get your desired result, let's see some of them.
1) EXISTS
You can use the EXISTS clause, specifying a subquery to match for the condition.
SELECT T2.*
FROM #MyTable T2
WHERE EXISTS (
SELECT 'x' x
FROM #MyTable T1
WHERE T1.course = 'TA' AND T1.flag = 1
AND T1.student = T2.student AND T2.rnum = T1.rnum + 1
)
2) LAG
You ca use window function LAG to access previous row for a given order and then filter your resultset with your conditions.
SELECT w.student, w.date, w.course, w.flag, w.rnum
FROM (
SELECT T1.*
, LAG(course, 1) OVER (PARTITION BY student ORDER BY rnum) prevCourse
, LAG(flag, 1) OVER (PARTITION BY student ORDER BY rnum) prevFlag
FROM #MyTable T1
) w
WHERE prevCourse = 'TA' AND prevFlag = 1
3) JOIN
You can self-JOIN your table on the next rnum and keep only the rows who match the right condition.
SELECT T2.*
FROM MyTable T1
JOIN MyTable T2 ON T1.student = T2.student AND T2.rnum = T1.rnum + 1
WHERE T1.course = 'TA' AND T1.flag = 1
4) CROSS APPLY
You can use CROSS APPLY to specify a subquery with the matching condition. It is pretty similar to EXISTS clause, but you will also get in your resultset the columns from the subquery.
SELECT T2.*
FROM #MyTable T2
CROSS APPLY (
SELECT 'x' x
FROM #MyTable T1
WHERE T1.course = 'TA' AND T1.flag = 1
AND T1.student = T2.student AND T2.rnum = T1.rnum + 1
) x
5) CTE
You can use common table expression (CTE) to extract matching rows and then use it to filter your table with a JOIN.
;WITH
T1 AS (
SELECT student, rnum
FROM #MyTable T1
WHERE T1.course = 'TA' AND T1.flag = 1
)
SELECT T2.*
FROM #MyTable T2
JOIN T1 ON T1.student = T2.student AND T2.rnum = T1.rnum + 1
Adding the rownumber was a good start, you can use it to join the table with itself:
WITH matches AS (
SELECT
student,
rnum
FROM table
WHERE flag = 1
AND course = 'TA'
)
SELECT t.*
FROM table t
JOIN matches m
on t.student = m.student
and t.rnum = m.rnum + 1

SQL - Get value from next row based on specific condition

I have the following table:
| ID | Ref | Type | LogTime |
|----|-----|------|----------------------|
| 1 | AA | 1 | 2019-05-03 18:30:01 |
| 2 | BB | 1 | 2019-05-03 19:42:02 |
| 3 | AA | 3 | 2019-05-04 12:30:03 |
| 4 | BB | 3 | 2019-05-05 19:42:04 |
| 5 | AA | 1 | 2019-05-06 20:55:05 |
I would like to list all rows with Type = 1 and to include the value of LogTime where Ref values are equal and Type =3
Something like this:
| ID | Ref | Type | LogTime | LogTime_Type3 |
|----|-----|------|----------------------|----------------------|
| 1 | AA | 1 | 2019-05-03 18:30:01 | 2019-05-04 12:30:03 |
| 2 | BB | 1 | 2019-05-03 19:42:02 | 2019-05-05 19:42:04 |
| 5 | AA | 1 | 2019-05-06 20:55:05 | NULL |
I tried to use LEAD(LogTime) Over.. but I were not able to specify the records with type=3
Can you please help.
Here is my SqlFiddle
You can just use join:
SELECT t.*, t3.LogTime as LogTime3
FROM Trans t LEFT JOIN
Trans t3
ON t3.ref = t.ref and t3.TYPE = '3'
WHERE t.TYPE = '1'
ORDER BY t.id;
One way to get the next time is to use OUTER APPLY:
SELECT t.*, t3.LogTime as LogTime3
FROM Trans t OUTER APPLY
(SELECT TOP (1) t3.*
FROM Trans t3
WHERE t3.ref = t.ref and
t3.LogTime > t.LogTime and
t3.TYPE = '3'
ORDER BY t.LogTime ASC
) t3
WHERE t.TYPE = '1'
ORDER BY t.id;
Or, using window functions, a cumulative minimum seems the most appropriate:
SELECT t.*
FROM (SELECT t.*,
MIN(CASE WHEN t.TYPE = '3' THEN t.LogTime END) OVER (PARTITION BY ref ORDER BY LogTime DESC) as LogTime3
FROM Trans t
) t
WHERE t.TYPE = '1'
ORDER BY t.id;
By using subquery also we can achieve your expected output
SELECT DISTINCT
O.ID,
O.Ref,
O.[Type],
O.LogTime,
(SELECT TOP 1 I.LogTime FROM LoginLogout I
WHERE I.Ref = O.Ref
AND I.[Type] = 3
AND I.LogTime > O.LogTime) AS LogTime_Type3
FROM LoginLogout O
WHERE O.[Type] = 1

Oracle - Select row where desired column contains only one specific type of data

I've two Table
Table 1
+--------+--------+
| LC | STATUS |
+--------+--------+
| 010051 | 6 |
+--------+--------+
| 010071 | 2 |
+--------+--------+
| 010048 | 2 |
+--------+--------+
| 010113 | 2 |
+--------+--------+
| 010125 | 2 |
+--------+--------+
Table 2
+--------+-------------+-----------+------------+--------+
| LC | BILL | LAST_BILL | PAYMENT_BY | STATUS |
+--------+-------------+-----------+------------+--------+
| 010125 | BILL/17/001 | 0 | C | 6 |
+--------+-------------+-----------+------------+--------+
| 010125 | BILL/17/002 | 0 | I | 1 |
+--------+-------------+-----------+------------+--------+
| 010125 | BILL/17/003 | 0 | F | 1 |
+--------+-------------+-----------+------------+--------+
| 010125 | BILL/17/004 | 0 | C | 6 |
+--------+-------------+-----------+------------+--------+
| 010113 | BILL/17/005 | 0 | C | 6 |
+--------+-------------+-----------+------------+--------+
| 010113 | BILL/17/006 | 0 | I | 1 |
+--------+-------------+-----------+------------+--------+
| 010048 | BILL/17/007 | 0 | C | 6 |
+--------+-------------+-----------+------------+--------+
| 010071 | BILL/17/008 | 0 | C | 6 |
+--------+-------------+-----------+------------+--------+
Where I just want to get the LC whose PAYMENT_BY is 'C', but others who have 'C' value and other than 'C' value, I don't want to get this LC.
I've try following query, but I think there's have expert who can done it in better way or most tuning way.
SELECT LC
FROM (SELECT T1.LC
FROM TABLE1 T1, TABLE2 T2
WHERE T1.STATUS = 2
AND T1.LC = T2.LC
AND T2.PAYMENT_BY = 'C'
AND LAST_BILL = 0
AND T2.STATUS = 6
MINUS
SELECT T1.LC
FROM TABLE1 T1, TABLE2 T2
WHERE T1.STATUS = 2
AND T1.LC = T2.LC
AND T2.PAYMENT_BY = 'I'
AND LAST_BILL = 0)
Query/Expected Result:
+--------+
| LC |
+--------+
| 010048 |
+--------+
| 010071 |
+--------+
You can do it with NOT EXISTS:
select t2.lc from table2 t2
where
t2.payment_by = 'C'
and
not exists (
select lc from table2
where lc = t2.lc and payment_by <> 'C'
)
If you want all the columns of table2, then:
select t2.* from table2 t2
..........................
select t.lc,
count(case when t.payment_by = 'C' THEN 1 else NULL end ) as count_c,
count(case when t.payment_by <> 'C' THEN 1 else NULL end ) as count_not_c
from table2 t
group by t.lc
having count(case when t.payment_by <> 'C' THEN 1 else NULL end ) < 1
demo
If I understand correctly, I think group by and having is the simplest query:
select t2.lc
from table2 t2
group by t2.lc
having min(t2.payment_by) = 'C' and max(t2.payment_by) = 'C';
This also has the advantage of returning each lc exactly once.

How rotate sql result entries into columns (pivot)

I have table a
| id | value | comment |
|--------------------------|
| 1 | Some1 | comm1 |
|--------------------------|
| 2 | Some2 | comm2 |
|--------------------------|
and i have table b with table a as foreign key
| id | id_a |name | amount | factor |
|--------------------------------------------|
| 1 | 1 |Car | 12 | 2 |
|--------------------------------------------|
| 2 | 1 |Bike | 22 | 5 |
|--------------------------------------------|
| 3 | 2 |Car | 54 | 1 |
|--------------------------------------------|
| 4 | 2 |Bike | 55 | 4 |
|--------------------------------------------|
As result I want to have a combination:
|id| value | comment | Car_Amount | Car_factor | Bike_Amount | Bike_Factor |
|--------------------------------------------------------------------------|
| 1| Some1 | comm1 | 12 | 2 | 22 | 5 |
|--------------------------------------------------------------------------|
| 2| Some2 | comm2 | 54 | 1 | 55 | 4 |
|--------------------------------------------------------------------------|
It is not a pivot as far as I can see. But I am not sure if this is good practise at all. I am not an expert in SQL things, but it looks utterly wrong to mix tables like that.
I mean "they" want to have it as a flat result to use it for reporting...
Is it possible at all?
thanks
Aggregate values like this:
select
a.id, a.value, a.comment,
sum(case when b.name='Car' then b.amount end) as Car_Amount,
sum(case when b.name='Car' then b.factor end) as Car_Factor,
sum(case when b.name='Bike' then b.amount end) as Bike_Amount,
sum(case when b.name='Bike' then b.factor end) as Bike_Factor
from a left join b on a.id=b.id_a
group by a.id, a.value, a.comment;
SELECT t1.id,
t1.value,
MAX(CASE WHEN t2.name = 'Car' THEN t2.amount END) AS Car_Amount,
MAX(CASE WHEN t2.name = 'Car' THEN t2.factor END) AS Car_Factor,
MAX(CASE WHEN t2.name = 'Bike' THEN t2.amount END) AS Bike_amount,
MAX(CASE WHEN t2.name = 'Bike' THEN t2.factor END) AS Bike_Factor
FROM a t1
INNER JOIN b t2
ON t1.id = t2.id_a
GROUP BY t1.id
Try this
SELECT ID,value,comment,
SUM(CASE WHEN Name='Car' THEN Amount END) AS Car_Amount,
SUM(CASE WHEN Name='Car' THEN factor END) AS Car_factor ,
SUM(CASE WHEN Name='Bike' THEN Amount END) AS Bike_Amount,
SUM(CASE WHEN Name='Bike' THEN factor END) AS Bike_factor
FROM TableB
INNER JOIN TableA on TableB.ID= TableA.id
Group by ID,value,comment

Select Query Joined on Two Fields?

I've got a few tables in an access database:
ID | LocationName
1 | Location1
2 | Location2
ID | LocationID | Date | NumProductsDelivered
1 | 1 | 12/10 | 3
2 | 1 | 01/11 | 2
3 | 1 | 02/11 | 2
4 | 2 | 11/10 | 1
5 | 2 | 12/10 | 1
ID | LocationID | Date | NumEmployees | EmployeeType
1 | 1 | 12/10 | 10 | 1 (=Permanent)
2 | 1 | 12/10 | 3 | 2 (=Temporary)
3 | 1 | 12/10 | 1 | 3 (=Support)
4 | 2 | 10/10 | 1 | 1
5 | 2 | 11/10 | 2 | 1
6 | 2 | 11/10 | 1 | 2
7 | 2 | 11/10 | 1 | 3
8 | 2 | 12/10 | 2 | 1
9 | 2 | 12/10 | 1 | 3
What I want to do is pass in the LocationID as a parameter and get back something like the following table. So, if I pass in 2 as my LocationID, I should get:
Date | NumProductsDelivered | NumPermanentEmployees | NumSupportEmployees
10/10 | | 1 |
11/10 | 1 | 2 | 1
12/10 | 1 | 2 | 1
It seems like this should be a pretty simple query. I really don't even need the first table except as a way to fill in the combo box on the form from which the user chooses which location they want a report for. Unfortunately, everything I've done has resulted in me getting a lot more data than I should be getting. My confusion is in how to set up the join (presumably that's what I'm looking for here) given that I want both the date and locationID to be the same for each row in the result set.
Any help would be much appreciated.
Thanks.
EDIT:
Ok - the answer below didn't quite work, but it did set me on the right track and I was able to use the following query:
SELECT t1.Date, t2.NumProductsDelivered,
(SELECT t1a.NumEmployees
FROM table3 t1a
WHERE t1a.EmployeeType=1 AND t1a.LocationID=t1.LocationID AND t1a.Date= t1.Date)
AS "PermEmps",
(SELECT t1b.NumEmployees
FROM table3 t1b
WHERE t1b.EmployeeType=3 AND t1b.LocationID=t1.LocationID AND t1b.Date=t1.Date)
AS "SupportEmps"
FROM table3 AS t1 LEFT JOIN table2 AS t2 ON (t2.Date=t1.Date)
AND (t2.LocationID=t1.LocationID)
WHERE t1.LocationID=2
GROUP BY t1.Date, t1.LocationID, t2.NumProductsDelivered;
This is getting me the results I was looking for. However, in a case where the location has a break between products being delivered, I don't see the correct results. It seems that the recordset stops as soon as there's an empty row and then never picks back up again. So, where I might expect to see this:
Date | NumProductsDelivered | NumPermanentEmployees | NumSupportEmployees
10/10 | | 1 |
11/10 | 1 | 2 | 1
12/10 | 1 | 2 | 1
01/10 | 2 | | 1
06/10 | 1 | |
I only see this:
Date | NumProductsDelivered | NumPermanentEmployees | NumSupportEmployees
10/10 | | 1 |
11/10 | 1 | 2 | 1
12/10 | 1 | 2 | 1
01/10 | 2 | | 1
I think this would work:
DECLARE #LocationId int
SET #LocationId=2
SELECT L2.LocationId, L2.Date, COUNT(DISTINCT NumProductsDelivered) as NumProductsDelivered,
SUM(case when L2.EmployeeType =1 then NumEmployees else 0 end) as NumPermanentEmployees,
SUM(case when L2.EmployeeType =3 then NumEmployees else 0 end) as NumSupportEmployees
FROM L1
RIGHT JOIN L2
ON L1.LocationID=L2.LocationID
AND L1.Date=L2.Date
WHERE L2.LocationId=#LocationId
GROUP BY L2.LocationId, L2.Date
Something like this should work:
[deleted original]
Try this instead (untested):
select t3.date, t2.numproductsdelivered,
(select sum(t3.numemployees)
from table3 t3a
where t3a.locationid = t3.locationid and t3a.date = t3.date and t3a.employeetype = 1
) as numpermanentemployees,
(select sum(t3.numemployees)
from table3 t3b
where t3b.locationid = t3.locationid and t3b.date = t3.date and t3b.employeetype = 3
) as numsupportemployees
from table3 as t3
left join table2 as t2 on t2.locationid = t3.locationid and t2.date = t3.date
where t3.locationid = 2
group by t3.date, t2.numproductsdelivered
If you didn't mind having separate rows for each employee type it could be simplified:
select t3.date, t2.numproductsdelivered, t3.employeetype, sum(t3.numemployees) as numemployees
from table3 as t3
left join table2 as t2 on t2.locationid = t3.locationid and t2.date = t3.date
where t3.locationid = 2 and t3.employeetype in (1, 3)
group by t3.date, t2.numproductsdelivered, t3.employeetype
Edit: Try this query:
SELECT t1.Date
FROM table3 AS t1
WHERE t1.LocationID=2
GROUP BY t1.Date
...and see if you get all the dates.
Then add the left join:
SELECT t1.Date, t2.NumProductsDelivered
FROM table3 AS t1 LEFT JOIN table2 AS t2 ON (t2.Date=t1.Date)
AND (t2.LocationID=t1.LocationID)
WHERE t1.LocationID=2
GROUP BY t1.Date, t1.LocationID, t2.NumProductsDelivered;
If it's doing a left INNER join, then it will remove rows from t1 that don't have a matching row in t2. Try explicitly setting a left OUTER join and see if that works. The RDBMS I've used the most defaults to outer, but maybe yours (Access) defaults to inner.
So I am thinking the following will work (add "OUTER" and remove "t1.LocationId"):
SELECT t1.Date, t2.NumProductsDelivered,
(SELECT t1a.NumEmployees
FROM table3 t1a
WHERE t1a.EmployeeType=1 AND t1a.LocationID=t1.LocationID AND t1a.Date= t1.Date)
AS "PermEmps",
(SELECT t1b.NumEmployees
FROM table3 t1b
WHERE t1b.EmployeeType=3 AND t1b.LocationID=t1.LocationID AND t1b.Date=t1.Date)
AS "SupportEmps"
FROM table3 AS t1 LEFT OUTER JOIN table2 AS t2 ON (t2.Date=t1.Date)
AND (t2.LocationID=t1.LocationID)
WHERE t1.LocationID=2
GROUP BY t1.Date, t2.NumProductsDelivered;