(KO) PostgreSQL Foreign Data Wrapper — 인-코어 FDW 메커니즘
목차
이론적 배경
섹션 제목: “이론적 배경”외부 데이터 래퍼는 데이터베이스가 반복해서 맞닥뜨리는 문제에 대한 해법이다. 질의가 로컬 스토리지 엔진 바깥에 있는 데이터를 읽어야 할 때, 그 데이터는 원격 PostgreSQL 서버일 수도 있고, CSV 파일일 수도 있고, REST API, 다른 RDBMS, 또는 컬럼형 분석 스토어일 수도 있다. 모든 시스템이 답해야 하는 질문은 하나다: 외부 시스템 전체를 엔진 안으로 흡수하지 않고도 SQL이 외부 데이터를 참조하게 할 방법은 무엇인가?
크게 세 가지 접근 방식이 있다.
ETL / 구체화 — 외부 데이터를 일정 주기로 로컬 테이블에 복사한다. 질의 성능은 빠르고 일반 스토리지 엔진을 그대로 쓸 수 있지만, 로드 사이에 데이터가 낡고 복사본이 스토리지를 두 배로 쓴다. 이것은 데이터베이스 기능이 아니라 외부 파이프라인이다.
연합(Federation) / 중재(Mediation) — 데이터베이스가 외부 소스에 대한 기술 정보만 보관하고, 각 질의를 소스가 이해할 수 있는 연산으로 번역해 필요한 행만 가져온다. 복사본을 만들지 않으므로 항상 최신 데이터를 볼 수 있다. 대신 옵티마이저는 통계·비용 모델·능력 범위를 스스로 제어할 수 없는 소스를 대상으로 추론해야 한다. 이것이 데이터 통합 문헌에서 말하는 미디에이터-래퍼 아키텍처다(Wiederhold의 미디에이터 모델, Garlic과 TSIMMIS 연구 시스템). 소스별 래퍼가 균일한 인터페이스를 드러내고, 미디에이터(여기서는 플래너)가 래퍼들을 조합해 질의 플랜을 구성한다.
폴리스토어(Polystore) / 멀티스토어 — 질의 엔진이 여러 전문화된 스토어 위에 위치하며, 서브쿼리를 가장 잘 처리할 수 있는 스토어로 라우팅한다. 이는 연합을 능력이 크게 다른 이종 엔진으로 일반화한 것이다(BigDAWG, Myria). PostgreSQL의 FDW 계층은 단일 엔진 연합 메커니즘이지만, 소스별 능력 협상(어느 조건·조인·집계를 푸시다운할 수 있는지)은 폴리스토어가 더 큰 규모에서 푸는 문제와 같다.
PostgreSQL의 FDW 기구는 두 번째 방식을 구현하며, 이를 SQL:2003 표준의 일부인 SQL/MED(“Management of External Data”, 외부 데이터 관리)로 규격화한다. SQL/MED는 카탈로그 어휘—foreign-data wrapper, foreign server, user mapping, foreign table—를 정의하고, PostgreSQL은 C 콜백 API로 이를 구현한다. 래퍼는 핸들러 함수 하나를 제공하는 로드 가능한 확장이다. 핸들러가 함수 포인터 구조체를 반환하면, 플래너와 익스큐터가 정해진 시점에 그 포인터들을 호출한다. 엔진은 파스 트리, 옵티마이저 탐색, 익스큐터 튜플 파이프라인을 소유하고, 래퍼는 원격 소스와 통신하는 모든 것을 소유한다.
개념적 계약은 두 부분으로 나뉜다.
-
기술(Description) — 카탈로그의 정적 메타데이터. 외부 테이블의 컬럼 목록과
OPTIONS (...)백(예: 원격 테이블 이름, 파일 경로)이 외부 객체가 무엇인지를 래퍼에 알린다. foreign server는 연결 끝점을 명명하고, user mapping은 역할별 자격증명을 제공한다. -
행위(Behavior) —
FdwRoutine콜백들. 외부 객체에 어떻게 크기를 추정하고, 플랜을 세우고, 실행할지를 엔진에 알린다. 이 분리는 미디에이터-래퍼 분리를 그대로 반영한다. 카탈로그가 미디에이터의 소스 기술이고, 콜백이 래퍼의 런타임이다.
Database System Concepts(Silberschatz, Korth, Sudarshan)의 데이터 통합·이종/분산 데이터베이스 장에서는 긴밀히 결합된 분산 데이터베이스(하나의 엔진, 공유 카탈로그, 공유 트랜잭션 매니저)와 래퍼로 자율 소스에 접근하는 느슨하게 결합된 연합을 대비시킨다. 그 책이 지목한 핵심 긴장—비용 모델을 모르는 자율 소스에 대한 질의 최적화—이 정확히 PostgreSQL의 GetForeignRelSize / GetForeignPaths 콜백이 해소하는 문제다. 엔진은 소스별 비용과 능력 결정을 래퍼에 위임하고, 래퍼의 ForeignPath를 자신의 비용 기반 경로 경쟁에 편입한다.
SQL/MED 구현자가 선택해야 하는 설계 공간:
-
행 단위 vs. 집합 단위 가져오기 — 익스큐터가 콜백 호출마다 튜플 하나를 당기는가, 래퍼가 배치를 구체화하는가? PostgreSQL의
IterateForeignScan은 계약상 행 단위지만, 래퍼는 내부적으로 커서 분량의 행을 버퍼에 담고 하나씩 내보낼 수 있다. -
얼마나 많은 연산을 푸시다운하는가 — 테이블 스캔만, 아니면 필터·조인·집계·정렬·LIMIT도? 더 많이 내려보낼수록 네트워크를 오가는 데이터가 줄지만 래퍼가 복잡해진다. PostgreSQL은 이를 단계적 선택적 콜백 집합으로 드러낸다(
GetForeignPaths: 조건절,GetForeignJoinPaths: 조인,GetForeignUpperPaths: 집계/정렬/LIMIT). -
쓰기 지원 여부 — 소스가
INSERT/UPDATE/DELETE를 지원해야 하는가? PostgreSQL은 수정 콜백을 전부 선택적으로 만든다. 읽기 전용 래퍼는 그 포인터들을NULL로 두면 된다.
공통 DBMS 설계
섹션 제목: “공통 DBMS 설계”연합 엔진들은 작은 수의 구조적 관례로 수렴한다. 이를 명명해 두면 PostgreSQL의 특정 심볼이 임의적 발명이 아니라 공유된 플레이북 안의 한 가지 선택임을 알 수 있다.
이름으로 로드하는 소스별 디스크립터 객체
섹션 제목: “이름으로 로드하는 소스별 디스크립터 객체”모든 연합 계층은 각 외부 소스를 안정적인 식별자와 이름이 있는 일급(first-class) 카탈로그 객체로 표현한다. SQL/MED는 이를 foreign server라 부르고, 다른 시스템은 linked server(SQL Server), database link(Oracle DBLINK), connection이라 부른다. 디스크립터는 래퍼 유형과 자유 형식의 옵션 백을 기록하므로, 엔진이 연결 세부 정보를 하드코딩하지 않아도 된다.
플랜 시점의 능력 협상 핸드셰이크
섹션 제목: “플랜 시점의 능력 협상 핸드셰이크”소스가 자율적이므로 옵티마이저는 소스가 산술 연산을 처리하거나, 정규표현식을 평가하거나, 해시 조인을 수행할 수 있다고 가정할 수 없다. 공통 패턴은 *능력 질의(capability query)*다. 플랜 시점에 엔진이 래퍼에 어떤 연산을 떠맡을 의향이 있는지 묻고, 래퍼는 로컬 평가 집합에서 절을 제거(원격으로 내려보냄)하거나 유지(로컬에서 재검사)하는 방식으로 답한다. PostgreSQL은 이를 GetForeignPaths / GetForeignPlan 안에서 구현한다. 래퍼가 scan_clauses를 원격 부분과 로컬 재검사 부분으로 분리하는 것이다.
커서 유사 런타임 인터페이스
섹션 제목: “커서 유사 런타임 인터페이스”실행 시점에 엔진은 데이터베이스 커서를 닮은 작고 상태 있는 인터페이스를 구동한다. 열기(원격 질의/연결 수립), 다음(행 가져오기), 닫기(리소스 해제). 선택적 재스캔은 중첩 루프 재실행을 위해 커서를 되감는다. PostgreSQL은 이를 BeginForeignScan, IterateForeignScan, ReScanForeignScan, EndForeignScan이라 명명한다.
소스 디스크립터와 분리된 자격증명
섹션 제목: “소스 디스크립터와 분리된 자격증명”하나의 소스에 여러 역할이 각자의 자격증명으로 접근한다. 연합 계층은 무엇(what)(서버)과 누구(who)(역할별 자격증명)를 분리한다. SQL/MED의 user mapping은 (역할, 서버) 쌍을 사용자명/패스워드를 담은 옵션 백에 바인딩한다. PostgreSQL은 이를 pg_user_mapping에 저장하고, 역할별 매핑이 없으면 PUBLIC 매핑으로 폴백하는 GetUserMapping으로 조회한다.
래퍼 본체를 위한 함수 포인터 디스패치
섹션 제목: “래퍼 본체를 위한 함수 포인터 디스패치”엔진이 가능한 모든 소스 드라이버에 링크할 수는 없다. 이식 가능한 패턴은 vtable 간접 참조다. 래퍼는 함수 포인터 구조체를 반환하는 진입점 하나를 드러내는 로드 가능 모듈이고, 엔진은 그 구조체로 호출한다. PostgreSQL이 테이블 접근 방법(TableAmRoutine), 인덱스 접근 방법(IndexAmRoutine), 커스텀 스캔(CustomScanMethods)에 쓰는 것과 같은 형태다. FDW의 vtable은 FdwRoutine이며, 핸들러 함수가 반환하고 GetFdwRoutine이 가져온다.
이론 ↔ PostgreSQL 매핑
섹션 제목: “이론 ↔ PostgreSQL 매핑”| 개념 | PostgreSQL 이름 |
|---|---|
| 래퍼 유형 / 드라이버 | foreign-data wrapper (pg_foreign_data_wrapper, ForeignDataWrapper) |
| 소스 디스크립터 | foreign server (pg_foreign_server, ForeignServer) |
| 역할별 자격증명 | user mapping (pg_user_mapping, UserMapping) |
| 카탈로그된 외부 객체 | foreign table (pg_foreign_table, ForeignTable) |
| 래퍼 vtable | FdwRoutine 구조체 (핸들러가 반환, makeNode(FdwRoutine)) |
| 핸들러 진입점 | fdwhandler 함수, fdw_handler 반환 (FDW_HANDLEROID) |
| vtable 가져오기 | GetFdwRoutine, GetFdwRoutineForRelation |
| 능력 협상 | GetForeignPaths / GetForeignPlan 절 분리 |
| 커서 열기/다음/닫기 | BeginForeignScan / IterateForeignScan / EndForeignScan |
| 플랜 시점 크기 추정 | GetForeignRelSize (set_foreign_size에서 호출) |
| 익스큐터 플랜 노드 | ForeignScan (create_foreignscan_plan이 생성) |
| 익스큐터 상태 노드 | ForeignScanState (scanstate->fdwroutine, fdw_state) |
| 쓰기 경로 | PlanForeignModify + ExecForeignInsert/Update/Delete |
PostgreSQL의 접근 방식
섹션 제목: “PostgreSQL의 접근 방식”PostgreSQL은 FDW 기구를 세 계층으로 나눈다. 카탈로그 계층(래퍼·서버·매핑·테이블을 기술하는 네 개의 시스템 카탈로그), DDL 계층(foreigncmds.c, 카탈로그 유지), 디스패치 + 통합 계층(foreign.c, 핸들러를 FdwRoutine으로 해석하고 플래너와 익스큐터가 콜백을 호출)이다. 래퍼 작성자가 제공하는 것은 핸들러 C 함수 하나뿐이며, 구현하고 싶은 콜백을 채우면 된다.
FdwRoutine vtable: 필수 콜백과 선택적 콜백
섹션 제목: “FdwRoutine vtable: 필수 콜백과 선택적 콜백”핸들러는 makeNode(FdwRoutine) 구조체를 반환한다. 일곱 개의 스캔 콜백은 필수이고, 나머지는 모두 선택적이며 NULL로 둘 수 있다. 헤더에 계약이 직접 명시되어 있다.
// FdwRoutine — src/include/foreign/fdwapi.htypedef struct FdwRoutine{ NodeTag type;
/* Functions for scanning foreign tables */ GetForeignRelSize_function GetForeignRelSize; GetForeignPaths_function GetForeignPaths; GetForeignPlan_function GetForeignPlan; BeginForeignScan_function BeginForeignScan; IterateForeignScan_function IterateForeignScan; ReScanForeignScan_function ReScanForeignScan; EndForeignScan_function EndForeignScan;
/* * Remaining functions are optional. Set the pointer to NULL for any that * are not provided. */ /* Functions for remote-join planning */ GetForeignJoinPaths_function GetForeignJoinPaths; /* Functions for remote upper-relation (post scan/join) planning */ GetForeignUpperPaths_function GetForeignUpperPaths; /* Functions for updating foreign tables */ AddForeignUpdateTargets_function AddForeignUpdateTargets; PlanForeignModify_function PlanForeignModify; BeginForeignModify_function BeginForeignModify; ExecForeignInsert_function ExecForeignInsert; /* ... ExecForeignBatchInsert, ExecForeignUpdate, ExecForeignDelete, ... */ EndForeignModify_function EndForeignModify; /* ... direct-modify, row-locking, EXPLAIN, ANALYZE, IMPORT, TRUNCATE, */ /* parallelism, path reparameterization, async execution ... */} FdwRoutine;헤더의 주석은 래퍼 작성자가 따라야 할 설계 규칙을 담고 있다.
// FdwRoutine comment — src/include/foreign/fdwapi.h * More function pointers are likely to be added in the future. Therefore * it's recommended that the handler initialize the struct with * makeNode(FdwRoutine) so that all fields are set to NULL. This will * ensure that no fields are accidentally left undefined.makeNode는 할당을 영-초기화(zero-fill)하므로, 구현하지 않은 선택적 콜백은 자동으로 NULL이 된다. 엔진은 선택적 포인터를 호출하기 전에 NULL 여부를 확인한다. 읽기 전용·스캔 전용 래퍼가 작게 유지될 수 있는 이유가 여기 있다. 포인터 일곱 개를 채우고 나머지 ~40개를 NULL로 두면 된다.
관계에서 vtable로 해석
섹션 제목: “관계에서 vtable로 해석”플래너나 익스큐터가 외부 테이블의 콜백을 필요로 할 때, 다음 경로를 따라간다. 외부 테이블 OID → foreign server OID → FDW OID → 핸들러 함수 OID → 핸들러 호출 → FdwRoutine. GetFdwRoutine이 마지막 단계를 수행하며, 핸들러를 함수 매니저로 호출하고 결과를 타입 검사한다.
// GetFdwRoutine — src/backend/foreign/foreign.cFdwRoutine *GetFdwRoutine(Oid fdwhandler){ Datum datum; FdwRoutine *routine;
/* Check if the access to foreign tables is restricted */ if (unlikely((restrict_nonsystem_relation_kind & RESTRICT_RELKIND_FOREIGN_TABLE) != 0)) ereport(ERROR, ...);
datum = OidFunctionCall0(fdwhandler); routine = (FdwRoutine *) DatumGetPointer(datum);
if (routine == NULL || !IsA(routine, FdwRoutine)) elog(ERROR, "foreign-data wrapper handler function %u did not return an FdwRoutine struct", fdwhandler); return routine;}익스큐터 전용 헬퍼 GetFdwRoutineForRelation은 결과를 relcache 항목(rd_fdwroutine)에 캐시해 질의 중 반복 조회 비용을 없앤다.
// GetFdwRoutineForRelation — src/backend/foreign/foreign.cFdwRoutine *GetFdwRoutineForRelation(Relation relation, bool makecopy){ FdwRoutine *fdwroutine; FdwRoutine *cfdwroutine;
if (relation->rd_fdwroutine == NULL) { /* Get the info by consulting the catalogs and the FDW code */ fdwroutine = GetFdwRoutineByRelId(RelationGetRelid(relation)); /* Save the data for later reuse in CacheMemoryContext */ cfdwroutine = (FdwRoutine *) MemoryContextAlloc(CacheMemoryContext, sizeof(FdwRoutine)); memcpy(cfdwroutine, fdwroutine, sizeof(FdwRoutine)); relation->rd_fdwroutine = cfdwroutine; return fdwroutine; } /* ... return cached copy, optionally palloc'd ... */ return relation->rd_fdwroutine;}해석 체인은 시스템캐시 조회의 연속이다(GetForeignServerIdByRelId → GetFdwRoutineByServerId → GetFdwRoutine). 각 단계가 카탈로그 행 하나를 읽는다.
flowchart TD
A["foreign table OID<br/>(RelationGetRelid)"] --> B["GetForeignServerIdByRelId<br/>pg_foreign_table.ftserver 읽기"]
B --> C["GetFdwRoutineByServerId<br/>pg_foreign_server.srvfdw 읽기<br/>→ pg_foreign_data_wrapper.fdwhandler"]
C --> D["GetFdwRoutine<br/>OidFunctionCall0(fdwhandler)"]
D --> E["핸들러가<br/>makeNode(FdwRoutine) 반환"]
E --> F["IsA(routine, FdwRoutine)?<br/>아니면 ERROR"]
F --> G["Relation.rd_fdwroutine에 캐시<br/>(GetFdwRoutineForRelation)"]
네 개의 카탈로그와 인메모리 미러
섹션 제목: “네 개의 카탈로그와 인메모리 미러”foreign.c는 카탈로그마다 Get* 접근자를 제공한다. 시스템캐시 행을 읽어 C 구조체를 palloc한다. GetForeignServer가 대표적이다. pg_foreign_server 튜플을 언패킹하며, srvoptions text 배열을 untransformRelOptions로 DefElem 리스트로 디코딩한다.
// GetForeignServerExtended — src/backend/foreign/foreign.cserver = (ForeignServer *) palloc(sizeof(ForeignServer));server->serverid = serverid;server->servername = pstrdup(NameStr(serverform->srvname));server->owner = serverform->srvowner;server->fdwid = serverform->srvfdw;/* Extract the srvoptions */datum = SysCacheGetAttr(FOREIGNSERVEROID, tp, Anum_pg_foreign_server_srvoptions, &isnull);server->options = isnull ? NIL : untransformRelOptions(datum);user mapping은 자격증명 폴백 규칙을 추가한다. (userid, serverid) 조회에 실패하면 InvalidOid로 재시도해 PUBLIC 매핑을 찾는다.
// GetUserMapping — src/backend/foreign/foreign.ctp = SearchSysCache2(USERMAPPINGUSERSERVER, ObjectIdGetDatum(userid), ObjectIdGetDatum(serverid));if (!HeapTupleIsValid(tp)){ /* Not found for the specific user -- try PUBLIC */ tp = SearchSysCache2(USERMAPPINGUSERSERVER, ObjectIdGetDatum(InvalidOid), ObjectIdGetDatum(serverid));}플래너 통합: 크기 → 경로 → 플랜
섹션 제목: “플래너 통합: 크기 → 경로 → 플랜”플래너는 외부 테이블을 일반 베이스 관계처럼 처리하되, 세 개의 훅이 래퍼에 위임한다. 크기 단계에서 set_foreign_size가 GetForeignRelSize를 호출하면 래퍼가 행 수와 너비 추정치를 정제한다. 경로 생성 단계에서 set_foreign_pathlist가 GetForeignPaths를 호출하면 래퍼가 하나 이상의 ForeignPath 노드를 추가해 일반 비용 기반 경로 경쟁에 참여시킨다.
// set_foreign_pathlist — src/backend/optimizer/path/allpaths.cstatic voidset_foreign_pathlist(PlannerInfo *root, RelOptInfo *rel, RangeTblEntry *rte){ /* Call the FDW's GetForeignPaths function to generate path(s) */ rel->fdwroutine->GetForeignPaths(root, rel, rte->relid);}ForeignPath가 경쟁에서 이기면 create_foreignscan_plan이 GetForeignPlan을 호출해 ForeignScan 플랜 노드를 구체화한다. 비용, 서버 OID, relid 같은 엔진 소유 필드는 래퍼가 채울 필요 없도록 엔진이 직접 복사한다.
// create_foreignscan_plan — src/backend/optimizer/plan/createplan.cscan_plan = rel->fdwroutine->GetForeignPlan(root, rel, rel_oid, best_path, tlist, scan_clauses, outer_plan);/* Copy cost data from Path to Plan; no need to make FDW do this */copy_generic_path_info(&scan_plan->scan.plan, &best_path->path);scan_plan->checkAsUser = rel->userid;scan_plan->fs_server = rel->serverid;flowchart LR
subgraph Planner
P1["set_foreign_size<br/>→ GetForeignRelSize"] --> P2["set_foreign_pathlist<br/>→ GetForeignPaths"]
P2 --> P3["create_foreignscan_plan<br/>→ GetForeignPlan"]
end
P3 --> N["ForeignScan 플랜 노드<br/>fdw_private, fdw_exprs"]
subgraph Executor
N --> E1["ExecInitForeignScan<br/>→ BeginForeignScan"]
E1 --> E2["ForeignNext 루프<br/>→ IterateForeignScan"]
E2 --> E3["ExecEndForeignScan<br/>→ EndForeignScan"]
end
익스큐터 통합: 스캔 커서
섹션 제목: “익스큐터 통합: 스캔 커서”ExecInitForeignScan은 ForeignScanState를 생성하고, vtable을 조회(베이스 관계 스캔은 relcache, 조인·상위 스캔은 GetFdwRoutineByServerId)해 scanstate->fdwroutine에 저장한 뒤 BeginForeignScan으로 커서를 연다.
// ExecInitForeignScan — src/backend/executor/nodeForeignscan.cif (scanrelid > 0){ currentRelation = ExecOpenScanRelation(estate, scanrelid, eflags); scanstate->ss.ss_currentRelation = currentRelation; fdwroutine = GetFdwRoutineForRelation(currentRelation, true);}else{ /* We can't use the relcache, so get fdwroutine the hard way */ fdwroutine = GetFdwRoutineByServerId(node->fs_server);}/* ... */scanstate->fdwroutine = fdwroutine;scanstate->fdw_state = NULL;/* ... */if (node->operation != CMD_SELECT){ if (estate->es_epq_active == NULL) fdwroutine->BeginDirectModify(scanstate, eflags);}else fdwroutine->BeginForeignScan(scanstate, eflags);scanstate->fdw_state는 래퍼가 소유하는 void * 스크래치 슬롯이다. 엔진은 그 내용에 손대지 않는다. ExecForeignScan 호출마다 ForeignNext가 호출되고, 이는 튜플별 메모리 컨텍스트 안에서 IterateForeignScan을 호출해 래퍼의 할당이 행마다 재설정되도록 한다.
// ForeignNext — src/backend/executor/nodeForeignscan.coldcontext = MemoryContextSwitchTo(econtext->ecxt_per_tuple_memory);if (plan->operation != CMD_SELECT) slot = node->fdwroutine->IterateDirectModify(node);else slot = node->fdwroutine->IterateForeignScan(node);MemoryContextSwitchTo(oldcontext);IterateForeignScan에서 NULL이나 빈 슬롯이 오면 스캔 종료 신호다. ExecReScanForeignScan은 ReScanForeignScan을 호출해 중첩 루프 재실행을 위해 커서를 되감고, ExecEndForeignScan은 EndForeignScan으로 리소스를 해제한다. 계약은 공통 DBMS 설계 절에서 명명한 열기/다음/재스캔/닫기 커서 인터페이스와 정확히 같다.
쓰기 경로: ModifyTable 콜백
섹션 제목: “쓰기 경로: ModifyTable 콜백”쓰기는 선택 사항이다. 플랜 시점에 create_modifytable_plan이 PlanForeignModify를 호출하면 래퍼는 원격 DML 텍스트를 생성해 fdw_private에 저장한다. 익스큐터 초기화 시 ExecInitModifyTable이 BeginForeignModify를 제공하는 각 외부 결과 릴레이션마다 이를 호출한다.
// ExecInitModifyTable (foreign init) — src/backend/executor/nodeModifyTable.cif (!resultRelInfo->ri_usesFdwDirectModify && resultRelInfo->ri_FdwRoutine != NULL && resultRelInfo->ri_FdwRoutine->BeginForeignModify != NULL){ List *fdw_private = (List *) list_nth(node->fdwPrivLists, i);
resultRelInfo->ri_FdwRoutine->BeginForeignModify(mtstate, resultRelInfo, fdw_private, i, eflags);}행 단위로 익스큐터는 ri_FdwRoutine의 포인터로 ExecForeignInsert, ExecForeignUpdate, ExecForeignDelete를 호출한다. 래퍼가 GetForeignModifyBatchSize와 ExecForeignBatchInsert도 제공하면, 익스큐터가 삽입을 배치로 묶어 왕복 비용을 줄인다. 쓰기 전략은 두 가지다.
-
행별 수정 — 엔진이 대상 행을 하나씩 가져와(대개
ForeignScan서브플랜) 행별 수정 콜백을 호출한다. 어떤 소스에든 동작하지만 행마다 왕복 비용이 발생한다. -
직접 수정(direct modify) — 래퍼가 전체
UPDATE/DELETE를 하나의 원격 문장으로 번역할 수 있으면, 플랜 시점에PlanDirectModify가 true를 반환하고 익스큐터가ForeignScan노드 위에서BeginDirectModify/IterateDirectModify/EndDirectModify를 구동한다.ri_usesFdwDirectModify가 이 선택을 기록하며, 위의 가드가 그 경우BeginForeignModify를 건너뛴다.
DDL 계층: 카탈로그 유지
섹션 제목: “DDL 계층: 카탈로그 유지”foreigncmds.c가 네 가지 객체 유형 각각에 CREATE/ALTER/DROP을 구현한다. CreateForeignDataWrapper는 슈퍼유저를 요구하고, HANDLER/VALIDATOR 함수 이름을 OID로 해석해 pg_foreign_data_wrapper 행을 삽입한다. 핸들러 이름 해석은 반환 타입 계약을 적용해 vtable 디스패치가 타입 안전하게 유지되도록 한다.
// lookup_fdw_handler_func — src/backend/commands/foreigncmds.chandlerOid = LookupFuncName((List *) handler->arg, 0, NULL, false);/* check that handler has correct return type */if (get_func_rettype(handlerOid) != FDW_HANDLEROID) ereport(ERROR, (errcode(ERRCODE_WRONG_OBJECT_TYPE), errmsg("function %s must return type %s", NameListToString((List *) handler->arg), "fdw_handler")));선택적 VALIDATOR 함수(시그니처 (text[], oid))는 래퍼, 서버, 매핑, 테이블에 옵션이 설정될 때마다 transformGenericOptions 안에서 호출되며, 래퍼가 잘못된 옵션을 질의 시점이 아닌 DDL 시점에 거부할 수 있게 한다. foreign.c에는 더 이상 사용되지 않는 참조 검증기 postgresql_fdw_validator가 있으며, 소규모 libpq conninfo 허용 목록에 맞춰 옵션을 확인한다.
ImportForeignSchema는 DDL 경로 중 가장 흥미롭다. vtable로 진입하기 때문이다. 래퍼의 ImportForeignSchema 콜백을 호출하면 CREATE FOREIGN TABLE SQL 문자열 목록이 반환되고, 이 명령이 각각을 파싱해 ProcessUtility로 실행한다.
// ImportForeignSchema — src/backend/commands/foreigncmds.cfdw_routine = GetFdwRoutine(fdw->fdwhandler);if (fdw_routine->ImportForeignSchema == NULL) ereport(ERROR, (errcode(ERRCODE_FDW_NO_SCHEMAS), errmsg("foreign-data wrapper \"%s\" does not support IMPORT FOREIGN SCHEMA", fdw->fdwname)));/* Call FDW to get a list of commands */cmd_list = fdw_routine->ImportForeignSchema(stmt, server->serverid);테이블별 필터(LIMIT TO / EXCEPT)는 코어 헬퍼 IsImportableForeignTable이 적용한다. 래퍼는 어떤 테이블이 존재하는지만 결정하고, 어떤 테이블을 실제로 만들지는 엔진이 결정한다.
소스 워크스루
섹션 제목: “소스 워크스루”계층별 심볼 목록. 파일은 /data/hgryoo/references/postgres/ 아래에 있다.
vtable과 접근자 (fdwapi.h, foreign.c)
섹션 제목: “vtable과 접근자 (fdwapi.h, foreign.c)”FdwRoutine(구조체) — 콜백 vtable. 7개의 필수 스캔 콜백과 ~40개의 선택적 콜백(조인/상위 푸시다운, 수정, 직접 수정, 행 잠금, EXPLAIN, ANALYZE, IMPORT, TRUNCATE, 병렬, 비동기).GetForeignRelSize_function/GetForeignPaths_function/GetForeignPlan_function(typedef) — 세 개의 플래너 콜백 시그니처.BeginForeignScan_function/IterateForeignScan_function/ReScanForeignScan_function/EndForeignScan_function(typedef) — 익스큐터 커서 콜백 시그니처.GetFdwRoutine— 핸들러 OID 호출, 반환된FdwRoutine타입 검사.restrict_nonsystem_relation_kind준수.GetFdwRoutineByServerId— 서버 OID → FDW → 핸들러 →GetFdwRoutine.GetFdwRoutineByRelId— 관계 OID →GetForeignServerIdByRelId→GetFdwRoutineByServerId.GetFdwRoutineForRelation— relcache 캐시된 래퍼(rd_fdwroutine). 익스큐터의 기본 진입점.GetForeignServerIdByRelId—pg_foreign_table.ftserver읽기.IsImportableForeignTable—LIMIT TO/EXCEPT필터 적용.GetExistingLocalJoinPath— 푸시다운된 외부 조인의 EPQ 재검사를 위한 로컬 조인 경로 가져오기.
카탈로그 접근자 (foreign.c)
섹션 제목: “카탈로그 접근자 (foreign.c)”GetForeignDataWrapper/GetForeignDataWrapperExtended/GetForeignDataWrapperByName—pg_foreign_data_wrapper→ForeignDataWrapper.GetForeignServer/GetForeignServerExtended/GetForeignServerByName—pg_foreign_server→ForeignServer.GetUserMapping—(userid, serverid)→UserMapping,PUBLIC(InvalidOid) 폴백 포함.GetForeignTable—pg_foreign_table→ForeignTable.GetForeignColumnOptions— 속성별attfdwoptions.get_foreign_data_wrapper_oid/get_foreign_server_oid— 이름 → OID.postgresql_fdw_validator— 더 이상 사용되지 않는 참조 검증기(libpq conninfo 허용 목록).pg_options_to_table— 옵션 배열을 이름/값 행으로 드러내는 SRF.
DDL 명령 (foreigncmds.c)
섹션 제목: “DDL 명령 (foreigncmds.c)”CreateForeignDataWrapper— 슈퍼유저 전용. 핸들러/검증기 해석.pg_foreign_data_wrapper삽입.lookup_fdw_handler_func/lookup_fdw_validator_func—FDW_HANDLEROID반환 타입 /(text[],oid)인자 타입 검사와 함께 이름 → OID.parse_func_options— 파스 트리에서HANDLER/VALIDATOR파싱.optionListToArray/transformGenericOptions—DefElem리스트 ↔ text[] 배열. 후자가 검증기를 호출한다.CreateForeignServer—pg_foreign_server삽입. FDW에 의존.CreateUserMapping—pg_user_mapping삽입.CreateForeignTable—pg_foreign_table삽입(힙 릴레이션은 일반 테이블 생성 경로가 먼저 만든다).ImportForeignSchema— 래퍼의ImportForeignSchema콜백 호출, 필터 적용, 반환된CREATE FOREIGN TABLE각각을ProcessUtility로 실행.
플래너 통합 (allpaths.c, createplan.c, appendinfo.c)
섹션 제목: “플래너 통합 (allpaths.c, createplan.c, appendinfo.c)”set_foreign_size—set_foreign_size_estimates,rel->fdwroutine->GetForeignRelSize,clamp_row_est순서.set_foreign_pathlist—rel->fdwroutine->GetForeignPaths.create_foreignscan_plan—rel->fdwroutine->GetForeignPlan. 비용,checkAsUser,fs_server,fs_relids를ForeignScan에 복사.PlanForeignModify호출 지점 (create_modifytable_plan) —fdwPrivLists구성.AddForeignUpdateTargets호출 지점 (appendinfo.c) — 래퍼가 행 식별에 필요한 정크(junk) 컬럼 추가.
익스큐터 통합 (nodeForeignscan.c, nodeModifyTable.c)
섹션 제목: “익스큐터 통합 (nodeForeignscan.c, nodeModifyTable.c)”ExecInitForeignScan—ForeignScanState구성, vtable 해석,BeginForeignScan(또는BeginDirectModify) 호출.ForeignNext— 튜플별 컨텍스트에서IterateForeignScan(또는IterateDirectModify) 호출.ForeignRecheck—RecheckForeignScan+fdw_recheck_quals를 통한 EPQ 재검사.ExecForeignScan—ExecScan(ForeignNext, ForeignRecheck).ExecReScanForeignScan—ReScanForeignScan.ExecEndForeignScan—EndForeignScan(또는EndDirectModify).ExecForeignScanEstimate/…InitializeDSM/…InitializeWorker/…ReInitializeDSM— 선택적EstimateDSMForeignScan계열로의 병렬 스캔 DSM 배관.ExecInitModifyTable(외부 브랜치) —BeginForeignModify.ExecForeignInsert/ExecForeignBatchInsert/ExecForeignUpdate/ExecForeignDelete호출 지점 — 행별/배치별 DML.ExecEndModifyTable(외부 브랜치) —EndForeignModify.
위치 힌트 (2026-06-05, REL_18 273fe94 기준)
섹션 제목: “위치 힌트 (2026-06-05, REL_18 273fe94 기준)”| 심볼 | 파일 | 줄 |
|---|---|---|
FdwRoutine (구조체) | src/include/foreign/fdwapi.h | 204 |
GetForeignRelSize_function (typedef) | src/include/foreign/fdwapi.h | 27 |
GetForeignPlan_function (typedef) | src/include/foreign/fdwapi.h | 35 |
IterateForeignScan_function (typedef) | src/include/foreign/fdwapi.h | 46 |
GetFdwRoutine (선언) | src/include/foreign/fdwapi.h | 285 |
GetForeignDataWrapperExtended | src/backend/foreign/foreign.c | 50 |
GetForeignServerExtended | src/backend/foreign/foreign.c | 124 |
GetUserMapping | src/backend/foreign/foreign.c | 201 |
GetForeignTable | src/backend/foreign/foreign.c | 255 |
GetFdwRoutine | src/backend/foreign/foreign.c | 326 |
GetForeignServerIdByRelId | src/backend/foreign/foreign.c | 356 |
GetFdwRoutineByServerId | src/backend/foreign/foreign.c | 378 |
GetFdwRoutineByRelId | src/backend/foreign/foreign.c | 420 |
GetFdwRoutineForRelation | src/backend/foreign/foreign.c | 443 |
IsImportableForeignTable | src/backend/foreign/foreign.c | 483 |
postgresql_fdw_validator | src/backend/foreign/foreign.c | 626 |
GetExistingLocalJoinPath | src/backend/foreign/foreign.c | 742 |
lookup_fdw_handler_func | src/backend/commands/foreigncmds.c | 486 |
lookup_fdw_validator_func | src/backend/commands/foreigncmds.c | 510 |
parse_func_options | src/backend/commands/foreigncmds.c | 529 |
CreateForeignDataWrapper | src/backend/commands/foreigncmds.c | 569 |
CreateForeignServer | src/backend/commands/foreigncmds.c | 854 |
CreateUserMapping | src/backend/commands/foreigncmds.c | 1116 |
CreateForeignTable | src/backend/commands/foreigncmds.c | 1420 |
ImportForeignSchema | src/backend/commands/foreigncmds.c | 1500 |
set_foreign_size | src/backend/optimizer/path/allpaths.c | 913 |
set_foreign_pathlist | src/backend/optimizer/path/allpaths.c | 937 |
create_foreignscan_plan | src/backend/optimizer/plan/createplan.c | 4115 |
PlanForeignModify 호출 지점 | src/backend/optimizer/plan/createplan.c | 7375 |
AddForeignUpdateTargets 호출 지점 | src/backend/optimizer/util/appendinfo.c | 944 |
ForeignNext | src/backend/executor/nodeForeignscan.c | 41 |
ForeignRecheck | src/backend/executor/nodeForeignscan.c | 78 |
ExecForeignScan | src/backend/executor/nodeForeignscan.c | 118 |
ExecInitForeignScan | src/backend/executor/nodeForeignscan.c | 142 |
ExecEndForeignScan | src/backend/executor/nodeForeignscan.c | 297 |
ExecReScanForeignScan | src/backend/executor/nodeForeignscan.c | 323 |
ExecForeignInsert 호출 지점 | src/backend/executor/nodeModifyTable.c | 1032 |
ExecForeignBatchInsert 호출 지점 | src/backend/executor/nodeModifyTable.c | 1383 |
ExecForeignDelete 호출 지점 | src/backend/executor/nodeModifyTable.c | 1621 |
ExecForeignUpdate 호출 지점 | src/backend/executor/nodeModifyTable.c | 2499 |
BeginForeignModify 호출 지점 | src/backend/executor/nodeModifyTable.c | 4862 |
EndForeignModify 호출 지점 | src/backend/executor/nodeModifyTable.c | 5241 |
소스 검증 (2026-06-05 기준)
섹션 제목: “소스 검증 (2026-06-05 기준)”검증된 사실
섹션 제목: “검증된 사실”-
FdwRoutine에 필수 스캔 콜백이 정확히 일곱 개다. 나머지는 선택적이다.fdwapi.h에서 확인: 일곱 개의 스캔 필드 뒤 구조체 주석에 *“Remaining functions are optional. Set the pointer to NULL for any that are not provided.”*라고 명시되어 있다. 또한 구조체 주석이makeNode(FdwRoutine)사용을 강제해 설정되지 않은 필드가NULL이 되도록 한다. -
핸들러 함수는
fdw_handler타입(FDW_HANDLEROID)을 반환해야 한다.lookup_fdw_handler_func(foreigncmds.c)에서 확인:if (get_func_rettype(handlerOid) != FDW_HANDLEROID)가ERRCODE_WRONG_OBJECT_TYPE을 발생시킨다.GetFdwRoutine도 호출 시점에IsA(routine, FdwRoutine)을 단언하므로, 잘못된 포인터를 반환하는 핸들러도 잡힌다. -
GetFdwRoutineForRelation은 vtable을 relcache에 캐시한다.foreign.c에서 확인:relation->rd_fdwroutine == NULL일 때 새로 해석한 루틴을CacheMemoryContext에 복사해 relcache 항목에 저장한다. 주석에 캐시된 포인터가 “relcache 리셋 시 사라진다”고 경고한다. -
foreign-data wrapper 생성은 슈퍼유저를 요구한다.
CreateForeignDataWrapper에서 확인:if (!superuser()) ereport(ERROR, ... "permission denied to create foreign-data wrapper"). 서버·매핑·테이블은 더 세밀한 ACL 검사(FDW/서버에 대한 USAGE)가 있지만, 래퍼 자체는 임의의 C 함수를 지명하므로 슈퍼유저 전용이다. -
user mapping 조회는 PUBLIC으로 폴백한다.
GetUserMapping에서 확인:(userid, serverid)에서 미스가 나면 에러를 내기 전에InvalidOid로SearchSysCache2를 재시도해 “user mapping not found”를 반환한다. -
플래너가
set_foreign_size에서GetForeignRelSize를,set_foreign_pathlist에서GetForeignPaths를 호출한다.allpaths.c에서 확인: 둘 다rel->fdwroutine->...로 호출한다.set_foreign_size는 래퍼가 행 추정치를 0으로 만들 수 없도록 결과를clamp_row_est로 고정하고,rel->tuples >= rel->rows를 보장한다. -
create_foreignscan_plan은rel->fdwroutine != NULL을 단언하고,GetForeignPlan후 엔진 소유 필드를 복사한다.createplan.c에서 확인:copy_generic_path_info,scan_plan->checkAsUser = rel->userid,scan_plan->fs_server = rel->serverid. 주석에 FDW가 “원격으로 실행하려는 제한 절을 제거하거나 더 추가할 수도 있다”고 명시한다. -
IterateForeignScan은 튜플별 메모리 컨텍스트 안에서 호출된다.ForeignNext에서 확인:MemoryContextSwitchTo(econtext->ecxt_per_tuple_memory)가IterateForeignScan호출을 감싸므로, 래퍼의 행별 할당이 다음ExecScan리셋 때 회수된다. -
직접 수정과 행별 수정은 결과 릴레이션당 상호 배타적이다.
ExecInitModifyTable에서 확인:resultRelInfo->ri_usesFdwDirectModify가 true이면BeginForeignModify를 건너뛴다.ExecInitForeignScan은node->operation != CMD_SELECT일 때BeginForeignScan대신BeginDirectModify를 호출한다. -
ImportForeignSchema는 래퍼로부터CreateForeignTableStmt만 돌려받는다.foreigncmds.c에서 확인: 반환된 파스 트리 각각을IsA(cstmt, CreateForeignTableStmt)로 검사하고 그렇지 않으면 에러를 발생시킨다. 스키마 이름은 IMPORT 문의local_schema로 강제된다.
미해결 질문
섹션 제목: “미해결 질문”-
래퍼는 어떻게
GetForeignPlan과BeginForeignScan사이에 푸시다운된 조건절을 전달하는가? 메커니즘은ForeignScan.fdw_private리스트(GetForeignPlan에서 구성,BeginForeignScan에서node->ss.ps.plan으로 읽음)와 런타임 파라미터 값을 위한fdw_exprs다. 정확한 직렬화 계약(무엇이copyObject가능해야 하는지 vs. 노드 트리인지)은 래퍼가 정의하며 코어가 강제하지 않는다. 플랜 캐싱 과정에서fdw_private가copyObject를 거치는 방식을 추적하면 제약이 명확해질 것이다. 탐구 경로:copyfuncs의_copyForeignScan과ForeignScan.fdw_private사용처. -
GetForeignPaths에 대한 비용 계약은 정확히 무엇인가? 래퍼가ForeignPath에startup_cost/total_cost를 설정하지만, 그 수치는 원격 소스에서 의미가 없는 엔진의 비용 단위(seq_page_cost등)로 표현된다. 래퍼가 원격 지연을 로컬 비용 단위로 어떻게 보정해야 하는지, 잘못된 보정이 조인 순서를 얼마나 왜곡하는지가 코어에 명시되지 않는다. 탐구 경로:cost_seqscan단위와set_foreign_size_estimates가 너비를 초기화하는 방식 비교. -
외부 조인에 대한 EPQ 재검사가 실제로 언제 실행되는가?
GetExistingLocalJoinPath는EvalPlanQual을 위해 로컬 조인 경로를 재구성하지만, 비파라미터화된 Hash/Merge/NestLoop 경로만 처리한다.SELECT ... FOR UPDATE하에서 파라미터화된 푸시다운 조인에 무슨 일이 일어나는지가 이 파일만으로는 불분명하다. 탐구 경로: 익스큐터의 EPQ 기구에서RefetchForeignRow/GetForeignRowMarkType호출처 추적.
PostgreSQL 너머 — 비교 설계와 연구 프론티어
섹션 제목: “PostgreSQL 너머 — 비교 설계와 연구 프론티어”PostgreSQL의 FDW 계층은 의도적으로 최소한의 연합 커널이다. 카탈로그 어휘와 콜백 vtable로 구성되며, 소스별 지능은 모두 래퍼 안으로 밀어 넣는다. 데이터 통합 연구의 계보와 다른 프로덕션 엔진들 옆에 놓으면, PostgreSQL이 무엇을 이어받았고 어디서 멈추는지가 드러난다.
미디에이터-래퍼 계보
섹션 제목: “미디에이터-래퍼 계보”FDW 설계는 1990년대 데이터 통합 문헌의 미디에이터-래퍼 아키텍처를 직접 계승한다. Wiederhold의 1992년 논문 “Mediators in the Architecture of Future Information Systems”가 PostgreSQL이 지금도 쓰는 분리를 명명했다. 소스별 래퍼가 균일한 질의 인터페이스를 소스 고유 연산으로 번역하고, 미디에이터가 래퍼들을 조합해 답을 만든다. PostgreSQL의 플래너가 미디에이터고, FdwRoutine이 래퍼 인터페이스다.
두 개의 연구 시스템이 PostgreSQL 콜백이 반향하는 어휘를 고정했다.
Garlic(IBM Almaden, Roth & Schwarz 1997, “Don’t Scrap It, Wrap It!”). Garlic의 핵심 아이디어는 능력 기반 질의 플래닝이다. 래퍼가 단순히 행을 가져오는 것이 아니라 어떤 질의 단편을 실행할 수 있는지 광고하며, 옵티마이저는 그 능력을 존중하는 플랜을 열거한다. 이것이 정확히 PostgreSQL의 GetForeignPaths / GetForeignJoinPaths / GetForeignUpperPaths가 구현하는 계약이다. 래퍼가 원격으로 실행하려는 경로를 추가하면, 엔진의 비용 기반 탐색이 그중에서 선택한다. Garlic의 “단편을 플랜으로 만들고, 비용을 매기고, 미디에이터가 조합하게 하라”는 루프는 PostgreSQL이 ForeignPath를 일반 경로 경쟁에 편입시키는 방식의 지적 선조다.
TSIMMIS(Stanford, Garcia-Molina et al. 1997). TSIMMIS는 반구조화된 소스와 래퍼 생성 툴킷을 강조했지만, FDW 형태에 남긴 가장 지속적인 기여는 질의 템플릿 모델이다. 래퍼가 지원하는 파라미터화된 질의를 선언하고, 지원되지 않는 술어는 가져온 뒤 미디에이터가 평가한다. PostgreSQL의 GetForeignPlan에서 조건절을 나누는 방식(원격 부분 vs. 로컬 재검사 scan_clauses, fdw_recheck_quals가 ForeignRecheck에서 재평가됨)이 이 push-some/keep-some 분리와 같다.
이 연구가 SQL/MED(ISO/IEC 9075-9, SQL:2003)로 표준화되면서 PostgreSQL이 카탈로그 명사들을 얻었다. foreign-data wrapper, foreign server, user mapping, foreign table. PostgreSQL은 SQL/MED의 datalink와 foreign-table 부분을 구현하지만, 대부분의 엔진처럼 전체 표준의 실용적인 부분집합만 구현한다.
PostgreSQL이 멈추는 곳: 연합이지 폴리스토어가 아니다
섹션 제목: “PostgreSQL이 멈추는 곳: 연합이지 폴리스토어가 아니다”PostgreSQL의 FDW는 단일 엔진 연합 메커니즘이다. 많은 소스를 PostgreSQL 테이블처럼 보이게 만들 수 있지만, 옵티마이저는 여전히 PostgreSQL의 행 지향 비용 기반 플래너고, 익스큐터는 여전히 PostgreSQL의 튜플 단위 Volcano 파이프라인이다. 질의를 가장 잘 답할 수 있는 엔진으로 라우팅하지 않는다. 이것이 폴리스토어 / 멀티스토어 프론티어다.
BigDAWG(MIT/Intel/MIT-LL, Duggan et al. 2015)는 스토어를 정보 섬으로 조직하고, 각각 고유한 질의 언어와 의미론을 갖게 하며, 섬 사이에 데이터를 이동시키는 명시적 CAST 연산자를 삽입한다. FDW가 Cassandra 테이블을 관계형인 척 만드는 곳에서, BigDAWG는 배열 스토어를 배열 형태로, 관계형 스토어를 관계형으로 유지하며 서브쿼리별로 섬을 선택한다. PostgreSQL의 GetForeignUpperPaths 푸시다운이 코어에서 가장 가까운 유사 기능이다. 집계를 원격으로 실행하게 해주지만, 결과를 조합하는 실행 모델은 항상 하나(Postgres의 것)다.
Myria와 Apache Calcite 기반 연합(Dremio, Trino의 커넥터 모델 등)은 더 일반화한다. 하나의 논리 플랜이 이종 백엔드에 걸쳐 분할되며 크로스 엔진 비용 모델을 쓴다. Calcite의 어댑터는 FdwRoutine의 정신적 사촌이지만, Calcite는 어댑터들을 가로지르는 관계 대수 재작성을 추론한다. PostgreSQL의 래퍼 협상은 관계별·조인 쌍별이며, 코어 플래너가 이미 열거하는 범위 안에 머문다.
PostgreSQL이 택한 트레이드오프는 명확성 대 도달 범위다. 래퍼 작성자는 몇 개의 콜백을 구현하고 성숙한 PostgreSQL 옵티마이저와 익스큐터 전체를 물려받는다. 대신 비관계형 대수로 엔진이 추론하게 만들 수는 없다.
다른 프로덕션 엔진과 비교
섹션 제목: “다른 프로덕션 엔진과 비교”| 엔진 | 연합 프리미티브 | 푸시다운 모델 | 자격증명 범위 |
|---|---|---|---|
| PostgreSQL | FDW + FdwRoutine vtable | 비용 탐색에 조건절/조인/상위 경로 추가 | user mapping (역할, 서버), PUBLIC 폴백 |
| Oracle | 데이터베이스 링크 / Heterogeneous Services | 분산 질의 옵티마이저가 원격 SQL 재작성 | 링크 정의 안의 링크별 자격증명 |
| SQL Server | 링크드 서버 + OLE DB 프로바이더 | 프로바이더 “행집합” 능력; 지원 시 원격 질의 | 링크드 서버 로그인 매핑 |
| MySQL | FEDERATED 스토리지 엔진 | 거의 없음 — 전체 스캔 + 로컬 필터 | CREATE TABLE의 연결 문자열 |
| Trino / Presto | 커넥터 (SPI) | 커넥터 푸시다운 API (술어/집계/topN) | 카탈로그별 설정 / 세션 |
PostgreSQL은 이 스펙트럼의 중간에 위치한다. MySQL의 FEDERATED 엔진(사실상 작업을 내려보내지 않음)보다 훨씬 더 능력이 있고, Oracle/SQL Server보다 더 열려 있다(어떤 C 확장도 래퍼가 될 수 있으며, 벤더 게이트웨이에 국한되지 않는다). 하지만 기존 관계형 플래너에 푸시다운을 덧붙이는 것이 아니라 처음부터 풍부한 푸시다운 어휘를 중심으로 설계된 Trino의 커넥터 SPI보다는 덜 공격적이다.
PostgreSQL FDW 자체의 활성 프론티어
섹션 제목: “PostgreSQL FDW 자체의 활성 프론티어”인-코어 기구는 FdwRoutine의 선택적 콜백들에서 볼 수 있는 여러 축을 따라 계속 발전 중이다.
비동기 실행(IsForeignPathAsyncCapable, ForeignAsyncRequest, ForeignAsyncConfigureWait, ForeignAsyncNotify). PG14부터 여러 외부 스캔에 걸친 Append가 원격 질의를 동시에 발행하고 먼저 응답한 것을 소비할 수 있다. 파티션된 외부 테이블을 분산 scatter-gather 팬아웃으로 전환한다. 이것이 코어 안에 있는 분산 실행 모델의 씨앗이다.
배치 삽입(GetForeignModifyBatchSize + ExecForeignBatchInsert). 행별 왕복 비용 상각은 원격 쓰기 가능 래퍼의 가장 큰 지연 레버다. 코어가 래퍼에 조절 장치를 주고 대신 행을 묶어준다.
파티션 단위 / 샤딩 패턴. 로컬 파티션된 테이블의 파티션으로서의 외부 테이블은, 비동기 append와 조인 푸시다운과 결합하면 별도의 코디네이터 없이 샤딩 클러스터와 유사해진다. 미해결 연구 질문은 전역 트랜잭션 원자성이다. PostgreSQL은 FDW 연결에 걸친 내장된 분산 2단계 커밋이 없다. 래퍼가 트랜잭션 콜백 훅으로 2PC를 선택할 수 있지만, 엔진이 대신 조율해 주지는 않는다. 크로스 샤드 원자적 커밋은 인-코어 FDW 계층이 아닌 확장과 포크에서 다루는 문제다.
직접 수정과 조인 푸시다운 성숙도. PlanDirectModify와 GetForeignJoinPaths로 능력 있는 래퍼는 이미 전체 UPDATE ... FROM이나 다중 테이블 조인을 하나의 원격 문장으로 축소할 수 있다. 상위 관계 푸시다운(GetForeignUpperPaths)을 윈도우 함수, DISTINCT, 더 풍부한 LIMIT/OFFSET 형태로 확장하는 것은 점진적 진행 중 작업이다.
일관된 기조는 이렇다. PostgreSQL은 FDW 커널을 작고 표준에 닻을 내린 상태로 유지하며, 원격 실행 정교함의 모든 향상을 래퍼가 구현할 수도 있고 하지 않을 수도 있는 새 선택적 콜백으로 처리한다. 필수 일곱 개의 스캔 콜백 계약은 변경하지 않는다.
코드 경로 (REL_18, commit 273fe94, 2026-06-05 관측)
섹션 제목: “코드 경로 (REL_18, commit 273fe94, 2026-06-05 관측)”src/include/foreign/fdwapi.h—FdwRoutinevtable, 모든 콜백 typedef(GetForeignRelSize_function부터 비동기 계열까지),GetFdwRoutine*/GetForeignServerIdByRelId선언. 이 헤더가 래퍼 작성자가 프로그래밍하는 표준 계약이다.src/include/foreign/foreign.h— 인메모리 카탈로그 미러 구조체(ForeignDataWrapper,ForeignServer,UserMapping,ForeignTable)와Get*접근자 선언.src/backend/foreign/foreign.c— 핸들러→vtable 디스패치(GetFdwRoutine,GetFdwRoutineByServerId,GetFdwRoutineByRelId,GetFdwRoutineForRelation), 네 개의 카탈로그 접근자, PUBLIC 폴백이 있는GetUserMapping,IsImportableForeignTable,GetExistingLocalJoinPath, 더 이상 사용되지 않는postgresql_fdw_validator.src/backend/commands/foreigncmds.c— DDL 계층:CreateForeignDataWrapper(슈퍼유저 검사, 핸들러/검증기 해석),CreateForeignServer,CreateUserMapping,CreateForeignTable,ImportForeignSchema, 옵션 배열 변환 헬퍼(lookup_fdw_handler_func,transformGenericOptions).src/backend/optimizer/path/allpaths.c—set_foreign_size,set_foreign_pathlist(GetForeignRelSize/GetForeignPaths호출 지점).src/backend/optimizer/plan/createplan.c—create_foreignscan_plan(GetForeignPlan호출 지점과 엔진 소유 필드 복사);create_modifytable_plan의PlanForeignModify호출 지점.src/backend/executor/nodeForeignscan.c— 스캔 커서 드라이버(ExecInitForeignScan,ForeignNext,ForeignRecheck,ExecReScanForeignScan,ExecEndForeignScan)와 병렬 스캔 DSM 배관.src/backend/executor/nodeModifyTable.c— 쓰기 경로 호출 지점(BeginForeignModify,ExecForeignInsert/Update/Delete,ExecForeignBatchInsert,EndForeignModify).
논문과 표준
섹션 제목: “논문과 표준”- Wiederhold, G. (1992). “Mediators in the Architecture of Future Information Systems.” IEEE Computer 25(3):38-49. PostgreSQL의 플래너/
FdwRoutine분리가 계승한 미디에이터-래퍼 분리. - Roth, M. T. & Schwarz, P. (1997). “Don’t Scrap It, Wrap It! A Wrapper Architecture for Legacy Data Sources.” VLDB ‘97.
GetForeignPaths/GetForeignJoinPaths가 실현하는 Garlic 능력 기반 질의 플래닝 모델. - Garcia-Molina, H. et al. (1997). “The TSIMMIS Approach to Mediation: Data Models and Languages.” Journal of Intelligent Information Systems 8(2):117-132. 조건절 분리와
fdw_recheck_quals에 반영된 질의 템플릿 / push-some-keep-some 중재. - Duggan, J. et al. (2015). “The BigDAWG Polystore System.” ACM SIGMOD Record 44(2):11-16. PostgreSQL의 단일 엔진 연합이 의도적으로 멈추는 폴리스토어 프론티어.
- ISO/IEC 9075-9: SQL/MED (Management of External Data), SQL:2003. PostgreSQL이 구현하는 카탈로그 어휘를 정의하는 표준. foreign-data wrapper, foreign server, user mapping, foreign table.
- Database System Concepts (Silberschatz, Korth, Sudarshan, 7e) — 데이터 통합·이종/분산 데이터베이스 장(긴밀히/느슨하게 결합된 연합; 자율 소스에 대한 최적화).
knowledge/research/dbms-general/에 캡처. - Database Internals (Petrov, 2019) — 비동기 append / scatter-gather 방향을 위한 분산 질의와 요청 라우팅 프레이밍.
형제 문서 (교차 참조 — 메커니즘은 해당 문서 소유)
섹션 제목: “형제 문서 (교차 참조 — 메커니즘은 해당 문서 소유)”postgres-planner-overview.md—ForeignPath가 경쟁하는 경로/비용 탐색.set_foreign_size/set_foreign_pathlist가 FDW의 진입점이다.postgres-executor.md—ForeignNext와ForeignRecheck가 꽂히는ExecScan/ Volcano 튜플 파이프라인.postgres-ddl-execution.md—CreateForeignTable과ImportForeignSchema가 기반으로 하는 일반ProcessUtility/ 테이블 생성 경로.postgres-extensions.md— 래퍼가 로드 가능한 확장으로 출시되고 핸들러 함수를 등록하는 방식.postgres-architecture-overview.md— 축 4(플러그인 접근 방법). FDW vtable이TableAmRoutine/IndexAmRoutine와 나란히 함수 포인터 확장성 인터페이스로 자리한다.