Simple SQL pivot - sql

Googling SQL PIVOT brings up answers to more complex situations than I need with aggregations, and although I did find this simple SQL Pivot Query , it's pivoting on a single table, whereas I have two, it's doing a rank partition which I don't know is necessary, I can't actually get it to work, plus it's 5 years old and I'm hoping there's an easier way.
I am sure this is a duplicate question so if someone can find it then please do!
People table:
PersonID
========
1
2
3
Device table:
DeviceID | PersonID
===================
1111 1
2222 1
3333 1
123 2
456 2
9999 3
I do a join like this:
SELECT p.PersonID, d.DeviceID FROM People p
LEFT JOIN Device d on d.PersonID = p.PersonID
Which gives me:
PersonID | DeviceID
===================
1 1111
1 2222
1 3333
2 123
2 456
3 9999
I know what you're thinking, it's just the Device table, but this is a minimal version of the query and tables, there's much more going on in the real ones,
I want to be able to inject a join on the People table to the Device table and get three columns:
Must I use PIVOT to get the results like this? (there will always be a max of three devices per person)
PersonID | 1 | 2 | 3
===============================================
1 1111 2222 3333
2 123 456
3 9999
(Where the blanks would be NULL)
I'm trying:
SELECT PersonID, [1], [2], [3]
FROM (
SELECT p.PersonID, d.DeviceID FROM People p
LEFT JOIN Device d on d.PersonID = p.PersonID) AS r
PIVOT
(
MAX(DeviceID)
FOR DeviceID IN([1], [2], [3])
) AS p;
But it's giving me NULL for all three columns.

The value list defined in the pivot clause must contain actual values from your table. [1], [2], [3] are values from your PersonId, not for DeviceId. So the part for DeviceId in [1], [2], [3] is not producing any results, hence all the null values.
Here is my solution. I constructed a new key_ column to pivot around.
Sample data with added person names
declare #person table
(
personid int,
personname nvarchar(100)
);
insert into #person (personid, personname) values
(1, 'Ann'),
(2, 'Britt'),
(3, 'Cedric');
declare #device table
(
personid int,
deviceid int
);
insert into #device (personid, deviceid) values
(1, 1111),
(1, 2222),
(1, 3333),
(2, 123),
(2, 456),
(3, 9999);
Solution
Run the CTE part on its own to see the intermediate result table. The key_ column contains values like DEVICE_* which are the same values used in the for key_ in part of the pivot clause.
with base as
(
select p.personname,
d.deviceid,
'DEVICE_' + convert(char, ROW_NUMBER() over(partition by p.personname order by d.deviceid)) as 'key_'
from #person p
join #device d
on d.personid = p.personid
)
select piv.personname, piv.DEVICE_1, piv.DEVICE_2, piv.DEVICE_3
from base
pivot( max(deviceid) for key_ in ([DEVICE_1], [DEVICE_2], [DEVICE_3]) ) piv;
Result
The intermediate CTE result table
personname deviceid key_
---------- ----------- ----------
Ann 1111 DEVICE_1
Ann 2222 DEVICE_2
Ann 3333 DEVICE_3
Britt 123 DEVICE_1
Britt 456 DEVICE_2
Cedric 9999 DEVICE_1
The final result
personname DEVICE_1 DEVICE_2 DEVICE_3
---------- ----------- ----------- -----------
Ann 1111 2222 3333
Britt 123 456 NULL
Cedric 9999 NULL NULL

Related

Find the Missing Key ID or Numbers from a Column values

