Почему «ВЫБРАТЬ ПЕРВЫЕ 1» без сортировки — это мина замедленного действия в запросах 1С

Программист 1С v8.3 (Управляемые формы) IT и автоматизация бизнеса
← На главную

Каждый разработчик 1С рано или поздно сталкивается с задачей, когда нужно получить всего одну запись из таблицы базы данных. На первый взгляд, конструкция ВЫБРАТЬ ПЕРВЫЕ 1 кажется идеальным решением. Но задумывались ли вы, гарантирует ли СУБД, что при каждом выполнении такого запроса будет возвращаться один и тот же элемент справочника или документа, если в запросе отсутствует предложение УПОРЯДОЧИТЬ ПО? В этой статье мы подробно разберем внутренние механизмы СУБД (MS SQL Server и PostgreSQL), особенности файловой базы данных 1С, стандарты разработки фирмы «1С» и выясним, почему отсутствие явной сортировки может привести к критическим сбоям в работе системы.

Суть проблемы: Свобода выбора оптимизатора СУБД

Начнем с главного правила реляционных баз данных: физический порядок хранения строк в таблице не гарантирует логический порядок их выборки. Если в запросе не указано предложение УПОРЯДОЧИТЬ ПО (в SQL-эквиваленте — ORDER BY), СУБД возвращает строки в том порядке, в котором ей в данный момент удобнее и быстрее всего их прочитать.

Рассмотрим пример запроса, который часто пишут начинающие разработчики, полагая, что справочник всегда вернет «первый» созданный элемент:


ВЫБРАТЬ ПЕРВЫЕ 1
	Ссылка КАК Договор
ИЗ
	Справочник.ДоговорыКонтрагентов
ГДЕ
	Владелец = &Контрагент

В тестовой базе или на небольшом объеме данных этот запрос может месяцами возвращать один и тот же договор. Однако это поведение абсолютно случайно и может измениться в любой момент под воздействием различных внешних факторов.

Как работает оптимизатор MS SQL Server при отсутствии сортировки

Для понимания физики процесса разберем, как MS SQL Server обрабатывает запрос TOP 1 (транслируемый платформой 1С из ПЕРВЫЕ 1):

  1. Использование «узких» некластерных индексов. Поле Ссылка (уникальный идентификатор GUID) в таблицах 1С является кластерным ключом. Особенность архитектуры SQL Server заключается в том, что кластерный ключ неявно присутствует во всех некластерных индексах таблицы в качестве указателя на физическую строку. Если у справочника договоров есть некластерный индекс (например, по реквизиту «Код» или «Наименование»), оптимизатор SQL Server предпочтет сканировать именно его. Поскольку этот индекс физически занимает меньше страниц памяти (он «уже»), СУБД прочитает первую страницу некластерного индекса. В итоге возвращенное значение будет определяться сортировкой этого индекса (например, по алфавиту наименований), а вовсе не порядком создания записи.
  2. Влияние кэша данных (Buffer Pool). Если нужная страница данных или страница некластерного индекса уже находится в оперативной памяти (в кэше СУБД), оптимизатору выгоднее взять её, чтобы избежать физического чтения с диска. В зависимости от того, какие запросы выполнялись ранее другими пользователями и какие данные закэшированы, «первый попавшийся» элемент может меняться.
  3. Параллелизм (Parallelism). На больших таблицах при наличии нескольких ядер процессора SQL Server может построить параллельный план выполнения запроса. Несколько потоков будут сканировать разные части таблицы одновременно. Какой из потоков первым вернет строку в оператор сборщика потоков (Gather Streams), та строка и станет результатом TOP 1. Под разной вычислительной нагрузкой этот результат будет непредсказуемо меняться.

Официальная документация MSDN прямо заявляет: «Если вы используете TOP без предложения ORDER BY, то TOP возвращает строки в неопределенном порядке». Таким образом, полагаться на «порядок по умолчанию» в клиент-серверной архитектуре — грубейшая ошибка.

Особенности поведения PostgreSQL

Если ваша база данных 1С работает под управлением PostgreSQL, ситуация становится еще более непредсказуемой из-за механизма синхронизированного сканирования (Synchronized Sequential Scans):

Когда одна транзакция уже выполняет последовательное чтение (Seq Scan) большой таблицы (например, строится тяжелый отчет), а другой сеанс запускает свой запрос LIMIT 1 (аналог ПЕРВЫЕ 1) по этой же таблице, PostgreSQL «пристыковывает» второй запрос к первому для экономии дискового ввода-вывода. Чтение для второго запроса начнется не с начала таблицы, а с той страницы, которую первый запрос читает прямо сейчас. Как только нужная запись найдена, запрос завершается. В результате вы получите в качестве «первой» случайную строку из середины или конца таблицы.

Почему файловая база данных (.1CD) тоже подвержена этой проблеме

