Header RSS-подписка на обновления сайта eMail-подписка на обновления сайта

Понимаете ли вы коллейшены? Часть 4/8.

















Как формируются имена коллейшенов.

Мы закончили предыдущую, 3-ю часть статьи возможно самой сложной ее темой, темой разрешения конфликтов коллейшенов. Давайте немного переведем дыхание и, для контраста, возьмем тему самую легкую, об именах коллейшенов. Пора уже всем этим [collation_name1], [collation_name2] и т.д. из предыдущих частей статьи показать свое «истинное лицо». Какие же реально коллейшены есть в нашем распоряжении? Сразу напомню (если кто подзабыл самое начало статьи) что каждый отдельный коллейшен принадлежит строго одной из двух групп:

  • группа Windows коллейшенов
  • группа SQL Server коллейшенов

Так вот в рамках данной темы мы не касаемся вопросов «какая группа лучше», «как выбрать», и им подобным. Это все вопросы заключительного раздела. До его начала мы условимся воспринимать такое двух-групповое деление как факт и данность не подлежащее (пока!) обсуждению. Наша задача будет значительно проще: как из «корня» и нескольких «суффиксов» сформировать полноценное имя коллейшена адекватно воспринимаемое движком сервера. Вот к ней давайте и приступать.

Сразу же договоримся, что на понимание столь тонких материй как разница между хираганой и катаканой (составляющие японской письменности) мы не претендуем абсолютно. Еще меньше мы претендуем на понимание необходимости кодирования одного и того же символа иногда одним байтом, иногда (его же!) двумя. Это все актуально для дальневосточных языков по своей экзотичности равными японскому, а местами и превосходящими оный. Принимая указанные упрощения, для подавляющего большинства мировых языков, можно констатировать следующие факты.

Имена коллейшенов группы SQL Server.

Главное назначение коллейшенов этой группы — обеспечить работу с не-юникод символами, а значит в каждом конкретном случае должна быть известна не менее конкретная кодовая страница. И действительно, формальные правила построения имен коллейшенов этой группы говорят нам, что имя каждого формируется следующим образом:

SQL_основа_страница_суффикс1_суффикс2

где:

  • SQL и все четыре подчеркивания — буквальные литералы, которые так прямо и следует писать;
  • основа — фактически, алфавит и/или язык правила сортировки которого будут применятся в случае назначения данного коллейшена. Логично ожидать здесь увидеть названия языков, т.е. буквально Russian, English, Spanish, Italian и т.д. Однако ни одного из перечисленных и многих других ожидаемых языков вы не увидите. Всех их поглотил условный «мега-язык» по имени Latin1_General (обратите внимание, что его подчерк — добавочный к тем 4-м о которых говорит предыдущий пункт). Можно сказать что это обозначение любого языка не имеющего особых «фич», когда дело доходит до выстраивания слов по алфавиту. Если же язык обладает такими специфическими «отклонениями», то он выделяется персонально. Так, есть Czech, Hungarian, Polish, Scandinavian (снова группа языков, но все-таки отделенная от упомянутой «мега-группы»), Icelandic и ряд других. Число таких «персональщиков» (или языков выделенных в отдельные подгруппы) невелико. В любом случае, эта часть имени коллейшена ни в коем случае ничего не говорит о визуальном представлении каждого отдельного символа. Т.е. это ни разу не кодовая страница! Это именно указание на то «как правильно расположить слова по алфавиту»;
  • страница — а вот это прямая противоположность предыдущей части, именно кодовая страница определяющая внешний вид символов и не имеющая отношения к их алфавитному порядку. Эта часть имени сама, по хорошему, должна следовать такой схеме своего образования: CPxxxx, где CP — буквальный литерал, xxxx — трех-/четырех-значный персональный номер кодовой страницы поддерживаемый операционной системой Windows. С возможными номерами, если они вам интересны, можете ознакомиться по этой ссылке. А почему «по хорошему» и «должна следовать» вместо простого «следует», или даже «строго следует», спрашиваете вы? Читайте далее, автор обещает ваше любопытство удовлетворить;
  • суффикс1 — один из двух возможных литералов: CS или CI. Первый «постановляет», что при сравнении слов строчные/прописные буквы считать разными (case-sensitive). Второй декларирует ровно противоположное (case-insensitive);
  • суффикс2 — один из двух возможных литералов: AS или AI. Нужно ли учитывать диакритические знаки (первый, accent-sensitive) или их следует считать равными их базовым символам (второй, accent-insensitive). Не шибко актуально для языков вроде нашего с вами, да и английского тоже;
  • наконец — особый случай: сочетание (и только его) обоих суффиксов можно заменить литералом BIN, т.е. вместо суффикс1_суффикс2 просто пишется BIN. Это так называемая бинарная сортировка, или, как для краткости называю ее я сам — BIN-коллейшен. Тут надо понять следующее: при сравнении любых двух символов SQL Server может использовать 2 принципиально разных подхода (читай — алгоритма): лингвистический и числовой. Лингвистический алгоритм учитывает специфику именно языка, т.е. в некотором смысле он «думает» как человек при чтении справочников и прочих книг с алфавитными указателями. Можете считать, что этот алгоритм работает именно с «буквами», как они есть (хотя, понятно, любой алгоритм будет изначально извлекать код символа, так что этот взгляд верен исключительно с логической точки зрения). Числовой же алгоритм, напротив, не учитывает никаких черт присущих данному языку и работает с «буквами», как с байтами т.е. опирается исключительно на код символа в той или иной таблице (а именно — или в одной из кодовых страниц, или в «глобальной» таблице юникода). Отличить эти два алгоритма (а задаются они, как вы уже поняли, именем коллейшена) очень просто: все не BIN-коллейшены берут на вооружение лингвистический алгоритм, все BIN — числовой. И не зря схема построения имен коллейшенов организована таким образом, что вы принципиально не можете создать коллейшен оканчивающийся на _CS_BIN, или на _AI_BIN и т.п. Потому что уж либо вы анализируете и применяете синтаксические правила данного языка, и тогда вам доступно распознавание идентичных слов различающихся только регистром или применением диакритических знаков. Либо вы не делаете этого и тогда две буквы совпадают тогда и только тогда, когда равны их коды в таблицах упомянутых чуть ранее. Надеюсь основную «канву сюжета» повествующего о разнице двух подходов вы ухватили. В конце данного раздела у нас будет практический пример еще раз ее поясняющий.

