Oracle:如何在SQL查询中实现"自然"的顺序?

Mar*_*son 7 sql oracle

例如,

foo1
foo2
foo10
foo100
Run Code Online (Sandbox Code Playgroud)

而不是

foo1
foo10
foo100
foo2
Run Code Online (Sandbox Code Playgroud)

更新:对自己编码排序不感兴趣(虽然这本身就很有趣),但让数据库为我做排序.

Mar*_*son 8

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

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

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

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

  • 他同时发布了问题和答案.他或者想要分享他发现的这些知识,收集代表点,或两者兼而有之. (2认同)
  • 不是不礼貌,但它被标记为SQL. (2认同)

Vla*_*kov 5

对于短字符串、少量数字

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

这个想法是:

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

假设:

  • 数字的最大长度预先已知(例如 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*?(\d{21}(\D|$))', '\1');
Run Code Online (Sandbox Code Playgroud)

“巧妙”的做法

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

主要想法:

  • 从所有数字中去除前导零,以便在和:02之间排序。注意:这可能会导致>的“意外”排序(因为转换为),但是没有单一的答案应该如何排序13regexp_replace(val, '(^|\D)0+(\d+)', '\1\2')10.0210.102210.02.03
  • 转换"""带有引号的文本可以正常工作
  • 将输入字符串转换为逗号分隔格式:'"'||regexp_replace(..., '([^0-9]+)', '","\1","')||'"'
  • 通过以下方式将 csv 转换为项目列表xmltable
  • 增强类似数字的项目,以便字符串排序正常工作
  • 使用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;
Run Code Online (Sandbox Code Playgroud)

有趣的是,它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
);
Run Code Online (Sandbox Code Playgroud)