Oracle:如何在 SQL 查询中实现“自然”排序?

Posted

技术标签:

【中文标题】Oracle:如何在 SQL 查询中实现“自然”排序?【英文标题】:Oracle: How can I implement a "natural" order-by in a SQL query? 【发布时间】:2008-10-20 21:20:56 【问题描述】:

例如,

foo1
foo2
foo10
foo100

而不是

foo1
foo10
foo100
foo2

更新:对自己编码排序不感兴趣(尽管这本身很有趣),但让数据库为我进行排序。

【问题讨论】:

Jeff also has a post 关于该主题,还有更多其他语言的资源。 【参考方案1】:

您可以在 order-by 子句中使用函数。在这种情况下, 您可以拆分的非数字和数字部分 字段并将它们用作两个排序标准。

select * from t
 order by to_number(regexp_substr(a,'^[0-9]+')),
          to_number(regexp_substr(a,'[0-9]+$')),
          a;

您还可以创建基于函数的索引来支持这一点:

create index t_ix1
    on t (to_number(regexp_substr(a, '^[0-9]+')),
          to_number(regexp_substr(a, '[0-9]+$')), 
          a);

【讨论】:

如果您想要 SQL 答案,您可能应该澄清您的问题以指定它。还是想收集各种分拣技巧? 他同时发布了问题和答案。他要么希望分享他发现的这一点知识,要么收集代表点数,要么两者兼而有之。 不要失礼,但它被标记为SQL。 尽管如此,问题中的更多描述不会有什么坏处 $[0-9]+ 不匹配行尾之后的数字,所以总是为空?此外,如果有一组以上的数字,这也不起作用。【参考方案2】:

我使用以下函数对值中可以找到的所有小于 10 的数字序列进行 0 填充,使每个数字的总长度变为 10 位。它甚至与包含一个、多个或没有数字序列的混合值集兼容。

CREATE OR replace function NATURAL_ORDER(
    P_STR   varchar2
) return varchar2
IS
/** --------------------------------------------------------------------
    Replaces all sequences of numbers shorter than 10 digits by 0-padded
    numbers that exactly 10 digits in length. Usefull for ordering-by
    using NATURAL ORDER algorithm.
 */
    l_result  varchar2( 32700 );
    l_len     integer;
    l_ix      integer;
    l_end     integer;
begin
    l_result := P_STR;
    l_len := LENGTH( l_result );
    l_ix := 1;
    while l_len > 0 loop
        l_ix := REGEXP_INSTR( l_result, '[0-9]1,9', l_ix, 1, 0 );
        EXIT when l_ix = 0;
        l_end := REGEXP_INSTR( l_result, '[^0-9]|$', l_ix, 1, 0 );
        if ( l_end - l_ix >= 10 ) then
            l_ix := l_end;
        else
            l_result := substr( l_result, 1, l_ix - 1 )
                     || LPAD( SUBSTR( l_result, l_ix, l_end-l_ix ), 10, '0' )
                     || substr( l_result, l_end )
                     ;
            l_ix := l_ix + 10;
        end if;
    end loop;
    return l_result;
end;
/

例如:

select 'ABC' || LVL || 'DEF' as STR
  from (
          select LEVEL as LVL
            from DUAL
           start with 1=1
           connect by LEVEL <= 35
       )
 order by NATURAL_ORDER( STR )

【讨论】:

聪明!感谢分享。虽然我怀疑不是特别有效。并且显然会分解为超过 10 位的较大整数。不过,很有趣。【参考方案3】:

对于短字符串,少量数字

如果“数字”的数量和最大长度受到限制,则有一个基于正则表达式的解决方案。

想法是:

用 20 个零填充所有数字 使用另一个正则表达式删除过多的零。由于regexp backtracking,这可能会很慢。

假设:

