Разработчики в системе «1С:Предприятие» регулярно сталкиваются с ситуациями, когда внешне логичный и правильный запрос внезапно возвращает пустой результат. Особенно часто подобные недоразумения возникают при работе с пакетными запросами, временными таблицами и конструкциями отбора. В рамках этой статьи мы подробно разберем одну очень распространенную синтаксическую ошибку, проанализируем, почему СУБД реагирует на нее именно таким образом, и рассмотрим стандарты проектирования производительных запросов, которые помогут избежать подобных проблем в будущем.
Рассмотрим ситуацию, с которой столкнулся автор темы. В его коде присутствовал следующий фрагмент во втором запросе пакета:
Выбрать
СправочникЗаказы.Ссылка
Из
Справочник.ЗаказыКлиентов КАК СправочникЗаказы
Где
СправочникЗаказы.Ссылка в ("Выбрать ВрТаб.Ссылка Из СписокНоменклатурыВЗаказе как ВрТаб")
Внимательно посмотрим на условие в круглых скобках после оператора в. Автор запроса обернул текст подзапроса в двойные кавычки: "Выбрать ВрТаб.Ссылка...". На первый взгляд, это может показаться безобидной опечаткой или техническим нюансом написания, однако для платформы «1С:Предприятие» и СУБД это в корне меняет всю логику выполнения запроса.
Выясним причину, почему СУБД не выдала ошибку синтаксиса при компиляции, а молча вернула пустой результат. В языке запросов 1С двойные кавычки используются исключительно для обозначения строковых литералов (констант). Когда платформа транслирует этот запрос на уровень SQL, она не воспринимает текст внутри кавычек как исполняемый код подзапроса. Вместо этого она генерирует SQL-инструкцию, в которой уникальный идентификатор ссылки (имеющий бинарный тип данных uniqueidentifier или binary в СУБД) сравнивается с обычной текстовой строкой: 'Выбрать ВрТаб.Ссылка Из СписокНоменклатурыВЗаказе как ВрТаб'.
Поскольку типы данных абсолютно несовместимы, СУБД при выполнении сравнения для каждой строки физической таблицы получает значение FALSE (Ложь). Ошибка несоответствия типов на уровне СУБД в данном случае не инициируется, так как технически СУБД может сравнивать разные типы данных на соответствие, результатом чего всегда будет пустая выборка. Достаточно было просто убрать двойные кавычки, чтобы запрос заработал, превратив строковую константу в полноценный исполняемый подзапрос.
Исправление синтаксической ошибки — это лишь первый шаг на пути к созданию качественного программного продукта. Проанализируем структуру исходного запроса с точки зрения производительности и стандартов разработки «1С». Обратим внимание на то, как именно автор получает итоговые данные. Сначала он выбирает заказы по определенным условиям из таблицы Справочник.ЗаказыКлиентов и помещает их во временную таблицу СписокНоменклатурыВЗаказе. Затем во втором запросе пакета он снова обращается к физической таблице Справочник.ЗаказыКлиентов и накладывает условие отбора по этой временной таблице.
Такой подход порождает серьезную проблему, известную как двойное чтение (Double Read) физических таблиц. Заставляя СУБД повторно сканировать таблицу справочника, мы тратим драгоценные ресурсы процессора и дисковой подсистемы. Временная таблица на первом шаге уже содержит готовые ссылки на нужные документы (или элементы справочника). Повторное обращение к физической таблице только ради получения тех же самых ссылок абсолютно бессмысленно и неэффективно.
Обсуждение на форуме затронуло важный архитектурный спор: что лучше использовать — вложенные запросы или временные таблицы? Для того чтобы дать объективный ответ, обратимся к официальным стандартам разработки фирмы «1С» и рекомендациям для подготовки к аттестации «1С:Эксперт по технологическим вопросам».
Использование конструкции В (ВЫБРАТЬ ...) (подзапрос в условии отбора ГДЕ) при росте базы данных неизбежно приводит к деградации производительности. Рассмотрим, почему это происходит:
IN. Вместо однократного выполнения подзапроса СУБД может начать выполнять его повторно для каждой строки внешней таблицы (сканирование типа correlated subquery), что увеличивает время выполнения запроса в геометрической прогрессии.Nested Loops вместо быстрого Hash Match).Фирма «1С» в стандартах разработки прямо запрещает использование вложенных запросов в соединениях, а также настойчиво рекомендует отказываться от сложных вложенных конструкций в условиях отбора. Вместо этого данные следует предварительно помещать во временные таблицы. И вот почему это работает быстрее:
Ссылка) позволяет СУБД выполнять поиск по индексу за доли миллисекунды.Разберем два правильных варианта решения задачи автора в зависимости от того, как планируется использовать полученные данные в дальнейшем.
Если нашей целью является просто получение списка уникальных заказов клиентов за период, удовлетворяющих условиям по состоянию, нам вообще не требуются ни временные таблицы, ни вложенные запросы. Достаточно использовать оператор ОБЪЕДИНИТЬ в рамках одного простого запроса. Обратите внимание, что мы используем именно ОБЪЕДИНИТЬ вместо ОБЪЕДИНИТЬ ВСЕ, так как первый автоматически удаляет дублирующиеся строки на уровне СУБД:
ВЫБРАТЬ
ЗаказыКлиентов.Ссылка КАК Ссылка
ИЗ
Справочник.ЗаказыКлиентов КАК ЗаказыКлиентов
ГДЕ
ЗаказыКлиентов.ДатаДобавления МЕЖДУ &ДатаНачала И &ДатаОкончания
И ЗаказыКлиентов.СостояниеЗаказа = ЗНАЧЕНИЕ(Перечисление.СостоянияЗаказаКлиента.Добавлен)
ОБЪЕДИНИТЬ
ВЫБРАТЬ
ЗаказыКлиентов.Ссылка
ИЗ
Справочник.ЗаказыКлиентов КАК ЗаказыКлиентов
ГДЕ
ЗаказыКлиентов.ДатаОтгрузки МЕЖДУ &ДатаНачала И &ДатаОкончания
И ЗаказыКлиентов.СостояниеЗаказа = ЗНАЧЕНИЕ(Перечисление.СостоянияЗаказаКлиента.Отгружен)
Этот запрос выполнится за один проход СУБД по таблице справочника, объединит результаты двух выборок и вернет уникальный список ссылок без лишней нагрузки на сервер.
Если полученный список заказов необходим для последующего соединения с другими таблицами (например, для получения остатков товаров по этим заказам или их спецификаций из табличных частей), то правильным архитектурным решением будет создание проиндексированной временной таблицы и ее последующее Внутреннее соединение с целевой таблицей. Рассмотрим по шагам, как это выглядит на практике:
// Шаг 1: Формируем временную таблицу уникальных заказов и индексируем ее
ВЫБРАТЬ РАЗЛИЧНЫЕ
ЗаказыКлиентов.Ссылка КАК Ссылка
ПОМЕСТИТЬ СписокЗаказов
ИЗ
Справочник.ЗаказыКлиентов КАК ЗаказыКлиентов
ГДЕ
ЗаказыКлиентов.ДатаДобавления МЕЖДУ &ДатаНачала И &ДатаОкончания
И ЗаказыКлиентов.СостояниеЗаказа = ЗНАЧЕНИЕ(Перечисление.СостоянияЗаказаКлиента.Добавлен)
ОБЪЕДИНИТЬ
ВЫБРАТЬ
ЗаказыКлиентов.Ссылка
ИЗ
Справочник.ЗаказыКлиентов КАК ЗаказыКлиентов
ГДЕ
ЗаказыКлиентов.ДатаОтгрузки МЕЖДУ &ДатаНачала И &ДатаОкончания
И ЗаказыКлиентов.СостояниеЗаказа = ЗНАЧЕНИЕ(Перечисление.СостоянияЗаказаКлиента.Отгружен)
ИНДЕКСИРОВАТЬ ПО
Ссылка
;
// Шаг 2: Выбираем детальные данные через ВНУТРЕННЕЕ СОЕДИНЕНИЕ с временной таблицей
ВЫБРАТЬ
СправочникЗаказы.Ссылка КАК Ссылка,
СправочникЗаказы.Номер КАК Номер,
СправочникЗаказы.Дата КАК Дата,
СправочникЗаказы.СостояниеЗаказа КАК СостояниеЗаказа
ИЗ
Справочник.ЗаказыКлиентов КАК СправочникЗаказы
ВНУТРЕННЕЕ СОЕДИНЕНИЕ СписокЗаказов КАК ВрТаб
ПО СправочникЗаказы.Ссылка = ВрТаб.Ссылка
Проанализируем преимущества данного подхода подробнее. Во-первых, мы использовали ключевое слово РАЗЛИЧНЫЕ в первом запросе объединения (хотя при использовании ОБЪЕДИНИТЬ дубли и так исключаются, явное указание помогает разработчикам лучше понимать структуру выборки). Во-вторых, мы применили конструкцию ИНДЕКСИРОВАТЬ ПО Ссылка. Это создает физический индекс для временной таблицы в СУБД. В-третьих, во втором запросе пакета вместо медленного отбора ГДЕ Ссылка В (...) мы связали таблицы через ВНУТРЕННЕЕ СОЕДИНЕНИЕ. Такое соединение по проиндексированному полю выполняется СУБД мгновенно, гарантируя максимальную стабильность и скорость работы вашей базы данных даже при миллионных объемах записей.