Как неразрывный пробел мешает миграции MS SQL → PostgreSQL на Windows Server

13.08.26

База данных - Администрирование СУБД

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

Мы переносили боевую базу на 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 да
и три точки да
и 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 - неважно.

 

Что делать перед своим переездом

  1. Прогоните скрипт выше на копии боевой базы. Несколько минут. Если он вернул пусто, вы избавились от главного риска.
  2. Если что-то нашлось, чините всё до последней строки. Частичная починка не работает, проверено пятьюдесятью потерянными минутами.
  3. Решите, что делать, если обе строки настоящие. У меня был служебный регистр, и лишние строки просто удалились. Но если это два разных контрагента, у которых наименования различаются неразрывным пробелом, удалять нельзя ни одного. Тогда остаётся нормализовать значение у одного из них штатно, средствами платформы - перезаписью элемента, а не правкой в СУБД, - и разводить их так, чтобы они отличались чем-то, кроме невидимого символа.
  4. Перепроверьте непосредственно перед окном. Данные меняются каждый день, вчерашняя чистота ничего не гарантирует.
  5. Посмотрите коллацию источника. Если она *_CS_* или *_BIN*, добавьте к проверке поиск пар, различающихся только регистром.
  6. Считайте место с запасом 15 %. По размеру старой базы планировать нельзя.
  7. Во время переноса смотрите на размер целевой базы. 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С:

Вступайте в нашу телеграмм-группу Инфостарт

PostgreSQL переезд на PostgreSQL миграция с MS SQL ibcmd infobase replicate уникальные индексы ICU неразрывный пробел mchar mvarchar Postgres Pro импортозамещение перенос базы 1С большие базы администрирование 1С нормализация Unicode коллации дубли строк окно переключения

Вы можете заказать платную адаптацию этой статьи под ваши задачи на «Бирже заказов».

  • 0% комиссии — оплата напрямую исполнителю;
  • Исполнители любого масштаба — от отдельных специалистов до команд под проект;
  • Прямой обмен контактами между заказчиком и исполнителем;
  • Безопасная сделка — при необходимости;
  • Рейтинги, кейсы и прозрачная система откликов.

См. также

Администрирование СУБД Системный администратор Программист Бесплатно (free)

5,5 тысячи пользователей в единой базе 1С, розница в режиме 24/7 и SLA 99,98% – в таких условиях любая авария быстро превращается в очереди на кассах, потерю денег и давление со стороны бизнеса. Показываем, как выстроить процесс аварийно-восстановительных работ: от первых алертов и базового скрининга системы до подключения команды, проверки гипотез и дебрифа после инцидента. Разбираем, как метрики, дашборды, техжурнал, Zabbix, Prometheus, Grafana, Telegram-боты и скрипты помогают не гадать, а быстро находить причину проблемы. На реальных авариях объясняем, почему «быстро» не должно означать «рискованно», как работа над ошибками снижает панику и почему каждая авария может сделать систему надежнее.

11.08.2026    763    jul.dolganova    8    

17

Облачные сервисы, хостинг Сервера Нейросети Программист Бесплатно (free)

В последние годы спор «облако или локалка» стал одним из самых горячих в мире работы с нейросетями. Давайте разберемся.

03.08.2026    2126    dsdred    48    

10

Администрирование СУБД 1С 8.3 1С:ERP Управление предприятием 2 Бесплатно (free)

База 1С:ERP размером 646 Гб, полное маскирование за 5 часов - без создания промежуточной незащищенной копии. Разбираем бесплатный pg_anon на сквозном примере с реального продуктива.

27.07.2026    2124    Tantor    16    

13

HighLoad оптимизация Администрирование СУБД Программист Россия Бесплатно (free)

