SQL Server: CTE tree with priority - sql

I have some troubles how to build a query for a recursive parent-child tree in SQL Server.
I have found some common table expression but i have to consider another parameter (the priority in each level) that i have not found from a search over the internet, so the structure of my table is like this
id - parentid - proprity
For example if i have this data:
1 - NULL - 2
2 - NULL - 1
3 - 2 - 2
4 - 3 - 1
5 - 2 - 0
6 - 1 - 0
7 - 2 - 3
The query must return this at right order:
-2
--5
--3
---4
--7
-1
--6
Also this list is orderable so if any order is change I have to reset the priority at right place. Anyone has already a solution for this case with queries in SQL Server?
Thanks in advance

This will give you the order that you need. Personally, I'd try to do that ordering in the display layer, but this will do it in SQL.
This relies on single-digit priority values. If those might be larger then you'll need to prepend leading 0s to the priorities, like: RIGHT('0' + priority, 2).
The query just builds a string of the priorities at each point so that you can then order off of it.
;WITH Hierarchy_CTE AS
(
SELECT
id,
parent_id,
CAST([priority] AS VARCHAR(100)) AS priority_string,
id AS base,
1 AS lvl
FROM
My_Table
WHERE
parent_id IS NULL
UNION ALL
SELECT
MT.id,
MT.parent_id,
CAST(H.priority_string + CAST(MT.priority AS VARCHAR(20)) AS VARCHAR(100)),
H.base,
H.lvl + 1
FROM
Hierarchy_CTE H
INNER JOIN My_Table MT ON MT.parent_id = H.id
)
SELECT
id,
base,
lvl
FROM
Hierarchy_CTE
ORDER BY
priority_string

It seems that this solution is working (taken from Tom H answer):
;WITH Hierarchy_CTE AS
(
SELECT
id,
parent_id,
CAST(REPLICATE('0', 10 - LEN(CAST([Order] AS VARCHAR))) + CAST([Order] AS VARCHAR) + '.' +
REPLICATE('0', 10 - LEN(CAST(MerchantCategoryId AS VARCHAR))) + CAST(MerchantCategoryId AS VARCHAR) AS VARCHAR(4000)) AS priority_string,
id AS base,
1 AS lvl
FROM
My_Table
WHERE
parent_id IS NULL
UNION ALL
SELECT
MT.id,
MT.parent_id,
CAST(H.priority_string + '|' + REPLICATE('0', 10 - LEN(CAST(MT.[priority] AS VARCHAR))) + CAST(MT.[priority] AS VARCHAR) + '.' +
REPLICATE('0', 10 - LEN(CAST(MT.id AS VARCHAR))) + CAST(MT.id AS VARCHAR) AS VARCHAR(4000)) AS priority_string,
H.base,
H.lvl + 1
FROM
Hierarchy_CTE H
INNER JOIN My_Table MT ON MT.parent_id = H.id
)
SELECT
id,
base,
lvl
FROM
Hierarchy_CTE
ORDER BY
priority_string
I added padding zeros equal to a fixed length minus then "stringified" priority length and also for the id, so i can order them to the priority and then for id.
This solution works only in ascending orders (not work for descending orders with the maintenance of a tree view), but this list is useful for me only when i have to save updated priorities, so it may be enough.

Related

Converting alphanumeric to numeric and vice versa in oracle

I have a requirement to convert alphanumeric to numeric and vice-versa.
Example: If 'A2' is passed then I have written below query to convert it to numeric:
select sum(val) from (
select power(36, loc - 1) *
case when letter between '0'
and '9'
then to_number(letter)
else 10 + ascii(letter) - ascii('A')
end as val from(
select substr(ip_str, length(ip_str) + 1 - level, 1) letter,
level loc from(select 'A2'
ip_str from dual) connect by level <= length(ip_str)
)
); --sum(val) returns 362
How do I decode 362 back to 'A2'?
Base N Convert - this site describes algorithm. I implemented it as recursive query:
with
t(num) as (select 362 from dual),
r(md, div, lvl) as (
select mod(num, 36), floor(num/36), 1 from t union all
select mod(div, 36), floor(div/36), lvl + 1 from r where div > 0)
select listagg(case when md > 9 then chr(ascii('A') - 10 + md)
else to_char(md)
end) within group (order by lvl desc) b36
from r
dbfiddle demo
Seems to work, I tested several values comparing results with online calculators. Theoretically you can use other bases, not only 36, algorithm is the same, but I did not test it.

