Count by two columns in SQL Server? - sql

Here is an example table:
CREATE TABLE Example
(
LastName varchar(255),
FirstName varchar(255),
HomeAddress varchar(255),
City varchar(255)
);
INSERT INTO Example VALUES ('Murphy', 'James','123 Easy St', 'New York');
INSERT INTO Example VALUES ('Black', 'John','123 Easy St', 'Boston');
INSERT INTO Example VALUES ('Black', 'Amy','123 Easy St', 'Chicago');
INSERT INTO Example VALUES ('Simpson', 'Bill','123 Easy St', 'New York');
INSERT INTO Example VALUES ('Jones', 'James','123 Easy St', 'Chicago');
INSERT INTO Example VALUES ('Black', 'John','123 Easy St', 'Boston');
INSERT INTO Example VALUES ('Murhpy', 'James','123 Easy St', 'New York');
I want to be able to count by two columns, 'LastName' and 'City'. That is - I want this:
Name New York Boston Chicago
-------------------------------------
Jones 0 0 1
Black 0 2 1
Simpson 1 0 0
Murphy 2 0 0
My question is similar to this question. I created the following, which produces very close results:
DECLARE #DynamicPivotQuery AS NVARCHAR(MAX),
#PivotColumnNames AS NVARCHAR(MAX),
#PivotSelectColumnNames AS NVARCHAR(MAX)
--Get distinct values of the PIVOT Column
SELECT #PivotColumnNames = ISNULL(#PivotColumnNames + ',','') + QUOTENAME(City)
FROM (SELECT DISTINCT City FROM Example) AS cat
--Get distinct values of the PIVOT Column with isnull
SELECT #PivotSelectColumnNames
= ISNULL(#PivotSelectColumnNames + ',','')
+ 'ISNULL(' + QUOTENAME(City) + ', 0) AS '
+ QUOTENAME(City)
FROM (SELECT DISTINCT City FROM Example) AS cat
--Prepare the PIVOT query using the dynamic
SET #DynamicPivotQuery =
N'SELECT LastName, ' + #PivotSelectColumnNames + '
FROM Example
pivot(count(City) for City in (' + #PivotColumnNames + ')) as pvt';
--Execute the Dynamic Pivot Query
EXEC sp_executesql #DynamicPivotQuery
BUT, this is not quite what I am after. It produces this:
I tried to add 'GROUP BY LastName' to the end of the query, but it does not work. I get an error:
Column 'pvt.Boston' is invalid in the select list because it is not contained in either an aggregate function or the GROUP BY clause.

You are so close!
Two things:
Use a subquery/derived table to select only the columns you need for your pivot
Don't misspell Murphy
--Prepare the PIVOT query using the dynamic
SET #DynamicPivotQuery =
N'SELECT LastName, ' + #PivotSelectColumnNames + '
FROM (select LastName, City from Example) e
pivot(count(City) for City in (' + #PivotColumnNames + ')) as pvt';
rextester demo: http://rextester.com/ETJ57985
returns:
+----------+--------+---------+----------+
| LastName | Boston | Chicago | New York |
+----------+--------+---------+----------+
| Black | 2 | 1 | 0 |
| Jones | 0 | 1 | 0 |
| Murphy | 0 | 0 | 2 |
| Simpson | 0 | 0 | 1 |
+----------+--------+---------+----------+

I think simple pivot will give your query results
Select * from (
Select LastName, City from Example ) a
pivot (count(city) for city in ([New York],[Boston],[Chicago])) p
If you change your query as below you will get appropriate results:
--Prepare the PIVOT query using the dynamic
SET #DynamicPivotQuery =
N'Select LastName, ' + #PivotSelectColumnNames + ' from ( SELECT LastName, City
FROM Example ) a
pivot(count(City) for City in (' + #PivotColumnNames + ')) as pvt';
Output as below:
+----------+----------+--------+---------+
| LastName | New York | Boston | Chicago |
+----------+----------+--------+---------+
| Black | 0 | 2 | 1 |
| Jones | 0 | 0 | 1 |
| Murhpy | 1 | 0 | 0 |
| Murphy | 1 | 0 | 0 |
| Simpson | 1 | 0 | 0 |
+----------+----------+--------+---------+

