Теоретический конспект: сортировка и фильтрация данных в СУБД

Тема 3. Этапы проектирования баз данных — Практическое занятие №7

1. Реляционная природа порядка и предложение ORDER BY

В реляционной алгебре Эдгара Кодда отношение (relation) математически строго определяется как подмножество декартова произведения доменов, то есть неупорядоченное множество кортежей. В реляционной таблице физический порядок строк на дисковых страницах кучи (Heap Pages) является недетерминированным и подвержен случайным факторам (порядок параллельной вставки, повторное использование блоков после DELETE, фоновая очистка VACUUM).

Аксиома реляционного порядка: Если в SQL-запросе отсутствует явное предложение ORDER BY, клиентское приложение не имеет права делать никаких предположений о порядке следования строк в результирующем наборе.

Синтаксис расширенного предложения ORDER BY

Спецификация ANSI SQL определяет следующую структуру секции упорядочивания:

SELECT <список_полей>
FROM <источник_данных>
[WHERE <предикаты_фильтрации>]
ORDER BY <выражение_1> [ASC | DESC] [NULLS FIRST | NULLS LAST]
       [, <выражение_2> [ASC | DESC] [NULLS FIRST | NULLS LAST] ...];

При многоуровневой сортировке сравнение кортежей выполняется по правилу лексикографического порядка: сначала по выражению 1; если значения равны — по выражению 2; и так далее до разрешения коллизии.

2. Поведение NULL-значений и языковые правила сопоставления (Collation)

2.1. Неопределенность NULL и явные директивы NULLS FIRST / NULLS LAST

Поскольку NULL обозначает отсутствие значения, он не может быть строго сравнен с константами (результат 5 > NULL есть UNKNOWN). Стандарт SQL возлагает позиционирование NULL на движок СУБД, предоставляя явные директивы:

Директива Позиция NULL в выборке Типовое применение в бизнес-логике
NULLS LAST Всегда в самом конце результирующего списка Сортировка товаров по рейтингу от 5 до 1: товары без отзывов (rating IS NULL) выводятся в конце, а не на первых позициях.
NULLS FIRST Всегда в самом начале результирующего списка Мониторинг авиарейсов: срочные рейсы с неназначенным гейтом (gate IS NULL) выводятся вверху табло диспетчера.

2.2. Collation: кодовые таблицы и локализованная сортировка

Строковые поля (VARCHAR, TEXT) сортируются на основе таблицы сопоставления (Collation). Разница между бинарным порядком (C/POSIX) и естественным языковым порядком (Natural Sort):

Сравнение Collation при упорядочивании кириллицы:
1. Collation "C" (бинарный код Unicode UTF-8): [ 'Ёлка' (код 0x0401) ] --> [ 'Арбуз' (0x0410) ] --> [ 'Яблоко' (0x042F) ] --> [ 'аист' (0x0430) ] (Ошибка: буква 'Ё' выпадает перед буквой 'А', заглавные отделены от строчных). 2. Collation "ru_RU.UTF-8" (ICU локаль русского языка): [ 'аист' / 'Арбуз' ] --> [ 'Ель' ] --> [ 'Ёлка' ] --> [ 'Яблоко' ] (Корректно: буква 'Ё' следует строго за буквой 'Е', регистр не разрывает алфавитный ряд).

Синтаксис явного указания локали в запросе: ORDER BY student_last_name COLLATE "ru_RU" ASC.

3. Физические алгоритмы сортировки в СУБД: Quicksort и External Merge Sort

Когда СУБД встречает узел плана Sort, оптимизатор принимает решение о выделении памяти на основе параметра процесса (например, work_mem в PostgreSQL, по умолчанию 4 MB):

