Sql select to string - sql-server

I have a query that returns a result set of one column, but I want it to be converted into one long string for use as a subquery. I.E. I want to be able to do:
SELECT user.*, (SELECT group_name FROM user_groups WHERE userID = user.ID) AS Groups FROM user
However, that will fail because a user can belong to more than one group. For example if the user belong to {"writer", "editor"} I want it to return a string like this: "writer, editor".
How can I do this?

You can use FOR XML to do this pivoting action. Here is a working sample.
set nocount on
declare #Users Table
(
UserId int,
UserName varchar (20)
)
declare #UserGroups Table
(
GroupId int,
UserId int,
GroupName varchar (20)
)
Insert #Users Values (1, 'Tim')
Insert #Users Values (2, 'Jane')
Insert #Users Values (3, 'Hal')
Insert #UserGroups Values (1, 1, 'Admin')
Insert #UserGroups Values (2, 1, 'Power-users')
Insert #UserGroups Values (3, 2, 'Noobs')
Insert #UserGroups Values (4, 2, 'Users')
Insert #UserGroups Values (5, 3, 'Noobs')
/* How this works */
SELECT 'FirstStep : Users table'
SELECT * FROM #Users
SELECT 'NextStep : User Groups table'
SELECT * FROM #UserGroups
SELECT 'NextStep : Users & UserGroups table'
SELECT *
FROM #Users U
INNER JOIN #UserGroups UG ON U.UserId = UG.UserId
SELECT 'NextStep : Just get the groups for one user (UserId = 2)'
SELECT GroupName
FROM #UserGroups UG
WHERE UG.userID = 2
ORDER BY GroupName
SELECT 'NextStep : When you use XML Path the output comes out in XML format'
SELECT GroupName
FROM #UserGroups UG
WHERE UG.userID = 2
ORDER BY GroupName
FOR XML PATH('') -- XML Path added
SELECT 'NextStep : When you remove the column name the XML tags go away,
but it looks ugly because there is no separator'
SELECT GroupName + '' -- Added an empty string to remove the column name
FROM #UserGroups UG
WHERE UG.userID = 2
ORDER BY GroupName
FOR XML PATH('')
SELECT 'NextStep : Add a separator
We add it to the beginning instead of the end so that we can STUFF later on.'
SELECT ', ' + GroupName -- Added an empty string to remove the column name
FROM #UserGroups UG
WHERE UG.userID = 2
ORDER BY GroupName
FOR XML PATH('')
SELECT 'NextStep : I don''t like that ugly XML column name. Let me give it my own name'
SELECT
(
SELECT ', ' + GroupName
FROM #UserGroups UG
WHERE UG.userID = 2
ORDER BY GroupName
FOR XML PATH('')
) as UserGroups
SELECT 'NextStep : STUFF the extra comma'
SELECT STUFF
(
(
SELECT ', ' + GroupName
FROM #UserGroups UG
WHERE UG.userID = 2
ORDER BY GroupName
FOR XML PATH('')
),
1, 2, ''
) as UserGroups
SELECT 'NextStep : Now join it with the Users table by the UserId and get the UserName'
SELECT
U.UserName,
STUFF
(
(
SELECT ', ' + GroupName
FROM #UserGroups UG
WHERE UG.userID = U.userID
ORDER BY GroupName
FOR XML PATH('')
),
1, 2, ''
) as UserGroups
FROM #Users U

Related

SQL Synapse Compare data between Column(multiple values) and Column(multiple values)

