How to sum data from string - sql-server

Have table :
id name
1 A1=7|A5=1|A10=5|A20=12|A50=8
2 A1=10|A5=2|A10=10|A20=14|A50=4
3 A1=3|A5=3|A10=5|A20=12|A50=8
.
.
Want sum all A1,A5,A10,A20,A50
Response must be like :
A1=20|A5=6|A10=20|A20=38|A50=20
How to do it ?

I also upvoted comment about changing table design but there are situations when somebody cannot do this. So here is the solution for this case and it's not so difficult. We call XML for assistance as usual when need to process formatted strings in XML columns.
-- Prepare data for solution testing
DECLARE #srctable TABLE (
id INT,
name VARCHAR(999),
namexml XML
)
INSERT INTO #srctable
SELECT id, name, namexml FROM ( VALUES
(1, 'A1=7|A5=1|A10=5|A20=12|A50=8', null),
(2, 'A1=10|A5=2|A10=10|A20=14|A50=4', null),
(3, 'A1=3|A5=3|A10=5|A20=12|A50=8', null)
) v (id, name, namexml)
-- Transform source formatted string to XML string
UPDATE #srctable
SET namexml = CAST('<row><data ' + REPLACE(REPLACE(name, '|', '"/><data '), '=', '="') + '"/></row>' AS XML)
-- Final select from XML data
SELECT SUM(x.data.value('(#A1)[1]', 'INT')) AS SUMA1,
SUM(x.data.value('(#A5)[1]', 'INT')) AS SUMA5,
SUM(x.data.value('(#A10)[1]', 'INT')) AS SUMA10,
SUM(x.data.value('(#A20)[1]', 'INT')) AS SUMA20,
SUM(x.data.value('(#A50)[1]', 'INT')) AS SUMA50
FROM #srctable AS t
CROSS APPLY t.namexml.nodes('/row/data') x (data)
You need to format your resulting string in any way you wish.

