ORDER BY и сравнение смешанных строк букв и цифр

9

Нам нужно сделать несколько отчетов о значениях, которые обычно представляют собой смешанные строки из цифр и букв, которые должны быть отсортированы «естественно». Такие вещи, как, например, «P7B18» или «P12B3». @ Строки в основном состоят из последовательности букв, а затем цифр. Число этих сегментов и длина каждого могут варьироваться.

Мы бы хотели, чтобы их числовые части сортировались в числовом порядке. Очевидно, что если я просто обработаю эти строковые значения напрямую ORDER BY, то «P12B3» будет предшествовать «P7B18», поскольку «P1» раньше, чем «P7», но я бы хотел обратного, поскольку «P7» естественным образом предшествует "P12".

Я также хотел бы иметь возможность делать сравнения диапазонов, например, @bin < 'P13S6'или что-то подобное. Мне не нужно обрабатывать числа с плавающей запятой или отрицательные числа; это будут строго неотрицательные целые числа, с которыми мы имеем дело. Длина строки и количество сегментов могут быть произвольными без фиксированных верхних границ.

В нашем случае строковый регистр не важен, хотя, если есть способ сделать это с учетом параметров сортировки, другие могут найти это полезным. Самая уродливая часть всего этого - я бы хотел иметь возможность упорядочивать и фильтровать диапазон в WHEREпредложении.

Если бы я делал это в C #, это было бы довольно простой задачей: выполнить некоторый анализ, чтобы отделить альфа от числового значения, реализовать IComparable, и вы в основном сделали. Разумеется, SQL Server, похоже, не обладает подобной функциональностью, по крайней мере, насколько мне известно.

Кто-нибудь знает какие-нибудь хорошие приемы, чтобы сделать эту работу? Есть ли какая-то малоизвестная возможность создания пользовательских типов CLR, которые реализуют IComparable и ведут себя так, как ожидается? Я также не против глупых хитростей XML (см. Также: конкатенация списков), и у меня также есть функции оболочки соответствия / извлечения / замены регулярных выражений CLR, доступные на сервере.

РЕДАКТИРОВАТЬ: В качестве более подробного примера я хотел бы, чтобы данные вели себя примерно так.

SELECT bin FROM bins ORDER BY bin

bin
--------------------
M7R16L
P8RF6JJ
P16B5
PR7S19
PR7S19L
S2F3
S12F0

то есть разбить строки на токены всех букв или всех чисел и отсортировать их по алфавиту или по номерам соответственно, причем наиболее левый токен является наиболее значимым термином сортировки. Как я уже упоминал, в .NET легко, если вы реализуете IComparable, но я не знаю, как (или если) вы можете делать такие вещи в SQL Server. Это, конечно, не то, с чем я когда-либо сталкивался за 10 или около того лет работы с ним.

db2
источник
Вы можете сделать это с каким-то индексированным вычисляемым столбцом, превратив строку в целое число. Так P7B12могло бы стать P 07 B 12, то (через ASCII) 80 07 65 12, так80076512
Philᵀᴹ
Я предлагаю вам создать вычисляемый столбец, который дополняет каждый числовой компонент большой длиной (т. Е. 10 нулями). Поскольку формат довольно произвольный, вам понадобится довольно большое встроенное выражение, но это выполнимо. Затем вы можете индексировать / упорядочивать / где в этом столбце столько, сколько хотите.
Nick.McDermaid
Пожалуйста, смотрите ссылку, которую я только что добавил в начало моего ответа :)
Соломон Руцки
1
@srutzky Хорошо, я голосовал за это.
db2
Эй, db2: из-за того, что Microsoft перешла из Connect в UserVoice и не совсем сохранила счетчик голосов (они помещают его в комментарий, но не уверены, что смотрят на это), вам, возможно, придется повторно проголосовать за него: поддержка «естественной сортировки» / DIGITSASNUMBERS как опция сортировки . Спасибо!
Соломон Руцкий

