字母数字文本的二进制排序不符合自然排序

时间:2016-09-13 09:26:20

标签: oracle natural-sort varchar2

在过去的几天里,我一直在尝试按自然顺序对字母数字文本列表进行排序。我发现使用NLS_SORT选项可以正确排序列表(see this answer)。但在尝试解决方案时,我发现它没有任何区别。该列表仍然显示为正常的ORDER BY查询。请注意,solution involving regex不适合我。

出于测试目的,我制作了一张桌子,并填写了一些数据。运行SELECT name FROM test ORDER BY name ASC时,我得到以下结果:

enter image description here

如您所见,排序不自然。它应该更像1, 2, 3, 4, 5, 6, 7, 8, 9, 10

我尝试的解决方案涉及设置nls_sort选项。

ALTER SESSION SET nls_sort='BINARY'; -- or BINARY_AI
SELECT name FROM test ORDER BY NLSSORT(name,'NLS_SORT=BINARY') -- or BINARY_AI

它应该根据ASCII table中所述的每个字符的十进制代码对列表中的文本进行排序。所以我希望它能以正确的方式出现(因为该表中的顺序是' space',' dot',数字,字母),但它并没有改变任何东西。订单仍然与图片中的顺序相同。

  

如果它是BINARY,则排序顺序基于每个字符的数值,因此它取决于数据库字符集

这可能与我使用的字符集有关,但我不确定它有什么问题。正在运行SELECT value$ FROM sys.props$ WHERE name = 'NLS_CHARACTERSET';会给我AL32UTF8的值。这似乎是UTF8的略微扩展版本(如果我错了,请纠正我)。我在Oracle数据库版本11.2.0.4.0上运行。

所以有人能告诉我我做错了什么或我错过了什么?

提前致谢。

2 个答案:

答案 0 :(得分:3)

您似乎希望二进制排序能够同时查看多个字符。它没有。它有效地按第一个字符排序(所以从1开始的所有内容都以2开头);然后是第二个字符(所以一个句点出现在0之前) - 这意味着1.10之前出现是正确的,而且10(或100000)来了在2之前。您无法改变排序行为的这一方面。在您链接的早期问题中,看起来只有第一个字符是数字,这是一种稍微不同的情况。

From the documentation

  

当字符值在语言上与ORDER BY子句进行比较时,它们首先转换为校对键,然后比较RAW值。归类键生成为NLSSORT中指定的显式生成,或隐式使用NLSSORT使用的相同方法生成。

您可以看到用于排序的字节顺序:

with t (name) as (
  select level - 1 || '. test' from dual connect by level < 13
  union all select '20. test' from dual
  union all select '100. test' from dual
)
select name, nlssort(name, 'NLS_SORT=BINARY') as sort_bytes
from t
order by name;

NAME       SORT_BYTES         
---------- --------------------
0. test    302E207465737400    
1. test    312E207465737400    
10. test   31302E207465737400  
100. test  3130302E207465737400
11. test   31312E207465737400  
2. test    322E207465737400    
20. test   32302E207465737400  
3. test    332E207465737400    
4. test    342E207465737400    
5. test    352E207465737400    
6. test    362E207465737400    
7. test    372E207465737400    
8. test    382E207465737400    
9. test    392E207465737400  

您可以看到原始NLSRORT结果(归类键)符合逻辑顺序。

如果您不想使用正则表达式,可以使用substr()instr()获取句点/空格之前的部分并将其转换为数字;虽然这假定格式是固定的:

with t (name) as (
  select level - 1 || '. test' from dual connect by level < 13
  union all select '20. test' from dual
  union all select '100. test' from dual
)
select name
from t
order by to_number(substr(name, 1, instr(name, '. ') - 1)),
  substr(name, instr(name, '. '));

NAME     
----------
0. test   
1. test   
2. test   
3. test   
4. test   
5. test   
6. test   
7. test   
8. test   
9. test   
10. test  
11. test  
20. test  
100. test 

如果可能没有句号/空格,您可以检查:

select name
from t
order by case when instr(name, '. ') > 0 then to_number(substr(name, 1, instr(name, '. ') - 1)) else 0 end,
  case when instr(name, '. ') > 0 then substr(name, instr(name, '. ')) else name end;

...但是,如果您在名称中有两个句子,但第一个句子无法转换为数字,那么您仍有问题。你可以实现一个安全的&#39; to_number()函数,如果发生这种情况,则会压缩ORA-01722。

使用正则表达式会更简单,更安全,例如:

select name
from t
order by to_number(regexp_substr(name, '^\d+', 1)), name;

答案 1 :(得分:2)

添加到Alex Poole的优秀帖子,这是我从Tom Kyte帖子(here)学到的一个简单技巧。无论如何它都适用于这种情况:

-- padding with spaces ala Tom Kyte approach
with t (name) as (
  select level - 1 || '. test' from dual connect by level < 13
  union all select '20. test' from dual
  union all select '100. test' from dual
)
select name
from t
order by lpad(name, 20);

输出:

0. test
1. test
2. test
3. test
4. test
5. test
6. test
7. test
8. test
9. test
10. test
11. test
20. test
100. test

希望有所帮助

修改

这种方法更复杂,但涵盖了Alex Poole提出的情况(再次,归功于Tom Kyte):

with t (name) as (
  select level - 1 || '. test' from dual connect by level < 13
  union all select '20. hello' from dual
  union all select '100. test' from dual
)
select 
    --substr(name,1,length(name)-nvl(length(replace(translate(name,'0123456789','0000000000'),'0','')),0)),
    --substr(name,1+length(name)-nvl(length(replace(translate(name,'0123456789','0000000000'),'0','')),0)) ,
    name
from t
order by
    to_number(substr(name,1,length(name)-nvl(length(replace(translate(name,'0123456789','0000000000'),'0','')),0))),
              substr(name,1+length(name)-nvl(length(replace(translate(name,'0123456789','0000000000'),'0','')),0)) NULLS FIRST;

输出:

0. test
1. test
2. test
3. test
4. test
5. test
6. test
7. test
8. test
9. test
10. test
11. test
20. hello
100. test