Перейти к содержимому
Tez Base

Миграция 1С на PostgreSQL: как проверить базу до переезда и не потерять ночное окно

· Опубликовано: · Обновлено:

PostgreSQL SQL Server и PostgreSQL

Миграция 1С с MS SQL на PostgreSQL выглядит на бумаге как техническая формальность: одна команда платформы перекладывает базу из одной СУБД в другую. На практике перенос боевой базы 276 ГБ вставал у нас четыре раза подряд, всякий раз примерно после часа работы и молча, без сообщения об ошибке. Процесс просто останавливался и висел, неотличимо от долгой операции.

Причина в итоге оказалась размером в один невидимый символ: неразрывный пробел в служебном регистре сведений. А точнее, в том, что PostgreSQL сравнивает строки не так, как MS SQL. Ниже полный разбор: почему перенос молчит вместо того чтобы упасть, две расхожие страшилки, которые не подтвердились на замерах, настоящая причина, готовый SQL-скрипт для поиска проблемных строк до переезда, и что закладывать в план миграции.

Это развёрнутая техническая версия. Короткий детективный разбор той же истории у нас опубликован на Инфостарте; здесь всё целиком, с полным кодом скрипта и граблями, которые в кейс не поместились.

Материал на данных одной инсталляции: клиент-серверная 1С, бухгалтерская конфигурация, 4465 таблиц хранения, база 276 ГБ. Репетировали переключение на копии прода, поднятой на отдельном стенде. Источник MS SQL 2022, коллация Cyrillic_General_CI_AS, приёмник PostgreSQL 17.9-3.1C от 1С. Платформа 8.3.27.

Как выглядел перенос

Подготовка была серьёзной: отдельный стенд, на нём восстановленная копия боевой базы, и переключение мы репетировали именно там. План простой: никакого .dt (на таких объёмах его просто не выгрузить), перенос сразу из MS SQL в PostgreSQL:

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=***

Команда ibcmd infobase replicate есть в платформе начиная с 8.3.23, она переносит базу на уровне СУБД, без промежуточного файла. После запуска я следил за ростом целевой базы: 20 ГБ, 40, 60, потом 73. Процесс жив, память стоит ровно, диск работает. Я отвлёкся на другие дела.

Через сорок минут возвращаюсь, а база всё те же 73 ГБ. Процесс висит, память не сдвинулась, CPU около нуля. Ни ошибки, ни кода возврата. Выждал ещё двадцать минут, ничего. Снял процесс и запустил снова: встало через час, на 81,7 ГБ, а третий прогон остановился на 73,2 ГБ. Докатить с точки остановки нельзя, каждый прогон идёт с нуля: целевую базу сносишь и льёшь заново.

Вот когда я оценил, что это репетиция на копии. В настоящем окне переключения к утру у меня была бы мёртвая база, пропавшая ночь и тяжёлый разговор. Про сам подход мы писали отдельно: тяжёлые работы с базой мы прогоняем полностью на копии до боевой ночи, иногда по многу раз - “Семь репетиций одной ночи”.

Почему перенос молчит вместо того чтобы упасть

День ушёл на то, чтобы понять простую вещь: все привычные индикаторы прогресса в этой операции врут.

  • 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] Обнаружена ошибка репликации информационной базы, ожидание завершения...

Утилита знает, что упала. Но с кодом ошибки она не выходит: пишет предупреждение в stderr, куда никто не заглядывает, и садится ждать. Ждёт окончания остальных потоков, у которых открыты курсоры, и те в свою очередь ждут её. Исходную ошибку она покажет только при корректном завершении, а оно так и не наступает.

Оговорюсь честно, поведение недетерминированное. Молча ibcmd виснет при ошибке приёмника. Когда позже, уже на Linux, ошибка 802 случилась на стороне источника, утилита добавила в stderr расшифровку от СУБД и вернула честный код 1. Смотреть надо оба лога.

Настоящая причина: невидимый символ и ICU

