Работа с внешними базами данных через ADO (ActiveX Data Objects) — распространенная задача при интеграции 1С с другими системами (SQL Server, Access, Oracle, Informix и др.). Однако часто разработчики сталкиваются с проблемой производительности: получение данных происходит быстро, а вот перенос этих данных из COM-объекта RecordSet в родную для 1С ТаблицаЗначений занимает непозволительно много времени.
В этой статье мы подробно разберем, почему возникает эта проблема, и рассмотрим несколько эффективных способов ускорения выгрузки данных, основанных на практике сообщества разработчиков. Мы пройдем путь от стандартного (медленного) цикла до использования массовой выгрузки и тонкой настройки параметров соединения.
Рассмотрим классический пример кода, который чаще всего пишут начинающие разработчики. Он заключается в переборе записей набора данных в цикле While и переборе полей внутри каждой строки.
// Медленный способ
COMRecordSet.Open(ТекстЗапросаSQL);
COMRecordSet.MoveFirst();
Пока COMRecordSet.EOF() = 0 Цикл
НоваяСтрока = ТаблицаЗначений.Добавить();
Для Каждого Поле Из COMRecordSet.Fields() Цикл
НоваяСтрока[Поле.Name] = COMRecordSet.Fields(Поле.Name).Value;
КонецЦикла;
COMRecordSet.MoveNext();
КонецЦикла;
В чем причина "тормозов"?
Основная проблема кроется в маршалинге (передаче данных) между средой исполнения 1С и COM-объектом. В приведенном выше коде при каждом обращении к COMRecordSet.Fields, Value или EOF происходит вызов внешнего объекта. Если у вас 5000 строк и 20 колонок, это означает 100 000+ переключений контекста и обращений к COM-интерфейсу. Это создает огромные накладные расходы.
Самый эффективный способ ускорить процесс — минимизировать количество обращений к COM-объекту. Вместо того чтобы тянуть данные по одной ячейке, мы можем забрать весь набор данных (или его часть) одним вызовом. Для этого в ADO существует метод GetRows.
Метод GetRows возвращает двумерный массив (COMSafeArray), содержащий данные запроса. В 1С мы можем преобразовать его в обычный массив и работать с ним уже внутри памяти 1С, что на порядки быстрее.
Важный нюанс: Массив, возвращаемый методом GetRows, является транспонированным. Это означает, что первым индексом в нем идет номер колонки, а вторым — номер строки. То есть формат данных: Массив[Колонка][Строка].
Разберем оптимизированный алгоритм:
RecordSet.ТаблицаЗначений (создаем колонки).GetRows.Посмотрим на пример реализации:
// Создаем соединение и выполняем запрос
RecordSet = Connection.Execute(ТекстЗапроса);
Если RecordSet.EOF = 0 Тогда
// 1. Подготовка ТаблицыЗначений
ТЗ = Новый ТаблицаЗначений;
КоличествоКолонок = RecordSet.Fields.Count - 1;
// Создаем колонки в ТЗ по именам полей из запроса
Для Сч = 0 По КоличествоКолонок Цикл
ИмяКолонки = RecordSet.Fields(Сч).Name;
ТЗ.Колонки.Добавить(ИмяКолонки);
КонецЦикла;
// 2. Массовое получение данных
// COMSafeArray преобразуем в массив 1С методом Выгрузить() (или Unload в англ. синтаксисе)
МассивДанных = RecordSet.GetRows().Выгрузить();
// Получаем границы массива
// Напомним: МассивДанных[Колонка][Строка]
КоличествоСтрок = МассивДанных[0].ВГраница();
// 3. Заполнение ТаблицыЗначений
// Предварительно создаем нужное количество строк для скорости
Для ИндСтр = 0 По КоличествоСтрок Цикл
НоваяСтрока = ТЗ.Добавить();
// Заполняем поля, обращаясь к массиву
Для ИндКол = 0 По КоличествоКолонок Цикл
// Обратите внимание на порядок индексов: [Колонка][Строка]
НоваяСтрока[ИндКол] = МассивДанных[ИндКол][ИндСтр];
КонецЦикла;
КонецЦикла;
КонецЕсли;
RecordSet.Close();
Практика показывает, что для набора в 5000 строк и 20 полей время выгрузки снижается с 9–10 секунд до долей секунды. Это колоссальный прирост производительности.
Даже при использовании GetRows, выборка данных может работать недостаточно быстро, если ADO запрашивает у сервера данные слишком мелкими порциями. На помощь приходит свойство CacheSize объекта RecordSet.
Этот параметр определяет, сколько записей ADO хранит в локальной памяти за один раз. По умолчанию значение часто равно 1, что крайне неэффективно для больших выборок.
Попробуйте установить размер кэша перед открытием набора данных или перед перебором:
RecordSet = Новый COMОбъект("ADODB.RecordSet");
RecordSet.CursorLocation = 3; // adUseClient - важно для клиентского кэширования
RecordSet.CacheSize = 500; // Размер пачки записей
RecordSet.Open(ТекстЗапроса, Connection);
Увеличение CacheSize до 100, 500 или даже 1000 может значительно сократить количество сетевых обращений к серверу БД. В одном из примеров время обработки 150 000 строк сократилось с 60 секунд до 1 секунды после настройки этого параметра.
Иногда проблема кроется не в коде 1С, а в драйвере, который используется для подключения. Разные провайдеры имеют разную производительность при работе с курсорами.
Если вы используете устаревшие драйверы ODBC (например, Microsoft Access Driver), стоит попробовать переключиться на нативные OLEDB провайдеры (например, Microsoft.Jet.OLEDB.4.0 для MDB файлов или SQLNCLI / MSOLEDBSQL для MS SQL Server).
Пример изменения строки подключения, который дал прирост производительности в 10 раз:
"Driver={Microsoft Access Driver (*.mdb)}; ...""Provider=Microsoft.Jet.OLEDB.4.0; ..."Проверьте вашу строку подключения (Connection String) и убедитесь, что вы используете наиболее актуальный и прямой метод доступа к данным.
Еще один момент, который часто упускают — типизация колонок при создании ТаблицаЗначений. Если вы создаете колонку просто методом ТЗ.Колонки.Добавить("Имя"), то тип значения в колонке не определен (составной). При записи данных платформа 1С тратит время на определение типа и выделение памяти.
Если типы данных известны заранее (или их можно определить по первому значению из RecordSet), явное указание типов даст дополнительный прирост скорости.
// Пример создания типизированной колонки (строка 100 символов)
ТЗ.Колонки.Добавить("CustomerName", Новый ОписаниеТипов("Строка", , Новый КвалификаторыСтроки(100)));
При работе с выгрузкой больших объемов данных через ADO стоит учитывать еще несколько нюансов:
NULL. В COM-объекте это специальное значение. При попытке записать его напрямую в типизированную колонку 1С (например, числовую) возникнет ошибка. Рекомендуется либо обрабатывать NULL на стороне SQL-запроса (функция ISNULL или COALESCE), заменяя их на пустые строки или нули, либо проверять каждое значение в цикле 1С.CursorLocation = 3, adUseClient). Это позволяет загрузить данные в память машины, где выполняется код 1С, и работать с ними локально, что значительно ускоряет методы GetRows и RecordCount.SELECT * FROM #TempTable) уже средствами ADO.Чтобы максимально ускорить загрузку данных из ADO в 1С:
GetRows() для получения данных массивом.[Колонка][Строка].CacheSize (например, 500-1000).CursorLocation = 3 (adUseClient).