Need PIVOT or CASE solution for converting columns to rows (NON-Dynamic if possible) - sql

So I have a table that has data such as this:
SCHD_ID | INST_ID |
|---------|---------|
| 1001 | Mike |
| 1001 | Ted |
| 1001 | Chris |
| 1002 | Jill |
| 1002 | Jamie |
| 1003 | Brad |
| 1003 | Carl |
| 1003 | Drew |
| 1003 | Nick |
I need to come up with a query to display the data like below:
|SCHD_ID | INST 1 | INST 2 | INST 3 |
|---------|--------|--------|--------|
| 1001 | Mike | Ted | Chris |
| 1002 | Jill | Jamie | Null |
| 1003 | Brad | Carl | Drew |
I have tried looking into all the pivot descriptions and some case examples but everything seems to use a common repeated value to pivot around. This is one of those cases where the columns need to be dynamic, but only to a point. I can drop off any data after the third instructor. In the example above I did not put in a column for INST 4 for SCHD_ID 1003 even though in my data set example it existed. Can adding in a restraint like this make it possible to come up with a non dynamic solution for the pivot/case statement?
Thanks for the help,
Dwayne

You can do this using row_number() and conditional aggregation. However, your data doesn't have an ordering column, so you cannot guarantee which three instructors you will get:
select schd_id,
max(case when seqnum = 1 then inst_id end) as inst1,
max(case when seqnum = 2 then inst_id end) as inst2,
max(case when seqnum = 3 then inst_id end) as inst3
from (select t.*,
row_number() over (partition by schd_id order by sched_id) as seqnum
from table t
) t
group by SCHD_ID ;
If you have a priority ordering for choosing the instructors, then put the logic in the order by clause.

Related

SQL - Group two rows by columns that value and null on different columns

Question
Say I have a table with such rows:
id | country | place | last_action | second_to_last_action
----------------------------------------------------------
1 | US | 2 | reply |
1 | US | 2 | | comment
4 | DE | 5 | reply |
4 | | | | comment
What I want to do is to combine these by id, country and place so that the last_action and second_to_last_action would be on the same row
id | country | place | last_action | second_to_last_action
----------------------------------------------------------
1 | US | 2 | reply | comment
4 | DE | 5 | reply | comment
How would I approach this? I guess I would need an aggregate here but my mind is hitting completely blank on which one should I use.
It can be expected that there will always be a matching pair.
Background:
Note: this table has been derived from something like this:
id | country | place | action | time
----------------------------------------------------------
1 | US | 2 | reply | 16:15
1 | US | 2 | comment | 15:16
1 | US | 2 | view | 13:16
4 | DE | 5 | reply | 17:15
4 | DE | 5 | comment | 16:16
4 | DE | 5 | view | 14:12
Code used to partition was:
row_number() over (partition by id order by time desc) as event_no
And then I got the last and second_to_last action by getting event_no 1 & 2. So if there's more efficient way to get the last two actions in two distinct columns I would be happy to hear that.
You can fix your first data by using aggregation:
select id, country, place, max(last_action), max(second_to_last_action)
from derived
group by id, country, place;
You can do this from the original table using conditional aggregation:
select id, country, place,
max(case when seqnum = 1 then action end) as last_action,
max(case when seqnum = 2 then action end) as second_to_last_action
from (select t.*,
row_number() over (partition by id order by time desc) as seqnum
from t
) t
group by id, country, place;

Transposing SQL rows data to column

In SQL we need to transform a table in the following way:
Table1:
+-----+---------+-----------+
| ID | insured | DOD |
+-----+---------+-----------+
| 123 | Pam | 6/18/2013 |
| 123 | Nam | 2/12/2010 |
| 123 | Tam | 2/10/2013 |
| 456 | Jessi | 4/6/2003 |
| 457 | Ron | 4/10/2010 |
| 457 | Tom | 5/5/2008 |
+-----+---------+-----------+
Desired output table:
+-----+---------+-----------+-----------+-----------+
| ID | insured | DOD1 | DOD2 | DOD3 |
+-----+---------+-----------+-----------+-----------+
| 123 | Pam | 6/18/2013 | 2/12/2010 | 2/10/2013 |
| 456 | Jessi | 4/6/2003 | null | null |
| 457 | Ron | 4/10/2010 | 5/5/2008 | null |
+-----+---------+-----------+-----------+-----------+
I have seen somewhere that we can use pivot and unpivot, but I am not sure how can I use it here.
Your help is much appreciated.
Assuming that you really want -- or can accept -- the dates in descending order, then you can use conditional aggregation for this:
select id,
max(case when seqnum = 1 then insured end) as insured,
max(case when seqnum = 1 then dod end) as dod_1,
max(case when seqnum = 2 then dod end) as dod_2,
max(case when seqnum = 3 then dod end) as dod_3
from (select t.*,
row_number() over (partition by id order by dod desc) as seqnum
from t
) t
group by id;
If you want to preserve the original ordering, then your question does not have enough information. If you have a column with the ordering, that can be used for row_number().
I would run it like this, guessing that your id is a number and that you want only those records. from vertical to horizontal , pivot is the best option
select id , insured, DOD from yourtable
pivot(max(DOD) for ID in (123,456,457));