Проблемные строки я выгрузил из целевой базы и сравнил символ за символом, по unicode-коду каждого. И увидел вот это:

SELECT ('a b')::mvarchar = ('a' || chr(160) || 'b')::mvarchar AS nbsp_vs_space;
-- true

160 - код неразрывного пробела. MS SQL считает его отдельным символом, и строки для него разные. А PostgreSQL сравнивает mvarchar через библиотеку ICU, и для него это одно и то же.

Виноват был служебный регистр сведений АналитикаУчетаСоответствий на 436 604 строки с уникальным индексом по трём полям. Номенклатура ни при чём, это рядовая служебная аналитика, значения в неё идут из документов. Групп, схлопывающихся под ICU, в нём нашлось 755. Неразрывные пробелы годами приезжали вместе с текстом, скопированным из Excel и со страниц поставщиков, и никто их не замечал: MS SQL спокойно строил уникальный индекс. Тот же символ 160 1С умеет вставлять и сама, когда превращает число в строку функцией Строка(), и там он рвёт уже сопоставление ключей при загрузке: это разобрано в статье про неразрывный пробел в ключах.

Тут есть неочевидная деталь: MS SQL тоже умеет схлопывать, только другое. Если сравнивать по коллации, в той же колонке 379 266 различных значений, а если побайтово, то 380 432. Эталоном различимости источник не служит, правила у него собственные и с ICU просто не совпадают.

Частичная починка не работает совсем

Из 755 групп я вычистил 745, нормализация в MS SQL отловила не все. Перезапуск, пятьдесят минут, и перенос встал на том же самом месте.

Разберу арифметику сразу, иначе в уме она потом сложится криво. На 745 групп приходится 745 лишних строк, по одной на каждую. Ещё 14 строк сидело в оставшихся десяти группах. Всего получается 759 строк, и в таблице-бэкапе, снятой до удаления, их столько же.

Главный урок того дня как раз в этом. Индекс не создастся из-за одной уцелевшей пары так же, как из-за семисот сорока пяти. Частичной готовности не бывает: пока проблемных строк больше нуля, перенос не поедет. Поэтому проверка должна отдавать все строки, которые надо исправить. Ответ в духе “нашли проблемы, вот пара примеров” здесь ничего не даёт.

Две страшилки, которые не подтвердились

До правды я дошёл не сразу: сначала проверил два тезиса, которые гуляют по обсуждениям миграции на PostgreSQL как главные риски. Для нашей базы оба не подтвердились, и говорю об этом прямо, ведь на них уходят время и деньги.

Страшилка первая: mvarchar регистронезависим, и вас ждут дубли

Сам факт верный, тут спорить не о чем. 1С хранит строки в PostgreSQL в типах mchar и mvarchar, а они сравнивают без учёта регистра:

SELECT ('ABC')::mvarchar = ('abc')::mvarchar AS case_insensitive;
-- true

Отсюда следствие: если база на MS SQL живёт на регистрозависимой коллации (*_CS_* или *_BIN*), значения, отличающиеся одним регистром, после переезда превратятся в дубли. Риск реальный, но есть оговорка. Для MS SQL 1С штатно требует регистронезависимую коллацию, поэтому база на _CS_ или _BIN уже сама по себе нештатная, и к ней есть вопросы безо всякого переезда.

На практике базы 1С почти всегда стоят на Cyrillic_General_CI_AS, а она и без того регистронезависима: поведение совпадёт, ломаться нечему. Наша база как раз из таких, и ни один из четырёх отказов не случился из-за регистра.

Страшилка вторая: лимит ключа индекса в btree

Ещё один ходовой тезис: предел ключа btree в PostgreSQL 2704 байта, и длинные строковые измерения регистров сведений 1С его легко превышают. Поверил и пошёл мерить, а вышло наоборот:

СУБДЛимит ключа индекса
MS SQL, кластерный900 байт
MS SQL, некластерный1700 байт (с версии 2016; на 2012 и 2014 тоже 900)
PostgreSQL btree2704 байта

