MSSQL - Masking data based on mapping table - sql

wanted to perform data masking according to mapping as below by using MSSQL 2008R2:
Mapping Table
A = C
B = A
C = E
1 = 3
2 = 1
3 = 9
Original
ABC123
Masked
CAE319
The idea would be using replace however the second replace function will replacing previous replaced value.
select Replace(Replace(Replace(Replace(Replace(REPLACE('ABC123', 'A', 'C'), 'B', 'A'), 'C', 'E'), '1', '3'), '2', '1'), '3', '9')
Result: CAE319
P.s. value edited, because Reverse or reverse replace cannot be use in this case
any idea?

If you want a more table approach.
There are two code segments below which will Mask or UnMask a string. Easily converted into a UDF or even placed in a CROSS APPLY
Declare #Mask table (MapFrom varchar(10),MapTo varchar(10))
Insert into #Mask values
('A','C'),
('B','D'),
('C','E'),
('1','2'),
('2','3'),
('3','9')
Declare #Yourtable table (ID int,SomeCol varchar(max))
Insert Into #Yourtable values
(1,'ABC123')
-- To Mask
Declare #U varchar(max) ='ABC123'
Select NewSting = Stuff((Select ''+S
From (
Select N
,S=IsNull(MapTo,Substring(#U,N,1))
From (Select Top (Len(#U)) N=Row_Number() Over (Order By (Select null)) From master..spt_values) N
Left Join #Mask on Substring(#U,N,1)=MapFrom
) X
Order By N
For XML Path ('')),1,0,'')
-- To UnMask
Declare #M varchar(max) = 'CDE239'
Select NewSting = Stuff((Select ''+S
From (
Select N
,S=IsNull(MapFrom,Substring(#M,N,1))
From (Select Top (Len(#M)) N=Row_Number() Over (Order By (Select null)) From master..spt_values) N
Left Join #Mask on Substring(#M,N,1)=MapTo
) X
Order By N
For XML Path ('')),1,0,'')

Just change the order of replace and reverse the result
select REVERSE( Replace(Replace(Replace(Replace(Replace(REPLACE('321CBA', '3', '9'), '2', '3'), '1', '2'), 'C', 'E'), 'B', 'D'), 'A', 'C'))
RESULT :
CDE239
EDIT:
Declare #Mask table (MapFrom varchar(10),MapTo varchar(10))
Insert into #Mask values
('A','C'),
('B','A'),
('C','E'),
('1','3'),
('2','1'),
('3','9')
DECLARE #pos INT
,#result VARCHAR(100)
,#maskfrom NCHAR(1)
,#mask_to NCHAR(1);
SET #result = 'ABC123';
SET #pos = 1
WHILE #pos < LEN(#result) + 1
BEGIN
SELECT #mask_to = MapTo
FROM #mask
WHERE MapFrom = substring(#result, #pos, 1)
SET #result = STUFF(#result, #pos, 1, #mask_to);
SET #pos = #pos + 1;
END
SELECT #result
RESULT
CAE319

Related

How to parse SQL Server String in hour, min and second

I have time stored as PT1H22M59.551S.
There it reads read 1 hour 22 min and 59.551 sec.
What is the most efficient way to split this?
Data could be like this. So no separator is mandatory
PT0S
PT6H4M29.212S
PT0S
PT2M55.126S
PT54M4.12S
PT3H6M5.74S
PT16H27M52.069S
You can also create a function to perform the conversion with basic text manipulation:
CREATE FUNCTION dbo.f_GetTime(#TimeInput VARCHAR(50)) RETURNS TIME
AS BEGIN
SET #TimeInput = REPLACE(REPLACE(#TimeInput, 'PT', ''), 'S', '')
IF CHARINDEX('H', #TimeInput) = 0 -- Add missing H
SET #TimeInput = '0H' + #TimeInput
IF CHARINDEX('M', #TimeInput) = 0 -- Add missing M
SET #TimeInput = REPLACE(#TimeInput, 'H', 'H0M')
RETURN CONVERT(TIME, REPLACE(REPLACE(#TimeInput, 'H', ':'), 'M', ':'))
END
GO
Then you can use it with just scalar queries:
SELECT dbo.f_GetTime('PT1H22M59.551S') AS TimeValue
Or you can use it with tables:
DECLARE #Input TABLE (TimeInput VARCHAR(20))
INSERT #Input VALUES ('PT1H22M59.551S'), ('PT0S'), ('PT6H4M29.212S'), ('PT0S'),
('PT2M55.126S'), ('PT54M4.12S'), ('PT3H6M5.74S'), ('PT16H27M52.069S')
SELECT TimeInput, dbo.f_GetTime(TimeInput) AS TimeValue FROM #Input
This would return:
TimeInput TimeValue
-------------------- ----------------
PT1H22M59.551S 01:22:59.5510000
PT0S 00:00:00.0000000
PT6H4M29.212S 06:04:29.2120000
PT0S 00:00:00.0000000
PT2M55.126S 00:02:55.1260000
PT54M4.12S 00:54:04.1200000
PT3H6M5.74S 03:06:05.7400000
PT16H27M52.069S 16:27:52.0690000
--sql server
declare #data table(
lineid int identity,
a varchar(100),
b varchar(100),
hr varchar(20),
[minute] varchar(20),
[second] varchar(20)
)
insert #data(a) select 'PT0S'
insert #data(a) select 'PT6H4M29.212S'
insert #data(a) select 'PT0S'
insert #data(a) select 'PT2M55.126S'
insert #data(a) select 'PT54M4.12S'
insert #data(a) select 'PT3H6M5.74S'
insert #data(a) select 'PT16H27M52.069S'
update #data
set b = replace(a,'PT','')
update #data
set hr = LEFT(b,charindex('H',b))
update #data
set b = REPLACE(b,hr,'')
update #data
set [minute] = LEFT(b,charindex('M',b))
update #data
set b = REPLACE(b,[minute],'')
update #data
set [second] = LEFT(b,charindex('S',b))
update #data
set b = REPLACE(b,[second],'')
update #data
set [hr] = REPLACE([hr],'H',''),[minute]=REPLACE([minute],'M',''),[second]=REPLACE([second],'S','')
update #data
set hr = isnull(nullif(hr,''),'0'),[minute] = isnull(nullif([minute],''),'0'),[second] = isnull(nullif([second],''),'0')
select *,
[hr] + ' hour'+ case when hr <> '1' then 's ' else ' ' end +
[minute] + ' minute'+ case when [minute] <> '1' then 's ' else ' ' end +
[second] + ' second(s)'
from #data
--sql server
declare #data table(
lineid int identity,
[inboundTime] varchar(100)
)
insert #data([inboundTime]) select 'PT0S'
insert #data([inboundTime]) select 'PT6H4M29.212S'
insert #data([inboundTime]) select 'PT0S'
insert #data([inboundTime]) select 'PT2M55.126S'
insert #data([inboundTime]) select 'PT54M4.12S'
insert #data([inboundTime]) select 'PT3H6M5.74S'
insert #data([inboundTime]) select 'PT16H27M52.069S'
insert #data([inboundTime]) select 'PT52.069S'
update #data
set [inboundTime] = replace([inboundTime],'PT','')
SELECT
INBOUND_H
, INBOUND_M
, INBOUND_S
, INBOUND_H*3600 + INBOUND_M*60 inboundTimeMin
, [inboundTime]
from (
SELECT
IIF ( CHARINDEX('H', [inboundTime]) > 0 , SUBSTRING([inboundTime], 0, CHARINDEX('H', [inboundTime])),0) INBOUND_H
, IIF ( CHARINDEX('M', [inboundTime]) > 0 , SUBSTRING([inboundTime], CHARINDEX('H', [inboundTime])+1,CHARINDEX('M', [inboundTime]) - CHARINDEX('H', [inboundTime])-1 ), 0) INBOUND_M
, SUBSTRING([inboundTime], CHARINDEX('M', [inboundTime])+1,CHARINDEX('S', [inboundTime]) - CHARINDEX('M', [inboundTime])-1 ) INBOUND_S
, [inboundTime]
FROM #data)x
This is a more robust function built off solution:
CREATE FUNCTION dbo.f_GetTime(#TimeInput VARCHAR(50)) RETURNS VARCHAR(50)
AS BEGIN
SET #TimeInput = REPLACE(REPLACE(#TimeInput, 'PT', ''), 'S', '')
IF CHARINDEX('H', REVERSE(#TimeInput)) = 1
SET #TimeInput = #TimeInput + '00:00' -- Add missing S
IF CHARINDEX('M', REVERSE(#TimeInput)) = 1
SET #TimeInput = #TimeInput + '00' -- Add missing S
IF CHARINDEX('H', #TimeInput) = 0 -- Add missing H
SET #TimeInput = '00H' + #TimeInput
IF CHARINDEX('M', #TimeInput) = 0 -- Add missing M
SET #TimeInput = REPLACE(#TimeInput, 'H', 'H0M')
RETURN CONVERT(VARCHAR(50), REPLACE(REPLACE(#TimeInput, 'H', ':'), 'M', ':'))
END
GO

Converting CHAR string to nth letter in Alphabet string in SQL

I have to build a process that takes a VARCHAR string (for example 'AHT559') and converts it to a INT only string by converting the Alphabetic chars to INTEGERS based on the nth letter in the alphabet. The above would thus result in: 010820559.
I have done this in SAS before, but I'm relatively new to SQL. What would be the best way to do this in SQL?
Here is what I've done in SAS:
DO _i = 1 TO length( account );
IF (rank( char( account, _i ) ) -64) < 0 THEN agreement_hash = CATS( agreement_hash, char( account, _i ) );
ELSE IF (rank( char( account, _i ) ) -64) < 10 THEN agreement_hash = CATS( agreement_hash, 0, rank( char( account, _i ) )-64 );
ELSE agreement_hash = CATS( agreement_hash, rank( char( account, _i ) )-64 );
END;
If the format of the values is always the same as you state in the comments and you only need to process a single value at a time you can do some simple string manipulation to convert the characters to integers using their ASCII values, and subtracting 64 to get the number of the alphabetic character:
SELECT ASCII('A') -- produces 65
SELECT ASCII('A') - 64 -- produces 1
This is a little long winded and could be done in less lines of code, but it's separated for clarity.
DECLARE #val NVARCHAR(10) = 'AHT559'
-- get first, second and third character numeric values
DECLARE #first INT = ASCII(SUBSTRING(#val, 1, 1)) - 64
DECLARE #second INT = ASCII(SUBSTRING(#val, 2, 1)) - 64
DECLARE #third INT = ASCII(SUBSTRING(#val, 3, 1)) - 64
-- join them together adding a '0' if < 10
SELECT RIGHT('0' + CAST(#first AS VARCHAR(2)), 2)
+ RIGHT('0' + CAST(#second AS VARCHAR(2)), 2)
+ RIGHT('0' + CAST(#third AS VARCHAR(2)), 2)
+ RIGHT(#val, 3)
Tested on 4 million rows:
-- temp table creation - takes approx 100 seconds on my machine
CREATE TABLE #temp (val NVARCHAR(6))
DECLARE #rowno INT = 1
SELECT #rowno = 1
WHILE #rowno <= 4000000
BEGIN
INSERT INTO #temp ( val ) VALUES ( 'AHT559' )
SELECT #rowno = #rowno + 1
END
To run this code against the entire temp table takes < 20 seconds on my machine:
SELECT val AS OrignalValue,
RIGHT('0' + CAST( ASCII(SUBSTRING(val, 1, 1)) - 64 AS VARCHAR(2)), 2)
+ RIGHT('0' + CAST( ASCII(SUBSTRING(val, 2, 1)) - 64 AS VARCHAR(2)), 2)
+ RIGHT('0' + CAST( ASCII(SUBSTRING(val, 3, 1)) - 64 AS VARCHAR(2)), 2)
+ RIGHT(val, 3) AS FormattedValue
FROM #temp
Here is a similar script for sqlserver, any character which is not a capital letter is assumed a digit in this syntax:
DECLARE #x varchar(100) = 'AHT559'
DECLARE #p int = len(#x)
WHILE #p > 0
SELECT #x =
CASE WHEN substring(#x, #p, 1) between 'A' and 'Z'
THEN stuff(#x, #p, 1, right(ascii(substring(#x, #p, 1)) - 64 + 100, 2))
ELSE #x END,
#p -= 1
SELECT #x
Result:
010820559
You could use something like the below, possibly as a scalar function to do this conversion.
DECLARE #i INT
DECLARE #Item NVARCHAR(4000) = 'AHT1234'
DECLARE #ItemTable TABLE
(
Item NCHAR(1)
)
SET #i = 1
--Split the input string into separate characters, store in temp table
WHILE (#i <= LEN(#Item))
BEGIN
INSERT INTO #ItemTable(Item)
VALUES(SUBSTRING(#Item, #i, 1))
SET #i = #i + 1
END
DECLARE #AlphaTable TABLE (
Letter NCHAR(1),
Position NVARCHAR(2)
)
-- Populate this with the whole alphabet obviously. Could be a permanent rather than temp table.
INSERT INTO #AlphaTable
( Letter, Position )
VALUES ( N'A', '01'),
(N'H', '08'),
(N'T', '20')
DECLARE #Output NVARCHAR(50)
-- Convert the output and concatenate it back to a single output.
SELECT #Output = COALESCE(#output, '') + Converted
FROM (
SELECT CASE WHEN ISNUMERIC(Item) = 1
THEN CONVERT(NVARCHAR(1), Item)
ELSE (SELECT Position FROM #AlphaTable WHERE Letter = CONVERT(NCHAR(1), Item))
END AS Converted
FROM #ItemTable
) AS T1
SELECT #Output
GO
Try this.
DECLARE #STR VARCHAR(MAX)= 'AHT559',
#SP INT,
#SP_STR VARCHAR(50),
#OUTPUT VARCHAR(MAX)=''
DECLARE #TEMP_STR VARCHAR(50)
SET #TEMP_STR = #STR
WHILE Patindex('%[A-Z]%', #TEMP_STR) <> 0
BEGIN
SELECT #SP = Patindex('%[A-Z]%', #TEMP_STR)
SELECT #SP_STR = Upper(LEFT(#TEMP_STR, #SP))
SELECT #SP_STR = ( Ascii(#SP_STR) - 65 ) + 1
SELECT #TEMP_STR = Stuff(#TEMP_STR, 1, #SP, '')
SET #OUTPUT += RIGHT('0' + #SP_STR, 2)
END
SELECT #OUTPUT + Substring(#STR, Patindex('%[0-9]%', #STR), Len(#STR))
How about using a CTE to create every combination of the first 3 letters and using that to match to:
SQL Fiddle
MS SQL Server 2008 Schema Setup:
CREATE TABLE Accounts
(
Account VARCHAR(6)
)
INSERT INTO Accounts
VALUES ('AHT559'), ('BXC556'),
('CST345')
Query 1:
;WITH AlphaToNum
AS
(
SELECT *
FROM (VALUES
('A', '01'), ('B', '02'), ('C', '03'), ('D', '04'),
('E', '05'), ('F', '06'), ('G', '07'), ('H', '08'),
('I', '09'), ('J', '10'), ('K', '11'), ('L', '12'),
('M', '13'), ('N', '14'), ('O', '15'), ('P', '16'),
('Q', '17'), ('R', '18'), ('S', '19'), ('T', '20'),
('U', '21'), ('V', '22'), ('W', '23'), ('X', '24'),
('Y', '25'), ('Z', '26')
) X(alpha, num)
),
MappingTable
As
(
SELECT A1.alpha + A2.alpha + A3.alpha as match, A1.num + A2.num + A3.num as val
FROM AlphaToNum A1
CROSS APPLY AlphaToNum A2
CROSS APPLY AlphaToNum A3
)
SELECT A.Account, M.val + SUBSTRING(A.Account,4, 3) As ConvertedAccount
FROM MappingTable M
INNER JOIN Accounts A
ON LEFT(A.Account,3) = M.match
Results:
| Account | ConvertedAccount |
|---------|------------------|
| AHT559 | 010820559 |
| BXC556 | 022403556 |
| CST345 | 031920345 |
This is probably best done using a CLR UDF, but a full answer is too long for this format.
Basically you need to create a UDF (User defined function) that takes a string (nvarchar...) as an input and returns a string as an output. You can do that with C# quite easily, and you need to wrap it with the CLR integration requirements.
You can see here for relevant information.
The code could look something like:
[Microsoft.SqlServer.Server.SqlFunction(
IsDeterministic=true,
IsPrecise=true,
SystemDataAccess=SystemDataAccessKind.None)]
public static SqlString ToNthAlpha(SqlString value)
{
if(value.IsNull)
return value;
char []chars = value.Value.ToCharArray();
StringBuilder res = new StringBuilder();
for(int i = 0; i < chars.Length; i++)
{
if(chars[i] >= 'A' && chars[i] <= 'Z')
res.AppendFormat("{0:00}", chars[i] - 'A');
res.Append(chars[i]);
}
return new SqlString(res.ToString());
}

SQL Server 2005 Using CHARINDEX() To split a string

How can I split the following string based on the '-' character?
So if I had this string: LD-23DSP-1430
How could I split it into separate columns like this:
LD 23DSP 1430
Also, is there a way to split each character into a separate field if I needed to (without the '-')? I'm trying to find a way to replace each letter with the NATO alphabet.
So this would be..... Lima Delta Twenty Three Delta Sierra Papa Fourteen Thirty.... in one field.
I know I can get the left side like this:
LEFT(#item, CHARINDEX('-', #item) - 1)
I wouldn't exactly say it is easy or obvious, but with just two hyphens, you can reverse the string and it is not too hard:
with t as (select 'LD-23DSP-1430' as val)
select t.*,
LEFT(val, charindex('-', val) - 1),
SUBSTRING(val, charindex('-', val)+1, len(val) - CHARINDEX('-', reverse(val)) - charindex('-', val)),
REVERSE(LEFT(reverse(val), charindex('-', reverse(val)) - 1))
from t;
Beyond that and you might want to use split() instead.
Here's a little function that will do "NATO encoding" for you:
CREATE FUNCTION dbo.NATOEncode (
#String varchar(max)
)
RETURNS TABLE
WITH SCHEMABINDING
AS
RETURN (
WITH L1 (N) AS (SELECT 1 UNION ALL SELECT 1),
L2 (N) AS (SELECT 1 FROM L1, L1 B),
L3 (N) AS (SELECT 1 FROM L2, L2 B),
L4 (N) AS (SELECT 1 FROM L3, L3 B),
L5 (N) AS (SELECT 1 FROM L4, L4 C),
L6 (N) AS (SELECT 1 FROM L5, L5 C),
Nums (Num) AS (SELECT Row_Number() OVER (ORDER BY (SELECT 1)) FROM L6)
SELECT
NATOString = Substring((
SELECT
Convert(varchar(max), ' ' + D.Word)
FROM
Nums N
INNER JOIN (VALUES
('A', 'Alpha'),
('B', 'Beta'),
('C', 'Charlie'),
('D', 'Delta'),
('E', 'Echo'),
('F', 'Foxtrot'),
('G', 'Golf'),
('H', 'Hotel'),
('I', 'India'),
('J', 'Juliet'),
('K', 'Kilo'),
('L', 'Lima'),
('M', 'Mike'),
('N', 'November'),
('O', 'Oscar'),
('P', 'Papa'),
('Q', 'Quebec'),
('R', 'Romeo'),
('S', 'Sierra'),
('T', 'Tango'),
('U', 'Uniform'),
('V', 'Victor'),
('W', 'Whiskey'),
('X', 'X-Ray'),
('Y', 'Yankee'),
('Z', 'Zulu'),
('0', 'Zero'),
('1', 'One'),
('2', 'Two'),
('3', 'Three'),
('4', 'Four'),
('5', 'Five'),
('6', 'Six'),
('7', 'Seven'),
('8', 'Eight'),
('9', 'Niner')
) D (Digit, Word)
ON Substring(#String, N.Num, 1) = D.Digit
WHERE
N.Num <= Len(#String)
FOR XML PATH(''), TYPE
).value('.[1]', 'varchar(max)'), 2, 2147483647)
);
This function will work on even very long strings, and performs pretty well (I ran it against a 100,000-character string and it returned in 589 ms). Here's an example of how to use it:
SELECT NATOString FROM dbo.NATOEncode('LD-23DSP-1430');
-- Output: Lima Delta Two Three Delta Sierra Papa One Four Three Zero
I intentionally made it a table-valued function so it could be inlined into a query if you run it against many rows at once, just use CROSS APPLY or wrap the above example in parentheses to use it as a value in the SELECT clause (you can put a column name in the function parameter position).
Try the following query:
DECLARE #item VARCHAR(MAX) = 'LD-23DSP-1430'
SELECT
SUBSTRING( #item, 0, CHARINDEX('-', #item)) ,
SUBSTRING(
SUBSTRING( #item, CHARINDEX('-', #item)+1,LEN(#ITEM)) ,
0 ,
CHARINDEX('-', SUBSTRING( #item, CHARINDEX('-', #item)+1,LEN(#ITEM)))
),
REVERSE(SUBSTRING( REVERSE(#ITEM), 0, CHARINDEX('-', REVERSE(#ITEM))))
USE [master]
GO
/****** this function returns Pakistan where as if you want to get ireland simply replace (SELECT SUBSTRING(#NEWSTRING,CHARINDEX('$#$#$',#NEWSTRING)+5,LEN(#NEWSTRING))) with
SELECT #NEWSTRING = (SELECT SUBSTRING(#NEWSTRING, 0,CHARINDEX('$#$#$',#NEWSTRING)))******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE FUNCTION [dbo].[FN_RETURN_AFTER_SPLITER]
(
#SPLITER varchar(max))
RETURNS VARCHAR(max)
AS
BEGIN
--declare #testString varchar(100),
DECLARE #NEWSTRING VARCHAR(max)
-- set #teststring = '#ram?eez(ali)'
SET #NEWSTRING = #SPLITER ;
SELECT #NEWSTRING = (SELECT SUBSTRING(#NEWSTRING,CHARINDEX('$#$#$',#NEWSTRING)+5,LEN(#NEWSTRING)))
return #NEWSTRING
END
--select [dbo].[FN_RETURN_AFTER_SPLITER] ('Ireland$#$#$Pakistan')
Create FUNCTION [dbo].[fnSplitString]
(
#string NVARCHAR(200),
#delimiter CHAR(1)
)
RETURNS #output TABLE(splitdata NVARCHAR(10)
)
BEGIN
DECLARE #start INT, #end INT
SELECT #start = 1, #end = CHARINDEX(#delimiter, #string)
WHILE #start < LEN(#string) + 1 BEGIN
IF #end = 0
SET #end = LEN(#string) + 1
INSERT INTO #output (splitdata)
VALUES(SUBSTRING(#string, #start, #end - #start))
SET #start = #end + 1
SET #end = CHARINDEX(#delimiter, #string, #start)
END
RETURN
END**strong text**
DECLARE #variable VARCHAR(100) = 'LD-23DSP-1430';
WITH Split
AS ( SELECT #variable AS list ,
charone = LEFT(#variable, 1) ,
R = RIGHT(#variable, LEN(#variable) - 1) ,
'A' AS MasterOne
UNION ALL
SELECT Split.list ,
LEFT(Split.R, 1) ,
R = RIGHT(split.R, LEN(Split.R) - 1) ,
'B' AS MasterOne
FROM Split
WHERE LEN(Split.R) > 0
)
SELECT *
FROM Split
OPTION ( MAXRECURSION 10000 );

How to make a list of T-SQL results with comma's between them?

Suppose we have a simple query like this:
SELECT x
FROM t
WHERE t.y = z
If we have one record in the result set, I want to set variable #v to that one value. If we have two or more records, I'd like the results to be separated by a comma and a space. What is the best way to write this T-SQL code?
Example:
result set of 1 record:
Value1
result set of 2 records:
Value1, Value2
result set of 3 records:
Value1, Value2, Value3
this will give you the list of values in a comma separated list
create table #temp
(
y int,
x varchar(10)
)
insert into #temp values (1, 'value 1')
insert into #temp values (1, 'value 2')
insert into #temp values (1, 'value 3')
insert into #temp values (1, 'value 4')
DECLARE #listStr varchar(255)
SELECT #listStr = COALESCE(#listStr+', ', '') + x
FROM #temp
WHERE #temp.y = 1
SELECT #listStr as List
drop table #temp
You can use XML to do that:
DECLARE #V VarChar(4000);
SELECT #V = CONVERT(VarChar(4000), (
SELECT x + ', '
FROM t
WHERE t.y = z
FOR XML PATH('')
));
-- To remove the final , in the list:
SELECT #V = LEFT(#V, LEN(#V) - 2);
SELECT #V;
For other options check out Concatenating Row Values in SQL.
Since it's SQL Server 2008, you can use FOR XML:
SELECT SUBSTRING(
(SELECT ',' + t.x
FROM t
WHERE t.y = z
FOR XML PATH('')),
2,
200000) AS CSV
FOR XML PATH('') selects the table as XML, but with a blank path.
The SUBSTRING(select, 2, 2000000) removes the leading ', '
You could use a recursive CTE for this:
CREATE TABLE #TableWithId (Id INT IDENTITY(1,1), x VARCHAR)
INSERT INTO #TableWithId
SELECT x
FROM t
WHERE t.y = z
WITH Commas(ID, Flattened)
AS
(
-- Anchor member definition
SELECT ID, x AS Flattened
FROM #TableWithId
WHERE ID = 1
UNION ALL
-- Recursive member definition
SELECT #TableWithId.Id, Flattened + ',' + x
FROM #TableWithId
INNER JOIN Commas
ON #TableWithId.Id + 1 = Commas.Id
)
-- Statement that executes the CTE
SELECT TOP 1 Flattened
FROM Commas
ORDER BY id;
GO
How about something like this???
DECLARE #x AS VARCHAR(2000)
SET #x = ''
SELECT #x = #x + RTRIM(x) + ','
FROM t
SELECT #x = SUBSTRING(#x, 1, LEN(#x) - 1)
PRINT #x

How to perform a join in SQL Server without using tables

I have two lists and I want to see what the two lists DON'T have in common. For example:
List1:
'a','b','c','123'
List2:
'd','e','f','a','asd','c'
I want output to be:
'b','123','d','e','f','asd'
Something like this?
select * from ('a','b','c','123')
join ('d','e','f','a','asd','c')
on ???
Is there a pure SQL Server solution for this without using tables?
If you have control over the lists, I would just make them table variables:
DECLARE #a TABLE (str varchar(100))
INSERT INTO #a
VALUES
('a'),
('b')...
DECLARE #b table (str varchar(100))
INSERT INTO #b
VALUES
...
(SELECT str FROM #a
EXCEPT
SELECT str FROM #b)
UNION
(SELECT str FROM #b
EXCEPT
SELECT str FROM #a)
Given this function:
CREATE FUNCTION dbo.SplitStrings ( #List NVARCHAR(MAX) )
RETURNS TABLE
AS
RETURN ( SELECT Item FROM (
SELECT Item = x.i.value('(./text())[1]', 'nvarchar(max)')
FROM (
SELECT [XML] = CONVERT(XML, '<i>' + REPLACE(#List,',', '</i><i>')
+ '</i>').query('.')) AS a CROSS APPLY [XML].nodes('i') AS x(i)
) AS y WHERE Item IS NOT NULL);
GO
You can do it with a full outer join:
DECLARE
#list1 NVARCHAR(MAX) = N'a,b,c,123',
#list2 NVARCHAR(MAX) = N'd,e,f,a,asd,c',
#output NVARCHAR(MAX) = N'';
SELECT #output += N',' + COALESCE(l1.Item, l2.Item)
FROM dbo.SplitStrings(#list1) AS l1
FULL OUTER JOIN dbo.SplitStrings(#list2) AS l2
ON l1.Item = l2.Item
WHERE l1.Item IS NULL OR l2.Item IS NULL;
SELECT STUFF(#output, 1, 1, N'');
Or similar to #JNK's:
DECLARE
#list1 NVARCHAR(MAX) = N'a,b,c,123',
#list2 NVARCHAR(MAX) = N'd,e,f,a,asd,c',
#output NVARCHAR(MAX) = N'';
;WITH l1 AS (SELECT Item FROM dbo.SplitStrings(#list1)),
l2 AS (SELECT Item FROM dbo.SplitStrings(#list2))
SELECT #output += N',' + Item
FROM ( (SELECT Item FROM l1 EXCEPT SELECT Item FROM l2)
UNION
(SELECT Item FROM l2 EXCEPT SELECT Item FROM l1)) AS x;
SELECT STUFF(#output, 1, 1, N'');
And probably a variety of other ways too. If order matters, it's going to be a little more complex, but still possible.
There is no easy way to accomplish this. To filter the values from a list you need to have them as rows. So you would end up with something like:
SELECT col FROM (
SELECT 'a' as col
UNION
SELECT 'b'
UNION
SELECT 'c') t
WHERE col NOT IN ('a', 'b')
How about:
with
list1(j) as (select 'a' union select 'b'),
list2(j) as (select 'b' union select 'c')
select coalesce(list1.j, list2.j)
from list1 full join list2
on list1.j = list2.j
where (list1.j is null or list2.j is null)
I think you'll have to insert the values into 2 variable tables.
DECLARE #Table1 TABLE (Value VARCHAR(1))
DECLARE #Table2 TABLE (Value VARCHAR(1))
INSERT INTO #Table1 (Value) VALUES ('a')
INSERT INTO #Table1 (Value) VALUES ('b')
INSERT INTO #Table2 (Value) VALUES ('b')
INSERT INTO #Table2 (Value) VALUES ('c')
Then perform some set operations on the 2 tables.
DECLARE #TableUnion TABLE (Value VARCHAR(1))
DECLARE #TableIntersection TABLE (Value VARCHAR(1))
DECLARE #TableExcept TABLE (Value VARCHAR(1))
INSERT INTO #TableUnion
SELECT * FROM
((SELECT * FROM #Table1)
UNION
(SELECT * FROM #Table2)) U
INSERT INTO #TableIntersection
SELECT * FROM
((SELECT * FROM #Table1)
INTERSECT
(SELECT * FROM #Table2)) I
INSERT INTO #TableExcept
SELECT * FROM
((SELECT * FROM #TableUnion)
EXCEPT
(SELECT * FROM #TableIntersection)) E
The result set of the final select statement will contain 'a' and 'c'. Which can be concatenated into a single string as follows.
DECLARE #ExceptString VARCHAR(3)
SELECT #ExceptString =
CASE
WHEN #ExceptString IS NULL THEN Value
ELSE #ExceptString + ',' + Value
END
FROM #TableExcept