SQL: Perform arithmetic operations on values in a column - sql

I have a varchar that contains a formula:
declare #formula varchar(50) = 'X + Y + Z'
and I also have a table:
+---+---+
| A | B |
+---+---+
| X | 1 |
+---+---+
| Y | 2 |
+---+---+
| Z | 3 |
+---+---+
Column A values are unique and the formula may change. For example, if the formula is set to 'X + Y + Z' then the result would be 6. And if the formula is set to 'Z - X + Y' then the result is 4. Operations include only addition and subtraction. How can I achieve this? Having a hard time looking for where to start.

SQL Server does NOT support macro substitution, nor does it have an Eval()... this leaves Dynamic SQL
Example
Declare #YourTable Table ([A] varchar(50),[B] varchar(50))
Insert Into #YourTable Values
('X',1)
,('Y',2)
,('Z',3)
Declare #formula varchar(50) = 'X + Y + Z'
Select #formula=replace(#formula,[A],[B])
From #YourTable
Exec('Select NewValue='+#formula)
Returns
NewValue
6

Just for fun, here is a modified option which will support a TABLE
Example
Declare #YourValues Table ([A] varchar(50),[B] varchar(50))
Insert Into #YourValues Values
('X',1)
,('Y',2)
,('Z',3)
Declare #YourFormula Table (ID int,Formula varchar(50))
Insert Into #YourFormula Values
(1,'X + Y + Z'),
(2,'X - Y + Z')
Declare #SQL varchar(max) = stuff((Select concat(',(',ID,',',Formula,')') From #YourFormula For XML Path ('')),1,1,'')
Select #SQL=replace(#SQL,[A],[B])
From #YourValues
Create Table #TempResults (ID int,Calc money)
Exec('Insert Into #TempResults Select * from (values '+#SQL+')A(ID,Calc)')
Select * from #TempResults
Returns
ID Calc
1 6.00
2 2.00

This is rather crude and only works for +/- operands, however, I believe it satisfies the question.
DECLARE #Formulas TABLE (Formula NVARCHAR(MAX))
INSERT INTO #Formulas SELECT 'Z-X+Y'
DECLARE #Values TABLE(Name NVARCHAR(50), Value DECIMAL(18,2))
INSERT #Values VALUES ('X',1),('Y',2),('Z',3)
;WITH MySplitFormula AS
(
SELECT Value = SUBSTRING(Formula,Number,1) FROM #Formulas
CROSS APPLY (SELECT DISTINCT number FROM master..spt_values WHERE number > 0 AND number <= LEN(Formula))V
)
,NormalizedFormula AS
(
SELECT
DerivedOperations = CASE WHEN F.Value IN('+','-') THEN F.Value ELSE NULL END,
IsOperator = CASE WHEN F.Value IN('+','-') THEN 1 ELSE 0 END,
DerivedValues = CASE WHEN F.Value IN('+','-') THEN NULL ELSE V.Value END
FROM
MySplitFormula F
LEFT OUTER JOIN #Values V ON V.Name = F.Value
WHERE
NOT F.Value IS NULL
),
ValidatedFormula AS
(
SELECT DerivedOperations,DerivedValues FROM NormalizedFormula WHERE NOT((DerivedOperations IS NULL) AND (DerivedValues IS NULL))
),
Operators AS
(
SELECT
OrderIndex=ROW_NUMBER() OVER (ORDER BY (SELECT NULL)),
Operator=DerivedOperations FROM ValidatedFormula WHERE NOT DerivedOperations IS NULL
),
Operands AS
(
SELECT
OrderIndex=ROW_NUMBER() OVER (ORDER BY (SELECT NULL)),
Operand=DerivedValues FROM ValidatedFormula WHERE NOT DerivedValues IS NULL
)
,Marked AS
(
SELECT
OP.OrderIndex,
DoOperation = CASE WHEN OP.OrderIndex % 2 = 1 THEN 1 ELSE 0 END,
Operand1 = Operand,
Operator,
Operand2 = LEAD(Operand) OVER(ORDER BY OP.OrderIndex)
FROM
Operands OP
LEFT OUTER JOIN Operators OPR ON OPR.OrderIndex = OP.OrderIndex
)
,MarkedAgain AS
(
SELECT
*,
CalculatedValue = CASE WHEN DoOperation = 1 THEN
CASE
WHEN Operator = '+' THEN Operand1 + Operand2
WHEN Operator = '-' THEN Operand1 - Operand2
WHEN Operator IS NULL THEN
CASE WHEN LAG(Operator) OVER(ORDER BY OrderIndex) ='+' THEN Operand1 ELSE -Operand1 END
ELSE NULL
END
END
FROM
Marked
)
SELECT SUM(CalculatedValue) FROM MarkedAgain

Related

Reverse order of elements in a string

I have the following string:
1119/2/483/11021
I would like to reverse the order of the elements in that string. Desired output:
11021/483/2/1119
T-SQL Version 2014
You need an ordered split function, e.g. (inspiration):
CREATE FUNCTION dbo.SplitOrdered
(
#list nvarchar(max),
#delim nvarchar(10)
)
RETURNS TABLE
WITH SCHEMABINDING
AS
RETURN
(
WITH w(n) AS (SELECT 0 FROM (VALUES (0),(0),(0),(0)) w(n)),
k(n) AS (SELECT 0 FROM w a, w b),
r(n) AS (SELECT 0 FROM k a, k b, k c, k d, k e, k f, k g, k h),
p(n) AS (SELECT TOP (COALESCE(LEN(#list), 0))
ROW_NUMBER() OVER (ORDER BY ##SPID) -1 FROM r),
spots(p) AS
(
SELECT n FROM p
WHERE (SUBSTRING(#list, n, LEN(#delim + 'x') - 1) LIKE #delim OR n = 0)
),
parts(p,val) AS
(
SELECT p, SUBSTRING(#list, p + LEN(#delim + 'x') - 1,
LEAD(p, 1, 2147483647) OVER (ORDER BY p) - p - LEN(#delim))
FROM spots AS s
)
SELECT listpos = ROW_NUMBER() OVER (ORDER BY p),
Item = LTRIM(RTRIM(val))
FROM parts
);
Then you can reassemble using STRING_AGG() (if SQL Server 2017 or better) or FOR XML PATH on lower versions:
SQL Server 2017 +
DECLARE #OriginalString nvarchar(255) = N'1119/2/483/11021';
SELECT NewString = STRING_AGG(o.Item, N'/')
WITHIN GROUP (ORDER BY listpos DESC)
FROM dbo.SplitOrdered(#OriginalString, N'/') AS o;
SQL Server < 2017
DECLARE #OriginalString nvarchar(255) = N'1119/2/483/11021';
SELECT NewString = STUFF(
(SELECT N'/' + o.Item
FROM dbo.SplitOrdered(#OriginalString, N'/') AS o
ORDER BY o.listpos DESC
FOR XML PATH(''), TYPE).value(N'./text()[1]', N'nvarchar(max)'),1,1,N'');
Example db<>fiddle
Please try the following solution based on the built-in PARSENAME() T-SQL function.
SQL
-- DDL and sample data population, start
DECLARE #tbl TABLE (ID INT IDENTITY PRIMARY KEY, Tokens VARCHAR(MAX));
INSERT INTO #tbl (Tokens) VALUES
('1119/2/483/11021'),
('1120/25/484/1102');
-- DDL and sample data population, end
SELECT tbl.*
, PARSENAME(c, 1) + '/' +
PARSENAME(c, 2) + '/' +
PARSENAME(c, 3) + '/' +
PARSENAME(c, 4) AS Result
FROM #tbl AS tbl
CROSS APPLY (VALUES (REPLACE(Tokens, '/', '.') )) AS t(c);
Output
+----+------------------+------------------+
| ID | Tokens | Result |
+----+------------------+------------------+
| 1 | 1119/2/483/11021 | 11021/483/2/1119 |
| 2 | 1120/25/484/1102 | 1102/484/25/1120 |
+----+------------------+------------------+
First, split the string and convert it into a column then order by desc and display into multiple row values into a single row. In the following code, you can set any string and split char.
Try following way.
DECLARE #S varchar(max) ,
#Split char(1),
#X xml
DECLARE #Names VARCHAR(8000)
SELECT #S = '1119/2/483/11021',
#Split = '/'
SELECT #X = CONVERT(xml,' <root> <myvalue>' +
REPLACE(#S,#Split,'</myvalue> <myvalue>') + '</myvalue> </root> ')
select #Names = COALESCE(#Names + '/', '') + Value from (
select rowno,Value from (
select ROW_NUMBER() OVER(ORDER BY d) AS rowno , Value from (
SELECT T.c.value('.','varchar(20)') as Value,0 as d
FROM #X.nodes('/root/myvalue') T(c)
) m
) r
) t order by t.rowno desc
select #Names as ReverseString
Splitting the string into sub-strings, and then joining them back up, is most likely going to be a good approach.
Some comments mention using string-reverse, but that doesnt seem to be a good approach at all in your case, since you just want to reverse the order of words within the current string, not actually reverse the entire text-string character-by-character.
PS: string_split does not guarantee the order of the chunks!

MS SQL Combining 5 columns to 3 columns

I am trying to figure out how to combine 5 columns to 3 columns and avoiding duplicities at the same time.
I have been thinking about it for a few days and I can't figure out any solution so far, so I have decided to ask for help here.
Basically right now we use 5 columns in our database let's say A,B,C,D,E and somebody from our company has decided that we will be moving this information to new 3 columns let's call them NEW_1,NEW_2,NEW_3, because there are no cases when we would have more than 3 values per row in the DB.
I need to somehow extract those 5 columns and get them to just 3 columns without repeating them.
So far I was thinking about and trying using CASE, but I can't figure it out how to stop it from showing the same value in all 3 NEW columns. I was thinking about assigning some variables to determine which columns to skip in select CASE, but I have found out that I can't do it like that.
If someone could at least direct me to the right way I would really appreciate it.
SELECT CASE
WHEN A is not null THEN A
WHEN B is not null THEN B
WHEN C is not null THEN C
WHEN D is not null THEN D
WHEN E is not null THEN E
ELSE NULL
END as NEW_1,
CASE
WHEN B is not null THEN B
WHEN C is not null THEN C
WHEN D is not null THEN D
WHEN E is not null THEN E
ELSE NULL
END as NEW_2,
CASE
WHEN C is not null THEN C
WHEN D is not null THEN D
WHEN E is not null THEN E
ELSE NULL
END as NEW_3
Here is a option where we unpivot the data and then apply a conditional aggregation within a CROSS APPLY
Example
Declare #YourTable Table ([A] varchar(50),[B] varchar(50),[C] varchar(50),[D] varchar(50),[E] varchar(50))
Insert Into #YourTable Values
(1,2,3,NULL,NULL)
,(NULL,4,NULL,5,NULL)
,(NULL,null,6,7,8)
,(9,NULL,NULL,NULL,NULL)
Select A.*
,B.*
From #YourTable A
Cross Apply (
Select Val1 = max(case when ColNr=1 then Value end)
,Val2 = max(case when ColNr=2 then Value end)
,Val3 = max(case when ColNr=3 then Value end)
From (
Select ColNr = row_number() over (order by Seq)
,B1.*
From (values (1,A)
,(2,B)
,(3,C)
,(4,D)
,(5,E)
) B1(Seq,Value)
Where Value is not null
) B2
) B
Returns
Just for Fun, here is an XML version
Select A.*
,Val1 = XMLData.value('/x[1]','varchar(max)')
,Val2 = XMLData.value('/x[2]','varchar(max)')
,Val3 = XMLData.value('/x[3]','varchar(max)')
From #YourTable A
Cross Apply ( values ( convert(xml,
concat('<x>'+A+'</x>'
,'<x>'+B+'</x>'
,'<x>'+C+'</x>'
,'<x>'+D+'</x>'
,'<x>'+E+'</x>'
) ) ) ) B(XMLData)
I like John's answer and probably better than mine, but here's a slightly different version if the columns are fixed to a specific number.
Build a delimited string of the column values, then use xml to extract the 1st, 2nd, 3rd values.
Declare #YourTable Table ([A] varchar(50),[B] varchar(50),[C] varchar(50),[D] varchar(50),[E] varchar(50));
Insert Into #YourTable Values
(1,2,3,NULL,NULL),(NULL,4,NULL,5,NULL),(NULL,null,6,7,8),(9,NULL,NULL,NULL,NULL);
WITH CTE AS (
SELECT CASE WHEN a IS NULL THEN '' ELSE a + '~' END +
CASE WHEN b IS NULL THEN '' ELSE b + '~' END +
CASE WHEN c IS NULL THEN '' ELSE c + '~' END +
CASE WHEN d IS NULL THEN '' ELSE d + '~' END +
CASE WHEN e IS NULL THEN '' ELSE e + '~' END AS Combined
FROM #YourTable
)
SELECT
ISNULL(CAST(N'<x>' + REPLACE(Combined, '~', N'</x><x>') + N'</x>' AS XML).value('/x[1]', 'nvarchar(max)'), '') [new_1],
ISNULL(CAST(N'<x>' + REPLACE(Combined, '~', N'</x><x>') + N'</x>' AS XML).value('/x[2]', 'nvarchar(max)'), '') [new_2],
ISNULL(CAST(N'<x>' + REPLACE(Combined, '~', N'</x><x>') + N'</x>' AS XML).value('/x[3]', 'nvarchar(max)'), '') [new_3]
FROM CTE;

SQL Server - Return All possible combinations of a 4 digit number passed to a stored procedure

I have a task to write a stored procedure or a function to return all possible combinations of a 4 digit number.
For example, if I pass 1234 to the stored procedure or function, it should return 4 digit numbers (all possible combinations), like
1123, 1112, 1324, 1342, 2134, 2234
and so on.
It can be of 4 digits only.
I have been doing this with using LIKE operator:
select *
from Table
where mynumber like '%1%'
and mynumber like '%2%'
and mynumber like '%3%'
and mynumber like '%4%'
but the problem is, I have hardcoded the numbers 1,2,3 and 4.
The number can be anything.
And these many LIKE operators can also impact the performance on a large table.
Can anybody give me some generic query to get the combinations?
Thanks in advance.
You can use a cross join:
with digits as (
select substring(num, 1, 1) as d union all
select substring(num, 2, 1) as d union all
select substring(num, 3, 1) as d union all
select substring(num, 4, 1) as d
)
select (d1.d + d2.d + d3.d + d4.d)
from digits d1 cross join
digits d2 cross join
digits d3 cross join
digits d4;
Note: This assumes that the number is a string (based on the fact that you use like in your question).
First you need to be able to break a four-digit number into separate digits. I suggest using a table variable and the modulus operator. Assuming we have an integer input named #input, we can break it into its digits using this:
DECLARE #Digits Table(Number int)
INSERT INTO #Digits(Number)
VALUES (#input % 10),
(#input / 10 % 10),
(#input / 100 % 10),
(#input / 1000 % 10)
Now we have a table with four rows, one row per digit.
To create a combination of four digits, we need to include the table four times, meaning we need three joins. The joins have to be set up so no digit is duplicated. Thus our FROM and JOIN clauses will look like this:
FROM #Digits D1
JOIN #Digits D2 ON D2.Number <> D1.Number
JOIN #Digits D3 ON D3.Number <> D1.Number
AND D3.Number <> D2.Number
JOIN #Digits D4 ON D4.Number <> D1.Number
AND D4.Number <> D2.Number
AND D4.Number <> D3.Number
Now to take the values and make a new, four-digit integer:
SELECT Number = D1.Number * 1000
+ D2.Number * 100
+ D3.Number * 10
+ D4.Number
The complete solution:
CREATE PROC Combine(#input AS int)
AS
BEGIN
DECLARE #Digits Table(Number int)
;
INSERT INTO #Digits(Number)
VALUES (#input % 10),
(#input / 10 % 10),
(#input / 100 % 10),
(#input / 1000 % 10)
;
SELECT Number = D1.Number * 1000
+ D2.Number * 100
+ D3.Number * 10
+ D4.Number
FROM #Digits D1
JOIN #Digits D2 ON D2.Number <> D1.Number
JOIN #Digits D3 ON D3.Number <> D1.Number
AND D3.Number <> D2.Number
JOIN #Digits D4 ON D4.Number <> D1.Number
AND D4.Number <> D2.Number
AND D4.Number <> D3.Number
ORDER BY Number
;
END
Usage:
EXEC Combine 1234
Resultset:
Number
------
1234
1243
1324
1342
1423
1432
2134
2143
2314
2341
2413
2431
3124
3142
3214
3241
3421
4123
4132
4213
4231
4312
4321
24 row(s)
Click here to run the above code on RexTester
Improving #GordonLinoff's answer, you can add an additional column in you CTE so that you can only make sure that each number is only used once:
declare #num varchar(max);
set #num = '1234';
with numCTE as (
select SUBSTRING(#num, 1,1) as col, 1 as cnt union
select SUBSTRING(#num, 2,1) as col, 3 as cnt union
select SUBSTRING(#num, 3,1) as col, 9 as cnt union
select SUBSTRING(#num, 4,1) as col, 27 as cnt
)
select DISTINCT (a1.col+a2.col+a3.col+a4.col)
from numCTE a1
cross join numCTE a2
cross join numCTE a3
cross join numCTE a4
where a1.cnt + a2.cnt + a3.cnt + a4.cnt = 40
Additionally, you can remove the WHERE to allow each number to be used more than once.
Don't forget the DISTINCT keyword. :)
You can try this.
select * from Table where mynumber like '%[1234][1234][1234][1234]%'
if it should only be 4 digit
select * from Table where mynumber like '[1234][1234][1234][1234]'
Also, you can use [1-4] instead of [1234]
Here's query to return all combinations of four digits (characters in general):
select A.col + B.col + C.col + D.col [Combinations] from
(values ('1'),('2'),('3'),('4')) as A(col) cross join
(values ('1'),('2'),('3'),('4')) as B(col) cross join
(values ('1'),('2'),('3'),('4')) as C(col) cross join
(values ('1'),('2'),('3'),('4')) as D(col)
Taking inspiration from this answer:
WITH n AS (
SELECT n FROM (VALUES (1), (2), (3), (4)) n (n)
) SELECT ones.n + 10*tens.n + 100*hundreds.n + 1000*thousands.n
FROM n ones, n tens, n hundreds, n thousands
You can define a table in your stored procedure will all possible combinations but using letters for codding:
DECLARE #Combinations TABLE
(
[value] CHAR(4)
);
INSERT INTO #Combinations ([value])
VALUES ('AAAA')
,('AAAB')
,('AAAC')
,('AAAD')
...
Then update every latter with the input number:
DECLARE #Numner1 TINYINT = 2
,#Numner2 TINYINT = 5
,#Numner3 TINYINT = 1
,#Numner4 TINYINT = 3;
UPDATE #Combinations
SET [value] = REPLACE([value], 'A', #Numner1);
UPDATE #Combinations
SET [value] = REPLACE([value], 'B', #Numner2);
UPDATE #Combinations
SET [value] = REPLACE([value], 'C', #Numner3);
UPDATE #Combinations
SET [value] = REPLACE([value], 'D', #Numner4);
Then just join the table with your table:
select *
from Table A
INNER JOIN #Combinations B
ON A.[mynumber] = B.[value];
Try This approach
DECLARE #Num INT = 5432
;WITH CTE
AS
(
SELECT
SeqNo = 1,
Original = CAST(#Num AS VARCHAR(20)),
Num = SUBSTRING(CAST(#Num AS VARCHAR(20)),1,1)
UNION ALL
SELECT
SeqNo = SeqNo+1,
Original,
Num = SUBSTRING(Original,SeqNo+1,1)
FROM CTE
WHERE SeqNo < LEN(Original)
)
SELECT
MyStr = C1.Num+C2.Num+C3.Num+C4.Num
FROM CTE C1
CROSS JOIN CTE C2
CROSS JOIN CTE C3
CROSS JOIN CTE C4
WHERE
(
C1.SeqNo <> C2.SeqNo
AND
C3.SeqNo <> C4.SeqNo
AND
C4.SeqNo <> C1.SeqNo
AND
C2.SeqNo <> C3.SeqNo
AND
C1.SeqNo <> C3.SeqNo
AND
C4.SeqNo <> C2.SeqNo
)
ORDER BY 1
My Result
MyStr
2345
2354
2435
2453
2534
2543
3245
3254
3425
3452
3524
3542
4235
4253
4325
4352
4523
4532
5234
5243
5324
5342
5423
5432
Please try this. SET BASED Approach to generate all Possible combinations of a number-
IF OBJECT_ID('Tempdb..#T') IS NOT NULL
DROP TABLE tempdb..#T
DECLARE # AS INT = 1234
IF LEN(#) <= 7
BEGIN
DECLARE #str AS VARCHAR(100)
SET #str = CAST(# AS VARCHAR(100))
DECLARE #cols AS VARCHAR(100) = ''
SELECT DISTINCT SUBSTRING(#str,NUMBER,1) n INTO #T FROM MASTER..spt_values WHERE number > 0 AND number <= LEN(#)
SELECT #cols = #cols + r
FROM ( SELECT DISTINCT CONCAT(', o',number,'.n') r FROM MASTER..spt_values WHERE number > 0 AND number <= (LEN(#)-1) )q
DECLARE #ExecStr AS VARCHAR(1000) = ''
SET #ExecStr = 'SELECT CAST(CONCAT( a.n' + #cols + ' ) AS INT) Combinations FROM #T a'
SELECT #ExecStr = #ExecStr + r FROM
(
SELECT DISTINCT CONCAT(' CROSS APPLY ( SELECT * FROM #T b' , number , ' WHERE ( b' , number, '.n' , ' <> a.n ) ',
CASE WHEN number = 1 then ''
WHEN number = 2 then ' AND ( b2.n <> o1.n )'
WHEN number = 3 then ' AND ( b3.n <> o1.n ) AND ( b3.n <> o2.n ) '
WHEN number = 4 then ' AND ( b4.n <> o1.n ) AND ( b4.n <> o2.n ) AND ( b4.n <> o3.n ) '
WHEN number = 5 then ' AND ( b5.n <> o1.n ) AND ( b5.n <> o2.n ) AND ( b5.n <> o3.n ) AND ( b5.n <> o4.n ) '
WHEN number = 6 then ' AND ( b6.n <> o1.n ) AND ( b6.n <> o2.n ) AND ( b6.n <> o3.n ) AND ( b6.n <> o4.n ) AND ( b6.n <> o5.n ) '
END
,') o' , number ) r FROM
MASTER..spt_values
WHERE number > 0 AND number <= (LEN(#)-1)
)p
EXEC (#ExecStr)
END
IF OBJECT_ID('tempdb..#T') IS NOT NULL
DROP TABLE tempdb..#T
OUTPUT
1432
1342
1423
1243
1324
1234
2431
2341
2413
2143
2314
2134
3421
3241
3412
3142
3214
3124
4321
4231
4312
4132
4213
4123
from - https://msbiskills.com/2016/05/20/sql-puzzle-generate-possible-combinations-of-a-number-puzzle/
You can try following alternative SQL Script as well
declare #param varchar(4) = '1234'
;with combination as (
select
distinct rn = DENSE_RANK() over (Order By num), num
from (
select substring(#param,1,1) as num
union all
select substring(#param,2,1)
union all
select substring(#param,3,1)
union all
select substring(#param,4,1)
) t
)
select
c1.num, c2.num, c3.num, c4.num,
cast(c1.num as char(1)) + cast(c2.num as char(1)) + cast(c3.num as char(1)) + cast(c4.num as char(1)) as number
from combination c1, combination c2, combination c3, combination c4
It produces 256 numbers for 4 digits
Actually this code is from SQL code which returns non-repeatable combinations in SQL of given set of items, but modified it to enable repeats of items in the output

Split string and divide value

I got a table Test with columns A and B.
The A column contains different values in one entry, e.g. abc;def;ghi, all separated by ;. And the B column contains numeric values, but only one.
What I want is to seperate the values from column A into multiple rows.
So:
abc;def;ghi;jkl
-->
abc
def
ghi
jkl
In column B is one value, e.g. 20 and I want that value split to the amount of rows,
So the final result shut be:
abc 5
def 5
ghi 5
jkl 5
The issue is that the amount of values in column A must be variable.
First you need to create this function
REATE FUNCTION Split
(
#delimited nvarchar(max),
#delimiter nvarchar(100)
) RETURNS #t TABLE
(
-- Id column can be commented out, not required for sql splitting string
id int identity(1,1), -- I use this column for numbering splitted parts
val nvarchar(max),
origVal nvarchar(max)
)
AS
BEGIN
declare #xml xml
set #xml = N'<root><r>' + replace(#delimited,#delimiter,'</r><r>') + '</r></root>'
insert into #t(val,origval)
select
r.value('.','varchar(max)') as item, #delimited
from #xml.nodes('//root/r') as records(r)
RETURN
END
GO
then this query might help
Select x.Val, test.B / (len(test.A) - len(replace(Test.A, ';', '')) + 1) from Test
inner join dbo.Split(Test.A,';') x on x.origVal = Test.A
this part len(test.A) - len(replace(Test.A, ';', '')) will count the number of ; in string
Be aware this query might have some malfunctioning if there will be duplicate strings in A column, in this situation you need to pass the unique value (for example ID) to split function and return it in the result table, then join it by this value (ie. x.origVal = Test.A => x.origID = Test.ID)
You can use some tricks with CTE, STUFF and windows functions
DECLARE #t TABLE
(
ID INT ,
A NVARCHAR(MAX) ,
B INT
)
INSERT INTO #t
VALUES ( 1, 'a;b;c;d;', 20 ),
( 2, 'x;y;z;', 40 );
WITH cte ( ID, B, D, A )
AS ( SELECT ID ,
B ,
LEFT(A, CHARINDEX(';', A + ';') - 1) ,
STUFF(A, 1, CHARINDEX(';', A + ';'), '')
FROM #t
UNION ALL
SELECT ID ,
B ,
LEFT(A, CHARINDEX(';', A + ';') - 1) ,
STUFF(A, 1, CHARINDEX(';', A + ';'), '')
FROM cte
WHERE A > ''
)
SELECT ID ,
B ,
D,
CAST(B AS DECIMAL) / COUNT(*) OVER (PARTITION BY ID) AS Portion
FROM cte
Output:
ID B D Portion
1 20 a 5.00000000000
1 20 b 5.00000000000
1 20 c 5.00000000000
1 20 d 5.00000000000
2 40 x 13.33333333333
2 40 y 13.33333333333
2 40 z 13.33333333333
this an example how you can achieve required result
DECLARE #table AS TABLE
(
ColumnA VARCHAR(100) ,
ColumnB FLOAT
)
INSERT INTO #table
( ColumnA, ColumnB )
VALUES ( 'abc;def;ghi;jkl', 20 ),
( 'asf;ret;gsd;jas', 30 ),
( 'dfa;aef;gffhi;fjfkl', 40 );
WITH C AS ( SELECT n = 1
UNION ALL
SELECT n + 1
FROM C
WHERE n <= 100
),
SetForSplit
AS ( SELECT T.ColumnA ,
T.ColumnB ,
C.n ,
( CASE WHEN LEFT(SUBSTRING(T.ColumnA, n, 100), 1) = ';'
THEN SUBSTRING(T.ColumnA, n + 1, 100) + ';'
ELSE SUBSTRING(T.ColumnA, n, 100) + ';'
END ) AS SomeText
FROM #table AS T
JOIN C ON C.n <= LEN(T.ColumnA)
WHERE SUBSTRING(T.ColumnA, n, 1) = ';'
OR n = 1
)
SELECT ROW_NUMBER() OVER ( PARTITION BY columnA ORDER BY LEFT(SomeText,
CHARINDEX(';',
SomeText) - 1) ) AS RowN,
LEFT(SomeText, CHARINDEX(';', SomeText) - 1) AS ColA ,
ColumnB / COUNT(*) OVER ( PARTITION BY ColumnA ) AS ColB
FROM SetForSplit
ORDER BY ColumnA
This is full working exmaple:
DECLARE #DataSource TABLE
(
[A] VARCHAR(MAX)
,[B] INT
);
INSERT INTO #DataSource ([A], [B])
VALUES ('a;b;c;d', 20 ),
('x;y;z', 40 );
SELECT T.c.value('.', 'VARCHAR(100)')
,[B] / COUNT([B]) OVER (PARTITION BY [B])
FROM #DataSource
CROSS APPLY
(
SELECT CONVERT(XML, '<t>' + REPLACE([A], ';', '</t><t>') + '</t>')
) DS([Bxml])
CROSS APPLY [Bxml].nodes('/t') AS T(c)
and of couse you can ROUND the devision as you like.

Count Of Distinct Characters In Column

Say I have the following data set
Column1 (VarChar(50 or something))
Elias
Sails
Pails
Plane
Games
What I'd like to produce from this column is the following set:
LETTER COUNT
E 3
L 4
I 3
A 5
S 5
And So On...
One solution I thought of was combining all strings into a single string, and then count each instance of the letter in that string, but that feels sloppy.
This is more an exercise of curiosity than anything else, but, is there a way to get a count of all distinct letters in a dataset with SQL?
I would do this by creating a table of your letters similar to:
CREATE TABLE tblLetter
(
letter varchar(1)
);
INSERT INTO tblLetter ([letter])
VALUES
('a'),
('b'),
('c'),
('d'); -- etc
Then you could join the letters to your table where your data is like the letter:
select l.letter, count(n.col) Total
from tblLetter l
inner join names n
on n.col like '%'+l.letter+'%'
group by l.letter;
See SQL Fiddle with Demo. This would give a result:
| LETTER | TOTAL |
|--------|-------|
| a | 5 |
| e | 3 |
| g | 1 |
| i | 3 |
| l | 4 |
| m | 1 |
| p | 2 |
| s | 4 |
If you create a table of letters, like this:
create table letter (ch char(1));
insert into letter(ch) values ('A'),('B'),('C'),('D'),('E'),('F'),('G'),('H')
,('I'),('J'),('K'),('L'),('M'),('N'),('O'),('P')
,('Q'),('R'),('S'),('T'),('U'),('V'),('W'),('X'),('Y'),('Z');
you could do it with a cross join, like this:
select ch, SUM(len(str) - len(replace(str,ch,'')))
from letter
cross join test -- <<== test is the name of the table with the string
group by ch
having SUM(len(str) - len(replace(str,ch,''))) <> 0
Here is a running demo on sqlfiddle.
You can do it without defining a table by embedding a list of letters into a query itself, but the idea of cross-joining and grouping by the letter would remain the same.
Note: see this answer for the explanation of the expression inside the SUM.
To me, this is a problem almost tailored for a CTE (Thanks, Nicholas Carey, for the original, my fiddle here: http://sqlfiddle.com/#!3/44f77/8):
WITH cteLetters
AS
(
SELECT
1 AS CharPos,
str,
MAX(LEN(str)) AS MaxLen,
SUBSTRING(str, 1, 1) AS Letter
FROM
test
GROUP BY
str,
SUBSTRING(str, 1, 1)
UNION ALL
SELECT
CharPos + 1,
str,
MaxLen,
SUBSTRING(str, CharPos + 1, 1) AS Letter
FROM
cteLetters
WHERE
CharPos + 1 <= MaxLen
)
SELECT
UPPER(Letter) AS Letter,
COUNT(*) CountOfLetters
FROM
cteLetters
GROUP BY
Letter
ORDER BY
Letter;
Use the CTE to calculate character positions and deconstruct each string. Then you can just aggregate from the CTE itself. No need for additional tables or anything.
This should work even if you have case sensitivity turned on.
The setup:
CREATE TABLE _test ( Column1 VARCHAR (50) )
INSERT _test (Column1) VALUES ('Elias'),('Sails'),('Pails'),('Plane'),('Games')
The work:
DECLARE #counter AS INT
DECLARE #results TABLE (LETTER VARCHAR(1),[COUNT] INT)
SET #counter=65 --ascii value for 'A'
WHILE ( #counter <=90 ) -- ascii value for 'Z'
BEGIN
INSERT #results (LETTER,[COUNT])
SELECT CHAR(#counter),SUM(LEN(UPPER(Column1)) - LEN(REPLACE(UPPER(Column1), CHAR(#counter),''))) FROM _test
SET #counter=#counter+1
END
SELECT * FROM #results WHERE [Count]>0
It's often useful to have a range or sequence table that gives you a source of large runs of contiguous sequential numbers, like this one covering the range -100,000–+100,000.
drop table dbo.range
go
create table dbo.range
(
id int not null primary key clustered ,
)
go
set nocount on
go
declare #i int = -100000
while ( #i <= +100000 )
begin
if ( #i > 0 and #i % 1000 = 0 ) print convert(varchar,#i) + ' rows'
insert dbo.range values ( #i )
set #i = #i + 1
end
go
set nocount off
go
Once you have such a table, you can do something like this:
select character = substring( t.some_column , r.id , 1 ) ,
frequency = count(*)
from dbo.some_table t
join dbo.range r on r.id between 1 and len( t.some_column )
group by substring( t.some_column , r.id , 1 )
order by 1
If you want to ensure case-insensitivity, just mix in the desired upper() or lower():
select character = upper( substring( t.some_column , r.id , 1 ) ) ,
frequency = count(*)
from dbo.some_table t
join dbo.range r on r.id between 1 and len( t.some_column )
group by upper( substring( t.some_column , r.id , 1 ) )
order by 1
Given your sample data:
create table dbo.some_table
(
some_column varchar(50) not null
)
go
insert dbo.some_table values ( 'Elias' )
insert dbo.some_table values ( 'Sails' )
insert dbo.some_table values ( 'Pails' )
insert dbo.some_table values ( 'Plane' )
insert dbo.some_table values ( 'Games' )
go
The latter query above produces the following results:
character frequency
A 5
E 3
G 1
I 3
L 4
M 1
N 1
P 2
S 5