AlloyDB에서 BigQuery 바로 조회하기 – 레이크하우스 페더레이션 실시간 조회와 동기화 실측 비교
안녕하세요. GS Neotek 안지현입니다.
실측 결과를 먼저 요약하면 다음과 같습니다.
- 실시간 조회(외부 테이블)는 푸시다운 여부와 관계없이 호출마다 1.2~1.5초(중앙값 1,258~1,476ms)가 걸렸습니다.
- 동기화한 로컬 테이블 JOIN은 174ms(서버 실행 3.7ms)로 실시간 조회보다 약 8배 빨랐습니다.
응답 시간은 한국 로컬 PC에서 측정해 왕복 지연 약 169ms가 포함된 값입니다. - 18만 행 규모에서는 푸시다운 유무에 따른 응답 시간과 BigQuery 처리량(4.75MB) 차이가 거의 없었습니다.
서비스 DB는 AlloyDB에, 몇 년 치 이력과 집계 결과는 BigQuery에 있는 구조는 흔합니다.
문제는 애플리케이션이 두 데이터를 함께 써야 할 때입니다.
“이 고객의 최근 주문(AlloyDB)과 지난 3년 구매 패턴(BigQuery)을 같이 보여줘” 같은 요구가 오면, 보통 Reverse ETL 파이프라인을 만들거나 애플리케이션에서 두 DB를 따로 호출해 코드로 합칩니다.
AlloyDB의 레이크하우스 페더레이션(Lakehouse Federation, Preview)은 이 과정을 SQL 한 줄로 줄입니다.
AlloyDB 안에 BigQuery 테이블을 외부 테이블(foreign table)로 붙이고, 로컬 테이블처럼 JOIN하면 됩니다.
이 글에서는 다음을 직접 해 봅니다.
- BigQuery 공개 데이터셋을 AlloyDB에 외부 테이블로 연결하기
- AlloyDB의 운영 테이블과 BigQuery 테이블을 JOIN하기
- 필터·집계 푸시다운이 실제로 동작하는지 실행 계획으로 확인하기
- 실시간 조회와 동기화(sync) 방식의 응답 시간·비용 비교하기
참고: BigQuery에서 AlloyDB를 조회하는 반대 방향은 기존 BigQuery 연합 쿼리(EXTERNAL_QUERY)로 이미 가능했습니다.
이번 기능은 운영 DB 쪽에서 분석 데이터를 끌어오는 방향이라는 점이 새롭습니다.
세 가지 방식, 무엇이 다른가
BigQuery 데이터를 AlloyDB에서 쓰는 방법은 세 가지이고, 모두 bigquery_fdw와 alloydb_sync 두 확장(extension) 위에서 동작합니다.
핵심 차이는 데이터를 옮기느냐, 옮긴다면 얼마나 자주 옮기느냐입니다.
| 방식 | 사용 확장 / 함수 | 데이터 위치 | 신선도 | 로컬 쓰기 | 잘 맞는 상황 |
| 실시간 조회 (레이크하우스 페더레이션) | bigquery_fdw 외부 테이블 | BigQuery에 그대로 | 항상 최신 | 불가(읽기 전용) | 탐색성 분석, 최신 데이터와 운영 데이터 JOIN |
| 일회성 동기화 | alloydb_sync.import_bq_table() | AlloyDB로 복사 | 복사 시점 | 가능 | ML 예측 결과·고객 세그먼트 1회 반영, 로컬에서 가공·임베딩 |
| 주기적 동기화 | alloydb_sync.create_bq_sync_table() | AlloyDB로 주기 복사 | 스케줄 주기 | 불가(관리형 읽기 전용) | 동시 사용자가 많은 서비스 조회, 컬럼 엔진 가속 |
실시간 조회는 복사가 없는 대신 매 쿼리가 BigQuery 비용을 냅니다.
푸시다운되는 쿼리는 BigQuery 쿼리 작업으로, 푸시다운되지 않는 단순 스캔은 Storage Read API로 과금됩니다. WHERE 조건(=, >, <, LIKE 등)과 SUM·AVG·MIN·MAX·COUNT 집계를 BigQuery로 내려보내는 푸시다운을 지원하며, 이렇게 하면 AlloyDB로 넘어오는 행 수가 줄어듭니다.
다만 BigQuery가 읽는 양은 그대로라서, 이 글의 실측(18만 행)에서는 푸시다운 유무에 따른 응답 시간·처리량 차이가 거의 없었습니다.
9월 업데이트로 LIMIT 푸시다운과, 쿼리 비용을 다른 프로젝트로 청구하는 runtime_project 옵션도 추가됐습니다.
동기화 방식은 데이터가 AlloyDB 로컬에 있으므로 읽기 풀(read pool)로 수평 확장할 수 있습니다.
공식 문서도 수천 명이 동시에 조회하는 서비스라면 BigQuery 동시성 한도를 피하기 위해 이 방식을 권장합니다.
이 기능들은 2026년 10월 현재 Preview입니다.
운영 환경 도입 전 SLA와 지원 범위를 확인하세요.
실습 환경과 사전 준비
시나리오는 이커머스입니다.
오늘 들어온 주문은 AlloyDB에 있고, 과거 주문 이력은 BigQuery에 있습니다.
목표는 “오늘 주문한 고객의 누적 구매액”을 AlloyDB 쿼리 하나로 뽑는 것입니다.
| 항목 | 값 |
| 리전 | us-central1 (BigQuery 데이터셋은 US 멀티 리전) |
| AlloyDB | PostgreSQL 18 호환, 기본 인스턴스 2 vCPU (동기화 기능은 PG 18만 지원) |
| BigQuery 데이터 | 공개 데이터셋 thelook_ecommerce를 내 프로젝트로 복사 |
| 접속 | AlloyDB Studio 또는 psql (이 글의 실측은 로컬 PC의 psql → AlloyDB Auth Proxy → 공개 IP 경로) |
서울 리전(asia-northeast3)을 쓰려면 BigQuery 데이터셋도 서울에 두는 편이 좋습니다.
리전이 다르면 지연 시간과 네트워크 비용이 커집니다.
실습 편의상 공개 데이터셋이 있는 US를 썼습니다.
1) BigQuery에 실습 데이터 만들기
CREATE SCHEMA IF NOT EXISTS `my-project.thelook` OPTIONS (location = 'US');
CREATE TABLE `my-project.thelook.order_items` AS
SELECT * FROM `bigquery-public-data.thelook_ecommerce.order_items`;
2) AlloyDB 인스턴스에 플래그 켜기
gcloud alloydb instances update my-primary \
--cluster=my-cluster --region=us-central1 \
--database-flags=bigquery_fdw.enabled=on,alloydb_sync.enabled=on
bigquery_fdw.enabled는 실시간 조회용, alloydb_sync.enabled는 실습 2의 동기화용입니다. alloydb_sync.enabled를 빼면 확장은 만들어지지만 import_bq_table() 호출 시 alloydb_sync extension is disabled 오류가 납니다.
--database-flags는 기존 플래그를 덮어씁니다.
이미 설정한 플래그가 있다면 함께 넣어 주세요.
예를 들어 인스턴스에 공개 IP를 켰다면 password.enforce_complexity=on이 필수라서(없으면 인스턴스 생성 자체가 거부됩니다) 이 플래그도 같이 넣어야 합니다.
실습에서는 플래그 변경이 몇 분 안에 반영됐지만, 작업(operation) 자체는 15분 가까이 끝나지 않아 그동안 다음 update 요청이 unable to queue the operation으로 거부됐습니다.
플래그는 한 번에 모아서 넣는 편이 낫습니다.
3) 클러스터 서비스 계정에 BigQuery 권한 주기
SA=$(gcloud beta alloydb clusters describe my-cluster \
--region=us-central1 --format='value(serviceAccountEmail)')
for ROLE in roles/bigquery.dataViewer roles/bigquery.readSessionUser roles/bigquery.jobUser; do
gcloud projects add-iam-policy-binding my-project \
--member="serviceAccount:${SA}" --role="${ROLE}"
done
serviceAccountEmail 필드는 GA gcloud alloydb가 아니라 gcloud beta alloydb에서만 보이므로 beta 컴포넌트가 필요합니다.
실습이라 프로젝트 단위로 줬지만, 운영에서는 dataViewer를 데이터셋 단위로 좁히는 것이 좋습니다. readsessions.getData 권한이 빠지면 오류 없이 빈 결과가 나오니 주의하세요.
처음에 가장 헷갈리는 지점입니다.
콘솔의 AlloyDB Studio에서 Connect BigQuery table로 연결하면 API 활성화와 권한 부여를 자동으로 처리해 줍니다.
이 글은 동작을 이해하기 위해 SQL로 진행합니다.
실습 1: 외부 테이블로 실시간 조회하기
확장, 서버, 사용자 매핑 만들기
CREATE EXTENSION IF NOT EXISTS bigquery_fdw;
CREATE SERVER bigquery_server FOREIGN DATA WRAPPER bigquery_fdw
OPTIONS (runtime_project 'my-project');
CREATE USER MAPPING FOR postgres SERVER bigquery_server;
서버 이름은 bigquery_server로 두세요.
실시간 조회만 쓴다면 이름은 자유지만, 실습 2의 alloydb_sync 함수는 내부적으로 bigquery_server라는 이름의 서버를 찾습니다.
처음에 bq_server로 만들었더니 동기화 작업이 server "bigquery_server" does not exist로 실패했습니다.
runtime_project는 BigQuery 쿼리 비용이 청구될 프로젝트입니다.
생략하면 데이터가 있는 프로젝트로 청구됩니다.
분석팀 프로젝트의 데이터를 서비스팀이 조회한다면, 비용을 서비스팀 프로젝트로 돌릴 때 유용합니다.
외부 테이블 정의하기
CREATE FOREIGN TABLE bq_order_items (
order_id BIGINT,
user_id BIGINT,
product_id BIGINT,
status VARCHAR,
created_at TIMESTAMPTZ,
sale_price DOUBLE PRECISION
) SERVER bigquery_server OPTIONS (
project 'my-project',
dataset 'thelook',
table 'order_items'
);
필요한 컬럼만 정의해도 됩니다.
컬럼명은 BigQuery와 대소문자까지 같아야 합니다.
타입은 INT64→BIGINT, FLOAT64→DOUBLE PRECISION, TIMESTAMP→TIMESTAMPTZ처럼 매핑합니다.
AlloyDB 쪽 운영 테이블 만들기
CREATE TABLE orders_today (
order_id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL,
amount NUMERIC(12,2),
ordered_at TIMESTAMPTZ DEFAULT now()
);
-- user_id를 1~100,000 범위에서 무작위로 생성(이력 있는 사용자만 JOIN됨)
INSERT INTO orders_today (user_id, amount)
SELECT (random() * 99999 + 1)::bigint, round((random() * 200)::numeric, 2)
FROM generate_series(1, 1000);
JOIN: 오늘 주문 고객의 누적 구매액
WITH lifetime AS (
SELECT user_id, SUM(sale_price) AS total_spent, COUNT(*) AS item_cnt
FROM bq_order_items
WHERE status = 'Complete'
GROUP BY user_id
)
SELECT t.user_id, t.amount AS today_amount, l.total_spent, l.item_cnt
FROM orders_today t
JOIN lifetime l USING (user_id)
ORDER BY l.total_spent DESC
LIMIT 20;
파이프라인 없이 운영 데이터와 분석 데이터가 한 결과로 나옵니다.
실습에서 받은 결과는 다음과 같습니다(orders_today는 random()으로 만들었으므로 실행할 때마다 값이 다릅니다).
user_id | today_amount | total_spent | item_cnt
---------+--------------+--------------------+----------
34805 | 85.00 | 919.8599987030029 | 6
62616 | 26.14 | 903 | 1
41600 | 72.17 | 543.9500122070312 | 2
40997 | 180.37 | 445 | 3
33580 | 84.13 | 430.039999961853 | 4
66281 | 171.60 | 362.98999786376953 | 2
(20행 중 상위 6행)
Time: 2073.999 ms (00:02.074)
복사한 order_items는 180,946행이고, Complete 상태 주문이 있는 사용자는 27,575명이었습니다.
푸시다운이 실제로 일어났는지 확인하기
EXPLAIN (VERBOSE, ANALYZE)
SELECT user_id, SUM(sale_price)
FROM bq_order_items
WHERE status = 'Complete'
GROUP BY user_id;
실행 계획의 Foreign Scan 노드에서 원격으로 보낸 쿼리를 확인합니다. WHERE와 GROUP BY가 원격 쿼리에 들어가 있으면 푸시다운된 것입니다.
BigQuery 쪽에서도 실제 처리량을 교차 확인할 수 있습니다.
-- BigQuery 콘솔에서 실행
SELECT creation_time, query, total_bytes_processed
FROM `region-us`.INFORMATION_SCHEMA.JOBS
WHERE creation_time > TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 1 HOUR)
ORDER BY creation_time DESC;
실습 결과입니다.
실행 계획에는 Foreign Scan 하나만 있고, WHERE와 GROUP BY, SUM이 모두 Remote SQL에 들어가 있습니다.
Foreign Scan (cost=25.00..35.00 rows=200 width=16) (actual time=934.970..949.956 rows=27575.00 loops=1)
Output: user_id, (sum(sale_price))
Remote SQL: SELECT `user_id`, sum(`sale_price`) FROM `my-project.thelook.order_items` WHERE ((`status` = r'''Complete''')) GROUP BY 1
Planning Time: 156.334 ms
Execution Time: 952.942 ms
JOBS 뷰에는 같은 SQL이 /*Query from AlloyDB bigquery_fdw extension:*/ 주석이 붙은 쿼리 작업으로 남습니다.
-[ 1 ]-----------------
creation_time : 2026-10-28 08:11:43
query : /*Query from AlloyDB bigquery_fdw extension:*/
SELECT `user_id`, sum(`sale_price`)
FROM `my-project.thelook.order_items`
WHERE ((`status` = r'''Complete''')) GROUP BY 1
total_bytes_processed : 4750067
-[ 2 ]-----------------
creation_time : 2026-10-28 08:11:32
query : /*Query from AlloyDB bigquery_fdw extension:*/
SELECT `user_id`, sum(`sale_price`), count(*)
FROM `my-project.thelook.order_items`
WHERE ((`status` = r'''Complete''')) GROUP BY 1
total_bytes_processed : 4750067
푸시다운되지 않는 함수(예: PostgreSQL 전용 문자열 함수)를 WHERE에 넣고 같은 확인을 반복해 보면 실행 계획이 달라집니다. WHERE initcap(status) = 'Complete'로 바꾸면 Remote SQL에서 필터와 집계가 빠지고, AlloyDB가 18만 행을 모두 받아 직접 거른 뒤 HashAggregate로 집계합니다.
HashAggregate (actual time=529.995..534.007 rows=27575.00 loops=1)
Group Key: bq_order_items.user_id
-> Foreign Scan on public.bq_order_items (actual time=16.800..519.845 rows=45119.00 loops=1)
Filter: (initcap((bq_order_items.status)::text) = 'Complete'::text)
Rows Removed by Filter: 135827
Remote SQL: SELECT `user_id`,`status`,`sale_price` FROM `my-project.thelook.order_items`
Execution Time: 1314.748 ms
이때는 JOBS 뷰에 아무 작업도 남지 않았습니다.
필터·집계가 없는 단순 컬럼 스캔은 쿼리 작업을 만들지 않고 Storage Read API로 테이블을 직접 읽기 때문입니다.
그래서 푸시다운이 안 된 쿼리의 비용은 INFORMATION_SCHEMA.JOBS로는 보이지 않고, Storage Read API 사용량으로 따로 봐야 합니다.
실습 2: 동기화로 로컬 테이블 만들기
실시간 조회는 편하지만, 같은 집계를 초당 수백 번 호출하는 API라면 매번 BigQuery를 다녀오는 것이 부담입니다.
이럴 때는 BigQuery에서 미리 집계한 결과를 AlloyDB로 주기적으로 복사해 두는 편이 낫습니다.
동기화 기능은 PostgreSQL 18 클러스터에서만 지원되고, Preview 기간에는 접근 신청이 필요할 수 있습니다.
1) BigQuery에 집계 테이블 만들기
-- BigQuery 콘솔에서 실행
CREATE OR REPLACE TABLE `my-project.thelook.user_lifetime` AS
SELECT user_id, SUM(sale_price) AS total_spent, COUNT(*) AS item_cnt
FROM `my-project.thelook.order_items`
WHERE status = 'Complete'
GROUP BY user_id;
처음에는 같은 SQL을 뷰(CREATE OR REPLACE VIEW)로 만들어 동기화 소스로 썼는데, 작업이 non-table entities cannot be read with the storage API 오류로 실패했습니다.
공식 문서에는 뷰도 소스로 쓸 수 있다고 되어 있지만, 이번 환경에서는 실패했습니다.
동기화는 Storage Read API로 소스를 읽기 때문에, 이번 실습 환경에서는 뷰를 소스로 쓸 수 없었습니다.
집계 결과를 테이블로 만들어 두고(필요하면 BigQuery 예약 쿼리로 갱신) 그 테이블을 동기화하세요.
원본 18만 행 대신 사용자 수(27,575행)만큼의 집계 결과만 옮기므로 Storage Read API 비용과 AlloyDB 저장 공간이 함께 줄어듭니다.
2) 일회성 동기화: 쓰기 가능한 복사본
CREATE EXTENSION IF NOT EXISTS alloydb_sync;
SELECT alloydb_sync.import_bq_table(
'my-project.thelook.user_lifetime',
'public.user_lifetime_snapshot',
'replace',
ARRAY['user_id']
);
세 번째 인자는 대상 테이블이 이미 있을 때의 동작(error·skip·replace)이고, 네 번째는 기본 키입니다.
함수는 작업 ID(UUID)만 바로 돌려주고 복사는 백그라운드에서 진행되므로, 끝났는지는 아래 job_status 뷰로 확인합니다.
실습에서는 27,575행이 2.1초 만에 들어왔습니다.
결과는 독립된 일반 테이블이라 UPDATE나 인덱스 추가가 자유롭습니다(실제로 UPDATE가 정상 수행됐습니다).
3) 주기적 동기화: 1시간마다 자동 갱신
SELECT alloydb_sync.create_bq_sync_table(
'my-project.thelook.user_lifetime',
'public.user_lifetime_mirror',
'1 hour',
'replace',
ARRAY['user_id']
);
주기 동기화 테이블은 관리형 읽기 전용 테이블입니다.
진행 상황은 job_status 뷰로 확인합니다.
SELECT import_id, status, records_processed, total_records, error_message
FROM alloydb_sync.job_status;
실습 결과입니다.
앞의 두 줄은 위에서 설명한 서버 이름 문제와 뷰 소스 문제로 실패한 시도이고, 아래 두 줄이 일회성 동기화와 주기 동기화의 첫 실행입니다.
import_id | status | records_processed | total_records | error_message
--------------------------------------+-----------+-------------------+---------------+------------------------------------------------------------
9960c7ba-ef35-444c-8f28-e61526ec722f | FAILED | 0 | | server "bigquery_server" does not exist
4cc5005d-3446-4d9f-b4a5-91ebb1615630 | FAILED | 0 | | bigquery_fdw extension failed to create BigQuery read session
| | | | with error: ... non-table entities cannot be read with the storage API
0b7b04b1-ff87-490a-a904-7011a4ebdd55 | COMPLETED | 27575 | 27575 |
384b12b0-e60d-4d2e-b1c6-ab85b746406d | COMPLETED | 27575 | 27575 |
| 작업 | 대상 테이블 | 소요 시간 (end_time - start_time) |
| 일회성 동기화 | user_lifetime_snapshot | 2.11초 |
| 주기 동기화 첫 실행 | user_lifetime_mirror | 1.75초 |
주기 동기화 테이블에 UPDATE를 시도하면 Table is read-only (managed by AlloyDBSync) 오류가 납니다.
또 동기화 작업마다 BigQuery에 INFORMATION_SCHEMA.COLUMNS 메타데이터 조회 작업이 하나씩 생겼고, 각각 최소 과금 단위인 10MB로 청구됐습니다.
주기를 짧게 잡으면 이 비용도 횟수만큼 쌓입니다.
참고로 이번 실습에서는 Preview 접근 신청 없이 플래그만 켜고 동기화를 쓸 수 있었습니다.
이제 실습 1의 JOIN을 로컬 테이블로 바꾸면 BigQuery를 전혀 거치지 않습니다.
SELECT t.user_id, t.amount, m.total_spent
FROM orders_today t
JOIN user_lifetime_mirror m USING (user_id)
ORDER BY m.total_spent DESC
LIMIT 20;
운영 시 주의할 점
- 동기화 테이블을 지울 때는
DROP TABLE대신alloydb_sync.delete_bq_sync_table()을 써야 합니다.
그냥 지우면 백그라운드 스케줄러가 계속 오류를 내며 워커 슬롯을 잡아먹습니다. replace동기화 중에는 테이블이 잠시 비었다가 배치 단위로 채워집니다.
비어 있는 순간을 허용할 수 없다면 갱신 주기를 트래픽이 적은 시간대로 잡으세요.- 동기화 도중 DB가 비정상 종료되면 작업 상태가
RUNNING으로 남아 다음 동기화를 막을 수 있습니다.
공식 문서의 수동 해제 SQL을 미리 런북에 넣어 두는 것을 권합니다. ARRAY,BYTES,VECTOR,GEOGRAPHY타입 컬럼은 동기화되지 않으므로 집계 테이블을 만들 때 미리 빼 두세요.
실측 비교: 응답 시간과 비용
같은 질문(“오늘 주문 고객 상위 20명의 누적 구매액”)을 세 가지 방식으로 각각 실행해 비교했습니다.
각 방식마다 psql의 \timing으로 10회 실행해 중앙값을 기록하고, BigQuery 쪽 처리량은 INFORMATION_SCHEMA.JOBS의 total_bytes_processed로 확인했습니다.
측정 조건을 먼저 밝혀 둡니다.
클라이언트는 한국의 로컬 PC이고 AlloyDB Auth Proxy를 거쳐 us-central1 인스턴스에 붙었습니다.
이 경로의 왕복 지연만 약 169ms(SELECT 1 기준)라서 모든 응답 시간에 이 값이 포함돼 있습니다.
원본 테이블은 180,946행, 약 13.6MB로 작은 편입니다.
| 방식 | 응답 시간 중앙값 (ms, 왕복 지연 약 169ms 포함) | BigQuery 처리량 (MB) | 데이터 신선도 | 비고 |
| 실시간 조회, 푸시다운 O (쿼리 캐시 끔) | 1,476 | 4.75 (회당, 캐시를 쓰지 않으므로 과금은 최소 단위 10MB) | 실시간 | WHERE·GROUP BY 원격 실행 |
| 실시간 조회, 푸시다운 O (기본값, 캐시 적중) | 1,343 | 0 (10회 모두 cache_hit) | 실시간* | 첫 실행만 4.75MB 처리 |
| 실시간 조회, 푸시다운 X | 1,258 | 0 (JOBS에 작업 없음) | 실시간 | 비지원 함수(initcap)로 필터, Storage Read API로 3개 컬럼 전체(dry run 기준 4.75MB)를 직접 읽음 |
| 주기 동기화 (로컬 JOIN) | 174 | 0 (조회 시) | 최대 1시간 지연 | 서버 실행 시간 3.7ms, 나머지는 네트워크 왕복 |
측정 위치 주의: 위 응답 시간은 한국의 로컬 PC에서 us-central1 인스턴스로 측정한 값이라 네트워크 왕복 지연 약 169ms가 모두 포함돼 있습니다.
같은 리전에서 호출하면 각 값에서 이만큼이 빠집니다.
* bigquery_fdw.use_query_cache가 기본값 on이라 같은 쿼리를 반복하면 BigQuery 결과 캐시를 씁니다.
BigQuery 테이블이 바뀌면 캐시는 무효화되므로 결과가 오래되지는 않습니다.
처리량을 제대로 보려면 세션에서 SET bigquery_fdw.use_query_cache = off;로 끄고 측정해야 합니다.
측정에서 확인한 점은 세 가지입니다.
- 푸시다운 유무 차이: 예상과 달리 이 데이터 규모에서는 응답 시간도, BigQuery가 읽은 양도 거의 같았습니다.
BigQuery는WHERE가 있어도 해당 컬럼을 끝까지 읽기 때문에, 푸시다운 쿼리의 처리량(4.75MB)과 푸시다운이 안 된 경우 Storage Read API로 읽는 양(같은 3개 컬럼, 4.75MB)이 같습니다.
차이는 AlloyDB로 넘어오는 행 수(집계 결과 27,575행 vs 원본 180,946행)와 과금 경로(쿼리 작업 vs Storage Read API)에 있습니다.
이번 실습에서는 푸시다운이 속도를 높이는 효과를 확인하지 못했습니다.
파티션·클러스터링 테이블에서는 결과가 다를 수 있으며, 이번에는 측정하지 않았습니다.
행 수가 훨씬 큰 테이블에서는 전송량 차이가 응답 시간에 영향을 줄 수 있지만, 이 글에서는 측정하지 않았습니다.
푸시다운 여부는 실행 계획의 Remote SQL로 확인해 두는 편이 좋습니다. - 실시간 vs 로컬: 실시간 조회는 BigQuery 작업 생성과 결과 전송 때문에 매번 1.2~1.5초가 걸렸고, 이 중 BigQuery 작업 자체는 0.1~0.4초(JOBS의
end_time - start_time)였습니다.
로컬 테이블 JOIN은 서버에서 3.7ms로 끝났습니다. - 비용 구조: 실시간 조회는 호출 횟수에 비례하고(쿼리 캐시가 꺼져 있거나 캐시가 무효화된 경우, 이번 쿼리는 처리량이 작아도 회당 최소 10MB 과금. 캐시 적중 쿼리는 과금되지 않음), 동기화는 갱신 주기에 비례합니다(동기화 1회당 Storage Read API 읽기 + 메타데이터 조회 10MB). 하루 호출 수와 갱신 주기를 놓고 손익분기점을 계산해 볼 수 있습니다.
결론적으로 실시간 조회는 1.3~1.5초대, 동기화한 로컬 테이블은 네트워크 왕복을 포함해도 174ms(서버 실행만 보면 3.7ms)로 약 8배 차이가 났습니다.
같은 리전의 애플리케이션 서버에서 호출한다면 왕복 지연이 빠지므로 차이는 더 커집니다.
언제 무엇을 쓸까
정리하면 선택 기준은 “얼마나 최신이어야 하나”와 “얼마나 자주 조회하나” 두 가지입니다.
실측에서 실시간 조회는 푸시다운 여부와 관계없이 호출마다 1.2~1.5초가 걸렸으므로, 응답 속도가 중요한 경로라면 푸시다운보다 동기화 여부가 더 큰 변수입니다.
| 상황 | 추천 방식 |
| 분석가·운영자가 가끔 최신 데이터를 운영 데이터와 엮어 봄 | 실시간 조회 |
| ML 예측 결과·세그먼트를 한 번 가져와 앱에서 가공 | 일회성 동기화 |
| 고객 화면·API에서 초당 수십~수백 번 조회 | 주기 동기화 + 읽기 풀 |
| 조회 빈도가 높고 실시간성도 필요 | 기존 CDC/Reverse ETL 파이프라인 검토 |
이 기능의 가장 큰 가치는 “일단 SQL로 붙여 보고, 필요해지면 동기화로 바꾸는” 점진적 접근이 가능하다는 것입니다.
파이프라인부터 설계하던 기존 방식과 비교하면 검증 비용이 크게 줄어듭니다.
아직 Preview이고 동기화가 PostgreSQL 18 전용이라는 제약은 있지만, 운영 DB로 AlloyDB를 쓰고 있다면 지금 PoC로 시험해 보기 좋은 시점입니다.
실습 리소스 정리
-- AlloyDB
SELECT alloydb_sync.delete_bq_sync_table('public.user_lifetime_mirror');
DROP TABLE IF EXISTS user_lifetime_snapshot, orders_today;
DROP FOREIGN TABLE IF EXISTS bq_order_items;
DROP SERVER IF EXISTS bigquery_server CASCADE;
IAM 역할 회수는 클러스터를 지우기 전에 합니다. SA 변수에 클러스터 서비스 계정이 들어 있어야 하는데, 클러스터를 먼저 지우면 describe로 다시 조회할 수 없습니다.
사전 준비 3단계와 다른 셸이라면 SA를 다시 설정하세요.
# 클러스터 서비스 계정 조회 (사전 준비 3단계와 같은 명령)
SA=$(gcloud beta alloydb clusters describe my-cluster \
--region=us-central1 --format='value(serviceAccountEmail)')
# IAM 역할 회수 (클러스터를 지워도 바인딩은 남습니다)
for ROLE in roles/bigquery.dataViewer roles/bigquery.readSessionUser roles/bigquery.jobUser; do
gcloud projects remove-iam-policy-binding my-project \
--member="serviceAccount:${SA}" --role="${ROLE}"
done
# BigQuery 데이터셋, AlloyDB 클러스터 삭제
bq rm -r -f my-project:thelook
gcloud alloydb clusters delete my-cluster --region=us-central1 --force
참고 자료
- Choose how to access BigQuery data from AlloyDB
- Configure AlloyDB access to real-time data in BigQuery
- Lakehouse Federation in AlloyDB overview
- Sync BigQuery data to AlloyDB
- AlloyDB release notes
- What’s new for Google Cloud databases at Next ’26
궁금한 점이 있으신가요?
자세한 문의는 GS Neotek 홈페이지를 이용해 주세요.



최신 댓글