Limit Number of elements in a string aggregation query - sql

I have below table and string aggregation using XML:
CREATE TABLE TestTable ([ID] INT, [Name] CHAR(1))
INSERT INTO TestTable ([ID],[Name]) VALUES (1,'A')
INSERT INTO TestTable ([ID],[Name]) VALUES (2,'B')
INSERT INTO TestTable ([ID],[Name]) VALUES (3,'C')
INSERT INTO TestTable ([ID],[Name]) VALUES (1,'D')
INSERT INTO TestTable ([ID],[Name]) VALUES (1,'E')
INSERT INTO TestTable ([ID],[Name]) VALUES (2,'F')
INSERT INTO TestTable ([ID],[Name]) VALUES (3,'G')
INSERT INTO TestTable ([ID],[Name]) VALUES (4,'H')
SELECT
[ID],
STUFF((
SELECT ' ' + [Name]
FROM TestTable
WHERE (ID = Results.ID)
FOR XML PATH(''),TYPE).value('(./text())[1]','VARCHAR(MAX)')
,1,2,'') AS Names
FROM TestTable Results
GROUP BY ID
I get below result
ID Names
1 A D E
2 B F
3 C G
4 H
However i have requirement to limit number of Names to two, if its more than two, it should split to next row, Something like below. Here for ID=1, there were 3 Names, so 3rd name split into next row. How can i achieve this requirement
ID Names
1 A D
1 E
2 B F
3 C G
4 H
Thanks

It sounds like you want conditional aggregation, with up to two name columns per row. You can do this using row_number() and some arithmetic:
select id,
max(case when seqnum % 2 = 1 then name end) as name1,
max(case when seqnum % 2 = 0 then name end) as name2
from (select t.*,
row_number() over (partition by id order by name) as seqnum
from testtable t
) t
group by id, ceiling(seqnum / 2.0)
order by id, min(seqnum);
Here is a db<>fiddle.
I should note that you can concatenate these into a single column. I don't see a reason to do so, because you know the maximum number on each row.

Related

Need to return an ID which has start and END in sql server