Потолок у PostgreSQL втрое выше. Страшилка выходит наизнанку.

Дальше интереснее. Среди индексов я выбрал тот, у которого самая большая объявленная длина: измерение nvarchar(1024), 2048 байт на бумаге. Фактический максимум по всем записям регистра оказался 72 байта, то есть меньше объявленного в двадцать восемь раз.

Здесь же обнаружилось противоречие, и его стоит разобрать сразу. Кластерный индекс ограничен 900 байтами, а в этой базе спокойно работают кластерные индексы шириной 2172 байта. Это нормально: на объявленную длину SQL Server при создании только предупреждает, а отказывает жёстко при вставке, по фактической длине строки. Индекс живёт, пока настоящие значения укладываются в предел. PostgreSQL устроен так же: 2704 байта он сверяет с фактическим кортежем.

Отсюда правило: мерьте фактические длины. Если верить объявленным, можно на неделю засесть за переделку метаданных там, где никакой проблемы нет. Отдельно стоит глянуть только на индексы, которые уже сейчас выходят за лимит MS SQL: на PostgreSQL они станут законными, и это прямая выгода от миграции. В обратную сторону те же ключи становятся проблемой: при переносе с PostgreSQL на MS SQL индекс шире 900 байт создаётся с предупреждением и ждёт первой длинной записи.

Правило ICU: почему схлопывается именно это

С оставшимися десятью группами я поступил умнее и спросил сам PostgreSQL, какие символы он считает одинаковыми. Чаще всего встречалась неожиданная пара: код 8470 против сочетания No:

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нет
① и (1)нет
① и 1да
лигатура fi и fiда
кавычки-ёлочки и обычныенет
е и ёнет
табуляция и пробелнет

Случайности в этом наборе нет, только объяснение у него не то, что приходит в голову первым. Первое звучит красиво: “ICU схлопывает всё, что раскладывается по NFKD”. Красиво, но неверно. Между тем ½ раскладывается на 1, дробную косую и 2, ① на обычную 1, а ё на е с диакритикой. Зато у BOM и zero-width space, которые скрипт как раз нормализует, разложения нет совсем.

Работает другое. Для mvarchar ICU берёт вторичную силу сравнения: регистр и третичные различия ей безразличны, а диакритика важна. У совместимых вариантов (№, ™, …, надстрочные цифры, неразрывный пробел) от базовой формы отличается только третичный вес, вот они и слипаются. У ё и е разница в диакритике, а это вторичный вес, так что они расходятся. ½ и ① не совпадают с 1/2 и (1) по простой причине: их разложения дают другие строки. В ½ стоит дробная косая U+2044, обычного слэша там нет.

Неприятное следствие: опасные пары не кончаются на шести. Слипнутся и лигатура fi с fi, и полноширинные латинские буквы с обычными, и римские числа-лигатуры с набором букв. Поэтому шесть символов из скрипта ниже честнее воспринимать как самые частые случаи, а точный ответ по конкретной базе получится только прогоном на самой PostgreSQL.

Скрипт: как найти проблемные строки до переезда

Скрипт проходит по каждому уникальному индексу со строковыми колонками в ключе, приводит значения к частым формам ICU и находит группы, которые после такой нормализации дают дубли. Запускается на MS SQL, к целевой PostgreSQL не обращается, ничего не пишет, только читает.

Сразу про время, чтобы оно не удивило. Копию на 355 ГБ без нагрузки скрипт проходит примерно за четыре минуты. На боевом сервере с той же конфигурацией, где индексов больше, а диск приходится делить с работающими людьми, уходит до трёх часов. Закладывайте верхнюю оценку и запускайте на копии или в спокойное окно.

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 молча превращает их в строку, коллация при сравнении пропускает часть байтов, и разные ключи сливаются в один. На боевой базе вместо одного индекса и 759 строк вышло 5,45 миллиона строк в 150 поражённых индексах.

