При проектировании высоконагруженных систем на платформе 1С:Предприятие разработчики часто сталкиваются с вопросами производительности СУБД (будь то Microsoft SQL Server, PostgreSQL или IBM DB2). Один из наиболее частых вопросов: как физически происходит запись документа, содержащего табличную часть?
Допустим, в документе уже есть 10 строк, и пользователь (или программный код) добавляет 11-ю строку. Что происходит в СУБД при вызове метода Записать()? Добавляется ли только одна новая строка, или СУБД полностью перезаписывает все 11 строк? И как в этот момент ведет себя таблица шапки документа? Разберем эти механизмы во всех подробностях, изучим внутреннее устройство платформы и проанализируем влияние этих процессов на производительность базы данных.
Для начала проанализируем, как структуры данных 1С соотносятся с таблицами на уровне систем управления базами данных (СУБД). Платформа 1С не хранит весь документ в одной таблице. Вместо этого применяется нормализованная структура данных:
_IDRRef), номер, дату, признак проведения, а также все реквизиты шапки. Физическое имя таблицы в СУБД выглядит примерно так: _Document111._Document111_VT222. В этой таблице содержатся поля связи (ссылка на родительский документ _Document111_IDRRef), номер строки _LineNo и реквизиты табличной части.Поскольку данные физически разделены, операции записи для шапки документа и для его табличных частей выполняются СУБД независимо, хотя и в рамках одной общей транзакции базы данных.
Разберем ключевой аспект работы платформы. Платформа 1С не вычисляет разницу (diff) между старым состоянием табличной части в СУБД и новым состоянием в оперативной памяти. Она не пытается определить, какая именно строка была добавлена, изменена или удалена пользователем на форме.
Вместо этого платформа использует радикальный, но максимально надежный с точки зрения целостности данных подход — полную перезапись всей табличной части. Этот процесс состоит из двух последовательных шагов на уровне СУБД:
При записи документа платформа отправляет в СУБД запрос на удаление всех существующих строк данной табличной части, которые привязаны к уникальному идентификатору (GUID) записываемого документа. На уровне SQL это выглядит следующим образом:
DELETE FROM _Document111_VT222
WHERE _Document111_IDRRef = 0x80C200155D01010111E1A1A1A1A1A1A1;
Даже если в документе изначально было 10 000 строк, а мы просто изменили один символ в одной строке или добавили одну новую, СУБД сначала физически удалит все 10 000 строк.
После очистки таблицы от старых записей платформа считывает актуальный массив строк табличной части из оперативной памяти сервера приложений 1С и записывает их в СУБД "с чистого листа". В нашем примере запишутся все 11 строк.
На уровне СУБД выполняется серия инструкций INSERT:
INSERT INTO _Document111_VT222 (_Document111_IDRRef, _LineNo, _Fld333, _Fld334)
VALUES
(0x80C200155D01010111E1A1A1A1A1A1A1, 1, Значение1, Значение2),
(0x80C200155D01010111E1A1A1A1A1A1A1, 2, Значение1, Значение2),
...
(0x80C200155D01010111E1A1A1A1A1A1A1, 11, Значение1, Значение2);
Для оптимизации этого процесса современные версии платформы (8.3) используют параметризованные пакетные запросы (Bulk Insert), объединяя несколько инструкций вставки в один сетевой пакет. Это минимизирует накладные расходы на сетевое взаимодействие между сервером приложений 1С и сервером СУБД.
В отличие от табличной части, строка в основной таблице документа (шапке) никогда не удаляется при перезаписи. Для обновления реквизитов шапки платформа использует стандартную инструкцию UPDATE:
UPDATE _Document111
SET _Fld112 = 'НовоеЗначениеРеквизита', _Version = @НоваяВерсияДанных
WHERE _IDRRef = 0x80C200155D01010111E1A1A1A1A1A1A1;
Платформа 1С оптимизирует этот запрос и обновляет только те колонки шапки, значения которых реально изменились в процессе работы с объектом в памяти. Если реквизиты шапки вообще не менялись, СУБД все равно выполнит легкий UPDATE (как минимум, для обновления служебного поля версии данных _Version, которое необходимо для контроля объектной блокировки).
Выясним еще один важный момент, затронутый в обсуждении на форуме. Запись документа — это не только работа с таблицами самого документа. В реальных конфигурациях при вызове метода Записать() с режимом проведения запускается целый комплекс транзакционных изменений:
DELETE по _RecorderIDRRef), и записываются новые наборы записей.INSERT или UPDATE в соответствующих таблицах СУБД.Такой "радикальный" способ перезаписи табличных частей имеет ряд важных технических последствий, о которых должен знать каждый разработчик.
Поскольку при каждой перезаписи документа происходит массовое удаление старых строк и вставка новых, в физических файлах СУБД образуются "дыры". Индексы, построенные по таблицам табличных частей (особенно по ключевому полю _Document111_IDRRef), подвергаются сильной фрагментации.
Если документы в системе перезаписываются часто, это приводит к резкому падению производительности операций чтения. Для борьбы с этим требуется регулярное обслуживание СУБД — дефрагментация и реиндексация таблиц (Rebuild/Reorganize Indexes) во время технологических окон.
При выполнении операции DELETE современные СУБД не удаляют данные с диска мгновенно — это слишком дорогая операция. Вместо этого записи помечаются как удаленные.
Если темп перезаписи документов слишком высок, СУБД может не успевать вычищать удаленные строки, что приводит к раздуванию базы данных ("bloat") и замедлению сканирования таблиц.
При выполнении цикла DELETE + INSERT СУБД накладывает монопольные блокировки на строки (или даже на страницы данных/всю таблицу при эскалации блокировок). Если несколько сеансов одновременно пытаются записать или провести документы одного типа, это может приводить к длительным ожиданиям на блокировках и взаимным блокировкам (дедлокам).
В процессе удаления и повторной вставки платформа автоматически пересчитывает поле _LineNo (номер строки). Даже если пользователь интерактивно поменял строки местами или удалил строку из середины, СУБД запишет их с новыми, строго последовательными номерами от 1 до N. Это гарантирует сохранение правильного порядка строк при последующей выборке данных.
Рассмотрим, как разработчик может минимизировать негативное влияние механизма перезаписи табличных частей в высоконагруженных системах:
Записать() программно проверяйте, изменились ли данные документа. Не вызывайте перезапись "на всякий случай".