I have a scenario wherein I need to find the ID which only has start and END in it. Below is the table for reference.
Declare #T Table ( ID int, Name varchar(100))
Insert into #T values (1,'Start')
Insert into #T values (1,'END')
Insert into #T values (1,'Stuart')
Insert into #T values (1,'robin')
Insert into #T values (2,'Start')
Insert into #T values (2,'END')
Insert into #T values (3,'Start')
Insert into #T values (4,'END')
I want the Output as:
ID Name
2 Start
2 END
I want those ID which only has start and end in it.
What I tried so far:
SELECT * FROM #T t
WHERE EXISTS (SELECT * FROM #T WHERE id = t.id AND name = 'start')
AND EXISTS (SELECT * FROM #T WHERE id = t.id AND name = 'END')
But my query is giving ID 1 as well.
Can someone please help me rectify the problem.
I presume your issue is that record 1 has a 'Stuart' in it too?
As such, you can do a similar check in the WHERE e.g.,
SELECT * FROM #T t
WHERE EXISTS (SELECT * FROM #T WHERE id = t.id AND name = 'start')
AND EXISTS (SELECT * FROM #T WHERE id = t.id AND name = 'END')
AND NOT EXISTS (SELECT * FROM #T WHERE id = t.id AND name NOT IN ('start','END'))
Note that you may want to consider
What happens if you have two 'start' rows or two 'end' rows (e.g., start-start-end)? Can you even have two 'start' rows (e.g., start-start)?
What happens if you have a blank/NULL (e.g., start-NULL-end)?
EDIT: removed 'What happens if they're out of order (e.g., end-start)?' as a question as there is no sorting in the data at all (e.g., not even an implicit sort).
You can go for CTE to get group wise count and total count as 2.
Declare #T Table ( ID int, Name varchar(100))
Insert into #T values (1,'Start')
Insert into #T values (1,'END')
Insert into #T values (1,'Stuart')
Insert into #T values (1,'robin')
Insert into #T values (2,'Start')
Insert into #T values (2,'END')
Insert into #T values (3,'Start')
Insert into #T values (4,'END')
;WITH CTE_Total_StartEnd AS
(
select id, count(*) AS Total_Cnt
, COUNT( case when Name IN ('Start') THEN 1 END) as start_cnt
, COUNT( case when Name IN ('End') THEN 1 END) as end_cnt
from #t
group by id
having COUNT( case when Name IN ('Start') THEN 1 END) =1 and
COUNT( case when Name IN ('End') THEN 1 END) = 1 and
count(*) = 2
)
SELECT t.* from #t t
inner join CTE_Total_StartEnd as c
ON c.id = t.id
+----+-------+
| ID | Name |
+----+-------+
| 2 | Start |
| 2 | END |
+----+-------+
You can do this by using group by function also like below
WITH cte AS
(
SELECT 1 AS id , 'Start' AS name
UNION ALL
SELECT 1 AS id ,'END' AS name
UNION ALL
SELECT 1 AS id ,'Stuart' AS name
UNION ALL
SELECT 1 AS id ,'robin' AS name
UNION ALL
SELECT 2 AS id ,'Start' AS name
UNION ALL
SELECT 2 AS id ,'END' AS name
UNION ALL
SELECT 3 AS id ,'Start' AS name
UNION ALL
SELECT 4 AS id ,'END' AS name
)
SELECT T.ID,SUM(T.VAL)AS SUM
FROM
(
SELECT id,name , CASE WHEN name='Start' THEN 1
WHEN name='END' THEN 2
ELSE 3
END AS VAL
FROM cte
)T
GROUP BY T.ID
HAVING SUM(T.VAL) =3
could you please try this? Pls note i added collate command in the end of sql.
SQL Server check case-sensitivity?
SELECT * FROM #T t
WHERE EXISTS (SELECT * FROM #T WHERE id = t.id AND name = 'start' COLLATE SQL_Latin1_General_CP1_CS_AS)
AND EXISTS (SELECT * FROM #T WHERE id = t.id AND name = 'END' COLLATE SQL_Latin1_General_CP1_CS_AS)

Concat single column values without supporting column(ex:- ID) in SQL Server 2014

I have a single column
Value
A
B
C
NULL
NULL
F
R
D
NULL
T
R
NULL
Expected Output:
value
ABC
FRD
TR
Aim is to group column values until next NULL and so on..
CREATE TABLE DUMMY1(VALUE VARCHAR(50))
INSERT INTO DUMMY1(VALUE) VALUES('A')
INSERT INTO DUMMY1(VALUE) VALUES('B')
INSERT INTO DUMMY1(VALUE) VALUES('C')
INSERT INTO DUMMY1(VALUE) VALUES(NULL)
INSERT INTO DUMMY1(VALUE) VALUES(NULL)
INSERT INTO DUMMY1(VALUE) VALUES('F')
INSERT INTO DUMMY1(VALUE) VALUES('R')
INSERT INTO DUMMY1(VALUE) VALUES('D')
INSERT INTO DUMMY1(VALUE) VALUES(NULL)
INSERT INTO DUMMY1(VALUE) VALUES('T')
INSERT INTO DUMMY1(VALUE) VALUES('R')
INSERT INTO DUMMY1(VALUE) VALUES(NULL)
SQL tables represent unordered sets. Your question makes no sense, unless I assume that that rows are ordered. This requires a separate column for the ordering.
Let me assume that you have such a column, which I will call id. Then, you can use a cumulative count to assign the groups and then aggregate:
SELECT STRING_AGG(value, '') WITHIN GROUP (ORDER BY id)
FROM (SELECT d.*,
SUM(CASE WHEN VALUE IS NULL THEN 1 ELSE 0 END) OVER (ORDER BY id) as grp
FROM DUMMY1 d
) d
WHERE value IS NOT NULL
GROUP BY grp;
Here is a db<>fiddle.
You can try below:
SELECT value from STRING_SPLIT
(
(select SUBSTRING(
(
SELECT ''+ isnull(Value,',')
FROM DUMMY1
FOR XML PATH('')
), 1 , 9999)),
',') as SS
where SS.value <> ''

Can I return all columns as rows and their values in the second column?

I'm trying to create some reports for auditing, but I have a very specific question.
There's about 120 columns, each with a specific numeric answer. I'd like to return the column name and the value of the rows of that column. I'm aware I'll get a lot of results, but it's not a problem.
For example I have:
KEY |ColumnA | ColumnB
1 |Value A | ValueB
2 |ValueA2 | ValueB2
But want I want is:
1 |ColumnA | Value A
2 |ColumnA | Value A2
1 |ColumnB | Value B
2 |ColumnB | Value B2
I've tried returning all rows and then joining on itself, but it didn't provide me with the output I needed.
Simple unpivot will do the work :)
declare #tbl table ([Key] int, ColumnA varchar(15), ColumnB varchar(15));
insert into #tbl values
(1, 'Value A', 'ValueB'),
(2, 'ValueA2', 'ValueB2');
select [key], [column], [value] from
(select * from #tbl) p
unpivot
([value] for [column] in (ColumnA, ColumnB)) u
order by [column]
it's so simple...If you know the column names, you could use a simple UNION
SELECT * FROM tblAuditing
SELECT 'ColumnA' AS ColumnA,'ColumnB' AS ColumnA UNION
SELECT ColumnA AS ColumnA,ColumnB AS ColumnA FROM tblAuditing
The following query should do what you want - you need to do a customized sorting for the columns names:
CREATE TABLE #temp (ColumnA VARCHAR(20), ColumnB VARCHAR(20))
INSERT INTO #temp VALUES ('Value A','Value B'),('Value A2','Value B2')
SELECT t.Col, t.Val
FROM (SELECT *,ROW_NUMBER() OVER (ORDER BY (SELECT 1)) RNO FROM #temp t) tmp
CROSS APPLY (VALUES (tmp.ColumnA,'ColumnA',tmp.RNO),(tmp.ColumnB,'ColumnB',tmp.RNO)) AS T(Val,Col,sort)
ORDER BY T.Col, Sort
The result is as below,
Col Val
ColumnA Value A
ColumnA Value A2
ColumnB Value B
ColumnB Value B2

How to get the each record with some condition

I have following data:
DECLARE #temp TABLE (
ID int
,sn varchar(200)
,comment varchar(2000)
,rownumber int
)
insert into #temp values(1,'sn1',NULL,1)
insert into #temp values(2,'sn1','aaa',2)
insert into #temp values(3,'sn1','bbb',3)
insert into #temp values(4,'sn1',NULL,4)
insert into #temp values(5,'sn2',NULL,1)
insert into #temp values(6,'sn2',NULL,2)
insert into #temp values(7,'sn2',NULL,3)
select * from #temp
And I want to output like this:
2 sn1 aaa 2
5 sn2 NULL 1
same sn, if comment have value, get this lower rownumber's record. For sn1, have two records with comment value, so here, get the the record with rownumber=2
If comment doesn't have value, get the lower rownumber's record. For sn2, get the record with rownumber=1
May I know how to write this SQL?
This is a prioritization query. I think row_number() is the simplest method:
select t.*
from (select t.*,
row_number() over (partition by sn
order by (case when comment is not null then 1 else 2 end),
rownumber
) as seqnum
from #temp t
) t
where seqnum = 1;
Here is a db<>fiddle.

How do I replace strings of a table from another table column

How do I update/replace the value of the first table from the list of my second table in SQL. Sorry im not so good in using replace() of SQL especially replacing from values base from different table
First table.
ID | Value
======================
1 | Fruits[Apple]
2 | Fruits[Apple,Mango]
3 | Apple[Red,Green]
Second table
Search | Replace
=========================
Apple | Orange
Green | Yellow
You will need some kind of recursive replace.
something like a loop
declare #t1 table (ID int, Value varchar(max))
declare #t2 table (Search varchar(max), ReplaceWith varchar(max))
insert #t1 values (1, 'Fruits[Apple]'),(2, 'Fruits[Apple,Mango]'), (3, 'Apple[Red,Green]')
insert #t2 values ('Apple', 'Orange'),('Green', 'Yellow')
--loop nth times for rows that have more than one match
while exists(select top 1 * from #t1 inner join #t2 on charindex(Search, Value ) > 0)
begin
update #t1
set Value = replace(Value, Search, ReplaceWith)
from #t2
inner join #t1 on charindex(Search, Value ) > 0
end
select * from #t1
results
ID Value
----- -----------------------
1 Fruits[Orange]
2 Fruits[Orange,Mango]
3 Orange[Red,Yellow]
Alternatively, you could use recursive CTE
;with CTE(ID, Value, rec_count)
as (
select distinct ID, Value, 1 as rec_count from #t1 inner join #t2 on charindex(Search, Value ) > 0
union all
select ID, Value = replace(Value, Search, ReplaceWith), rec_count +1
from CTE
inner join #t2 on charindex(Search, Value ) > 0
)
update #t1
set Value= replaced.Value
from #t1 t
inner join
( select distinct ID, Value
from CTE c
where rec_count > 1
and rec_count = (select max(rec_count) from CTE where ID = c.ID) ) replaced on replaced.ID = t.ID
Simply use following UPDATE by cross-joined select statement and enjoy it! ;)
UPDATE tFirst
SET Value = REPLACE(tFirst.Value, tSecond.Search, tSecond.Replace)
FROM
[First] tFirst
CROSS JOIN [Second] tSecond