预先知道数字的最大长度(例如 20) 所有数字都可以填充(换句话说,lpad('1 ', 3000, '1 ') 将失败,因为无法将填充的数字放入 varchar2(4000)

以下查询针对“短数字”情况进行了优化(请参阅*?),它需要 0.4 秒。但是,在使用这种方法时,您需要预先定义填充长度。

select * from (
  select dbms_random.string('X', 30) val from xmltable('1 to 1000')
)
order by regexp_replace(regexp_replace(val, '(\d+)', lpad('0', 20, '0')||'\1')
                      , '0*?(\d21(\D|$))', '\1');

“聪明”的方法

尽管单独的 natural_sort 函数可以很方便,但在纯 SQL 中有一个鲜为人知的技巧。

关键思想:

去除所有数字的前导零,因此0213 之间排序:regexp_replace(val, '(^|\D)0+(\d+)', '\1\2')。注意:这可能会导致10.02 > 10.1 的“意外”排序(因为02 转换为2),但是没有单一的答案应该如何排序10.02.03 之类的东西 将 " 转换为 "" 以便带引号的文本正常工作 将输入字符串转换为逗号分隔格式:'"'||regexp_replace(..., '([^0-9]+)', '","\1","')||'"' 通过xmltable将csv转换为项目列表 增加类似数字的项目,以便字符串排序正常工作 使用length(length(num))||length(num)||num 代替lpad(num, 10, '0'),因为后者不太紧凑并且不支持11 位以上的数字。 注意:

对于 1000 个长度为 30 的随机字符串的排序列表,响应时间大约为 3-4 秒(随机字符串的生成本身需要 0.2 秒)。 主要时间消费者是xmltable,它将文本拆分为行。 如果使用 PL/SQL 而不是 xmltable 将字符串拆分为行,对于相同的 1000 行,响应时间将减少到 0.4 秒。

以下查询对 100 个随机字母数字字符串执行自然排序(注意:它在 Oracle 11.2.0.4 中产生错误结果,在 12.1.0.2 中有效):

select *
  from (
    select (select listagg(case when regexp_like(w, '^[0-9]')
                                then length(length(w))||length(w)||w else w
                           end
                   ) within group (order by ord)
              from xmltable(t.csv columns w varchar2(4000) path '.'
                                        , ord for ordinality) q
           ) order_by
         , t.*
    from (
           select '"'||regexp_replace(replace(
                                          regexp_replace(val, '(^|\D)0+(\d+)', '\1\2')
                                        , '"', '""')
                                    , '([^0-9]+)', '","\1","')||'"' csv
                , t.*
           from (
                  select dbms_random.string('X', 30) val from xmltable('1 to 100')
                ) t
         ) t
  ) t
order by order_by;

有趣的是order by 可以在没有子查询的情况下表达,所以它是一个让你的审阅者疯狂的方便工具(它适用于 11.2.0.4 和 12.1.0.2):

select *
  from (select dbms_random.string('X', 30) val from xmltable('1 to 100')) t
 order by (
   select listagg(case when regexp_like(w, '^[0-9]')
                       then length(length(w))||length(w)||w else w
                  end
          ) within group (order by ord)
     from xmltable('$X'
            passing xmlquery(('"'||regexp_replace(replace(
                                                     regexp_replace(t.val, '(^|\D)0+(\d+)', '\1\2')
                                                   , '"', '""')
                                                , '([^0-9]+)', '","\1","')||'"')
                             returning sequence
                    ) as X
            columns w varchar2(4000) path '.', ord for ordinality) q
);

【讨论】:

那一定表现得非常好... :-)

以上是关于Oracle:如何在 SQL 查询中实现“自然”排序?的主要内容,如果未能解决你的问题,请参考以下文章

java中数据库中实现分页的sql语句要求每页十条要查询的是第二页

如何在SQL 2005中实现循环每一行做一定的操作

我们如何在 Oracle SQL 或 PL/SQL 中实现 Standard Normal CDF?

Oracle&SQLServer中实现跨库查询

如何仅使用 sql 在 apex oracle 的交互式网格中实现选择列表以及 oracle APEX 中可用的内容?

我们如何在sql CTE中实现动态查询?