На копии я сравнил обе версии на регистре в 16,8 миллиона строк: у старой 469 532 группы дублей, у новой ни одной. Меня насторожило в этих цифрах одно: уникальный индекс, живущий в базе годами, физически не может содержать столько дублей. Это хорошая привычка при любой диагностике: спрашивать не “правдоподобна ли цифра”, а “возможна ли она в принципе”.

Починка: проекция собирается из sys.index_columns и sys.types, REPLACE вешается только на nvarchar / varchar / nchar / char. Плюс SET QUOTED_IDENTIFIER ON прямо в скрипте, потому что в sqlcmd он выключен по умолчанию и .value() по XML падает.

Фрагмент вывода без правок, копия уже вычищена. Поля: таблица, индекс, число строк, строковые поля ключа, группы дублей, лишние строки, секунды, статус:

_AccRg14374X1;_AccRg14374_8X1;27393123;_Fld14382;0;0;3;ok
_InfoRg15722X1;_InfoRg15722_3X1;16867103;_Fld15725;0;0;80,4;ok
_InfoRg18186X1;_InfoRg18186_1X1;163827;_Fld18187 _Fld18188 ...;0;0;9,6;ok
_Reference95;_Reference95_5;76;_Description;0;0;0;ok

Главное по времени: 27 миллионов строк, где в ключе одно строковое поле, проходятся за 3 секунды, а на 16,8 миллиона уходит 80. Всё решает, сколько работы требует группировка по конкретному ключу, размер таблицы сам по себе мало что значит. Вторая половина пользы, по-моему бо́льшая: список подозрительных строк. В одной нашей таблице таких 17 325, в дубли превратились не все, но смотреть стоит именно туда.

Что делать, если обе строки настоящие

Удалить лишнее можно не всегда. В моём случае регистр был служебный, и лишние строки ушли без последствий. Другое дело, если под ICU слиплись два разных контрагента, чьи наименования отличаются неразрывным пробелом: тут удалять нельзя ни того, ни другого.

Выход один: штатно, средствами платформы, привести к норме значение одного из них, перезаписав элемент, без правок в СУБД. Развести их надо так, чтобы различались они чем-то видимым, одного невидимого символа мало. Правка данных напрямую в СУБД в обход платформы ломает целостность и в 1С недопустима, поэтому чинить надо на стороне конфигурации.

Пять граблей запуска ibcmd

Отдельный список, не связанный с дублями. Каждая из этих граблей бьёт ещё до начала переноса, и первый вечер уходит на них целиком.

Что видноВ чём дело
Ошибка разбора параметра: --user=...параметров --user и --password у replicate нет: команда действует на уровне СУБД, в информационную базу она не заходит вовсе
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;

Агрегаций в запросе две, и считает каждая по-своему. Если нужен рабочий инструмент, пример с граблей не годится: берите штатное 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 %. Теперь это 300 ГБ вместо прежних 275,6. А место под переезд обычно считают по старой базе. Запас закладывайте минимум 15 %: иначе всё окно переезда уйдёт на разбор переполненного диска, и переключиться в него вы уже не успеете. Отдельные таблицы растут неравномерно: у нас один регистр накопления вырос в 3,6 раза, а один регистр сведений, наоборот, усох. Общие плюс 8,9 % это средняя температура по больнице.

Схема на приёмнике своя, это не копия источника. В PostgreSQL обнаружились пять таблиц для двоичных данных, которых в исходной базе не было: ibcmd строит базу по схеме собственной платформы и чужую схему не тащит. Так что переезд попутно обновляет и схему, это стоит держать в голове.

На Linux ровно то же самое

Проверил и на втором стенде, это Ubuntu 22.04 с Postgres Pro 1С 17.10. Продукт здесь иной: на Windows-стенде стоял PostgreSQL 17.9-3.1C в сборке самой 1С, поэтому сверка была тем интереснее. Опыты повторил на свежем кластере, локаль ru_RU.UTF-8. Всё совпало до буквы: неразрывный пробел равен обычному, № равен No, регистр значения не имеет.