Ответы:

8

Хотите разумный, эффективный способ сортировки чисел в строках как фактические числа? Рассмотрите возможность голосования за мое предложение Microsoft Connect: поддержка "естественной сортировки" / DIGITSASNUMBERS в качестве параметра сортировки


Нет простых встроенных способов сделать это, но есть возможность:

Нормализуйте строки, переформатируя их в сегменты фиксированной длины:

  • Создайте столбец сортировки типа VARCHAR(50) COLLATE Latin1_General_100_BIN2. Максимальная длина 50, возможно, должна быть скорректирована на основе максимального количества сегментов и их потенциальной максимальной длины.
  • Хотя нормализацию можно было бы выполнить на уровне приложений более эффективно, обработка этого в базе данных с использованием UDF T-SQL позволит поместить скалярный UDF в AFTER [or FOR] INSERT, UPDATEтриггер, так что вы гарантированно правильно установите значение для всех записей, даже тех, которые входящие с помощью специальных запросов и т. д. Конечно, этот скалярный UDF также может обрабатываться с помощью SQLCLR, но его нужно будет проверить, чтобы определить, какой из них действительно более эффективен. **
  • UDF (независимо от того, находится ли он в T-SQL или SQLCLR) должен:
    • Обработайте неизвестное количество сегментов, читая каждый символ и останавливаясь, когда тип переключается с буквенного на цифровой или числового на буквенный.
    • Для каждого сегмента он должен возвращать строку фиксированной длины с максимально возможными символами / цифрами любого сегмента (или, возможно, максимум + 1 или 2 для учета будущего роста).
    • Альфа-сегменты должны быть выровнены по левому краю и дополнены пробелами справа.
    • Числовые сегменты должны быть выровнены по правому краю и дополнены нулями слева.
    • Если буквенные символы могут входить в смешанный регистр, но порядок должен быть регистрозависимым, примените UPPER()функцию к конечному результату всех сегментов (так что это нужно сделать только один раз, а не для каждого сегмента). Это позволит правильно сортировать данные с учетом двоичного сопоставления столбца сортировки.
  • Создайте AFTER INSERT, UPDATEв таблице триггер, который вызывает UDF для установки столбца сортировки. Чтобы повысить производительность, используйте UPDATE()функцию, чтобы определить, находится ли этот столбец кода даже в SETпредложении UPDATEоператора (просто RETURNесли false), а затем присоедините псевдотаблицы INSERTEDи DELETEDк столбцу кода, чтобы обрабатывать только те строки, которые имеют изменения в значении кода. , Обязательно укажите COLLATE Latin1_General_100_BIN2в этом условии JOIN, чтобы обеспечить точность определения, есть ли изменение.
  • Создайте индекс для нового столбца сортировки.

Пример:

P7B18   -> "P     000007B     000018"
P12B3   -> "P     000012B     000003"
P12B3C8 -> "P     000012B     000003C     000008"

При таком подходе вы можете сортировать по:

ORDER BY tbl.SortColumn

И вы можете сделать фильтрацию диапазона с помощью:

WHERE tbl.SortColumn BETWEEN dbo.MyUDF('P7B18') AND dbo.MyUDF('P12B3')

или:

DECLARE @RangeStart VARCHAR(50),
        @RangeEnd VARCHAR(50);
SELECT @RangeStart = dbo.MyUDF('P7B18'),
       @RangeEnd = dbo.MyUDF('P12B3');

WHERE tbl.SortColumn BETWEEN @RangeStart AND @RangeEnd

И фильтр, ORDER BYи WHEREфильтр должны использовать двоичное сопоставление, определенное для SortColumnприоритета сопоставления .

Сравнение равенства все равно будет выполнено в исходном столбце значений.