Так вот, система (я имею в виду схему построения имен коллейшенов) выглядит весьма стройной и жизнеспособной. А что мы получили на практике? Тут, будем откровенны, у нас бардак. Нет, не так что бы полный, но... Судите сами. Согласно формальному синтаксису официальной документации префикс SQL_ обязателен для всех коллейшенов рассматриваемой группы, опускать его нельзя. В тоже время, вполне валидные коллейшены Danish_Norwegian_CS_AS, Icelandic_CS_AS, Greek_BIN и еще числом с десяток делают это с легкостью необычайной. Далее, разрешения опускать номер кодовой страницы (идущий после префикса CP) также никто не давал. В реальности? Вы уже все видели. Но и это еще не все, допустим такой коллейшен: SQL_Latin1_General_Cp1_CI_AS. Вроде как страница указана, но... что это за «страница 1»? Вроде же в коде страницы всегда было 3-4 цифры? Оказывается, это «для краткости и понятности», 8O так сократили вполне нормальную страницу с кодом 1252, та что Latin I. Понятно, что этот коллейшен и эта страница будут одними из самых востребованных среди пользователей сервера с алфавитами на основе латинского, но... еще три клавиши нажать совсем никак было? В общем, на практике дело обстоит так: идем в официальную таблицу имен коллейшенов группы SQL (благо там перечислены абсолютно все коллейшены указанной группы), находим там несколько (чаще всего; иногда кандидат изначально будет единственным) примерно удовлетворяющих нашим запросам коллейшенов, анализируем их имена согласно приведенной выше схеме и, наконец, отбираем для работы один, «самый-самый». Можно и поставить поиск подходящего коллейшена «на научную основу», написанием соответствующего запроса к системной функции fn_helpcollations. Допустим, поищем коллейшены интересные для нас с вами, а именно те, что относятся к русскому языку:

1
2
SELECT * FROM fn_helpcollations()
WHERE name LIKE 'Cyr%' OR name LIKE '%1251%'

Получаем вполне симпатичный резалт-сет, в левой колонке которого содержатся готовые (т.е. полностью сформированные) имена коллейшенов, а в правой — их краткое описание (которое тоже лишним не будет). Вот только «сепарацию» на принадлежность коллейшена одной из двух групп скрипт не делает, это уж «ручная работа». Итого, можно констатировать, что для русскоговорящей аудитории пользователей SQL Server, из группы «SQL коллейшены» самыми востребованными будут:

  • SQL_Latin1_General_CP1251_CI_AS — кодовая страница 1251 (Cyrillic), лингвистический алгоритм сортировки без учета регистра
  • SQL_Latin1_General_CP1251_CS_AS — то же самое, но с учетом регистра
  • Cyrillic_General_BIN — та же кодовая страница, алгоритм числовой

Теперь — обещанный практический пример освещающий разницу двух алгоритмов сортировки.

1
2
3
4
5
6
7
CREATE TABLE T1 (Col1 char(1))
GO
INSERT T1 VALUES ('б')
INSERT T1 VALUES ('ч')
INSERT T1 VALUES ('я')
INSERT T1 VALUES ('Ч')
INSERT T1 VALUES ('Б')

Вот с этой-то элементарной таблицей из 5-ти строк поставим кое-какие эксперименты. Сначала упорядочим буквы без учета регистра:

1
SELECT * FROM T1 ORDER BY Col1 COLLATE SQL_Latin1_General_Cp1251_CI_AS

На выходе:

Col1
б
Б
ч
Ч
я

Порядок букв, если мы еще помним азбуку, :) возражений не вызывает. Но почему для одних и тех же маленькие впереди больших? Чистая случайность, уверяю вас. Не исключено, что на вашем сервере будет наоборот. Буквы отличающиеся лишь регистром равны, прямо как мы и заказывали. Убедиться в этом очень легко:

