Каждый разработчик 1С рано или поздно сталкивается с задачей, когда нужно получить всего одну запись из таблицы базы данных. На первый взгляд, конструкция ВЫБРАТЬ ПЕРВЫЕ 1 кажется идеальным решением. Но задумывались ли вы, гарантирует ли СУБД, что при каждом выполнении такого запроса будет возвращаться один и тот же элемент справочника или документа, если в запросе отсутствует предложение УПОРЯДОЧИТЬ ПО? В этой статье мы подробно разберем внутренние механизмы СУБД (MS SQL Server и PostgreSQL), особенности файловой базы данных 1С, стандарты разработки фирмы «1С» и выясним, почему отсутствие явной сортировки может привести к критическим сбоям в работе системы.
Начнем с главного правила реляционных баз данных: физический порядок хранения строк в таблице не гарантирует логический порядок их выборки. Если в запросе не указано предложение УПОРЯДОЧИТЬ ПО (в SQL-эквиваленте — ORDER BY), СУБД возвращает строки в том порядке, в котором ей в данный момент удобнее и быстрее всего их прочитать.
Рассмотрим пример запроса, который часто пишут начинающие разработчики, полагая, что справочник всегда вернет «первый» созданный элемент:
ВЫБРАТЬ ПЕРВЫЕ 1
Ссылка КАК Договор
ИЗ
Справочник.ДоговорыКонтрагентов
ГДЕ
Владелец = &Контрагент
В тестовой базе или на небольшом объеме данных этот запрос может месяцами возвращать один и тот же договор. Однако это поведение абсолютно случайно и может измениться в любой момент под воздействием различных внешних факторов.
Для понимания физики процесса разберем, как MS SQL Server обрабатывает запрос TOP 1 (транслируемый платформой 1С из ПЕРВЫЕ 1):
Ссылка (уникальный идентификатор GUID) в таблицах 1С является кластерным ключом. Особенность архитектуры SQL Server заключается в том, что кластерный ключ неявно присутствует во всех некластерных индексах таблицы в качестве указателя на физическую строку. Если у справочника договоров есть некластерный индекс (например, по реквизиту «Код» или «Наименование»), оптимизатор SQL Server предпочтет сканировать именно его. Поскольку этот индекс физически занимает меньше страниц памяти (он «уже»), СУБД прочитает первую страницу некластерного индекса. В итоге возвращенное значение будет определяться сортировкой этого индекса (например, по алфавиту наименований), а вовсе не порядком создания записи.Gather Streams), та строка и станет результатом TOP 1. Под разной вычислительной нагрузкой этот результат будет непредсказуемо меняться.Официальная документация MSDN прямо заявляет: «Если вы используете TOP без предложения ORDER BY, то TOP возвращает строки в неопределенном порядке». Таким образом, полагаться на «порядок по умолчанию» в клиент-серверной архитектуре — грубейшая ошибка.
Если ваша база данных 1С работает под управлением PostgreSQL, ситуация становится еще более непредсказуемой из-за механизма синхронизированного сканирования (Synchronized Sequential Scans):
Когда одна транзакция уже выполняет последовательное чтение (Seq Scan) большой таблицы (например, строится тяжелый отчет), а другой сеанс запускает свой запрос LIMIT 1 (аналог ПЕРВЫЕ 1) по этой же таблице, PostgreSQL «пристыковывает» второй запрос к первому для экономии дискового ввода-вывода. Чтение для второго запроса начнется не с начала таблицы, а с той страницы, которую первый запрос читает прямо сейчас. Как только нужная запись найдена, запрос завершается. В результате вы получите в качестве «первой» случайную строку из середины или конца таблицы.
Бытует мнение, что в файловой базе данных порядок строк всегда фиксирован. Действительно, в файловой СУБД данные хранятся последовательно на страницах размером 4 Кб. Но и здесь стабильность выборки — это иллюзия:
.dt файлы таблиц создаются заново. Все пустые слоты очищаются, данные записываются последовательно, и физический порядок строк на диске полностью меняется. После этой процедуры алгоритмы, завязанные на ПЕРВЫЕ 1 без сортировки, гарантированно ломаются.Рассмотрим жизненную ситуацию. Разработчик написал запрос к регистру сведений ДанныеПроизводственныхКалендарей для получения ближайшего рабочего дня. Запрос успешно работал больше года:
ВЫБРАТЬ ПЕРВЫЕ 1
Календарь.Дата КАК РабочаяДата
ИЗ
РегистрСведений.ДанныеПроизводственныхКалендарей КАК Календарь
ГДЕ
Календарь.Дата > &ТекущаяДата И Календарь.ВидДня = &РабочийДень
После проведения планового обслуживания базы данных (выгрузки-загрузки через .dt и обновления статистики СУБД) отчеты, использующие этот запрос, стали выдавать некорректные даты. Выяснилось, что порядок строк в выборке изменился. Проблема решилась мгновенно после добавления явного упорядочивания:
ВЫБРАТЬ ПЕРВЫЕ 1
Календарь.Дата КАК РабочаяДата
ИЗ
РегистрСведений.ДанныеПроизводственныхКалендарей КАК Календарь
ГДЕ
Календарь.Дата > &ТекущаяДата И Календарь.ВидДня = &РабочийДень
УПОРЯДОЧИТЬ ПО
Календарь.Дата
Фирма «1С» строго регламентирует написание запросов. Использование конструкции ПЕРВЫЕ без УПОРЯДОЧИТЬ ПО классифицируется как потенциальная ошибка (дефект кода).
Встроенная система статического анализа 1С и инструмент **АПК (Автоматизированная проверка конфигураций)** содержат правило проверки SelectTopWithoutOrderBy. Анализатор выдаст предупреждение во всех случаях, за исключением одного важного сценария.
Исключение из правил: Допускается не указывать сортировку при использовании ПЕРВЫЕ 1, если запрос используется исключительно для проверки существования записей. Например:
// Проверка, есть ли в справочнике хотя бы один элемент
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ ПЕРВЫЕ 1
| 1
|ИЗ
| Справочник.Номенклатура";
РезультатЗапроса = Запрос.Выполнить();
Если Не РезультатЗапроса.Пустой() Тогда
// Записи существуют, логика программы не зависит от конкретного элемента
КонецЕсли;
В данном случае системе не важно, какая именно запись будет возвращена первой — важен сам факт наличия хотя бы одной строки в таблице.
Если перед вами стоит задача выбрать «единственный» или «актуальный» договор контрагента, никогда не надейтесь на то, что у контрагента всегда будет только один договор, даже если бизнес-пользователи «мамой клянутся», что это так. Жизненный цикл информационной системы доказывает: если бизнес-правило допускает лазейку, рано или поздно в системе появится второй договор (по ошибке пользователя или из-за обмена данными).
Для гарантированно стабильной работы алгоритма применяйте следующие правила:
УПОРЯДОЧИТЬ ПО. Сортировка должна осуществляться по уникальному полю или комбинации полей (например, по дате создания, коду или по полю Ссылка).Ссылка в качестве последнего уровня упорядочивания. Это гарантирует абсолютную детерминированность выборки, даже если другие поля сортировки (например, даты) совпадают у нескольких записей.Проанализируем пример правильного получения договора контрагента:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ ПЕРВЫЕ 1
| Договоры.Ссылка КАК Договор
|ИЗ
| Справочник.ДоговорыКонтрагентов КАК Договоры
|ГДЕ
| Договоры.Владелец = &Контрагент
| И НЕ Договоры.ПометкаУдаления
|УПОРЯДОЧИТЬ ПО
| Договоры.ЭтоГруппаИерархическая КАК Описание, // Сначала элементы, не группы
| Договоры.ДатаОформления УБЫВ, // Берем самый свежий по дате
| Договоры.Ссылка"; // Финальная детерминация по GUID
Использование ВЫБРАТЬ ПЕРВЫЕ 1 без явного указания УПОРЯДОЧИТЬ ПО — это архитектурная ошибка, создающая в коде «логическую бомбу». Она может не проявлять себя долгое время, но обязательно сработает при переносе базы на другой сервер, обновлении платформы, изменении индексов или обычном обслуживании СУБД. Пишите код осознанно, следуйте стандартам фирмы «1С» и всегда управляйте порядком выборки данных явно!