Variant without xml:
--------------------------------------------------------------------------------
-- Prepare data for solution testing
DECLARE #srctable TABLE ( id INT
, NAME VARCHAR(999) )
INSERT INTO #srctable
VALUES ( 1, 'A1=7|A5=1|A10=5|A20=12|A50=8' )
, ( 2, 'A1=10|A5=2|A10=10|A20=14|A50=4' )
, ( 3, 'A1=3|A5=3|A10=5|A20=12|A50=8' )
--------------------------------------------------------------------------------
-- prepare temp table for using in split string
DECLARE #Tally TABLE ( N INT )
DECLARE #i AS INT = 1
WHILE #i != 1000
BEGIN
INSERT INTO #Tally ( N )
VALUES ( #i )
SET #i = #i + 1
END
--------------------------------------------------------------------------------
--final query
;WITH cte AS
(
SELECT id,
(CASE WHEN CHARINDEX('|', S.string) > 0
THEN left(S.string, CHARINDEX('|', S.string) - 1)
ELSE string END ) NAME
FROM #srctable AS E
INNER JOIN #Tally AS T ON SUBSTRING('|' + NAME, T.N, 1) = '|'
AND T.N <= LEN(NAME)
CROSS APPLY (SELECT String = (CASE WHEN T.N = 1
THEN LEFT(E.NAME, CHARINDEX('|', E.NAME) - 1)
ELSE SUBSTRING(E.NAME, T.N, 1000) END )
) AS S
)
SELECT LEFT(NAME, CHARINDEX('=', NAME) - 1) AS NAME,
SUM(convert(float,RIGHT(NAME, CHARINDEX('=', REVERSE(NAME)) - 1))) AS Value
FROM cte
GROUP BY LEFT(NAME, CHARINDEX('=', NAME) - 1)

Related

filter out all datetime and integer values from a column in SQL SERVER

I have a string
04/09/2018 06:21:38 101342 CHARLESD JOHNSON:713-269-1878 CALL WHEN WE GET A PO 06/09/2018 08:41:38 101345 KHARLESD KOHNSON:813-269-1878 CALL WHEN WE GET A PO 08/09/2018 09:41:38 10356 THARLESD TOHNSON:913-269-1878 CALL WHEN WE GET A PO
I want output like
DateTime1 | EmpID
04/09/2018 06:21:38 101342
06/09/2018 08:41:38 101345
08/09/2018 09:41:38 10356
Please help
Create the function PatternSplitLoop from this awesome article:
Splitting Strings Based on Patterns
and execute the following:
declare #tab table (string varchar(max))
insert into #tab select '04/09/2018 06:21:38 101342 CHARLESD JOHNSON:713-269-1878 CALL WHEN WE GET A PO 06/09/2018 08:41:38 101345 KHARLESD KOHNSON:813-269-1878 CALL WHEN WE GET A PO 08/09/2018 09:41:38 10356 THARLESD TOHNSON:913-269-1878 CALL WHEN WE GET A PO '
select left(item, 19) DateTime1, substring(item, 20, len(item)) EmpID
from #tab t
cross apply [dbo].[PatternSplitLoop](string, '%[0-9][0-9][/][0-9][0-9][/][0-9][0-9][0-9][0-9]%') f
where matched = 1
Output:
XML split version:
DECLARE #Val NVARCHAR(MAX) = '04/09/2018 06:21:38 101342 CHARLESD JOHNSON:713-269-1878 CALL WHEN WE GET A PO 06/09/2018 08:41:38 101345 KHARLESD KOHNSON:813-269-1878 CALL WHEN WE GET A PO 08/09/2018 09:41:38 10356 THARLESD TOHNSON:913-269-1878 CALL WHEN WE GET A PO '
SELECT #Val = '<a>' + REPLACE(#Val, ' ', '</a><a>') + '</a>';
DECLARE #Xml XML = CONVERT(XML, #Val);
DECLARE #ValTable TABLE
(
ROWNUM INT IDENTITY(1, 1),
Val NVARCHAR(MAX)
)
INSERT into #ValTable
(Val)
SELECT *
FROM
(
SELECT c.value('.', 'NVARCHAR(64)') AS Val
FROM #Xml.nodes('/a') T(c)
) a
WHERE LEN(Val) <> 0
AND (TRY_CONVERT(DATETIME, Val) IS NOT NULL OR TRY_CONVERT(TIME, Val) IS NOT NULL OR TRY_CONVERT(INT, Val) IS NOT NULL);
SELECT CONVERT(DATETIME, CONCAT(a.Val, ' ', b.Val)) AS DateTime1, c.Val AS EmpId
FROM #ValTable a
JOIN #ValTable b
--Date and Time rows, B is time row
ON TRY_CONVERT(TIME, a.Val) IS NOT NULL AND TRY_CONVERT(TIME, b.Val) IS NOT NULL AND b.ROWNUM = a.ROWNUM + 1
-- Int rows
JOIN #ValTable c
ON TRY_CONVERT(INT, c.Val) IS NOT NULL AND c.ROWNUM = a.RowNum + 2;

Apply WHILE logic on Row-by-Row processing with Cursor

I have T-SQL script that is parsing the MDX expression. It looks as:
IF OBJECT_ID ( 'tempdb..#metrics' ) IS NOT NULL
DROP TABLE #metrics
CREATE TABLE #metrics (
Metric VARCHAR(255)
)
---
DECLARE #counter INT = 1
DECLARE #mdx VARCHAR(4000) = 'SELECT {[Measures].[One],[Measures].[Two],[Measures].[Three],[Measures].[Four]} DIMENSION, PROPERTIES OTHER'
DECLARE #startString INT
DECLARE #endString INT
DECLARE #metric VARCHAR(200)
WHILE (1=1)
BEGIN
-- loop data and process them
SET #startString = (SELECT PATINDEX('%[[]Measures%',#mdx))
SET #endString = (SELECT CHARINDEX(',',#mdx))
SET #metric = (SELECT SUBSTRING(#mdx, #startString, #endString - #startString))
IF #metric LIKE '%}%'
BEGIN
SET #metric = LEFT(#metric, CHARINDEX('}',#metric) - 1)
INSERT INTO #metrics ( Metric ) SELECT #metric
SET #mdx = REPLACE(#mdx, #metric, '')
END
ELSE
BEGIN
INSERT INTO #metrics ( Metric ) SELECT #metric
SET #metric = #metric + ','
SET #mdx = REPLACE(#mdx, #metric, '')
END
-- break while
IF #mdx NOT LIKE '%[[]Measures%,%'
BEGIN
BREAK;
END
END
---
SELECT * FROM #metrics
Now, I need to apply this on more rows, but did not figure out how. I tried it with cursor, but it never ends. How to loop the logic on the following rows?
DECLARE #srcTable TABLE (
ID INT
,textData VARCHAR(4000)
)
INSERT INTO #srcTable ( ID, textData ) ( 1, 'SELECT {[Measures].[One],[Measures].[Two],[Measures].[Three],[Measures].[Four]} DIMENSION, PROPERTIES OTHER' )
,(2, 'SELECT {[Measures].[Five],[Measures].[Six],[Measures].[Seven]} DIMENSION, PROPERTIES OTHER' )
Desired Result:
1 [Measures].[One]
1 [Measures].[Two]
1 [Measures].[Three]
1 [Measures].[Four]
2 [Measures].[Five]
2 [Measures].[Six]
2 [Measures].[Seven]
DECLARE #t TABLE (
ID INT,
Metric VARCHAR(255)
)
INSERT INTO #t
VALUES
(1, 'SELECT {[Measures].[One],[Measures].[Two],[Measures].[Three],[Measures].[Four]} DIMENSION, PROPERTIES OTHER'),
(2, 'SELECT {[Measures].[Five],[Measures].[Six],[Measures].[Seven]} DIMENSION, PROPERTIES OTHER')
SELECT r.ID, item = t.c.value('.', 'VARCHAR(255)')
FROM (
SELECT *, txml = CAST('<r>' + REPLACE(Metric, ',', '</r><r>') + '</r>' AS XML)
FROM (
SELECT ID, Metric = SUBSTRING(Metric, CHARINDEX('{',Metric) + 1, CHARINDEX('}',Metric) - CHARINDEX('{',Metric) - 1)
FROM #t
) t
) r
CROSS APPLY txml.nodes('/r') t(c)
Output -
ID item
----------- -----------------------
1 [Measures].[One]
1 [Measures].[Two]
1 [Measures].[Three]
1 [Measures].[Four]
2 [Measures].[Five]
2 [Measures].[Six]
2 [Measures].[Seven]