Всё определяет ICU, и на обеих платформах он одинаковый, так что операционная система роли не играет. Поэтому искать невидимые символы нужно при любой миграции 1С на PostgreSQL, и неважно, остаётесь вы на Windows или заодно уходите на Linux.

Сам переезд на Linux при этом добавляет свой набор проблем, к дублям не относящихся: английская локаль кластера, логический том на сотню гигабайт вместо семисот, ноль шрифтов на Ubuntu Server, опасные значения автотюнера --tune=1c. Их мы разобрали отдельно в статье “Перенос 1С на Linux и PostgreSQL: восемь ловушек”. А служебные таблицы, которые вы завели рядом с базой сами, теряют на переезде другое: ключ с необязательной колонкой на MS SQL не пускал вторую строку с пустым полем, а на PostgreSQL пускает. Как найти такие ключи запросом к каталогу и починить, разобрано в статье про уникальный индекс и NULL в PostgreSQL.

Чек-лист перед миграцией 1С на PostgreSQL

  1. Скрипт выше прогоните на копии боевой базы. Это несколько минут. Пустой результат значит, что главного риска у вас нет.
  2. Нашли хоть что-то, чините все строки без остатка. Частично чинить бесполезно, на этом ушло пятьдесят минут впустую.
  3. Решите заранее, что делать, если обе строки настоящие. Служебные строки удаляются, живые справочные разводятся штатной перезаписью, а не правкой в СУБД.
  4. Перепроверьте непосредственно перед окном. Данные меняются ежедневно, вчерашний чистый прогон ничего не обещает.
  5. Посмотрите коллацию источника. При *_CS_* или *_BIN* проверку нужно дополнить поиском пар, которые отличаются только регистром.
  6. Место считайте с запасом в 15 %. Размер старой базы для планирования не годится.
  7. Следите за размером целевой базы, пока идёт перенос. Не верьте ни CPU процесса, ни счётчикам статистики, проверено.
  8. Проверьте версию платформы против доступности ibcmd infobase replicate (команда с 8.3.23) и режим управления блокировкой данных: объекты на автоматических блокировках после переезда ведут себя иначе.
  9. Если в тех же работах меняется и платформа, посчитайте это отдельной задачей: в 8.5.1 подняты минимальные требования к СУБД (PostgreSQL до 9.6 включительно из поддержки вышел), а к ней прицепом идут накопленные изменения конфигурации - разбор в статье “Переход на 1С 8.5: что проверить в конфигурации и на серверах”. Что из этих изменений заденет вашу конфигурацию, проверка готовности к 8.5 показывает ещё на текущей платформе, начиная с 8.3.20. Две большие смены в одно окно мы не советуем.

Обработка, в которую сложились все проверки

Главный риск закрывает скрипт выше, однако перед миграцией невидимыми символами дело не ограничивается. За время разбора накопилось полтора десятка проверок, и мы собрали их в отдельную обработку.

Начинает обработка с вопроса о сценарии: переезжает одна СУБД или вместе с ней и операционная система. От ответа зависит набор проверок в отчёте, ведь риски в этих сценариях разные, а в чек-листах, которые нам встречались, их никто не разводил.

Без подключения к СУБД она проверяет сама: наличие ibcmd infobase replicate в текущей версии платформы, режим управления блокировкой данных вместе с объектами, оставшимися на автоматических, разделение итогов у регистров, укладываются ли строковые измерения в лимиты обеих СУБД, строки неограниченной длины среди измерений и индексируемых реквизитов, участие в РИБ, разделение данных и полнотекстовый индекс. Под Linux добавляются внешние компоненты (обработка вскрывает манифест и сообщает, найдётся ли в нём сборка для Linux), Windows-пути в константах, тома хранения файлов без заданного пути для Linux, шрифты в макетах и список регламентных заданий.

