SQL - self join 'n' times with condition - sql

I have the following table:
Id | Type
--------------------------------------------
C1C1A90D-B131-4450-B1BF-5041F36F9144 | 1
C7B1752D-FD30-445A-AD6C-51D1434607D3 | 2
3AAF8BB6-A6D4-4780-BEF9-ACBBF75A85DE | 3
--------------------------------------------
67EF1537-A22E-4D2D-AAEA-FC0D9E2B9912 | 1
546519ED-5E78-4DAD-ADFF-9DC0AA67B763 | 2
8F66A3F9-C652-4758-8E17-B4DE0B0D85A4 | 3
--------------------------------------------
-- ... and so on ... --
Now, I need a specific type of SELECT (something like this):
SELECT
[Id] AS [OneId] -- Where [Type] = 1,
[Id] AS [TwoId] -- Where [Type] = 2,
[Id] AS [ThreeId] -- Where [Type] = 3
FROM Table
This is what I've tried so far but with bad results:
SELECT
oneI.[Id] AS [OneId] -- Where [Type] = 1,
twoI.[Id] AS [TwoId] -- Where [Type] = 2,
threeI.[Id] AS [ThreeId] -- Where [Type] = 3
FROM Table AS i
INNER JOIN Table AS oneI ON
i.[Id] = oneI.[Id]
INNER JOIN Table AS twoI ON
i.[Id] = twoI.[Id]
INNER JOIN Table AS threeI ON
i.[Id] = threeI.[Id]
WHERE
oneI.[Type] = 1
AND twoI.[Type] = 2
AND threeI.[Type] = 3
Or even worse (which gave me more lots of combinations):
SELECT
oneI.[Id] AS [OneId] -- Where [Type] = 1,
twoI.[Id] AS [TwoId] -- Where [Type] = 2,
threeI.[Id] AS [ThreeId] -- Where [Type] = 3
FROM Table AS i, TABLE AS oneI, Table AS twoI, Table AS threeI
WHERE
oneI.[Type] = 1
AND twoI.[Type] = 2
AND threeI.[Type] = 3
EDIT:
I would also have another column on which I would group those Ids. So the updated table look like:
Id | GroupId | Type
------------------------------------------------------------------
C1C1A90D-B131-4450-B1BF-5041F36F9144 -- OneId | 1 | 1
C7B1752D-FD30-445A-AD6C-51D1434607D3 -- TwoId | 1 | 2
3AAF8BB6-A6D4-4780-BEF9-ACBBF75A85DE -- ThreeId | 1 | 3
------------------------------------------------------------------
67EF1537-A22E-4D2D-AAEA-FC0D9E2B9912 -- OneId | 2 | 1
546519ED-5E78-4DAD-ADFF-9DC0AA67B763 -- TwoId | 2 | 2
8F66A3F9-C652-4758-8E17-B4DE0B0D85A4 -- ThreeId | 2 | 3
----------------------------------------------------------------
So, I would need the following result:
OneI | TwoI | ThreeI | GroupId
------------------------------------------------------
OneId | TwoId | ThreeId | 1
OneId | TwoId | ThreeId | 2
EDIT:
I have one more special case - [Type] column can repeat:
Id | GroupId | Type
-----------------------------------------------------------------------
C1C1A90D-B131-4450-B1BF-5041F36F9144 -- OneId | 1 | 1
C7B1752D-FD30-445A-AD6C-51D1434607D3 -- TwoId | 1 | 2
3AAF8BB6-A6D4-4780-BEF9-ACBBF75A85DE -- ThreeId | 1 | 3
FEB4A345-FEA0-4530-AE52-6CF4F07E37BA -- OtherThreeId | 1 | 3
-----------------------------------------------------------------------
67EF1537-A22E-4D2D-AAEA-FC0D9E2B9912 -- OneId | 2 | 1
546519ED-5E78-4DAD-ADFF-9DC0AA67B763 -- TwoId | 2 | 2
8F66A3F9-C652-4758-8E17-B4DE0B0D85A4 -- ThreeId | 2 | 3
----------------------------------------------------------------
And now the result would be:
OneI | TwoI | ThreeI | GroupId
------------------------------------------------------
OneId | TwoId | ThreeId | 1
OneId | TwoId | OtherThreeId | 1
OneId | TwoId | ThreeId | 2
sqlfiddle