SQL on Self Table Join

I am trying to do a simple self-join SQL and a join to a 2nd table and for the life of me I can't figure it out. I've done some research and can't seem to glean the answer from similar questions. This query is for MS-Access running in VB.NET.
I have 2 tables:
TodaysTeams
-----------
TeamNum PlayerName PlayerID
------- ---------- --------
1 Mark 100
1 Brian 101
2 Mike 102
2 Mike 102
(Note the last 2 rows above are not a typo. In this case a player can be paired with themselves to form a team)
TodaysTeamsPoints
-----------------
TeamNum Points
------- ------
1 90
2 85
The result I want is (2 rows, 1 for each team):
TeamNum PlayerName1 PlayerName2 Points
------- ----------- ----------- ------
1 Mark Brian 90
2 Mike Mike 85
Here is my SQL:
SELECT DISTINCT A.TeamNum, A.PlayerName as PlayerName1, B.PlayerName AS PlayerName2, C.Points
FROM ((TodaysTeams A INNER JOIN
TodaysTeamsPoints C ON A.TeamNum = C.TeamNum) INNER JOIN
TodaysTeams B ON A.TeamNum = B.TeamNum)
ORDER BY C.Points DESC
I know I am missing another join as I'm returning a cartesian produce (i.e. too many rows).
I would appreciate help as to what I am missing here.
Thank you.
Whilst Gordon's suggested method will work well providing that there are at most two players per team, the method breaks down if ever you add another team member and wish to display them in a separate column.
The difficulty in displaying the data in a manner that you can describe logically but cannot easily produce using a query usually implies that the database structure is sub optimal.
For your particular setup, I would personally recommend the following structure:
+---------------+ +----------+------------+
| Players | | PlayerID | PlayerName |
+---------------+ +----------+------------+
| PlayerID (PK) | | 100 | Mark |
| PlayerName | | 101 | Brian |
+---------------+ | 102 | Mike |
+----------+------------+
+-------------+ +--------+----------+
| Teams | | TeamID | TeamName |
+-------------+ +--------+----------+
| TeamID (PK) | | 1 | Team1 |
| TeamName | | 2 | Team2 |
+-------------+ +--------+----------+
+-------------------+ +--------+--------------+----------+
| TeamPlayers | | TeamID | TeamPlayerID | PlayerID |
+-------------------+ +--------+--------------+----------+
| TeamID (PK) | | 1 | 1 | 100 |
| TeamPlayerID (PK) | | 1 | 2 | 101 |
| PlayerID (FK) | | 2 | 1 | 102 |
+-------------------+ | 2 | 2 | 102 |
+--------+--------------+----------+
Using this method, you can use condition aggregation or a crosstab query pivoting on the TeamPlayerID to produce each of your columns, and you would not be limited to two columns.
You can use aggregation:
SELECT ttp.TeamNum, MIN(tt.PlayerName) as PlayerName1,
MAX(tt.PlayerName) as PlayerName2,
ttp.Points
FROM TodaysTeamsPoints as ttp INNER JOIN
TodaysTeams as tt
ON tt.TeamNum = ttp.TeamNum
GROUP BY ttp.TeamNum, ttp.Points
ORDER BY ttp.Points DESC;

SQL Select Rows where a value of a multiple value field occurs only once