Пошаговая схема алгоритма External Merge Sort (Внешняя сортировка слиянием):
+---------------------------------------------------------------------------------------------------+ | 1. ВХОДНОЙ ПОТОК: Таблица 200 МБ (не помещается в оперативную память work_mem = 16 МБ) | +---------------------------------------------------------------------------------------------------+ | v +---------------------------------------------------------------------------------------------------+ | ФАЗА 1: Генерация серий (Run Generation) | | - Считывается блок данных объемом work_mem (16 МБ) | | - Сортируется в ОЗУ с помощью In-Memory Quicksort | | - Записывается во временный файл на диске как Run 1 (16 МБ отсортированных кортежей) | | - Повторяется до исчерпания таблицы: Run 1, Run 2, Run 3 ... Run 13 на SSD/диске | +---------------------------------------------------------------------------------------------------+ | v +---------------------------------------------------------------------------------------------------+ | ФАЗА 2: Многопутевое слияние (Multi-way K-Run Merge Pass) | | - В память загружаются первые страницы каждого Run-файла (буферы чтения) | | - Min-Heap (турнирное дерево) выбирает наименьший элемент среди K серий | | - Наименьший кортеж направляется в выходной поток / буфер записи | | - Опустевшие страницы Run-файлов подгружаются с диска по мере продвижения указателей | +---------------------------------------------------------------------------------------------------+ | v +---------------------------------------------------------------------------------------------------+ | ВЫХОД: Полностью отсортированный результирующий набор кортежей (Sort Method: external merge Disk) | +---------------------------------------------------------------------------------------------------+
Влияние на дисковую подсистему: Сортировка на диске генерирует интенсивный I/O трафик во временных табличных пространствах. Чтобы исключить сброс на диск, администраторы настраивают SET work_mem = '64MB'; для конкретной сессии аналитического запроса либо создают специализированный B-Tree индекс, возвращающий строки в готовом порядке без выполнения операции Sort.

4. Постраничная навигация (Пагинация): LIMIT / OFFSET vs Keyset Cursor

4.1. Проблема классического смещения OFFSET

Запрос SELECT * FROM ledger ORDER BY id ASC LIMIT 20 OFFSET 500000; заставляет СУБД физически просканировать и отсортировать 500 020 кортежей, после чего 500 000 строк отбрасываются. Вычислительная сложность $O(N)$ делает запрос медленнее с каждой следующей страницей.

4.2. Метод Keyset (Seek Method)

Основан на запоминании последнего значения ключа предыдущей страницы:

-- Запрос первой страницы:
SELECT id, contract_number, signed_date 
FROM contracts 
WHERE payout_sum > 500000 
ORDER BY signed_date DESC, id DESC 
LIMIT 20;

-- Запрос следующей страницы (Keyset Pagination):
SELECT id, contract_number, signed_date 
FROM contracts 
WHERE payout_sum > 500000 
  AND (signed_date, id) < ('2026-03-31', 48912) 
ORDER BY signed_date DESC, id DESC 
LIMIT 20;

Используя B-Tree индекс по (signed_date, id), СУБД мгновенно переходит к нужной ветке дерева за время $O(\log N)$ и читает ровно 20 кортежей.

5. Вопросы для самопроверки

1. Почему результат запроса без предложения ORDER BY считается математически недетерминированным?

Потому что реляционная модель рассматривает отношение как неупорядоченное множество. СУБД вправе считывать блоки из кучи в произвольном порядке (особенно при многопоточном сканировании Parallel Seq Scan или после операций UPDATE/VACUUM).

2. В чем разница в поведении NULL по умолчанию между PostgreSQL и MySQL при сортировке по возрастанию (ASC)?

В PostgreSQL по умолчанию NULL считается наибольшим возможным значением, поэтому при ASC попадает в конец (NULLS LAST). В MySQL NULL трактуется как наименьшее значение, поэтому при ASC выводится в начале.

3. При каких условиях СУБД переключается с алгоритма In-Memory Quicksort на External Merge Sort?

Когда объем сортируемого набора данных (ключей и строк) превышает размер оперативной памяти, выделенной процессу параметром конфигурации (work_mem в PostgreSQL или sort_buffer_size в MySQL).

4. Почему пагинация на основе конструкции OFFSET 1000000 LIMIT 20 опасна для высоконагруженной СУБД?

Потому что планировщик обязан физически прочитать и отбросить 1 000 000 строк. При Keyset пагинации поиск точки отсчета выполняется по индексу за $O(\log N)$ с чтением всего 20 строк.