Well, this would required some sequential ordering columns, but you could also express this as
select max(case when [Type] = 1 then Id end) OneId,
max(case when [Type] = 2 then Id end) TwoId,
max(case when [Type] = 3 then Id end) ThreeId
from (select *,
row_number() over (order by (select 1)) Seq
from table
) t
group by (Seq - [Type]);
EDIT :- However, if you want to include group also then use them as in select statement as
select (Seq - [Type]) as GroupId,
max(case when [Type] = 1 then 'OneId' end) OneI,
max(case when [Type] = 2 then 'TwoId' end) TwoI,
max(case when [Type] = 3 then 'ThreeId' end) ThreeI
from (select *,
row_number() over (order by (select 1)) Seq
from table
) t
group by (Seq - [Type]);
For your updated table you can directly use table with group by clause with your GroupId column as then you don't use row_number() function and subquery
select max(case when [Type] = 1 then 'OneId' end) OneI,
max(case when [Type] = 2 then 'TwoId' end) TwoI,
max(case when [Type] = 3 then 'ThreeId' end) ThreeI,
GroupId
from table t
group by GroupId;
Demo

You need an Grp attribute then specifies which rows are together in one group. Something like this
Id | Type | Grp
----------------------------------------------------
C1C1A90D-B131-4450-B1BF-5041F36F9144 | 1 | 1
C7B1752D-FD30-445A-AD6C-51D1434607D3 | 2 | 1
3AAF8BB6-A6D4-4780-BEF9-ACBBF75A85DE | 3 | 1
---------------------------------------------------
67EF1537-A22E-4D2D-AAEA-FC0D9E2B9912 | 1 | 2
546519ED-5E78-4DAD-ADFF-9DC0AA67B763 | 2 | 2
8F66A3F9-C652-4758-8E17-B4DE0B0D85A4 | 3 | 2
--------------------------------------------------
Then you can use conditional aggregation like this
SELECT
MAX(CASE WHEN [Type] = 1 THEN [Id] END) AS [OneId],
MAX(CASE WHEN [Type] = 2 THEN [Id] END) AS [TwoId],
MAX(CASE WHEN [Type] = 3 THEN [Id] END) AS [ThreeId]
FROM YourTable
GROUP BY Grp

Looks like this would do the job for you
; WITH CTE
AS
(
SELECT
Id,
[Type]
FROM YourTable
WHERE [Type]
IN
(
1,2,3,4,5
)
)
SELECT
*
FROM CTE
PIVOT
(
MAX(id)
FOR [Type] IN
(
[1],[2],[3],[4],[5]
)
)Pvt

Related

Aggregation for multiple SQL SELECT statements