I need to compare 2 columns from 2 tables.
table a:
ID|TEL
----------------
A1|1111,2222,3333
TABLE B:
ID|TEL
----------------
A1|2222,4444
Result should update in TABLE A
A1|1111,2222,3333,4444
As I know, maybe I should use select value from string_split (B.Tel,'|') to split it. However, I don't know how to loop to compare between A and B.
Please help.
Here is what I've tried but it's not working.
with split_tel as
(select id,
Value tel
from B
CROSS APPLY STRING_SPLIT(tel, ','))
,pre as (select sp.id
,sp.tel as split
,A.tel as target
from split_tel sp
inner join A
on sp.id = A.id)
select id,split,target
from pre
where split like '%' + target + '%' ;
First of all you should not really be storing your data like this, particularly if you are having to conduct set-based operations on it. However there is a simple solution using STRING_SPLIT and STRING_AGG if you are always effectively adding numbers, not taking away:
IF OBJECT_ID('tempdb..#tmpA') IS NOT NULL DROP TABLE #tmpA
IF OBJECT_ID('tempdb..#tmpB') IS NOT NULL DROP TABLE #tmpB
CREATE TABLE #tmpA (
id VARCHAR(5) NOT NULL,
tel VARCHAR(100) NOT NULL
);
CREATE TABLE #tmpB (
id VARCHAR(5) NOT NULL,
tel VARCHAR(100) NOT NULL
);
--INSERT INTO #tmpA VALUES ( 'A1', '1111,2222,3333' );
--INSERT INTO #tmpB VALUES ( 'A1', '2222,4444' );
INSERT INTO #tmpA
SELECT 'A1', '1111,2222,3333'
UNION ALL
SELECT 'B1', '2222'
INSERT INTO #tmpB
SELECT 'A1', '2222,4444'
UNION ALL
SELECT 'B1', '3333'
SELECT id, STRING_AGG( value, ',' ) tel
FROM
(
SELECT a.id, x.value
FROM #tmpA a
CROSS APPLY STRING_SPLIT( a.tel, ',' ) x
UNION
SELECT b.id, x.value
FROM #tmpB b
CROSS APPLY STRING_SPLIT( b.tel, ',' ) x
) y
GROUP BY id;

Eliminate Duplicates in Concatenated String