How to generate combinations

I have a requirement to create a table with an identifier column. The identifier data will be comprised of 3 parts, the first being a letter [A-Z], the second being a number [1-42] and the third being again a number [1-6].
I was wondering the quickest and best way to go about this as I'm really stuck. The output should look like this:
A-1-1
A-1-2
A-1-3
...
Z-42-6
Thanks for your help
You should use CROSS JOIN with derived tables containing all letters/numbers needed
SELECT letters.let + '-' + numbers.num + '-' + numbers2.num
FROM(SELECT 'A' as let UNION ALL SELECT 'B' .....) letters
CROSS JOIN(SELECT '1' as num UNION ALL SELECT '2' ....) numbers -- up to 42
CROSS JOIN(SELECT '1' as num UNION ALL SELECT '2' ....) numbers2 -- up to 6
Here is a cut-down version using CROSS JOIN acrross 3 valued tables
SELECT v1.val + '-' + CAST(v2.val AS VARCHAR(5)) + '-' + cast(v3.val AS VARCHAR(5))
FROM
(VALUES ('A'),('B'),('C'),('D')) v1(val)
CROSS JOIN
(VALUES (1),(2),(3),(4),(5),(6),(7),(8),(9),(10),(11),(12),(13),(14),(15),(16)) v2(val)
CROSS JOIN
(VALUES (1),(2),(3),(4),(5),(6)) v3(val)
A Tally table would save you the need to write all the values one by one.
If you don't already have a tally table, read this post on the best way to create one.
SELECT Letter +'-'+ cast(fn as varchar(2)) +'-'+ cast(sn as char(1))
FROM (SELECT CHAR(Number) As Letter FROM Tally WHERE Number BETWEEN 65 AND 90) a
CROSS JOIN (SELECT Number as fn FROM Tally WHERE Number BETWEEN 1 AND 42) b
CROSS JOIN (SELECT Number as sn FROM Tally WHERE Number BETWEEN 1 AND 6) c
Just for fun, a mathematical approach:
with cte as
(
select 0 nr
union all
select nr+1 from cte where nr < 6551 --(26 * 42 * 6 = 6552 , 0 based = 6551)
)
select char(65 + (nr / 252)), 1 + ((nr / 6) % 42), 1 + nr % 6, * from cte -- Letter: divider = 6 * 42 = 252 , 65 = 'A'
option (maxrecursion 10000)
The cte only generated a stream of numbers from 0 to 6551 (could be done with other approaches as well).
After that each segment of the sequence can be calculated.
But for the record, once a sequence is created, I like Zohar's solution best :)
One more way:
;WITH cte AS (
SELECT 1 as digit
UNION ALL
SELECT digit + 1
FROM cte
WHERE digit < 90
)
SELECT CHAR(c1.digit) + '-' +
CAST(c2.digit as nvarchar(2)) + '-' +
CAST(c3.digit as nvarchar(2)) as seq
FROM cte c1
CROSS JOIN (SELECT digit FROM cte WHERE digit between 1 and 42) c2
CROSS JOIN (SELECT digit FROM cte WHERE digit between 1 and 6) c3
WHERE c1.digit between 65 and 90 --65..90 in ASCII is A..Z
Output:
seq
A-1-1
A-1-2
A-1-3
A-1-4
A-1-5
A-1-6
A-2-1
A-2-2
A-2-3
A-2-4
A-2-5
A-2-6
...
Z-42-3
Z-42-4
Z-42-5
Z-42-6

Get max number from table add one and check with specific convention

