STUFF doesn't work well with NULL Values and Grouping - sql

I have table with below schema and data.
CREATE TABLE [dbo].[SearchTest]
(
[DocumentNumber] [int] NOT NULL,
[AlphaNumeric] [nvarchar](50) NULL,
[Integers] [int] NULL
) ON [PRIMARY]
GO
INSERT [dbo].[SearchTest] ([DocumentNumber], [AlphaNumeric], [Integers])
VALUES (1, N'abc', 1)
INSERT [dbo].[SearchTest] ([DocumentNumber], [AlphaNumeric], [Integers])
VALUES (2, N'abc', 1)
INSERT [dbo].[SearchTest] ([DocumentNumber], [AlphaNumeric], [Integers])
VALUES (3, N'bcd', 2)
INSERT [dbo].[SearchTest] ([DocumentNumber], [AlphaNumeric], [Integers])
VALUES (4, N'bcd', 2)
GO
Table data:
I would like to do grouping using Alphanumeric and Integers column and get the DocumentNumber as comma separated value in my final result.
My final result should look like this,
Here is my query that gives the above output,
SELECT *
FROM
(SELECT
STUFF((SELECT ', ' + CAST(DocumentNumber AS VARCHAR(10)) [text()]
FROM SearchTest
WHERE AlphaNumeric = Result.Alphanumeric
OR Integers = Result.Integers
FOR XML PATH(''), TYPE).value('.', 'NVARCHAR(MAX)'), 1, 2, ' ') DocumentNumbers,
COUNT(DocumentNumber) TotalDocuments,
Result.AlphaNumeric,
Result.Integers
FROM
(SELECT *
FROM SearchTest
WHERE AlphaNumeric LIKE '%b%' OR Integers = 1) AS Result
GROUP BY
Result.AlphaNumeric, Result.Integers) AS Final
However the above query breaks if I have null values in Integers column.
For example, if I have NULL value in my integer columns as shown here:
Now my query breaks and I get the wrong results in my stuff query as shown below
Grouping works fine in the above query but STUFF part which gives DocumentNumbers gives wrong result. In this case it has be 2 in first row and 1 in second row.
Here is the expected result:
| DocumentNumbers| TotalDocuments| AlphaNumeric | Integers |
+----------------+---------------+---------------+---------------+
| 2 | 1 | abc | NULL |
| 1 | 1 | abc | 1 |
| 3, 4 | 2 | bcd | 2 |
Please assist on where I'm going wrong

You need to change the WHERE clause of the inner query to a) use AND instead of OR and b) to check for NULLs too.
SELECT stuff((SELECT concat(', ', documentnumber)
FROM searchtest st2
WHERE (st2.alphanumeric = st1.alphanumeric
OR st2.alphanumeric IS NULL
AND st1.alphanumeric IS NULL)
AND (st2.integers = st1.integers
OR st2.integers IS NULL
AND st1.integers IS NULL)
FOR XML PATH('')),
1,
2,
'') documentnumbers,
count(*) totaldocuments,
alphanumeric,
integers
FROM searchtest st1
WHERE st1.alphanumeric LIKE '%b%'
OR st1.integers = 1
GROUP BY st1.alphanumeric,
st1.integers;

Following #GordonLinoff comments in the question. This can be easily achieved using STRING_AGG() provided you're using SQL Server 2017 and above. This simplifies the query as well.
Query:
SELECT *
FROM
(SELECT
STRING_AGG(Result.DocumentNumber, ', ') DocumentNumbers,
COUNT(DocumentNumber) TotalDocuments,
Result.AlphaNumeric,
Result.Integers
FROM
(SELECT *
FROM SearchTest
WHERE AlphaNumeric LIKE '%b%' OR Integers = 1) AS Result
GROUP BY
Result.AlphaNumeric, Result.Integers) AS Final
Expected Output:

I had edited the code now:
You can use the below code to attain this:
SELECT S.DocumentNumbers,COUNT(S.DocumentNumbers) AS TotalDocuments,S.AlphaNumeric,S.Integers FROM
(SELECT COALESCE(Stuff((SELECT ', ' + CAST(DocumentNumber AS VARCHAR(10))
FROM SearchTest T1
WHERE T1.AlphaNumeric=T2.AlphaNumeric AND T1.Integers=T2.Integers
FOR XML PATH(''), TYPE).value('.','NVARCHAR(MAX)'),1,2,' ')
,CAST (T2.DocumentNumber AS VARCHAR(20))) AS DocumentNumbers,T2.AlphaNumeric,T2.Integers
FROM SearchTest T2) S
GROUP BY DocumentNumbers,S.AlphaNumeric,S.Integers
ORDER BY S.Integers

