Condense or merge rows with null values not using group by - sql

Let's say I have a select which returns the following Data:
select nr, name, val_1, val_2, val_3
from table
Nr. | Name | Value 1 | Value 2 | Value 3
-----+------------+---------+---------+---------
1 | Max | 123 | NULL | NULL
1 | Max | NULL | 456 | NULL
1 | Max | NULL | NULL | 789
9 | Lisa | 1 | NULL | NULL
9 | Lisa | 3 | NULL | NULL
9 | Lisa | NULL | NULL | Hello
9 | Lisa | 9 | NULL | NULL
I'd like to condense the rows down to the bare minimum with.
I want the following result:
Nr. | Name | Value 1 | Value 2 | Value 3
-----+------------+---------+---------+---------
1 | Max | 123 | 456 | 789
9 | Lisa | 1 | NULL | Hello
9 | Lisa | 3 | NULL | NULL
9 | Lisa | 9 | NULL | NULL
For condensing the rows with Max (Nr. 1) a group by of the max values would help.
select nr, name, max(val_1), max(val_2), max(val_3)
from table
group by nr, name
But I am unsure how to get the desired results for Lisa (Nr. 9). The row for Lisa contains a value in the Value 3 column, in this example it's condensed with the first row that matches Nr and Name and has a Null value in Value 3.
I'm thankful for every input!

Basic principle is same as Vladimir's solution. This uses UNPIVOT and PIVOT
with cte as
(
select nr, name, col, val,
rn = row_number() over(partition by nr, name, col order by val)
from [table]
unpivot
(
val
for col in (val_1, val_2, val_3)
) u
)
select *
from (
select nr, name, rn, col, val
from cte
) d
pivot
(
max (val)
for col in ([val_1], [val_2], [val_3])
) p

Here is one way to do it. Assign a unique row number for each column by sorting them in such a way that NULLs come last and then join them back together using these row numbers and remove rows with all NULLs.
Run just the CTE first and examine the intermediate result to understand how it works.
Sample data
DECLARE #T TABLE (Nr varchar(10), Name varchar(10), V1 varchar(10), V2 varchar(10), V3 varchar(10));
INSERT INTO #T VALUES
('1', 'Max ', '123' , NULL , NULL ),
('1', 'Max ', NULL , '456', NULL ),
('1', 'Max ', NULL , NULL , '789'),
('9', 'Lisa', '1' , NULL , NULL ),
('9', 'Lisa', '3' , NULL , NULL ),
('9', 'Lisa', NULL , NULL , 'Hello'),
('9', 'Lisa', '9' , NULL , NULL );
Query
WITH CTE
AS
(
SELECT
Nr
,Name
,V1
,V2
,V3
-- here we use CASE WHEN V1 IS NULL THEN 1 ELSE 0 END to put NULLs last
,ROW_NUMBER() OVER (PARTITION BY Nr ORDER BY CASE WHEN V1 IS NULL THEN 1 ELSE 0 END, V1) AS rn1
,ROW_NUMBER() OVER (PARTITION BY Nr ORDER BY CASE WHEN V2 IS NULL THEN 1 ELSE 0 END, V2) AS rn2
,ROW_NUMBER() OVER (PARTITION BY Nr ORDER BY CASE WHEN V3 IS NULL THEN 1 ELSE 0 END, V3) AS rn3
FROM #T AS T
)
SELECT
T1.Nr
,T1.Name
,T1.V1
,T2.V2
,T3.V3
FROM
CTE AS T1
INNER JOIN CTE AS T2 ON T2.Nr = T1.Nr AND T2.rn2 = T1.rn1
INNER JOIN CTE AS T3 ON T3.Nr = T1.Nr AND T3.rn3 = T1.rn1
WHERE
T1.V1 IS NOT NULL
OR T2.V2 IS NOT NULL
OR T3.V3 IS NOT NULL
ORDER BY
T1.Nr, T1.rn1
;
Result
+----+------+-----+------+-------+
| Nr | Name | V1 | V2 | V3 |
+----+------+-----+------+-------+
| 1 | Max | 123 | 456 | 789 |
| 9 | Lisa | 1 | NULL | Hello |
| 9 | Lisa | 3 | NULL | NULL |
| 9 | Lisa | 9 | NULL | NULL |
+----+------+-----+------+-------+

