PostgreSQL 수동 마이그레이션

Mattermost v11.11번역일: 2026-09-02원본 보기

MySQL 데이터베이스를 PostgreSQL로 수동 마이그레이션하는 과정은 다음 단계로 이루어집니다.

팁

협업형 플레이북과 Boards를 마이그레이션하는 자세한 방법은 플러그인 마이그레이션 섹션을 참고하세요.

수동 업그레이드가 적절한 방법인지 확신이 서지 않나요? 배포 환경에 맞춘 안내가 필요한 Mattermost 고객은 Mattermost 전문가에게 문의할 수 있습니다.

권장 도구

Postgres로 직접 마이그레이션하려는 경우, 마이그레이션 과정에 다음 도구를 권장합니다.

이 문서에서는 각 도구의 설치 방법을 안내한 뒤 데이터베이스 마이그레이션을 진행합니다.

필요한 도구를 설치했다면 시스템 요구 사항 및 구성 문서를 검토하고, 마이그레이션을 시작하기 전에 필요한 준비 사항을 확인하세요.

마이그레이션은 대상 데이터베이스 준비로 시작해 데이터 마이그레이션을 진행하고 모든 마이그레이션 후 단계를 완료하는 순서로 진행합니다.

협업형 플레이북과 보드를 마이그레이션하는 자세한 방법은 플러그인 마이그레이션 문서를 참고하세요.

pgloader

MySQL에서 PostgreSQL로 데이터를 마이그레이션하려면 pgloader 도구를 사용합니다.

pgloader 설치

pgloader를 설치하려면 공식 설치 가이드를 참고하세요.

참고

MySQL v8을 사용하는 경우: pgloader로 컴파일된 바이너리의 알려진 버그 때문에 pgloader를 소스에서 직접 컴파일해야 합니다. 여기의 단계를 따라 소스에서 빌드하세요.

또는 pgloader를 설치하거나 빌드하지 않으려면 Mattermost의 mattermost-pgloader Docker 이미지를 사용할 수 있습니다. 자세한 내용은 아래 문서를 참고하세요.

pgloader 사용

Docker 이미지 내려받기 및 pgloader 확인

수동 마이그레이션에서는 다음 명령을 실행해 mattermost-pgloader 이미지를 내려받고 pgloader가 정상 동작하는지 확인합니다.

docker run -it --rm -v $(pwd):/home/migration mattermost/mattermost-pgloader:latest pgloader --version

이 명령은 mattermost/mattermost-pgloader:latest 이미지를 내려받고 pgloader를 실행해 버전을 확인함으로써 기대대로 동작하는지 검증합니다.

로컬 디렉터리 매핑

-v $(pwd):/home/migration 플래그를 사용해 현재 작업 디렉터리를 Docker 컨테이너에 매핑하세요. 이렇게 하면 로그와 기타 파일을 로컬 디렉터리에 저장할 수 있습니다.

네트워크 구성 설정

네트워크 요구 사항에 맞춰 --network 플래그를 설정하세요. 예를 들어 localhost에 접근하려면 네트워크를 host로 설정해야 합니다.

morph

morph 도구는 PostgreSQL 스키마를 생성합니다.

참고

morph와 dbcmp는 모두 Go 툴체인이 필요합니다. Go 컴파일러를 설치하려면 Go 문서를 따르세요.

morph 설치

다음 명령을 실행해 morph CLI를 설치할 수 있습니다.

go install github.com/mattermost/morph/cmd/morph@v1

dbcmp(선택 사항)

dbcmp 도구를 사용하면 마이그레이션 후 모든 테이블을 비교해 두 스키마 사이에 차이가 있는지 확인할 수 있습니다.

dbcmp 설치

다음 명령을 실행해 dbcmp를 설치할 수 있습니다.

go install github.com/mattermost/dbcmp/cmd/dbcmp@latest

수동 마이그레이션의 시스템 요구 사항 및 구성

수동 마이그레이션을 시작하기 전에 시스템이 원활하고 효율적인 마이그레이션에 필요한 요구 사항을 충족하는지 반드시 확인해야 합니다. 다음 시스템 사양과 조정 사항을 강력히 권장합니다.

  • 시스템 메모리 자원이 충분한지 확인하세요. 기본적으로 16GB RAM을 권장합니다. 시스템 메모리가 부족한 경우 number of workers, prefetch rows, 특히 concurrency가 1보다 크게 설정된 경우의 rows per range 같은 pgloader 설정을 조정할 수 있습니다. 이런 조정은 사용 가능한 시스템 자원에 맞춰 자원 활용을 최적화하는 데 도움이 됩니다. 자세한 내용은 pgloader 문서를 참고하세요.
  • 특히 대용량 데이터셋을 다룰 때는 충분한 처리 성능을 갖춘 멀티코어 프로세서를 권장합니다.
  • MySQL과 PostgreSQL 데이터베이스를 모두 저장하고 마이그레이션 과정에서 생성되는 임시 파일을 담을 디스크 공간이 충분한지 확인하세요. 필요한 디스크 공간은 마이그레이션할 데이터베이스의 크기에 따라 달라집니다.
  • 마이그레이션 시간을 더 줄이려면 마이그레이션을 시작하기 전에 대상 PostgreSQL 데이터베이스의 인덱스를 수동으로 제거할 수도 있습니다. 이 방법은 데이터 삽입 중 인덱스 빌드로 인한 부하를 줄여 마이그레이션 속도를 높일 수 있습니다.

수동 마이그레이션 전 준비

중요

