对字母数字文本的二进制排序不像自然排序

Binary sort on alphanumeric text not behaving as natural sort

最近几天我一直在尝试以自然的方式对字母数字文本列表进行排序 order.I 发现使用 NLS_SORT 选项可以正确排序列表(see this answer). But when trying out that solution I found that it made no difference. The list was still displayed as with a normal ORDER BY query. Please not that a solution involving regex 不适合我。

出于测试目的,我制作了一个 table 并在其中填充了一些数据。当 运行 SELECT name FROM test ORDER BY name ASC 我得到以下结果:

如您所见,排序不自然。它应该更像 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 中规定的每个字符的十进制代码对列表中的文本进行排序。所以我预计它会变成正确的方式(因为 table 中的顺序是 'space'、'dot'、数字、字母),但它没有改变任何东西。顺序还是和图片一样。

If it is BINARY then the sort order is based on the numeric value of each character, so it's dependant on the database character set

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

那么谁能告诉我我做错了什么或遗漏了什么?

提前致谢。

您似乎期望二进制排序一次查看多个字符。它没有。它有效地按第一个字符排序(因此以 1 开头的所有内容都在以 2 开头的任何内容之前);然后是第二个字符(所以句点出现在 0 之前) - 这意味着 1. 出现在 10 之前是正确的,而且 10(或 100000)出现在 [=17 之前也是正确的=].您无法更改排序行为的那个方面。在您链接到的先前问题中,看起来只有第一个字符是数字,情况略有不同。

From the documentation:

When character values are compared linguistically for the ORDER BY clause, they are first transformed to collation keys and then compared like RAW values. The collation keys are generated either explicitly as specified in NLSSORT or implicitly using the same method that NLSSORT uses.

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

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() 获取 period/space 之前的部分并将其转换为数字;尽管假设格式是固定的:

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 

如果可能没有 period/space,您可以检查一下:

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;

...但是如果您在名称中有两个句子但第一个不能转换为数字,您仍然会遇到问题。如果发生这种情况,您可以实施 'safe' to_number() 函数来压缩 ORA-01722。

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

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

除了 Alex Poole 的出色 post,这里还有一个我从 Tom Kyte post (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