Related

Select distinct one field other first non empty or null

I have table
| Id | val |
| --- | ---- |
| 1 | null |
| 1 | qwe1 |
| 1 | qwe2 |
| 2 | null |
| 2 | qwe4 |
| 3 | qwe5 |
| 4 | qew6 |
| 4 | qwe7 |
| 5 | null |
| 5 | null |
is there any easy way to select distinct 'id' values with first non null 'val' values. if not exist then null. for example
result should be
| Id | val |
| --- | ---- |
| 1 | qwe1 |
| 2 | qwe4 |
| 3 | qwe5 |
| 4 | qew6 |
| 5 | null |
In your case a simple GROUP BY should be the solution:
SELECT Id
,MIN(val)
FROM dbo.mytable
GROUP BY Id
Whenever using a GROUP BY, you have to use an aggregate function on all columns, which are not listed in the GROUP BY.
If an Id has a value (val) other than NULL, this value will be returned.
If there are just NULLs for the Id, NULL will be returned.
As far as i unterstood (regarding your comment), this is exactly what you're going to approach.
If you always want to have "the first" value <> NULL, you'll need another sort criteria (like a timestamp column) and might be able to solve it with a WINDOW-function.
If you want the first non-NULL value (where "first" is based on id), then MIN() doesn't quite do it. Window functions do:
select t.*
from (select t.*,
row_number() over (partition by id
order by (case when val is not null then 1 else 2 end),
id
) as seqnum
from t
) t
where seqnum = 1;
SQL Fiddle:
Create Table from SQL Fiddle:
CREATE TABLE tab1(pid integer, id integer, val varchar(25))
Insert dummy records :
insert into tab1
values (1, 1 , null),
(2, 1 , 'qwe1' ),
(3, 1 , 'qwe2'),
(4, 2 , null ),
(5, 2 , 'qwe4' ),
(6, 3 , 'qwe5' ),
(7, 4 , 'qew6' ),
(8, 4 , 'qwe7' ),
(9, 5 , null ),
(10, 5 , null );
fire below query:
SELECT Id ,MIN(val) as val FROM tab1 GROUP BY Id;

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 |
+-----+-------+------+-----------+-------+

Compare previus row to set a "StatusFlag"

