При написании запросов в платформе 1С:Предприятие разработчики нередко сталкиваются с необходимостью динамически ограничить количество возвращаемых записей с помощью конструкции ВЫБРАТЬ ПЕРВЫЕ. Попытка передать число строк через стандартный параметр запроса (например, ВЫБРАТЬ ПЕРВЫЕ &Кол) завершается синтаксической ошибкой: анализатор платформы сообщает, что ожидает целочисленную константу. Рассмотрим, почему возникает данная ошибка, как правильно динамически сформировать текст запроса и какими способами можно полностью уйти от неоптимальных запросов в цикле.
Синтаксический анализатор языка запросов 1С жестко регламентирует использование конструкции ВЫБРАТЬ ПЕРВЫЕ: непосредственно после ключевого слова ПЕРВЫЕ допускается исключительно целочисленный положительный литерал. Использование параметров вида &Параметр, выражений, встроенных функций или подзапросов здесь запрещено.
Кроме того, важно помнить фундаментальное правило работы с выборками: конструкция ВЫБРАТЬ ПЕРВЫЕ всегда должна сопровождаться явным указанием секции УПОРЯДОЧИТЬ ПО. Если порядок сортировки не задан, СУБД возвратит физически первые попавшиеся строки, что сделает выборку случайной и недетерминированной.
Разберем самый прямой путь, когда требуется запустить запрос с вычисленным в коде количеством строк. Поскольку подставить параметр через метод УстановитьПараметр() невозможно, выполним строковую замену в тексте запроса перед его выполнением.
Посмотрим на практическую реализацию с использованием функции СтрЗаменить:
ТекстЗапросаШаблон =
"ВЫБРАТЬ ПЕРВЫЕ [КоличествоСтрок]
| Номенклатура.Ссылка КАК Ссылка
|ИЗ
| Справочник.Номенклатура КАК Номенклатура
|УПОРЯДОЧИТЬ ПО
| Номенклатура.Наименование ВОЗР";
// Вычисляем требуемое количество строк
НужноеКоличество = 5;
// Формируем рабочий текст запроса
ТекстЗапроса = СтрЗаменить(ТекстЗапросаШаблон, "[КоличествоСтрок]", Формат(НужноеКоличество, "ЧГ=0"));
Запрос = Новый Запрос;
Запрос.Текст = ТекстЗапроса;
Результат = Запрос.Выполнить();
Обратите внимание на форматирование числа: при конвертации числа в строку критически важно использовать функцию Формат() с параметром ЧГ=0 (отключение группировки разрядов). Если этого не сделать, для чисел свыше 999 платформа добавит неразрывный пробел (например, 1 000), и СУБД вернет ошибку синтаксиса при разборе числа.
Часто задача динамического ограничения строк возникает при обходе дат или элементов в цикле: например, на каждую дату месяца требуется получить срез регистра и отобрать фиксированное или переменное число записей. Вызов запроса внутри цикла встроенного языка является грубой ошибкой проектирования по следующим причинам:
Проанализируем альтернативные способы решения исходной бизнес-задачи без циклов.
Выясним, как получить срез последних на каждую дату и ограничить строки средствами одного запроса. Поскольку в языке запросов 1С отсутствуют стандартные аналитические оконные функции (такие как ROW_NUMBER() OVER (...)), нумерация строк внутри групп эмулируется через нестрогое самосоединение.
Разберем алгоритм по шагам:
Регистр.Период <= ТаблицаДат.Дата (виртуальная таблица СрезПоследних не может принимать динамический период из другой таблицы).ВТ_Данные.ВТ_Данные саму с собой по условию совпадения группы (даты) и нестрогому неравенству ключа сортировки (Таблица1.Ключ >= Таблица2.Ключ).КОЛИЧЕСТВО(РАЗЛИЧНЫЕ Таблица2.Ключ) КАК НомерСтроки.ГДЕ НомерСтроки <= ТаблицаОграничений.Лимит.Важное предостережение: нестрогое соединение имеет квадратичную сложность $O(N^2)$. Если в разрезе одной даты находятся тысячи записей, время выполнения запроса резко возрастет. Чтобы оптимизировать выполнение, обязательно создавайте индексы для временных таблиц с помощью директивы ИНДЕКСИРОВАТЬ ПО по полям группировки и полям сортировки.
Если объем данных велик, эмуляция оконных функций в запросе может работать слишком медленно. Наиболее производительным и стабильным решением является гибридный подход: получение данных одним запросом с последующим отсечением лишних строк на встроенном языке.
Рассмотрим реализацию гибридного метода:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Лимиты.Период КАК Период,
| Лимиты.Количество КАК МаксСтрок
|ПОМЕСТИТЬ ВТ_Лимиты
|ИЗ
| &ТаблицаЛимитов КАК Лимиты
|;
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| Данные.Период КАК Период,
| Данные.Номенклатура КАК Номенклатура,
| Данные.Значение КАК Значение,
| ЕСТЬNULL(ВТ_Лимиты.МаксСтрок, 0) КАК МаксСтрок
|ИЗ
| РегистрСведений.Показатели КАК Данные
| ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_Лимиты КАК ВТ_Лимиты
| ПО Данные.Период = ВТ_Лимиты.Период
|ИТОГИ ПО
| Период";
Запрос.УстановитьПараметр("ТаблицаЛимитов", ТаблицаЛимитов);
РезультатЗапроса = Запрос.Выполнить();
// Обход с иерархией по группировке "Период"
ВыборкаПериод = РезультатЗапроса.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам);
Пока ВыборкаПериод.Следующий() Цикл
ЛимитСтрок = ВыборкаПериод.МаксСтрок;
Счетчик = 0;
ВыборкаДетали = ВыборкаПериод.Выбрать();
Пока ВыборкаДетали.Следующий() Цикл
Если Счетчик >= ЛимитСтрок Тогда
Прервать; // Лимит по текущей дате исчерпан
КонецЕсли;
// Обработка отобранной строки
Счетчик = Счетчик + 1;
КонецЦикла;
КонецЦикла;
Посмотрим на преимущества этого решения: данные считываются с сервера базы данных за одно обращение, не перегружают СУБД сложными самосоединениями $O(N^2)$ и исключают выполнение сотен мелких запросов в цикле.