I have a working solution for concatenating multiple records into one field, however I would like to eliminate the duplicates from the concatenated field, as well as order the values by another field.
Here is what I have:
CREATE VIEW vwImageDescriptions AS
SELECT i.ItemId, STUFF(ImageDescriptions.line,1,2,'') AS ImageDescriptions
FROM InventoryItems i
CROSS APPLY (
SELECT DISTINCT CAST((select CASE When p.Description = '' or p.Description is null Then '' Else '; ' + p.Description END
FROM Photos p
WHERE p.ItemId = i.ItemId
ORDER BY p.Sequence
FOR XML PATH('')) AS nvarchar(max)) line
) ImageDescriptions
go
select i.ItemName, id.ImageDescriptions
FROM InventoryItems i join vwImageDescriptions id on i.ItemId = id.ItemId
where id.ImageDescriptions like '%pla%'
This is my result:
What I want is to not have duplicated image descriptions so, for instance, I want to only see "My image" once in each row that it occurs, and not twice. As you see, I used DISTINCT in my code but that doesn't seem to be working.
Here is very simplified DDL to see the problem:
DROP TABLE IF EXISTS [InventoryItems]
DROP TABLE IF EXISTS Photos
GO
CREATE TABLE [dbo].[InventoryItems](
[ItemId] [int] NOT NULL,
[ItemName] [varchar](100) NOT NULL
) ON [PRIMARY]
GO
CREATE TABLE [dbo].[Photos](
[ItemId] [int] NOT NULL,
[Description] [varchar](175) NULL,
[Sequence] [int] NOT NULL
) ON [PRIMARY]
GO
INSERT INTO [InventoryItems]([ItemId],[ItemName]) VALUES(311,'11" Round Vegetable Bowl');
INSERT INTO [InventoryItems]([ItemId],[ItemName]) VALUES(312,'13" Oval Serving Platter');
INSERT INTO [InventoryItems]([ItemId],[ItemName]) VALUES(313,'19" Oval Serving Platter');
INSERT INTO [InventoryItems]([ItemId],[ItemName]) VALUES(314,'Creamer');
INSERT INTO [InventoryItems]([ItemId],[ItemName]) VALUES(315,'Gravy Boat with Attached Underplate');
INSERT INTO [InventoryItems]([ItemId],[ItemName]) VALUES(317,'Round Butter Dish and Lid');
INSERT INTO [InventoryItems]([ItemId],[ItemName]) VALUES(318,'Sugar Bowl and Lid');
INSERT INTO [InventoryItems]([ItemId],[ItemName]) VALUES(319,'Vegetable Server Bowl and Lid');
INSERT INTO [Photos]([ItemId],[Description],[Sequence])VALUES(311,'',1)
INSERT INTO [Photos]([ItemId],[Description],[Sequence])VALUES(312,'Replacements.com image',1)
INSERT INTO [Photos]([ItemId],[Description],[Sequence])VALUES(313,'Replacements.com image',1)
INSERT INTO [Photos]([ItemId],[Description],[Sequence])VALUES(313,'Outer platter is the 19"',2)
INSERT INTO [Photos]([ItemId],[Description],[Sequence])VALUES(313,'Outer platter is the 19"',3)
INSERT INTO [Photos]([ItemId],[Description],[Sequence])VALUES(313,'Outer platter is the 19"',4)
INSERT INTO [Photos]([ItemId],[Description],[Sequence])VALUES(313,'Another image',5)
INSERT INTO [Photos]([ItemId],[Description],[Sequence])VALUES(314,'Replacements.com image',1)
INSERT INTO [Photos]([ItemId],[Description],[Sequence])VALUES(315,'From replacements.com',1)
INSERT INTO [Photos]([ItemId],[Description],[Sequence])VALUES(315,'My image',2)
INSERT INTO [Photos]([ItemId],[Description],[Sequence])VALUES(315,'My image',3)
INSERT INTO [Photos]([ItemId],[Description],[Sequence])VALUES(315,'My image',4)
INSERT INTO [Photos]([ItemId],[Description],[Sequence])VALUES(316,'My image',1)
INSERT INTO [Photos]([ItemId],[Description],[Sequence])VALUES(316,'My image',2)
INSERT INTO [Photos]([ItemId],[Description],[Sequence])VALUES(316,'From replacements.com',3)
I think this can be simplified a bit. As I understand the desired results this should produce what you are looking for. Many thanks for the tables and sample data.
select i.ItemId
, ImageDescriptions = isnull(STUFF((select ',' + isnull(p.Description, '')
from Photos p
where p.ItemId = i.ItemId
group by isnull(p.Description, '')
order by min(p.Sequence)
for xml path('')), 1, 1, ''), '')
FROM InventoryItems i
group by i.ItemId
--EDIT--
Like I said the CROSS APPLY is a layer of complexity not needed here. But this should do the same thing.
select i.ItemId
, ImageDescriptions = isnull(x.asdf, '')
FROM InventoryItems i
cross apply
(
select asdf = STUFF((select ',' + isnull(p.Description, '')
from Photos p
where p.ItemId = i.ItemId
group by isnull(p.Description, '')
order by min(p.Sequence)
for xml path('')), 1, 1, '')
) x
I usually write like below:
CREATE VIEW vwImageDescriptions AS
SELECT i.ItemId, STUFF(ImageDescriptions.line,1,1,'') AS ImageDescriptions
FROM InventoryItems i
CROSS APPLY (
SELECT DISTINCT '; ' + ISNULL(p.Description,'')
FROM Photos p
WHERE p.ItemId = i.ItemId
ORDER BY p.Sequence
FOR XML PATH('')
) ImageDescriptions(line)
GO

How to Sum value of Pivoted Columns and add it into another Pivoted Column