Related

Replace a specific character with blank

How can I replace 'a' to blank?
`Name` `ID`
----------------------------------
`b,c,d,e,abb,a` `1`
`b,c,d,a,e,abb` `2`
`a,b,c,d,a,e,abb` `3`
One way to do it would be to add a , to the beginning and end of each Name, then replace every occurence of ',a,' with ',', then trim the result of the ,:
update table_name
set Name = trim(',' from replace(concat(',', Name, ','), ',a,', ','));
Fiddle
Or if you just want to do a select without changing the rows:
select trim(',' from replace(concat(',', Name, ','), ',a,', ',')) as Name, ID
from table_name;
To address #Iptr's comment, if there can be consecutive a such as a, a, ..., you could use STRING_SPLIT to get rows from comma-separated values, then filter out where the value is a, then STRING_AGG and group by to get the comma separated values back:
select ID, STRING_AGG(u.Value, ',') as Name
from table_name
cross apply STRING_SPLIT (Name, ',') u
where Value <> 'a'
group by ID
Fiddle
Here is a solution based on tokenization via XML/XQuery.
It will work starting from SQL Server 2012 onwards.
Steps:
We are tokenizing a string of tokens via XML.
XQuery FLWOR expression is filtering out the 'a' token.
Reverting it back to a string of tokens.
SQL
-- DDL and sample data population, start
DECLARE #tbl TABLE (ID INT IDENTITY PRIMARY KEY, tokens VARCHAR(1000));
INSERT INTO #tbl (tokens) VALUES
('b,c,d,e,abb,a'),
('b,c,d,a,e,abb'),
('a,b,c,d,a,e,abb');
-- DDL and sample data population, end
DECLARE #separator CHAR(1) = ',';
SELECT t.*
, REPLACE(c.query('
for $x in /root/r/text()
return if ($x = "a") then ()
else data($x)
').value('.', 'VARCHAR(MAX)'), SPACE(1), #separator) AS Result
FROM #tbl AS t
CROSS APPLY (SELECT TRY_CAST('<root><r><![CDATA[' +
REPLACE(tokens, #separator, ']]></r><r><![CDATA[') +
']]></r></root>' AS XML)) AS t1(c);
Output
+----+-----------------+-------------+
| ID | tokens | Result |
+----+-----------------+-------------+
| 1 | b,c,d,e,abb,a | b,c,d,e,abb |
| 2 | b,c,d,a,e,abb | b,c,d,e,abb |
| 3 | a,b,c,d,a,e,abb | b,c,d,e,abb |
+----+-----------------+-------------+
Try as follow:
select Replace(name, N'a', N'') as RepName , ID from yourTable
Try this.
SELECT ID,Name, REPLACE(Name, 'a', ' ')
FROM tableName;

SQL: How to split column by character count [duplicate]

This question already has answers here:
Split column into multiple columns based on character count
(3 answers)
Closed last year.
I have one column with letters. I want to split this column into chunks of three. What SQL code for Microsoft would I need? I have read 'split my a special character' but I am not sure how to create a split by value where the split is not restricted to number of columns either.
You can do :
select t.*, substring(col, 1, 3), substring(col, 4, 3), substring(col, 7, 3)
from table t
If you really want to do this dynamically, as stated in the question, and have a query that creates just as many columns as needed, then you do need dynamic SQL.
Here is a solution that uses a recusive CTE to generate the query string.
declare #sql nvarchar(max);
with cte as (
select
1 pos,
cast('substring(code, 1, 3) col1' as nvarchar(max)) q,
max(len(code)) max_pos from mytable
union all
select
pos + 1,
cast(
q
+ ', substring(code, ' + cast(pos * 3 + 1 as nvarchar(3))
+ ', 3) col'
+ cast(pos + 1 as nvarchar(3))
as nvarchar(max)),
max_pos
from cte
where pos < max_pos / 3
)
select #sql = N'select ' + q + ' from mytable'
from cte
where len(q) = (select max(len(q)) from cte);
select #sql sql;
EXEC sp_executesql #sql;
The anchor of the recursive query computes the length of the longest string in column code. Then, the recursive part generates a series of substring() expressions for each chunk of 3 characters, with dynamic column names like col1, col2 and so on. You can then (debug and) execute that query string.
Demo on DB Fiddle:
-- debug
| sql |
| :---------------------------------------------------------------------------------------------------------------------------------- |
| select substring(code, 1, 3) col1, substring(code, 4, 3) col2, substring(code, 7, 3) col3, substring(code, 10, 3) col4 from mytable |
-- results
col1 | col2 | col3 | col4
:--- | :--- | :--- | :---
ABC | DEF | GHI |
XYZ | ABC | |
JKL | MNO | PQR | STU
ABC | DEF | |
Try it like this, which does not need any generic SQL (as long as you can specify a maximum count of columns):
First we need to define a mockup scenario to simulate your issue
DECLARE #tbl TABLE(ID INT IDENTITY, YourString VARCHAR(100));
INSERT INTO #tbl VALUES ('AB')
,('ABC')
,('ABCDEFGHI')
,('XYZABC')
,('JKLMNOPQRSTU')
,('ABCDEF');
--We can set the chunk length generically. Try it with other values...
DECLARE #ChunkLength INT=3;
--The query
SELECT p.*
FROM
(
SELECT t.ID
,CONCAT('Col',A.Nmbr) AS ColumnName
,SUBSTRING(t.YourString,(A.Nmbr-1)*#ChunkLength + 1,#ChunkLength) AS Chunk
FROM #tbl t
CROSS APPLY
(
SELECT TOP((LEN(t.YourString)+(#ChunkLength-1))/#ChunkLength) ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) FROM master..spt_values
) A(Nmbr)
) src
PIVOT
(
MAX(Chunk) FOR ColumnName IN(Col1,Col2,Col3,Col4,Col5,Col6 /*add the maximum column count here*/)
) p;
The idea in short:
By using an APPLY call we can create a row-wise tally. This will return multiple rows per input string. The row count is defined by the computed TOP-clause.
We use the row-wise tally first to create a column Name and second as parameters in SUBSTRING().
Finally we can use PIVOT to return this as horizontal list.
One hint about generic result sets:
This might be kind of religion, but - at least in my point of view - I would prefer a fix resultset with a lot of empty columns, rather than a generically defined set. The consumer should know the result format in advance...
You might use exactly the same query as dynamically created SQL statement. The only thing you would need to change is the actual list of column names in the PIVOT's IN-clause.

How to concatenate strings in SQL Server, and sort/ order by a different column?

I've seen many examples of concatenating strings in SQL Server, but if they worry about sorting, it's always by the column being concatenated.
I need to order the values based on data in a different fields.
Sample table:
ClassID | StudentName | SortOrder
-----------------------------
A |James |1
A |Janice |3
A |Leonard |2
B |Luke |2
B |Leia |1
B |Artoo |3
And the results I'd like to get are:
ClassID |StudentName
--------------------------------
A |James, Leonard, Janice
B |Leia, Luke, Artoo
How can this be done in SQL Server 2016?
(I'm looking forward to STRING_AGG in 2017, but we're not there yet...)
Thanks!
Here you go:
SELECT
s1.ClassID
, STUFF((SELECT
',' + s2.StudentName
FROM dbo.Student AS s2
WHERE s1.classID = s2.ClassID
ORDER BY s2.SortOrder
FOR XML PATH('')), 1, 1, '') AS StudentNames
FROM dbo.Student AS s1
GROUP BY s1.ClassID
SQL Fiddle
MS SQL Server 2017 Schema Setup:
CREATE TABLE MyTable(ClassID varchar(255),StudentName varchar(255),SortOrder int)
INSERT INTO MyTable(ClassID,StudentName,SortOrder)VALUES('A','James',1),('A','Janice',3),('A','Leonard',2),
('B','Luke',2),('B','Lela',1),('B','Artoo',3)
Query 1:
SELECT
t.ClassID
, STUFF((SELECT
',' + t1.StudentName
FROM MyTable t1
WHERE t.classID = t1.ClassID
ORDER BY t1.SortOrder
FOR XML PATH('')), 1, 1, '') AS StudentNamesConcat
FROM MyTable AS t
GROUP BY t.ClassID
Results:
| ClassID | StudentNamesConcat |
|---------|----------------------|
| A | James,Leonard,Janice |
| B | Lela,Luke,Artoo |
Here the query
IF OBJECT_ID('tempdb..#Temp') IS NOT NULL DROP TABLE #Temp;
CREATE TABLE #Temp(ClassId varchar(10),studName varchar(100),SortOrder int)
INSERT INTO #Temp(ClassId , studName, SortOrder)
SELECT 'A','James',1 UNION ALL
SELECT 'A','Janice',3UNION ALL
SELECT 'A','Leonard',2 UNION ALL
SELECT 'B','Luke',2 UNION ALL
SELECT 'B','Leia',1 UNION ALL
SELECT 'B','Artoo',3
-- select * from #Temp
select
distinct
stuff((
select ',' + u.studName
from #Temp u
where u.studName = studName and U.ClassId = L.ClassId
order by u.SortOrder
for xml path('')
),1,1,'') as userlist,ClassId
from #Temp L
group by ClassId
You can try using PIVOT also. This will support even older version of SQL server.
Only limitation : you should know the maximum SortOrder value. Below code will work for SortOrder <=20 of any ClassID
SELECT ClassID, ISNULL([1],'') +ISNULL(', '+[2],'')+ISNULL(', '+[3],'')+ISNULL(', '+
[4],'')+ISNULL(', '+[5],'')+ISNULL(', '+[6],'')+ISNULL(', '+[7],'')+ISNULL(', '+
[8],'')+ISNULL(', '+[9],'')+ISNULL(', '+[10],'')+ISNULL(', '+[11],'')+ISNULL(', '+
[12],'')+ISNULL(', '+[13],'')+ISNULL(', '+[14],'')+ISNULL(', '+[15],'')+ISNULL(', '+
[16],'')+ISNULL(', '+[17],'')+ISNULL(', '+[18],'')+ISNULL(', '+[19],'')+ISNULL(', '+
[20],'') AS StudentName
FROM
(SELECT SortOrder,ClassID,StudentName
FROM [Table1] A
) AS SourceTable
PIVOT
(
MAX(StudentName)
FOR SortOrder IN ([1],[2],[3],[4],[5],[6],[7],[8],[9],[10],[11],[12],[13],[14],[15],[16],[17],[18],[19],[20])
) AS PivotTable

Sql query for merge two rows into one row if id is same

I have table named TICKET_LAB
I have table named TICKET_LAB
as shown
as shown
TICKET_LAB table
and table named test
and table named test
as shown
as shown
Test table:
I need a SQL query to get the result like that :
I need a SQL query to get the result like that :
TLAB_TICKETID | TEST_NAME
------------- | -------------
29 | blood, stool,...etc
any idea
In SQL Server it may looks like this:
CREATE TABLE #TICKET_LAB(Id int)
INSERT INTO #TICKET_LAB VALUES(1), (2)
CREATE TABLE #TEST(TicketId int, Name nvarchar(100))
INSERT INTO #TEST VALUES(1, 'blood'), (1, 'etc.'), (2, 'test')
SELECT Id, STUFF((
SELECT ', ' + t.Name FROM #TEST t WHERE tl.Id = t.TicketId FOR XML PATH('')), 1, 2, ''
)
FROM #TICKET_LAB tl
SELECT tckt.TLAB_TICKETID, STUFF(
(SELECT ',' + TEST_NAME
FROM [hos].[dbo].[test]
WHERE tckt.TLAB_TESTID = Test_ID
FOR XML PATH (''))
, 1, 1, '')
FROM [hos].[dbo].[test] tst
JOIN [hos].[dbo].[Ticket_Lab] tckt
on tst.Test_ID = tckt.TLAB_TESTID
GROUP BY tckt.TLAB_TICKETID
I think this should work.

Getting csv from table without any grouping SQL

I have a table that contains only two columns say Name, DepartmentId as follows
CREATE TABLE #TempDepartment
(
Name NVARCHAR(50)
,DepartmentId INT
)
INSERT INTO #TempDepartment VALUES
('ABC',1)
,('ABC2',1)
,('DEF',2)
,('XYZ',3)
I am looking to retrieve list of distinct departmentId in comma seperated format. How can I achieve this in SQL? Basically there is no common group item, hence the confusion with either using STUFF or COALESCE.
Expected output is - 1,2,3
Try this query
Select Top 1 STUFF((SELECT ', ' + CAST(DepartmentId AS Varchar(20))
FROM #TempDepartment b Group By DepartmentId
FOR XML PATH('')), 1, 2, '') CSV
From #TempDepartment
Fiddle Output
O/P :
+---------+
| CSV |
+---------|
| 1, 2, 3 |
+---------+
Try this:
;WITH CTE AS
(
SELECT DISTINCT DepartmentId
FROM #TempDepartment
)
SELECT TOP (1)
STUFF((SELECT ',' + CAST(DepartmentId AS VARCHAR(10))
FROM CTE FOR XML PATH('')), 1, 1, '')
FROM CTE
Should produce the required output:
1,2,3