对字母数字文本的二进制排序不像自然排序
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 之前也是正确的=].您无法更改排序行为的那个方面。在您链接到的先前问题中,看起来只有第一个字符是数字,情况略有不同。
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
最近几天我一直在尝试以自然的方式对字母数字文本列表进行排序 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 之前也是正确的=].您无法更改排序行为的那个方面。在您链接到的先前问题中,看起来只有第一个字符是数字,情况略有不同。
When character values are compared linguistically for the
ORDER BY
clause, they are first transformed to collation keys and then compared likeRAW
values. The collation keys are generated either explicitly as specified inNLSSORT
or implicitly using the same method thatNLSSORT
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