메인 콘텐츠로 건너뛰기
Postgres에서 ClickHouse로 복제된 업데이트 및 삭제 작업은 ClickHouse의 데이터 저장 구조와 복제 과정으로 인해 ClickHouse에 중복된 행이 생길 수 있습니다. 이 페이지에서는 그 이유와 ClickHouse에서 중복을 처리하기 위한 전략을 설명합니다.

데이터는 어떻게 복제됩니까?

PostgreSQL 논리 디코딩

ClickPipes는 Postgres에서 변경 사항이 발생하는 즉시 이를 읽어들이기 위해 Postgres Logical Decoding을 사용합니다. Postgres의 Logical Decoding 프로세스를 사용하면 ClickPipes와 같은 클라이언트가 사람이 읽을 수 있는 형식, 즉 일련의 INSERT, UPDATE, DELETE 형태로 변경 사항을 수신할 수 있습니다.

ReplacingMergeTree

ClickPipes는 ReplacingMergeTree 엔진을 사용해 Postgres 테이블을 ClickHouse에 매핑합니다. ClickHouse는 append-only 워크로드에서 가장 뛰어난 성능을 발휘하며, 빈번한 UPDATE는 권장하지 않습니다. 이때 ReplacingMergeTree가 특히 강력합니다. ReplacingMergeTree에서는 UPDATE를 더 최신 버전(_peerdb_version)의 행을 삽입하는 방식으로 모델링하고, DELETE는 더 최신 버전의 행을 삽입하되 _peerdb_is_deleted를 true로 표시하는 방식으로 처리합니다. ReplacingMergeTree 엔진은 백그라운드에서 데이터를 중복 제거하고 머지하며, 지정된 프라이머리 키(id)에 대해 가장 최신 버전의 행을 유지합니다. 이를 통해 UPDATE와 DELETE를 버전 기반 삽입으로 효율적으로 처리할 수 있습니다. 아래는 ClickPipes가 ClickHouse에 테이블을 생성하기 위해 실행하는 CREATE TABLE 문의 예시입니다.

이해를 돕기 위한 예시

아래 그림은 ClickPipes를 사용해 PostgreSQL과 ClickHouse 사이에서 테이블 users가 동기화되는 기본 예시를 보여줍니다. 1단계에서는 PostgreSQL의 2개 행에 대한 초기 스냅샷과, ClickPipes가 이 2개 행을 ClickHouse로 초기 적재하는 과정을 보여줍니다. 보시다시피 두 행 모두 변경 없이 그대로 ClickHouse에 복사됩니다. 2단계에서는 users 테이블에서 수행된 3가지 작업을 보여줍니다. 새 행 삽입, 기존 행 업데이트, 그리고 다른 행 삭제입니다. 3단계에서는 ClickPipes가 INSERT, UPDATE, DELETE 작업을 버전 기반 삽입 형태로 ClickHouse에 복제하는 방식을 보여줍니다. UPDATE는 ID 2인 행의 새 버전으로 나타나고, DELETE는 _is_deleted를 사용해 true로 표시된 ID 1의 새 버전으로 나타납니다. 이로 인해 ClickHouse에는 PostgreSQL보다 3개의 행이 더 존재하게 됩니다. 그 결과, SELECT count(*) FROM users;와 같은 단순한 쿼리를 실행하면 ClickHouse와 PostgreSQL에서 서로 다른 결과가 나올 수 있습니다. ClickHouse 머지 문서에 따르면, 오래된 행 버전은 결국 머지 과정에서 제거됩니다. 하지만 머지가 수행되는 시점은 예측할 수 없으므로, 그전까지는 ClickHouse의 쿼리가 일관되지 않은 결과를 반환할 수 있습니다. ClickHouse와 PostgreSQL에서 동일한 쿼리 결과를 보장하려면 어떻게 해야 할까요?

FINAL 키워드를 사용해 중복 제거

