Мы переносили боевую базу на 276 ГБ с MS SQL на PostgreSQL. Перенос встал четыре раза подряд, каждый раз через час работы и без единого сообщения об ошибке. Виноват оказался неразрывный пробел в служебном регистре сведений. Ниже вся история: две мои гипотезы, которые не подтвердились, настоящая причина, и почему она найдётся почти у каждого.
Первый прогон
Готовились к переезду по-взрослому: подняли отдельный стенд, восстановили на него копию боевой базы и репетировали переключение на ней. База 276 ГБ, бухгалтерия, 4465 таблиц хранения. План простой: ibcmd infobase replicate напрямую из MS SQL в PostgreSQL, без выгрузки в .dt. На таких объёмах .dt не вариант, его просто не выгрузить.
Команда, чтобы разговор был предметный. Она появилась в платформе с 8.3.23, у нас 8.3.27:
ibcmd infobase replicate --data="E:\1C-ibcmd" ^ --dbms=MSSQLServer --db-server="СЕРВЕР\SQL2022" --db-name=База ^ --db-user=sa --db-pwd=*** ^ --parallelism=6:6 --batch-size=10000 ^ --target-dbms=PostgreSQL --target-db-server="localhost port=5433" ^ --target-db-name=База --target-db-user=postgres --target-db-pwd=***
Стенд: 12 ядер, 15,8 ГБ ОЗУ, база на отдельном диске. Источник MS SQL 2022 Enterprise 16.0.4185.3, коллация Cyrillic_General_CI_AS, приёмник PostgreSQL 17.9-3.1C от 1С на порту 5433. Платформа 1С 8.3.27.1989, конфигурация бухгалтерская. Шесть потоков на чтение, шесть на запись, порция 10 000 строк. Без этих цифр время переноса ниже ничего не значит: на другом железе оно будет другим.
Запустил. Смотрю, как растёт целевая база: 20 ГБ, 40, 60, 73. Процесс живой, память ровная, диск шуршит. Пошёл заниматься другими делами.
Возвращаюсь через сорок минут. База так и 73 ГБ. Процесс ibcmd в списке задач висит, память та же, CPU почти ноль. Ошибки нет. Кода возврата нет. Ничего нет.
Подождал ещё двадцать минут. Тишина. Убил, запустил заново. Через час встало на 81,7 ГБ. Третий прогон - на 73,2 ГБ. Каждый раз с нуля: докатить с места, где встало, нельзя, целевую базу надо сносить и лить заново.
И вот тут я порадовался, что репетируем на копии. Случись это в реальном окне переключения, я бы к утру имел неработающую базу, потраченную ночь и очень неприятный разговор.
Ад, в котором я провёл день
Дальше начинается то, что и заставило меня написать эту статью. Перечислю по пунктам, потому что каждый пункт стоил мне отдельного часа:
- Процесс не завершается. Он не падает, не пишет в stdout, не возвращает код. Он просто стоит. Со стороны неотличимо от «идёт долгая операция».
- Индикаторы врут все. Счётчик CPU у живого
ibcmd, который перекладывает 460 МБ/с, растёт на секунду за пятнадцать минут: он не считает, он перекладывает, считают СУБД по обе стороны. То есть «CPU не растёт» не значит «завис». - Число заполненных таблиц тоже врёт. Смотрел
n_live_tupвpg_stat_user_tablesи видел, что счётчик замер. А он обновляется приANALYZE, которого не было, потому что автовакуум я сам же и выключил на время загрузки. - Честный признак прогресса остался один:
pg_database_size. Всё остальное я теперь проверяю вопросом «а что этот индикатор покажет, если сломается сам».
Полдня ушло на то, чтобы это понять. Потом я полез в логи PostgreSQL, и там наконец нашлось:
ERROR: could not create unique index "_inforg7834_1" DETAIL: Key (_fld728, _fld7835, _fld7836)=(...) is duplicated.
Уникальный индекс на регистре сведений не создаётся, потому что PostgreSQL видит дубли. На MS SQL этот индекс живёт годами и никаких дублей там нет.
А самое интересное лежало в stderr самого ibcmd, одной строкой:
[WARN] Обнаружена ошибка репликации информационной базы, ожидание завершения...
То есть он знает, что упал. И вместо того чтобы завершиться с кодом ошибки, пишет предупреждение в поток, который никто не читает, и уходит ждать. Ждёт он завершения остальных потоков, а те держат открытые курсоры и ждут его. Текст исходной ошибки утилита придерживает до корректного завершения, которого не наступает.
Оговорюсь честно, потому что поведение недетерминированное. Молча виснет он на ошибке приёмника. Позже, уже на Linux, мы поймали ошибку 802 на стороне источника - там ibcmd дописал расшифровку от СУБД в stderr и вернул честный код 1. Так что «он повиснет, я замечу» не работает в обе стороны: смотреть надо оба лога.
Две гипотезы, которые не подтвердились
Прежде чем дойти до правды, я отработал два тезиса, которые кочуют по интернету как главные страшилки переезда на PostgreSQL. Оба оказались неверны, и я считаю важным сказать это прямо, потому что на них тратят время.
Страшилка первая: mvarchar регистронезависим, и вас ждут дубли
С самим фактом не спорю, он верный. Кривой из него делают вывод. Строки 1С в PostgreSQL кладёт в типы mchar и mvarchar, и они действительно сравнивают без учёта регистра. Проверяем выполнением:
SELECT ('ABC')::mvarchar = ('abc')::mvarchar AS case_insensitive; -- true
Значит если на MS SQL база стоит на регистрозависимой коллации (*_CS_* или *_BIN*), то значения, различающиеся только регистром, после переезда станут дублями. Риск настоящий, но оговорюсь: 1С штатно требует для MS SQL регистронезависимую коллацию, так что база на _CS_ или _BIN - это уже нештатная конфигурация, и вопросы к ней есть и без всякого переезда.
Но почти все базы 1С стоят на Cyrillic_General_CI_AS, то есть уже регистронезависимой. Там поведение совпадёт и ничего не сломается. Наша база была именно такой, и регистр не был причиной ни одного из четырёх отказов.
Страшилка вторая: лимит ключа индекса в btree
Второй ходовой тезис: в PostgreSQL ключ btree ограничен 2704 байтами, а в 1С длинные строковые измерения регистров сведений легко за него выходят. Я поверил, полез мерить и получил обратное.
| СУБД | Лимит ключа индекса |
|---|---|
| MS SQL, кластерный | 900 байт |
| MS SQL, некластерный | 1700 байт (с версии 2016; на 2012 и 2014 тоже 900) |
| PostgreSQL btree | 2704 байта |
Получается, PostgreSQL поднимает потолок втрое. Страшилка работает ровно наоборот.
Дальше стало ещё интереснее. Я взял самый широкий по объявленной длине индекс - измерение nvarchar(1024), то есть 2048 объявленных байт, - и померил фактический максимум по всем 11 902 записям регистра. Получилось 72 байта. В двадцать восемь раз короче объявленного.
Тут же вылезло противоречие, которое надо снять сразу, иначе таблица выше выглядит как ошибка. В той же базе спокойно живут кластерные индексы на 2172 байта, хотя лимит кластерного - 900. Так и должно быть: SQL Server проверяет объявленную длину только предупреждением при создании, а жёстко режет по фактической длине строки при вставке. Пока реальные значения короткие, индекс работает. У PostgreSQL то же самое: 2704 байта проверяются на фактическом кортеже, и цифра эта - для стандартной страницы 8 КБ.
Вывод простой: мерьте фактические длины. По объявленным вы себе напридумываете переделку метаданных на неделю там, где проблемы нет вообще. Единственное, что стоит посмотреть, это индексы, которые уже сейчас нарушают лимит MS SQL: после переезда они станут легальными, и это чистый выигрыш от миграции.
Настоящая причина: неразрывный пробел
Я вытащил проблемные строки из целевой базы и стал сравнивать их посимвольно. И увидел вот это:
SELECT ('a b')::mvarchar = ('a' || chr(160) || 'b')::mvarchar AS nbsp_vs_space; -- true
Код 160 это неразрывный пробел. Для MS SQL это отдельный символ, и две строки разные. Для PostgreSQL, который сравнивает mvarchar через библиотеку ICU, они одинаковые.
Виновником оказался служебный регистр сведений АналитикаУчетаСоответствий: 436 604 строки, уникальный индекс по трём полям - разделитель данных numeric, строка nvarchar(450) и строка nvarchar(10). Никакой номенклатуры, обычная служебная аналитика, куда значения приезжают из документов. Таких групп в нём нашлось 755. Кто-то годами копировал текст из Excel и с сайтов поставщиков, неразрывные пробелы ехали вместе с ним, и никто ничего не замечал: на MS SQL уникальный индекс строился без проблем.
Тут стоит сказать одну вещь, которая мне самому была неочевидна: MS SQL тоже кое-что схлопывает, просто другое. По той же колонке различных значений оказалось 379 266 при сравнении по коллации и 380 432 при побайтовом сравнении. То есть источник не «эталон различимости», у него свои правила, они просто не совпадают с ICU.
Частичная починка не работает. Совсем
Я вычистил 745 групп из 755 - нормализация на стороне MS SQL поймала не все - и запустил заново. Через пятьдесят минут перенос встал ровно там же.
Про арифметику, чтобы потом не сходилось в уме криво. 745 групп это 745 строк, по одной лишней на группу. В оставшихся десяти группах строк было 14. Итого 759 строк, и столько же лежит в таблице-бэкапе, которую я снял перед удалением.
Вот это и есть главный урок за тот день. Одна оставшаяся пара роняет создание индекса так же надёжно, как семьсот сорок пять. Нет никакой «частичной готовности»: либо ноль проблемных строк, либо перенос не поедет. Значит проверка обязана выдавать полный список строк к исправлению. «Найдены проблемы, вот пара примеров» тут бесполезно.
Второй символ: знак номера
Оставшиеся десять групп, а в них 14 строк, я разбирал уже умнее: спросил у самого PostgreSQL, какие символы он схлопывает. Самая частая пара оказалась неожиданной, код 8470 против кода 78:
SELECT ('No5')::mvarchar = (chr(8470) || '5')::mvarchar AS numero_vs_No; -- true SELECT ('N5')::mvarchar = (chr(8470) || '5')::mvarchar AS numero_vs_N; -- false
ICU считает знак номера № равным сочетанию No. Но не равным одиночной N.
Правило вместо списка символов
Тут я и понял, что список символов это тупик. Прогнал ещё десяток проверок, и картина сложилась:
| Пара | ICU считает равными |
|---|---|
| неразрывный пробел и обычный | да |
№ и No |
да |
™ и TM |
да |
… и три точки |
да |
m² и m2 |
да |
½ и 1/2 |
нет |
]12; и (1) |
нет |
]12; и 1 |
да |
лигатура @257; и fi |
да |
| кавычки-ёлочки и обычные | нет |
е и ё |
нет |
| табуляция и пробел | нет |
Набор не случайный, но объясняется он не так, как я сначала решил. Первая моя версия звучала красиво: «ICU схлопывает всё, у чего есть разложение по NFKD». Красиво и неверно. Разложение есть и у ½ (это 1, дробная косая, 2), и у ]12; (это просто 1), и у ё (это е с диакритикой). А у zero-width space и BOM, которые я как раз нормализую в скрипте, разложения нет вовсе.
Работает другое. mvarchar сравнивается ICU на вторичной силе: регистр и третичные различия игнорируются, диакритика учитывается. Совместимые варианты - №, ™, …, надстрочные цифры, неразрывный пробел - отличаются от своих базовых форм ровно третичным весом, поэтому и слипаются. ё отличается от е диакритикой, то есть вторичным весом, поэтому расходится. А ½ и ]12; расходятся с 1/2 и (1) просто потому, что их разложения этим строкам не равны: у ½ внутри дробная косая U+2044, а не обычный слэш.
И вот ловушка, на которую я чуть не наступил, когда писал первую версию этого раздела. ]12; с одиночной 1 как раз схлопнется: разложение ]12; - это ровно 1. Я успел записать кружочки в безопасные, и хорошо, что перепроверил.
Вывод из этого неприятный: опасных пар больше, чем шесть. Лигатура @257; против fi, полноширинная A313; против A, римская U55; против XII - всё это слипнется тоже. Так что список символов в скрипте ниже честнее считать набором самых частых случаев. На правило он не тянет.
Как это искать до переезда
Скрипт ниже обходит все уникальные индексы базы, у которых есть строковые колонки в ключе, нормализует значения и ищет группы, которые после нормализации становятся дублями.
Про время сразу, чтобы не было сюрприза. На копии 355 ГБ без нагрузки прогон занимает около четырёх минут. На боевом сервере той же конфигурации, где 631 индекс и 240 миллионов строк, и где скрипт делит диск с работающими людьми, - до трёх часов. Разброс большой, и я специально не выдаю его за закон «время зависит от строк, а не от гигабайтов»: копия - это побайтовая копия того же самого прода, гигабайты и строки там одинаковые. Разница только в двух вещах: на продуктиве скрипт делит диск и процессор с работающими людьми, и боевую цифру я снимал ещё с прежней, более тяжёлой версии скрипта. После починки работы стало меньше, но на проде я не перемерял, так что планируйте по верхней границе и гоняйте на копии или в тихое окно.
SET NOCOUNT ON; SET QUOTED_IDENTIFIER ON; -- нужен для .value() по XML: в SSMS включён, в sqlcmd нет DECLARE @tbl sysname, @idx sysname, @cols nvarchar(max); DECLARE @proj nvarchar(max), @sql nvarchar(max); DECLARE @res TABLE (tbl sysname, idx sysname, dup_groups int, extra_rows int); DECLARE cur CURSOR FAST_FORWARD FOR SELECT o.name, i.name, -- колонки ключа как есть: по ним группируем STUFF((SELECT ',' + QUOTENAME(c.name) FROM sys.index_columns ic JOIN sys.columns c ON c.object_id = ic.object_id AND c.column_id = ic.column_id WHERE ic.object_id = i.object_id AND ic.index_id = i.index_id AND ic.is_included_column = 0 ORDER BY ic.key_ordinal FOR XML PATH(''), TYPE).value('.', 'nvarchar(max)'), 1, 1, ''), -- проекция: REPLACE вешаем ТОЛЬКО на строковые колонки ключа, -- остальные пропускаем как есть (почему - в абзаце под скриптом) STUFF((SELECT ',' + CASE WHEN t.name IN ('nvarchar','varchar','nchar','char') THEN 'REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(' + QUOTENAME(c.name) + ',NCHAR(160),N'' ''),NCHAR(8203),N''''),NCHAR(65279),N'''')' + ',NCHAR(8470),N''No''),NCHAR(8482),N''TM''),NCHAR(8230),N''...'')' ELSE QUOTENAME(c.name) END + ' AS ' + QUOTENAME(c.name) FROM sys.index_columns ic JOIN sys.columns c ON c.object_id = ic.object_id AND c.column_id = ic.column_id JOIN sys.types t ON t.user_type_id = c.user_type_id WHERE ic.object_id = i.object_id AND ic.index_id = i.index_id AND ic.is_included_column = 0 ORDER BY ic.key_ordinal FOR XML PATH(''), TYPE).value('.', 'nvarchar(max)'), 1, 1, '') FROM sys.indexes i JOIN sys.objects o ON o.object_id = i.object_id AND o.type_desc = 'USER_TABLE' WHERE i.is_unique = 1 AND EXISTS (SELECT 1 FROM sys.index_columns ic2 JOIN sys.columns c2 ON c2.object_id = ic2.object_id AND c2.column_id = ic2.column_id JOIN sys.types t2 ON t2.user_type_id = c2.user_type_id WHERE ic2.object_id = i.object_id AND ic2.index_id = i.index_id AND ic2.is_included_column = 0 AND t2.name IN ('nvarchar','varchar','nchar','char')) AND EXISTS (SELECT 1 FROM sys.partitions p WHERE p.object_id = o.object_id AND p.index_id IN (0,1) AND p.rows > 0); OPEN cur; FETCH NEXT FROM cur INTO @tbl, @idx, @cols, @proj; WHILE @@FETCH_STATUS = 0 BEGIN -- нормализуем ровно те формы, которые схлопывает ICU: -- 160 неразрывный пробел -> обычный -- 8203 zero-width space -> ничто -- 65279 BOM -> ничто -- 8470 знак номера -> No -- 8482 знак торговой марки-> TM -- 8230 многоточие -> три точки SET @sql = N'SELECT @t, @i, COUNT(*), ISNULL(SUM(cnt),0)-COUNT(*)' + N' FROM (SELECT ' + @cols + N', COUNT(*) AS cnt' + N' FROM (SELECT ' + @proj + N' FROM ' + QUOTENAME(@tbl) + N') AS n' + N' GROUP BY ' + @cols + N' HAVING COUNT(*) > 1) AS g'; BEGIN TRY INSERT INTO @res EXEC sp_executesql @sql, N'@t sysname, @i sysname', @t = @tbl, @i = @idx; END TRY BEGIN CATCH -- вычисляемые колонки и экзотические типы пропускаем молча END CATCH FETCH NEXT FROM cur INTO @tbl, @idx, @cols, @proj; END CLOSE cur; DEALLOCATE cur; SELECT tbl AS storage_table, idx AS index_name, dup_groups, extra_rows FROM @res WHERE dup_groups > 0 ORDER BY extra_rows DESC;
Почему REPLACE висит только на строковых колонках. Первая версия скрипта заворачивала в нормализацию все колонки ключа подряд, включая ссылочные binary(16) и даты. Выглядит безобидно, а SQL Server неявно приводит их к строке, и под коллацией часть байтов при сравнении игнорируется - разные ключи схлопываются в один. На боевой базе это дало 150 поражённых индексов и 5,45 миллиона строк там, где реально был один индекс и 759 строк. Проверил на копии: по регистру на 16,8 миллиона строк старая версия насчитала 469 532 группы дублей, новая - ноль. Насторожило меня в тех цифрах ровно одно: уникальный индекс, который годами живёт в базе, столько дублей физически содержать не может.
Три часа звучит много ровно до того момента, как перенос встанет ночью. У нас он вставал четыре раза по часу, и это без учёта дня, который ушёл на поиск причины.
Сразу про слабое место. Шесть символов - костыль. Совместимых форм в Unicode сотни, точный ответ даёт только прогон на самой PostgreSQL. Но эти шесть закрывают почти всё, что реально встречается, а четыре минуты против сорванного ночного окна - несопоставимые величины.
Вот кусок вывода как есть, с чистой уже копии. Колонки: таблица, индекс, строк, строковые поля ключа, групп дублей, лишних строк, секунд, статус.
_AccRg14374X1;_AccRg14374_8X1;27393123;_Fld14382;0;0;3;ok _InfoRg15722X1;_InfoRg15722_3X1;16867103;_Fld15725;0;0;80,4;ok _InfoRg15722X1;_InfoRg15722_4X1;16867103;_Fld15725;0;0;78;ok _InfoRg15722X1;_InfoRg15722_5X1;16867103;_Fld15725;0;0;73,2;ok _InfoRg18186X1;_InfoRg18186_1X1;163827;_Fld18187 _Fld18188 _Fld18189 ...;0;0;9,6;ok _Reference95;_Reference95_5;76;_Description;0;0;0;ok
Строк таких 627, по числу непустых уникальных индексов с текстом в ключе. Видно главное про время: 27 миллионов строк с одним строковым полем в ключе проходят за 3 секунды, а 16,8 миллиона - за 80. Дело тут в том, сколько работы даёт группировка по конкретному ключу. Объём сам по себе почти ни о чём не говорит.
Список дублей - половина пользы. Вторая половина, по-моему боL9;льшая, это экран подозрительных строк: в одной нашей таблице их 17 325. Дублями стали далеко не все, но смотреть надо туда.
Пять граблей запуска ibcmd
Это отдельный список, и он стоил мне первого вечера целиком. Ни одна из этих граблей не связана с дублями, но об каждую спотыкаешься до того, как вообще доберёшься до переноса.
| Что видно | В чём дело |
|---|---|
Ошибка разбора параметра: --user=… |
у replicate нет параметров --user и --password. Команда работает на уровне СУБД и в информационную базу не входит вообще |
connection to server at "localhost" port 5432 failed |
в значении localhost port=5433 есть пробел, и PowerShell разбил его на два аргумента. Утилита увидела только localhost и пошла на порт по умолчанию - в соседний ванильный PostgreSQL, который стоял на той же машине |
invalid integer value "5433;" for connection option "port" |
точка с запятой из примера официальной документации ломает разбор. Её надо убрать |
Ошибка СУБД: DATABASE не пригоден для использования |
целевую базу создали обычным CREATE DATABASE, а платформе нужны её расширения: mchar, fulleq, fasttrun |
| целевая база легла на диск C: вместо E: | самая опасная. Платформа создаёт базу из шаблона template0, а в нужное табличное пространство был перенесён только template1. Поймал на 1,7 ГБ; свободных на системном диске было 24 ГБ, то есть через несколько минут машина осталась бы без места |
Из последней родилось правило, которое я теперь выполняю всегда: после старта переноса первым делом смотреть, куда физически легла целевая база. Уходить ждать - потом. Проверка - один запрос:
SELECT d.datname, t.spcname FROM pg_database d JOIN pg_tablespace t ON t.oid = d.dattablespace;
Грабля, из-за которой я опубликовал бы неверную цифру
Отдельно расскажу про ошибку в собственном запросе, потому что она из тех, что проходят выборочную проверку.
Считал размеры и число строк по топ-таблицам. Получил у одного регистра 54 миллиона записей. Цифра правдоподобная, вопросов не вызвала. На самом деле их 18 миллионов.
Механизм: соединение с sys.allocation_units даёт до трёх строк на секцию (IN_ROW_DATA, LOB_DATA, ROW_OVERFLOW_DATA), и SUM(p.rows) умножает количество записей на это число. Коварство в том, что завышение происходит только на таблицах с LOB или переполнением строки. На остальных цифры верные, выборочная проверка проходит, и завышение выглядит правдоподобно.
-- размер считаем через SUM, число строк через MAX SELECT TOP 50 o.name AS storage_table, MAX(p.rows) AS row_cnt, CAST(SUM(a.total_pages) * 8.0 / 1024 / 1024 AS decimal(10,2)) AS size_gb FROM sys.objects o JOIN sys.partitions p ON p.object_id = o.object_id AND p.index_id IN (0,1) JOIN sys.allocation_units a ON a.container_id = p.hobt_id WHERE o.type_desc = 'USER_TABLE' GROUP BY o.name ORDER BY SUM(a.total_pages) DESC;
Две агрегации в одном запросе, и считают они по-разному. Я на этом купился и чуть не опубликовал цифру втрое больше настоящей.
Оговорю честно две вещи, которые в этом запросе остались кривыми. Во-первых, index_id IN (0,1) отсекает некластерные индексы, и в size_gb их страницы не попадают - для базы 1С это легко треть объёма объекта. Во-вторых, на секционированной таблице MAX(p.rows) соврёт. Если вам нужен рабочий инструмент, а не иллюстрация грабли, берите штатное sys.dm_db_partition_stats: там row_count при index_id IN (0,1) и reserved_page_count по всем index_id, и городить sys.allocation_units не нужно вовсе.
Чем всё кончилось
После чистки всех 759 строк перенос поехал. Изменены были только данные: ни настроек, ни версий, ни команды. Значит причина названа верно.
| Показатель | Результат |
|---|---|
| Время переноса 276 ГБ | 2 часа 28 минут |
| Средний темп | 1,86 ГБ/мин |
| Перенесено таблиц | 4460 (из 4465: пять служебных платформа не переносит) |
| Создано индексов | 10 130 |
| Ошибок | ноль |
| Сверка строк по 30 крупнейшим таблицам | 403 717 105, расхождений нет |
И два наблюдения, которых я не ждал.
PostgreSQL занял на 8,9 % больше места. Было 275,6 ГБ, стало 300 ГБ. А планируют переезд обычно по размеру старой базы. Закладывайте запас не меньше 15 %. Иначе окно переезда кончится разбором забитого диска, и переключиться вы в него не успеете.
Схема приёмника не копия схемы источника. В PostgreSQL появились пять таблиц хранения двоичных данных, которых в источнике не было: ibcmd создаёт базу по схеме своей платформы. Чужую схему он не переносит. Переезд это заодно и апгрейд схемы, о чём стоит знать заранее.
На Linux ровно то же самое
Отдельно проверил на втором стенде: Ubuntu 22.04 и Postgres Pro 1С 17.10. Это, к слову, другой продукт, чем PostgreSQL 17.9-3.1C от самой 1С, который стоял на Windows-стенде, - и тем интереснее было сверить. Прогнал те же опыты на свежем кластере с локалью ru_RU.UTF-8. Ответы совпали дословно: неразрывный пробел равен обычному, № равно No, регистр не важен.
ОС тут ни при чём, всё решает ICU, а он одинаковый и там, и там. Значит проверку эту делать надо при любом переезде на PostgreSQL. Уходите вы заодно на Linux или остаётесь на Windows - неважно.
Что делать перед своим переездом
- Прогоните скрипт выше на копии боевой базы. Несколько минут. Если он вернул пусто, вы избавились от главного риска.
- Если что-то нашлось, чините всё до последней строки. Частичная починка не работает, проверено пятьюдесятью потерянными минутами.
- Решите, что делать, если обе строки настоящие. У меня был служебный регистр, и лишние строки просто удалились. Но если это два разных контрагента, у которых наименования различаются неразрывным пробелом, удалять нельзя ни одного. Тогда остаётся нормализовать значение у одного из них штатно, средствами платформы - перезаписью элемента, а не правкой в СУБД, - и разводить их так, чтобы они отличались чем-то, кроме невидимого символа.
- Перепроверьте непосредственно перед окном. Данные меняются каждый день, вчерашняя чистота ничего не гарантирует.
- Посмотрите коллацию источника. Если она
*_CS_*или*_BIN*, добавьте к проверке поиск пар, различающихся только регистром. - Считайте место с запасом 15 %. По размеру старой базы планировать нельзя.
- Во время переноса смотрите на размер целевой базы. CPU процесса и счётчики статистики врут оба, проверено.
Обработка, в которую всё это сложилось
Скрипт выше закрывает главный риск, но перед переездом смотреть надо не только на него. Пока разбирались, набралось полтора десятка проверок, и я собрал их в отдельную обработку.
Что она делает. Первым вопросом спрашивает сценарий переезда: меняется только СУБД или заодно и операционная система. Состав отчёта от этого разный, риски у сценариев не совпадают, а ни один чек-лист, который мне попадался, их не разделяет.
Что проверяет сама, без подключения к СУБД: версию платформы против доступности ibcmd infobase replicate, режим управления блокировкой данных (включая объекты, оставшиеся на автоматических), разделение итогов регистров, длины строковых измерений против лимитов обеих СУБД, неограниченные строки в измерениях и индексируемых реквизитах, полнотекстовый индекс, участие в РИБ и разделение данных. Для сценария с Linux дополнительно: внешние компоненты (разбирает манифест внутри и говорит, есть ли сборка под Linux), шрифты в макетах, тома хранения файлов без заполненного Linux-пути, Windows-пути в константах, список регламентных заданий.
Как устроена работа с СУБД. Обработка к ней не подключается. Она печатает готовый скрипт с подсветкой, вы отдаёте его администратору, а вставленный обратно вывод она разбирает и превращает в вердикты. Так не нужны ни права, ни строка подключения к СУБД из 1С, и службе безопасности объяснять нечего: скрипт только читает системные представления, а запускает его тот, у кого доступ и так есть.
Вердикты четырёх видов: «чинить» (до переезда, иначе он не состоится), «внимание» (разобраться и решить осознанно), «норма», «инфо». По каждому пункту есть развёрнутое пояснение, почему это важно, и полный список затронутых объектов. Примерами тут не отделаешься.
На чём проверялась: платформа 8.3.17, 8.3.20 и 8.3.27; MS SQL 2022; на Windows PostgreSQL 17.9-3.1C от 1С, на Linux Postgres Pro 1С 17.10 под Ubuntu 22.04; конфигурации бухгалтерская (4465 таблиц хранения), ERP и Розница.
Чего она не делает. Не подключается к СУБД и не выполняет скрипты сама. Не читает тексты модулей, поэтому вызовы COM и ЗапуститьПриложение, которые ломаются на Linux, показать не может: по регламентным заданиям она отдаёт список для просмотра глазами. И проверка на невидимые символы приблизительная по своей природе, точный ответ даёт только прогон на самой PostgreSQL.
Карточка обработки на Инфостарте: Проверка базы данных перед миграцией на PostgreSQL.
Что я об этом думаю
Меня в этой истории задело не то, что перенос упал. Данные копились годами, мусор в них неизбежен, и что-нибудь всплывёт всегда. Задело, что он упал беззвучно. Инструмент знал об ошибке, написал о ней в поток, который никто не читает, и встал в ожидание. Час моего времени ушёл не на починку, а на выяснение факта поломки.
И второе. Обе гипотезы, с которых я начал, я взял из чужих статей и обсуждений. Обе звучали убедительно, обе не подтвердились на замерах. Настоящая причина не упоминалась нигде и нашлась только потому, что я полез сравнивать строки посимвольно. Так что готовитесь к переезду - потратьте час на прогон по своей копии. Толку будет больше, чем от десятка чеклистов, мой включая.
Другие наши инструменты диагностики 1С:
- Карта объёмов базы 1С - из чего состоит база и куда ушли гигабайты.
- Чек-ап СУБД под 1С - правильно ли СУБД настроена под платформу.
- Трансформатор SQL в 1С - что на самом деле делает конкретный запрос.
- Оптимизатор временных таблиц - как переписать то, что нашли.
Вступайте в нашу телеграмм-группу Инфостарт