Need to find the missing numbers which have been deleted or a Column does not have yet.
For example:
i have a table Named Person have Columns [PersonID] [PersonName]
[PersonID] is primary and incremented Number e.g. From 1 to N.
PersonID PersonName
1001 ABC
1002 ABC
1003 XYZ
1004 MNO
1006 ABC
1008 MNO
1009 ABC
1010 ABC
1011 XYZ
1014 ABC
1015 ABC
1016 XYZ
1017 MNO
In the given table ,there are some missing numbers in Column PersonID like
1005
1007
1012
1013
Need to find the missing Numbers only.
Note: There are more than 20 million records in my table.
So please suggest a faster method to find the desired numbers.
Thanks to all of you who supported and share some points. I have found the way to find the Missing using ROWNUMBER().
SELECT
NOTEXIST FROM (
SELECT ROW_NUMBER() OVER (ORDER BY PERSONID) NOTEXIST ,PERSONID FROM #A ) T
WHERE NOTEXIST NOT IN ( SELECT PERSONID FROM PERSONID )
Create another table and populate all the numbers between Min and Max ranges of PersonID. Do an anti join (Left/right) to get the list of numbers missing.
select * from NewIDTable a
left join OriginalTable b on a.PersonID=b.PersonID
where b.Personid is null
The simplest way is to get ranges. You can do this with lead():
select personid + 1, next_personid - 1 as end_range,
next_personid - personid - 1 as num_missing
from (select t.*,
lead(personid) over (order by personid) as next_personid
from t
) t
where next_personid <> personid + 1;
If you still want the list of ids, you can expand out the ranges, but that depends on the database.
In SQL Server 2008, this is much more performance intensive, but you can do it:
select personid + 1, tnext.personid - 1 as end_range,
text.personid - personid - 1 as num_missing
from t cross apply
(select top (1) t2.person_id
from t t2
where t2.personid > t.person_id
order by t2.personid asc
) tnext
where tnext.personid <> personid + 1;

Pivot Data Using a SQL Select Query

I need to pivot some data when doing a select query. I'm using SQL Server 2014. Here is the format of the original data.
StudentID | DocumentType | PersonID
---------- ------------- --------
00001 DocA 2222
00001 DocB 2222
00002 DocB 2222
00002 DocA 3333
00003 DocA 4444
And I want it to display like...
StudentID | DocumentTypeAPersonID | DocumentTypeBPersonID
--------- --------------------- -----------------------
00001 2222 2222
00002 3333 2222
00003 4444 NULL
Sometimes a student will have both document types. Sometimes they will only have one. Not sure if the "missing" document type would show up as NULL or just blank in that field.
this way might save you some code
SELECT StudentID,
DocumentTypeAPersonID = MAX(CASE WHEN DocumentType ='DocA' THEN PersonID END),
DocumentTypeBPersonID = MAX(CASE WHEN DocumentType ='DocB' THEN PersonID END)
FROM MyTable
GROUP BY StudentID
Here you go.
SELECT StudentID, DocA, DocB FROM
(
SELECT StudentID, DocumentType, PersonID
FROM myTable
) t
pivot
(
MAX(PersonID)
FOR DocumentType IN (DocA, DocB)
) p
This is a static pivot meaning that you have to manually input the columns you want to pivot. If, for example, you also have a DocC then just do this...
SELECT StudentID, DocA, DocB, DocC FROM
(
SELECT StudentID, DocumentType, PersonID
FROM myTable
) t
pivot
(
MAX(PersonID)
FOR DocumentType IN (DocA, DocB, DocC)
) p

countif type function in SQL where total count could be retrieved in other column

I have 36 columns in a table but one of the columns have data multiple times like below
ID Name Ref
abcd john doe 123
1234 martina 100
123x brittany 123
ab12 joe 101
and i want results like
ID Name Ref cnt
abcd john doe 123 2
1234 martina 100 1
123x brittany 123 2
ab12 joe 101 1
as 123 has appeared twice i want it to show 2 in cnt column and so on
select ID, Name, Ref, (select count(ID) from [table] where Ref = A.Ref)
from [table] A
Edit:
As mentioned in comments below, this approach may not be the most efficient in all cases, but should be sufficient on reasonably small tables.
In my testing:
a table of 5,460 records and 976 distinct 'Ref' values returned in less than 1 second.
a table of 600,831 records and 8,335 distinct 'Ref' values returned in 6 seconds.
a table of 845,218 records and 15,147 distinct 'Ref' values returned in 13 seconds.
You should provide SQL brand to know capabilities:
1) If your DB supports window functions:
Select
*,
count(*) over ( partition by ref ) as cnt
from your_table
2) If not:
Select
T.*, G.cnt
from
( select * from your_table ) T inner join
( select count(*) as cnt from your_table group by ref ) G
on T.ref = G.ref
You can use COUNT with OVERin following:
QUERY
select ID,
Name,
ref,
count(ref) over (partition by ref) cnt
from #t t
SAMPLE DATA
create table #t
(
ID NVARCHAR(400),
Name NVARCHAR(400),
Ref INT
)
insert into #t values
('abcd','john doe', 123),
('1234','martina', 100),
('123x','brittany', 123),
('ab12','joe', 101)