I want to sum up all the CountHours and display in pivoted column Total.Total is the sum of all SUnday , monday ... for particular UserName. How to achieve this
?
select FullName,Sunday,Monday,Tuesday,Wednesday,Thursday,Friday,Saturday,Total
from
(Select UserId_Fk,ISNULL(CAST(CountHours as decimal(18,2)),0)as CountHours,[Day] f rom CheckInCheckOut)
as convertedtable
inner join Users
on convertedtable.UserId_Fk=Users.UserId
PIVOT
(
SUM(CountHours)
FOR Day
IN([Sunday],[Monday],[Tuesday],[Wednesday],[Thursday],[Friday],[Saturday],[Total])
)
as PivotTable
Result of this query is:
Table Structure:
Table[CheckInCheckOut]
CheckInCheckOutId int
UserId_Fk int
CountHours nvarchar(50)
Day nvarchar(50)
Example would be appreciated.
you should calculate total column field, i.e it is not in list of pivot columns.
Data
create table #CheckInCheckOut(Id int identity(1,1),UserId_Fk int,CountHours varchar(50),[Day] varchar(50))
INSERT INTO #CheckInCheckOut(UserId_Fk,CountHours,[Day]) VALUES
(1,'2','Sunday'),(1,'2','Monday'),(1,'2','Tuesday'),(1,'2','Wednesday'),(1,'2','Thursday'),(1,'2','Friday'),(1,'2','Saturday')
,(2,'3','Sunday'),(2,'3','Monday'),(2,'3','Tuesday'),(2,'3','Wednesday'),(2,'3','Thursday'),(2,'3','Friday'),(2,'3','Saturday')
,(3,'3','Sunday'),(3,'3','Monday'),(3,'3','Tuesday'),(3,'3','Wednesday'),(3,'3','Thursday'),(3,'3','Friday'),(3,'3','Saturday')
create table #Users(UserId int identity(1,1),FullName varchar(50))
INSERT #Users(FullName) values('Abdul'),('khan'),('Tariq')
Query to find total too:
select FullName
,[Sunday] = SUM([Sunday])
,[Monday] = SUM([Monday])
,[Tuesday] = SUM([Tuesday])
,[Wednesday] = SUM([Wednesday])
,[Thursday] = SUM([Thursday])
,[Friday] = SUM([Friday])
,[Saturday] = SUM([Saturday])
, Total= SUM([Sunday]+[Monday]+[Tuesday]+[Wednesday]+[Thursday]+[Friday]+[Saturday])
from
(Select UserId_Fk,ISNULL(CAST(CountHours as decimal(18,2)),0)as CountHours,[Day]
from #CheckInCheckOut)
as convertedtable
inner join #Users
on convertedtable.UserId_Fk=#Users.UserId
PIVOT
(
SUM(CountHours)
FOR Day
IN([Sunday],[Monday],[Tuesday],[Wednesday],[Thursday],[Friday],[Saturday])
)
as PivotTable
GROUP BY FullName
Output
Also if u want total horizontal and vertical both then replace:
--GROUP BY FullName
GROUP BY ROLLUP(FullName);
For more follow link https://stackoverflow.com/a/17142530/1915855
DROP TABLE #CheckInCheckOut
DROP TABLE #Users
Try This Way here is the example.
Create a Table
CREATE TABLE cars
(
car_id tinyint,
attribute varchar(20),
value varchar(20),
sumd decimal(18,2)
)
Insert Values to it
insert into cars(car_id, attribute, value, sumd)
values (1, 'Make', 'VW',1),
(1, 'Model', 'Rabbit',2),
(1, 'Color', 'Gold',3),
(2, 'Make', 'Jeep',4),
(2, 'Model', 'Wrangler',5),
(2, 'Color', 'Gray',6)
For Making the Total
declare #Columns2 VARCHAR(8000)
declare #Sql VARCHAR(4000)
declare #Columns VARCHAR(8000)
SET #Columns = substring((select distinct ',['+attribute+']' from cars group by attribute for xml path('')),2,8000)
SET #Columns2 = substring((select distinct ',IsNull(['+attribute+'],0) as ['+attribute+']' from cars group by attribute for xml path('')),2,8000)
print #Columns
print #Columns2
SET #SQL = 'SELECT car_id, '+#Columns2+', total
FROM
(Select car_id,attribute, SUM(sumd) OVER (PARTITION BY attribute) as total
, sumd from cars) SourceData
PIVOT
(sum(sumd) for attribute in ('+#Columns+')) pivottable
Order by car_id '
exec(#sql)

String Replace column data with data from another table

I have two tables:
TableA:
ID Values
---------------
1 Q
2 B
3 TA
4 BS
TableB:
RawValue Value
------------------
[1][4] QBS
[2][1][3] BQTA
I need to generate TableB values with its given RawValues. each [X] in rawvalue is the ID coulmn of TableA and shoud be replace with its value .
[1][4] means that Value of TableA with has ID of 1 (Q) and Value of TableA with has ID of 4 (BS) then should equal to QBS.
can anyone suggest a way to do it?
this is what I have already tried:
update tableb set value=replace(rawvalue,'[' + (select id from tablea where id = cast(replace(replace(rawdata,'[',''),']','') as int)) + ']',
(select values from tablea where id = cast(replace(replace(rawdata,'[',''),']','') as int)))
By the way: this is still in test process and I can totally change tables, rowvalue format and replacement methods if anyone has a better idea.
declare #tableA table (id int, value varchar(50))
insert into #tableA (id, value)
select 1, 'Q' union all
select 2, 'B' union all
select 3, 'TA' union all
select 4, 'BS'
declare #tableB table (rawdata varchar(255), value varchar(255))
insert into #tableB (rawdata)
select '[1][4]' union all -- QBS
select '[2][1][3]' -- BQTA
update b
set value = (
select a.value + ''
from #tableA a
cross apply (select charindex ('[' + cast (a.id as varchar(50)) + ']', b.rawdata) as pos) p
where pos > 0
order by pos
for xml path('')
)
from #tableB b
select * from #tableB
P.S. I would recommend not to name field similar to reserved keywords (I mean Values).
Turn RawValue into XML, shred the XML to get one row for each value in RawValue and join to TableA to get the value.
Use the for xml path() trick to concatenate the values from TableA.
update TableB
set Value = (
select T.Value as '*'
from (
select row_number() over(order by T2.X) as SortOrder,
TableA.Value
from (select cast(replace(replace(TableB.RawValue, '[', '<x>'), ']', '</x>') as xml)) as T1(X)
cross apply T1.X.nodes('x') as T2(X)
inner join TableA
on TableA.ID = T2.X.value('text()[1]', 'int')
) as T
order by T.SortOrder
for xml path('')
)
SQL Fiddle

t-sql: dynamically filter XML on multiple conditions?

I'm trying to find a way to do a accept/reject on an XML string, by joining it to a table of conditions. I have one "filter" working now, but want to write it so that it can filter 2 or more.
Here's code that matches one of the two. If either matches, it will filter the string.
What I want to do is make it so it has to match BOTH, while still leaving the option for single-condition
CREATE TABLE #filter (exclusion_type CHAR(1), excluded_value varchar(10))
INSERT INTO #filter VALUES ('B','boy')
INSERT INTO #filter VALUES ('C','cat')
DECLARE #data XML
SELECT #data = '<A><B>boy</B><C>cat</C></A>'
SELECT * FROM (SELECT CONVERT(VARCHAR(128),node.query('fn:local-name(.)')) AS NodeName, CONVERT(VARCHAR(MAX),node.query('./text()')) AS NodeValue
FROM #data.nodes(N'//*') T(node))xml_shred
IF NOT EXISTS
(SELECT * FROM (SELECT CONVERT(VARCHAR(128),node.query('fn:local-name(.)')) AS NodeName, CONVERT(VARCHAR(MAX),node.query('./text()')) AS NodeValue
FROM #data.nodes(N'//*') T(node)) xml_shred
INNER JOIN #filter
ON (nodename = exclusion_type AND nodevalue LIKE excluded_value)
)
select 'record would be inserted '
ELSE select 'record was filtered'
Here's how I currently have it to filter both. Ugly and non-expandable.
IF NOT EXISTS
(SELECT * FROM (SELECT CONVERT(VARCHAR(128),node.query('fn:local-name(.)')) AS NodeName, CONVERT(VARCHAR(MAX),node.query('./text()')) AS NodeValue
FROM #data.nodes(N'//*') T(node)) xml_shred
INNER JOIN #filter
ON (nodename = exclusion_type AND nodevalue LIKE excluded_value)
)
--combination filters don't easily work within that xml_shred
and not(
#data.value('(/A/B)[1]', 'varchar(128)') = 'boy'
AND
#data.value('(/A/C)[1]', 'varchar(128)')='cat'
)
select 'record would be inserted '
ELSE select 'record was filtered'
My only other ideas:
some sort of GUID that would link records in the #filter table together, and then inner join on a GROUP BY of #filtertable, grouping by the GUID and using the SUM to match the number of records.
use semicolons to split the #filter rows, then use a CTE or something to fake a hierarchy and work from there.
Code changes made by Mikael's suggestion
CREATE TABLE #filter
(
exclusion_set SMALLINT,
exclusion_type CHAR(1) ,
excluded_value VARCHAR(10)
)
INSERT INTO #filter
VALUES (1, 'B', 'boy')
INSERT INTO #filter
VALUES (1, 'C', 'cat')
INSERT INTO #filter
VALUES (2, 'D', 'dog' )
DECLARE #data XML
SELECT #data = '<A><B>boy</B><C>cat</C></A>'
IF NOT EXISTS(
SELECT * FROM
(
select COUNT(*) AS match_count, exclusion_set
from #filter as F
where exists (
select *
from (
select X.N.value('local-name(.)', 'varchar(128)') as NodeName,
X.N.value('./text()[1]', 'varchar(max)') as NodeValue
from #data.nodes('//*') as X(N)
) T
where T.NodeName = F.exclusion_type and
T.NodeValue like F.excluded_value
)
GROUP BY exclusion_set
) matches_per_set
INNER JOIN
(SELECT COUNT(*) AS total_count, exclusion_set FROM #filter GROUP BY exclusion_set) grouped_set
ON match_count = total_count
AND grouped_set.exclusion_set = matches_per_set.exclusion_set
)
if not exists (
select *
from #filter as F
where exists (
select *
from (
select X.N.value('local-name(.)', 'varchar(128)') as NodeName,
X.N.value('./text()[1]', 'varchar(max)') as NodeValue
from #data.nodes('//*') as X(N)
) T
where T.NodeName = F.exclusion_type and
T.NodeValue like F.excluded_value
)
having count(*) = (select count(*) from #filter)
)
select 'record would be inserted '
else
select 'record was filtered'
Since I apparently get dinged if I don't mark something as the answer, I'm including mine from above. Many thanks for the help to Mikael Eriksson. His XML shred is faster than mine, and by adding the "exclusion_set" field (char(2) to make it obvious that it wasn't an IDENTITY or primary key), I can do multiple checks. If all conditions in a set match, then the record is filtered.
CREATE TABLE #filter
(
exclusion_set CHAR(2),
exclusion_type CHAR(1) ,
excluded_value VARCHAR(10)
)
INSERT INTO #filter
VALUES ('aa', 'B', 'boy')
INSERT INTO #filter
VALUES ('aa', 'C', 'cat')
INSERT INTO #filter
VALUES ('ab', 'D', 'dog' )
DECLARE #data XML
SELECT #data = '<A><B>boy</B><C>cat</C></A>'
IF NOT EXISTS(
SELECT * FROM
(
select COUNT(*) AS match_count, exclusion_set
from #filter as F
where exists (
select *
from (
select X.N.value('local-name(.)', 'varchar(128)') as NodeName,
X.N.value('./text()[1]', 'varchar(max)') as NodeValue
from #data.nodes('//*') as X(N)
) T
where T.NodeName = F.exclusion_type and
T.NodeValue like F.excluded_value
)
GROUP BY exclusion_set
) matches_per_set
INNER JOIN
(SELECT COUNT(*) AS total_count, exclusion_set FROM #filter GROUP BY exclusion_set) grouped_set
ON match_count = total_count
AND grouped_set.exclusion_set = matches_per_set.exclusion_set
)
select 'record would be inserted '
else
select 'record was filtered'

Resources