subquery with table value function not working

DECLARE #temp AS TABLE (id INT, NAME VARCHAR(20) )
DECLARE #str VARCHAR(20) = '1,2'
INSERT INTO #temp (id, NAME)
VALUES (1, ''), (2, ''), (2, '')
SELECT *
FROM #temp a
WHERE id IN ((SELECT String FROM dbo.FN_SplitStrings(#str,',')))
I'm getting the following error while running this
Conversion failed when converting the varchar value '1,2' to data type
int.
Code:
CREATE function [dbo].[FN_SplitStrings]
(
#StringToSplit varchar(8000),
#Separator varchar(128)
)
RETURN TABLE
AS
RETURN
with indices as
(
select
0 S, 1 E
union all
select
E, charindex(#Separator, #StringToSplit, E) + len(#Separator)
from
indices
where E > S
)
select
substring(#StringToSplit,S, case when E > len(#Separator)
then e-s-len(#Separator) else len(#StringToSplit) - s + 1 end) String ,
S StartIndex
from
indices
where
S > 0
Try this. This splitting can be used without a function
DECLARE #temp AS TABLE
(
id INT,
NAME VARCHAR(20)
)
DECLARE #str VARCHAR(20)='1,2'
INSERT INTO #temp
( id, NAME )
VALUES ( 1, '' ),
( 2, ''),
( 2, '')
SELECT * FROM #temp a
WHERE id IN
(
SELECT LTRIM(RTRIM(Split.a.value('.', 'VARCHAR(100)'))) 'KeyWords'
FROM
(
-- To change ',' to any other delimeter, just change ',' before '</M><M>' to your desired one
SELECT CAST ('<M>' + REPLACE(#str, ',', '</M><M>') + '</M>' AS XML) AS Data
) AS A
CROSS APPLY Data.nodes ('/M') AS Split(a)
)
Click here to view the result
EDIT:
You had some problems in function for splitting.
Function
ALTER FUNCTION dbo.FN_SplitStrings(#StringToSplit varchar(8000),#Separator char(1))
RETURNS table
AS
RETURN (
WITH splitter_cte AS (
SELECT CHARINDEX(#Separator, #StringToSplit) as pos, 0 as lastPos
UNION ALL
SELECT CHARINDEX(#Separator, #StringToSplit, pos + 1), pos
FROM splitter_cte
WHERE pos > 0
)
SELECT SUBSTRING(#StringToSplit, lastPos + 1,
case when pos = 0 then 80000
else pos - lastPos -1 end) as String
FROM splitter_cte
)
Query
DECLARE #temp AS TABLE (id INT, NAME VARCHAR(20) )
DECLARE #str VARCHAR(20) = '1,2'
INSERT INTO #temp (id, NAME)
VALUES (1, ''), (2, ''), (2, '')
SELECT *
FROM #temp a
WHERE id IN ((SELECT String FROM dbo.FN_SplitStrings(#str,',')))
Click here to view result
The reason is a mix in data types and the fact that your function is an inline table valued functions which means that it is embedded into the query before query optimization takes place.
If you remove where S > 0 from your function and execute it with 1,2, the result of the function is:
String
------
1,2
1
2
Notice the first row where the value is 1,2.
When the optimizer does its job with your query the comparison against the column id is done before the where clause of the function is evaluated. In that comparison you have an implicit cast to int and 1,2 can not be casted to an int.
To fix this you can make sure that the String column of your split function is always an int (and perhaps changing the name of the column in the process).
select
cast(substring(#StringToSplit,S, case when E > len(#Separator)
then e-s-len(#Separator)
else len(#StringToSplit) - s + 1
end) as int) String ,

SQL Server: collect values in an aggregation temporarily and re-use in the same query

How do I accumulate values in T-SQL? AFAIK there is no ARRAY type.
I want to re-use the values in the same query like demonstrated in this PostgreSQL example using array_agg().
SELECT a[1] || a[i] AS foo
, a[2] || a[5] AS bar -- assuming we have >= 5 rows for simplicity
FROM (
SELECT array_agg(text_col ORDER BY text_col) AS a
, count(*)::int4 AS i
FROM tbl
WHERE id BETWEEN 10 AND 100
) sub;
How would I best solve this with T-SQL?
Best I could come up with are two CTE and subselects:
;WITH x AS (
SELECT row_number() OVER (ORDER BY name) AS rn
, name AS a
FROM #t
WHERE id BETWEEN 10 AND 100
)
, i AS (
SELECT count(*) AS i
FROM x
)
SELECT (SELECT a FROM x WHERE rn = 1) + (SELECT a FROM x WHERE rn = i) AS foo
, (SELECT a FROM x WHERE rn = 2) + (SELECT a FROM x WHERE rn = 5) AS bar
FROM i;
Test setup:
CREATE TABLE #t(
id INT PRIMARY KEY
, name NVARCHAR(100))
;
INSERT INTO #t VALUES
( 3, 'John')
, ( 5, 'Mary')
, ( 8, 'Michael')
, (13, 'Steve')
, (21, 'Jack')
, (34, 'Pete')
, (57, 'Ami')
, (88, 'Bob')
;
Is there a simpler way?
Edit 1: I have added another solution that shows how to simulate ARRAY_AGG on SQL Server (the last answer).
Edit 2: For the solution number 4) I have added the third method for concatenation.
I'm not sure I have I understood correctly your question.
a) Instead of using arrays in SQL Server I would use table variables or XML.
b) To concatenate strings (in this case) I would use SELECT #var = #var + Name FROM tbl statements or XML xqueries.
c) The solution based on CTEs and multiple subqueries (WITH cte AS () FROM SELECT (SELECT * FROM cte.rn=1) + ()...) will generates a lot of scans and logical reads.
Solutions:
1) Table variable + SELECT #var = #var + Name FROM tbl:
--Creating the "array"
DECLARE #Array TABLE
(
Idx INT PRIMARY KEY,
Val NVARCHAR(100) NOT NULL
);
WITH Base
AS
(
SELECT Val = t.name,
Idx = ROW_NUMBER() OVER(ORDER BY t.name ASC)
FROM #t t
WHERE t.id between 10 AND 100
)
INSERT #Array (Idx, Val)
SELECT b.Idx, b.Val
FROM Base b;
--Concatenating all names
DECLARE #AllNames NVARCHAR(4000);
--”Reset”/Init #AllNames
SET #AllNames = '';
--String concatenation
SELECT #AllNames = #AllNames + ',' + a.Val
FROM #Array a;
--Remove first char (',')
SELECT #AllNames = STUFF(#AllNames, 1, 1, '');
--The final result
SELECT #AllNames [Concatenating all names - using a table variable];
/*
Concatenating all names - using a table variable
------------------------------------------------
Ami,Bob,Jack,Pete,Steve
*/
--Concatenating Idx=2 and Idx=5
--”Reset” #AllNames value
SET #AllNames = '';
--String concatenation
SELECT #AllNames = #AllNames + ',' + a.Val
FROM #Array a
WHERE a.Idx IN (2,5) --or a.Idx IN (2, (SELECT COUNT(*) FROM #Array))
ORDER BY a.Idx ASC;
--Remove first char (',')
SELECT #AllNames = STUFF(#AllNames, 1, 1, '');
--The final result
SELECT #AllNames [Concatenating Idx=2 and Idx=5 - using a table variable];
/*
Concatenating Idx=2 and Idx=5 - using a table variable
------------------------------------------------------
Bob,Steve
*/
2) Table variable + PIVOT:
--Concatenating a finite number of elements (names)
SELECT pvt.[1] + ',' + pvt.[0] AS [PIVOT Concat_1_and_i(0)]
,pvt.[2] + ',' + pvt.[5] AS [PIVOT Concat_2_and_5]
,pvt.*
FROM
(
SELECT a.Idx, a.Val
FROM #Array a
WHERE a.Idx IN (1,2,5)
UNION ALL
SELECT 0, a.Val --The last element has Idx=0
FROM #Array a
WHERE a.Idx = (SELECT COUNT(*) FROM #Array)
) src
PIVOT (MAX(src.Val) FOR src.Idx IN ([1], [2], [5], [0])) pvt;
/*
PIVOT Concat_1_and_i(0) PIVOT Concat_2_and_5
----------------------- --------------------
Ami,Steve Bob,Steve
*/
3) XML + XQuery:
SET ANSI_WARNINGS ON;
GO
DECLARE #x XML;
;WITH Base
AS
(
SELECT Val = t.name,
Idx = ROW_NUMBER() OVER(ORDER BY t.name ASC)
FROM #t t
WHERE t.id BETWEEN 10 AND 100
)
SELECT #x =
(
SELECT b.Idx AS [#Idx]
,b.Val AS [text()]
FROM Base b
FOR XML PATH('Element'), ROOT('Array')
);
/* #x content
<Array>
<Element Idx="1">Ami</Element>
<Element Idx="2">Bob</Element>
<Element Idx="3">Jack</Element>
<Element Idx="4">Pete</Element>
<Element Idx="5">Steve</Element>
</Array>
*/
--Concatenating all names (the result is XML, so a cast is needed)
DECLARE #r XML; --XML result
SELECT #r=#x.query('
(: $e = array element :)
for $e in (//Array/Element)
return string($e)
');
SELECT REPLACE(CONVERT(NVARCHAR(4000), #r), ' ', ',') AS [Concatenating all names - using XML];
/*
Concatenating all names - using XML
-----------------------------------
Ami,Bob,Jack,Pete,Steve
*/
--Concatenating Idx=1 and all names
SELECT #r=#x.query('
(: $e = array element :)
for $e in (//Array/Element[#Idx=1], //Array/Element)
return string($e)
');
SELECT REPLACE(CONVERT(NVARCHAR(4000), #r), ' ', ',') AS [Concatenating Idx=1 and all names - using XML];
/*
Concatenating Idx=1 and all names - using XML
---------------------------------------------
Ami,Ami,Bob,Jack,Pete,Steve
*/
--Concatenating Idx=1 and i(last name)
DECLARE #i INT;
SELECT #r=#x.query('
(: $e = array element :)
for $e in (//Array/Element[#Idx=1], //Array/Element[#Idx=count(//Array/Element)])
return string($e)
');
SELECT REPLACE(CONVERT(NVARCHAR(4000), #r), ' ', ',') AS [Concatenating Idx=1 and i(last name) - using XML];
/*
Concatenating Idx=1 and i(last name) - using XML
------------------------------------------------
Ami,Steve
*/
--Concatenating Idx=2 and Idx=5
SELECT #r=#x.query('
(: $e = array element :)
for $e in (//Array/Element[#Idx=2], //Array/Element[#Idx=5])
return string($e)
');
SELECT REPLACE(CONVERT(NVARCHAR(4000), #r), ' ', ',') AS [Concatenating Idx=2 and Idx=5 - using XML (method 1)];
/*
Concatenating Idx=2 and Idx=5 - using XML (method 1)
----------------------------------------------------
Bob,Steve
*/
--Concatenating Idx=2 and Idx=5
SELECT #x.value('(//Array/Element)[#Idx=2][1]', 'NVARCHAR(100)')
+ ','
+ #x.value('(//Array/Element)[#Idx=5][1]', 'NVARCHAR(100)') AS [Concatenating Idx=2 and Idx=5 - using XML (method 2)];;
/*
Concatenating Idx=2 and Idx=5 - using XML (method 2)
----------------------------------------------------
Bob,Steve
*/
4) If the question is how to simulate ARRAY_AGG on SQL Server then, one answer might be: by using XML.
Example:
SET ANSI_WARNINGS ON;
GO
DECLARE #Test TABLE
(
Id INT PRIMARY KEY
,GroupID INT NOT NULL
,Name NVARCHAR(100) NOT NULL
);
INSERT INTO #Test (Id, GroupID, Name)
VALUES
(3 , 1, 'John')
,(5 , 1, 'Mary')
,(8 , 1, 'Michael')
,(13, 1, 'Steve')
,(21, 1, 'Jack')
,(34, 2, 'Pete')
,(57, 2, 'Ami')
,(88, 2, 'Bob');
WITH BaseQuery
AS
(
SELECT a.GroupID, a.Name
FROM #Test a
WHERE a.Id BETWEEN 10 AND 100
)
SELECT x.*
, CONVERT(XML,x.SQLServer_Array_Agg).query
('
for $e in (//Array/Element[#Idx=1], //Array/Element[#Idx=count(//Array/Element)])
return string($e)
') AS [Concat Idx=1 and Idx=i (method 1)]
, CONVERT(XML,x.SQLServer_Array_Agg).query('
let $a := string((//Array/Element[#Idx=1])[1])
let $b := string((//Array/Element[#Idx=count(//Array/Element)])[1])
let $c := concat($a , "," , $b) (: " is used as a string delimiter :)
return $c
') AS [Concat Idx=1 and Idx=i (method 2)]
, CONVERT(XML,x.SQLServer_Array_Agg).query
('
for $e in (//Array/Element[#Idx=(1,count(//Array/Element))])
return string($e)
') AS [Concat Idx=1 and Idx=i (method 3)]
FROM
(
SELECT a.GroupID
,(SELECT ROW_NUMBER() OVER(ORDER BY b.Name) AS [#Idx]
,b.Name AS [text()]
FROM BaseQuery b
WHERE a.GroupID = b.GroupID
ORDER BY b.Name
FOR XML PATH('Element'), ROOT('Array') ) AS SQLServer_Array_Agg
FROM BaseQuery a
GROUP BY a.GroupID
) x;
Results:
GroupID SQLServer_Array_Agg Concat Idx=1 and Idx=i (method 1) Concat Idx=1 and Idx=i (method 2) Concat Idx=1 and Idx=i (method 3)
------- ---------------------------------------------------------------------------------------------------------- --------------------------------- --------------------------------- ---------------------------------
1 <Array><Element Idx="1">Jack</Element><Element Idx="2">Steve</Element></Array> Jack Steve Jack,Steve Jack Steve
2 <Array><Element Idx="1">Ami</Element><Element Idx="2">Bob</Element><Element Idx="3">Pete</Element></Array> Ami Pete Ami,Pete Ami Pete
If you're just collecting some values to reuse, try a table variable rather than a temp table
DECLARE #t TABLE
(
id INT PRIMARY KEY,
name NVARCHAR(100)
)
INSERT #t VALUES (3 , 'John')
-- etc
The table variable is in-memory only, instead of going in the tempdb database like a temp table does.
Check Should I use a #temp table or table variable for more information.
Not sure if this helps, but you can always...
select * into #MyTempTable from SomeTable

Recursive CTE Problem

I am trying to use a recursive CTE in SQL Server to build up a predicate formula from a table containing the underlying tree structure.
For example, my table looks like:
Id | Operator/Val | ParentId
--------------------------
1 | 'OR' | NULL
2 | 'AND' | 1
3 | 'AND' | 1
4 | '>' | 2
5 | 'a' | 4
6 | 'alpha' | 4
...
...which represents ((a > alpha) AND (b > beta)) OR ((c > gamma) AND (a < delta)).
ParentId is a reference to the Id in the same table of the parent node.
I want to write a query which will build up this string from the table. Is it possible?
Thanks
For a production environment, you may want to go with a recursive function for simplicity if performance and recursion depth limits (32 levels) is not a problem.
However, here's a quite clean and pretty efficient solution with CTEs (note that it will accept any number of "trees" and return one result for each item which has no parent):
DECLARE #tbl TABLE
(
id int PRIMARY KEY
NOT NULL,
op nvarchar(max) NOT NULL,
parent int
) ;
INSERT INTO #tbl
SELECT 1, 'OR', NULL UNION ALL
SELECT 2, 'AND', 1 UNION ALL
SELECT 3, 'AND', 1 UNION ALL
SELECT 4, '>', 2 UNION ALL
SELECT 5, 'a', 4 UNION ALL
SELECT 6, 'alpha', 4 UNION ALL
SELECT 7, '>', 2 UNION ALL
SELECT 8, 'b', 7 UNION ALL
SELECT 9, 'beta', 7 UNION ALL
SELECT 10, '>', 3 UNION ALL
SELECT 11, 'c', 10 UNION ALL
SELECT 12, 'gamma', 10 UNION ALL
SELECT 13, '>', 3 UNION ALL
SELECT 14, 'd', 13 UNION ALL
SELECT 15, 'delta', 13 ;
WITH nodes -- A CTE which sets a flag to 1 for non-leaf nodes
AS (
SELECT t.*, CASE WHEN p.parent IS NULL THEN 0
ELSE 1
END node
FROM #tbl t
LEFT JOIN (
SELECT DISTINCT parent
FROM #tbl
) p ON p.parent = T.id
),
rec -- the main recursive run to determine the sort order and add meta information
AS (
SELECT id rootId, node lvl, CAST(0 AS float) sort, CAST(0.5 AS float) offset, *
FROM nodes
WHERE parent IS NULL
UNION ALL
SELECT r.rootId, r.lvl+t.node, r.sort+r.offset*CAST((ROW_NUMBER() OVER (ORDER BY t.id)-1)*2-1 AS float),
r.offset/2, t.*
FROM rec r
JOIN
nodes t ON r.id = t.parent
),
ranked -- ranking of the result to sort and find the last item
AS (
SELECT rootId, ROW_NUMBER() OVER (PARTITION BY rootId ORDER BY sort) ix,
COUNT(1) OVER (PARTITION BY rootId) cnt, lvl, op
FROM rec
),
concatenated -- concatenate the string, adding ( and ) as needed
AS (
SELECT rootId, ix, cnt, lvl, CAST(REPLICATE('(', lvl)+op AS nvarchar(max)) txt
FROM ranked
WHERE ix = 1
UNION ALL
SELECT r.rootId, r.ix, r.cnt, r.lvl,
c.txt+COALESCE(REPLICATE(')', c.lvl-r.lvl), '')+' '+COALESCE(REPLICATE('(', r.lvl-c.lvl), '')+r.op
+CASE WHEN r.ix = r.cnt THEN REPLICATE(')', r.lvl)
ELSE ''
END
FROM ranked r
JOIN
concatenated c ON (r.rootId = c.rootId)
AND (r.ix = c.ix+1)
)
SELECT rootId id, txt
FROM concatenated
WHERE ix = cnt
OPTION (MAXRECURSION 0);
I found something, but it looks pretty nasty. You would be able to do this a lot easier using a recursive fundtion...
DECLARE #Table TABLE(
ID INT,
Op VARCHAR(20),
ParentID INT
)
INSERT INTO #Table SELECT 1,'OR',NULL
INSERT INTO #Table SELECT 2,'AND',1
INSERT INTO #Table SELECT 3,'AND',1
INSERT INTO #Table SELECT 4,'>',2
INSERT INTO #Table SELECT 5,'a',4
INSERT INTO #Table SELECT 6,'alpha',4
INSERT INTO #Table SELECT 7,'>',2
INSERT INTO #Table SELECT 8,'b',7
INSERT INTO #Table SELECT 9,'beta',7
INSERT INTO #Table SELECT 10,'>',3
INSERT INTO #Table SELECT 11,'c',10
INSERT INTO #Table SELECT 12,'gamma',10
INSERT INTO #Table SELECT 13,'<',3
INSERT INTO #Table SELECT 14,'a',13
INSERT INTO #Table SELECT 15,'delta',13
;WITH Vals AS (
SELECT t.*,
1 Depth
FROM #Table t LEFT JOIN
#Table parent ON t.ID = parent.ParentID
WHERE parent.ParentID IS NULL
UNION ALL
SELECT t.*,
v.Depth + 1
FROM #Table t INNER JOIN
Vals v ON v.ParentID = t.ID
),
ValLR AS(
SELECT DISTINCT
vLeft.ID LeftID,
vLeft.Op LeftOp,
vRight.ID RightID,
vRight.Op RightOp,
vLeft.ParentID OperationID,
vLeft.Depth
FROM Vals vLeft INNER JOIN
Vals vRight ON vLeft.ParentID = vRight.ParentID
AND vLeft.ID < vRight.ID
WHERE (vRight.ID IS NOT NULL)
),
ConcatVals AS(
SELECT CAST('(' + LeftOp + ' ' + Op + ' ' + RightOp + ')' AS VARCHAR(500)) ConcatOp,
t.ID OpID,
v.Depth,
1 CurrentDepth
FROM ValLR v INNER JOIN
#Table t ON v.OperationID = t.ID
WHERE v.Depth = 1
UNION ALL
SELECT CAST('(' + cL.ConcatOp + ' ' + t.Op + ' {' + CAST(v.RightID AS VARCHAR(10)) + '})' AS VARCHAR(500)) ConcatOp,
t.ID OpID,
v.Depth,
cL.CurrentDepth + 1
FROM ValLR v INNER JOIN
#Table t ON v.OperationID = t.ID INNER JOIN
ConcatVals cL ON v.LeftID = cL.OpID
WHERE v.Depth = cL.CurrentDepth + 1
),
Replaces AS(
SELECT REPLACE(
c.ConcatOp,
SUBSTRING(c.ConcatOp,PATINDEX('%{%', c.ConcatOp), PATINDEX('%}%', c.ConcatOp) - PATINDEX('%{%', c.ConcatOp) + 1),
(SELECT ConcatOp FROM ConcatVals WHERE OpID = CAST(SUBSTRING(c.ConcatOp,PATINDEX('%{%', c.ConcatOp) + 1, PATINDEX('%}%', c.ConcatOp) - PATINDEX('%{%', c.ConcatOp) - 1) AS INT))
) ConcatOp,
1 Num
FROM ConcatVals c
WHERE Depth = (SELECT MAX(Depth) FROM ConcatVals)
UNION ALL
SELECT REPLACE(
r.ConcatOp,
SUBSTRING(r.ConcatOp,PATINDEX('%{%', r.ConcatOp), PATINDEX('%}%', r.ConcatOp) - PATINDEX('%{%', r.ConcatOp) + 1),
(SELECT ConcatOp FROM ConcatVals WHERE OpID = CAST(SUBSTRING(r.ConcatOp,PATINDEX('%{%', r.ConcatOp) + 1, PATINDEX('%}%', r.ConcatOp) - PATINDEX('%{%', r.ConcatOp) - 1) AS INT))
) ConcatOp,
Num + 1
FROM Replaces r
WHERE PATINDEX('%{%', r.ConcatOp) > 0
)
SELECT TOP 1
*
FROM Replaces
ORDER BY Num DESC
OUTPUT
ConcatOp
----------------------------------------------------------------
(((a > alpha) AND (b > beta)) OR ((c > gamma) AND (a < delta)))
If you would rather want to look at a recursive function, give me a shout and we can have a look.
EDIT: Recursive Function
Have a look at how much easier this is
CREATE TABLE TableValues (
ID INT,
Op VARCHAR(20),
ParentID INT
)
INSERT INTO TableValues SELECT 1,'OR',NULL
INSERT INTO TableValues SELECT 2,'AND',1
INSERT INTO TableValues SELECT 3,'AND',1
INSERT INTO TableValues SELECT 4,'>',2
INSERT INTO TableValues SELECT 5,'a',4
INSERT INTO TableValues SELECT 6,'alpha',4
INSERT INTO TableValues SELECT 7,'>',2
INSERT INTO TableValues SELECT 8,'b',7
INSERT INTO TableValues SELECT 9,'beta',7
INSERT INTO TableValues SELECT 10,'>',3
INSERT INTO TableValues SELECT 11,'c',10
INSERT INTO TableValues SELECT 12,'gamma',10
INSERT INTO TableValues SELECT 13,'<',3
INSERT INTO TableValues SELECT 14,'a',13
INSERT INTO TableValues SELECT 15,'delta',13
GO
CREATE FUNCTION ReturnMathVals (#ParentID INT, #Side VARCHAR(1))
RETURNS VARCHAR(500)
AS
BEGIN
DECLARE #RetVal VARCHAR(500)
IF (#ParentID IS NULL)
BEGIN
SELECT #RetVal = ' (' + dbo.ReturnMathVals(ID,'L') + Op + dbo.ReturnMathVals(ID,'R') + ') '
FROM TableValues
WHERE ParentID IS NULL
END
ELSE
BEGIN
SELECT TOP 1 #RetVal = ' (' + dbo.ReturnMathVals(ID,'L') + Op + dbo.ReturnMathVals(ID,'R') + ') '
FROM TableValues
WHERE ParentID = #ParentID
ORDER BY CASE WHEN #Side = 'L' THEN ID ELSE -ID END
SET #RetVal = ISNULL(#RetVal, (SELECT TOP 1 Op FROM TableValues WHERE ParentID = #ParentID ORDER BY CASE WHEN #Side = 'L' THEN ID ELSE -ID END))
END
RETURN #RetVal
END
GO
SELECT dbo.ReturnMathVals(NULL, NULL)
GO
DROP FUNCTION ReturnMathVals
DROP TABLE TableValues
Yes it is possible to do it but the problem is not the CTE, check it with PIVOT
read more about it from this link
http://msdn.microsoft.com/en-us/library/ms177410.aspx
some examples in this documentation is similar with your problem
I couldn't figure out how to do the double-recursion, but hopefully one of the intermediate CTEs in this will set you on the right track:
SET NOCOUNT ON
DECLARE #tree AS TABLE
(
Id int NOT NULL
,Operator varchar(10) NOT NULL
,ParentId int
)
INSERT INTO #tree
VALUES (1, 'OR', NULL)
INSERT INTO #tree
VALUES (2, 'AND', 1)
INSERT INTO #tree
VALUES (3, 'AND', 1)
INSERT INTO #tree
VALUES (4, '>', 2)
INSERT INTO #tree
VALUES (5, 'a', 4)
INSERT INTO #tree
VALUES (6, 'alpha', 4)
INSERT INTO #tree
VALUES (7, '>', 2)
INSERT INTO #tree
VALUES (8, 'b', 7)
INSERT INTO #tree
VALUES (9, 'beta', 7)
INSERT INTO #tree
VALUES (10, '>', 3)
INSERT INTO #tree
VALUES (11, 'c', 10)
INSERT INTO #tree
VALUES (12, 'gamma', 10)
INSERT INTO #tree
VALUES (13, '>', 3)
INSERT INTO #tree
VALUES (14, 'd', 13)
INSERT INTO #tree
VALUES (15, 'delta', 13) ;
WITH lhs_selector
AS (
SELECT ParentId
,MIN(Id) AS Id
FROM #tree
GROUP BY ParentId
),
rhs_selector
AS (
SELECT ParentId
,MAX(Id) AS Id
FROM #tree
GROUP BY ParentId
),
leaf_selector
AS (
SELECT Id
FROM #tree AS leaf
WHERE NOT EXISTS ( SELECT *
FROM #tree
WHERE ParentId = leaf.Id )
),
recurse
AS (
SELECT operator.Id
,CASE WHEN lhs_is_leaf.Id IS NOT NULL THEN NULL
ELSE lhs.Id
END AS LhsId
,CASE WHEN rhs_is_leaf.Id IS NOT NULL THEN NULL
ELSE rhs.Id
END AS RhsId
,CASE WHEN COALESCE(lhs_is_leaf.Id, rhs_is_leaf.Id) IS NULL
THEN '({' + CAST(lhs.Id AS varchar) + '} ' + operator.Operator + ' {'
+ CAST(rhs.Id AS varchar) + '})'
ELSE '(' + lhs.Operator + ' ' + operator.Operator + ' ' + rhs.Operator + ')'
END AS expression
FROM #tree AS operator
INNER JOIN lhs_selector
ON lhs_selector.ParentID = operator.Id
INNER JOIN rhs_selector
ON rhs_selector.ParentID = operator.Id
INNER JOIN #tree AS lhs
ON lhs.Id = lhs_selector.Id
INNER JOIN #tree AS rhs
ON rhs.Id = rhs_selector.Id
LEFT JOIN leaf_selector AS lhs_is_leaf
ON lhs_is_leaf.Id = lhs.Id
LEFT JOIN leaf_selector AS rhs_is_leaf
ON rhs_is_leaf.Id = rhs.Id
)
SELECT *
,REPLACE(REPLACE(op.expression, '{' + CAST(op.LhsId AS varchar) + '}', lhs.expression),
'{' + CAST(op.RhsId AS varchar) + '}', rhs.expression) AS final_expression
FROM recurse AS op
LEFT JOIN recurse AS lhs
ON lhs.Id = op.LhsId
LEFT JOIN recurse AS rhs
ON rhs.Id = op.RhsId

Resources