ClickHouse 쿼리에서 데이터 중복을 제거하는 권장 방법은 FINAL 수정자를 사용하는 것입니다. 이렇게 하면 중복 제거된 행만 반환됩니다. 이제 이를 서로 다른 3개의 쿼리에 어떻게 적용하는지 살펴보겠습니다. 다음 쿼리의 WHERE 절에 유의하십시오. 이 절은 삭제된 행을 걸러내는 데 사용됩니다.
  • 단순 count 쿼리: Posts의 수를 계산합니다.
동기화가 제대로 이루어졌는지 확인할 때 실행할 수 있는 가장 간단한 쿼리입니다. 두 쿼리는 동일한 개수를 반환해야 합니다.
  • JOIN을 사용한 단순 집계: 조회 수를 가장 많이 누적한 상위 10명의 사용자
단일 테이블(table)에 대한 집계 예시입니다. 여기에서 중복이 있으면 sum 함수 결과에 큰 영향을 줄 수 있습니다.

FINAL 설정

쿼리에서 각 table 이름에 FINAL 수정자를 추가하는 대신, FINAL 설정을 사용하면 쿼리의 모든 table에 자동으로 적용할 수 있습니다. 이 설정은 쿼리별로 또는 전체 session에 적용할 수 있습니다.

ROW policy

중복된 _peerdb_is_deleted = 0 filter를 숨기는 가장 쉬운 방법은 ROW policy를 사용하는 것입니다. 아래는 테이블 votes의 모든 쿼리에서 삭제된 행이 제외되도록 ROW policy를 생성하는 예시입니다.
행 정책은 사용자와 역할 목록을 대상으로 적용됩니다. 이 예시에서는 모든 사용자와 역할에 적용됩니다. 필요에 따라 특정 사용자 또는 역할에만 적용하도록 조정할 수 있습니다.

Postgres처럼 쿼리하기

분석용 데이터셋을 PostgreSQL에서 ClickHouse로 마이그레이션할 때는 데이터 처리 방식과 쿼리 실행 방식의 차이로 인해 애플리케이션 쿼리를 수정해야 하는 경우가 많습니다. 이 섹션에서는 기존 쿼리는 그대로 유지하면서 데이터를 중복 제거하는 기법을 살펴봅니다.

는 쿼리에서 FINAL 키워드를 감추는 데 매우 효과적인 방법입니다. 데이터를 저장하지 않고, 액세스할 때마다 다른 테이블을 단순히 읽기만 하기 때문입니다. 아래는 ClickHouse에서 데이터베이스의 각 테이블에 대해 FINAL 키워드와 삭제된 행을 걸러내는 필터를 적용한 뷰를 생성하는 예시입니다.
그런 다음 PostgreSQL에서 사용하던 것과 동일한 쿼리로 뷰를 조회할 수 있습니다.

갱신 가능 구체화 뷰

또 다른 방법은 갱신 가능 구체화 뷰를 사용하는 것입니다. 이를 사용하면 행의 중복을 제거하는 쿼리 실행을 예약하고, 그 결과를 대상 테이블에 저장할 수 있습니다. 예약된 갱신이 실행될 때마다 대상 테이블은 최신 쿼리 결과로 대체됩니다. 이 방식의 핵심적인 장점은 FINAL 키워드를 사용하는 쿼리가 갱신 시 한 번만 실행된다는 점입니다. 따라서 이후 대상 테이블을 조회하는 쿼리에서는 FINAL을 사용할 필요가 없습니다. 하지만 단점은 대상 테이블의 데이터가 가장 최근 갱신 시점까지만 반영된다는 점입니다. 그럼에도 많은 사용 사례에서는 몇 분에서 몇 시간 정도의 갱신 주기로도 충분할 수 있습니다.
그런 다음 deduplicated_posts 테이블에 대해 평소처럼 쿼리할 수 있습니다.
마지막 수정일 2026년 7월 3일