Другие мысли:

  • Используйте SQLCLR UDT. Это может сработать, хотя неясно, представляет ли это чистый выигрыш по сравнению с подходом, описанным выше.

    Да, в SQLCLR UDT операторы сравнения могут быть переопределены пользовательскими алгоритмами. Это обрабатывает ситуации, когда значение сравнивается либо с другим значением того же пользовательского типа, либо с тем, которое необходимо неявно преобразовать. Это должно обрабатывать фильтр диапазона в WHEREусловии.

    Что касается сортировки UDT как обычного типа столбца (не вычисляемого столбца), это возможно только в том случае, если UDT имеет «байтовый порядок». Быть «упорядоченным в байтах» означает, что двоичное представление UDT (которое может быть определено в UDT) естественно сортируется в соответствующем порядке. Предполагая, что двоичное представление обрабатывается аналогично подходу, описанному выше для столбца VARCHAR (50), который имеет сегменты фиксированной длины, которые дополняются, это будет соответствовать. Или, если было непросто убедиться, что двоичное представление естественным образом упорядочено надлежащим образом, вы можете предоставить метод или свойство UDT, которые выводят значение, которое будет правильно упорядочено, и затем создать PERSISTEDвычисляемый столбец для этого. метод или свойство. Метод должен быть детерминированным и помечен как IsDeterministic = true.

    Преимущества этого подхода:

    • Нет необходимости в поле «оригинальное значение».
    • Не нужно вызывать UDF для вставки данных или сравнения значений. Предполагая, что Parseметод UDT принимает P7B18значение и преобразует его, вы можете просто вставить значения естественным образом как P7B18. А с помощью метода неявного преобразования, установленного в UDT, условие WHERE также позволяет использовать просто P7B18`.

    Последствия такого подхода:

    • Простой выбор поля вернет двоичное представление, если в качестве типа данных столбца используется UDT, упорядоченный в байтах. Или, если использовать PERSISTEDвычисляемый столбец для свойства или метода UDT, вы получите представление, возвращаемое свойством или методом. Если вам нужно исходное P7B18значение, то вам нужно вызвать метод или свойство UDT, которое закодировано, чтобы вернуть это представление. Так как вы ToStringвсе равно должны переопределить метод, это хороший вариант для этого.
    • Неясно (по крайней мере для меня сейчас, поскольку я не тестировал эту часть), насколько легко / сложно было бы внести какие-либо изменения в двоичное представление. Изменение сохраненного сортируемого представления может потребовать удаления и повторного добавления поля. Кроме того, удаление сборки, содержащей UDT, не удастся, если используется каким-либо образом, поэтому вы должны убедиться, что в сборке не было ничего кроме этого UDT. Вы можете ALTER ASSEMBLYзаменить определение, но на это есть некоторые ограничения.

      С другой стороны, VARCHAR()поле - это данные, которые отсоединены от алгоритма, поэтому потребуется только обновить столбец. И если есть десятки миллионов строк (или больше), то это можно сделать в пакетном режиме.

  • Реализовать ICU библиотека , которая фактически позволяет для делать это буквенно - цифровой сортировки. Несмотря на высокую функциональность, библиотека поставляется только на двух языках: C / C ++ и Java. Это означает, что вам может потребоваться внести некоторые изменения, чтобы заставить его работать в Visual C ++, или есть вероятность, что код Java может быть преобразован в MSIL с использованием IKVM . На этом сайте есть один или два проекта стороны .NET, которые предоставляют интерфейс COM, к которому можно получить доступ в управляемом коде, но я считаю, что они не обновлялись в течение некоторого времени, и я не пробовал их. Лучше всего было бы обрабатывать это на уровне приложения с целью генерации ключей сортировки. Ключи сортировки будут сохранены в новом столбце сортировки.

    Это может быть не самый практичный подход. Тем не менее, все еще очень круто, что такая способность существует. Я предоставил более подробный обзор этого примера в следующем ответе:

    Существует ли сортировка для сортировки следующих строк в следующем порядке 1,2,3,6,10,10A, 10B, 11?

    Но шаблон, который рассматривается в этом вопросе, немного проще. Для примера, показывающего, что тип шаблона, рассматриваемый в этом Вопросе, также работает, перейдите на следующую страницу:

    ICU Collation Demo

    В разделе «Настройки» установите «числовой» параметр на «вкл.», А для всех остальных - «по умолчанию». Далее, справа от кнопки «Сортировка», снимите флажок с «Сила различий» и установите флажок «Ключи сортировки». Затем замените список элементов в текстовой области «Ввод» следующим списком:

    P12B22
    P7B18
    P12B3
    as456456hgjg6786867
    P7Bb19
    P7BA19
    P7BB19
    P007B18
    P7Bb20
    P7Bb19z23

    Нажмите кнопку «Сортировка». В текстовой области «Вывод» должно отображаться следующее:

    as456456hgjg6786867
        29 4D 0F 7A EA C8 37 35 3B 35 0F 84 17 A7 0F 93 90 , 0D , , 0D .
    P7B18
        47 0F 09 2B 0F 14 , 08 , FD F1 , DC C5 DC 05 .
    P007B18
        47 0F 09 2B 0F 14 , 08 , FD F1 , DC C5 DC 05 .
    P7BA19
        47 0F 09 2B 29 0F 15 , 09 , FD FF 10 , DC C5 DC DC 05 .
    P7Bb19
        47 0F 09 2B 2B 0F 15 , 09 , FD F2 , DC C5 DC 06 .
    P7BB19
        47 0F 09 2B 2B 0F 15 , 09 , FD FF 10 , DC C5 DC DC 05 .
    P7Bb19z23
        47 0F 09 2B 2B 0F 15 5B 0F 19 , 0B , FD F4 , DC C5 DC 08 .
    P7Bb20
        47 0F 09 2B 2B 0F 16 , 09 , FD F2 , DC C5 DC 06 .
    P12B3
        47 0F 0E 2B 0F 05 , 08 , FD F1 , DC C5 DC 05 .
    P12B22
        47 0F 0E 2B 0F 18 , 08 , FD F1 , DC C5 DC 05 .

    Обратите внимание, что ключи сортировки имеют структуру в нескольких полях, разделенных запятыми. Каждое поле должно быть отсортировано независимо, поэтому возникает еще одна небольшая проблема, которую необходимо решить, если необходимо реализовать это в SQL Server.


** Если есть какие-либо сомнения по поводу эффективности использования пользовательских функций, обратите внимание, что предлагаемые подходы используют их минимально. Фактически, главная причина хранения нормализованного значения состояла в том, чтобы избежать вызова UDF для каждой строки каждого запроса. В первичном подходе UDF используется для установки значения SortColumn, и это делается только после INSERTи UPDATEчерез триггер. Выбор значений гораздо более распространен, чем вставка и обновление, и некоторые значения никогда не обновляются. Для каждого SELECTзапроса, который использует SortColumnфильтр диапазона в WHEREпредложении, UDF требуется только один раз для каждого из значений range_start и range_end, чтобы получить нормализованные значения; UDF не называется для каждой строки.

Что касается UDT, то использование фактически такое же, как и для скалярного UDF. Значение, вставка и обновление будет вызывать метод нормализации один раз для каждой строки, чтобы установить значение. Затем метод нормализации будет вызываться один раз для каждого запроса для каждого range_start и range_value в фильтре диапазона, но не для каждой строки.

Точка в пользу обработки нормализации полностью в SQLCLR UDF является то , что дано это не делает никакого доступа к данным и является детерминированным, если он помечен как IsDeterministic = true, то он может участвовать в параллельных планах (которые могли бы помочь INSERTи UPDATEоперации) , в то время как T-SQL UDF предотвратит использование параллельного плана.

Соломон Руцкий
источник