STRING_AGG substitute for SQL Server 2008 [duplicate] - sql

Can anyone help me make this query work for SQL Server 2014?
This is working on Postgresql and probably on SQL Server 2017. On Oracle it is listagg instead of string_agg.
Here is the SQL:
select
string_agg(t.id,',') AS id
from
Table t
I checked on the site some xml option should be used but I could not understand it.

In SQL Server pre-2017, you can do:
select stuff( (select ',' + cast(t.id as varchar(max))
from tabel t
for xml path ('')
), 1, 1, ''
);
The only purpose of stuff() is to remove the initial comma. The work is being done by for xml path.

Note that for some characters, the values will be escaped when using FOR XML PATH, for example:
SELECT STUFF((SELECT ',' + V.String
FROM (VALUES('7 > 5'),('Salt & pepper'),('2
lines'))V(String)
FOR XML PATH('')),1,1,'');
This returns the string below:
7 > 5,Salt & pepper,2
lines'
This is unlikely desired. You can get around this using TYPE and then getting the value of the XML:
SELECT STUFF((SELECT ',' + V.String
FROM (VALUES('7 > 5'),('Salt & pepper'),('2
lines'))V(String)
FOR XML PATH(''),TYPE).value('(./text())[1]','varchar(MAX)'),1,1,'');
This returns the string below:
7 > 5,Salt & pepper,2
lines
This would replicate the behaviour of the following:
SELECT STRING_AGG(V.String,',')
FROM VALUES('7 > 5'),('Salt & pepper'),('2
lines'))V(String);
Of course, there might be times where you want to group the data, which the above doesn't demonstrate. To achieve this you would need to use a correlated subquery. Take the following sample data:
CREATE TABLE dbo.MyTable (ID int IDENTITY(1,1),
GroupID int,
SomeCharacter char(1));
INSERT INTO dbo.MyTable (GroupID, SomeCharacter)
VALUES (1,'A'), (1,'B'), (1,'D'),
(2,'C'), (2,NULL), (2,'Z');
From this wanted the below results:
GroupID
Characters
1
A,B,D
2
C,Z
To achieve this you would need to do something like this:
SELECT MT.GroupID,
STUFF((SELECT ',' + sq.SomeCharacter
FROM dbo.MyTable sq
WHERE sq.GroupID = MT.GroupID --This is your correlated join and should be on the same columns as your GROUP BY
--You "JOIN" on the columns that would have been in the PARTITION BY
FOR XML PATH(''),TYPE).value('(./text())[1]','varchar(MAX)'),1,1,'')
FROM dbo.MyTable MT
GROUP BY MT.GroupID; --I use GROUP BY rather than DISTINCT as we are technically aggregating here
So, if you were grouping on 2 columns, then you would have 2 clauses your sub query's WHERE: WHERE MT.SomeColumn = sq.SomeColumn AND MT.AnotherColumn = sq.AnotherColumn, and your outer GROUP BY would be GROUP BY MT.SomeColumn, MT.AnotherColumn.
Finally, let's add an ORDER BY into this, which you also define in the subquery. Let's, for example, assume you wanted to sort the data by the value of the ID descending in the string aggregation:
SELECT MT.GroupID,
STUFF((SELECT ',' + sq.SomeCharacter
FROM dbo.MyTable sq
WHERE sq.GroupID = MT.GroupID
ORDER BY sq.ID DESC --This is identical to the ORDER BY you would have in your OVER clause
FOR XML PATH(''),TYPE).value('(./text())[1]','varchar(MAX)'),1,1,'')
FROM dbo.MyTable MT
GROUP BY MT.GroupID;
For would produce the following results:
GroupID
Characters
1
D,B,A
2
Z,C
Unsurprisingly, this will never be as efficient as a STRING_AGG, due to having the reference the table multiple times (if you need to perform multiple aggregations, then you need multiple sub queries), but a well indexed table will greatly help the RDBMS. If performance really is a problem, because you're doing multiple string aggregations in a single query, then I would either suggest you need to reconsider if you need the aggregation, or it's about time you conisidered upgrading.

Related

Stripping Values between two brackets {}

Good Afternoon,
I'm trying to query a column that gets data between two brackets. there may be multiple sets in the column such as : {Abrasision} {None} {Bruise}
i use this and it doesn't do exactly what i want, because i think i only use one bracket in the query. i want to get each value in my result set and insert into a table variable. Just having a little bit of trouble.
SELECT
LEFT(InjuryCategory, CHARINDEX('{', InjuryCategory)-1),
SUBSTRING(InjuryCategory, CHARINDEX('{', InjuryCategory)+1, LEN(InjuryCategory)-CHARINDEX('{', InjuryCategory)-CHARINDEX('{',REVERSE(InjuryCategory ))),
RIGHT(InjuryCategory, CHARINDEX('{', REVERSE(InjuryCategory))-1)
FROM TblVictim
You may use STRING_SPLIT(), STUFF() and STRING_AGG() to get the expected results. Note, that STRING_SPLIT() orders the results (using enable_ordinal parameter) only in Azure SQL Database, Azure SQL Managed Instance, and Azure Synapse Analytics (serverless SQL pool only), so STRING_AGG() may aggregate differently.
Test data:
SELECT *
INTO tblVictim
FROM (
VALUES ('{Abrasision} {None} {Bruise}')
) t (InjuryCategory)
Statement:
SELECT STRING_AGG(STUFF(s.[value], 1, CHARINDEX('{', s.[value]), ''), ' ') AS Category
FROM tblVictim t
CROSS APPLY STRING_SPLIT(t.InjuryCategory, '}') s
WHERE s.[value] <> ''
Result:
Category
----------------------
Abrasision None Bruise
In newer versions of SQL Server, you can combine STRING_SPLIT and TRIM
SELECT TRIM('{}' FROM s.[value]) AS Category
FROM TblVictim v
CROSS APPLY STRING_SPLIT(v.InjuryCategory, ' ') s
WHERE s.[value] <> '';
db<>fiddle
Quick and dirty, since this is delimited data, pretend it's XML. Setup:
DECLARE #tblVictim TABLE(ID INT IDENTITY, InjuryCategory NVARCHAR(MAX));
INSERT #tblVictim(InjuryCategory)
VALUES
('{Abrasision} {None} {Bruise}'),
('{Abrasision} {<5} {Bruise; very severe}');
Query:
WITH data AS (
SELECT ID, xml = CAST(REPLACE(REPLACE(InjuryCategory,
'{', '<i><![CDATA['),
'}', ']]></i>') AS XML
)
FROM #tblVictim
)
SELECT ID, node.value('text()[1]', 'nvarchar(max)')
FROM data
CROSS APPLY xml.nodes('i') AS nodes(node)
Note that this completely breaks down (with no easy fixes) if there are unbalanced delimiters.

How to use Join with like operator and then casting columns

I have 2 tables with these columns:
CREATE TABLE #temp
(
Phone_number varchar(100) -- example data: "2022033456"
)
CREATE TABLE orders
(
Addons ntext -- example data: "Enter phone:2022033456<br>Thephoneisvalid"
)
I have to join these two tables using 'LIKE' as the phone numbers are not in same format. Little background I am joining the #temp table on the phone number with orders table on its Addons value. Then again in WHERE condition I am trying to match them and get some results. Here is my code. But my results that I am getting are not accurate. As its not returning any data. I don't know what I am doing wrong. I am using SQL Server.
select
*
from
order_no as n
join
orders as o on n.order_no = o.order_no
join
#temp as t on t.phone_number like '%'+ cast(o.Addons as varchar(max))+'%'
where
t.phone_number = '%' + cast(o.Addons as varchar(max)) + '%'
You can not use LIKE statement in the JOIN condition. Please provide more information on your tables. You have to convert the format of one of the phone field to compile with other phone field format in order to join.
I think your join condition is in the wrong order. Because your question explicitly mentions two tables, let's stick with those:
select *
from orders o JOIN
#temp t
on cast(o.Addons as varchar(max)) like '%' + t.phone_number + '%';
It has been so long since I dealt with the text data type (in SQL Server), that I don't remember if the cast() is necessary or not.
Instead of trying to do everything in a single top-level query, you should apply a transformation projection to your orders table and use that as a subquery, which will make the query easier to understand.
Using the CHARINDEX function will make this a lot easier, however it does not support ntext, you will need to change your schema to use nvarchar(max) instead - which you should be doing anyway as ntext is deprecated, fortunately you can use CONVERT( nvarchar(max), someNTextValue ), though this will reduce performance as you won't be able to use any indexes on your ntext values - but this query will run slowly anyway.
SELECT
orders2.*,
CASE WHEN orders2.PhoneStart > 0 AND orders2.PhoneEnd > 0 THEN
SUBSTRING( orders2.Addons, orders2.PhoneStart, orders2.PhoneEnd - orders2.PhoneStart )
ELSE
NULL
END AS ExtractedPhoneNumber
FROM
(
SELECT
orders.*, -- never use `*` in production, so replace this with the actual columns in your orders table
CHARINDEX('Enter phone:', Addons) AS PhoneStart,
CHARINDEX('<br>Thephoneisvalid', AddOns, CHARINDEX('Enter phone:', Addons) ) AS PhoneEnd
FROM
orders
) AS orders2
I suggest converting the above into a VIEW or CTE so you can directly query it in your JOIN expression:
CREATE VIEW ordersWithPhoneNumbers AS
-- copy and paste the above query here, then execute the batch to create the view, you only need to do this once.
Then you can use it like so:
SELECT
* -- again, avoid the use of the star selector in production use
FROM
ordersWithPhoneNumbers AS o2 -- this is the above query as a VIEW
INNER JOIN order_no ON o2.order_no = order_no.order_no
INNER JOIN #temp AS t ON o2.ExtractedPhoneNumber = t.phone_number
Actually, I take back my previous remark about performance - if you add an index to the ExtractedPhoneNumber column of the ordersWithPhoneNumbers view then you'll get good performance.

Search for a particular value in a string with commas

I have a TEXT column in my Table T and contains some values separated by Commas.
Example
Columns BNFT has text values such as
B20,B30,B3,B13,B31,B14,B25,B29,B1,B2,B4,B5
OR
B1,B2,B34,B31,B8,B4,B5,B33,B30,B20,B3
I want to return result in my query only if B3 is present.
It should not consider B30-B39 or B[1-9]3 (i.e. B13, B23 .... B93).
I tried with below query, but want to implement REGEXP or REGEXP_LIKE/INSTR etc. Haven't used them before and unable to understand also.
Select *
FROM T
Where BNFT LIKE '%B3,%' or BNFT LIKE '%B3'
Pls advise
Procedures will not work. Query must start with Select as 1st statement.
The first advice is to fix your data structure. Storing lists of ids in strings is a bad idea:
You are storing numbers as strings. That is the wrong representation.
You are storing multiple values in a string column. That is not using SQL correctly.
These values are probably ids. You cannot declare proper foreign key relationships.
SQL does not have particularly strong string functions.
The resulting query cannot take advantage of indexes.
That said, sometimes we are stuck with other people's bad design decisions.
In SQL Server, you would do:
where ',' + BNFT + ',' LIKE '%,33,%'
This question was originally tagged MySQL, which offers find_in_set() for this purpose:
Where find_in_set(33, BNFT) > 0
Select *
FROM T
Where ',' + BNFT + ',' LIKE '%,B3,%';
or
Select *
FROM T
Where CHARINDEX (',B3,',',' + BNFT + ',') > 0;
This can be easily achieve by CTE, REGEXP/REGEXP_Like/INSTR works better with oracle, for MS SQL Server you can try this
DECLARE #CSV VARCHAR(100) ='B2,B34,B31,B8,B4,B5,B33,B30,B20,B3';
SET #CSV = #CSV+',';
WITH CTE AS
(
SELECT SUBSTRING(#CSV,1,CHARINDEX(',',#CSV,1)-1) AS VAL, SUBSTRING(#CSV,CHARINDEX(',',#CSV,1)+1,LEN(#CSV)) AS REM
UNION ALL
SELECT SUBSTRING(A.REM,1,CHARINDEX(',',A.REM,1)-1)AS VAL, SUBSTRING(A.REM,CHARINDEX(',',A.REM,1)+1,LEN(A.REM))
FROM CTE A WHERE LEN(A.REM)>=1
) SELECT VAL FROM CTE
WHERE VAL='B3'

Merging of fields using xml path in sql server, display comma where NULL

I have a table in which two or more different dates are listed for a single id. I want to merge all the dates for a single id. Example code is as below.
create table number(id nvarchar(255), billdate nvarchar(255))
insert into number(id,billdate) values ('56465','12/10/2011'),('56465','02/11/2011'),
('46462','12/09/2009'),('46462','12/06/2010'),('32169','12/22/2009'),
('32169','12/31/2011'),('86835','12/10/2010'),('86835','22-Jan-2010'),
('65641',''),('65641','12-Aug-2009'),('22458','25-Aug-2007'),('22458','')
For merging the rows I am using xml path as below
select Main.id,LEFT(Main.billdate,nullif(LEN(Main.billdate)-1,-1)) as "billdate"
from (select distinct ST2.id,(SELECT ST1.billdate + ',' AS [text()]
from NUMBER ST1 where ST1.id=ST2.id ORDER BY ST1.id FOR XML PATH (''))billdate
from NUMBER ST2)[Main]
It is working perfectly for this sample data, But the Problem is I have huge data, and when I apply this XML path code a comma is not displayed if a date is NULL, like for the id 65641. Its important for me to display a comma in the place of NULL. Where am I going wrong? Can anyone suggest why it's not displaying a comma in the place of NULL?
I'm not sure I perfectly understand you, since the putatively NULL value for 65641 is actually a blank. To treat NULL values like blanks, you can use this:
select Main.id,LEFT(Main.billdate,nullif(LEN(Main.billdate)-1,-1)) as "billdate"
from
(
select distinct ST2.id,
(
SELECT ISNULL(ST1.billdate + ',', ',') AS [text()]
from NUMBER ST1
where ST1.id=ST2.id
ORDER BY ST1.id
FOR XML PATH ('')
) billdate
from NUMBER ST2
)[Main]
The other issue you might be having is that if there is only a single blank/NULL value for a given id, you don't get even a single comma for it. This is happening because a single blank value only generates a single comma, which is then stripped off by your LEFT statement. You can make it leave single commas alone by changing it like so:
select Main.id,LEFT(Main.billdate,nullif(LEN(Main.billdate)-CASE WHEN LEN(Main.billdate) = 1 THEN 0 ELSE 1 END,-1)) as "billdate"
from
(
select distinct ST2.id,
(
SELECT ISNULL(ST1.billdate + ',', ',') AS [text()]
from NUMBER ST1
where ST1.id=ST2.id
ORDER BY ST1.id
FOR XML PATH ('')
) billdate
from NUMBER ST2
)[Main]
You still have issues, one of which is that you have no explicit ordering of dates, but I hope that covers the problems that you have. If not, clarify and I'll attempt to help some more.

How can I pull a list of ID's from a SQL table as a comma-separated values string?

I have to pull a list of integer IDs from a table using only records that match some criteria. For example:
Select ProdID From Products Where (ProdType='XYZ');
The catch is that I have to return it as a set of comma separated values so I can use it to select items in a multi-select list:
111,231,554,112
rather than as records. I do not want to do this in my C# code - I'd like it to come right out of the database via a query this way. Any ideas?
MySQL
SELECT GROUP_CONCAT(t.prodid SEPARATOR ',')
FROM PRODUCTS t
WHERE t.prodtype = 'XYZ'
Oracle:
There is an excellent summary of the available string aggregation techniques on Tim Hall's site.
SQL Server 2005+
SELECT STUFF((SELECT ','+ t.prodid
FROM PRODUCTS t
WHERE t.prodtype = 'XYZ'
FOR XML PATH('')), 1, 1, '')
In addition to #OMG Ponies method, you could also try this COALESCE trick from:
Using COALESCE to Build Comma-Delimited Strings
declare #string nvarchar(255)
select #string = coalesce(#string + ', ', '') + cast(prodid as nvarchar(5))
from products
From SQL Server 2017 onwards, you can now use the STRING_AGG function.
This allows you to create the comma-separated list from within the SELECT statement (so works nicely with views). Given your example, it will become:
SELECT STRING_AGG(ProdID, ',')
FROM Products
WHERE (ProdType='XYZ');
This is a very old question but I'm adding an answer that applies the already-accepted answer using COALESCE by Justin Niessner. This application is how I would normally want to apply this technique where I'm querying a parent and I want to also have a single column which contains a comma-delimited list of child IDs.
These examples go against an AdventureWorksLT database as created in Azure SQL Database if you use the dropdown to select it when you provision a database. Nothing new here, just a convenient application that might help somebody.
The first query is how I'll normally use it:
SELECT
SalesLT.ProductCategory.*,
STUFF((SELECT ','+ cast(ProductID as nvarchar(10)) FROM SalesLT.Product WHERE ProductCategoryID=SalesLT.ProductCategory.ProductCategoryID ORDER BY ProductID FOR XML PATH('')), 1, 1, '') AS ProductIDs
FROM SalesLT.ProductCategory
The second query shows a self-referencing use of it:
SELECT
ParentCategory.*,
STUFF((SELECT ','+ cast(child.ProductCategoryID as nvarchar(10)) FROM SalesLT.ProductCategory child WHERE child.ParentProductCategoryID=ParentCategory.ProductCategoryID ORDER BY child.ProductCategoryID FOR XML PATH('')), 1, 1, '') AS ChildCategoryIDs
FROM SalesLT.ProductCategory ParentCategory
WHERE
EXISTS (SELECT ParentProductCategoryID FROM SalesLT.ProductCategory children WHERE children.ParentProductCategoryID=ParentCategory.ProductCategoryID)
For SQL server see here: Concatenate Values From Multiple Rows Into One Column
Theres a way to do it without additional functions:
DECLARE #Test nvarchar(max) = ''
SELECT #Test = #Test + ProdID + ', '
FROM Products
WHERE (ProdType='XYZ')
SELECT #Test
#Test will contain a list of your IDs, although I cannot explain why this works.
For the future PostgreSQL users, please find the solution below (it is the same as #Matt Tester answered.
SELECT STRING_AGG(cast(id as varchar), ',') from table1
where col1 = 'ABC';
Please note that the cast is required if the column you are selecting is not string (or varchar in database terms).