1
SELECT * FROM T1 WHERE Col1='б' COLLATE SQL_Latin1_General_Cp1251_CI_AS

На выходе:

Col1
б
Б

Как видим, с точки зрения сервера, и при таком коллейшене оба символа идентичны. Эксперимент номер 2 — то же, но с различением регистра:

1
SELECT * FROM T1 ORDER BY Col1 COLLATE SQL_Latin1_General_Cp1251_CS_AS

Резалт-сет:

Col1
б
Б
ч
Ч
я

Казалось бы — ничего не изменилось. А между тем разница принципиальна. Теперь указанный порядок строк гарантируется на всех серверах и машинах мира, потому что буквы «разнорегистровые» стали различаться. Подтвердим последний посыл:

1
SELECT * FROM T1 WHERE Col1='б' COLLATE SQL_Latin1_General_Cp1251_CS_AS

Резалт-сет:

Col1
б

А Б-большое исчезло. Кстати — не удивил ли вас предпоследний резалт-сет выводящий всю таблицу с учетом регистра? Нет? А многих ваших коллег удивляет. Рассуждают они на первый взгляд логично: в ASCII-таблице Б-большая (да и Ч тоже) впереди б-малой, мы заказываем «с учетом» — ну и? Но удивляются они единственно потому, что не читали данную статью :roll: и упускают из вида, что все CI/CS/AI/AS сортировки не знают слов «ASCII», «таблица символов», «код символа» и прочих в том же ключе. Вот «буква большая» и «такая же малая» — знают. А про коды — нет. А дальше, если совсем чуть-чуть упростить ситуацию для более быстрого/компактного ее изложения, все происходит по такому примерно пути. Каждой сличаемой букве (символу) движок присваивает «вес». Да не один, а пару: первичный и вторичный. Символы с более «легким» весом «всплывают» в «шапку» резалт-сета, с более «тяжелым» — идут к ее «дну». Сначала соревнуются друг с другом первичные веса, вычисляемые из простого положения буквы в букваре. Обычном «человеческом» букваре, из первого класса. Чем ближе к его началу — тем «легче» первичный вес. Если на этом этапе веса различаются — сличение закончено, «легкий» претендент отправляется вверх, «тяжелый» — «вниз». Таким образом никакая Ч не будет впереди никакого Б. Никогда (разумеется, автор рассуждает только о сортировке «прямого» порядка, от А к Я). Просто потому, что нету таких букварей, с таким раскладом как Ч...Б. Если же позиция двух букв в букваре совпала (и первичные веса уравнялись) в борьбу вступают веса вторичные, выводимые из регистра букв и факта наличия в них диакритических знаков. Для нас с вами, говорящих на «великом-могучем» и пишущих код на English, актуально всего одно, простое и понятное, но от этого не менее важное правило:

Вторичный вес буквы нижнего регистра (строчной) меньше вторичного веса такой же буквы верхнего регистра (прописной).

Вот и все «загадки», и при чем тут ASCII? Заметьте, что «постигнув» последний параграф вы элементарно можете сформулировать «техническую» разницу между сортировками CS/CI: первая учитывает вторичный вес символа, а вторая — нет.

Наконец, вспоминаем, что пример вообще-то затевался для демонстрации отличия в числовой (BIN) сортировке. Давайте уже и ее посмотрим:

1
SELECT * FROM T1 ORDER BY Col1 COLLATE Cyrillic_General_BIN

Резалт-сет:

Col1
Б
Ч
б
ч
я

Как говорится — специально для «ASCII-fun», все точненько по табличечке. Однако с этой ASCII-сортировкой связан интересный вопрос, и даже два. Первый вопрос — а нельзя ли «регистро-независимый-ASCII», т.е. так что б резалт-сет был бы:

Col1
Б
б
Ч
ч
я

Вопрос отнюдь не праздный. Вот, допустим у этих .NET-программеров (к коим автор скромно причисляет и себя :oops: ) есть enum (перечисление) CompareOptions, и в нем член OrdinalIgnoreCase, делающий ровно требуемое! Ну а «SQLщики» чем хуже? На скромный взгляд автора последний (предполагаемый) резалт-сет будет «поактуальнее» предпоследнего (реального). Он как-бы больше «человеко-ориентированный». Да, почти искомого можно добиться лингвистической CS-сортировкой, резалт-сет чуть выше тому подтверждение. Почти-то оно почти, НО! Во-первых, как мы знаем теперь, при таком подходе маленькие буквы будут «сверху». А это — не факт что хорошо, ой не факт... Но это еще ладно, есть «засада» посерьезнее. Допустим, у нас в базе есть две строки каждая из 3-х символов ровно. Все символы входят в нашу родную кодовую страницу 1251 (Cyrillic) причем первые две совпадают абсолютно, это буквы ЧБ, прописные. Третья же различна, для первой строки это символ с кодом 0xB2, для второй — 0xB5. Мы, вполне резонно, желаем видеть первую строчку сверху (B2<B5). Попробуем добиться этой цели обеими лингвистическими сортировками, и одной числовой.