Oracle: Как я могу реализовать «естественный» порядок в запросе SQL? - PullRequest
7 голосов
/ 21 октября 2008

например,

foo1
foo2
foo10
foo100

вместо

foo1
foo10
foo100
foo2

Обновление: не заинтересован в кодировании сортировки сам (хотя это интересно само по себе), но имеет базу данных, чтобы сделать сортировку для меня.

Ответы [ 3 ]

8 голосов
/ 21 октября 2008

Вы можете использовать функции в предложении 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);
3 голосов
/ 22 ноября 2010

Я использую следующую функцию для дополнения нулями всех последовательностей цифр короче 10, которые можно найти в значении, чтобы общая длина каждой из них составляла 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 )
2 голосов
/ 26 августа 2015

Для коротких строк, небольшое количество чисел

Если количество числовых значений и максимальная длина ограничены, существует решение на основе регулярных выражений.

Идея такова:

Предположения:

  • Максимальная длина чисел известна заранее (например, 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');

"Умный" подход

Несмотря на то, что отдельная функция natural_sort может быть полезна, существует малоизвестный прием для этого в чистом SQL.

Ключевые идеи:

  • Уберите начальные нули из всех чисел так, чтобы 02 был упорядочен между 1 и 3: regexp_replace(val, '(^|\D)0+(\d+)', '\1\2'). Примечание: это может привести к «неожиданной» сортировке 10.02> 10.1 (поскольку 02 преобразуется в 2), однако нет единого ответа, как сортировать такие вещи, как 10.02.03
  • Преобразовать " в "", чтобы текст с кавычками работал правильно
  • Преобразовать входную строку в формат с разделителями-запятыми: '"'||regexp_replace(..., '([^0-9]+)', '","\1","')||'"'
  • Конвертировать csv в список предметов через xmltable
  • Увеличивать числовые элементы, чтобы сортировка строк работала правильно
  • Используйте length(length(num))||length(num)||num вместо lpad(num, 10, '0'), так как последний менее компактен и не поддерживает 11+ цифр. Примечание:

Время ответа составляет около 3-4 секунд для сортировки списка из 1000 случайных строк длиной 30 (генерация случайных строк занимает само 0,2 секунды). Основным потребителем времени является xmltable, который разбивает текст на строки. При использовании PL / SQL вместо xmltable для разделения строки на строки время отклика сокращается до 0,4 с для тех же 1000 строк.

Следующий запрос выполняет естественную сортировку 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
);
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...