При интеграции 1С с внешними базами данных MS SQL Server посредством технологии COM-объектов ADODB.Connection разработчики часто сталкиваются с крайне неприятной проблемой: процесс аварийно завершается с ошибкой «Недостаточно памяти». При этом на сервере может оставаться значительный объем свободной оперативной памяти (например, 6 ГБ или даже больше), а сама запрашиваемая таблица в SQL может содержать всего несколько десятков строк.
В этой статье мы подробно разберем, почему возникает эта ошибка, проанализируем неочевидные архитектурные особенности взаимодействия 1С с COM-объектами Windows, СУБД MS SQL и предложим надежные решения для оптимизации вашего кода.
Рассмотрим классический пример кода, в котором на первый взгляд все написано правильно, но при выполнении происходит моментальное падение платформы по памяти. Проанализируем этот фрагмент:
Попытка
Стр = "SELECT [stgr_ID], [stgr_Name] FROM [Central].[dbo].[StoreG]";
Rec = SQL.Execute(Стр);
Сообщить("Запрос выполнен");
Исключение
Сообщить("Невозможно выполнить запрос");
КонецПопытки;
i = 0;
Пока Rec.EOF <> 1 Цикл
Сообщить("i=" + i + " : " + Строка(Rec.Fields(1).Value));
НоваяСтрока = Объект.ГруппаСкладовФирма.Добавить();
НоваяСтрока.ГруппаСкладов = Строка(Rec.Fields(1).Value);
// Внимание: здесь пропущен переход к следующей записи!
КонецЦикла;
Выясним причину сбоя. В данном цикле отсутствует вызов метода Rec.MoveNext(). Из-за этого указатель выборки базы данных (Recordset) навсегда остается на первой строке. Условие Rec.EOF <> 1 никогда не выполнится, и цикл становится бесконечным.
Почему же система падает именно с ошибкой нехватки памяти, а не просто зависает? Дело в том, что на каждой итерации бесконечного цикла в табличную часть Объект.ГруппаСкладовФирма добавляется новая строка. Если база данных запущена в файловом режиме или используется 32-битный клиент 1С (даже на 64-битной операционной системе Windows), адресное пространство процесса 1cv8.exe жестко ограничено 2 ГБ (в редких случаях до 3–4 ГБ при использовании специальных флагов ОС).
Непрерывное выделение оперативной памяти под новые строки табличной части внутри бесконечного цикла мгновенно исчерпывает этот виртуальный лимит. В результате платформа аварийно завершает работу, игнорируя терабайты свободной физической памяти на сервере.
Решение проблемы: Всегда контролируйте обязательное наличие перехода к следующей строке выборки внутри цикла обхода:
Пока Rec.EOF <> 1 Цикл
// Ваша полезная логика обработки данных
НоваяСтрока = Объект.ГруппаСкладовФирма.Добавить();
НоваяСтрока.ГруппаСкладов = Строка(Rec.Fields(1).Value);
// Обязательный переход к следующей строке SQL-выборки!
Rec.MoveNext();
КонецЦикла;
Обратим внимание на строчку отладочного вывода: Сообщить("i=" + i + " : " + ... ). В языке 1С попытка прямого сложения строкового литерала с числовой переменной без явного преобразования типов может приводить к неявным ошибкам приведения типов или некорректной работе платформы.
Чтобы избежать подобных недоразумений во время отладки, всегда принудительно приводите числовые счетчики к строковому типу или используйте встроенную функцию Шаблон. Рассмотрим правильный вариант написания вывода:
// Вариант 1: Явное преобразование типа
Сообщить("i=" + Строка(i) + " : " + Строка(Rec.Fields(1).Value));
// Вариант 2: Использование функции Шаблон (рекомендуется)
Сообщить(Шаблон("Итерация %1: Значение поля = %2", i, Строка(Rec.Fields(1).Value)));
В строке подключения большинства старых обработок можно встретить конструкцию: Provider=SQLOLEDB;.
Важно знать, что Microsoft официально признала этот провайдер устаревшим (deprecated). Он содержит критические системные ошибки (баги), связанные с утечками памяти (Memory Leaks) при работе в многопоточных серверных средах 1С или при обработке определенных типов данных SQL Server (таких как длинные строки VARCHAR(MAX), NVARCHAR(MAX) или двоичные данные VARBINARY(MAX)).
Для стабильной работы с современными версиями СУБД MS SQL Server (начиная с SQL Server 2012 и новее) настоятельно рекомендуется переходить на современный драйвер MSOLEDBSQL (Microsoft OLE DB Driver for SQL Server).
Перепишем строку подключения с использованием актуального и безопасного драйвера:
ServerName = "TEST1C\TEST1C";
DSN = "Central";
UID = "sa";
PWD = "as";
// Заменяем SQLOLEDB на современный MSOLEDBSQL
ConnectString = "Provider=MSOLEDBSQL;"
+ "Data Source=" + ServerName + ";"
+ "Initial Catalog=" + DSN + ";"
+ "User ID=" + UID + ";"
+ "Password=" + PWD + ";";
Этот драйвер работает значительно быстрее, корректно утилизирует память и решает проблемы с утечками при интенсивных COM-вызовах со стороны сервера 1С.
В экосистеме 1С при работе с внешними COM-объектами (такими как ADODB.Connection или ADODB.Recordset) очистка оперативной памяти компьютера происходит не сразу после того, как переменная выходит из области видимости процедуры.
Платформа 1С использует собственный отложенный сборщик мусора. Если разработчик не закрывает соединения принудительно, память, выделенная под структуры данных СУБД внутри операционной системы, остается заблокированной до тех пор, пока сеанс пользователя 1С не будет полностью завершен. При частом выполнении регламентных заданий это приводит к лавинообразному росту потребления RAM рабочими процессами сервера 1С (rphost.exe).
Разберем, как правильно освобождать ресурсы. После завершения работы с запросом всегда нужно явно закрывать выборку данных и соединение в блоке Попытка - Исключение, а затем обнулять переменные:
// Явное закрытие Recordset
Если Rec <> Неопределено Тогда
Попытка
Rec.Close();
Исключение
// Игнорируем ошибку, если объект уже закрыт
КонецПопытки;
Rec = Неопределено; // Уничтожаем COM-ссылку в 1С
КонецЕсли;
// Явное закрытие Connection
Если SQL <> Неопределено Тогда
Попытка
SQL.Close();
Исключение
КонецПопытки;
SQL = Неопределено; // Уничтожаем COM-ссылку в 1С
КонецЕсли;
По умолчанию метод Execute у объекта ADODB.Connection создает серверный последовательный курсор (forward-only, read-only). Однако, если в кодовой базе принудительно переключается расположение курсора на сторону клиента (свойство CursorLocation = 3, что соответствует клиенту adUseClient), поведение ADO резко меняется.
При выполнении запроса драйвер пытается выгрузить весь результат запроса целиком в локальную оперативную память клиентского компьютера или сервера приложений 1С до начала выполнения цикла обхода. Если запрашиваемая таблица СУБД содержит сотни тысяч строк или «тяжелые» бинарные поля (например, файлы, изображения или длинный XML-текст), то попытка выгрузить их в локальный кэш гарантированно спровоцирует ошибку нехватки памяти прямо в момент вызова метода SQL.Execute().
Для предотвращения подобных ситуаций используйте серверные курсоры и запрашивайте только те поля, которые действительно необходимы для бизнес-логики программы, избегая конструкции SELECT *.
Подводя итог нашему анализу, соберем все лучшие практики в один надежный и оптимизированный шаблон кода для работы в 1С. Этот шаблон содержит безопасное подключение, правильный обход выборки, корректную обработку типов и гарантированное освобождение памяти в случае возникновения любых нештатных ситуаций:
SQL = Новый COMОбъект("ADODB.Connection");
Rec = Новый COMОбъект("ADODB.Recordset");
ServerName = "TEST1C\TEST1C";
DSN = "Central";
UID = "sa";
PWD = "as";
// Использование современного драйвера MSOLEDBSQL
ConnectString = "Provider=MSOLEDBSQL;"
+ "Data Source=" + ServerName + ";"
+ "Initial Catalog=" + DSN + ";"
+ "User ID=" + UID + ";"
+ "Password=" + PWD + ";";
SQL.ConnectionString = ConnectString;
SQL.ConnectionTimeOut = 15;
SQL.CommandTimeout = 30;
// Устанавливаем серверный курсор для экономии памяти клиента
SQL.CursorLocation = 2; // adUseServer
Попытка
SQL.Open();
Сообщить("Подключение к MS SQL успешно установлено!");
Исключение
Сообщить("Ошибка подключения к базе данных: " + ОписаниеОшибки());
SQL = Неопределено;
Возврат;
КонецПопытки;
ТекстЗапроса = "SELECT [stgr_ID], [stgr_Name] FROM [Central].[dbo].[StoreG]";
Попытка
// Открываем Recordset безопасным способом
Rec.Open(ТекстЗапроса, SQL, 0, 1); // adOpenForwardOnly, adLockReadOnly
Сообщить("Запрос успешно выполнен!");
Исключение
Сообщить("Не удалось выполнить запрос: " + ОписаниеОшибки());
// Закрываем соединение при ошибке
Попытка SQL.Close(); Исключение КонецПопытки;
SQL = Неопределено;
Rec = Неопределено;
Возврат;
КонецПопытки;
i = 0;
Попытка
Пока Rec.EOF <> 1 Цикл
i = i + 1;
// Безопасный вывод отладочной информации
Сообщить(Шаблон("Строка %1: Идентификатор = %2, Наименование = %3",
i,
Строка(Rec.Fields("stgr_ID").Value),
Строка(Rec.Fields("stgr_Name").Value)));
// Заполнение табличной части 1С
НоваяСтрока = Объект.ГруппаСкладовФирма.Добавить();
НоваяСтрока.ГруппаСкладов = Строка(Rec.Fields("stgr_Name").Value);
// Критически важный переход к следующей записи!
Rec.MoveNext();
КонецЦикла;
Исключение
Сообщить("Ошибка при обработке данных выборки: " + ОписаниеОшибки());
КонецПопытки;
// ГАРАНТИРОВАННОЕ ОСВОБОЖДЕНИЕ ВСЕХ РЕСУРСОВ
Попытка
Если Rec.State <> 0 Тогда
Rec.Close();
КонецЕсли;
Исключение
КонецПопытки;
Rec = Неопределено;
Попытка
Если SQL.State <> 0 Тогда
SQL.Close();
КонецЕсли;
Исключение
КонецПопытки;
SQL = Неопределено;
Сообщить("Работа с базой данных полностью завершена, ресурсы освобождены.");
Использование этого шаблона позволит вам полностью предотвратить неконтролируемые утечки памяти при интеграции 1С с СУБД MS SQL Server, сделает ваш код устойчивым к ошибкам и гарантирует высокую скорость обработки данных даже при больших объемах информации.