How do I select a row from nearly duplicate rows based on a field value?

If I have rows with this data:
ID |Name |ContractType|
---|------------|------------|
1 |Aaron Shatz | 6-month |
2 |Jim Smith |12-month |
3 |Jim Smith | 6-month |
4 |Mark Johnson|12-month |
I can't use Id to determine which record to use: I have to use ContractType. I want to select all records from a table, but if there are records with the same Name value, I want to pick the 12-month contract record.
The result of the query should be:
ID |Name |ContractType|
---|------------|------------|
1 |Aaron Shatz | 6-month |
2 |Jim Smith |12-month |
4 |Mark Johnson|12-month |
Hard coded version
This solution assumes that there are only two contract types namely 6-month and 12-month. Please scroll to the bottom for dynamic version.
Click here to view the demo in SQL Fiddle.
Script:
CREATE TABLE contracts
(
id INT NOT NULL IDENTITY
, name VARCHAR(30) NOT NULL
, contracttype VARCHAR(30) NOT NULL
);
INSERT INTO contracts (name, contracttype) VALUES
('Aaron Shatz', '6-month'),
('Jim Smith', '12-month'),
('Jim Smith', '12-month'),
('Mark Johnson', '12-month'),
('John Doe', '6-month'),
('Mark Johnson', '6-month'),
('Aaron Shatz', '6-month');
SELECT id
, name
, contracttype
FROM
(
SELECT id
, name
, contracttype
, ROW_NUMBER() OVER(PARTITION BY name ORDER BY contracttype) AS rownum
FROM contracts
) T1
WHERE rownum = 1
ORDER BY id;
Output:
id name contracttype
-- ------------ ------------
1 Aaron Shatz 6-month
2 Jim Smith 12-month
4 Mark Johnson 12-month
5 John Doe 6-month
Dynamic version
This moves the contract type data into a table of its own with a sequence column. Based on how the contract types are ordered, the query will fetch the appropriate records.
Click here to view the demo in SQL Fiddle.
Script:
CREATE TABLE contracts
(
id INT NOT NULL IDENTITY
, name VARCHAR(30) NOT NULL
, contracttypeid INT NOT NULL
);
CREATE TABLE contracttypes
(
id INT NOT NULL IDENTITY
, contracttype VARCHAR(30) NOT NULL
, sequence INT NOT NULL
)
INSERT INTO contracttypes (contracttype, sequence) VALUES
('12-month', 1),
('6-month', 3),
('15-month', 2);
INSERT INTO contracts (name, contracttypeid) VALUES
('Aaron Shatz', 2),
('Jim Smith', 2),
('Jim Smith', 3),
('Mark Johnson', 1),
('John Doe', 2),
('Mark Johnson', 2),
('Aaron Shatz', 2);
SELECT id
, name
, contracttype
FROM
(
SELECT c.id
, c.name
, ct.contracttype
, ROW_NUMBER() OVER(PARTITION BY name ORDER BY ct.sequence) AS rownum
FROM contracts c
LEFT OUTER JOIN contracttypes ct
ON c.contracttypeid = ct.id
) T1
WHERE rownum = 1
ORDER BY id;
Output:
id name contracttype
-- ------------ ------------
1 Aaron Shatz 6-month
3 Jim Smith 15-month
4 Mark Johnson 12-month
5 John Doe 6-month
This works only because the OP has confirmed that only two contract types are possible, and the one he wants (for each contractor) happens to be the one that orders first alphabetically. So a couple of coincidences make this solution straight-forward.
;WITH x AS
(
SELECT ID, Name, ContractType, rn = ROW_NUMBER() OVER
(PARTITION BY Name ORDER BY ContractType)
FROM dbo.some_table
)
SELECT ID, Name, ContractType
FROM x
WHERE rn = 1
ORDER BY ID;
If you need to make this more dynamic, I suppose you could say:
DECLARE #PreferredContractType VARCHAR(32);
SET #PreferredContractType = '12-month';
;WITH x AS
(
SELECT ID, Name, ContractType, rn = ROW_NUMBER() OVER
(PARTITION BY Name ORDER BY CASE ContractType
WHEN #PreferredContractType THEN 1 ELSE 2 END
)
FROM dbo.some_table
)
SELECT ID, Name, ContractType
FROM x
WHERE rn = 1
ORDER BY ID;

