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