I have to produce artikel number based on some convention, and this convention is as below
The number of digits
{1 or 2 or 3}.{4 or 5}.{n}
example products numbers:
7.1001.1
1.1453.1
3.5436.1
12.7839.1
12.3232.1
13.7676.1
3.34565.1
12.56433.1
247.23413.1
The first part is based on producent, and every producent has its own number. Let's say Rebook - 12, Nike - 256 and Umbro - 3.
I have to pass this number and check in table if there are some rows containing it e.g i pass 12 then i should get everything which starts from 12.
and now there should be three cases what to do:
1st CASE: no rows at the table:
then retrieve 1001
2nd case: if there are rows
so for sure there is already at least one:
12.1001.1
and more if they are let's say:
12.1002.1
12.1003.1
...
12.4345.1
so should be retreived next one so: 4346
and if there are already 5-digits for this product so let's say:
12.1002.1
12.1003.1
...
12.9999.1
so should be retreived next one so: 10001
3rd case: in fact same as 2nd but if it rached 9999 for second part:
12.1001.1
...
12.9999.1
then returned should be: 10001
or
12.1002.1
12.1003.1
...
12.9999.1
12.10001.1
12.10002.1
so should be retreived next one so: 10003
Hope you know what i mean
I already have started something. This code is taking producent number - looking for all rows starting with it and then just simply adding 1 to the second part unfortunetly i am not sure how should i change it according to those 3 cases.
select
parsename(max(nummer), 3) + '.' -- 3
+ ltrim(max(cast(parsename(nummer, 2) as int) +1)) -- 5436 -> 5437
+ '.1'
from tbArtikel
where Nummer LIKE '3.%'
Counting on your help. If something unclear let me know.
Additional question:
Using cmd As New SqlCommand("SELECT CASE WHEN r.number Is NULL THEN 1001
WHEN r.number = 9999 THEN 10001
Else r.number + 1 End number
FROM (VALUES(#producentNumber)) AS a(art) -- this will search this number within inner query And make case..
LEFT JOIN(
-- Get producent (in Like) number And max number Of it (without Like it Get all producent numbers And their max number out Of all
SELECT PARSENAME(Nummer, 3) art,
MAX(CAST(PARSENAME(Nummer, 2) AS INT)) number
FROM tbArtikel WHERE Nummer Like '#producentNumber' + '[.]%'
GROUP BY PARSENAME(Nummer, 3)
) r
On r.art = a.art", con)
cmd.CommandType = CommandType.Text
cmd.Parameters.AddWithValue("#producentNumber", producentNumber)
A fairly straight forward way is to (ab)use PARSENAME to split the string to be able to extract the current maximum. An outer query can then just implement the rules for the value being missing/9999/other.
The value (12 here) is inserted in a table value constructor to be able to detect a missing value using a LEFT JOIN.
SELECT CASE WHEN r.number IS NULL THEN 1001
WHEN r.number = 9999 THEN 10001
ELSE r.number + 1 END number
FROM ( VALUES(12) ) AS a(category)
LEFT JOIN (
SELECT PARSENAME(prodno, 3) category,
MAX(CAST(PARSENAME(prodno, 2) AS INT)) number
FROM products
GROUP BY PARSENAME(prodno, 3)
) r
ON r.category = a.category;
An SQLfiddle to test with.
As a further optimization, you could add a WHERE prodno LIKE '12[.]%' in the inner query to not parse through un-necessary rows.
I don't fully understand what you're asking for. I am unsure about the examples...but if i was doing it I'd try to break the field into 3 fields first and then do something with them.
sqlfiddle
SELECT nummer,LEFT(nummer,first-1) as field1,
RIGHT(LEFT(nummer,second-1),second-first-1) as field2,
RIGHT(nummer,LEN(nummer)-second) as field3
FROM
(SELECT nummer,
CHARINDEX('.',nummer) as first,
CHARINDEX('.',nummer,CHARINDEX('.',nummer)+1)as second
from tbArtikel)T
Hopefully with the 3 fields broken up, it's much easier to apply logics to them now.
update:
Okay i reread your question and i sort of know what you're trying to get at..
if user search for a value that doesn't exist for example 8.
Then you want 1001 returned
if they search for anything else that has results then return the max+1
unless it's 9999 then return 10001.
If this is correct then check this sqlfiddle2
DECLARE #search varchar(20)
SET #search = '8'
SELECT field1,max(nextvalue) as nextvalue FROM
(SELECT field1,
MAX(CASE (field2)
WHEN 9999 THEN 10001
ELSE field2+1
END) as nextvalue
FROM
(SELECT nummer,
CAST(LEFT(nummer,first-1) as INTEGER) as field1,
CAST(RIGHT(LEFT(nummer,second-1),second-first-1) as INTEGER) as field2,
CAST(RIGHT(nummer,LEN(nummer)-second) as INTEGER) as field3
FROM
(SELECT nummer,
CHARINDEX('.',nummer) as first,
CHARINDEX('.',nummer,CHARINDEX('.',nummer)+1)as second
FROM tbArtikel
)T
)T2
GROUP BY field1
UNION
SELECT CAST (#search as INTEGER)as field1 ,1001
)T3
WHERE field1 = #search
GROUP BY field1
Just change the #search variable to see it's results
I think there might be a cleaner way to do this but it's not coming to me right now :(
If you really can't add 2 new fields (is't probably the simplest and fastest solution), and probably can't add functional index, you must extract 2nd part number and get max of this, increment, then concatenate with your condition 1st part number and '.1' at the end:
SELECT :par1 || '.' || (Max(To_Number(SubStr(nummer, dot1 + 1, dot2 - dot1 -1 ))) + 1) || '.1' NEW_number
--SELECT SubStr(nummer, 1, dot1 - 1) N1st, SubStr(nummer, dot1 + 1, dot2 - dot1 -1 ) N2nd, SubStr(nummer, dot2 + 1) N1th
FROM (
SELECT nummer, InStr(nummer, '.') dot1, InStr(nummer, '.', 1, 2) dot2
FROM tbArtikel
WHERE nummer LIKE :par1 || '.%')
;
--GROUP BY SubStr(nummer, 1, dot1 – 1)
it was for oracle sql, i don't have sql-serwer to test, but probably this is simplest answer:
select #par1 + '.' + (select max(cast(SUBSTRING(nummer, CHARINDEX( '.', nummer, 1 ) +1, CHARINDEX( '.', nummer, CHARINDEX( '.', nummer, 1 ) +1 ) - CHARINDEX( '.', nummer, 1 ) -1) as int)) + 1 from tbArtikel where nummer LIKE #par1 || '.%') + '.1'
if parsename(nummer, 2) is you defined function to get 2nd number then:
select #parm + '.' + (max(cast(parsename(nummer, 2) as int)) + 1) + '.1'
from tbArtikel
where Nummer LIKE #parm + '.%'

Query Split string into rows

I have a table that looks like this:
ID Value
1 1,10
2 7,9
I want my result to look like this:
ID Value
1 1
1 2
1 3
1 4
1 5
1 6
1 7
1 8
1 9
1 10
2 7
2 8
2 9
I'm after both a range between 2 numbers with , as the delimiter (there can only be one delimiter in the value) and how to split this into rows.
Splitting the comma separated numbers is a small part of this problem. The parsing should be done in the application and the range stored in separate columns. For more than one reason: Storing numbers as strings is a bad idea. Storing two attributes in a single column is a bad idea. And, actually, storing unsanitized user input in the database is also often a bad idea.
In any case, one way to generate the list of numbers is to use a recursive CTE:
with t as (
select t.*, cast(left(value, charindex(',', value) - 1) as int) as first,
cast(substring(value, charindex(',', value) + 1, 100) as int) as last
from table t
),
cte as (
select t.id, t.first as value, t.last
from t
union all
select cte.id, cte.value + 1, cte.last
from cte
where cte.value < cte.last
)
select id, value
from cte
order by id, value;
You may need to fiddle with the value of MAXRECURSION if the ranges are really big.
Any table that a field with multiple values such as this is a problem in terms of design. The only way to deal with these records as it is is to split the values on the delimiter and put them into a temporary table, implement custom splitting code, integrate a CTE as noted, or redesign the original table to put the comma-delimited fields into separate fields, eg
ID LOWLIMIT HILIMIT
1 1 10
similar with Gordon Linoff variant, but has some difference
--create temp table for data sample
DECLARE #Yourdata AS TABLE ( id INT, VALUE VARCHAR(20) )
INSERT #Yourdata
( id, VALUE )
VALUES ( 1, '1,10' ),
( 2, '7,9' )
--final query
;WITH Tally
AS ( SELECT MIN(CONVERT(INT, SUBSTRING(y.VALUE, 1, CHARINDEX(',', y.value) - 1))) AS MinV ,
MAX(CONVERT(INT, SUBSTRING(y.VALUE, CHARINDEX(',', y.value) + 1, 18))) AS MaxV
FROM #yourdata AS y
UNION ALL
SELECT MinV = MinV + 1 , MaxV
FROM Tally
WHERE MinV < Maxv
)
SELECT y.id , t.minV AS value
FROM #yourdata AS y
JOIN tally AS t ON t.MinV BETWEEN CONVERT(INT, SUBSTRING(y.VALUE, 1, CHARINDEX(',', y.value) - 1))
AND CONVERT(INT, SUBSTRING(y.VALUE, CHARINDEX(',', y.value) + 1, 18))
ORDER BY id, minV
OPTION ( MAXRECURSION 999 ) --change it if required
output

Find overlapping sets of data in a table

I need to identify duplicate sets of data and give those sets who's data is similar a group id.
id threshold cost
-- ---------- ----------
1 0 9
1 100 7
1 500 6
2 0 9
2 100 7
2 500 6
I have thousands of these sets, most are the same with different id's. I need find all the like sets that have the same thresholds and cost amounts and give them a group id. I'm just not sure where to begin. Is the best way to iterate and insert each set into a table and then each iterate through each set in the table to find what already exists?
This is one of those cases where you can try to do something with relational operators. Or, you can just say: "let's put all the information in a string and use that as the group id". SQL Server seems to discourage this approach, but it is possible. So, let's characterize the groups using:
select d.id,
(select cast(threshold as varchar(8000)) + '-' + cast(cost as varchar(8000)) + ';'
from data d2
where d2.id = d.id
for xml path ('')
order by threshold
) as groupname
from data d
group by d.id;
Oh, I think that solves your problem. The groupname can serve as the group id. If you want a numeric id (which is probably a good idea, use dense_rank():
select d.id, dense_rank() over (order by groupname) as groupid
from (select d.id,
(select cast(threshold as varchar(8000)) + '-' + cast(cost as varchar(8000)) + ';'
from data d2
where d2.id = d.id
for xml path ('')
order by threshold
) as groupname
from data d
group by d.id
) d;
Here's the solution to my interpretation of the question:
IF OBJECT_ID('tempdb..#tempGrouping') IS NOT NULL DROP Table #tempGrouping;
;
WITH BaseTable AS
(
SELECT 1 id, 0 as threshold, 9 as cost
UNION SELECT 1, 100, 7
UNION SELECT 1, 500, 6
UNION SELECT 2, 0, 9
UNION SELECT 2, 100, 7
UNION SELECT 2, 500, 6
UNION SELECT 3, 1, 9
UNION SELECT 3, 100, 7
UNION SELECT 3, 500, 6
)
, BaseCTE AS
(
SELECT
id
--,dense_rank() over (order by threshold, cost ) as GroupId
,
(
SELECT CAST(TblGrouping.threshold AS varchar(8000)) + '/' + CAST(TblGrouping.cost AS varchar(8000)) + ';'
FROM BaseTable AS TblGrouping
WHERE TblGrouping.id = BaseTable.id
ORDER BY TblGrouping.threshold, TblGrouping.cost
FOR XML PATH ('')
) AS MultiGroup
FROM BaseTable
GROUP BY id
)
,
CTE AS
(
SELECT
*
,DENSE_RANK() OVER (ORDER BY MultiGroup) AS GroupId
FROM BaseCTE
)
SELECT *
INTO #tempGrouping
FROM CTE
-- SELECT * FROM #tempGrouping;
UPDATE BaseTable
SET BaseTable.GroupId = #tempGrouping.GroupId
FROM BaseTable
INNER JOIN #tempGrouping
ON BaseTable.Id = #tempGrouping.Id
IF OBJECT_ID('tempdb..#tempGrouping') IS NOT NULL DROP Table #tempGrouping;
Where BaseTable is your table, and and you don't need the CTE "BaseTable", because you have a data table.
You may need to take extra-precautions if your threshold and cost fields can be NULL.