Бытует мнение, что в файловой базе данных порядок строк всегда фиксирован. Действительно, в файловой СУБД данные хранятся последовательно на страницах размером 4 Кб. Но и здесь стабильность выборки — это иллюзия:

  1. Эффект «пустых слотов». При удалении элементов из справочника платформа 1С не сдвигает физически строки в файле базы данных, а лишь помечает эти строки как удаленные. При создании новых элементов платформа старается оптимизировать дисковое пространство и записывает новые данные в первый освободившийся «слот». Физически новая запись может оказаться в самом начале таблицы, сдвинув прежний «первый» элемент.
  2. Реструктуризация и выгрузка/загрузка базы (.dt). Это классический сценарий сбоя, с которым сталкивались многие разработчики. При выполнении операции «Тестирование и исправление» (сжатие таблиц) или при перезаливке базы через выгрузку в .dt файлы таблиц создаются заново. Все пустые слоты очищаются, данные записываются последовательно, и физический порядок строк на диске полностью меняется. После этой процедуры алгоритмы, завязанные на ПЕРВЫЕ 1 без сортировки, гарантированно ломаются.

Реальный пример из практики

Рассмотрим жизненную ситуацию. Разработчик написал запрос к регистру сведений ДанныеПроизводственныхКалендарей для получения ближайшего рабочего дня. Запрос успешно работал больше года:


ВЫБРАТЬ ПЕРВЫЕ 1
	Календарь.Дата КАК РабочаяДата
ИЗ
	РегистрСведений.ДанныеПроизводственныхКалендарей КАК Календарь
ГДЕ
	Календарь.Дата > &ТекущаяДата И Календарь.ВидДня = &РабочийДень

После проведения планового обслуживания базы данных (выгрузки-загрузки через .dt и обновления статистики СУБД) отчеты, использующие этот запрос, стали выдавать некорректные даты. Выяснилось, что порядок строк в выборке изменился. Проблема решилась мгновенно после добавления явного упорядочивания:


ВЫБРАТЬ ПЕРВЫЕ 1
	Календарь.Дата КАК РабочаяДата
ИЗ
	РегистрСведений.ДанныеПроизводственныхКалендарей КАК Календарь
ГДЕ
	Календарь.Дата > &ТекущаяДата И Календарь.ВидДня = &РабочийДень
УПОРЯДОЧИТЬ ПО
	Календарь.Дата

Стандарты разработки 1С и требования АПК

Фирма «1С» строго регламентирует написание запросов. Использование конструкции ПЕРВЫЕ без УПОРЯДОЧИТЬ ПО классифицируется как потенциальная ошибка (дефект кода).

Встроенная система статического анализа 1С и инструмент **АПК (Автоматизированная проверка конфигураций)** содержат правило проверки SelectTopWithoutOrderBy. Анализатор выдаст предупреждение во всех случаях, за исключением одного важного сценария.

Исключение из правил: Допускается не указывать сортировку при использовании ПЕРВЫЕ 1, если запрос используется исключительно для проверки существования записей. Например:


// Проверка, есть ли в справочнике хотя бы один элемент
Запрос = Новый Запрос;
Запрос.Текст = 
    "ВЫБРАТЬ ПЕРВЫЕ 1
    |	1
    |ИЗ
    |	Справочник.Номенклатура";

РезультатЗапроса = Запрос.Выполнить();
Если Не РезультатЗапроса.Пустой() Тогда
    // Записи существуют, логика программы не зависит от конкретного элемента
КонецЕсли;

В данном случае системе не важно, какая именно запись будет возвращена первой — важен сам факт наличия хотя бы одной строки в таблице.

Как правильно проектировать алгоритмы

Если перед вами стоит задача выбрать «единственный» или «актуальный» договор контрагента, никогда не надейтесь на то, что у контрагента всегда будет только один договор, даже если бизнес-пользователи «мамой клянутся», что это так. Жизненный цикл информационной системы доказывает: если бизнес-правило допускает лазейку, рано или поздно в системе появится второй договор (по ошибке пользователя или из-за обмена данными).

Для гарантированно стабильной работы алгоритма применяйте следующие правила:

  1. Всегда пишите предложение УПОРЯДОЧИТЬ ПО. Сортировка должна осуществляться по уникальному полю или комбинации полей (например, по дате создания, коду или по полю Ссылка).
  2. Добавляйте в сортировку поле Ссылка в качестве последнего уровня упорядочивания. Это гарантирует абсолютную детерминированность выборки, даже если другие поля сортировки (например, даты) совпадают у нескольких записей.

Проанализируем пример правильного получения договора контрагента:


Запрос = Новый Запрос;
Запрос.Текст = 
    "ВЫБРАТЬ ПЕРВЫЕ 1
    |	Договоры.Ссылка КАК Договор
    |ИЗ
    |	Справочник.ДоговорыКонтрагентов КАК Договоры
    |ГДЕ
    |	Договоры.Владелец = &Контрагент
    |	И НЕ Договоры.ПометкаУдаления
    |УПОРЯДОЧИТЬ ПО
    |	Договоры.ЭтоГруппаИерархическая КАК Описание, // Сначала элементы, не группы
    |	Договоры.ДатаОформления УБЫВ,                // Берем самый свежий по дате
    |	Договоры.Ссылка";                              // Финальная детерминация по GUID

Резюме

Использование ВЫБРАТЬ ПЕРВЫЕ 1 без явного указания УПОРЯДОЧИТЬ ПО — это архитектурная ошибка, создающая в коде «логическую бомбу». Она может не проявлять себя долгое время, но обязательно сработает при переносе базы на другой сервер, обновлении платформы, изменении индексов или обычном обслуживании СУБД. Пишите код осознанно, следуйте стандартам фирмы «1С» и всегда управляйте порядком выборки данных явно!

← На главную