I've got a table TABLE1 like this:
|--------------|--------------|--------------|
| POS | TYPE | VOLUME |
|--------------|--------------|--------------|
| 1 | A | 34 |
| 2 | A | 2 |
| 1 | A | 12 |
| 3 | B | 200 |
| 4 | C | 1 |
|--------------|--------------|--------------|
I want to get something like this (TABLE2):
|--------------|--------------|--------------|--------------|--------------|
| POS | Amount_A | Amount_B | Amount_C | Sum_Volume |
|--------------|--------------|--------------|--------------|--------------|
| 1 | 2 | 0 | 0 | 46 |
| 2 | 1 | 0 | 0 | 2 |
| 3 | 0 | 1 | 0 | 200 |
| 4 | 0 | 0 | 1 | 1 |
|--------------|--------------|--------------|--------------|--------------|
My Code so far is:
SELECT
(SELECT COUNT(TYPE)
FROM TABLE1
WHERE TYPE = 'A') AS [Amount_A]
,(SELECT COUNT(TYPE)
FROM TABLE1
WHERE TYPE = 'B') AS [Amount_B]
,(SELECT COUNT(TYPE)
FROM TABLE1
WHERE TYPE = 'C') AS [Amount_C]
,(SELECT SUM(VOLUME)
FROM TABLE AS [Sum_Volume]
INTO [TABLE2]
Now two Questions:
How can I include the distinction concerning POS?
Is there any better way to count each TYPE?
I am using MSSQLServer.
What you're looking for is to use GROUP BY, along with your Aggregate functions. So, this results in:
USE Sandbox;
GO
CREATE TABLE Table1 (Pos tinyint, [Type] char(1), Volume smallint);
INSERT INTO Table1
VALUES (1,'A',34 ),
(2,'A',2 ),
(1,'A',12 ),
(3,'B',200),
(4,'C',1 );
GO
SELECT Pos,
COUNT(CASE WHEN [Type] = 'A' THEN [Type] END) AS Amount_A,
COUNT(CASE WHEN [Type] = 'B' THEN [Type] END) AS Amount_B,
COUNT(CASE WHEN [Type] = 'C' THEN [Type] END) AS Amount_C,
SUM(Volume) As Sum_Volume
FROM Table1 T1
GROUP BY Pos;
DROP TABLE Table1;
GO
if you have a variable, and undefined, number of values for [Type], then you're most likely going to need to use Dynamic SQL.
your first column should be POS, and you'll GROUP BY POS.
This will give you one row for each POS value, and aggregate (COUNT and SUM) accordingly.
You can also use CASE statements instead of subselects. For instance, instead of:
(SELECT COUNT(TYPE)
FROM TABLE1
WHERE TYPE = 'A') AS [Amount_A]
use:
COUNT(CASE WHEN TYPE = 'A' then 1 else NULL END) AS [Amount_A]

How to display data horizontally in SQL Server without using pivot and unpivot?

Is there any way to display Horizontal data in sql without using pivot and unpivot method ? Below image is a sample of using pivot and unpivot but I want to know is there any other method of doing it ?
Example
Original Data
+----+-------+-----------------+
| id | Place | Location |
+----+-------+-----------------+
| 1 | CP01 | 3.1415,101.7231 |
| 2 | CP02 | 3.2314,101.3254 |
| 3 | CP03 | 3.9415,101.0192 |
| 4 | CP04 | 3.5490,102.0435 |
| 5 | CP05 | 3.2562,101.2597 |
| 6 | CP06 | 3.1134,102.5915 |
+----+-------+-----------------+
Horizontal Data
+----------+-----------------+-----------------+-----------------+-----+
| Cols | 1 | 2 | 3 | ... |
+----------+-----------------+-----------------+-----------------+-----+
| Location | 3.1415,101.7231 | 3.2314,101.3254 | 3.9415,101.0192 | ... |
| Place | CP01 | CP02 | CP03 | ... |
+----------+-----------------+-----------------+-----------------+-----+
Image for clarification
You may looking for something like this:
DECLARE #GeoData TABLE (Id INT, Place VARCHAR(10), [Location] VARCHAR(100))
INSERT INTO #GeoData
SELECT 1,'CP01','3.1415,101.7231'
UNION ALL
SELECT 2,'CP02','3.2314,101.3254'
SELECT 'Location' AS Cols,
MAX(CASE WHEN t.Id = 1 THEN t.Location END) AS [1],
MAX(CASE WHEN t.Id = 2 THEN t.Location END) AS [2],
MAX(CASE WHEN t.Id = 2 THEN t.Location END) AS [3]
FROM #GeoData t
UNION ALL
SELECT 'Place' AS Cols,
MAX(CASE WHEN t.Id = 1 THEN t.Place END) AS [1],
MAX(CASE WHEN t.Id = 2 THEN t.Place END) AS [2],
MAX(CASE WHEN t.Id = 2 THEN t.Place END) AS [3]
FROM #GeoData t
The similar way is you could use of cross apply with some conditional aggregation in order to achieve pivot and unpivot way
SELECT a.Col1, MAX(case when a.ID = 1 then a.Value end) [1],
MAX(case when a.ID = 2 then a.Value end) [2],
MAX(case when a.ID = 3 then a.Value end) [3]
FROM table T
CROSS APPLY (VALUES ('Place', T.place, T.ID),
('Location', T.Location, T.ID)
) as a(Col1, Value, ID)
group by a.Col1

Conditional Group By in SQL

I have the following table that I want to group by type. When there are multiple rows with the same type (e.g., A & B type), I want to preserve the 'value' from the row with the highest rank (i.e., primary > secondary > tertiary..)
rowid | type | rank | value
1 | A | primary | 1
2 | A | secondary | 2
3 | B | secondary | 3
4 | B | tertiary | 4
5 | C | primary | 5
So the resulting table should look like
rowid | type | rank | value
1 | A | primary | 1
3 | B | secondary | 3
5 | C | primary | 5
Any suggestions will be highly appreciated!
p.s., I'm working in MS SQL Server.
You can use row_number(). Here is a simple'ish method:
select t.*
from (select t.*,
row_number() over (partition by type
order by charindex(rank, 'primary,secondary,tertiary')
) as seqnum
from t
) t
where seqnum = 1;
This uses charindex() as a simple method of ordering the ranks.
try this,
;WITH CTE
AS (
SELECT *
,row_number() OVER (
PARTITION BY [type] ORDER BY value
) rn
FROM #t
)
SELECT *
FROM cte
WHERE rn = 1
Another way of doing is with Row_Number and an Order By specifying your rule with CASE.
Schema:
CREATE TABLE #TAB(rowid INT, [type] VARCHAR(1), rankS VARCHAR(50) , value INT)
INSERT INTO #TAB
SELECT 1 , 'A' , 'primary' , 1
UNION ALL
SELECT 2 , 'A' , 'secondary', 2
UNION ALL
SELECT 3 , 'B' , 'secondary' , 3
UNION ALL
SELECT 4 , 'B' , 'tertiary' , 4
UNION ALL
SELECT 5 , 'C' , 'primary' , 5
Now apply rank rule with Row_Number
SELECT * FROM (
SELECT ROW_NUMBER() OVER(PARTITION BY [type] ORDER BY (CASE rankS
WHEN 'primary' THEN 1
WHEN 'secondary' THEN 2
WHEN 'tertiary' THEN 3 END )) AS SNO, * FROM #TAB
)A
WHERE SNO =1
Result:
+-----+-------+------+-----------+-------+
| SNO | rowid | type | rankS | value |
+-----+-------+------+-----------+-------+
| 1 | 1 | A | primary | 1 |
| 1 | 3 | B | secondary | 3 |
| 1 | 5 | C | primary | 5 |
+-----+-------+------+-----------+-------+

Count each condition within group

For every unique GroupId I would like to get a count of each IsGreen, IsRound, IsLoud condition and a total number of rows.
Sample data:
-----------------------------------------------------
id | ItemId | GroupId | IsGreen | IsRound | IsLoud
----+--------+---------+---------+---------+---------
1 | 1001 | 1 | 0 | 1 | 1
2 | 1002 | 1 | 1 | 1 | 0
3 | 1003 | 2 | 0 | 0 | 0
4 | 1004 | 2 | 1 | 0 | 1
5 | 1005 | 2 | 0 | 0 | 0
6 | 1006 | 3 | 0 | 0 | 0
7 | 1007 | 3 | 0 | 0 | 0
Desired result:
----------------------------------------------------------
GroupId | TotalRows | TotalGreen | TotalRound | TotalLoud
--------+-----------+------------+------------+-----------
1 | 2 | 1 | 2 | 1
2 | 3 | 1 | 0 | 1
3 | 2 | 0 | 0 | 0
I'm using the following code to create the table, the problem I'm having is that if any of the groups have no rows that match one of the conditions that group does not appear in the final table. What is the best way to accomplish what I want to do?
SELECT total.GroupId
, total.[Count] AS TotalRows
, IsGreen.[Count] AS TotalGreen
, IsRound.[Count] AS TotalRound
, IsLoud.[Count] AS TotalLoud
FROM (
SELECT GroupId
, count(*) AS [Count]
FROM TestData
GROUP BY GroupId
) TotalRows
INNER JOIN (
SELECT GroupId
, count(*) AS [Count]
FROM TestData
WHERE IsGreen = 1
GROUP BY GroupId
) IsGreen ON IsGreen.GroupId = TotalRows.GroupId
INNER JOIN (
SELECT GroupId
, count(*) AS [Count]
FROM TestData
WHERE IsRound = 1
GROUP BY GroupId
) IsRound ON IsRound.GroupId = TotalRows.GroupId
INNER JOIN (
SELECT GroupId
, count(*) AS [Count]
FROM TestData
WHERE IsLoud = 1
GROUP BY GroupId
) IsLoud ON IsLoud.GroupId = TotalRows.GroupId
You can use count to count rows per each [GroupId] and sum to count each property .
select [GroupId]
, count([GroupId]) as [TotalRows]
, sum([IsGreen]) as [TotalGreen]
, sum([IsRound]) as [TotalRound]
, sum([IsLoud]) as [TotalLoud]
from [TestData]
group by [GroupId]
Use conditional Aggregate. Try this.
SELECT GroupId,
Count(GroupId) TotalRows,
Count(CASE WHEN IsGreen = 1 THEN 1 END) TotalGreen,
Count(CASE WHEN IsRound = 1 THEN 1 END) TotalRound,
Count(CASE WHEN IsLoud = 1 THEN 1 END) TotalLoud
FROM tablename
GROUP BY GroupId

Pivot multiple rows in SQL Sever

I have data like below:
CREATE TABLE #Temp
(RuleName VARCHAR(100),
ParamValue VARCHAR(100),
ParamName VARCHAR(100))
INSERT INTO #Temp
VALUES ('BroadBandAviva','BroadBandAviva1','Rule Name'),
('BroadBandAviva','BroadBand','Expense Category name'),
('BroadBandAviva','DSF','org unit name'),
('BroadBandAviva','ASSISTANT SALES MANAGER','designation'),
('BroadBandAviva','Category A','cityclassification'),
('BroadBandAviva','2000','Eligible Amount'),
('BroadBandAviva','BroadBandAviva2','Rule Name'),
('BroadBandAviva','BroadBand','Expense Category name'),
('BroadBandAviva','DSF','org unit name'),
('BroadBandAviva','ASSISTANT SALES MANAGER','designation'),
('BroadBandAviva','ROI-Rest of India','cityclassification'),
('BroadBandAviva','2000','Eligible Amount')
and I want the output like this:
RuleName Col1 Col2 Col3 Col4 Col5 Col6
---------------------------------------------------------------------------------
BroadBandAviva Rule Name Expense Category name org unit name designation cityclassification Eligible Amount
BroadBandAviva BroadBandAviva1 BroadBand DSF ASSISTANT SALES MANAGER Category A 2000
BroadBandAviva BroadBandAviva2 BroadBand DSF ASSISTANT SALES MANAGER ROI-Rest of India 2000
Basically the first column should stay as it is, third column should become the first row and the second column should come in second row onwards. I tried with pivot function but I could not get the desired output.
Here is an alternative approach which places the values into separate columns, like this:
| RULENAME | GRPNO | COL1 | COL2 | COL3 | COL4 | COL5 | COL6 |
|----------------|-------|-----------------|-----------------------|---------------|-------------------------|---------------------|-----------------|
| BroadBandAviva | -1 | Rule Name | Expense Category name | org unit name | designation | cityclassification | Eligible Amount |
| BroadBandAviva | 0 | BroadBandAviva1 | BroadBand | DSF | ASSISTANT SALES MANAGER | Category A | 2000 |
| BroadBandAviva | 1 | BroadBandAviva2 | BroadBand | DSF | ASSISTANT SALES MANAGER | ROI-Rest of India | 2000 |
Like my previous attempt I use a CTE to retain some needed calculations that are reused in a subsequent union. Unlike the previous attempt though I use conventional case expressions and group by to achieve the pivot. Here is the query.
declare #i int = (select count(distinct ParamName) from Temp)
;with cte as (
select
*
, row_number() over(order by (select 1)) as rn
, (row_number() over(order by (select 1))-1) / #i as grpno
, row_number() over(order by (select 1)) % #i as prow
from temp
)
select
rulename
, -1 as grpno
, max(case when prow = 1 then paramname end) as col1
, max(case when prow = 2 then paramname end) as col2
, max(case when prow = 3 then paramname end) as col3
, max(case when prow = 4 then paramname end) as col4
, max(case when prow = 5 then paramname end) as col5
, max(case when prow = 0 then paramname end) as col6
from cte
WHERE rn <= #i
group by
rulename
union all
select
rulename
, grpno
, max(case when prow = 1 then paramvalue end) as col1
, max(case when prow = 2 then paramvalue end) as col2
, max(case when prow = 3 then paramvalue end) as col3
, max(case when prow = 4 then paramvalue end) as col4
, max(case when prow = 5 then paramvalue end) as col5
, max(case when prow = 0 then paramvalue end) as col6
from cte
group by
rulename
, grpno
;
To follow the logic it is useful to understand the columns PROW (parameter row) and GRPNO (grouping number), here is the CTE contents showing those columns:
/*
| RULENAME | PARAMVALUE | PARAMNAME | RN | GRPNO | PROW |
|----------------|-------------------------|-----------------------|----|-------|------|
| BroadBandAviva | BroadBandAviva1 | Rule Name | 1 | 0 | 1 |
| BroadBandAviva | BroadBand | Expense Category name | 2 | 0 | 2 |
| BroadBandAviva | DSF | org unit name | 3 | 0 | 3 |
| BroadBandAviva | ASSISTANT SALES MANAGER | designation | 4 | 0 | 4 |
| BroadBandAviva | Category A | cityclassification | 5 | 0 | 5 |
| BroadBandAviva | 2000 | Eligible Amount | 6 | 0 | 0 |
| BroadBandAviva | BroadBandAviva2 | Rule Name | 7 | 1 | 1 |
| BroadBandAviva | BroadBand | Expense Category name | 8 | 1 | 2 |
| BroadBandAviva | DSF | org unit name | 9 | 1 | 3 |
| BroadBandAviva | ASSISTANT SALES MANAGER | designation | 10 | 1 | 4 |
| BroadBandAviva | ROI-Rest of India | cityclassification | 11 | 1 | 5 |
| BroadBandAviva | 2000 | Eligible Amount | 12 | 1 | 0 |
*/
see the SQLFiddle Demo
This is as close as I can get, maybe others can use it? note the 2000 below is in the wrong place and I cannot see why or how.
| RULENAME | PARAMS |
|----------------|----------------------------------------------------------------------------------------------------|
| BroadBandAviva | Rule Name, Expense Category name, org unit name, designation, cityclassification, Eligible Amount |
| BroadBandAviva | BroadBandAviva1, BroadBand, DSF, ASSISTANT SALES MANAGER, Category A |
| BroadBandAviva | 2000, BroadBandAviva2, BroadBand, DSF, ASSISTANT SALES MANAGER, ROI-Rest of India |
^^^^^
Here's the query I used:
declare #i int = (select count(distinct ParamName) from Temp)
;with cte as (
select
*
, row_number() over(order by (select 1)) as rn
, row_number() over(order by (select 1)) / #i as grpno
, row_number() over(order by (select 1)) % #i as prow
from temp
)
SELECT
(select RuleName from cte where rn = 1) as RULENAME
, STUFF((
SELECT
', ' + c2.ParamName
FROM CTE as c2
WHERE rn <= #i
order by c2.rn
FOR XML PATH ('')
)
, 1, 1, '') as PARAMS
--, -1, -1, -1
UNION ALL
select
cte.RuleName
, caxml.params
--, cte.grpno, prow, rn
from cte
CROSS APPLY (
SELECT
STUFF((
SELECT
', ' + c2.ParamValue
FROM CTE as c2
WHERE c2.grpno = cte.grpno
order by case when c2.prow = #i then #i else c2.prow end
FOR XML PATH ('')
)
, 1, 1, '')
) caxml (params)
where prow = 1
;
see SQLFiddle Demo for more