Improve performance of SQL query with a FOR XML PATH in SQL Server 2008 - sql

I am trying to concatenate LoadIds for a user using For XML path which is a portion of my whole query, I verified that the maximum time gets elapsed in calculating this concatenated LoadId column. Below is the syntax, can anyone suggest a way to rewrite this efficiently?
SELECT
Col1, col2,
LoadIds = STUFF((SELECT ' , ' + CAST([LoadId] AS varchar(5))
FROM Table1 AS t1
WHERE t1.[UserId] = [t2].[UserId]
FOR XML PATH ('')), 1, 2, '')
FROM
Table1 AS t2
GROUP BY
[UserId]

1). I would try to collect the required data in a temp table or table variable, create an index on it and then play with the concatination.
2). FOR XML PATH works good for small sets of records, for large sets I would try a recursion.
declare #T table (
UserId int not null,
RowNumber int not null,
LoadId varchar(5) not null
primary key clustered (UserId, RowNumber)
);
insert into #T
select
UserId,
row_number() over(partition by UserId order by LoadId),
CAST(LoadId AS varchar(5))
from
Table1 ;
with cte (UserId, RowNumber, LoadIds) as
(
select
UserId,
RowNumber,
LoadIds = convert(varchar(8000), LoadId)
from #T
where RowNumber = 1
union all
select
t.UserId,
t.RowNumber,
convert(varchar(8000), cte.LoadIds + ', ' + t.LoadId)
from
cte inner join #T t on t.UserId = cte.UserId and t.RowNumber = cte.RowNumber + 1
)
select UserId, LoadIds = max(LoadIds) from cte group by UserId;

One thing you can do is select the distinct user ids before doing the string aggregation:
SELECT t2.UserId,
STUFF((SELECT ' , ' + CAST([LoadId] AS varchar(8000))
FROM Table1 t1
WHERE t1.[UserId] = [t2].[UserId]
FOR xml PATH ('')
), 1, 2, ''
) as LoadIds
FROM (SELECT DISTINCT userId
FROM Table1 t2
) t2;
For performance, an index on table1(UserId, LoadId) would also help.

Related

Combine Multi Rows with COALESCE

