При написании запросов в 1С часто возникает вопрос о последовательности выполнения его частей. Особенно это касается конструкций ВЫБРАТЬ ПЕРВЫЕ и условия ГДЕ. Что отработает сначала: фильтрация записей или отбор первых N строк? От правильного понимания этого механизма зависит не только производительность, но и корректность получаемых данных. Рассмотрим эту тему подробно, а также разберем нюансы поведения запросов в файловом и клиент-серверном режимах.
Чтобы понять, что происходит "под капотом", важно знать логическую последовательность, по которой система обрабатывает запрос. Эта последовательность близка к стандарту SQL и выглядит следующим образом:
ГДЕ, отбрасываются. Это ключевой момент: фильтрация происходит до отбора первых записей.Таким образом, на первоначальный вопрос автора "что сначала отработает: Выбрать Первые или Где?" ответ однозначен: сначала ГДЕ, потом ПЕРВЫЕ. Система сначала отберет все записи, подходящие под условие, а уже затем из них возьмет первые несколько.
Из описанной выше последовательности виден еще один важный аспект: ПЕРВЫЕ применяется к отсортированному набору данных. Если вы не укажете секцию УПОРЯДОЧИТЬ ПО, порядок записей в результате не гарантирован. СУБД вернет вам "какие-то" первые N записей, и этот результат может меняться от запуска к запуску.
Использование ВЫБРАТЬ ПЕРВЫЕ без УПОРЯДОЧИТЬ ПО — это почти всегда ошибка в логике, если вам важен конкретный результат (например, самые новые документы или товары с наибольшей ценой).
Посмотрим на пример. Задача: получить последний по номеру документ реализации за определенную дату.
Правильный запрос:
Запрос.Текст =
"ВЫБРАТЬ ПЕРВЫЕ 1
| РеализацияТоваровУслуг.Ссылка
|ИЗ
| Документ.РеализацияТоваровУслуг КАК РеализацияТоваровУслуг
|ГДЕ
| РеализацияТоваровУслуг.Дата МЕЖДУ &НачалоДня И &КонецДня
| И РеализацияТоваровУслуг.Проведен
|
|УПОРЯДОЧИТЬ ПО
| РеализацияТоваровУслуг.Номер УБЫВ";
Здесь мы сначала отбираем все проведенные документы за день (ГДЕ), затем сортируем их по номеру в порядке убывания (УПОРЯДОЧИТЬ ПО ... УБЫВ), и только после этого берем самую первую запись из отсортированного списка (ПЕРВЫЕ 1), которая и будет документом с самым большим номером.
Самые интересные нюансы начинаются, когда мы переходим к более сложным запросам, например, с объединениями, и начинаем работать в клиент-серверном режиме (например, на MS SQL Server). В обсуждении на форуме был поднят именно такой случай, который приводил к неожиданным результатам.
Рассмотрим проблемный запрос из обсуждения:
ВЫБРАТЬ ПЕРВЫЕ 1
РеализацияТоваровУслуг.Номер КАК Номер
ИЗ
Документ.РеализацияТоваровУслуг КАК РеализацияТоваровУслуг
ГДЕ
РеализацияТоваровУслуг.Дата >= НАЧАЛОПЕРИОДА(&ДатаДок, ДЕНЬ)
И РеализацияТоваровУслуг.Дата <= КОНЕЦПЕРИОДА(&ДатаДок, ДЕНЬ)
ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ ПЕРВЫЕ 1
ПередачаТоваровМеждуОрганизациями.Номер
ИЗ
Документ.ПередачаТоваровМеждуОрганизациями КАК ПередачаТоваровМеждуОрганизациями
ГДЕ
ПередачаТоваровМеждуОрганизациями.Дата >= НАЧАЛОПЕРИОДА(&ДатаДок, ДЕНЬ)
И ПередачаТоваровМеждуОрганизациями.Дата <= КОНЕЦПЕРИОДА(&ДатаДок, ДЕНЬ)
УПОРЯДОЧИТЬ ПО
Номер УБЫВ
Ожидание: запрос выберет по одному документу каждого вида, объединит их, отсортирует по убыванию номера и вернет один с наибольшим номером.
Реальность:
УПОРЯДОЧИТЬ ПО применяется ко всему результату ПОСЛЕ выполнения ОБЪЕДИНИТЬ. А операторы ПЕРВЫЕ 1 внутри каждого подзапроса выбирают по одной случайной (неотсортированной) записи. В итоге общая сортировка применяется к двум случайным номерам, что не решает исходную задачу.Вывод: поведение файловой базы может быть "удобным", но оно не соответствует стандарту SQL. Всегда пишите запросы так, чтобы они корректно работали в более строгой среде клиент-серверного варианта. Нельзя полагаться на сортировку внутри частей запроса до объединения.
В первом сообщении автор предложил два варианта решения своей задачи.
Вариант 1: Простой запрос
ТекстЗапроса = "ВЫБРАТЬ ПЕРВЫЕ " + КоличествоВыборки + "
| ВыборкаЭлементов.Ссылка КАК Ссылка
|ИЗ
| " + ИмяТаблицы + " КАК ВыборкаЭлементов
|ГДЕ
| НЕ ВыборкаЭлементов.ПометкаУдаления";
Этот вариант подходит для простых случаев. Он компактен и понятен. Но для получения предсказуемого результата ему не хватает УПОРЯДОЧИТЬ ПО.
Вариант 2: С использованием временной таблицы
ТекстЗапроса = "ВЫБРАТЬ
| ВыборкаЭлементов.Ссылка КАК Ссылка
|ПОМЕСТИТЬ ВТ_Элементы
|ИЗ
| " + ИмяТаблицы + " КАК ВыборкаЭлементов
|ГДЕ
| НЕ ВыборкаЭлементов.ПометкаУдаления
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ ПЕРВЫЕ " + КоличествоВыборки + "
| ВТ.Ссылка КАК Ссылка
|ИЗ
| ВТ_Элементы КАК ВТ
|УПОРЯДОЧИТЬ ПО
| ВТ.Ссылка.Наименование"; // Добавим сортировку для предсказуемости
Этот подход является более универсальным, надежным и часто более читаемым для сложных задач:
Рекомендация: для простых выборок можно использовать первый вариант (не забывая про сортировку). Для сложных, многоэтапных запросов или для обеспечения кроссплатформенности лучше сразу использовать временные таблицы.
В обсуждении было сделано очень важное замечание по стилю кода. Формирование текста запроса путем конкатенации (сложения) строк — плохая практика.
Плохой пример:
ТекстЗапроса = "ВЫБРАТЬ ПЕРВЫЕ " + КоличествоВыборки + " | ... | ИЗ | "+ВидОбъекта+"."+ИмяМетаданных;
Такой код сложно читать, отлаживать (его нельзя просто скопировать в консоль запросов), и он потенциально небезопасен. Вместо этого используйте параметры и шаблоны.
Хороший пример:
// Текст запроса лучше хранить как шаблон
// Имя таблицы можно подставить через СтрЗаменить, а количество - через параметр
ТекстЗапроса = "
|ВЫБРАТЬ ПЕРВЫЕ &КоличествоВыборки
| ВыборкаЭлементов.Ссылка
|ИЗ
| #ИмяТаблицы КАК ВыборкаЭлементов
|ГДЕ
| НЕ ВыборкаЭлементов.ПометкаУдаления
|УПОРЯДОЧИТЬ ПО
| ВыборкаЭлементов.Наименование";
ТекстЗапроса = СтрЗаменить(ТекстЗапроса, "#ИмяТаблицы", "Справочник.Номенклатура");
Запрос = Новый Запрос;
Запрос.Текст = ТекстЗапроса;
Запрос.УстановитьПараметр("КоличествоВыборки", 10);
Результат = Запрос.Выполнить();
Этот подход делает код чистым, понятным и удобным для поддержки и отладки.