이 가이드는 v7.1 ESR 이상의 스키마를 전제로 합니다. 그보다 낮은 버전을 사용 중이고 마이그레이션을 계획하고 있다면 Mattermost 서버를 최소 v7.1로 업데이트하세요. 자세한 내용은 Extended Support Release 문서를 참고하세요.

  • MySQL 데이터를 백업하세요.
  • Mattermost 버전을 확인하세요. 자세한 내용은 About(정보) 모달에서 확인할 수 있습니다.
  • 마이그레이션 시간대를 계획하세요. 이 과정에서는 마이그레이션 중 Mattermost 서버를 중지해야 합니다.
  • 스키마 간 데이터 호환성을 확인하려면 스키마 차이 섹션을 참고하세요.
  • 데이터베이스와 사용자를 생성해 PostgreSQL 환경을 준비하세요. 자세한 내용은 데이터베이스 문서를 참고하세요.
  • PostgreSQL 최신 버전에서는 새로 만든 사용자가 public 스키마에 접근할 수 없습니다. GRANT ALL ON SCHEMA public to mmuser를 실행해 접근 권한을 명시적으로 부여해야 합니다.

스키마 차이

수동 마이그레이션 전에는 두 스키마의 차이 때문에 오류 없는 마이그레이션을 위해 몇 가지 수동 작업이 필요할 수 있습니다.

text에서 character varying으로

Mattermost MySQL 스키마는 여러 테이블에서 PostgreSQL 스키마의 varchar 대신 text 컬럼 타입을 사용하므로, 크기가 PostgreSQL 스키마의 제한 범위 안에 들어오는지 확인할 것을 권장합니다.

삭제하거나 수정해야 할 항목이 있는지 확인할 수 있습니다. 예를 들어 Audits 테이블의 Action 컬럼을 확인하려면 다음을 실행하세요.

SELECT FROM mattermost.Audits where LENGTH(Action) > 512;

다음 표는 추가적인 영향 없이 진행할 수 있는 삭제 또는 수정 항목을 보여 줍니다.

테이블컬럼데이터 타입 변환삭제 시 영향
AuditsActiontext -> varchar(512)애플리케이션 동작에 부작용 없음(해당 행을 삭제해야 합니다).
AuditsExtraInfotext -> varchar(1024)애플리케이션 동작에 부작용 없음(해당 행을 삭제해야 합니다).
ClusterDiscoveryHostNametext -> varchar(512)애플리케이션 동작에 부작용 없음(해당 행을 삭제해야 합니다).
CommandsIconURLtext -> varchar(1024)해당 필드를 삭제하거나 새 URL로 수정할 수 있습니다.
CommandsAutoCompleteDesctext -> varchar(1024)해당 필드를 삭제하거나 다시 작성할 수 있습니다.
CommandsAutoCompleteHinttext -> varchar(1024)해당 필드를 삭제하거나 다시 작성할 수 있습니다.
RemoteClustersTopicstext -> varchar(512)해당 필드를 제거할 수 있습니다.
SystemsValuetext -> varchar(1024)예외적인 경우로, 이상적으로는 발생하지 않아야 합니다.

다음 표는 스키마가 달라질 수 있고 PostgreSQL 스키마의 데이터 크기 제약이 오류를 유발할 수 있는 여러 사례를 보여 줍니다. 각 테이블·행은 개별 점검이 필요하므로 삭제 시 발생할 수 있는 영향을 함께 표기했습니다.

팁

커뮤니티에서 LinkMetadata와 FileInfo 테이블에 오버플로가 있었다는 보고가 여러 건 있었으므로, 특히 이 테이블들을 확인할 것을 권장합니다. MySQL 스키마의 데이터가 이 제한을 초과하는지 확인하세요.

테이블컬럼데이터 타입 변환삭제 시 영향
CompliancesKeywordstext -> varchar(512)컴플라이언스 필터를 갱신해야 합니다.
CompliancesEmailstext -> varchar(1024)컴플라이언스 필터를 갱신해야 합니다.
FileInfoPathtext -> varchar(512)이 파일을 가리키던 기존 링크가 동작하지 않습니다(해당 행을 삭제해야 합니다).
FileInfoThumbnailPathtext -> varchar(512)이 파일을 가리키던 기존 링크가 동작하지 않습니다(해당 행을 삭제해야 합니다).
FileInfoPreviewPathtext -> varchar(512)이 파일을 가리키던 기존 링크가 동작하지 않습니다(해당 행을 삭제해야 합니다).
FileInfoNametext -> varchar(256)이 파일을 가리키던 기존 링크가 동작하지 않습니다(해당 행을 삭제해야 합니다).
FileInfoMimeTypetext -> varchar(256)이 파일을 가리키던 기존 링크가 동작하지 않습니다(해당 행을 삭제해야 합니다).
LinkMetadataURLtext -> varchar(2048)이 파일을 가리키던 기존 링크가 동작하지 않습니다(해당 행을 삭제해야 합니다).
RemoteClustersSiteURLtext -> varchar(512)기존 원격 클러스터가 제거됩니다(해당 행을 삭제해야 합니다).
SessionsDeviceIdtext -> varchar(512)해당 기기의 사용자가 로그아웃됩니다(해당 행을 삭제해야 합니다).
UploadSessionsFileNametext -> varchar(256)업로드 세션이 사라집니다(해당 행을 삭제해야 합니다).
UploadSessionsPathtext -> varchar(512)업로드 세션이 사라집니다(해당 행을 삭제해야 합니다).

전문 검색 인덱스

Posts와 FileInfo 테이블의 일부 단어가 전문 검색 인덱싱의 최대 토큰 길이 제한을 초과할 수 있습니다. 이런 경우를 대비해 PostgreSQL 스키마에서 idx_posts_message_txt와 idx_fileinfo_content_txt 인덱스를 제거한 뒤, 마이그레이션 후 다음 쿼리로 이 인덱스를 생성합니다.

