Сортировка результата SQL запроса – это то, с чем программисты сталкиваются постоянно. Обычно СУБД при сортировке текстовых полей для сравнения строк использует лексикографический порядок. Согласно Википедии, он означает, что слово X предшествует слову Y (X < Y), если:
Недавно аналитик спросила меня: "А можно сделать так, чтобы документы сортировались по номеру "правильно"?". Под "правильно" подразумевался естественный порядок сортировки. Я ответил "Не проблема" и отдал разработчикам, функцию, которую давно своровал где-то на просторах интернета и немного упростил "обработав напильником":
– Нет, но я могу ее портировать.
При портировании функции на PL/SQL, я реализовал описанный алгоритм с использованием регулярных выражений:
- либо слово X является началом слова Y (например, "МАТЕМАТИК" < "МАТЕМАТИКА");
- либо первые m символов этих слов совпадают, а m+1-й символ слова X меньше m+1-го символа слова Y (например, "АБАК" < "АБРАКАДАБРА", так как первые две буквы у этих слов совпадают, а третья буква у первого слова меньше, чем у второго).
| Лексикографический порядок | Естественный порядок |
| a12 | a2a |
| a22 | a2b |
| a2a | a3a2 |
| a2b | a3a2a |
| a30z | a12 |
| a3a2 | a22 |
| a3a2a | a30z |
create function sort_string(@SortStr varchar(4000), @NumberLength int)
returns varchar(4000)
as
begin
declare @SortedStr varchar(4000) = @SortStr,
@StartIndex int = PATINDEX('%[0-9]%', @SortStr),
@EndIndex int = 0,
@PadLength int,
@TotalPadLength int = 0;
while @StartIndex <> 0
begin
set @StartIndex = @StartIndex + @EndIndex;
set @EndIndex = @StartIndex;
while PATINDEX('[0-9]', SUBSTRING(@SortStr, @EndIndex, 1)) = 1
set @EndIndex = @EndIndex + 1;
set @PadLength = @NumberLength - (@EndIndex - @StartIndex);
if @PadLength > 0
begin
set @SortedStr = STUFF(@SortedStr,
@TotalPadLength + @StartIndex,
0,
REPLICATE('0', @PadLength));
set @TotalPadLength = @TotalPadLength + @PadLength;
end;
set @EndIndex = @EndIndex - 1;
set @StartIndex = PATINDEX('%[0-9]%',
RIGHT(@SortStr, LEN(@SortStr) - @EndIndex));
end;
return @SortedStr;
end
Эта функция формирует строку, по которой MS SQL Server сделает "правильную" сортировку по текстовому полю. Как она работает? Предположим, что наши строки содержат только цифры: "12", "2", "22", "3". Если отсортировать их в лексикографическом порядке, то получим: "12", "2", "22", "3". Что бы отсортировать эти строки в порядке, который соответствует сортировке чисел, необходимо дополнить их слева до длины поля нулями. Допустим, длина нашего текстового поля будет 5 символов (@NumberLength = 5). Дополним строки нулями до длины в 5 символов и отсортируем: "00002", "00003", "00012", "00022". Теперь представим, что эти строки с цифрами и есть те самые многозначные числа, которые необходимо обработать атомарно. Получаем простой алгоритм формирования строки для сортировки: делим строку на символьные и числовые подстроки, числовые подстроки дополняем слева нулями до максимальной длины (@NumberLength) и объединяем подстроки в одну строку. В результате применения этого алгоритма набор строк для сортировки моего примера будет таким: "a00002a", "a00002b", "a00003a00002", "a00003a00002a", "a00012", "a00022", "a00030z".
select doc_index, doc_title from table order by dbo.sort_string(doc_index, 5)– А для Oracle у тебя такая функция есть?
– Нет, но я могу ее портировать.
При портировании функции на PL/SQL, я реализовал описанный алгоритм с использованием регулярных выражений:
create or replace function sort_string(SortStr varchar2,
NumberLength pls_integer)
return varchar2
is
SortedStr varchar2(4000);
begin
select LISTAGG(case
when REGEXP_INSTR(rs, '\d') > 0
then LPAD(rs, NumberLength, '0')
else rs
end) within group (order by order_num)
into SortedStr
from (select LEVEL as order_num,
REGEXP_SUBSTR(SortStr, '\d+|\D+', 1, LEVEL) as rs
from dual
connect by REGEXP_INSTR(SortStr, '\d+|\D+', 1, LEVEL) > 0) t;
return SortedStr;
end;
Новая версия нашего продукта кроме MS SQL Server и Oracle поддерживает еще и PostgreSQL. Поэтому под него пришлось написать третий вариант функции:
CREATE OR REPLACE FUNCTION sort_string(SortStr varchar(4000), NumberLength int)
RETURNS varchar(4000)
AS
$BODY$
select STRING_AGG(case
when rm[1]='' then ''
else LPAD(rm[1], NumberLength, '0')
end || rm[2], '')
from (select REGEXP_MATCHES(SortStr, '(^|\d+)(\D+|$)', 'g') rm) t;
$BODY$
LANGUAGE SQL STABLE;
P.S. В результате в таблицу с документами добавили поле, в которое триггер сохраняет строку для сортировки.
дизлайк и печальный смайлик, чё не на паскале?
ОтветитьУдалитьзы и сколько у этих регекспов суммарная скорость сортировки на мульенах строк?
Не на паскале, т.к. задача мне прилетела для БД. Да и тот софт написан на C#. На мульёнах строк не пробовал, т.к. не вижу в этом смысла. Как часто пользователю прикладной программы нужен список из миллиона документов? А хотите скорости, сохраните, как я, сформированную для сортировки строку в БД.
Удалить