USER_ID COLUMN1 COLUMN2
JOHN 24 CA
JOHN 24 LA
JOHN 63 CA
JOHN 63 LA
JOHN 66 CA
JOHN 66 LA
JOHN 9 AF
JOHN 9 AL
JOHN 9 AW
JOHN 9 DF
Required output:
USER_ID RESULT
JOHN 24~CA-LA + 63~CA-LA + 66~CA-LA + 9~AF-AL-AW-DF
This is my requirement. I am trying listagg():
select USER_ID,
(listagg(case when seqnum_p = 1 then COLUMN1 end, '-') within group (order by COLUMN1) ||
'~' ||
listagg(case when seqnum_b = 1 then COLUMN2 end, '-') within group (order by COLUMN2)
) as result
from (select TABLE.*,
row_number() over (partition by USER_ID, COLUMN1 order by COLUMN1) as seqnum_p,
row_number() over (partition by USER_ID, COLUMN2 order by COLUMN2) as seqnum_b
from TABLE
)
group by USER_ID;
Current output:
JOHN || AF-AL-AW-CA-DF-LA~24-63-66-9
You can do two levels of aggregation instead of dealing with the row numbers:
select user_id,
listagg(tmp, ' + ') within group (order by tmp) as result
from (
select user_id,
column1 ||'~'|| listagg(column2, '-') within group (order by column2) as tmp
from your_table
group by user_id, column1
)
group by user_id
order by user_id;
USER RESULT
---- --------------------------------------------------
JOHN 24~CA-LA + 63~CA-LA + 66~CA-LA + 9~AF-AL-AW-DF
The inner query gives you the first level:
USER TMP
---- --------------------------------------------------
JOHN 9~AF-AL-AW-DF
JOHN 24~CA-LA
JOHN 63~CA-LA
JOHN 66~CA-LA
and the outer level further aggregates those into a single string per user.
The order-by in the outer query aggregation is of a string starting with a number, which puts '9~...' after '24~...', which would normally be odd but seems to be what you expect.
If you actually wanted them in numerical column-1-order you can include that in the subquery and use it for ordering:
select user_id,
listagg(tmp, ' + ') within group (order by column1) as result
from (
select user_id, column1,
column1 ||'~'|| listagg(column2, '-') within group (order by column2) as tmp
from your_table
group by user_id, column1
)
group by user_id
order by user_id;
USER RESULT
---- --------------------------------------------------
JOHN 9~AF-AL-AW-DF + 24~CA-LA + 63~CA-LA + 66~CA-LA
Related
I have a table Contacts that basically looks like following:
Id | Name | ContactId | Contact | Amount
---------------------------------------------
1 | A | 1 | 12323432 | 555
---------------------------------------------
1 | A | 2 | 23432434 | 349
---------------------------------------------
2 | B | 3 | 98867665 | 297
--------------------------------------------
2 | B | 4 | 88867662 | 142
--------------------------------------------
2 | B | 5 | null | 698
--------------------------------------------
Here, ContactId is unique throughout the table. Contact can be NULL & I would like to exclude those.
Now, I want to select top 5 contacts for each Id based on their Amount. I am accomplished that by following query:
WITH cte AS (
SELECT id, Contact, amount, ROW_NUMBER()
over (
PARTITION BY id
order by amount desc
) AS RowNo
FROM contacts
where contact is not null
)
select *from cte where RowNo <= 5
It's working fine upto this point. Now I want to concate these (<=5) record for each group & show them in a single row by concatenating them.
Expected Result :
Id | Name | Contact
-------------------------------
1 | A | 12323432;23432434
-------------------------------
2 | B | 98867665;88867662
I am using following query to achieve this but it still gives all records in separate rows and also including Null values too:
WITH cte AS (
SELECT id, Contact, amount,contactid, ROW_NUMBER()
over (
PARTITION BY id
order by amount desc
) AS RowNo
FROM contacts
where contact is not null
)
select *from id, name,
STUFF ((
SELECT distinct '; ' + isnull(contact,'') FROM cte
WHERE co.id= cte.id and co.contactid= cte.contactid
and RowNo <= 5
FOR XML PATH('')),1, 1, '')as contact
from contacts co inner join cte where cte.id = co.id and co.contactid= cte.contactid
Above query still gives me all top 5 contacts in diff rows & including null too.
Is it a good idea to use CTE and STUFF togather? Please suggest if there is any better approach than this.
I got the problem with my final query:
I don't need original Contact table in my final Select, since I already have everything I needed in CTE. Also, Inside STUFF(), I'm using contactid to join which is what actually I'm trying to concat here. Since I'm using that condition for join, I am getting records in diff rows. I've removed these 2 condition and it worked.
WITH cte AS (
SELECT id, Contact, amount,contactid, ROW_NUMBER()
over (
PARTITION BY id
order by amount desc
) AS RowNo
FROM contacts
where contact is not null
)
select *from id, name,
STUFF ((
SELECT distinct '; ' + isnull(contact,'') FROM cte
WHERE co.id= cte.id
and RowNo <= 5
FOR XML PATH('')),1, 1, '')as contact
from cte where rowno <= 5
You can use conditional aggregation:
id, name, contact,
select id, name,
concat(max(case when seqnum = 1 then contact + ';' end),
max(case when seqnum = 2 then contact + ';' end),
max(case when seqnum = 3 then contact + ';' end),
max(case when seqnum = 4 then contact + ';' end),
max(case when seqnum = 5 then contact + ';' end)
) as contacts
from (select c.*
row_number() over (partition by id order by amount desc) as seqnum
from contacts c
where contact is not null
) c
group by id, name;
If you are running SQL Server 2017 or higher, you can use string_agg(): as most other aggregate functions, it ignores null values by design.
select id, name, string_agg(contact, ',') within group (order by rn) all_contacts
from (
select id, name, contact
row_number() over (partition by id order by amount desc) as rn
from contacts
where contact is not null
) t
where rn <= 5
group by id, name
Note that you don't strictly need a CTE here; you can return the columns you need from the subquery, and use them directly in the outer query.
In earlier versions, one approach using stuff() and for xml path is:
with cte as (
select id, name, contact,
row_number() over (partition by id order by amount desc) as rn
from contacts
where contact is not null
)
select id, name,
stuff(
(
select ', ' + c1.concat
from cte c1
where c1.id = c.id and c1.rn <= 5
order by c1.rn
for xml path (''), type
).value('.', 'varchar(max)'), 1, 2, ''
) all_contacts
from cte
group by id, name
I agree with #GMB. STRING_AGG() is what you need ...
WITH
contacts(Id,nm,ContactId,Contact,Amount) AS (
SELECT 1,'A',1,12323432,555
UNION ALL SELECT 1,'A',2,23432434,349
UNION ALL SELECT 2,'B',3,98867665,297
UNION ALL SELECT 2,'B',4,88867662,142
UNION ALL SELECT 2,'B',5,NULL ,698
)
,
with_filter_val AS (
SELECT
*
, ROW_NUMBER() OVER(PARTITION BY id ORDER BY amount DESC) AS rn
FROM contacts
)
SELECT
id
, nm
, STRING_AGG(CAST(contact AS CHAR(8)),',') AS contact_list
FROM with_filter_val
WHERE rn <=5
GROUP BY
id
, nm
-- out id | nm | contact_list
-- out ----+----+-------------------
-- out 1 | A | 12323432,23432434
-- out 2 | B | 98867665,88867662
I want to create a SQL query that SELECT a ID column and adds an extra column to the query which is a group number as shown in the output below.
Each group consists of 3 rows and should have the MIN(ID) as a GroupID for each group. The order by should be ASC on the ID column.
ID GroupNr
------------
100 100
101 100
102 100
103 103
104 103
105 103
106 106
107 106
108 106
I've tried solutions with ROW_NUMBER() and DENSE_RANK(). And also this query:
SELECT
*, MIN(ID) OVER (ORDER BY ID ASC ROWS 2 PRECEDING) AS Groupnr
FROM
Table
ORDER BY
ID ASC
Use row_number() to enumerate the rows, arithmetic to assign the group and then take the minimum of the id:
SELECT t.*, MIN(ID) OVER (PARTITION BY grp) as groupnumber
FROM (SELECT t.*,
( (ROW_NUMBER() OVER (ORDER BY ID) - 1) / 3) as grp
FROM Table
) t
ORDER BY ID ASC;
It is possible to do this without a subquery, but the logic is rather messy:
select t.*,
(case when row_number() over (order by id) % 3 = 0
then lag(id, 2) over (order by id)
when row_number() over (order by id) % 3 = 2
then lag(id, 1) over (order by id)
else id
end) as groupnumber
from table t
order by id;
Assuming you want the lowest value in the group, and they are always groups of 3, rather than the NTILE (as Saravantn suggests, which splits the data into that many even(ish) groups), you could use a couple of window functions:
WITH Grps AS(
SELECT V.ID,
(ROW_NUMBER() OVER (ORDER BY V.ID) -1) / 3 AS Grp
FROM (VALUES(100),
(101),
(102),
(103),
(104),
(105),
(106),
(107),
(108))V(ID))
SELECT G.ID,
MIN(G.ID) OVER (PARTITION BY G.Grp) AS GroupNr
FROM Grps G;
SELECT T2.ID, T1.ID
FROM (
SELECT MIN(ID) AS ID, GroupNr
FROM
(
SELECT ID, ( Row_number()OVER(ORDER BY ID) - 1 ) / 3 + 1 AS GroupNr
FROM Table
) AS T1
GROUP BY GroupNr
) AS T1
INNER JOIN (
SELECT ID, ( Row_number()OVER(ORDER BY ID) - 1 ) / 3 + 1 AS GroupNr
FROM Table
) T2 ON T1.GroupNr = T2.GroupNr
I have a sequence of numbers that need to be rendered with a hyphen but not sure how best to do this from the SQL database selection.
The expected result:
Peter: 1,3-7,10,11,13
Andrew: 1-3
Paul: 1-3
An example of the data from the table (small selection):
NAME #
Peter 1
Andrew 1
Paul 1
Andrew 2
Paul 2
Peter 3
Andrew 3
Paul 3
Peter 4
Peter 5
Peter 6
Peter 7
This is part gaps-and-islands and part string aggregation. This identifies the groupings:
select name,
(case when min(number) = max(number)
then convert(varchar(max), min(num))
else concat(min(number), '-', max(number))
end) as range
from (select name, number,
row_number() over (partition by name order by number) as seqnum
from t
) t
group by name, (number - seqnum);
With this you can add an additional level of aggregation to get the final result:
select name,
string_agg(range, ',') within group (order by min(min_number)) as col
from (select name, min(number) as min_number,
(case when min(number) = max(number)
then convert(varchar(max), min(num))
else concat(min(number), '-', max(number))
end) as range
from (select name, number,
row_number() over (partition by name order by number) as seqnum
from t
) t
group by name, (number - seqnum)
) n
group by name;
I have this
ID | Name
----+-------
31 | Abby
24 | Bruce
44 | Carl
49 | Derek
55 | Eric
81 | Fred
I want to concatenate groups of N rows into a single row. For N = 3, this would give me this
ID | Name
----------+----------------
31,24,44 | Abby,Bruce,Carl
49,55,81 | Derek,Eric,Fred
I managed to generate a row to use GROUP BY and CONCAT on, but it only works in mysql...
SET #row_number = 0;
SELECT *, (#row_number:=#row_number + 1) AS r1, (#row_number - 1) DIV 3 AS r2 FROM table1
ID | Name | r1| r2
----+-------+---+---
31 | Abby | 1 | 0
24 | Bruce | 2 | 0
44 | Carl | 3 | 0
49 | Derek | 4 | 1
55 | Eric | 5 | 1
81 | Fred | 6 | 1
For clarification:
I want a vanilla-like SQL solution (So it will work in mysql, sybase, oracle and postgres)
I don't need any order, I just want to reconstitute the original table at some point
I don't have writing privileges on this base, only reading
I want to concatenate any columns type (by casting them to a string) and handle NULLs
It's ok if some groups are not exactly of size N (like the last one)
The standard SQL solution looks something like this:
select listagg(id, ',') within group (order by id) as ids,
listagg(name, ',') within group (order by id) as names
from (select t.*, row_number() over (order by id) as seqnum
from t
) t
group by cast( (seqnum - 1) / 3 as int);
I think this will work as-is in Oracle. In MySQL, you need to change listagg() to group_concat() (and be using MySQL 8+) and in Postgres, you need to change listagg() to string_agg().
And, you pretty much can't do this easily in Sybase.
Oh, wait, there is another way:
select concat( (case when seqnum % 3 = 1 then concat(id, ';') else '' end),
(case when seqnum % 3 = 2 then concat(id, ';') else '' end),
(case when seqnum % 3 = 0 then concat(id, ';') else '' end)
) as ids,
concat( (case when seqnum % 3 = 1 then concat(name, ';') else '' end),
(case when seqnum % 3 = 2 then concat(name, ';') else '' end),
(case when seqnum % 3 = 0 then concat(name, ';') else '' end)
) as name
from (select t.*, row_number() over (order by id) as seqnum
from t
) t
group by cast( (seqnum - 1) / 3 as int);
Of course, Sybase doesn't support concat(), so you have to use +. And this produces ; for the separator rather than ,, but it is pretty close.
Amazing script. But I suspect you left out an AS. So I made it like so:
select string_agg(t.[id], ',') as ids,
string_agg(t.[name], ',') as names
from
(select t.*, row_number() over (order by id) as seqnum
from [tablename]
) AS t
group by cast( (seqnum - 1) / 3 as int);
In my case, it is as such (though I could not get the 'within group (order by id)' to work at any way..... hmmmm)
Here is mine that works well, which is a list of emails to all my students, combined into a row for every 100 rows. The String_Agg limits it to 8000 character, sadly. Anyone knows any alternative to String_Agg for an SQL Server?
SELECT
string_agg(t.[Student Name], ';') as [All Names],
string_agg(t.[Student Email], ';') as [All Emails]
FROM
(
SELECT [Student Name], [Student Email], ROW_NUMBER() OVER (ORDER BY [Student Email]) AS RowNo
FROM [Mailing List For Courses] where [Prod Name]='Online Courses'
) AS t
group by cast( (RowNo - 1) / 100 as int);
Hope it helps <3
I've data in below format.
ID GRP VALUE
1 P_1 AA
1 P_2 BB
1 X_1 CC
1 X_2 DD
1 M_1 EE
1 M_2 FF
1 N_1 GG
1 N_2 HH
1 K_1 II
1 K_2 JJ
And I need output in below format
ID GRP PAIRS
1 P_1,P_2 AA,BB
1 X_1,X_2 CC,DD
1 M_1,M_2 EE,FF
1 N_1,N_2 GG,HH
1 K_1,K_2 II,JJ
Kindly suggest a sql for this in oracle
You can use LISTAGG:
SELECT
MAX(ID),
LISTAGG(GRP, ',') WITHIN GROUP (ORDER BY SUBSTR(GRP, 1, 1)) AS GRP,
LISTAGG(VALUE, ',') WITHIN GROUP (ORDER BY SUBSTR(GRP, 1, 1)) AS PAIRS
FROM YourTable
GROUP BY SUBSTR(GRP, 1, 1);
I am assuming you want the maximum ID for each group in first column of the output (you wrote all 1s in your example).
If you only have two values, you can use MIN() and MAX():
select id,
min(grp) || ',' || max(grp) as grp,
min(value) || ',' || max(value) as pairs
from t
group by id, substr(grp, 1, 1);