SQL to Make XML Path list Columns - sql

I have the following SQL Server query which cranks out a comma delimited list into one field.
Result looks like this 2003, 9083, 4567, 3214
Question: What would be the best way (SQL syntax) to put this into columns?
Meaning, I need these to show up as 1 column for "2003", 1 column for "9083" 1 column, for "4567" ..etc.
Obviously the number of columns would be dynamic based on the policy ID I give it . Any idea would be most appreciated.
My query is below .
SELECT DISTINCT x.ClassCode + ', '
FROM PremByClass x
WHERE x.PolicyId = 1673885
FOR XML PATH('')

If you take out the XML and the comma you are left with
SELECT DISTINCT x.ClassCode
FROM PremByClass x
WHERE x.PolicyId = 1673885
Which gives you a single column of the values, to turn this into columns you need to PIVOT it. However, you need to specify the names of the columns.
There is some more information in this answer https://stackoverflow.com/a/15931734/350188

You need PIVOT and if number of values could be different - dynamic SQL:
SELECT *
FROM (
SELECT DISTINCT ClassCode,
ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) RN
FROM PremByClass
WHERE PolicyId = 1673885
) as t
PIVOT (
MAX(ClassCode) FOR RN IN ([1],[2],[3],[4])
) as pvt
Will give you:
1 2 3 4
-----------------------------
2003 9083 4567 3214
Dynamic SQL will be something like:
DECLARE #sql nvarchar(max),
#columns nvarchar(max)
SELECT #columns = STUFF((
SELECT DISTINCT ','+QUOTENAME(ROW_NUMBER() OVER (ORDER BY (SELECT NULL)))
FROM PremByClass
WHERE PolicyId = 1673885
FOR XML PATH('')
),1,1,'')
SELECT #sql = N'
SELECT *
FROM (
SELECT DISTINCT ClassCode,
ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) RN
FROM PremByClass
WHERE PolicyId = 1673885
) as t
PIVOT (
MAX(ClassCode) FOR RN IN ('+#columns+')
) as pvt'
EXEC sp_executesql #sql

Assuming that there's a limit to the amount of numbers in that csv string.
You could cast or convert it to an xml type, and then put the values in as many columns you expect.
In this example it's assumed that there's no more than 6 values in the text:
declare #PolicyId INT = 1673885;
select PolicyId
,x.value('/x[1]','int') as n1
,x.value('/x[2]','int') as n2
,x.value('/x[3]','int') as n3
,x.value('/x[4]','int') as n4
,x.value('/x[5]','int') as n5
,x.value('/x[6]','int') as n6
from (
select
PolicyId,
cast('<x>'+replace(ClassCode,',','</x><x>')+'</x>' as xml) as x
from PremByClass
where PolicyId = #PolicyId
) q;

Related

Divided by some number into column SQL