How to join a one-to-many relationship to ensure that only 1 row from the left comes back?

Sorry about a vague title, but here's what i am trying to do.
I have the following two tables
TABLE_PERSON | TABLE_PHONE
|
col_Name | col_Name col_phoneID col_phoneNbr
-------- | -------- ----------- ------------
Clark Kent | Clark Kent 1 111-111-1111
Bruce Wayne | Clark Kent 2 222-222-2222
Peter Parker | Peter Parker 2 333-333-3333
| Peter Parker 3 444-444-4444
| Bruce Wayne 3 555-555-5555
| Bruce Wayne 4 666-666-6666
The col_Name is actually an ID field, but i am using a name to demostrate. Here's what i want to get by using one SQL statement
col_Name col_phoneNbr
-------- ------------
Clark Kent 111-111-1111
Peter Parker 333-333-3333
Bruce Wayne 555-555-5555
The phone_ID is actually a type of phone number such as main, fax, toll-free, etc. So in my output i would like to have a phone number for each individual, but be selected in a such a way that if type "1" is not available, select type "2", if type "2" is not there, select type "3", etc. In my case there are 6 types, and their priority is something like 4 > 2 > 1 > 3 > 5 > 6, so i would like to be able to select only the first available number, and NULL if neither is there.
I did this in a previous project by using a very convoluted method where i would select all type "1" and then all type "2" while filtering type "1" using a "NOT IN" clause and then UNIONing the two sets together. Actually, it ended up being like 4 sets, and for the 1000+ records i was pulling out, it ran really slow.
I should mention that this is MS SQL Server 2000, so i can't use any 2005 features.
Thanks in advance!
SELECT
pe.col_Name,
(SELECT TOP (1)
ph.col_PhoneNbr
FROM TABLE_PHONE ph
WHERE pe.col_Name = ph.col_Name
ORDER BY
CASE col_phoneID
WHEN 4 THEN 1
WHEN 2 THEN 2
WHEN 1 THEN 3
WHEN 3 THEN 4
WHEN 5 THEN 5
WHEN 6 THEN 6
END
) as col_phoneNbr
FROM TABLE_PERSON pe
The simple case where the priority of the phone ID is its numeric value:
select pers.col_Name, ph2.col_phoneNbr
from TABLE_PERSON pers
inner join (select col_Name, min(col_phoneID) from TABLE_PHONE group by col_Name) as ph1 on ph1.col_Name=pers.col_Name
inner join TABLE_PHONE ph2 on ph2.col_Name=pers.col_Name and ph2.col_PhoneID=ph1.col_PhoneID
Now, let's create a mapping to handle the priority <-> ID:
Try this:
create table phone_prio (phone_ID int, prio int);
insert into phone_prio (phone_ID, prio) values (4, 1);
insert into phone_prio (phone_ID, prio) values (2, 2);
insert into phone_prio (phone_ID, prio) values (1, 3);
insert into phone_prio (phone_ID, prio) values (3, 4);
insert into phone_prio (phone_ID, prio) values (5, 5);
insert into phone_prio (phone_ID, prio) values (6, 6);
and update the naive case:
select pers.col_Name, ph2.col_phoneNbr
from TABLE_PERSON pers
inner join (
select col_Name, min(pr.prio) as prio
from TABLE_PHONE ph
inner join phone_prio pr on pr.phone_ID=ph.col_phoneID
group by col_Name
) as ph1 on ph1.col_Name=pers.col_Name
inner join phone_prio pr1 on pr.prio=ph1.prio
inner join TABLE_PHONE ph2 on ph2.col_Name=pers.col_Name and ph2.col_PhoneID=pr1.phoneID
You want to use the HAVING clause
GROUP by col_phoneID
HAVING col_phoneID = MAX(col_phoneID)
Obviously the MAX isn't what you want so you may have to add some things to your query
But I hope it points you in the right direction.