I have the following problem:
I have a table with different columns describing objects. One of this column let's assume can contain the values 1,2,3,4,5,6,7,8,9,10. Within this table objects can contain all of these values or some just contain for example value 1,3,5 (so 0 to n values)
Now I want to find all the objects containing only the value 1 and 2, but I do not want them in my result set if they contain 1,2,3 or other combinations but (1,2).
How do I write this SQL statement?
Sample data (Result set to be expected --> Mark and Michael):
+---------+--------------------+---------------------------+--+
| OBJ | OBJ_CHARACTERISTIC | CHARACTERISTIC_DATE_ADDED | |
+---------+--------------------+---------------------------+--+
| Mark | 1 | 15.01.2018 | |
| Mark | 2 | 15.02.2018 | |
| Jimmy | 1 | 31.01.2018 | |
| Jimmy | 2 | 11.02.2018 | |
| Jimmy | 4 | 15.03.2018 | |
| Jimmy | 5 | 15.04.2018 | |
| Jimmy | 6 | 15.04.2018 | |
| Harry | 1 | 08.01.2018 | |
| Harry | 2 | 11.01.2018 | |
| Harry | 3 | 15.02.2018 | |
| Michael | 1 | 15.06.2018 | |
| Michael | 2 | 15.07.2018 | |
| Dwayne | 4 | 15.01.2018 | |
| Dwayne | 5 | 15.01.2018 | |
| Dwayne | 6 | 15.01.2018 | |
+---------+--------------------+---------------------------+--+
You could use analytic counts to see how many characteristics each object has, and how many of the ones you are looking for; and then compare those counts:
select obj, obj_characteristic, characteristic_date_added
from (
select obj, obj_characteristic, characteristic_date_added,
count(distinct obj_characteristic) over (partition by obj) as c1,
count(distinct case when obj_characteristic in (1,2) then obj_characteristic end)
over (partition by obj) as c2
from your_table
)
where c1 = c2;
With your sample data that gives:
OBJ OBJ_CHARACTERISTIC CHARACTERI
------- ------------------ ----------
Mark 1 2018-01-15
Mark 2 2018-02-15
Michael 1 2018-06-15
Michael 2 2018-07-15
From the way the question is worded it sounds like you want the complete rows, as above; froma comment you may only want the names. If so you can just change the outer select to:
select distinct obj
from ...
OBJ
-------
Mark
Michael
or use aggregates instead via a having clause:
select obj
from your_table
group by obj
having count(distinct obj_characteristic)
= count(distinct case when obj_characteristic in (1,2) then obj_characteristic end);
OBJ
-------
Mark
Michael
db<>fiddle demo of all three.
In this case, as 1 and 2 are contiguous, you could also do this with min/max, as an aggregate to just get the names:
select obj
from your_table
group by obj
having min (obj_characteristic) = 1
and max(obj_characteristic) = 2;
or analytically to get the complete rows:
select obj, obj_characteristic, characteristic_date_added
from (
select obj, obj_characteristic, characteristic_date_added,
min(obj_characteristic) over (partition by obj) as min_char,
max(obj_characteristic) over (partition by obj) as max_char
from your_table
)
where min_char = 1
and max_char = 2;
but the earlier versions are more generic.
If you are just looking for sql to return rows values '1,2' and nothing else use:
select * from table where column like '%1,2'
Post an example of the data, it may be more helpful to understand.
#dwin90 You could try:
SELECT obj
FROM your_table
WHERE (OBJ_CHARACTERISTIC=1 OR OBJ_HARACTERISTIC=2 AND OBJ_CHARACTERISTIC !> 2
)GROUP BY OBJ

select multiple columns count one column and group by one column in one table

I have a table named [delivery], with columns [ID],[Employee],[Post],[c_name],[Deli_Date],[note]
I need to Select all columns and count Employee and then group by Employee.
data in the table like this:
---------------------------------------------------
| Employee| Post |c_name |Deli_Date |Note |
|---------|-----------|---------------------|-----|
| DAVID |MANAGER | a |11/11/2018 |note |
| DAVID |MANAGER | b |01/01/2015 |note |
| SAM |ACOUNTS | c |10/10/2016 |note |
| DAVID |IT | a |10/02/2015 |note |
| DAVID |DOCTOR | c |20/02/2017 |note |
| JAMES |DELIVERYMAN| b |05/05/2015 |note |
| OLIVER |DRIVER | b |02/02/2014 |note |
| SAM |ACOUNTS | c |02/02/2012 |note |
this code:
select Employee, count(Employee) as TIMES from delivery
group by Employee
the result is: (but I need to show the other columns too).
| Employee| TIMES |
|---------|-------|
| DAVID | 4 |
| JAMES | 1 |
| OLIVER | 1 |
| SAM | 2 |
I need my code show Like This:
| Employee| TIMES | Post |c_name |Deli_Date |Note |
|---------|-------|-----------|---------------------|-----|
| DAVID | 4 |MANAGER | a |11/11/2018 |note |
| JAMES | 1 |DELIVERYMAN| b |05/05/2015 |note |
| OLIVER | 1 |DRIVER | b |02/02/2014 |note |
| SAM | 2 |ACOUNTS | c |10/10/2016 |note |
what is the best Query could give me that result?
see columns [c_name],[Deli_Date] they shows the last inserted data.
or just give me the result without [c_name],[Deli_Date] it's ok.
If you want the last date along with the count, you can use window functions:
select d.Employee, d.cnt, d.Post, d.c_name, d.Deli_Date, d.Note
from (select d.*,
count(*) over (partition by employee) as cnt,
row_number() over (partition by employee order by deli_date desc) as seqnum
from delivery d
) d
where seqnum = 1;
You need to include all the non-aggregated columns in the select and group by clauses
select Employee,
count(Employee) as TIMES,
max(Deli_date) as LAST_DELI_DATE,
post,
note
from delivery
group by Employee, post, note
Max(Deli_date) will give you the latest date.
To get the latest c_name, note, post, etc. you will need a subquery with a rank function sorted by the deli_date. I will add the example later.
select Employee, count(Employee) as TIMES,Post,c_name,Deli_Date,Note from delivery
group by Employee