Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Properly sorting dotted numbers stored as character in SQL Server

I have a SQL table that stores a custom item number. Each of these can have a child broken off from it with a separator of .. Each of those can have a child too.

An example of what it could be (again, dynamic, don't know what it will be):

Item Number
1
1.1
1.1.1
1.1.1.1
1.1.1.1.a
1.1.1.1.b
10
11
2.1
2.10
2.2
2.20
20
3
30

The thing that makes this tough is those numbers are created on the fly and not necessarily in order. You may create 5 numbers (1, 2, 3, 4, 5) and then create a child of 1 so it will not be stored in order in the db.

How do I select from the table and order by the Item Number so that it shows properly, as above, when the data isn't stored in that order?

Most solutions I've tried either gives me 1, 2, 3, 4, 5...1.1, 1.2 OR 1, 1.1, 1.1.1, 10, 11...2, 2.1, 20....3, 30, etc.

like image 572
Scott Avatar asked Dec 13 '22 12:12

Scott


2 Answers

If you have SQL 2008 you can use the new hierarchyid data type:

WITH Items (ItemNumber) AS (
    SELECT '1' UNION ALL SELECT '1.1' UNION ALL SELECT '1.1.1'
    UNION ALL SELECT '10' UNION ALL SELECT '11' UNION ALL SELECT '2'
    UNION ALL SELECT '2.1' UNION ALL SELECT '20' UNION ALL SELECT '3'
    UNION ALL SELECT '30'
)
SELECT *
FROM Items 
ORDER BY Convert(hierarchyid, '/' + ItemNumber + '/');
like image 184
Vito Avatar answered Dec 30 '22 10:12

Vito


I discussed this in another forum where we came up with an XML solution that was very dynamic. Adam Haines helped to optimize it, which dramatically improved the performance. This version includes a fix to correctly sort alphabetic digits.

Given the following values:

declare @temp table (id varchar(255))

insert into @temp (id) values
  ('1.1.a.1'),('1.1.aa.2'),
  ('1.1.b.3'),('1.1.a.4'),
  ('1.1.a.5'),('1.1.a.6'),
  ('1.1.a.7'),('1.1.a.8'),
  ('1.1.a.9'),('1.1.a.10'),
  ('1.1.a.11'),('1.1.b.1'),
  ('1.1.b.2'),('1.2.a.1'),
  ('1.10.a.1'),('1.11.a.1'),
  ('1.20.a.1'),('101.20.a.2'),
  ('1.20.a.150'),('1.1'),
  ('1.2'),('1')

This query:

declare @xml xml,
        @max_len int

set @xml =
(
select id as id, cast('<i>' + replace(id,'.','</i><i>') + '</i>' as xml)
from @temp
for xml path('id_root'),type
)

select @max_len = max(len(x.i.value('.','varchar(10)')))
from @xml.nodes('/id_root/i') x(i)

select [id]--, srt.srtvalue
from @temp
cross apply(
    select case when ISNUMERIC(x.i.value('.','varchar(10)')) = 1 then right(replicate('0',@max_len) + x.i.value('.','varchar(10)'),@max_len) else x.i.value('.','varchar(10)') end + '.'
    from @xml.nodes('/id_root/i') x(i)
    where x.i.value('../id[1]','varchar(50)') = [@temp].id
    for xml path('')
) as srt(srtvalue)
order by srt.srtvalue

Returns these values:

id
1
1.1
1.1.a.1
1.1.a.4
1.1.a.5
1.1.a.6
1.1.a.7
1.1.a.8
1.1.a.9
1.1.a.10
1.1.a.11
1.1.aa.2
1.1.b.1
1.1.b.2
1.1.b.3
1.2
1.2.a.1
1.10.a.1
1.11.a.1
1.20.a.1
1.20.a.150
101.20.a.2

If you have more than 10 characters in a single digit, you'll have to change the varchar(10) appropriately.

-- James

like image 37
James L. Avatar answered Dec 30 '22 10:12

James L.