Базу 1С переносят между СУБД регулярно: уходят на PostgreSQL, возвращаются на MS SQL Server, переезжают с Windows на Linux и обратно. Инструмент штатный, ibcmd infobase replicate, работает с платформы 8.3.23. Процедура выглядит просто: запустил, дождался, сверил.
Вот на слове "сверил" всё и ломается.
Почти везде приёмка переноса устроена одинаково: посчитать строки в контрольных таблицах до и после, убедиться, что сошлось, и подписать. Эта проверка не доказывает ничего, кроме того, что строки не потерялись. А теряются не строки.
Разберём, что именно портится молча при переносе, как это найти до переезда и какой набор проверок заменяет счётчик строк.
Метод везде один - штатные средства платформы: структура хранения, которую 1С отдаёт сама, запросы на языке запросов 1С и вывод утилиты переноса. К СУБД ничего не подключается и никаких скриптов для администратора не печатается, поэтому проверки работают у обычного пользователя базы, без DBA и без доступа к серверу СУБД.
Числа ниже сняты на трёх разных базах, и каждое подписано: стенд - та самая бухгалтерия, которую переносили; УПП - боевая база на платформе 8.3.20; УТ 11.5 - типовая на 8.3.27. Складывать их между собой нельзя, базы разного размера и возраста.
Почему счётчик строк не работает
Возьмём реальный прогон. Стенд: тестовая бухгалтерская база, PostgreSQL 17 на Windows, приёмник MS SQL Server 2022, платформа 8.3.27. Объём источника 22 ГБ, 11 396 таблиц.
Перенос завершился так:
Репликация информационной базы успешно завершена Код возврата: 0
Счётчики строк по шести контрольным таблицам совпали до единицы. Включая ту таблицу, данные в которой уже были испорчены.
В stderr при этом лежала одна строка на весь прогон, 208 байт: таблица такая-то содержит значения типа Дата, которые не могут быть записаны в MS SQL Server с нулевым смещением дат.
Что произошло на самом деле:
| Было в PostgreSQL | Стало в MS SQL |
|---|---|
0001-01-01 00:00:00 |
1753-01-01 00:00:00 |
0001-01-02 00:00:00 |
1753-01-01 00:00:00 |
Две разные даты стали одной. Строк по-прежнему шесть, значения внутри них другие. Задета одна таблица из 11 396, и найти это глазами нельзя.
Отсюда первый вывод, который стоит принять до всякой методики: приёмка по количеству строк ловит потерю строк и слепа к порче значений. А при переносе между СУБД портятся именно значения.
Шаг 1. Понять, что смещение дат это не формальность
Смещение дат задаётся при создании базы-приёмника: --target-date-offset у ibcmd или поле в диалоге создания. Варианта два, 0 и 2000.
Механика такая. MS SQL Server хранит дату в типе с нижней границей 01.01.1753. У платформы 1С минимальная дата 01.01.0001, и она же служит признаком "дата не заполнена". Чтобы пустая дата влезла в диапазон СУБД, платформа прибавляет к году смещение. При смещении 2000 год 1 превращается в 2001 и укладывается спокойно. При смещении 0 не укладывается ничего раньше 1753 года, и платформа подрезает такие значения до нижней границы.
Что даёт шаг: понимание, что 0 и 2000 не равнозначны и выбор между ними не стилистический.
Ловушка, на которой сворачивают не туда
Соблазн взять 0 сильный и выглядит обоснованным: источник на PostgreSQL живёт со смещением 0, данные не надо пересчитывать, значения в двух базах совпадут один в один. В нашем прогоне так и рассуждали.
Проверку перед этим сделали: посмотрели две другие рабочие базы MS SQL, у обеих смещение 0, обе живы. Вывод "ноль рабочий" казался замеренным.
Он был неверным, и вот почему. Обе базы пустые. В них нет ни одной реальной даты. Опыт показал, что платформа принимает 0 при создании базы, и ничего не сказал о том, переживут ли его данные. Проверялся не тот образец.
Это общий класс ошибки, и он дороже конкретной граблей: инструмент отвечает честно, а вопрос задан не тот. За один заход такое случилось четырежды, и каждый раз ответ выглядел правдоподобно. Две другие истории того же рода разобраны ниже, в разделе про развилку.
Что делать практически
Ставить смещение 2000. Это значение ibcmd подставляет сам, если не вмешиваться, и оно не случайно.
Отдельный вопрос, который возникает сразу: а какое смещение стоит у моей текущей базы? Платформа его в интерфейсе не показывает, но проверяется оно косвенно и надёжно. Запустите проверку дат из шага 2 с параметром "смещение 0": если она находит значения раньше 1753 года, значит у базы смещение НЕ нулевое и эти даты живы. Если не находит ничего на базе с историей за много лет, это повод присмотреться.
Если смещение уже выбрано нулевым и менять его нельзя, надо знать заранее, сколько значений пострадает и где. Об этом шаг 2.
Шаг 2. Найти даты, которые не переживут смещение
Задача формулируется так: пройти все таблицы с данными, найти поля типа Дата и посчитать в них значения меньше границы. Граница вычисляется как 1753 минус выбранное смещение.
При смещении 2000 арифметика даёт отрицательный год, и это правильный ответ: под границу не попадает ничего, проверять нечего. Смысл проверки появляется только при смещении меньше 1752.
Всё делается средствами платформы, без обращения к СУБД. Отправная точка - ПолучитьСтруктуруХраненияБазыДанных().
Что даёт шаг: поимённый список реквизитов, значения которых испортятся, с числом строк по каждому.
Три вещи, которые здесь неочевидны
Первая. Полное имя реквизита состоит из четырёх частей. Структура хранения отдаёт в свойстве Метаданные строку вида Справочник.Банки.Реквизит.Адрес, а у реквизита табличной части из шести частей: Документ.Х.ТабличнаяЧасть.Y.Реквизит.Z. Разбирать её вручную не нужно вовсе: в описании таблицы уже лежит ИмяТаблицы - готовый источник языка запросов, а в описании поля ИмяПоля - готовое имя поля.
Вторая. Самые массовые даты лежат в полях без метаданных. У даты документа и периода регистра свойство Метаданные пустое, и обход, который отсеивает поля по этому признаку, пропускает их целиком. Отличать их надо по имени хранения: _Date_Time и _Period.
Третья. Не все таблицы структуры годятся для запроса. Кроме основных таблиц и табличных частей там лежат производные и служебные: обороты регистров, итоги, значения субконто, границы последовательностей, регистрация изменений. Запрос к виртуальной таблице оборотов вернёт ошибку "Поле не найдено Период", потому что набор полей у неё другой.
Замер назначений на двух конфигурациях, УПП на 8.3.20 и Управление торговлей 11.5 на 8.3.27:
| Назначение | УПП | УТ 11.5 | Брать? |
|---|---|---|---|
| Основная | 2529 | 2678 | да |
| ТабличнаяЧасть | 1980 | 1185 | да |
| Константа | 184 | 899 | да |
| РегистрацияИзменений | 870 | 1640 | нет, дат не содержит |
| Обороты | - | 22 | нет, производная |
| ГраницыПоследовательности | 6 | - | нет, повторяет даты документов |
Данные, которые вводит человек, лежат ровно в трёх видах таблиц. Остальное либо производное, либо без дат.
Один запрос на таблицу
Запрос на каждое поле превращает проверку в тысячи полных проходов. Агрегаты по всем датам таблицы считаются за один проход:
ВЫБРАТЬ РАЗРЕШЕННЫЕ
КОЛИЧЕСТВО(ВЫБОР КОГДА Поле1 < &Граница ТОГДА 1 КОНЕЦ) КАК Kolvo0,
МИНИМУМ(ВЫБОР КОГДА Поле1 < &Граница ТОГДА Поле1 КОНЕЦ) КАК Minim0,
...
ИЗ Справочник.Банки
Псевдоним источнику давать не стоит. Любое придуманное имя рано или поздно совпадёт с именем реквизита: в боевой УПП есть табличная часть с реквизитом Источник, и запрос с таким псевдонимом падает на "Неоднозначное поле". Когда источник один, имена полей разрешаются и без префикса.
Пустая дата печатается пустотой
Мелочь, которая бьёт точно в цель. Формат() над пустой датой возвращает пустую строку:
Формат(Дата(1,1,1), "ДЛФ=DD") // [] Формат(Дата(1,1,1), "ДФ=dd.MM.yyyy") // [] Формат(Дата(1,1,1), "ДФ=dd.MM.yyyy; ДП=01.01.0001") // [01.01.0001]
Тот же класс, что Формат(0) без ЧН. И попадает он ровно в главный случай: пустая дата и есть то значение, которое схлопывается с соседним. В отчёте вместо даты оказывается пустое место, и человек читает это как "данных нет".
Сколько это находит на живой базе
УТ 11.5, обычная типовая, не специально испорченная: при смещении 0 нашлось 41 675 значений в 119 реквизитах, обход 1 480 таблиц с данными занял 10 секунд. Крупнейшие - производственный календарь и регистры себестоимости, где пустая дата это штатное состояние записи.
На УПП с боевыми данными счёт другой: 3,5 миллиона значений, и почти миллион из них сидит в одном реквизите регистра расчёта. Сравнивать эти два числа между собой смысла нет, базы разного размера и возраста.
Все они при смещении 0 стали бы одной датой. Без единой ошибки в логе.
Шаг 3. Посчитать ширину ключей индексов
Второй тихий отказ, и он опаснее первого.
У MS SQL Server предел ключа кластерного индекса 900 байт, некластерного 1700 начиная с версии 2016. Кластерный - тот, по которому физически упорядочены строки таблицы; он в таблице один. У PostgreSQL около 2704. То есть индекс, который в источнике живёт спокойно, на приёмнике может не поместиться.
Что даёт шаг: список индексов, которые станут проблемой после переезда, с разбором ключа по байтам.
Ожидали отказ, получили тишину
Гипотеза была простая: перенос упадёт на длинном ключе. Он не упал.
Замер на стенде. Состав индексов и ширина ключей считаются по структуре хранения, ошибки берутся из лога самого переноса:
| Замер | Значение |
|---|---|
Создано индексов, по выводу ibcmd |
24 535 |
| Ошибок при создании | 0 |
| Ключей шире 900 байт | 37 |
| Ключей шире 1700 байт | 0 |
| Самый широкий ключ | 1234 байта |
SQL Server разрешает создать индекс с объявленным ключом сверх предела, пока фактические данные в предел влезают, и ограничивается предупреждением. Перенос проходит и оставляет за собой 37 мест, где запись с длинным значением упадёт потом - через недели или месяцы, когда связь с переездом уже не очевидна.
И то же самое зависит от версии приёмника. На 2016 и новее предел некластерного ключа подняли до 1700 байт, и все 37 наших ключей в него укладываются. На 2012 и 2014 такого послабления нет: там 900 байт действуют для любого индекса, то есть под подозрением оказываются все 37 сразу. Итог переезда определяется не только базой, но и тем, куда её везут.
Тонкость, из-за которой полоса от 901 до 1700 байт остаётся серой зоной: какой из индексов кластерный, платформа не сообщает. Значит ключ в этой полосе означает не "пройдёт", а "скорее всего пройдёт": окажись он кластерным, предел у него останется 900 байт и в свежей версии. Замеренный факт тут один, и он важнее прогноза - ключи шире 900 байт создались без единой ошибки.
Как считать ширину платформенными средствами
Состав каждого индекса отдаёт та же ПолучитьСтруктуруХраненияБазыДанных(), что и в шаге 2: имя индекса, список колонок, имя хранения каждой колонки. Ширина набирается по колонкам, и считать её надо по суффиксу имени хранения.
Разница принципиальная. У составного реквизита хранение разложено на несколько колонок - _TYPE, _RTRef, _RRRef, _S, _N - и в свойстве Метаданные у каждой лежит один и тот же реквизит. Считая по типу, полную ширину составного типа прибавляешь на каждую колонку. На одном индексе УПП это дало 8 316 байт против настоящих 2 107, почти вчетверо.
Ширина колонки определяется тем, какой тип СУБД платформа под неё заводит, а типы и их длины задокументированы. Отсюда таблица:
| Поле или суффикс | Байт | Поле или суффикс | Байт |
|---|---|---|---|
_TYPE |
1 | _S |
2 x длина строки |
_RTRef, TRef |
4 | _N |
5 / 9 / 13 / 17 по разрядности |
_RRRef, RRef |
16 | _L, _Marked, _Posted |
1 |
_Period, _T, любая дата |
6 | _LineNo, _MessageNo |
5 |
_IDRRef, _PredefinedID |
16 | _Code |
2 x ДлинаКода |
Обратите внимание на дату: 6 байт. С 2008 года MS SQL хранит дату в типе datetime2, и длина у него зависит от заданной точности: от 6 байт при точности 0-2 до 8 при 5-7. Платформа заводит колонки дат с той точностью, которая даёт 6. Цифра 8 от старого datetime кочует из статьи в статью и даёт систематическую ошибку: лишние 2 байта на каждую дату в ключе.
Служебные таблицы платформы считаются отдельно
Часть таблиц платформа в структуре показывает, но метаданных у них нет: хранилища настроек, тела сообщений сервисов интеграции. Признак простой - пустое ИмяТаблицы. Различать обязательно, потому что одно и то же поле в них имеет другую длину: _Version в справочнике это 8 байт, а в служебной таблице 16.
Ширины их полей: _UserId 128, _ObjectKey 512, _SettingsKey 512, _MessageBodyKey 1024.
Сложите первые три и получите 1152 байта - ключ хранилища настроек. Это на 252 байта выше предела кластерного индекса MS SQL, и такие индексы платформа создаёт в любой базе, независимо от конфигурации.
Почему тогда базы живут годами. Предел проверяется по фактическим данным, а не по объявленной ширине: ключи настроек короткие, реальные значения в 900 байт укладываются с запасом, и отказа никто никогда не видел. Это и есть та самая отложенная мина, которая не взрывается, пока в поле не попадёт длинное значение.
Вендор упёрся в тот же предел и обошёл его хешами
Замер на двух базах:
| Платформа | Ключ хранилища настроек | Как устроен |
|---|---|---|
| 8.3.20 | 1152 байта | _UserId 128 + _ObjectKey 512 + _SettingsKey 512 |
| 8.3.27 | 556 байт | длинные ключи ушли в текст неограниченной длины, рядом появились числовые хеш-колонки, индекс строится по ним |
Оговорка, без которой это не факт, а догадка: базы отличаются не только платформой, но и конфигурацией. Чтобы утверждать "дело в версии платформы", нужен третий замер. Пока это два наблюдения рядом.
Практический вывод от этого не зависит: порог зависит и от версии приёмника, и от версии платформы, потому что платформа меняет устройство собственных служебных таблиц. Считать надо по структуре хранения конкретной базы: готовая табличка констант из чужой статьи тут врёт.
Шаг 4. Заменить счётчик строк на контрольный срез
Возвращаемся к тому, с чего начали. Если строки ничего не доказывают, что считать?
Что даёт шаг: набор величин, которые меняются при порче значений внутри строк.
Все они снимаются платформенными запросами с обеих сторон, до и после переноса:
- суммы числовых ресурсов по крупным регистрам накопления и бухгалтерии. Порча значений внутри строк сдвигает сумму, потеря строк тоже;
- границы дат: минимальная и максимальная дата по документам и периодам регистров. Именно здесь вылезает схлопывание в 1753 год;
- количество пустых дат по реквизитам из шага 2. Если до переноса их было 41 675, а после ноль, значит они превратились во что-то другое;
- количество элементов справочников с разбивкой по владельцу или родителю;
- контрольные суммы двоичных данных там, где они критичны: размер в байтах по крупным полям хранилищ.
Сверка по таким величинам занимает столько же времени, сколько счёт строк, и ловит то, что счёт строк пропускает.
Шаг 5. Код возврата не отвечает за данные
Отдельным пунктом, потому что об это спотыкаются все.
Код возврата 0 у ibcmd infobase replicate означает, что процедура дошла до конца. Про сохранность данных он не говорит ничего.
Предупреждение о непереносимых значениях уходит в stderr, и в нашем прогоне это была ровно одна строка на 208 байт. Проблема не в том, что её трудно заметить в файле, а в том, что её обычно вообще не читают: смотрят на код возврата и на счётчики строк.
Практика простая: перенаправлять stderr в отдельный файл и открывать его всегда, даже когда команда отчиталась успехом. Пустой файл это результат, непустой - повод разобраться до того, как базу отдали пользователям.
Развилка, на которой обычно сворачивают не туда
Кроме дат и индексов в том же прогоне встретилось несколько вещей, каждая из которых выглядела понятной и оказалась не тем.
Усадка базы втрое. Стенд: 22 ГБ на PostgreSQL превратились в 7,9 ГБ на MS SQL. Соблазн написать "MS SQL втрое компактнее" сильный и неверный: полезного содержимого в двоичных полях базы 1,44 ГБ, остальное раздутие хранилища на источнике. Кластер сняли физическим бэкапом с продуктива, и обслуживания на нём давно не было.
Вычитать эти числа друг из друга бесполезно, баланс не сойдётся: 22 ГБ это физический размер источника вместе с раздутием, 1,44 ГБ - полезное содержимое двоичных полей, 7,9 ГБ - занятое место в приёмнике. Три разные линейки. Вывод из них ровно один: усадка это чистка раздутия источника, а свойством приёмника она не является, и планировать место по объёму источника нельзя ни в ту, ни в другую сторону.
Проверка перенесённой базы пакетным запуском. Убедиться, что база жива, хотелось автоматически, из скрипта. Конфигуратор в пакетном режиме без подавления стартовых диалогов показывает окно логина и встаёт колом: скрипт висит, пока его не снимут руками. С подавлением диалогов он же отвечает "Пользователь ИБ не идентифицирован", и это читается как приговор перенесённой базе.
База целая, и та же учётная запись заходит в неё обычным клиентом с первого раза. Для автоматической приёмки не годится ни один из двух вариантов, а вывод из обоих одинаково ложный.
Рядом та же грабля у ibcmd config save: команда просит аутентификацию в базе, а когда вывод перенаправлен в файл, печатает приглашение "Имя пользователя:" в бесконечном цикле. За десять минут вышел файл на 106 МБ из одной повторяющейся строки. Перенаправлять вывод у команд, которые могут спросить пароль, нельзя.
Пустой список пользователей и БСП. Отдельная история, уже после переезда. Если в базе не осталось ни одного пользователя информационной базы, платформа пускает в неё свободно, а библиотека на первом же запуске отказывает: "не осталось бы ни одного пользователя с административными правами" при том, что права стоят и видны глазами. В коде видно почему: перед проверкой она вычёркивает текущего пользователя из списка администраторов, потому что ему как раз переписывают роли. Единственный админ после вычёркивания даёт пустой список. Лечится вторым пользователем с полными правами. Правка первого не помогает.
Порядок действий целиком
- До переноса. Снять контрольный срез по шагу 4 на источнике. Проверить даты по шагу 2 с тем смещением, которое поставите приёмнику. Посчитать ширину ключей по шагу 3 с той версией MS SQL, которая будет приёмником.
- Решить про смещение. Если проверка дат нашла значения - ставить 2000. Если по каким-то причинам нужен 0, чинить данные до переезда.
- Решить про индексы. Ключи шире предела укоротить в конфигурации либо снять с реквизитов индексирование. Если широкие ключи нашлись только в служебных таблицах платформы, вариантов два: обновить платформу или взять приёмник посвежее.
- Перенос. Перенаправить
stderrв файл. Проверить его сразу после завершения, не глядя на код возврата. - После переноса. Повторить контрольный срез и сравнить величины из шага 4.
Что с этим делать, если считать руками не хочется
Шаги 2 и 3 я собрал в обработку, она обходит структуру хранения и выдаёт результат деревом: вопрос человеческим языком, вердикт и что делать. Разбор ключа с главным виновником по байтам там же, поэтому видно не "у вас широкий индекс", а "реквизит Идентификатор даёт 2058 байт из 2167" (это УТ 11.5).
К СУБД обработка не подключается, в базу ничего не пишет, перенос не запускает. Карточка: Проверка базы 1С перед переносом на MS SQL Server
Встречное направление, проверка перед переездом на PostgreSQL, разбиралась в отдельной паре материалов: Проверка базы перед миграцией на PostgreSQL
Другие наши инструменты диагностики 1С:
- Проверка базы перед миграцией на PostgreSQL - встречное направление, что сломается при уходе с MS SQL.
- Чек-ап СУБД под 1С - правильно ли СУБД настроена под платформу.
- Карта объёмов базы 1С - из чего состоит база и куда ушли гигабайты.
- Журнал транзакций переполнен - кто его держит и что с этим делать.
Коротко
- Код возврата 0 и совпавшие счётчики строк не доказывают, что данные перенесены верно.
- При смещении дат 0 всё раньше 1753 года схлопывается в одну дату. В обычной типовой базе таких значений десятки тысяч.
- Индексы с ключом шире предела MS SQL создаются без ошибки и падают позже, на первой длинной записи.
- Ширину ключа считать по колонкам хранения. Дата занимает 6 байт, а не 8.
- Проверять надо до переноса: после него испорченная база выглядит исправной.
А вы чем проверяете, что перенесли всё?
Вступайте в нашу телеграмм-группу Инфостарт