Related
I have a field containing multiple questions and answers. I need to extract the answers into a column each.
Text Example:
Sorry I had to add as a picture as the text kept disappearing.
I need to extract the text between the first instance of the yellow and green highlight (not including the highlighted sections) as the first line in the select clause, followed by the second instance between the yellow and green highlight as the second line in the select clause etc etc.
There are 5 questions (between the pink and blue highlight) and 5 answers (between the yellow and green highlight).
I tried the code below using the text in the yellow and green highlight as bookends but I got the same error message as below.
Then I tried the following code using the question as the first bookend:
SELECT distinct subjectidname
, title
, i.description
, SUBSTRING(i.description, CHARINDEX('<b>Please indicate your company''s export status:</b><br />', i.description),
CHARINDEX('<br /><br />',i.description) -
CHARINDEX('<b>Please indicate your company''s export status:</b><br />', i.description) + Len('<br /><br />'))
from FilteredIncident i
Both efforts resulted in an error message:
Msg 537, Level 16, State 3, Line 2 Invalid length parameter passed to
the LEFT or SUBSTRING function.
And it also does not account for the 2nd, 3rd, 4th & 5th instances.
What is the best way to extract the 5 answers from the description box containing a single line of text?
Start with a string splitter that can split on a string and returns an index for each row:
CREATE FUNCTION [dbo].[DelimitedSplit8K]
--===== Define I/O parameters
(#pString VARCHAR(8000), #pDelimiter VARCHAR(16))
--WARNING!!! DO NOT USE MAX DATA-TYPES HERE! IT WILL KILL PERFORMANCE!
RETURNS TABLE WITH SCHEMABINDING AS
RETURN
--===== "Inline" CTE Driven "Tally Table" produces values from 1 up to 10,000...
-- enough to cover VARCHAR(8000)
WITH E1(N) AS (
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1
), --10E+1 or 10 rows
E2(N) AS (SELECT 1 FROM E1 a, E1 b), --10E+2 or 100 rows
E4(N) AS (SELECT 1 FROM E2 a, E2 b), --10E+4 or 10,000 rows max
cteTally(N) AS (--==== This provides the "base" CTE and limits the number of rows right up front
-- for both a performance gain and prevention of accidental "overruns"
SELECT TOP (ISNULL(DATALENGTH(#pString),0)) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) FROM E4
),
cteStart(N1) AS (--==== This returns N+1 (starting position of each "element" just once for each delimiter)
SELECT 1 UNION ALL
SELECT t.N+ Len( #pDelimiter ) FROM cteTally t WHERE SUBSTRING(#pString,t.N, Len( #pDelimiter ) ) = #pDelimiter
),
cteLen(N1,L1) AS(--==== Return start and length (for use in substring)
SELECT s.N1,
ISNULL(NULLIF(CHARINDEX(#pDelimiter,#pString,s.N1),0)-s.N1 ,8000)
FROM cteStart s
)
--===== Do the actual split. The ISNULL/NULLIF combo handles the length for the final element when no delimiter is found.
SELECT ItemNumber = ROW_NUMBER() OVER(ORDER BY l.N1),
Item = SUBSTRING(#pString, l.N1, l.L1)
FROM cteLen l;
(Credit to Jeff Moden for years of successful string splitting.)
Then pick the right substrings to split on:
declare #QandA as NVarChar(1000) = '<b>Q1:</b><br />A1<br /><br /><b>Q2:</b><br />A2<br /><br /><b>Q3:</b><br />A3<br /><br /><b>Q4:</b><br />A4<br /><br />';
-- A single split gets Q/A pairs:
select ItemNumber, Item
from dbo.DelimitedSplit8K( #QandA, '<br /><br />' )
order by ItemNumber;
-- A second split gets Q's and A's:
with QAPairs as (
select ItemNumber as QuestionNumber, Item as QA
from dbo.DelimitedSplit8K( #QandA, '<br /><br />' ) )
select QuestionNumber, QA, ItemNumber, Item, case when ItemNumber % 2 = 1 then 'Q' else 'A' end as 'Q/A'
from QAPairs cross apply
dbo.DelimitedSplit8K( QA, '<br />' );
dbfiddle.
That ought to be a good start. There is a bit of cleanup to do, e.g. there is a spurious empty Q/A pair since the string ends with a '<br /><br />' which, as a delimiter, must mean there is a Q/A pair on each side.
This example retrieves the data from a table a breaks down each row into its component questions and answers:
-- Sample data.
declare #QandAs as Table ( QandAId Int Identity, QandA NVarChar(1000) );
insert into #QandAs ( QandA ) values
( '<b>Q1a:</b><br />A1a<br /><br /><b>Q2a:</b><br />A2a<br /><br /><b>Q3a:</b><br />A3a<br /><br /><b>Q4a:</b><br />A4a<br /><br />' ),
( '<b>Q1b:</b><br />A1b<br /><br /><b>Q2b:</b><br />A2b<br /><br /><b>Q3b:</b><br />A3b<br /><br /><b>Q4b:</b><br />A4b<br /><br />' );
select * from #QandAs;
-- A single split gets Q/A pairs:
with QAPairs as (
select QandAId, ItemNumber, Item, Row_Number() over ( partition by QandAId order by ItemNumber desc ) as RN
from #QandAs cross apply
dbo.DelimitedSplit8K( QandA, '<br /><br />' ) )
select QandAId, ItemNumber, Item, RN
from QAPairs
where RN > 1 -- Eliminate the extraneaous empty Q/A pair at the end of the string.
order by QandAId, ItemNumber;
-- A second split gets Q's and A's:
with QAPairs as (
select QandAId, ItemNumber as QuestionNumber, Item as QA, Row_Number() over ( partition by QandAId order by ItemNumber desc ) as RN
from #QandAs cross apply
dbo.DelimitedSplit8K( QandA, '<br /><br />' ) )
select QandAId, QuestionNumber, QA, ItemNumber, Item, case when ItemNumber % 2 = 1 then 'Q' else 'A' end as 'Q/A'
from QAPairs cross apply
dbo.DelimitedSplit8K( QA, '<br />' )
where RN > 1 -- Eliminate the extraneaous empty Q/A pair at the end of the string.
order by QandAId, QuestionNumber, ItemNumber;
dbfiddle.
I tried this query
DECLARE #AdvancedSearchSelectedDropdownName TABLE (
SelectedIds VARCHAR(2048),
AdvanceSearchOptionTypeId INT
)
INSERT INTO #AdvancedSearchSelectedDropdownName
VALUES ('4_0,5_1,6_2,7_3', 23),
('62_3', 21), ('2_4', 23)
DECLARE #selectedIds VARCHAR(MAX) = '';
SELECT #selectedIds +=
CASE WHEN SelectedIds IS NULL
THEN #selectedIds + ISNULL(SelectedIds + ',', '')
WHEN SelectedIds IS NOT NULL
THEN SUBSTRING(SelectedIds, 0, CHARINDEX('_', SelectedIds, 0)) + ','
END
FROM #AdvancedSearchSelectedDropdownName WHERE advanceSearchOptionTypeId = 23
SELECT #selectedIds
Current output: 4,2
Required output: 4,5,6,7,2
We may have n number of comma separated values in the SelectedIds column.
You might go this route:
WITH Casted AS
(
SELECT *
,CAST('<x><y>' + REPLACE(REPLACE(SelectedIds,'_','</y><y>'),',','</y></x><x><y>') + '</y></x>' AS XML) SplittedToXml
FROM #AdvancedSearchSelectedDropdownName
)
SELECT *
FROM Casted;
This will return your data in this form:
<x>
<y>4</y>
<y>0</y>
</x>
<x>
<y>5</y>
<y>1</y>
</x>
<x>
<y>6</y>
<y>2</y>
</x>
<x>
<y>7</y>
<y>3</y>
</x>
Now we can grab all the x and just the first y:
WITH Casted AS
(
SELECT *
,CAST('<x><y>' + REPLACE(REPLACE(SelectedIds,'_','</y><y>'),',','</y></x><x><y>') + '</y></x>' AS XML) SplittedToXml
FROM #AdvancedSearchSelectedDropdownName
)
SELECT Casted.AdvanceSearchOptionTypeId AS TypeId
,x.value('y[1]/text()[1]','int') AS IdValue
FROM Casted
CROSS APPLY SplittedToXml.nodes('/x') A(x);
The result:
TypeId IdValue
23 4
23 5
23 6
23 7
21 62
23 2
Hint: Do not store comma delimited values!
It is a very bad idea to store your data in this format. You can use a generic format like my XML to store this or a structure of related side tables. But such construction tend to turn out as a real pain in the neck...
After a little re-think. Perhaps something a little more straightforward.
Now, if you have a limited number of _N
Example
;with cte as (
Select *
,RN = Row_Number() over(Order by (Select NULL))
From #AdvancedSearchSelectedDropdownName A
)
Select AdvanceSearchOptionTypeId
,IDs = replace(
replace(
replace(
replace(
replace(
stuff((Select ',' +SelectedIds From cte Where AdvanceSearchOptionTypeId=A.AdvanceSearchOptionTypeId Order by RN For XML Path ('')),1,1,'')
,'_0','')
,'_1','')
,'_2','')
,'_3','')
,'_4','')
From cte A
Group By AdvanceSearchOptionTypeId
Returns
AdvanceSearchOptionTypeId IDs
21 62
23 4,5,6,7,2
If interested in a helper function.
Tired of extracting strings (left, right, charindex, patindex, ...) I modified s split/parse function to accept TWO non-like delimiters. In this case a , and _.
Example
;with cte as (
Select A.AdvanceSearchOptionTypeId
,B.*
,RN = Row_Number() over(Order by (Select NULL))
From #AdvancedSearchSelectedDropdownName A
Cross Apply [dbo].[tvf-Str-Extract](','+A.SelectedIds,',','_') B
)
Select AdvanceSearchOptionTypeId
,IDs = stuff((Select ',' +RetVal From cte Where AdvanceSearchOptionTypeId=A.AdvanceSearchOptionTypeId Order by RN,RetVal For XML Path ('')),1,1,'')
From cte A
Group By AdvanceSearchOptionTypeId
Returns
AdvanceSearchOptionTypeId IDs
21 62
23 4,5,6,7,2
The TVF if Interested
CREATE FUNCTION [dbo].[tvf-Str-Extract] (#String varchar(max),#Delimiter1 varchar(100),#Delimiter2 varchar(100))
Returns Table
As
Return (
with cte1(N) As (Select 1 From (Values(1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) N(N)),
cte2(N) As (Select Top (IsNull(DataLength(#String),0)) Row_Number() over (Order By (Select NULL)) From (Select N=1 From cte1 N1,cte1 N2,cte1 N3,cte1 N4,cte1 N5,cte1 N6) A ),
cte3(N) As (Select 1 Union All Select t.N+DataLength(#Delimiter1) From cte2 t Where Substring(#String,t.N,DataLength(#Delimiter1)) = #Delimiter1),
cte4(N,L) As (Select S.N,IsNull(NullIf(CharIndex(#Delimiter1,#String,s.N),0)-S.N,8000) From cte3 S)
Select RetSeq = Row_Number() over (Order By N)
,RetPos = N
,RetVal = left(RetVal,charindex(#Delimiter2,RetVal)-1)
From (
Select *,RetVal = Substring(#String, N, L)
From cte4
) A
Where charindex(#Delimiter2,RetVal)>1
)
/*
Max Length of String 1MM characters
Declare #String varchar(max) = 'Dear [[FirstName]] [[LastName]], ...'
Select * From [dbo].[tvf-Str-Extract] (#String,'[[',']]')
*/
Disclaimer.As per first Normal form, you should not store multiple values in a single cell. I would suggest you to avoid storing this way.
Still the approach would be: Create a UDF function which separates comma separated list into a table valued variable. Below code I have not tested. but, it gives idea on how to approach this problem.
Refer to CSV to table approaches
Declare #selectedIds varchar(max) = '';
SET #selectedIds = SELECT STUFF
(SELECT ','+ (SUBSTRING(c.value, 0, CHARINDEX('_', c.value, 0))
FROM #AdvancedSearchSelectedDropdownName AS tv
CROSS APPLY dbo.udfForCSVToList(SelectedIds) AS c
WHERE advanceSearchOptionTypeId = 23
FOR XML PATH('')),1,2,'');
SELECT #selectedIds
If I have a table with a column that contains fullnames such as:
fullname
------------
Joe Bloggs
Peter Smith
Mary Jones and Liz Stone
How can I retrieve the first and last name from each of the entries in the full name column using SQL. I'm not worried about the second name in the 3rd entry in my example i.e. Liz Stone.
So basically to retrieve
Firstname
---------
Joe
Peter
Mary
Lastname
--------
Bloggs
Smith
Jones
Here is a pre SQL Server 2016 method, which uses basic string functions to isolate the first and last names.
SELECT SUBSTRING(fullname, 1, CHARINDEX(' ', fullname) - 1) AS Firstname,
SUBSTRING(fullname,
CHARINDEX(' ', fullname) + 1,
LEN(fullname) - CHARINDEX(' ', fullname)) AS Lastname
FROM yourTable
Note that this solution assumes that the fullname column only contains a single first name and a single last name (i.e. no middle names, initials, etc.).
This is a slippery slope and there are no easy answers. That said, consider the following
Declare #YourTable table (FullName varchar(50))
Insert Into #YourTable values
('Joe Bloggs'),
('Peter Smith'),
('Betty Jane Martinez'),
('Mary Jones and Liz Stone')
Select A.*
,FirstName = Pos1+case when Pos3 is not null then ' '+Pos2 else '' end
,LastName = case when Pos3 is null then Pos2 else Pos3 end
From #YourTable A
Cross Apply (
Select Pos1 = xDim.value('/x[1]','varchar(max)')
,Pos2 = xDim.value('/x[2]','varchar(max)')
,Pos3 = xDim.value('/x[3]','varchar(max)')
,Pos4 = xDim.value('/x[4]','varchar(max)')
,Pos5 = xDim.value('/x[5]','varchar(max)')
,Pos6 = xDim.value('/x[6]','varchar(max)')
From (Select Cast('<x>' + replace((Select substring(FullName,1,charindex(' and ',FullName+' and ')-1) as [*] For XML Path('')),' ','</x><x>')+'</x>' as xml) as xDim) as A
) B
Returns
FullName FirstName LastName
Joe Bloggs Joe Bloggs
Peter Smith Peter Smith
Betty Jane Martinez Betty Jane Martinez
Mary Jones and Liz Stone Mary Jones
If it helps with the visual, the CROSS APPLY generates
SELECT CASE
WHEN CHARINDEX(' ', FullName) > 0
THEN SUBSTRING(FullName, 1, LEN(FullName) - CHARINDEX(' ', REVERSE(FullName)))
ELSE ''
END AS FirstName,
CASE
WHEN CHARINDEX(' ', FullName) > 0
THEN REVERSE(SUBSTRING(REVERSE(FullName),
1,
CHARINDEX(' ', REVERSE(FullName)) - 1))
ELSE FullName
END AS LastName
FROM(VALUES('Mary Anne Bloggs'), ('Joe Bloggs'), ('Bloggs')) AS T(FullName);
This version checks that there is a space in the full name to split on. If there isn't then the first name is set to an empty string and the full name is put into the surname. Also, reverse is employed to split on the last space when there is more than one space
I use this query to retrieve first and lastname
SELECT
SUBSTRING(FULLNAME, 1, CASE WHEN CHARINDEX(' ', FULLNAME)>0 THEN CHARINDEX(' ', FULLNAME) - 1 ELSE LEN(FULLNAME) END ) AS Firstname,
REVERSE(SUBSTRING(REVERSE(FULLNAME), 1, CASE WHEN CHARINDEX(' ', REVERSE(FULLNAME))>0 THEN CHARINDEX(' ', REVERSE(FULLNAME)) - 1 ELSE LEN(REVERSE(FULLNAME)) END ) ) AS Firstname
FROM HRMDESFO.EMPLOID
Results
BigQuery: Standard SQL
substr(name,1,STRPOS(name,' ')-1) as FirstName,
substr(name,STRPOS(name,' ')+1,length(name)) as LastName
This is the easiest and shortest to this question without any assumptions. Also you can even further enhance this with a rtrim(ltrim('firstname lastname')).
Just in case of any spaces before the strings,
Select
substring('Firstname Lastname',1,CHARINDEX(' ', 'Firstname Lastname')) as firstname,
substring('Firstname Lastname',CHARINDEX(' ', 'Firstname Lastname'),LEN('Firstname Lastname')) as Lastname
select passemail,substring(passemail,1,instr(passemail,'#') - 1) as name ,
substring(passemail,instr(passemail,'#') + 1,length(passemail)) from passenger
For getting firstName
SELECT SUBSTR(FULLNAME,1,(LOCATE(' ',FULLNAME))) AS FIRSTTNAME from EmployeeDetails;
FOR LASTNAME
SELECT SUBSTR(FULLNAME,(LOCATE(' ',FULLNAME))) AS LASTNAME from EmployeeDetails;
SO
SELECT SUBSTR(FULLNAME,1,(LOCATE(' ',FULLNAME))) AS FIRSTTNAME, SUBSTR(FULLNAME,(LOCATE(' ',FULLNAME))) AS LASTNAME from EmployeeDetails;
SELECT
LEFT(column_name, POSITION(' ' IN column_name)-1) AS first_name,
RIGHT(column_name, LENGTH(column_name) - POSITION(' ' IN column_name)) AS last_name
FROM table_name
SELECT SUBSTRING(candidate_name, 1, CASE WHEN CHARINDEX(' ', candidate_name)>0 THEN CHARINDEX(' ', candidate_name) - 1
ELSE LEN(candidate_name) END ) AS Firstname,
SUBSTRING(substring(candidate_name,CHARINDEX(' ', candidate_name)+1,LEN(candidate_name)), 1,
CASE WHEN CHARINDEX(' ', candidate_name)>1 THEN CHARINDEX(' ', substring(candidate_name,CHARINDEX(' ', candidate_name)+1,LEN(candidate_name)))
ELSE null END ) AS middle_name,
REVERSE(SUBSTRING(REVERSE(candidate_name), 1,
CASE WHEN CHARINDEX(' ', REVERSE(candidate_name))>0
THEN CHARINDEX(' ', REVERSE(candidate_name)) - 1
ELSE null END ) )AS last_name
FROM Test_name
So first we have to find the index for space(" ") because space is the character which is separating the two words ( first_name+" "+last_name).
In my case, its mid_index is a variable that stores the index for space(" ").
SELECT primary_poc, STRPOS(fullname,' ') AS "mid_index"
FROM yourTable_name
*Now we will use min_index to find the left and right sides of words. For this, we can use a subquery.
Below is the final query *
SELECT fullname,
LEFT(fullname, mid_index - 1) AS "first_name",
RIGHT(fullname, LENGTH(primary_poc) - mid_index) AS "last_name"
FROM
(
SELECT primary_poc, STRPOS(fullname,' ') AS "mid_index"
FROM yourTable_name
) AS t1
SELECT
SUBSTR(NAME,1,(LOCATE(NAME, ' '))) AS FIRSTTNAME
, SUBSTR(NAME,(LOCATE(NAME, ' ')+1)) AS LASTNAME
FROM yourTABLE;
FOR SQL SERVER
SELECT
SUBSTRING(fullname, 0, CHARINDEX(' ', fullname)) AS FirstName
,SUBSTRING(fullname, CHARINDEX(' ', fullname), LEN(fullname)) AS LastName
FROM [YourTable]
If your full name has another delimiter aside from space, such as dashes, you substitute in the dash e.g
SELECT
SUBSTRING(fullname, 0, CHARINDEX('-', fullname)) AS FirstName
,SUBSTRING(fullname, CHARINDEX('-', fullname), LEN(fullname)) AS LastName
FROM [YourTable]
In Postgres SQL
SELECT fullname,
SUBSTRING(fullname, 1, POSITION(' ' IN fullname) - 1) as first_name ,
SUBSTRING(fullname,(position(' 'in fullname))) AS lastname from details;
You can use,
STRING_SPLIT (string , separator)
I have an amount field and a commission field that I need to remove the comma: , decimal point: . dash: - and the percent sign: %.
I have tried replicate, format, replace and stuff,
right ('000000000')
right('000000000') + rtrim(field), len#)
RTRIM(replicate('0', 9 - len(field)) + REPLACE(REPLACE(REPLACE(cast(field as varchar), ',', ''), '.',''), '-', ''))
RTRIM(replicate('0', 9 - len(t.Commission_Amount)) + REPLACE(REPLACE(REPLACE(cast(t.Commission_Amount as varchar(9)), ',', ''), '.',''), '-', ''))
but I never get the results that I want. When I use replace it replaces the comma, dash, or % but cuts the field short and does not pad to the left with zeros. I know it's probably right in front of my face I just need some clarity please.
00-126.47 comes out as 0012647
0.00 comes out as 00000000
000126.47 comes out as 00012647
Try this:
create sample table
DECLARE #Table as table (
field varchar(15)
)
populate sample table
INSERT INTO #Table VALUES
('00-126.47'),
('0.00'),
('000126.47'),
('00033%2.422')
select
SELECT field As before,
RIGHT(REPLICATE('0', 9) +
REPLACE(
REPLACE(
REPLACE(
REPLACE(field, '-', '')
, '.', '')
, ',', '')
, '%', '')
, 9) As [After]
FROM #Table
results:
before After
--------------- ---------
00-126.47 000012647
0.00 000000000
000126.47 000012647
00033%2.422 000332422
this solved my problem;
RIGHT(REPLICATE('0', 9) + REPLACE(REPLACE(REPLACE(REPLACE(field, '-', ''), '.', ''), ',', ''), '%', ''), 9) As [After]
I have a sql server 2008 stored proc with joins which returns multiple rows:
default ~/ / NULL NULL NULL Lorem
default ~/ / NULL NULL NULL Ipsum
I would like to return these two rows as one with the last column combined:
default ~/ / NULL NULL NULL Lorem, Ipsum
My Procedure is below
BEGIN
SET NOCOUNT ON;
SELECT
p.display_name AS Name,
p.url AS Url,
ISNULL(REPLACE(parentPage.url,'~','') + parentPage.alias,REPLACE(p.url,'~','')) as ParentPageURL,
p.page_image AS PageImage,
p.synopsis AS Synopsis,
p.metatitle AS Metatitle,
t.tag_name as tag
FROM page p
LEFT JOIN page parentPage on parentPage.id = p.parentid
LEFT JOIN page_features pf on p.id = pf.pageid and (pf.feature_type = 'TEXT' OR pf.feature_type = 'BLOG')
JOIN dbo.tag_collection_tag tc on tc.tag_collection_id = p.tag_collection_id
JOIN dbo.tag t on t.id = tc.tag_id
JOIN dbo.[Split](UPPER(#tags),',') Split on UPPER(t.tag_name) like '%' + split.Data + '%'
WHERE p.status = 'PUBLISHED'
AND p.include_in_nav = 1
AND (p.pagelevel = #level OR #level IS NULL)
AND (p.section_id = #sectionId OR #sectionId IS NULL)
ORDER BY ISNULL(REPLACE(parentPage.url,'~','') + parentPage.alias,REPLACE(p.url,'~',''))
END
Thank you.
Note if you have a column id in table page. It should be used instead of display_name
;WITH a as
(
SELECT
/*p.id,*/
p.display_name AS Name,
p.url AS Url,
ISNULL(REPLACE(parentPage.url,'~','') + parentPage.alias,REPLACE(p.url,'~','')) as ParentPageURL,
p.page_image AS PageImage,
p.synopsis AS Synopsis,
p.metatitle AS Metatitle,
t.tag_name as tag,
ISNULL(REPLACE(parentPage.url,'~','')
+ parentPage.alias,REPLACE(p.url,'~','')) XXX
FROM page p
LEFT JOIN page parentPage on parentPage.id = p.parentid
LEFT JOIN page_features pf on p.id = pf.pageid and (pf.feature_type = 'TEXT' OR pf.feature_type = 'BLOG')
JOIN dbo.tag_collection_tag tc on tc.tag_collection_id = p.tag_collection_id
JOIN dbo.tag t on t.id = tc.tag_id
JOIN dbo.[Split](UPPER(#tags),',') Split on UPPER(t.tag_name) like '%' + split.Data + '%'
WHERE p.status = 'PUBLISHED'
AND p.include_in_nav = 1
AND (p.pagelevel = #level OR #level IS NULL)
AND (p.section_id = #sectionId OR #sectionId IS NULL)
)
SELECT Name, Url, ParentPageURL, PageImage, Synopsis, Metatitle
,STUFF((
select ',' + [tag]
from a t1
-- I assume display_name is unique. I would use page.id,
-- but I am not sure you have that column
-- t1.id = t.id
where t1.display_name = t.display_name
for xml path(''), type
).value('.', 'varchar(max)'), 1, 1, '') [tags]
from a t
group by Name, Url, ParentPageURL, PageImage, Synopsis, Metatitle
ORDER BY XXX