• 1с запрос поместить временную таблицу. Чрезмерное разрастание базы TempDB

    Участников наших курсов часто интересует внутренняя логика работы временных таблиц.

    Какие критерии индексирования временных таблиц? Где они хранятся? Нужно ли в явном виде их удалять?

    Ответы на эти вопросы мы рассмотрим в данной статье.

    Также мы приведем случаи, когда не нужно использовать временные таблицы .

    Где хранятся временные таблицы?

    Временные таблицы - это объекты СУБД, никаких временных таблиц на сервере 1С нет, и не путайте их с таблицами значений.

    Под вопросом: «Где хранятся временные таблицы?» – имеется ввиду физическое расположение, т.е. либо жесткий диск, либо оперативная память.

    Вероятно, этот вопрос чаще других остается без ответа, либо ответы на него кардинально различаются.

    Но все сходятся в том, что временные таблицы создаются и хранятся в базе TempDB.

    Действительно, все временные таблицы относятся к базе данных TempDB , но это вовсе не значит, что они обязательно будут записываться на диск.

    Правильный ответ на этот вопрос звучит так: все временные таблицы по умолчанию создаются в оперативной памяти, а именно – в буферном кэше.

    Конечно, есть исключения. Например, если таблица слишком большая, то сервер может принять решение сбросить ее на диск. Также возможна ситуация, когда сервер по каким-либо причинам решил отдать память под другие данные, тогда таблица тоже будет записана на диск.

    Почему таблица создается именно в памяти? Тут все очевидно – дело в производительности, думаю, не стоит объяснять, что чтение из оперативной памяти гораздо быстрее чтения с диска, даже если этот диск SSD.

    Проведем эксперимент и проверим, где создается временная таблица.

    Пишем следующий запрос в консоли:

    ВЫБРАТЬ 1 КАК Поле1 ПОМЕСТИТЬ ВТ

    Запускаем трассировку SQL Profiler с событием SQL:BatchComplited, выполняем запрос в консоли и получаем следующий текст SQL запроса:

    INSERT INTO #tt1 (_Q_001_F_000) SELECT 1.0

    Здесь мы видим только заполнение временной таблицы, т.к. код создания временных таблиц в нашей трассировке не отображается.

    Чтобы понять, где создается временная таблица, необходимо понять, откуда читаются данные – с диска или из памяти. Для этого используем показатель physical reads (количество физических чтений), т.е. сколько 8Кб страниц данных было прочитано с диска для выполнения запроса.

    Чтобы получить значение этого показателя, необходимо выполнить создание и чтение временной таблицы в Management Studio .

    Создаем новый запрос и пишем следующее:

    Create table #tt1 (_Q_001_F_000 int); -- создаем локальную временную таблицу tt1 INSERT INTO #tt1 (_Q_001_F_000) SELECT 1.0 – заполняем таблицу set statistics io on; -- включаем вывод статистики ввода/вывода select * from #tt1 -- читаем данные из таблицы set statistics io off; -- выключаем вывод статистики drop table #tt1 -- удаляем таблицу

    После выполнения данного кода на закладке «Сообщения» получим следующий текст:

    (строк обработано: 1) Таблица «#tt1_____________________________________________000000000066″. Число просмотров 1, логических чтений 1, физических чтений 0 , упреждающих чтений 0, lob логических чтений 0, lob физических чтений 0, lob упреждающих чтений 0.

    Самое важное здесь то, что данные с диска не читались, т.к. число физических чтений равно 0, при этом есть 1 логическое чтение, т.е. данные были прочитаны только из памяти.

    Отсюда можно сделать вывод, что временные таблицы в большинстве случаев создаются и хранятся в оперативной памяти, исключения уже описаны выше.

    Надо ли индексировать временные таблицы?

    На дисках ИТС, на экзамене 1С: Эксперт, да и я на своих курсах говорю, что нужно индексировать поля условий и соединений во временных таблицах.

    Но как показывает практика, чаще всего индексы во временных таблицах не используются, либо используются, но запрос выполняется еще медленнее, чем без них. Скажу даже больше: для себя я сделал вывод, что польза от индексов на временных таблицах – скорее исключение, чем правило.

    Это не значит, что индексы не нужно использовать, это значит, что необходимо анализировать каждый конкретный случай, и случаев, когда индекс не нужен, значительно больше.

    Именно поэтому на курсах по оптимизации постоянно делается акцент на то, что с каждой проблемой надо разбираться отдельно, а не слепо выполнять рекомендации, не понимая, почему эти рекомендации именно такие.

    Давайте рассмотрим ситуацию с индексацией на примере.

    Создадим временную таблицу с одним числовым полем и значениями от 1 до 1 млн.

    Это можно сделать с помощью следующего пакетного запроса:

    ВЫБРАТЬ 0 КАК Цифра ПОМЕСТИТЬ ВТ_Цифры ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ 1 ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ 2 ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ 3 ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ 4 ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ 5 ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ 6 ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ 7 ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ 8 ОБЪЕДИНИТЬ ВСЕ ВЫБРАТЬ 9 ; ////////////////////////////////////////////////////////////////////////// ВЫБРАТЬ 100000 * Таб6.Цифра + 10000 * Таб5.Цифра + 1000 * Таб4.Цифра + 100 * Таб3.Цифра + 10 * Таб2.Цифра + Таб1.Цифра + 1 КАК Число ПОМЕСТИТЬ ВТ_Числа ИЗ ВТ_Цифры КАК Таб1, ВТ_Цифры КАК Таб2, ВТ_Цифры КАК Таб3, ВТ_Цифры КАК Таб4, ВТ_Цифры КАК Таб5, ВТ_Цифры КАК Таб6 ; ///////////////////////////////////////////////////////////// ВЫБРАТЬ ВТ_Числа.Число ИЗ ВТ_Числа КАК ВТ_Числа ГДЕ ВТ_Числа.Число = 777

    Весь запрос выполняется в среднем за 1.2 секунды.

    Если посмотреть трассировку SQL Profiler , то мы увидим следующее:

    На создание таблицы уходит 1.1 секунда и еще 0.1 секунда на сканирование всей таблицы, чтобы вернуть нам 1 строку.

    Давайте посмотрим, что изменится, если добавить индекс в таблицу ВТ_Числа .

    На моем компьютере запрос стал выполняться в среднем за 6 секунд.

    Время создания таблицы увеличилось с 1 секунды до 5.3, при этом даже поиск по индексу в таблице все равно происходит медленнее, чем сканирование: 0.5 сек. против 0.1 без индекса. Единственное, в чем этот запрос выигрывает, - немного меньше логических чтений: 2057 против 2233 при сканировании.

    Используйте индексирование только в том случае, если вы видите от этого явный положительный эффект.

    Надо ли явно удалять временные таблицы после создания?

    Ответ будет зависеть от способа создания временной таблицы.

    Если Вы используете временную таблицу только в одном пакетном запросе, то ваша таблица «живет», пока выполняется этот пакетный запрос. Это значит, что менеджер временных таблиц был создан неявно.

    В данном случае MS SQL создает локальную временную таблицу с одной решеткой (#), например #tt1 .

    Как только пакетный запрос завершается, неявный МВТ закрывается, и автоматически последует команда «Truncate table» , которая удаляет созданную таблицу.

    Если временная таблица проиндексирована, то сначала будет удален индекс и только потом таблица.

    Пример можно посмотреть выше в трассировке.

    В данном случае нет необходимости использовать команду «УНИЧТОЖИТЬ» , только если Вы не хотите создать в том же запросе новую таблицу с таким же именем, ну или считаете это хорошим стилем написания кода.

    Здесь главное понимать, что таблица все равно будет удалена при завершении пакетного запроса.

    Ситуация меняется, если Вы явно используете менеджер временных таблиц (МВТ) , т.е. создаете соответствующий объект метаданных. В этом случае MS SQL создает глобальную временную таблицу с двумя решетками (##).

    Такая таблица будет удалена в любом из следующих вариантов:

    1. в запросе использована команда УНИЧТОЖИТЬ
    2. вызван метод МенеджерВременныхТаблиц.Закрыть()
    3. объект МенеджерВременныхТаблиц перестал существовать, например, завершилась работа процедуры/функции, которая породила этот объект, или пользователь закрыл программу

    Если Вы используете объект МВТ, то временные таблицы рекомендуется удалять одним из первых 2 методов, как только в них отпала необходимость, иначе они будут висеть в памяти сервера СУБД, пока процедура/функция не закончит работу, что не есть хорошо. Если же у Вас процедура, в которой был создан МВТ завершается как раз выполнением запроса, тогда, конечно, МВТ можно не удалять, т.к. сработает 3 условие.

    Подведем итог: если Вы не используете МВТ, то явно удалять временную таблицу не требуется, она будет удалена после завершения пакетного запроса.

    Если явное объявление МВТ используется, то рекомендуется удалить таблицу вручную, например, в запросе командой «Уничтожить» , либо методом МВТ.Закрыть() .

    Минусы временных таблиц

    Идеальных инструментов не бывает, тем более в мире 1С.

    Давайте рассмотрим, какие проблемы может принести активное использование временных таблиц.

    Чрезмерное разрастание базы TempDB

    Если Вы активно используете временные таблицы, то у Вас может довольно сильно разрастаться база TempDB и в один прекрасный день может занять все свободное место на диске. Размер TempDB автоматически только увеличивается, но не уменьшается. Внутри файла место может как заниматься, так и освобождаться, но сам размер файла только увеличивается.

    Типичная ситуация, установили обновление, и через несколько дней TempDB раздуло до неимоверных размеров, а потом выясняется, что разработчики переписали запросы с использованием временных таблиц, причем таблицы эти внушительного размера и их много.

    Для исправления ситуации необходимо выполнить следующие команды:

    Dbcc shrinkfile (tempdev, ЖелаемыйРазмерФайлаДанныхМб) dbcc shrinkfile (templog, ЖелаемыйРазмерФайлаЛоговМб)

    Чрезмерное упрощение запросов

    Нельзя сказать, что это очень большой минус, но все же.

    Часто временные таблицы используются как раз для упрощения сложных запросов, чтобы серверу было легче подобрать оптимальный план, и это правильно.

    Но можно встретить и другую крайность: запрос, который вполне можно было бы написать без временных таблиц, и он бы работал быстро и оптимально, все равно пишут через временные таблицы. Просто разработчику лень думать, как это можно сделать по-другому.

    Если можно написать оптимальный запрос без использования временных таблиц, то лучше обойтись без них.

    Запрос с оптимальным кодом без временных таблиц в любом случае будет работать быстрее и использовать меньше ресурсов, чем запрос с временными таблицами.

    Заключение

    После прочтения статьи у Вас может сложиться мнение, что я не советую использовать временные таблицы, но это не так. Напротив, я активно их использую для оптимизации запросов, но делаю это только после того, как удостоверюсь в их эффективности в данном конкретном случае.

    Временные таблицы - это инструмент для решения определенных задач, а не волшебная таблетка на все случаи жизни.

    Бурмистров Андрей

    Механизм запросов, возникнув в 7 версии программы 1С, с выходом первых версий 8 платформы получил все более широкое распространение и постоянно растущую популярность. Появление управляемых форм и системы компоновки данных существенно увеличило область применения этого инструмента. Однако многим начинающим программистам достаточно сложно его освоить.

    Использование объекта, который носит название «Менеджер временных таблиц» позволяет:

    • Значительно упростить текст запроса;
    • Разбить его на более простые блоки;
    • Увеличить его читаемость и структурированность.

    Несколько слов о том, как это работает

    В принципе, в работе менеджера временных таблиц можно выделить четыре этапа использования:

    1. Создание менеджера;
    2. Его заполнение;
    3. Чтение данных из таблиц;
    4. Уничтожение менеджера и очистка таблиц.

    Давайте поговорим о каждом этапе поподробнее.

    Создание менеджера временных таблиц

    Для того, чтобы определить этот объект необходимо выполнить код, показанный на Рис.1

    Здесь следует обратить внимание, что определение менеджера временных таблиц дается перед оператором Выполнить(), в противном случае выполнение кода гарантированно прервется ошибкой, информационное окно которой указанно на Рис.2.

    Рис.2

    Заполнение менеджера

    На том же рисунке 1 указана строка, которая передает выборку во временную таблицу. Начинается она с оператора «Поместить». В качестве места назначения указывается имя таблицы-приемника.

    Используя «Конструктор запроса» эту строку можно создать на закладке «Дополнительно» Рис.3.

    Рис.3

    Для этого необходимо:

    1. Установить переключатель «Тип запроса» в положение «Создание временной таблицы»;
    2. Указать имя приемника.

    Если с помощью отладчика проверить последовательность заполнения менеджера, можно обнаружить, что данные в нем появятся только после исполнения метода запроса Выполнить().

    Определить заполненость менеджера можно с помощью оператора Количество(). Пример строки: МВТ.Таблицы.Количество().

    Чтение таблиц менеджера

    Следующий этап работы – чтение данных из существующих таблиц в другом запросе. Здесь существует одна проблема: новый создаваемый запрос не знает о существовании заполненной таблицы, поэтому в окне «База данных» консоли она не появится.

    Её необходимо прописывать и создавать вручную.

    Для этого на закладке «Таблицы и поля» (Рис.4) необходимо выполнить некоторую последовательность действий:

    Рис.4

    1. В меню окна «Таблицы» этой закладки необходимо нажать кнопку «Создать описание временной таблицы»;
    2. В открывшемся окне необходимо указать наименование поля и его описание (тип поля), как оно задано в существующей таблице;
    3. Необходимые поля или функции с ними нужно перенести в третье окно закладки.

    Все время выполнения временные таблицы хранятся в оперативной памяти компьютера пользователя при файловом варианте работе или засоряют память сервера в клиент-серверном режиме до полного завершения работы функции или процедуры, вызвавшей менеджер. Большие объемы данных могут значительно снижать производительность даже не самого слабого железа.

    Удаление таблиц

    В общем случае опытные специалисты, для того, чтобы не сильно засорять память рекомендуют удалять временные таблицы сразу после их использования, если они больше нигде не будут вызываться.

    Удаление таблиц из менеджера можно производить двумя основными способами:

    • Указав непосредственно в тексте запроса ключевое слово Уничтожить;
    • Использовав метод Закрыть(), примененный непосредственно к менеджеру.

    Во втором случае будут принудительно уничтожены все таблицы, созданные различными запросами.

    Использование первого варианта удаления данных можно прописать явно, записав в тексте запроса строку вида «Уничтожить ИмяТаблицы», либо воспользовавшись закладкой «Дополнительно» (Рис.2) окна «Конструктор запроса».

    Переключив переключатель в положение «Уничтожение» и указав имя объекта, который необходимо удалить, Вы без проблем выполните это действие.

    Передача (ТЗ) в запрос

    Одно из наиболее частых направлений использования этого менеджера – передача в качестве одного из источников данных в запрос таблицы значений. Как известно, непосредственное использование этой коллекции в запросе в 1С не предусмотрено.

    Для этого текст запроса должен содержать строку вида «Выбрать * Поместить МВТ из &Тз Как Таб» . Передав в качестве параметра «ТЗ» для запроса существующую таблицу значений мы получим объект, подходящий для дальнейшей обработки в других запросах.

    Единственным условием, препятствующим передаче ТЗ в качестве параметра, являются неявно объявленные типы ее колонок. То есть при создании колонок ТЗ необходимо заполнять второй параметр строкой вида «Новый ОписаниеТипов(«»)).

    43
    NULL – отсутствующие значения. Не путать с нулевым значением! NULL – это не число, не равно пробелу, пустой ссылке, Неопределено. NULL – типообразующее значение, т.е. есть тип NULL и единственное значение этого типа. NULL... 26
    Для формирования и выполнения запросов к таблицам базы данных в платформе 1С используется специальный объект языка программирования Запрос. Создается этот объект вызовом конструкции Новый Запрос. Запрос удобно... 18
    В статье приведены полезные приемы при работе с запросами 1С v.8.2, а также сведения, которые не так хорошо известны о языке запросов. Я не стремлюсь дать полное описание языка запросов, а хочу остановиться лишь на... 13
    ПОДОБНО - Оператор проверки строки на подобие шаблону. Аналог LIKE в SQL. Оператор ПОДОБНО позволяет сравнить значение выражения, указанного слева от него, со строкой шаблона, указанной справа. Значение выражения...

    При построении запросов, в языке 1с есть возможность получения результата выполнения запроса с помощью команды: " ПОМЕСТИТЬ <ИмяВременнойТаблицы> ", где <ИмяВременнойТаблицы> - имя "временной таблицы", которая является свойством объекта "Запрос" и представляет из себя объект типа МенеджерВременныхТаблиц, который описывает пространство имен временных таблиц и отвечает за их создание и уничтожение в базе данных. Данный механизм применяется для получения промежуточных данных запросов для дальнейшего их использования в других запросах, что позволяет сделать удобней их отладку, оптимизировать код, сделать запросы более читаемыми.

    Допустим у вас есть запрос с несколькими временными таблицами и вам нужно в отладке посмотреть содержимое каждой из них. Обычно такие задачи решаются использованием консоли запросов, но если это невозможно(например в запрос передаются списки значений, таблицы и т.п.), используйте приведенный ниже способ.

    Добавляем к себе в модуль следующую функцию (серверную, если это управляемое приложение):

    Функция ДанныеВТ(Запрос, ИмяВТ) ДанныеТаблицы = Новый Запрос; ДанныеТаблицы. Текст = "Выбрать * | Из " + ИмяВТ+ " |" ; Возврат ДанныеТаблицы. Выполнить( ) . Выгрузить() ; КонецФункции

    Вызовем ее после текста запроса, например:

    Запрос = Новый Запрос; Запрос. Текст = "ВЫБРАТЬ |ТЗИтоговая.Наименование |ПОМЕСТИТЬ Итоговая |ИЗ |&ТЗИтоговая КАК ТЗИтоговая |; |////////////////////////////////////////////////////////// |ВЫБРАТЬ |* |ПОМЕСТИТЬ Ном |ИЗ |Справочник.Номенклатура КАК Номенклатура |; |////////////////////////////////////////////////////////// |ВЫБРАТЬ |* |ИЗ |Итоговая КАК Итоговая | ЛЕВОЕ СОЕДИНЕНИЕ Ном КАК Ном | ПО Итоговая.Наименование = Ном.Наименование" ; Запрос. МенеджерВременныхТаблиц = новый МенеджерВременныхТаблиц; Запрос. выполнить( ) ; Табл = ДанныеВТ(Запрос, "Итоговая" ) ;

    Теперь в переменной Табл содержится временная таблица Итоговая, ее можно просмотреть в отладке или вывести в табличный документ.

    Время жизни

    При выполнении запроса платформа уничтожает все временные таблицы, которые были созданы в рамках его выполнения. То есть, время жизни временных таблиц, создаваемых в рамках одного запроса со стороны платформы 1С:Предприятие, начинается от момента создания таблицы в запросе и завершается по его выполнении.

    При этом возможно уничтожить временную таблицу программным образом, выполнив в одном из пакетов запроса инструкцию "УНИЧТОЖИТЬ <ИмяВременнойТаблицы>". Тогда платформа выполняет SQL-команду "TRUNCATE" на уничтожение временной таблицы.

    При использовании менеджера временных таблиц, если таблицы не были явно уничтожены разработчиком (инструкция запроса "УНИЧТОЖИТЬ", сброс менеджера временных таблиц в "Неопределено" или методом "Закрыть()"), то платформа уничтожает их самостоятельно после того, как был уничтожен контекст, в рамках которого они были сформированы. Другими словами, объект будет уничтожен при завершении процедуры или функции, в которой он был создан.

    Влияние на производительность

    Использование временных таблиц позволяет существенно снизить нагрузку на SQL-сервер за счет многократного использования ранее сформированной выборки из таблицы в других запросах. Конечно, создание и хранение временных таблиц занимает дополнительные ресурсы, но, по сравнению с тратой ресурсов при повторных запросах к таблицам, этот вариант более оптимальный.

    Создание же временных таблиц на основе таблицы значений может нести негативное влияние на производительность, поскольку передача таблицы значений и заполнение на ее основе временной таблицы на SQL-сервере может занять длительное время и создать дополнительную нагрузку на систему. Использовать эту возможность нужно только в том случае, если в дальнейшем запросы интенсивно используют данные из таблицы значений. В противном случае более оптимальным был бы шаг по обработке этой таблицы программным образом после выполнения запроса к базе данных. Точный ответ зависит от конкретной задачи.

    Вывод

    Поддержка временных таблиц платформой 1С:Предприятие позволяет писать более оптимальные запросы к базе данных, при этом сам текст запроса также упрощается и становится более читабельным.

    Самым главным аргументом в пользу использования временных таблиц является то, что их использование позволяет SQL-серверу строить более оптимальные планы запросов.

    Бывают ситуации когда в одном запросе необходимо объединить несколько запросов, причем соединения таблиц никак не могут в этом помочь. Проще всего показать на примере.

    Допустим в нашей системе факты покупки и продажи товара регистрируются документами Приход и Расход соответственно. Контрагент может являться как покупателем, так и поставщиком. Зачет задолженности может производится поставкой товара:

    Чтобы подсчитать общую задолженность контрагента необходимо сложить сумму всех расходов по этому контрагенту и вычесть сумму всех приходов от этого же контрагента, проще всего это сделать с помощью оператора ОБЪЕДИНИТЬ ВСЕ:

    Запрос.Текст =
    "
    //посчитаем на какую сумму мы отгрузили контрагентам
    |ВЫБРАТЬ
    | Расход.Контрагент,
    |ИЗ
    | Документ.Расход КАК Расход
    |СГРУППИРОВАТЬ ПО
    | Расход.Контрагент
    |ОБЪЕДИНИТЬ ВСЕ
    //посчитаем на какую сумму контрагенты
    //поставили нам товара
    |ВЫБРАТЬ
    | Приход.Контрагент,
    //сумму берем с отрицательным знаком,
    //что при объединении она вычлась из суммы расхода
    | СУММА(-Приход.Сумма)
    |ИЗ
    | Документ.Приход КАК Приход
    |СГРУППИРОВАТЬ ПО
    | Приход.Контрагент";

    В первом запросе мы считаем сумму расходов по каждому контрагенту, во втором - сумму на которую нам поставил товар каждый из контрагентов. Сумма во втором запросе берется со знаком минус, что бы при свертке результирующей таблицы, она вычлась из суммы отгрузки данному контрагенту. В итоге получим таблицу вида:

    Это не совсем то, что мы хотели, но уже близко. Для достижения требуемого результата осталось сгруппировать по контрагенту. Для этого запрос необходимо поместить во временную таблицу (работа с временными таблицами рассматривается в закрытой части курса ) и уже из нее выбрать и сгруппировать поля:

    Запрос = Новый Запрос;
    Запрос.Текст =
    "ВЫБРАТЬ
    | Расход.Контрагент,
    | СУММА(Расход.Сумма) КАК Долг
    |ПОМЕСТИТЬ ВТ_ПриходРасход
    |ИЗ
    | Документ.Расход КАК Расход
    |СГРУППИРОВАТЬ ПО
    | Расход.Контрагент
    |ОБЪЕДИНИТЬ ВСЕ
    |ВЫБРАТЬ
    | Приход.Контрагент,
    | СУММА(-Приход.Сумма)
    |ИЗ
    | Документ.Приход КАК Приход
    |СГРУППИРОВАТЬ ПО
    | Приход.Контрагент
    |;
    |////////////////////////////////////////////////////////////////////////////////
    |ВЫБРАТЬ
    | ВТ_ПриходРасход.Контрагент,
    | СУММА(ВТ_ПриходРасход.Долг) КАК Долг
    |ИЗ
    | ВТ_ПриходРасход КАК ВТ_ПриходРасход
    |СГРУППИРОВАТЬ ПО
    | ВТ_ПриходРасход.Контрагент";

    Требования к объединению запросов

    При объединении двух запросов количество полей у них должно быть одинаковым, если в каком либо из запросов не хватает полей, то их надо добавить в виде констант. Обратимся к примеру выше, пусть в документе расход также есть поле скидка, которое уменьшает сумму долга контрагента, но в документе приход нет никаких скидок. Как быть в этом случае? Так:

    Запрос = Новый Запрос;
    Запрос.Текст =
    "ВЫБРАТЬ
    | Расход.Контрагент,

    |ИЗ
    | Документ.Расход КАК Расход
    |СГРУППИРОВАТЬ ПО
    | Расход.Контрагент
    |ОБЪЕДИНИТЬ ВСЕ
    |ВЫБРАТЬ
    | Приход.Контрагент,
    | СУММА(-Приход.Сумма),
    //добавляем нулевое поле скидка
    | 0
    |ИЗ
    | Документ.Приход КАК Приход
    |СГРУППИРОВАТЬ ПО
    | Приход.Контрагент";

    Осталось вычесть скидку и сгруппировать.

    Также важен порядок. Поля будут объединятся именно в том порядке в котором они указаны в секциях ВЫБРАТЬ обоих запросов. Применительно к предыдущему примеру - поменяем местами поля скидка и сумма в выборке приходов:

    Запрос = Новый Запрос;
    Запрос.Текст =
    "ВЫБРАТЬ
    | Расход.Контрагент,
    | СУММА(Расход.Сумма) КАК Долг,
    | СУММА(Расход.Скидка) КАК Скидка
    |ИЗ
    | Документ.Расход КАК Расход
    |СГРУППИРОВАТЬ ПО
    | Расход.Контрагент
    |ОБЪЕДИНИТЬ ВСЕ
    |ВЫБРАТЬ
    | Приход.Контрагент,
    //меняем местами
    | 0,
    | СУММА(-Приход.Сумма)
    |ИЗ
    | Документ.Приход КАК Приход
    |СГРУППИРОВАТЬ ПО
    | Приход.Контрагент";