К СУБД обработка не подключается. Она выдаёт готовый скрипт для администратора, а когда его вывод вставят обратно, разбирает результат и раскладывает по четырём вердиктам: “чинить” (без этого переезд не состоится), “внимание” (решать осознанно, разобравшись), “норма” и “инфо”. К каждому пункту идёт подробное пояснение с полным списком затронутых объектов. Проверена на платформах 8.3.27, 8.3.20 и 8.3.17, на MS SQL 2022, на PostgreSQL 17.9-3.1C в сборке 1С для Windows и на Postgres Pro 1С 17.10 для Linux, на бухгалтерской конфигурации, ERP и Рознице.

На Инфостарте у обработки своя карточка: Проверка базы данных перед миграцией на PostgreSQL. Для обратного направления, с PostgreSQL на MS SQL, у нас отдельная проверка перед переносом на MS SQL: там главный риск другой, даты раньше 1753 года, которых тип datetime в MS SQL не держит.

Что в итоге

Сам по себе отказ переноса меня не зацепил. За годы в данных неизбежно копится мусор, и что-нибудь да всплывёт. Зацепило молчание: утилита знала об ошибке, сообщила о ней в stderr, куда никто не смотрит, и замерла в ожидании. Час ушёл только на то, чтобы понять, что поломка вообще случилась.

И ещё. Обе мои стартовые гипотезы я взял из обсуждений и чужих статей. Звучали они убедительно, но замеры не подтвердили ни одну. О настоящей причине не писал никто, и нашёл я её лишь потому, что взялся сравнивать строки символ за символом. Собираетесь переезжать, выделите час и прогоните проверку на своей копии. Пользы от этого больше, чем от десятка чек-листов, включая наш.

Готовые обработки по теме:

Если планируете миграцию на PostgreSQL и не хотите проверять базу вручную, это часть нашей работы по обслуживанию баз 1С: прогон проверок на копии, разбор находок и сопровождение окна переключения.

Могу починить это за вас

Настроим регламентное обслуживание так, чтобы база работала стабильно, бэкапы гарантированно восстанавливались, а диски не кончались внезапно.

Стоимость
450 000 - 600 000 ₸ТЗ и разовая наладка, без налогов; сопровождение - от 150 000 ₸/мес отдельно

Рано писать? Измерьте сами

Переезд на PostgreSQL падает беззвучно

Открыть обработку

INFOSTART TECH EVENT 2026: конференция для 1с-специалистов. если собираетесь, регистрируйтесь по нашей ссылке. Регистрация →

Читайте также

Перенос базы 1С с PostgreSQL 18 на 17: восстановление по сети клиентом старшей версии

pg_restore версии 17 архив версии 18 не читает, а обновлять приёмник нельзя или некогда. Рабочий путь: запустить pg_restore 18 на источнике и направить его по сети в сервер 17. Методика целиком: какие пути тупиковые и почему, настройка доступа на приёмнике, выгрузка и загрузка в четыре потока, SQL-сверка таблиц и индексов, почему база после переноса меньше и как считать окно работ по одной таблице. С замерами на двух базах 1С.

Уникальный индекс в PostgreSQL пропускает дубли с NULL: найти, посчитать, починить

Методика для служебных таблиц рядом с базой 1С: один запрос к каталогу PostgreSQL находит все уникальные ключи с необязательными колонками, второй считает, какая доля строк идёт мимо защиты. Дальше ремонт по версиям: NULLS NOT DISTINCT с 15-й, частичные индексы до неё, и проверка повтором в транзакции с откатом. Все запросы прогнаны на PostgreSQL 17.

Персональные данные в базе 1С: карта мест перед тем, как копия уедет наружу

Копию рабочей базы отдают подрядчику, тестировщику или на свой тестовый сервер, и первым делом спрашивают, что в ней надо спрятать. Список обычно заканчивается на справочнике физлиц. На одной рабочей базе карта нашла 164 объекта с персональными данными. Разбираем, где они лежат, почему поиск по имени реквизита промахивается в обе стороны и как проверить копию до отправки.