마이그레이션 중 오류를 방지하기 위해 다음 쿼리를 포함했습니다.

DROP INDEX IF EXISTS {{ .source_db }}.idx_posts_message_txt;
DROP INDEX IF EXISTS {{ .source_db }}.idx_fileinfo_content_txt;

지원되지 않는 유니코드 시퀀스

PostgreSQL에서 허용되지 않는 특정 유니코드 시퀀스가 있는데, 바로 \u0000입니다. 이 시퀀스가 MySQL 데이터베이스의 여러 테이블에 걸쳐 여러 행에 나타날 수 있습니다. 그런 경우 마이그레이션 중 다음과 같은 오류가 발생할 가능성이 높습니다: unsupported Unicode escape sequence: \u0000 cannot be converted to text.. 이를 방지하려면 마이그레이션을 시작하기 전에 데이터를 정리할 것을 권장합니다. 다음 쿼리를 사용해 \u0000 시퀀스를 빈 문자열로 바꿀 수 있습니다.

참고

이 쿼리는 스크립트에서 그대로 사용할 수 있지만, MySQL 콘솔에서 정의할 때는 구분자를 다른 값으로 설정해야 할 수 있습니다(예: DELIMITER //). 프로시저 정의를 마친 뒤에는 구분자를 원래대로(즉 DELIMITER ;) 되돌리세요.

CREATE PROCEDURE SanitizeUnsupportedUnicode()
BEGIN
 DECLARE done INT DEFAULT FALSE;
 DECLARE curTableName text;
 DECLARE curColumnName text;
 DECLARE cursors CURSOR FOR
    SELECT table_name, column_name
    FROM information_schema.COLUMNS
    WHERE data_type = 'json'
    AND table_schema = DATABASE();
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;

OPEN cursors;

WHILE NOT DONE DO
 FETCH cursors INTO curTableName, curColumnName;
 SET @query_string = CONCAT('UPDATE ', curTableName, ' SET ', curColumnName, ' = REPLACE(', curColumnName, ', \'\\\\u0000\', \'\') WHERE ', curColumnName, ' LIKE \'%\\u0000%\';');

 PREPARE dynamic_query FROM @query_string;
 EXECUTE dynamic_query;
 DEALLOCATE PREPARE dynamic_query;
END WHILE;

CLOSE cursors;
END;

CALL SanitizeUnsupportedUnicode();

DROP PROCEDURE IF EXISTS SanitizeUnsupportedUnicode;
참고

허용되지 않는 특정 바이트 시퀀스 값도 있는데, 마이그레이션 중 invalid byte sequence for encoding 'UTF8': 0x00" 오류를 일으킵니다. 이 오류를 방지하려면 텍스트 캐스팅 규칙에 remove-null-characters 절을 추가할 수 있습니다. 다만 pgloader가 데이터를 즉석에서 변경하므로, 비교 단계에서 (영향을 받은 테이블이 있다면) 테이블 간 차이가 나타날 수 있습니다.

이전 구성·버전에서 남은 잔여물

v6.4 이전의 Mattermost는 스키마 마이그레이션을 처리하는 데 golang-migrate를 사용했습니다. 지금은 사용하지 않으므로 schema_migrations 테이블을 제외합니다. v6.4 이전의 Mattermost를 사용했다면 이 테이블을 제거하고 비교 대상에서도 제외하는 것을 고려하세요.

DROP TABLE mattermost.schema_migrations;

일부 커뮤니티 구성원이 SharedChannelRemotes 테이블에 description과 nextsyncat 컬럼이 있었다고 보고했습니다. 이 컬럼들은 테이블에서 제거해야 합니다. 다음 DDL을 실행해 컬럼을 삭제하는 것을 고려하세요. (이 마이그레이션은 향후 Mattermost 버전에 추가될 예정입니다.)

ALTER TABLE SharedChannelRemotes DROP COLUMN description, DROP COLUMN nextsyncat;

이전에 릴리스된 96번째 마이그레이션에서 오류가 확인되었습니다. 마이그레이션을 진행하기 전에 특정 컬럼을 제거해야 합니다. Threads 테이블이 기대한 상태가 되도록 다음 준비된 문(prepared statement)을 실행하세요.

SET @preparedStatement = (SELECT IF(
 (
    SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
    WHERE table_name = 'Threads'
    AND table_schema = DATABASE()
    AND column_name = 'TeamId'
 ) > 0,
 'ALTER TABLE Threads DROP COLUMN TeamId;',
 'SELECT 1'
));

PREPARE alterIfExists FROM @preparedStatement;
EXECUTE alterIfExists;
DEALLOCATE PREPARE alterIfExists;

데이터베이스 내 구성

이전에 Mattermost 구성을 데이터베이스에서 관리했다면, 해당 테이블은 마이그레이션 스크립트로 MySQL 데이터베이스에서 옮겨지지 않습니다.

두 가지 마이그레이션이 필요합니다.

  • 데이터베이스 구성을 파일 시스템으로 마이그레이션
  • 파일 시스템 구성을 다시 데이터베이스로 마이그레이션
데이터베이스 구성을 파일 시스템으로 마이그레이션

mmctl config migrate 명령을 사용해 다음과 같이 구성을 마이그레이션해 파일 시스템으로 옮깁니다.

mmctl config migrate "<DB_USER>:<DB_PASS>@tcp(<DB_HOST>:3306)/<DB_NAME>?charset=utf8mb4,utf8&readTimeout=30s&writeTimeout=30s&multiStatements=true" /opt/mattermost/config/config.json --local

여기서 <DB_USER>, <DB_PASS>, <DB_HOST>, <DB_NAME>은 사용 환경의 값으로 바꿉니다. 이 명령을 실행할 때 --local을 반드시 사용하세요. 첫 번째 파라미터(<DB_USER>, <DB_PASS>)는 구성이 저장된 데이터베이스이고, 두 번째 파라미터(<DB_HOST>, <DB_NAME>)는 구성을 저장할 파일입니다.

구성 파일에서 SqlSettings.DataSource와 SqlSettings.DriverName 필드를 새 내용에 맞게 수정하세요. json 파일을 열어 해당 필드를 변경하면 됩니다.

파일 시스템 구성을 다시 데이터베이스로 마이그레이션

구성을 다시 데이터베이스에 저장하려면 mmctl config migrate 명령을 다시 사용하되 파라미터를 반대로 지정합니다. 대상 데이터베이스로 되돌릴 때는 새 데이터베이스 자격 증명을 사용해야 합니다.

SELECT * FROM Configurations WHERE Active = 't';

SqlSettings.DataSource와 SqlSettings.DriverName 필드를 그에 맞게 수정해야 합니다. 또한 마이그레이션이 완료된 뒤에는 MM_CONFIG 환경 변수가 새 DSN을 가리켜야 한다는 점에 유의하세요.

대상 데이터베이스 준비

PostgreSQL 데이터베이스 스키마가 요구 사양에 맞게 올바로 구성되도록 테이블과 인덱스를 생성해야 합니다. Mattermost 저장소에 이를 위한 SQL 쿼리가 모두 들어 있으므로, 다음 단계를 실행해 활용할 수 있습니다.

  • 사용 중인 버전에 해당하는 mattermost 저장소를 복제합니다.
git clone -b <your current version (eg. release-7.8)> git@github.com:mattermost/mattermost.git --depth=1
  • 다음 명령으로 morph CLI를 사용해 PostgreSQL 데이터베이스에 모든 스키마 마이그레이션*을 실행합니다.
morph apply up --driver postgres --dsn "postgres://user:pass@localhost:5432/<target_db_mame>?sslmode=disable" --path ./mattermost/db/migrations/postgres --number -1

* v8 이후에는 프로젝트 구조 개편으로 마이그레이션 디렉터리가 Mattermost 저장소를 복제한 위치를 기준으로 ./mattermost/server/channels/db/migrations/postgres/로 변경되었습니다. --path 플래그를 그에 맞게 설정하세요.

데이터 마이그레이션

스키마를 원하는 상태로 만들었다면 pgloader를 실행해 데이터 마이그레이션을 시작할 수 있습니다.

참고

아래 예시에서는 두 데이터베이스의 호스트가 같은 인스턴스에 있다고 가정합니다. 서로 다른 머신에 있다면 주소를 그에 맞게 수정하세요. 또한 pgloader를 --dry-run 플래그와 함께 실행해 .load 파일을 테스트할 수 있습니다. 예를 들어 pgloader --dry-run migration.load 명령을 사용합니다.

데이터 마이그레이션의 기준으로 다음 구성을 사용하세요.

LOAD DATABASE
 FROM      mysql://{{ .mysql_user }}:{{ .mysql_password }}@{{ .mysql_address }}/{{ .source_db }}
 INTO      pgsql://{{ .pg_user }}:{{ .pg_password }}@{{ .postgres_address }}/{{ .target_db }}

WITH data only,
 workers = 8, concurrency = 1,
 multiple readers per thread, rows per range = 10000,
 prefetch rows = 10000, batch rows = 2500,
 create no tables, create no indexes,
 preserve index names

SET PostgreSQL PARAMETERS
 maintenance_work_mem to '128MB',
 work_mem to '12MB'

SET MySQL PARAMETERS
 net_read_timeout  = '120',
 net_write_timeout = '120'

CAST column Channels.Type to "channel_type" drop typemod,
 column Teams.Type to "team_type" drop typemod,
 column UploadSessions.Type to "upload_session_type" drop typemod,
 column ChannelBookmarks.Type to "channel_bookmark_type" drop typemod,
 column Drafts.Priority to text,
 type int when (= precision 11) to integer drop typemod,
 type bigint when (= precision 20) to bigint drop typemod,
 type text to varchar drop typemod using remove-null-characters,
 type tinyint when (<= precision 4) to boolean using tinyint-to-boolean,
 type json to jsonb drop typemod using remove-null-characters

EXCLUDING TABLE NAMES MATCHING ~<IR_>, ~<focalboard>, 'schema_migrations', 'db_migrations', 'db_lock',
 'Configurations', 'ConfigurationFiles', 'db_config_migrations', 'calls'

BEFORE LOAD DO
 $$ ALTER SCHEMA public RENAME TO {{ .source_db }}; $$,
 $$ TRUNCATE TABLE {{ .source_db }}.systems; $$,
 $$ DROP INDEX IF EXISTS {{ .source_db }}.idx_posts_message_txt; $$,
 $$ DROP INDEX IF EXISTS {{ .source_db }}.idx_fileinfo_content_txt; $$

AFTER LOAD DO
 $$ UPDATE {{ .source_db }}.db_migrations set name='add_createat_to_teamembers' where version=92; $$,
 $$ ALTER SCHEMA {{ .source_db }} RENAME TO public; $$,
 $$ SELECT pg_catalog.set_config('search_path', '"$user", public', false); $$,
 $$ ALTER USER {{ .pg_user }} SET SEARCH_PATH TO 'public'; $$;

이 구성 파일을 예를 들어 migration.load로 저장한 뒤 다음 명령으로 pgloader를 실행할 수 있습니다.

pgloader migration.load > migration.log

마이그레이션 과정에서 알게 된 내용을 자유롭게 제보하거나 기여해 주세요.

마이그레이션 이후

전문 검색 인덱스 복원

Posts와 FileInfo 테이블 접근 성능이 저하되지 않도록, 마이그레이션이 끝나면 다음 쿼리를 실행해야 합니다.

CREATE INDEX IF NOT EXISTS idx_posts_message_txt ON public.posts USING gin(to_tsvector('english', message));
CREATE INDEX IF NOT EXISTS idx_fileinfo_content_txt ON public.fileinfo USING gin(to_tsvector('english', content));
참고

Posts와 FileInfo 테이블의 항목 중 위에서 언급한 제한을 초과하는 것이 있다면, 인덱스를 생성하려 할 때 ERROR: string is too long for tsvector 로그와 함께 경고가 발생합니다. 이는 tsvector에 담기지 못한 내용이 무시되었다는 뜻입니다. 잘린 내용도 인덱싱하려면 인덱스를 생성할 때 내용에 substring() 함수를 사용할 수 있습니다.

substring 길이를 1000000부터 시작해 오류가 계속되면 점차 줄여 나가세요.

-- Start with 1000000
CREATE INDEX IF NOT EXISTS idx_fileinfo_content_txt ON public.fileinfo USING gin(to_tsvector('english', substring(content,0,1000000)));

-- If that fails, try 800000
CREATE INDEX IF NOT EXISTS idx_fileinfo_content_txt ON public.fileinfo USING gin(to_tsvector('english', substring(content,0,800000)));

-- Continue decreasing as needed (e.g., 600000, 500000) until the index creates successfully

데이터 비교

Mattermost는 데이터베이스 비교를 간소화하기 위해 dbcmp라는 내부 도구를 개발했습니다. 이 도구는 두 데이터베이스의 모든 테이블을 점검해 스키마 차이를 보고합니다. 다만 dbcmp는 개별 행을 비교하지 않습니다. 대신 지정된 page-size를 기준으로 체크섬 값을 계산해 비교합니다. 따라서 행 수준의 차이는 만들어 내지 못합니다.

특히 마이그레이션 중 (기본값이나 Mattermost가 제공한 것 이외의) 사용자 정의 캐스팅 규칙을 사용했다면 데이터 무결성을 확인하는 추가 점검으로 dbcmp를 사용할 것을 권장합니다. 그렇지 않다면 이 도구를 실행할 필요는 없습니다.

이 도구에는 비교 실행을 위한 몇 가지 플래그가 있습니다.

Usage:
  dbcmp [flags]

Flags:
      --exclude strings   exclude tables from comparison, takes comma-separated values.
      --include strings   include only matching tables for comparison, takes comma-separated values.
  -h, --help              help for dbcmp
      --source string     source database dsn
      --target string     target database dsn
  -v, --version           version for dbcmp
참고

--exclude와 --include 플래그는 상호 배타적이므로 함께 사용할 수 없습니다.

이 경우에는 다음 명령을 실행하면 됩니다.

dbcmp --source "${MYSQL_DSN}" --target "${POSTGRES_DSN} " --include="posts,users"

명령 예시는 다음과 같습니다: dbcmp --source "user:password@tcp(address:3306)/db_name --target "postgres://user:password@address:5432/db_name

참고

POSTGRES_DSN은 postgres:// 접두사로 시작해야 합니다. 이를 통해 dbcmp가 데이터베이스에 연결할 때 사용할 드라이버를 결정합니다.

또 하나 제외하는 대상은 db_migrations 테이블입니다. 이 테이블에는 작은 차이(마이그레이션 이름 하나의 오타)가 있어 diff가 발생합니다. PostgreSQL 스키마를 morph와 공식 mattermost 소스로 생성했으므로 안심하고 건너뛸 수 있습니다. 반면 systems 테이블은 일부 마이그레이션에서 키가 추가된 경우 추가적인 diff가 나타날 수 있습니다. 문제가 발생하면 systems 테이블을 제외하고, 이 테이블의 데이터는 상대적으로 작으므로 수동으로 비교하는 것을 고려하세요.

참고

마이그레이션 중 remove-null-characters 변환 함수를 사용했고 MySQL 데이터베이스에 0x00 바이트 시퀀스가 있었다면, 비교 단계에서 해당 테이블에 차이가 나타납니다.

search path 복원

pgloader 구성 파일(예: migration.load)을 자세히 살펴보면 데이터베이스 사용자의 search_path가 public으로 설정되어 있는 것을 확인할 수 있습니다. Mattermost 애플리케이션에 필요한 것은 이것뿐입니다. 다만 다른 스키마를 검색 경로에 포함해야 한다면 요구 사항에 맞게 search_path를 수정해야 합니다.

플러그인 마이그레이션

플러그인 쪽에서는 위와 다른 방식을 취합니다. 이번에는 테이블과 인덱스를 만드는 데 morph 도구를 사용하지 않고, pgloader가 대신 테이블을 생성하도록 합니다. 그 이유는 협업형 플레이북과 보드가 SQL 쿼리를 처리하는 데 애플리케이션 로직을 활용하는데, 이 시점에는 어떤 수준의 애플리케이션도 사용하고 싶지 않기 때문입니다.

협업형 플레이북

플레이북용으로 제공되는 pgloader 구성은 v1.38.1 기준이며, 마이그레이션을 수행하려면 플러그인이 최소 v1.36.0 이상이어야 합니다.

마이그레이션 준비가 되면 pgloader를 실행해 스키마와 데이터 마이그레이션을 시작할 수 있습니다.

데이터 마이그레이션의 기준으로 다음 구성을 사용하세요.

LOAD DATABASE
 FROM      mysql://{{ .mysql_user }}:{{ .mysql_password }}@{{ .mysql_address }}/{{ .source_db }}
 INTO      pgsql://{{ .pg_user }}:{{ .pg_password }}@{{ .postgres_address }}/{{ .target_db }}

WITH include drop, create tables, create indexes, no foreign keys,
 workers = 8, concurrency = 1,
 multiple readers per thread, rows per range = 50000,
 preserve index names

SET PostgreSQL PARAMETERS
 maintenance_work_mem to '128MB',
 work_mem to '12MB'

SET MySQL PARAMETERS
 net_read_timeout  = '120',
 net_write_timeout = '120'

CAST column IR_ChannelAction.ActionType to text drop typemod,
 column IR_ChannelAction.TriggerType to text drop typemod,
 column IR_Incident.ChecklistsJSON to "json" drop typemod

INCLUDING ONLY TABLE NAMES MATCHING
 ~/IR_/

BEFORE LOAD DO
 $$ ALTER SCHEMA public RENAME TO {{ .source_db }}; $$

AFTER LOAD DO
 $$ ALTER TABLE {{ .source_db }}.IR_ChannelAction ALTER COLUMN ActionType TYPE varchar(65536); $$,
 $$ ALTER TABLE {{ .source_db }}.IR_ChannelAction ALTER COLUMN TriggerType TYPE varchar(65536); $$,
 $$ ALTER TABLE {{ .source_db }}.IR_Incident ALTER COLUMN ReminderMessageTemplate TYPE varchar(65536); $$,
 $$ ALTER TABLE {{ .source_db }}.IR_Incident ALTER COLUMN ReminderMessageTemplate SET DEFAULT ''::text;  $$,
 $$ ALTER TABLE {{ .source_db }}.IR_Incident ALTER COLUMN ConcatenatedInvitedUserIDs TYPE varchar(65536); $$,
 $$ ALTER TABLE {{ .source_db }}.IR_Incident ALTER COLUMN ConcatenatedInvitedUserIDs SET DEFAULT ''::text; $$,
 $$ ALTER TABLE {{ .source_db }}.IR_Incident ALTER COLUMN ConcatenatedWebhookOnCreationURLs TYPE varchar(65536); $$,
 $$ ALTER TABLE {{ .source_db }}.IR_Incident ALTER COLUMN ConcatenatedWebhookOnCreationURLs SET DEFAULT ''::text; $$,
 $$ ALTER TABLE {{ .source_db }}.IR_Incident ALTER COLUMN ConcatenatedInvitedGroupIDs TYPE varchar(65536); $$,
 $$ ALTER TABLE {{ .source_db }}.IR_Incident ALTER COLUMN ConcatenatedInvitedGroupIDs SET DEFAULT ''::text; $$,
 $$ ALTER TABLE {{ .source_db }}.IR_Incident ALTER COLUMN Retrospective TYPE varchar(65536); $$,
 $$ ALTER TABLE {{ .source_db }}.IR_Incident ALTER COLUMN Retrospective SET DEFAULT ''::text; $$,
 $$ ALTER TABLE {{ .source_db }}.IR_Incident ALTER COLUMN MessageOnJoin TYPE varchar(65536); $$,
 $$ ALTER TABLE {{ .source_db }}.IR_Incident ALTER COLUMN MessageOnJoin SET DEFAULT ''::text; $$,
 $$ ALTER TABLE {{ .source_db }}.IR_Incident ALTER COLUMN ConcatenatedWebhookOnStatusUpdateURLs TYPE varchar(65536); $$,
 $$ ALTER TABLE {{ .source_db }}.IR_Incident ALTER COLUMN ConcatenatedWebhookOnStatusUpdateURLs SET DEFAULT ''::text; $$,
 $$ ALTER TABLE {{ .source_db }}.IR_Incident ALTER COLUMN CategoryName TYPE varchar(65536); $$,
 $$ ALTER TABLE {{ .source_db }}.IR_Incident ALTER COLUMN CategoryName SET DEFAULT ''::text; $$,
 $$ ALTER TABLE {{ .source_db }}.IR_Incident ALTER COLUMN ConcatenatedBroadcastChannelIds TYPE varchar(65536); $$,
 $$ ALTER TABLE {{ .source_db }}.IR_Incident ALTER COLUMN ConcatenatedBroadcastChannelIds SET DEFAULT ''::text; $$,
 $$ ALTER TABLE {{ .source_db }}.IR_Incident ALTER COLUMN ChannelIDToRootID TYPE varchar(65536); $$,
 $$ ALTER TABLE {{ .source_db }}.IR_Incident ALTER COLUMN ChannelIDToRootID SET DEFAULT ''::text; $$,
 $$ ALTER TABLE {{ .source_db }}.IR_Playbook ALTER COLUMN ReminderMessageTemplate TYPE varchar(65536); $$,
 $$ ALTER TABLE {{ .source_db }}.IR_Playbook ALTER COLUMN ReminderMessageTemplate SET DEFAULT ''::text; $$,
 $$ ALTER TABLE {{ .source_db }}.IR_Playbook ALTER COLUMN ConcatenatedInvitedUserIDs TYPE varchar(65536); $$,
 $$ ALTER TABLE {{ .source_db }}.IR_Playbook ALTER COLUMN ConcatenatedInvitedUserIDs SET DEFAULT ''::text; $$,
 $$ ALTER TABLE {{ .source_db }}.IR_Playbook ALTER COLUMN ConcatenatedWebhookOnCreationURLs TYPE varchar(65536); $$,
 $$ ALTER TABLE {{ .source_db }}.IR_Playbook ALTER COLUMN ConcatenatedWebhookOnCreationURLs SET DEFAULT ''::text; $$,
 $$ ALTER TABLE {{ .source_db }}.IR_Playbook ALTER COLUMN ConcatenatedInvitedGroupIDs TYPE varchar(65536); $$,
 $$ ALTER TABLE {{ .source_db }}.IR_Playbook ALTER COLUMN ConcatenatedInvitedGroupIDs SET DEFAULT ''::text; $$,
 $$ ALTER TABLE {{ .source_db }}.IR_Playbook ALTER COLUMN MessageOnJoin TYPE varchar(65536); $$,
 $$ ALTER TABLE {{ .source_db }}.IR_Playbook ALTER COLUMN MessageOnJoin SET DEFAULT ''::text; $$,
 $$ ALTER TABLE {{ .source_db }}.IR_Playbook ALTER COLUMN RetrospectiveTemplate TYPE varchar(65536); $$,
 $$ ALTER TABLE {{ .source_db }}.IR_Playbook ALTER COLUMN RetrospectiveTemplate SET DEFAULT ''::text; $$,
 $$ ALTER TABLE {{ .source_db }}.IR_Playbook ALTER COLUMN ConcatenatedWebhookOnStatusUpdateURLs TYPE varchar(65536); $$,
 $$ ALTER TABLE {{ .source_db }}.IR_Playbook ALTER COLUMN ConcatenatedWebhookOnStatusUpdateURLs SET DEFAULT ''::text; $$,
 $$ ALTER TABLE {{ .source_db }}.IR_Playbook ALTER COLUMN ConcatenatedSignalAnyKeywords TYPE varchar(65536); $$,
 $$ ALTER TABLE {{ .source_db }}.IR_Playbook ALTER COLUMN ConcatenatedSignalAnyKeywords SET DEFAULT ''::text; $$,
 $$ ALTER TABLE {{ .source_db }}.IR_Playbook ALTER COLUMN CategoryName TYPE varchar(65536); $$,
 $$ ALTER TABLE {{ .source_db }}.IR_Playbook ALTER COLUMN CategoryName SET DEFAULT ''::text; $$,
 $$ ALTER TABLE {{ .source_db }}.IR_Playbook ALTER COLUMN ChecklistsJSON TYPE JSON USING ChecklistsJSON::JSON; $$,
 $$ ALTER TABLE {{ .source_db }}.IR_Playbook ALTER COLUMN ConcatenatedBroadcastChannelIds TYPE varchar(65536); $$,
 $$ ALTER TABLE {{ .source_db }}.IR_Playbook ALTER COLUMN ConcatenatedBroadcastChannelIds SET DEFAULT ''::text; $$,
 $$ ALTER TABLE {{ .source_db }}.IR_Playbook ALTER COLUMN RunSummaryTemplate TYPE varchar(65536); $$,
 $$ ALTER TABLE {{ .source_db }}.IR_Playbook ALTER COLUMN RunSummaryTemplate SET DEFAULT ''::text; $$,
 $$ ALTER TABLE {{ .source_db }}.IR_Playbook ALTER COLUMN ChannelNameTemplate TYPE varchar(65536); $$,
 $$ ALTER TABLE {{ .source_db }}.IR_Playbook ALTER COLUMN ChannelNameTemplate SET DEFAULT ''::text; $$,
 $$ ALTER TABLE {{ .source_db }}.IR_PlaybookMember ALTER COLUMN Roles TYPE varchar(65536); $$,
 $$ ALTER TABLE {{ .source_db }}.IR_Category_Item ADD CONSTRAINT ir_category_item_categoryid FOREIGN KEY (CategoryId) REFERENCES {{ .source_db }}.IR_Category(Id); $$,
 $$ ALTER TABLE {{ .source_db }}.IR_Metric ADD CONSTRAINT ir_metric_metricconfigid FOREIGN KEY (MetricConfigId) REFERENCES {{ .source_db }}.IR_MetricConfig(Id); $$,
 $$ ALTER TABLE {{ .source_db }}.IR_Metric ADD CONSTRAINT ir_metric_incidentid FOREIGN KEY (IncidentId) REFERENCES {{ .source_db }}.IR_Incident(Id); $$,
 $$ ALTER TABLE {{ .source_db }}.IR_MetricConfig ADD CONSTRAINT ir_metricconfig_playbookid FOREIGN KEY (PlaybookId) REFERENCES {{ .source_db }}.IR_Playbook(Id); $$,
 $$ ALTER TABLE {{ .source_db }}.IR_PlaybookAutoFollow ADD CONSTRAINT ir_playbookautofollow_playbookid FOREIGN KEY (PlaybookId) REFERENCES {{ .source_db }}.IR_Playbook(Id); $$,
 $$ ALTER TABLE {{ .source_db }}.IR_PlaybookMember ADD CONSTRAINT ir_playbookmember_playbookid FOREIGN KEY (PlaybookId) REFERENCES {{ .source_db }}.IR_Playbook(Id); $$,
 $$ ALTER TABLE {{ .source_db }}.IR_Run_Participants ADD CONSTRAINT ir_run_participants_incidentid FOREIGN KEY (IncidentId) REFERENCES {{ .source_db }}.IR_Incident(Id); $$,
 $$ ALTER TABLE {{ .source_db }}.IR_StatusPosts ADD CONSTRAINT ir_statusposts_incidentid FOREIGN KEY (IncidentId) REFERENCES {{ .source_db }}.IR_Incident(Id); $$,
 $$ ALTER TABLE {{ .source_db }}.IR_TimelineEvent ADD CONSTRAINT ir_timelineevent_incidentid FOREIGN KEY (IncidentId) REFERENCES {{ .source_db }}.IR_Incident(Id); $$,
 $$ CREATE UNIQUE INDEX IF NOT EXISTS ir_playbookmember_playbookid_memberid_key on {{ .source_db }}.IR_PlaybookMember(PlaybookId,MemberId); $$,
 $$ CREATE INDEX IF NOT EXISTS ir_statusposts_incidentid_postid_key on {{ .source_db }}.IR_StatusPosts(IncidentId,PostId); $$,
 $$ CREATE INDEX IF NOT EXISTS ir_playbookmember_playbookid on {{ .source_db }}.IR_PlaybookMember(PlaybookId); $$,
 $$ ALTER SCHEMA {{ .source_db }} RENAME TO public; $$,
 $$ SELECT pg_catalog.set_config('search_path', '"$user", public', false); $$,
 $$ ALTER USER {{ .pg_user }} SET SEARCH_PATH TO 'public'; $$;
pgloader playbooks.load > playbooks_migration.log

Focalboard

v9.0부터 Boards는 Focalboard 플러그인으로서 전적으로 커뮤니티가 지원하는 형태로 전환됩니다. 따라서 이 가이드는 스키마의 v7.10.x 버전만 다룹니다. 공식 공지를 참고하세요.

마이그레이션 준비가 되면 pgloader를 실행해 스키마와 데이터 마이그레이션을 시작할 수 있습니다.

데이터 마이그레이션의 기준으로 다음 구성을 사용하세요.

LOAD DATABASE
 FROM      mysql://{{ .mysql_user }}:{{ .mysql_password }}@{{ .mysql_address }}/{{ .source_db }}
 INTO      pgsql://{{ .pg_user }}:{{ .pg_password }}@{{ .postgres_address }}/{{ .target_db }}

WITH include drop, create tables, create indexes, reset sequences,
 workers = 8, concurrency = 1,
 multiple readers per thread, rows per range = 50000,
 preserve index names

SET PostgreSQL PARAMETERS
 maintenance_work_mem to '128MB',
 work_mem to '12MB'

SET MySQL PARAMETERS
 net_read_timeout  = '120',
 net_write_timeout = '120'

CAST column focalboard_blocks.fields to "json" drop typemod,
 column focalboard_blocks_history.fields to "json" drop typemod,
 column focalboard_schema_migrations.name to "varchar" drop typemod,
 column focalboard_sessions.props to "json" drop typemod,
 column focalboard_teams.settings to "json" drop typemod,
 column focalboard_users.props to "json" drop typemod,
 type int when (= precision 11) to int4 drop typemod,
 type json to jsonb drop typemod

INCLUDING ONLY TABLE NAMES MATCHING
 ~/focalboard/

BEFORE LOAD DO
 $$ ALTER SCHEMA public RENAME TO {{ .source_db }}; $$

AFTER LOAD DO
 $$ UPDATE {{ .source_db }}.focalboard_blocks SET "fields" = '{}'::json WHERE "fields"::text = ''; $$,
 $$ UPDATE {{ .source_db }}.focalboard_blocks_history SET "fields" = '{}'::json WHERE "fields"::text = ''; $$,
 $$ UPDATE {{ .source_db }}.focalboard_sessions SET "props" = '{}'::json WHERE "props"::text = ''; $$,
 $$ UPDATE {{ .source_db }}.focalboard_teams SET "settings" = '{}'::json WHERE "settings"::text = ''; $$,
 $$ UPDATE {{ .source_db }}.focalboard_users SET "props" = '{}'::json WHERE "props"::text = ''; $$,
 $$ ALTER SCHEMA {{ .source_db }} RENAME TO public; $$,
 $$ SELECT pg_catalog.set_config('search_path', '"$user", public', false); $$,
 $$ ALTER USER {{ .pg_user }} SET SEARCH_PATH TO 'public'; $$;
pgloader focalboard.load > focalboard_migration.log

Calls

v9.9보다 높은 Mattermost 버전이나 v0.27보다 높은 Calls 플러그인을 사용 중이라면 이 플러그인의 데이터도 마이그레이션할 수 있습니다. Boards, Playbooks 마이그레이션과 비슷한 방식으로 pgloader가 테이블을 생성하도록 합니다.

마이그레이션 준비가 되면 pgloader를 실행해 스키마와 데이터 마이그레이션을 시작할 수 있습니다.

데이터 마이그레이션의 기준으로 다음 구성을 사용하세요.

LOAD DATABASE
 FROM      mysql://{{ .mysql_user }}:{{ .mysql_password }}@{{ .mysql_address }}/{{ .source_db }}
 INTO      pgsql://{{ .pg_user }}:{{ .pg_password }}@{{ .postgres_address }}/{{ .target_db }}

WITH include drop, create tables, create indexes, reset sequences,
 workers = 8, concurrency = 1,
 multiple readers per thread, rows per range = 50000,
 preserve index names

SET PostgreSQL PARAMETERS
 maintenance_work_mem to '128MB',
 work_mem to '12MB'

SET MySQL PARAMETERS
 net_read_timeout  = '120',
 net_write_timeout = '120'

CAST type json to jsonb drop typemod

INCLUDING ONLY TABLE NAMES MATCHING
 ~/calls/

BEFORE LOAD DO
 $$ ALTER SCHEMA public RENAME TO {{ .source_db }}; $$

AFTER LOAD DO
 $$ ALTER SCHEMA {{ .source_db }} RENAME TO public; $$,
 $$ SELECT pg_catalog.set_config('search_path', '"$user", public', false); $$,
 $$ ALTER USER {{ .pg_user }} SET SEARCH_PATH TO 'public'; $$;
pgloader calls.load > calls_migration.log

문제 해결

MySQL에서 PostgreSQL로 마이그레이션하는 중 발생하는 오류 해결을 참고하세요.