Using SQL-Server 2012
I have the following Table:
Id Description
6192 Salzburg
6193 Salzburg
6194 Salzburg
6196 Innsbruck
6197 Innsbruck
6198 Innsbruck
6199 Innsbruck
6201 Bregenz
6202 Bregenz
6203 Bregenz
I want to Select each Distinct "Description" with all the Id's together in one string:
Description Ids
Salzburg '6192,6193,6194'
Innsbruck '6196,6197,6198'
I saw some similar code on this site [How to concatenate text from multiple rows into a single text string in SQL server?, but I couldn't figure it out yet for my purpose (don't want to use XML Path!). Here is what I have tried so far:
DECLARE #ids AS Nvarchar(MAX)
SELECT #ids = COALESCE(#ids + ',', '') + CAST(t.Id AS nvarchar(5))
FROM (SELECT tmp.Id FROM (SELECT id, [Description] FROM tblMasterPropValues WHERE IdCategory = 253 AND IsActive = 1) as tmp
WHERE [Description] = tmp.[Description]) AS t
SELECT #ids
--SELECT DISTINCT [Description], #ids AS IDs FROM tblMasterPropValues WHERE IdCategory = 253 AND IsActive = 1 AND Id IN (#ids)
I can't really get my head around it, and would appreciate any help on it.
You can try using STUFF() function
SELECT description, Ids = STUFF(
(SELECT ',' + Id
FROM tblMasterPropValues t1
WHERE t1.description = t2.description
FOR XML PATH (''))
, 1, 1, '') from tblMasterPropValues t2
group by description;
For that FOR XML PATH() is the right clause so, you can do :
SELECT DISTINCT v.description, STUFF(v1.ids, 1, 1, '''') + ''''
FROM tblMasterPropValues v CROSS APPLY
(SELECT ', '+ CAST(v1.Id AS VARCHAR(255))
FROM tblMasterPropValues v1
WHERE v1.description = v.description
FOR XML PATH('')
) v1(ids);
You can also make it by using recursive CTE
DECLARE #tblMasterPropValues TABLE (Id INT, Description VARCHAR(20))
INSERT INTO #tblMasterPropValues VALUES
(6192 , 'Salzburg'),
(6193 , 'Salzburg'),
(6194 , 'Salzburg'),
(6196 , 'Innsbruck'),
(6197 , 'Innsbruck'),
(6198 , 'Innsbruck'),
(6199 , 'Innsbruck'),
(6201 , 'Bregenz'),
(6202 , 'Bregenz'),
(6203 , 'Bregenz')
;WITH Tbl AS
(
SELECT
*,
ROW_NUMBER() OVER(PARTITION BY Description ORDER BY Id) AS RN,
COUNT(*) OVER(PARTITION BY Description) AS CNT
FROM #tblMasterPropValues
)
, Rcr AS (
SELECT *, CAST(Id AS varchar(max)) Ids
FROM Tbl WHERE RN = 1
UNION ALL
SELECT T.*, Rcr.Ids + ',' + CAST(T.Id AS VARCHAR(10)) Ids
FROM Rcr
INNER JOIN Tbl T ON T.RN = Rcr.RN + 1 and Rcr.Description = T.Description
)
SELECT RN, Description, Ids FROM Rcr
WHERE RN = CNT
Result:
Description Ids
-------------------- -----------------------
Salzburg 6192,6193,6194
Innsbruck 6196,6197,6198,6199
Bregenz 6201,6202,6203
Try this:
DECLARE #Table TABLE(ID INT, Description VARCHAR(25))
INSERT INTO #Table
VALUES (6192,'Salzburg' )
,(6193,'Salzburg' )
,(6194,'Salzburg' )
,(6196,'Innsbruck')
,(6197,'Innsbruck')
,(6198,'Innsbruck')
,(6199,'Innsbruck')
,(6201,'Bregenz' )
,(6202,'Bregenz' )
,(6203,'Bregenz' )
Query:
SELECT DISTINCT T2.Description,
SUBSTRING(
(
SELECT ','+CAST(T1.ID AS VARCHAR) AS [text()]
FROM #Table T1
WHERE T1.Description = T2.Description
ORDER BY T1.Description
FOR XML PATH ('')
), 2, 1000) [Ids]
FROM #Table T2
Result:
Description Ids
Bregenz 6201,6202,6203
Innsbruck 6196,6197,6198,6199
Salzburg 6192,6193,6194

SQL Recursive CTE replace statement too slow

I have a recursive CTE that replaces multiple values from an expression, but it is too slow when there are many expressions.
CREATE TABLE #table1(IdExpresion INT, expresion VARCHAR(MAX))
CREATE TABLE #table2(IdExpresion INT, searchExpresion VARCHAR(50), replacementExpresion VARCHAR(50))
INSERT INTO #table1(IdExpresion, expresion)
VALUES(1, 'Mary had a little lamb'),
(2, 'The new student, student_name has the following grades Math - math_grade, Science - Science_grade')
INSERT INTO #table2(IdExpresion, searchExpresion, replacementExpresion)
VALUES(1, 'lamb','dog'),
(2, 'student_name','Joe Smith'),
(2, 'math_grade','A'),
(2, 'Science_grade','B+')
;WITH cte(IdExpresion, expresion, lvl) AS
(
SELECT t1.IdExpresion, t1.expresion, 1
FROM #table1 t1
UNION ALL
SELECT cte.IdExpresion, REPLACE(cte.expresion, t2.searchExpresion, t2.replacementExpresion), cte.lvl + 1
FROM cte
INNER JOIN #table2 t2
ON cte.IdExpresion = t2.IdExpresion
AND CHARINDEX(t2.searchExpresion, cte.expresion) > 0
)
SELECT DISTINCT c2.expresion
FROM (SELECT IdExpresion, MAX(lvl) AS lvl
FROM cte
GROUP BY IdExpresion) c1
INNER JOIN cte c2
ON c1.IdExpresion = c2.IdExpresion
AND c1.lvl = c2.lvl
OPTION (MAXRECURSION 0);
Anyone have any advice? I am using SQL Server by the way
Not sure if any more performant, but here is a brute force approach just for fun.
Already +1 LukStorm's answer, I suspect that is the way to go.
Example
Declare #S varchar(max) = (Select IdExpresion,expresion = replace(' '+expresion,' ',concat(' ',IdExpresion,'|||')) From #Table1 For XML Raw )
Select #S = replace(#S,concat(IdExpresion,'|||',searchExpresion),replacementExpresion) From #table2
Select IdExpresion = B.i.value('#IdExpresion', 'int')
,expresion = ltrim(replace(B.i.value('#expresion', 'varchar(max)'),B.i.value('#IdExpresion', 'varchar(25)')+'|||',''))
From (Select x = Cast(#S as xml).query('.')) as A
Cross Apply x.nodes('row') AS B(i)
Returns
IdExpresion expresion
1 Mary had a little dog
2 The new student, Joe Smith has the following grades Math - A, Science - B+
You could add another CTE to it that gets a row_number for each replacement, partitioned by the IdExpresion.
Then in the recursive CTE, instead of counting up, count down till there's no match with the replacement row_number.
The last entry in the CTE, that had all replacements, will have Lvl 0 then.
;WITH SEARCH AS (
SELECT
IdExpresion,
row_number() over (partition by IdExpresion order by searchExpresion) as rn,
searchExpresion, replacementExpresion
FROM #table2
), CTE(IdExpresion, expresion, lvl) AS
(
SELECT t1.IdExpresion, t1.expresion, count(*)
FROM #table1 t1
JOIN #table2 t2 ON t2.IdExpresion = t1.IdExpresion
GROUP BY t1.IdExpresion, t1.expresion
UNION ALL
SELECT c.IdExpresion, REPLACE(c.expresion, s.searchExpresion, s.replacementExpresion), c.lvl - 1
FROM CTE c
JOIN SEARCH s
ON s.IdExpresion = c.IdExpresion AND s.rn = c.lvl
)
SELECT IdExpresion, expresion
FROM CTE
WHERE lvl = 0
OPTION (MAXRECURSION 0);
This way, each REPLACE is only done once per IdExpresion.
And that without having to use CHARINDEX.
You could also replace that SEARCH cte with a temporary table.
One that has the records from #table2 with that row_number.
This has the benefit that with a table you can add a compound index.
On a large table it should speed up the recursive join to the replacements.
Test on rextester here
CREATE TABLE #tmpSearch (
IdExpresion INT,
rn INT,
searchExpresion VARCHAR(50),
replacementExpresion VARCHAR(50),
primary key (IdExpresion, rn));
insert into #tmpSearch (IdExpresion, rn, searchExpresion, replacementExpresion)
select
IdExpresion,
row_number() over (partition by IdExpresion order by searchExpresion) as rn,
searchExpresion,
replacementExpresion
from #table2
order by IdExpresion, searchExpresion;
;WITH CTE(IdExpresion, expresion, lvl) AS
(
SELECT t1.IdExpresion, t1.expresion, max(s.rn)
FROM #table1 t1
JOIN #tmpSearch s ON s.IdExpresion = t1.IdExpresion
GROUP BY t1.IdExpresion, t1.expresion
UNION ALL
SELECT c.IdExpresion, REPLACE(c.expresion, s.searchExpresion, s.replacementExpresion), c.lvl - 1
FROM CTE c
JOIN #tmpSearch s
ON s.IdExpresion = c.IdExpresion AND s.rn = c.lvl
)
SELECT IdExpresion, expresion
FROM CTE
WHERE lvl = 0
OPTION (MAXRECURSION 0);
Good day,
Here is another solution. Please check if this fit your needs. This solution does not use any loop but simple dynamic query.
DECLARE #SQLString nvarchar(MAX);
-- do not make mistake, this is simple CTE and not a recursive CTE (no Loop)
;With MyCTE as (
select R
From table1 t1
CROSS APPLY (
SELECT R = 'SELECT ' + CONVERT (NVARCHAR(MAX),t1.IdExpresion) + ' as IdExpresion,' + STRING_AGG ('REPLACE','(') + '(' + 't1.expresion,''' + STRING_AGG(t2.searchExpresion + ''',''' + t2.replacementExpresion , '''),''') + ''') as expresion FROM table1 t1 where t1.IdExpresion = ' + CONVERT (NVARCHAR(MAX),t1.IdExpresion)
from table2 t2
where t2.IdExpresion = t1.IdExpresion
) C
)
SELECT #SQLString = STRING_AGG(R,'
UNION ALL
')
FROM MyCTE
--PRINT #SQLString
EXECUTE sp_executesql #SQLString
GO
Note! I recommend to execute some tests to confirm that this solves all cases
Note! I am using the function STRING_AGG which was added to SQL Server 2017. In older version you can get the exact same solution using FOR XML statement.
Since we don't have the real DDL+DML we cannot really discuss about performance, but the difference in the execution plans of the solutions is 10% to 90% (In general, You should check IO and Time statistics in production in addition, before choosing your solution)
So... here is the Execution Plans Image (above query is my dynamic SQL solution and bellow is LukStorms solution using recursive CTE = Loop)

Concatenate results in select

I am trying to insert values into a table that come from an other (lookup) table.
The first 3 results from the table are selected and need to be concatenated before they are inserted into an other table.
How can I alter the following insert to first concatenates them with no separation characters between the 3 names (example: JohnMaxLouise)?
INSERT INTO Table 2 VALUES ((SELECT TOP 3 names FROM Table1 ORDER BY NEWID()))
I am using SQL Server 2016 so string_agg is not available.
Personally, I think this is simplest with conditional aggregation:
INSERT INTO Table2
SELECT (MAX(CASE WHEN seqnum = 1 THEN name ELSE '' END) +
MAX(CASE WHEN seqnum = 2 THEN name ELSE '' END) +
MAX(CASE WHEN seqnum = 3 THEN name ELSE '' END)
)
FROM (SELECT name, ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) as seqnum
FROM (SELECT TOP 3 name
FROM Table1
ORDER BY NEWID()
) t
) t;
An alternative is an XML approach, but if you know you want three, then conditional aggregation (or pivot) works fine.
try the following:
declare #tab table (names varchar(max))
declare #tab1 table ([name] varchar(100))
insert into #tab1
select 'John' union select 'Max' union select 'Louise' union select 'xxx'
insert into #tab select (select top 3 [name] + '' from #tab1 for xml path(''))
select * from #tab
Thanks.
This will return '1,2,3,4'
DECLARE #x TABLE (i INTEGER)
DECLARE #r VARCHAR(255)
INSERT INTO #x VALUES (1),(3),(2),(4)
SELECT #r= STUFF(( SELECT ',' + CAST(i AS VARCHAR(max))
FROM #x
ORDER BY i
FOR XML PATH(''), type
).value('.','varchar(255)'), 1, 1, '')
SELECT #r
Solution Overview
You can use FOR XML PATH('') to achieve this, just use the following command:
SELECT '' + NAME
FROM (SELECT Top 3 NAME FROM TBL_1 ORDER BY NEWID()) AS T
FOR XML PATH('')
Or simple concatenation
SELECT #x = #x + NAME
FROM (SELECT Top 3 NAME FROM TBL_1 ORDER BY NEWID()) AS T1
Detailed Solution
SQLFiddle Demo
First i created the test environment using the following query
CREATE TABLE TBL_1 (NAME Varchar(50))
CREATE TABLE TBL_2 (NAME Varchar(50))
INSERT INTO TBL_1 (Name) VALUES ('John'),('Max'),('Louise'),('Mark'),('Peter')
Then i Used the following command
DECLARE #x varchar(255)
SELECT #x = (SELECT '' + NAME
FROM (SELECT Top 3 NAME FROM TBL_1 ORDER BY NEWID()) AS T1
FOR XML PATH('') )
INSERT INTO TBL_2(NAME) SELECT #x;
SELECT * FROM TBL_2
And the Result is JohnLouiseMax
Or you can use simple concatenation to achieve this
SQLFiddle Demo
DECLARE #x varchar(255)
SET #x = ''
SELECT #x = #x + NAME
FROM (SELECT Top 3 NAME FROM TBL_1 ORDER BY NEWID()) AS T1
INSERT INTO TBL_2(NAME) SELECT #x;
SELECT * FROM TBL_2

Get result from select without repeated records next to each other

i have table with records :
City Name Seq
London 1
London 2
London 3
Madrid 4
London 5
Porto 6
Problem is how to get a result in string ( merge all without repeated records ).
Result : London-Madrid-London-Porto
Another option if 2012+ ... LAG()
Example
Declare #YourTable Table ([City Name] varchar(50),[Seq] int)
Insert Into #YourTable Values
('London',1)
,('London',2)
,('London',3)
,('Madrid',4)
,('London',5)
,('Porto',6)
Select Stuff((Select '-' +Value From
(
Select top 1000 Value = case when [City Name]=lag([City Name],1) over (Order By Seq) then null else [City Name] end
From #YourTable
Order By Seq
) A
For XML Path ('')),1,1,'')
Returns
London-Madrid-London-Porto
How about this?
declare #table table (CityName varchar(64), seq int)
insert into #table
values
('London',1),
('London',2),
('London',3),
('Madrid',4),
('London',5),
('Porto',6)
--find the next row that isn't the same city name (t2seq)
;with cte as(
select distinct
t.CityName
,t.seq
,min(t2.seq) as t2seq
from #table t
left join #table t2 on
t2.seq > t.seq
and t2.CityName <> t.CityName
group by
t.CityName
,t.seq),
--limit the result set to distinct list
cte2 as(
select distinct
CityName
,seq = isnull(t2seq,9999999)
from cte)
--use stuff to concat it together
select distinct
stuff(( select '-', + t2.CityName
from cte2 t2
order by seq
FOR XML PATH(''), TYPE).value('.', 'NVARCHAR(MAX)'), 1, 1, '')
from cte2

Comma Separated SQL Server Result Set 'JOINED' with other Columns

I have a table say ProjectMaster:
Id ProjectName
1 A
2 B
3 C
another table ProjectMeter
Id ProjectId MeterNumber
1 1 #0001
2 1 #0002
3 1 #0003
4 2 #0004
5 2 #0005
6 3 #0006
I wish to have following output
ProjectName MeterNumbers
A #0001, #0002, #0003
B #0004, #0005
C #0006
I tried this and this, but unable to solve my problem.
I cannot use a table variable.
I have a already written Stored Procedure and it brings data from many joined tables. ProjectMaster also happens to be joined in one of these tables. Now am required to fetch data from ProjectMeter, such that, each row has concatenated ProjectMeter.MeterNumber corresponding to the ProjectId in that column.
right now, I get concatenated list of all meternumbers in all the rows.
I cannot use CURSOR, TABLE variable , Temp TABLE
( I hope still something can be done to my cause)
please help.....
Try this:
SELECT projectname, STUFF((SELECT distinct ', ' + meternumber
from projectmeter m
where p.id = m.projectid
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)')
,1,1,'') MeterNumbers
from projectmaster p
See SQL Fiddle with Demo
DECLARE #ProjectMaster AS TABLE
(
ID INT IDENTITY(1, 1) ,
ProjectName VARCHAR(2)
)
DECLARE #ProjectMeter AS TABLE
(
ID INT IDENTITY(1, 1) ,
ProjectID INT ,
MeterNumber VARCHAR(50)
)
INSERT INTO #ProjectMaster
( ProjectName )
VALUES ( 'A' )
INSERT INTO #ProjectMeter
( ProjectID, MeterNumber )
VALUES ( 1, '#0001' )
INSERT INTO #ProjectMeter
( ProjectID, MeterNumber )
VALUES ( 1, '#0002' )
SELECT pMaster.ID, STUFF(( SELECT ',' + MeterNumber
FROM #ProjectMeter
FOR
XML PATH('')
), 1, 1, '') AS 'Concat Result'
FROM #ProjectMeter pMeter
INNER JOIN #ProjectMaster pMaster ON pMaster.ID = pMeter.ProjectID
GROUP BY pMaster.ID
I have used table variables here but surely you just need to drop the #'s as I have used the same table names as you have specified? Not sure if this is okay? :)
Also in MS SQL you can do it using recursive query with CTE.
Here is a SQLFiddle demo
;with t1 as (
select t.*,
cast(meternumber as varchar(max)) as m2,
0 as level
from ProjectMeter t
where not exists
(select id
from ProjectMeter l
where l.id<t.id and l.ProjectId=t.ProjectID
)
union all
select b.*,
cast(c.m2+','+b.MeterNumber as varchar(max)) as m2,
c.level+1 as level
from ProjectMeter b
inner join t1 c
on (c.id < b.id) and (b.ProjectID=c.ProjectId)
)
select pm.ProjectName as ProjectName,
t1.m2 as MeterNumbers
from t1
inner join
(select ProjectId,max(level) ml
from t1
group by ProjectId
) t2
on (t1.ProjectId=t2.ProjectID) and (t1.level=t2.ml)
left join ProjectMaster pm
on (t1.ProjectId=pm.Id)
order by t1.ProjectID