Если вы работаете с 1С на PostgreSQL и жалуетесь на тормоза — скорее всего, дело в join predicate pushdown, которого в стандартном PostgreSQL нет. В MS SQL Server этот механизм работает «из коробки», и при миграции именно запросы к виртуальным таблицам 1С бьют по производительности сильнее всего. В этой статье — реальный кейс от Postgres Professional с разбором плана выполнения, ручным экспериментом и доработкой планировщика СУБД, которая ускорила запросы от 22 до 54 000 раз.

16.06.2026    8314    postgres_professional    13    

12

HighLoad оптимизация Администрирование СУБД Системный администратор Программист 1С:Предприятие 8 Бесплатно (free)

Вышел релиз СУБД Tantor Postgres 18, и мы хотим рассказать о его новых возможностях для работы с приложениями на платформе "1С:Предприятие". В обзоре разберем улучшения планировщика, по традиции коснемся работы временных таблиц и не обойдем вниманием вспомогательные утилиты, которые упрощают поиск и диагностику проблем в высоконагруженных системах. За каждым пунктом - реальные запросы 1С, реальные рабочие базы и сотни часов тестирования!

16.06.2026    3058    Tantor    7    

10

Администрирование СУБД Системный администратор Программист 1С:Предприятие 8 Россия Бесплатно (free)

База 1С за несколько лет эксплуатации разрослась, - стала большой, медленно работает, требует много места и времени для копирования и прочего обслуживания. Нужна ли обязательно свертка или можно обойтись более «мягкими» средствами. Делюсь своим опытном как для новых конфигураций, так и для старых УПП, УТ 10…

01.06.2026    7762    2ncom    30    

11

Сервера Системный администратор Программист 1С:Предприятие 8 Бесплатно (free)

Пошаговый опыт миграции файловой базы 1С УНФ 1.6 с арендованного сервера на собственную железку в частный дом. Разбираем: сборку ПК за 50 тыс. руб., нюансы публикации базы через IIS на Windows 11, решение проблем с правами доступа и настройку проброса портов на роутере TP-Link, а так же настройка ежедневного бекапа c архивацией в облако встроенными средствами Windows. Итог — быстрая работа через веб-клиент без арендных платежей и кошмаров о потере данных

12.05.2026    2516    war41k    34    

11

Администрирование СУБД Системный администратор Программист Бесплатно (free)

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

20.04.2026    8786    berserg    12    

27
Комментарии
Подписаться на ответы Инфостарт бот Сортировка: Древо развёрнутое
Свернуть все
1. Vasvas05 27 13.08.26 12:57 Сейчас в теме
так, а как поняли где искать ошибку?
Настоящая причина: неразрывный пробел
Я вытащил проблемные строки из целевой базы и стал сравнивать их посимвольно. И увидел вот это:
2. nedomolkov.ivan 93 13.08.26 13:03 Сейчас в теме
(1)
Я вытащил проблемные строки из целевой базы и стал сравнивать их посимвольно. И увидел вот это:


Наводку дал сам PostgreSQL, гадать не пришлось. Когда я наконец долез до его лога, там лежало прямым текстом:
ERROR: could not create unique index "_inforg7834_1" и DETAIL: Key (...)=(...) is duplicated.

Это сразу сузило всё до одной точки. По _inforg7834 находишь конкретный регистр сведений (у меня это оказалась АналитикаУчетаСоответствий), по перечисленным _fld - три поля уникального индекса, а в DETAIL уже лежат сами значения, которые PG считает дублями. На MS SQL те же строки годами жили как уникальные - значит различие в чём-то, что MS SQL видит, а PG нет.

Дальше уже механика: вытащил эти пары строк из целевой базы и погнал посимвольно (через unicode-коды каждого символа). На глаз-то они одинаковые, отличие вылезло только по кодам - 160 против 32.

Главная потеря времени была не в поиске причины, а в том, чтобы вообще добраться до лога: ibcmd ошибку придерживал и молча висел. Как только лог прочитан - причина находится за полчаса.
Для отправки сообщения требуется регистрация/авторизация