I want to compare the previous row in my table on "ExtractTypeNum". So if it changed from the previous the flag should be set to "isChanged".
I have tried to develop this with a case statement but without any success.
select *
(case
when rownum = rownum-1
then
(case when
extractTypeNum <> extractTypeNum
then Null
else 'IsChanged' end)
when rownum = rownum -'1' then '3'
else '4' end) as StatusFlag
from myTable
This is the structure of the table and some sample data:
CREATE TABLE mytable(
ExtractTypeNum INTEGER NOT NULL PRIMARY KEY
,FileOrderNum VARCHAR(11) NOT NULL
,PrevFileOrderNum VARCHAR(11) NOT NULL
,NextFileOrderNum VARCHAR(11) NOT NULL
,rownum1 INTEGER NOT NULL
,Statusflag1 VARCHAR(9) NOT NULL
);
INSERT INTO mytable(ExtractTypeNum,FileOrderNum,PrevFileOrderNum,NextFileOrderNum,rownum1,Statusflag1)
VALUES (1,'2016-09-191',NULL,'2016-09-192',1,'IsInitial');
INSERT INTO mytable(ExtractTypeNum,FileOrderNum,PrevFileOrderNum,NextFileOrderNum,rownum1,Statusflag1)
VALUES (2,'2016-09-192','2016-09-191','2016-09-201',2,NULL);
INSERT INTO mytable(ExtractTypeNum,FileOrderNum,PrevFileOrderNum,NextFileOrderNum,rownum1,Statusflag1)
VALUES (1,'2016-09-201','2016-09-192','2016-09-211',3,NULL);
INSERT INTO mytable(ExtractTypeNum,FileOrderNum,PrevFileOrderNum,NextFileOrderNum,rownum1,Statusflag1)
VALUES (1,'2016-09-211','2016-09-201','2016-09-222',4,NULL);
INSERT INTO mytable(ExtractTypeNum,FileOrderNum,PrevFileOrderNum,NextFileOrderNum,rownum1,Statusflag1)
VALUES (2,'2016-09-222','2016-09-211',NULL,5,'IsLatest');
Expected output
+----------------+--------------+------------------+------------------+--------+-------------+
| ExtractTypeNum | FileOrderNum | PrevFileOrderNum | NextFileOrderNum | rownum | Statusflag1 |
+----------------+--------------+------------------+------------------+--------+-------------+
| 1 | 2016-09-191 | NULL | 2016-09-192 | | IsInitial |
| 2 | 2016-09-192 | 2016-09-191 | 2016-09-201 | | IsChanged |
| 1 | 2016-09-201 | 2016-09-192 | 2016-09-211 | | IsChanged |
| 1 | 2016-09-211 | 2016-09-201 | 2016-09-222 | | NULL |
| 2 | 2016-09-222 | 2016-09-211 | NULL | | IsLatest |
+----------------+--------------+------------------+------------------+--------+-------------+
If you are using SQL Server 2012 or later, then you can try the following query:
;WITH CTE AS (
SELECT ExtractTypeNum, FileOrderNum, PrevFileOrderNum,
NextFileOrderNum, rownum1, Statusflag1,
ROW_NUMBER() OVER (ORDER BY rownum1) AS rn,
COUNT(*) OVER () AS totalCnt,
LAG(ExtractTypeNum) OVER (ORDER BY rownum1) AS prevExtractTypeNum
FROM mytable
)
SELECT ExtractTypeNum, FileOrderNum, PrevFileOrderNum,
NextFileOrderNum, rownum1, Statusflag1,
CASE
WHEN rn = 1 THEN 'IsInitial'
WHEN rn = totalCnt THEN 'IsLatest'
WHEN prevExtractTypeNum <> ExtractTypeNum THEN 'IsChanged'
END AS StatusFlag
FROM CTE

Dynamically create ranges from numeric sequences