Try this -
DECLARE #DynamicPivotQuery AS NVARCHAR(MAX),
#PivotColumnNames AS NVARCHAR(MAX),
#PivotSelectColumnNames AS NVARCHAR(MAX)
--Get distinct values of the PIVOT Column
SELECT #PivotColumnNames = ISNULL(#PivotColumnNames + ',','') + QUOTENAME(City)
FROM (SELECT DISTINCT City FROM #example) AS cat
--Get distinct values of the PIVOT Column with isnull
SELECT #PivotSelectColumnNames
= ISNULL(#PivotSelectColumnNames + ',','')
+ 'ISNULL(' + QUOTENAME(City) + ', 0) AS '
+ QUOTENAME(City)
FROM (SELECT DISTINCT City FROM #example) AS cat
--Prepare the PIVOT query using the dynamic
SET #DynamicPivotQuery =
N'SELECT LastName, ' + #PivotSelectColumnNames + '
FROM #example
pivot(count(City) for City in (' + #PivotColumnNames + ')) as pvt group by LastName, ' + #PivotColumnNames;
--Execute the Dynamic Pivot Query
EXEC sp_executesql #DynamicPivotQuery

Alternative approach not using PIVOT:
DECLARE #sql NVARCHAR(max);
SET #sql = ''; -- not necessary for CONCAT, but necessary if converting this answer to + style string concatenation
SELECT
#sql = CONCAT(#sql, ', COUNT(CASE WHEN city = ''', City, ''' THEN 1 END) as ', QUOTENAME(City))
FROM
Example
GROUP BY City
SET #sql = CONCAT('SELECT Name', #sql, ' FROM example GROUP BY name')
EXEC sp_executesql #sql

Related

How do I include an additional non-aggregated column for each of my in my PIVOT values?

I have the following code fragment which gives me the current results below. I'm attempting to add an additional column for each of my pivoted values in order to include the lastview data for each of my siteuserid / tagname combo (see expected results). Since this column isn't an aggregation, I don't believe an additional pivot would help. I've tried multiple ways of adding lastview, but it always results in additional rows rather than the desired output.
create table #taghits (userid int, email varchar(20), tagname varchar(20), hits int, lastview date)
insert into #taghits select 1, 'email1#here.com', 'tag1', 3, '2020-03-24';
insert into #taghits select 2, 'email2#here.com', 'tag1', 1, '2020-03-17';
insert into #taghits select 2, 'email2#here.com', 'tag2', 1, '2020-03-18';
insert into #taghits select 3, 'email3#here.com', 'tag1', 2, '2020-03-25';
insert into #taghits select 3, 'email3#here.com', 'tag2', 5, '2020-03-28';
select * from #taghits;
DECLARE #Columns3 as NVARCHAR(MAX)
SELECT #Columns3 = ISNULL(#Columns3 + ', ','') + QUOTENAME(TagName)
FROM (
select distinct TagName
from #taghits
) AS TagNames
ORDER BY TagNames.TagName
DECLARE #scolumns as NVARCHAR(MAX)
SELECT #scolumns = ISNULL(#Scolumns + ', ','')+ 'ISNULL(' + QUOTENAME(TagName) + ', 0) AS '+ QUOTENAME(TagName)
FROM (select distinct TagName
from #taghits) AS TagNames
ORDER BY TagNames.TagName
DECLARE #SQL as NVARCHAR(MAX)
SET #SQL = '
select userid, email, ' + #scolumns + '
from
(
select userid, email, tagname, hits
from #taghits
) as TagHits
PIVOT (
SUM(hits)
FOR TagName IN (' + #Columns3 + ')
) AS PivotTable
order by userId
'
exec sp_executesql #SQL;
Current Result
| userid | email | tag1 | tag2 |
|--------|-----------------|------|------|
| 1 | email1#here.com | 3 | 0 |
| 2 | email2#here.com | 1 | 1 |
| 3 | email3#here.com | 2 | 5 |
Desired Result
| userid | email | tag1_hits | tag1_lastview | tag2_hits | tag2_lastview |
|--------|-----------------|-----------|---------------|-----------|---------------|
| 1 | email1#here.com | 3 | 2020-03-24 | 0 | null |
| 2 | email2#here.com | 1 | 2020-03-17 | 1 | 2020-03-18 |
| 3 | email3#here.com | 2 | 2020-03-25 | 5 | 2020-03-28 |
try the following:
DECLARE #cols AS NVARCHAR(MAX),
#query AS NVARCHAR(MAX);
SET #cols = STUFF((select distinct ',
SUM(CASE WHEN tagname=''' + CAST(tagname as varchar(10)) + ''' THEN [hits] ELSE 0 END) AS [' + CAST(tagname as varchar(10)) + '_hits],
MAX(CASE WHEN tagname=''' + CAST(tagname as varchar(10)) + ''' THEN [lastview] ELSE NULL END) AS [' + CAST(tagname as varchar(10)) + '_lastview]'
/*---------------You can add other columns here similar to above--------------*/
FROM #taghits
FOR XML PATH(''),type).value('.','varchar(max)'),1,2,'')
SET #query = 'SELECT userid, email, ' + #Cols + ' FROM #taghits group by userid, email'
print (#query)
exec(#query)
Please see db<>fiddle here.

Dynamic Pivot of Email Addresses

I have tried to research this, and I am unable to find something quite like it. I have a table that may have entries added many times over as well as deleted. I have no idea how many columns I will need, therefore I need a Dynamic Pivot. All the examples I see use a windows function, but I am pivoting email addresses.
The Table would look something like this:
Number | Email
--------------
1 | email1#email.com
1 | email2#email.com
1 | email3#email.com
2 | email4#email.com
2 | email5#email.com
3 | email6#email.com
4 | email7#email.com
4 | email8#email.com
I want the table to look like this(when all are included):
Number | Email1 | Email2 | Email3
---------------------------------------------------------------
1 | email1#email.com | email2#email.com | email3#email.com
2 | email4#email.com | email5#email.com |
3 | email6#email.com | |
4 | email7#email.com | email8#email.com |
If Number 1 wasn't included it would look like:
Number | Email1 | Email2
--------------------------------------------
2 | email4#email.com | email5#email.com
3 | email6#email.com |
4 | email7#email.com | email8#email.com
Thanks for the help!
Here is code to create a mock table:
CREATE TABLE #table
(number INT, email VARCHAR(30))
INSERT INTO #table (number, email)
VALUES (1,'email1#email.com')
,(1,'email2#email.com')
,(1,'email3#email.com')
,(2,'email4#email.com')
,(2,'email5#email.com')
,(3,'email6#email.com')
,(4,'email7#email.com')
,(4,'email8#email.com')
This is similar to what I have tried, I used count to try and just make it work, but was unable.
DECLARE #columns NVARCHAR(MAX), #sql NVARCHAR(MAX);
SET #columns = N'';
SELECT #columns += N', p.' + QUOTENAME(Number)
FROM (SELECT p.Number FROM #table AS p
GROUP BY p.Name) AS x;
SELECT #columns
SET #sql = N'
SELECT ' + STUFF(#columns, 1, 2, '') + '
FROM
(
SELECT p.number, p.email
FROM #test p
) AS j
PIVOT
(
Count(email) FOR Name IN ('
+ STUFF(REPLACE(#columns, ', p.[', ',['), 1, 1, '')
+ ')
) AS p;';
PRINT #sql;
EXEC sp_executesql #sql;
You first need to create a RNO by partition over Number.
The following query should do what you want:
CREATE TABLE #table (Number INT, Email VARCHAR(30))
INSERT INTO #table (Number, Email)
VALUES (1,'email1#email.com')
,(1,'email2#email.com')
,(1,'email3#email.com')
,(2,'email4#email.com')
,(2,'email5#email.com')
,(3,'email6#email.com')
,(4,'email7#email.com')
,(4,'email8#email.com')
SELECT *, ROW_NUMBER() OVER (PARTITION BY Number ORDER BY Number, Email) AS [RNO] INTO #temp FROM #table
DECLARE #DynamicCols NVARCHAR(MAX) = '';
DECLARE #pvt NVARCHAR(MAX) = '';
SET #pvt = STUFF(
(SELECT DISTINCT N', ' + QUOTENAME([RNO]) FROM #temp FOR XML PATH('')),1,2,N'')
SET #DynamicCols = STUFF(
(SELECT DISTINCT N', ' + QUOTENAME([RNO]) + ' AS '+ QUOTENAME('Email' + CAST([RNO] AS VARCHAR(MAX))) FROM #temp FOR XML PATH('')),1,2,N'')
EXEC (N'SELECT [Number],'+ #DynamicCols+'
FROM #temp tmp
PIVOT (MAX([Email]) FOR RNO IN('+#pvt+')) AS PIV');

rows to columns based on first column

I have a student table like below
name | subject | scode
sam | science | 20
sam | computer | 30
sam | language | 50
sam | history | 20
joe | PET | 30
joe | computer | 50
dan | lab | 40
i am looking for out put like below
name | 20 | 30 | 40 | 50
sam | science | computer | null | language
sam | history | null | null | null
joe | null | PET | null | Computer
dan | null | null | lab | null
there are chances a student can add one more subject in future and code is dynamic for that particular student
i tried using for xml however able to get in the format of xml but not able to transpose it. any help in pivoting this as per the output is possible?
I think pivoting in combination with dynamix SQL qould do the trick. I created an approach for this, but this will require some more modification: currently it would group away the second line with scode 20 for student sam. Give it a try - when you get stuck, I will try to modify it a little more:
IF OBJECT_ID('dbo.tbl_test') IS NOT NULL
DROP TABLE tbl_test
GO
CREATE TABLE tbl_test (
sName varchar(25)
,sSubject varchar(25)
,sCode int
)
GO
INSERT INTO tbl_test VALUES
('sam', 'science', 20)
,('sam', 'computer', 30)
,('sam', 'language', 50)
,('sam', 'history', 20)
,('joe', 'PET', 30)
,('joe', 'computer', 50)
,('dan', 'lab', 40)
DECLARE #Cols NVARCHAR(MAX);
DECLARE #Qry NVARCHAR(MAX);
SELECT #Cols = STUFF((SELECT DISTINCT ', [' + CAST(scode AS VARCHAR(5)) + ']'
FROM tbl_test
ORDER BY 1
FOR XML PATH ('')), 1, 1, '')
SET #Qry = 'WITH cte AS(
SELECT sName_GRP, sName, ' + #Cols + '
FROM (
SELECT sName, sCode, sSubject, sName + ' + CHAR(39) + '_' + CHAR(39) + ' + RIGHT(' + char(39) + '0000' + CHAR(39) +' + CAST(ROW_NUMBER() OVER (PARTITION BY sName, sCode ORDER BY sName, sCode) AS VARCHAR(5)), 5) sName_GRP
FROM tbl_test
) AS j
PIVOT
(
MAX(sSubject) FOR sCode in (' + #Cols + ')
) AS p
)
SELECT sName, ' + #Cols + '
FROM cte'
EXEC sp_executesql #Qry

Convert Decimal to String on Pivot

example Code
Declare
#table1 VARCHAR(MAX)
Set #table1 = 'Select * from #tempTbl'
Declare
#List VARCHAR(MAX),
#Pivot VARCHAR(MAX)
Select #List = ISNULL(#List + ',', '') + TrxCd From TransacMaster Where Module = 'CB'
Set #Pivot = '
SELECT ROW_NUMBER() OVER (ORDER BY DebtorCd ASC) As RowIndex, *
FROM (
Select Distinct
c.UserId,
d.Name,
b.TrxCd
SUM(b.Amount) As Amount
From tbl_InvH a
Inner Join tbl_InvD b on a.subCd = b.subCd and a.InvNo = b.InvNo
Left Join tbl_User c On a.UserId = c.UserId
Left Join tbl_Personnel d on c.PersonnelId = d.PersonnelId
Group By c.UserId, d.Name, b.TrxCd
) as s
PIVOT
(
SUM(Amount)
FOR TrxCd IN (' + #List + ')
)AS pvt'
Declare #Result nVarchar(MAX)
Set #Result = #table1 + '
Union All
' + #Pivot
Exec sp_executesql #Result
I want to Convert Field Amount from decimal to String, because after this I want to UNION with another tables, but field amount and field from another table is different type.
I have tried CAST(SUM(Amount) as Varchar) But Error :
'CAST' is not a recognized aggregate function.
I can't Convert on Main Select because Field of TrxCd is Dynamic
Result Of Pivot
RowIndex | UserId | Name | IT01 | IT02 | IT03 | IT04
--------------------------------------------------------------------------------
1 | John | John Ivy | 2,000 | 2,000 | 1,000 | 5,000
2 | Merry | Merry Ish | 1,000 | 1,000 | 1,000 | 6,000
other Table
RowIndex | UserId | Name | Transac1 | Transac2 | Transac3 | Transac4
-------------------------------------------------------------------------------------------------
1 | John | John Ivy | Trx Bank A | Trx Bank B | Trx Bank C | Trx Bank D
What should I do to Convert Field Amount from Pivot.
Because of the dynamic nature, you could take the same #List expression Select #List = ISNULL(#List + ',', '') + TrxCd From TransacMaster Where Module = 'CB' and create a second for the dynamic casting in the main select;
Select #List2 = ISNULL(#List2 + ',', '') + 'cast(' + TrxCd + 'as varchar)' From TransacMaster Where Module = 'CB'
Then use this and additional columns required to replace the asterix in the main select;
'SELECT ROW_NUMBER() OVER (ORDER BY DebtorCd ASC) As RowIndex, UserId, Name,' + #List2 +'....'

Dynamically create columns in SQL select query and join tables

I have 2 tables. They are as follows
Table : Grade
GradeID | Grade
-----------------
1 | Chopsaw
2 | Classic
3 | Chieve
Table : Moulded Quantity
Batch ID | Grade | Moulded | Date
-------------------------------------
1 | 1 | 150 | 21st May
2 | 1 | 150 | 22nd May
3 | 2 | 150 | 21st May
4 | 2 | 150 | 21st May
5 | 2 | 150 | 22nd May
I should get the Output like the following
Date | Moulded | Chopsaw | Classic | Cieve
--------------------------------------------------
21st May | 450 | 150 | 300 | 0
22nd May | 300 | 150 | 150 | 0
I am using MSSQL 2008 and i use Crystal report to display the same.
If the number of grades is known beforehand then you can do it with a static query.
SELECT date,
SUM(moulded) moulded,
SUM(CASE WHEN grade = 1 THEN moulded ELSE 0 END) Chopsaw,
SUM(CASE WHEN grade = 2 THEN moulded ELSE 0 END) Classic,
SUM(CASE WHEN grade = 3 THEN moulded ELSE 0 END) Chieve
FROM moulded_quantity
GROUP BY date
This query is not vendor specific so it should work on any major RDBMS.
Now, if the number of grades is unknown or you want it to work even if you make changes to grade table (without changing the query itself) you can resort to dynamic query. But dynamic SQL is vendor specific. Here is an example of how you can do that in MySql
SELECT CONCAT (
'SELECT date, SUM(moulded) moulded,',
GROUP_CONCAT(DISTINCT
CONCAT('SUM(CASE WHEN grade = ',gradeid,
' THEN moulded ELSE 0 END) ', grade)),
' FROM moulded_quantity GROUP BY date') INTO #sql
FROM grade;
PREPARE stmt FROM #sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
Output (in both cases):
| DATE | MOULDED | CHOPSAW | CLASSIC | CHIEVE |
---------------------------------------------------
| 21st May | 450 | 150 | 300 | 0 |
| 22nd May | 300 | 150 | 150 | 0 |
Here is SQLFiddle demo (for both approaches).
UPDATE In Sql Server you can use STUFF and PIVOT to produce expected result with dynamic sql
DECLARE #colx NVARCHAR(MAX), #colp NVARCHAR(MAX), #sql NVARCHAR(MAX)
SET #colx = STUFF((SELECT ', ISNULL(' + QUOTENAME(Grade) + ',0) ' + QUOTENAME(Grade)
FROM grade
ORDER BY GradeID
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)'),1,1,'')
SET #colp = STUFF((SELECT DISTINCT ',' + QUOTENAME(Grade)
FROM grade
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)'),1,1,'')
SET #sql = 'SELECT date, total moulded, ' + #colx +
' FROM
(
SELECT date, g.grade gradename, moulded,
SUM(moulded) OVER (PARTITION BY date) total
FROM moulded_quantity q JOIN grade g
ON q.grade = g.gradeid
) x
PIVOT
(
SUM(moulded) FOR gradename IN (' + #colp + ')
) p
ORDER BY date'
EXECUTE(#sql)
Output is the same as in MySql case.
Here is SQLFiddle demo.
I would suggest you before asking any question research first as it is very common question.
Updated
DECLARE #COLUMNS varchar(max)
SELECT #COLUMNS = COALESCE(#COLUMNS+'],[' ,'') + CAST(Grade as varchar)
FROM Grade
GROUP BY Grade
SET #COLUMNS = '[' + #COLUMNS + ']'
DECLARE #COLUMNS_WITH_NULL varchar(max)
SELECT #COLUMNS_WITH_NULL = COALESCE(#COLUMNS_WITH_NULL+',ISNULL([' ,'ISNULL([') + CAST(Grade as varchar) + '], 0) AS ' + CAST(Grade as varchar)
FROM Grade
GROUP BY Grade
DECLARE #COLUMNS_SUMS varchar(max)
SELECT #COLUMNS_SUMS = COALESCE(#COLUMNS_SUMS+' + ISNULL([' ,'ISNULL([') + CAST(Grade as varchar) + '], 0) '
FROM Grade
GROUP BY Grade
SET #COLUMNS_SUMS = '(' + #COLUMNS_SUMS + ') as Moulded'
PRINT #COLUMNS_SUMS
EXECUTE (
'
SELECT
Date, ' + #COLUMNS_SUMS + ', ' + #COLUMNS_WITH_NULL + '
FROM (
SELECT
m.Moulded,
m.date AS Date,
g.Grade
FROM Grade g
INNER JOIN [Moulded Quantity] m
ON m.GRADE = g.GradeID
) up
PIVOT (SUM(Moulded) FOR Grade IN ('+ #COLUMNS +')) AS pvt')