Well, I have a number which stored in a column. If I want to divide the value, I just need:
select columnName / 12 from myTable
Is it possible to put the result into 12 column? I want to make the 12 is flexible. So for instance, if I divide the value by 4, so the result should be 4 column.
Value Result1 Result2 Result3 Result4
12000 3000 3000 3000 3000
Does anyone know how to achieve this?
Thank you.
This achievable using dynamic query.
declare #cols nvarchar(max);
declare #sql nvarchar(1000);
with cte as (
select 12000 as col1, 12000/4 as col2
union all
select col1-col2, col2 from cte where col1 > col2
)
select #cols =
STUFF((select N',' + QUOTENAME(col2) from cte
FOR XML PATH('')
), 1, 1, '') + N'';
select #cols
You can avoid dynamic SQL if you know a maximum count:
Start with a mockup-table to simulate your issue
DECLARE #tbl TABLE(InitialValue DECIMAL(16,4));
INSERT INTO #tbl VALUES(12000),(20000),(10000);
--To test this I use a divisor of 4, try with other numbers
DECLARE #divisor DECIMAL(16,4)=4;
--This is the query
SELECT p.*
FROM
(
SELECT t.InitialValue / #divisor As DivResult
,t.InitialValue
,CONCAT('div',FORMAT(A.Nmbr,'00')) AS ColumnName
FROM #tbl t
CROSS APPLY(SELECT TOP(CAST(#divisor AS INT)) ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) FROM master..spt_values) A(Nmbr)
) t
PIVOT
(MAX(DivResult) FOR ColumnName IN(div01,div02,div03,div04,div05,div06,div07 /*add as many as you might need*/)) p;
The result
InitialValue div01 div02 div03 div04 div05 div06 div07
10000.0000 2500.000000000000000000000 2500.000000000000000000000 2500.000000000000000000000 2500.000000000000000000000 NULL NULL NULL
12000.0000 3000.000000000000000000000 3000.000000000000000000000 3000.000000000000000000000 3000.000000000000000000000 NULL NULL NULL
20000.0000 5000.000000000000000000000 5000.000000000000000000000 5000.000000000000000000000 5000.000000000000000000000 NULL NULL NULL
As you can see, the unused columns are returned but stay NULL.
I'd prefer this approach over dynamic sql as the consumer is better of in most cases if the result set and its structure is fixed and predictable...
Hint: You can add the divisor to your result set if needed...
It has to be dynamic query
Check below
Create TABLE Table1 (Origional int)
Declare #DivisonValue INT = 4
insert into Table1
VALUES (12000)
--Change the Top (12) to you what ever number you like. this will be your total number of columns
DECLARE #Columns VARCHAR(MAX) = (SELECT
',' + C.ColumnName + ' = Origional / ' + CAST(#DivisonValue AS VARCHAR(5))
/* Above line which you need to change for division */
FROM
(SELECT TOP (#DivisonValue)
ColumnId = ROW_NUMBER() OVER (ORDER BY s1.[object_id])
,ColumnName ='Result' + CONVERT(VARCHAR(10), ROW_NUMBER() OVER (ORDER BY s1.[object_id]))
FROM sys.all_objects AS s1 CROSS JOIN sys.all_objects AS s2
) AS C
ORDER BY
C.ColumnId
FOR XML PATH (''))
DECLARE #FullQuery VARCHAR(MAX) = 'SELECT Origional,'+ substring(#Columns,2,LEN(#Columns)-1) + ' FROM Table1'
EXEC (#FullQuery)
DROP TABLE table1
GO
Change the logic as per your need for division.

Order Concatenated field

I have a field which is a concatenation of single letters. I am trying to order these strings within a view. These values can't be hard coded as there are too many. Is someone able to provide some guidance on the function to use to achieve the desired output below? I am using MSSQL.
Current output
CustID | Code
123 | BCA
Desired output
CustID | Code
123 | ABC
I have tried using a UDF
CREATE FUNCTION [dbo].[Alphaorder] (#str VARCHAR(50))
returns VARCHAR(50)
BEGIN
DECLARE #len INT,
#cnt INT =1,
#str1 VARCHAR(50)='',
#output VARCHAR(50)=''
SELECT #len = Len(#str)
WHILE #cnt <= #len
BEGIN
SELECT #str1 += Substring(#str, #cnt, 1) + ','
SET #cnt+=1
END
SELECT #str1 = LEFT(#str1, Len(#str1) - 1)
SELECT #output += Sp_data
FROM (SELECT Split.a.value('.', 'VARCHAR(100)') Sp_data
FROM (SELECT Cast ('<M>' + Replace(#str1, ',', '</M><M>') + '</M>' AS XML) AS Data) AS A
CROSS APPLY Data.nodes ('/M') AS Split(a)) A
ORDER BY Sp_data
RETURN #output
END
This works when calling one field
ie.
Select CustID, dbo.alphaorder(Code)
from dbo.source
where custid = 123
however when i try to apply this to top(10) i receive the error
"Invalid length parameter passed to the LEFT or SUBSTRING function."
Keeping in mind my source has ~4million records, is this still the best solution?
Unfortunately i am not able to normalize the data into a separate table with records for each Code.
This doesn't rely on a id column to join with itself, performance is almost as fast
as the answer by #Shnugo:
SELECT
CustID,
(
SELECT
chr
FROM
(SELECT TOP(LEN(Code))
SUBSTRING(Code,ROW_NUMBER() OVER(ORDER BY (SELECT NULL)),1)
FROM sys.messages) A(Chr)
ORDER by chr
FOR XML PATH(''), type).value('.', 'varchar(max)'
) As CODE
FROM
source t
First of all: Avoid loops...
You can try this:
DECLARE #tbl TABLE(ID INT IDENTITY, YourString VARCHAR(100));
INSERT INTO #tbl VALUES ('ABC')
,('JSKEzXO')
,('QKEvYUJMKRC');
--the cte will create a list of all your strings separated in single characters.
--You can check the output with a simple SELECT * FROM SeparatedCharacters instead of the actual SELECT
WITH SeparatedCharacters AS
(
SELECT *
FROM #tbl
CROSS APPLY
(SELECT TOP(LEN(YourString)) ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) FROM master..spt_values) A(Nmbr)
CROSS APPLY
(SELECT SUBSTRING(YourString,Nmbr,1))B(Chr)
)
SELECT ID,YourString
,(
SELECT Chr As [*]
FROM SeparatedCharacters sc1
WHERE sc1.ID=t.ID
ORDER BY sc1.Chr
FOR XML PATH(''),TYPE
).value('.','nvarchar(max)') AS Sorted
FROM #tbl t;
The result
ID YourString Sorted
1 ABC ABC
2 JSKEzXO EJKOSXz
3 QKEvYUJMKRC CEJKKMQRUvY
The idea in short
The trick is the first CROSS APPLY. This will create a tally on-the-fly. You will get a resultset with numbers from 1 to n where n is the length of the current string.
The second apply uses this number to get each character one-by-one using SUBSTRING().
The outer SELECT calls from the orginal table, which means one-row-per-ID and use a correalted sub-query to fetch all related characters. They will be sorted and re-concatenated using FOR XML. You might add DISTINCT in order to avoid repeating characters.
That's it :-)
Hint: SQL-Server 2017+
With version v2017 there's the new function STRING_AGG(). This would make the re-concatenation very easy:
WITH SeparatedCharacters AS
(
SELECT *
FROM #tbl
CROSS APPLY
(SELECT TOP(LEN(YourString)) ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) FROM master..spt_values) A(Nmbr)
CROSS APPLY
(SELECT SUBSTRING(YourString,Nmbr,1))B(Chr)
)
SELECT ID,YourString
,STRING_AGG(sc.Chr,'') WITHIN GROUP(ORDER BY sc.Chr) AS Sorted
FROM SeparatedCharacters sc
GROUP BY ID,YourString;
Considering your table having good amount of rows (~4 Million), I would suggest you to create a persisted calculated field in the table, to store these values. As calculating these values at run time in a view, will lead to performance problems.
If you are not able to normalize, add this as a denormalized column to the existing table.
I think the error you are getting could be due to empty codes.
If LEN(#str) = 0
BEGIN
SET #output = ''
END
ELSE
BEGIN
... EXISTING CODE BLOCK ...
END
I can suggest to split string into its characters using referred SQL function.
Then you can concatenate string back, this time ordered alphabetically.
Are you using SQL Server 2017? Because with SQL Server 2017, you can use SQL String_Agg string aggregation function to concatenate characters splitted in an ordered way as follows
select
t.CustId, string_agg(strval, '') within GROUP (order by strval)
from CharacterTable t
cross apply dbo.SPLIT(t.code) s
where strval is not null
group by CustId
order by CustId
If you are not working on SQL2017, then you can follow below structure using SQL XML PATH for concatenation in SQL
select
CustId,
STUFF(
(
SELECT
'' + strval
from CharacterTable ct
cross apply dbo.SPLIT(t.code) s
where strval is not null
and t.CustId = ct.CustId
order by strval
FOR XML PATH('')
), 1, 0, ''
) As concatenated_string
from CharacterTable t
order by CustId

SQL Server Loop thru rows to form Groups

I using SQL Server 2008 R2 / 2014. I wish to find a SQL query that can do the following:
Rules:
Each [Group] must have [Number] 1 to 6 to be complete group.
[Name] in each [Group] must be unique.
Each row only can use 1 time.
Table before sorting is...
Name Number Group
---- ------ -----
A 1
B 6
A 123
C 3
B 4
C 23
D 45
D 4
C 56
A 12
D 56
After sorting, result I want is below or similar....
Name Number Group
---- ------ -----
A 1 1
C 23 1
D 45 1
B 6 1
A 123 2
D 4 2
C 56 2
A 12 3
C 3 3
B 4 3
D 56 3
What I tried before is to find a subgroup that have [Number] consist of 1-6 with below concatenate method...
SELECT *
FROM [Table1] ST2
WHERE
SUBSTRING((SELECT ST1.[Number] AS [text()]
FROM [Table1] ST1
-- WHERE ST1.[Group] = ST2.[Group]
ORDER BY LEFT(ST1.[Number],1)
FOR XML PATH ('')), 1, 1000) = '123456'
Maybe you should check ROW_NUMBER function.
select Name
, Number
, ROW_NUMBER () OVER(PARTITION BY Name ORDER BY Number) as Group
from [Table1]
If you have more than 6 rows with same NAME value then it will return more groups. You can filter additional groups out since you are interested in only 6 groups with unique values of NAME column.
I'm not sure if this can be done more simply or not, but here's my go at it...
Advanced warning, this requires some means of splitting strings. Since you're not on 2016, I've included a function at the beginning of the script.
The bulk of the work is a recursive CTE that builds the Name and Number columns into comma delimited groups. We then reduce our working set to only the groups where the numbers would create 123456, split the groups and use ROW_NUMBER() OVER... to identify them, and then select based on the new data.
Demo: http://rextester.com/NEXG53500
CREATE FUNCTION [dbo].[SplitStrings]
(
#List NVARCHAR(MAX),
#Delimiter NVARCHAR(255)
)
RETURNS TABLE
WITH SCHEMABINDING
AS
RETURN
(
SELECT Item = y.i.value('(./text())[1]', 'nvarchar(4000)')
FROM
(
SELECT x = CONVERT(XML, '<i>'
+ REPLACE(#List, #Delimiter, '</i><i>')
+ '</i>').query('.')
) AS a CROSS APPLY x.nodes('i') AS y(i)
);
GO
CREATE TABLE #temp
(
name VARCHAR(MAX),
number INT
)
INSERT INTO #temp
VALUES
('a',1),
('b',6),
('a',123),
('c',3),
('b',4),
('c',23),
('d',45),
('d',4),
('c',56),
('a',12),
('d',56);
/*** Recursively build groups based on information from #temp ***/
WITH groupFinder AS
(
SELECT CAST(name AS VARCHAR(MAX)) AS [groupNames], CAST(number AS VARCHAR(max)) AS [groupNumbers] FROM #temp
UNION ALL
SELECT
cast(CONCAT(t.[Name],',',g.[groupNames]) as VARCHAR(MAX)),
CAST(CONCAT(CAST(t.[Number] AS VARCHAR(max)),',',CAST(g.[groupNumbers] AS VARCHAR(max))) AS VARCHAR(max))
FROM #temp t
JOIN groupFinder g
ON
g.groupNames NOT LIKE '%' + t.name+'%'
AND g.[groupNumbers] NOT LIKE '%' + CAST(t.number/100 AS VARCHAR(10)) +'%'
AND g.[groupNumbers] NOT LIKE '%' + CAST(t.number/10 AS VARCHAR(10)) +'%'
AND g.[groupNumbers] NOT LIKE '%' + CAST(t.number%10 AS VARCHAR(10)) +'%'
)
/*** only get groups where the numbers form 123456 ***/
, groupPruner AS
(
SELECT *, ROW_NUMBER() OVER (ORDER BY [groupNames]) AS [rn] FROM groupFinder WHERE REPLACE([groupNumbers],',','') = '123456'
)
/*** split the name group and give it identifiers ***/
, nameIdentifier AS
(
SELECT g.*, c1.[item] AS [Name], ROW_NUMBER() OVER (PARTITION BY [rn] ORDER BY (SELECT NULL)) AS [rn1]
FROM groupPruner g
CROSS APPLY splitstrings(g.groupnames,',') c1
)
/*** split the number group and give it identifiers ***/
, numberIdentifier AS
(
SELECT g.*, c1.[item] AS [Number], ROW_NUMBER() OVER (PARTITION BY [rn], [rn1] ORDER BY (SELECT NULL)) AS [rn2]
FROM nameIdentifier g
CROSS APPLY splitstrings(g.groupNumbers,',') c1
)
SELECT [Name], [Number], [rn] AS [Group]
--,groupnames, groupNumbers /*uncomment this line to see the groups that were built*/
FROM numberIdentifier
WHERE rn1 = rn2
ORDER BY rn, rn1
DROP TABLE #temp

SQL Server - Dynamic Pivot with 2 Group Variables and 2 Aggregate Calculations

I have a dataset that is shaped like this:
I am trying to convert the data to this format:
As you can see, I'd like to sum the accounts and revenue (for each month) by State and Account Type. It is important to note that I seek a dynamic solution as these ARE NOT the only values (hard-coding is not an option!).
What SQL query can I write to accomplish this task, dynamically? (as these values are not the only ones present in the complete dataset).
Thanks!
I'm assuming you want to keep the columns in order by date, thus the top 100 percent ... order by in the section where we generate the columns
Example
Declare #SQL varchar(max) = '
Select *
From (
Select [State]
,[AccountType]
,B.*
From YourTable A
Cross Apply ( values (concat(''Accounts_'',format([Date],''MM/dd/yyyy'')),Accounts)
,(concat(''Revenue_'' ,format([Date],''MM/dd/yyyy'')),Revenue)
) B (Item,Value)
) A
Pivot (sum([Value]) For [Item] in (' + Stuff((Select ','+QuoteName('Accounts_'+format([Date],'MM/dd/yyyy'))
+','+QuoteName('Revenue_' +format([Date],'MM/dd/yyyy'))
From (Select top 100 percent [Date] from YourTable Group By [Date] Order by [Date] ) A
For XML Path('')),1,1,'') + ') ) p'
--Print #SQL
Exec(#SQL)
Returns
If it helps, the generated SQL looks like this:
Select *
From (
Select [State]
,[AccountType]
,B.*
From YourTable A
Cross Apply ( values (concat('Accounts_',format([Date],'MM/dd/yyyy')),Accounts)
,(concat('Revenue_' ,format([Date],'MM/dd/yyyy')),Revenue)
) B (Item,Value)
) A
Pivot (sum([Value]) For [Item] in ([Accounts_12/31/2017],[Revenue_12/31/2017],[Accounts_01/31/2018],[Revenue_01/31/2018]) ) p

Need to Pivot String values in SQL server

I have table with values, described as:
Occupation String
Name String
Developer
A
Developer
B
Designer
X
Coder
Y
Coder
Z
I need values in pivot format as:
Designer
Developer
Coder
X
A
Y
Null
B
Z
Can anyone help on this ?
Thanks in advance
The basic PIVOT with ROW_NUMBER() will do things for you:
SELECT [Developer],
[Designer],
[Coder]
FROM (
SELECT *,
ROW_NUMBER() OVER (PARTITION BY Occupation ORDER BY (SELECT NULL)) RN
FROM #temp
) as t
PIVOT (
MAX(Name) FOR Occupation IN ([Developer],[Designer],[Coder])
) as pvt
Output:
Developer Designer Coder
A X Y
B NULL Z
If the number of Occupations may vary then you need dynamic SQL:
DECLARE #columns nvarchar(max),
#sql nvarchar(max)
SELECT #columns = (
SELECT DISTINCT ','+QUOTENAME(Occupation)
FROM #temp
FOR XML PATH('')
)
SELECT #sql = N'
SELECT '+STUFF(#columns,1,1,'')+'
FROM (
SELECT *,
ROW_NUMBER() OVER (PARTITION BY Occupation ORDER BY (SELECT NULL)) RN
FROM #temp
) as t
PIVOT (
MAX(Name) FOR Occupation IN ('+STUFF(#columns,1,1,'')+')
) as pvt'
EXEC sp_executesql #sql
Note: I have used ORDER BY (SELECT NULL) just to get some random ordering. Better use some actual field for this purpose.