I have a table like the following:
+----+-----+-----+
| ID | GRP | NR |
+----+-----+-----+
| 1 | 1 | 101 |
| 2 | 1 | 102 |
| 3 | 1 | 103 |
| 4 | 1 | 105 |
| 5 | 1-2 | 106 |
| 6 | 1-2 | 109 |
| 7 | 1-2 | 110 |
| 8 | 2 | 201 |
| 9 | 2 | 202 |
| 10 | 3 | 300 |
| 11 | 3 | 350 |
| 12 | 3 | 351 |
| 13 | 3 | 352 |
+----+-----+-----+
I wanted to create a view which groups this list by GRP and concatenates values in NR.
Is it possible to dynamically detect sequences and shorten them into ranges?
Like 1, 2, 3, 5 would become 1-3, 5.
So the result should look like this:
+-----+--------------------+
| GRP | NRS |
+-----+--------------------+
| 1 | 101 - 103, 105 |
| 1-2 | 106, 109 - 110 |
| 2 | 201 - 202 |
| 3 | 300, 350 - 352 |
+-----+--------------------+
What i got now is simply concatenate values, so the table above would become this:
+-----+--------------------+
| GRP | NRS |
+-----+--------------------+
| 1 | 101, 102, 103, 105 |
| 1-2 | 106, 109, 110 |
| 2 | 201, 202 |
| 3 | 300, 350, 351, 352 |
+-----+--------------------+
Here's the actual statement:
DECLARE #T TABLE
(
ID INT IDENTITY(1, 1)
, GRP VARCHAR(10)
, NR INT
)
INSERT INTO #T
VALUES ('1',101),('1',102),('1',103),('1',105)
,('1-2',106),('1-2',109), ('1-2',110)
,('2',201),('2',202)
,('3',300),('3',350),('3',351),('3',352)
SELECT * FROM #T
;WITH GROUPNUMS (RN, GRP, NR, NRS) AS
(
SELECT 1, GRP, MIN(NR), CAST(MIN(NR) AS VARCHAR(MAX))
FROM #T
GROUP BY GRP
UNION ALL
SELECT CT.RN + 1, T.GRP, T.NR, CT.NRS + ', ' + CAST(T.NR AS VARCHAR(MAX))
FROM #T T
INNER JOIN GROUPNUMS CT ON CT.GRP = T.GRP
WHERE T.NR > CT.NR
)
SELECT NRS.GRP, NRS.NRS
FROM GROUPNUMS NRS
INNER JOIN (
SELECT GRP, MAX(RN) AS MRN
FROM GROUPNUMS
GROUP BY GRP
) R
ON NRS.RN = R.MRN AND NRS.GRP = R.GRP
ORDER BY NRS.GRP
Can anyone tell me if it's easily possible to do something like that?
Would be great if anyone has an idea and would like to share it.
SQLFiddle demo
with TRes
as
(
select T.GRP,T.NR NR,
CASE WHEN T1.NR IS NULL and T2.NR is null
THEN CAST(T.NR as VARCHAR(MAX))
WHEN T1.NR IS NULL and T2.NR IS NOT NULL
THEN '-'+CAST(T.NR as VARCHAR(MAX))
WHEN T1.NR IS NOT NULL and T2.NR IS NULL
THEN CAST(T.NR as VARCHAR(MAX))+'-'
END AS NR_GRP
from T
left join T T1 on T.Grp=T1.Grp and t.Nr+1=t1.Nr
left join T T2 on T.Grp=T2.Grp and t.Nr-1=t2.Nr
WHERE T1.NR IS NULL or T2.NR IS NULL
)
SELECT
GRP,
REPLACE(
substring((SELECT ( ',' + NR_GRP)
FROM TRes t2
WHERE t1.GRP = t2.GRP
ORDER BY
GRP,
NR
FOR XML PATH( '' )
), 2, 10000 )
,'-,-','-')
FROM TRes t1
GROUP BY GRP
Please check my try:
DECLARE #T TABLE
(
ID INT IDENTITY(1, 1)
, GRP VARCHAR(10)
, NR INT
)
INSERT INTO #T
VALUES ('1',101),('1',102),('1',103),('1',105)
,('1-2',106),('1-2',109), ('1-2',110)
,('2',201),('2',202)
,('3',300),('3',350),('3',351),('3',352)
SELECT * FROM #T
;WITH T1 as
(
SELECT GRP, NR, ROW_NUMBER() over(order by GRP, NR) ID FROM #T
)
,T as (
SELECT *, 1 CNT FROM T1 where ID=1
union all
SELECT b.*, (case when T.NR+1=b.NR and T.GRP=b.GRP then t.CNT
else T.CNT+1 end)
from T1 b INNER JOIN T on b.ID=T.ID+1
)
, TN as(
select *,
MIN(NR) over(partition by GRP, CNT) MinVal,
MAX(NR) over(partition by GRP, CNT) MaxVal
From T
)
SELECT GRP, STUFF(
(SELECT distinct ','+(CASE WHEN MinVal=MaxVal THEN CAST(MinVal as nvarchar(10)) ELSE CAST(MinVal as nvarchar(10))+'-'+cast(MaxVal as nvarchar(10)) END)
FROM TN b where b.GRP=a.GRP
FOR XML PATH(''),type).value('.','nvarchar(max)'),1,1,'') AS [ACCOUNT NAMES]
FROM TN a GROUP BY GRP

Querying data groups with total row before starting next group?

I need to query some data in the below format in SQL Server:
Id Group Price
1 A 10
2 A 20
Sum 30
1 B 6
2 B 4
Sum 10
1 C 100
2 C 200
Sum 300
I was thinking to do it in the follwoing steps:
Query one group
In other query do sum
Use Union operator to combine this result set
Do step 1-3 for all groups and finally return all sub sets of data using union.
Is there a better way to do this ? May be using some out of box feature ? Please advise.
Edit:
As per suggestions and code sample I tried this code:
Select
Case
when id is null then 'SUM'
else CAST(id as Varchar(10)) end as ID,
Case when [group] is null then 'ALL' else CAST([group] as Varchar(50)) end as [group]
,Price from
(
SELECT Id, [Group],BGAApplicationID,Section, SUM(PrimaryTotalArea) AS price
FROM vwFacilityDetails
where bgaapplicationid=1102
GROUP BY Id, [Group],BGAApplicationID,Section WITH ROLLUP
) a
And Even this code as well:
Select Id, [Group],BGAApplicationID,Section, SUM(PrimaryTotalArea) AS price
From vwFacilityDetails
Where Not ([group] Is Null And id Is Null And BGAApplicationId is null and section is null) and BGAApplicationId=1102
Group By Id, [Group],BGAApplicationID,Section
With Rollup
In results it groups up the data but for every record it shows it 3 times (in both above codes) like:
2879 Existing Facilities Whole School 25.00
2879 Existing Facilities Whole School 25.00
2879 Existing Facilities Whole School 25.00
2879 ALL 25.00
I guess there is some issue in my query, please guide me here as well.
Thanks
SQL Server introduced GROUPING SETS which is what you should be looking to use.
SQL Fiddle
MS SQL Server 2008 Schema Setup:
Create Table vwFacilityDetails (
id int not null,
[group] char(1) not null,
PrimaryTotalArea int not null,
Section int,
bgaapplicationid int
);
Insert Into vwFacilityDetails (id, [group], Section,bgaapplicationid,PrimaryTotalArea) values
(1, 'A', 1,1102,2),
(1, 'A', 1,1102,1),
(1, 'A', 1,1102,7),
(2, 'A', 1,1102,20),
(1, 'B', 1,1102,6),
(2, 'B', 1,1102,4),
(1, 'C', 1,1102,100),
(2, 'C', 1,1102,200);
Query 1:
SELECT CASE WHEN Id is null then 'SUM'
ELSE Right(Id,10) end Id,
[Group],BGAApplicationID,Section,
SUM(PrimaryTotalArea) price
FROM vwFacilityDetails
where bgaapplicationid=1102
GROUP BY GROUPING SETS (
(Id,[Group],BGAApplicationID,Section),
([Group])
)
ORDER BY [GROUP],
ID;
Results:
| ID | GROUP | BGAAPPLICATIONID | SECTION | PRICE |
----------------------------------------------------
| 1 | A | 1102 | 1 | 10 |
| 2 | A | 1102 | 1 | 20 |
| SUM | A | (null) | (null) | 30 |
| 1 | B | 1102 | 1 | 6 |
| 2 | B | 1102 | 1 | 4 |
| SUM | B | (null) | (null) | 10 |
| 1 | C | 1102 | 1 | 100 |
| 2 | C | 1102 | 1 | 200 |
| SUM | C | (null) | (null) | 300 |
Select
id,
[Group],
SUM(price) AS price
From
Test
Group By
[group],
id
With
Rollup
http://sqlfiddle.com/#!3/080cd/8
Select Case when id is null then 'SUM' else CAST(id as Varchar(10)) end as ID
,Case when [group] is null then 'ALL' else CAST([group] as Varchar(10)) end as [group]
,Price from
(
SELECT id, [group], SUM(price) AS Price
FROM IG
GROUP BY [GROUP],ID WITH ROLLUP
) a