# BD 手册:FDW 反向映射 — ETL 库读取业务库维客线索 > 创建日期:2026-02-26(原 `member_birthday_manual`),2026-03-19 重写为当前版本 > 替代文档:`docs/database/_archived/BD_Manual_fdw_reverse_member_birthday.md` > 关联 SQL:`db/fdw/setup_fdw_reverse.sql`(生产)、`db/fdw/setup_fdw_reverse_test.sql`(测试) > 关联表文档:`docs/database/BD_Manual_member_retention_clue.md` --- ## 1. 概述 在 `etl_feiqiu`(生产)/ `test_etl_feiqiu`(测试)数据库中,通过 `postgres_fdw` 创建指向 `zqyy_app` / `test_zqyy_app` 的外部表 `fdw_app.member_retention_clue`,使 ETL DWS 任务可只读访问助教为会员记录的维客线索数据。 方向:`etl_feiqiu → zqyy_app`(与正向 FDW `setup_fdw.sql` 的 `zqyy_app → etl_feiqiu` 方向相反)。 ### 数据流向 ``` zqyy_app.public.member_retention_clue (业务库源表,后端 API 写入) │ │ postgres_fdw(只读) ▼ etl_feiqiu.fdw_app.member_retention_clue (ETL 库外部表,DWS 任务读取) ``` ### 历史沿革 | 日期 | 事件 | |------|------| | 2026-02-22 | 初版:`fdw_app.member_birthday_manual`(映射助教手动补录生日表) | | 2026-02-26 | 重构:`member_birthday_manual` → `member_retention_clue`(维客线索替代单一生日方案) | | 2026-02-27 | 业务库侧新增 `source` 列(线索来源),FDW 外部表定义未同步更新 | | 2026-03-08 | 业务库侧 `category` 枚举对齐:`客户基础信息` → `客户基础` | --- ## 2. 变更说明 ### 2.1 新增对象 | 所在库 | 对象类型 | 名称 | 说明 | |--------|---------|------|------| | etl_feiqiu / test_etl_feiqiu | 扩展 | `postgres_fdw` | PostgreSQL 外部数据包装器(如已安装则跳过) | | etl_feiqiu | 外部服务器 | `zqyy_app_server` | 指向 `zqyy_app` 业务库(host/port 按环境配置) | | test_etl_feiqiu | 外部服务器 | `test_zqyy_app_server` | 指向 `test_zqyy_app` 测试业务库 | | etl_feiqiu / test_etl_feiqiu | 用户映射 | `etl_user → app_reader` | ETL 连接角色映射到业务库只读角色 | | etl_feiqiu / test_etl_feiqiu | Schema | `fdw_app` | 存放来自业务库的外部表 | | etl_feiqiu / test_etl_feiqiu | 外部表 | `fdw_app.member_retention_clue` | 映射 `public.member_retention_clue` | ### 2.2 外部表列定义 | 列名 | 类型 | 说明 | |------|------|------| | id | BIGINT | 自增主键 | | member_id | BIGINT | 会员 ID | | category | VARCHAR(20) | 线索大类(6 值枚举:客户基础/消费习惯/玩法偏好/促销偏好/社交关系/重要反馈) | | summary | VARCHAR(200) | 摘要 | | detail | TEXT | 详情 | | recorded_by_assistant_id | BIGINT | 记录助教 ID | | recorded_by_name | VARCHAR(50) | 记录助教姓名 | | recorded_at | TIMESTAMPTZ | 记录时间 | | site_id | BIGINT | 门店 ID | > **注意**:业务库侧 `member_retention_clue` 已于 2026-02-27 新增 `source VARCHAR(20)` 列(线索来源:`manual` / `ai_consumption` / `ai_note`),但当前 FDW 外部表定义(`db/fdw/setup_fdw_reverse*.sql`)尚未包含此列。如 ETL 任务需要读取 `source` 字段,需更新外部表定义并重新执行部署脚本。 ### 2.3 角色与权限 | 角色 | 所在库 | 用途 | |------|--------|------| | `etl_user` | etl_feiqiu / test_etl_feiqiu | ETL 连接角色,通过 FDW 只读访问业务库数据 | | `app_reader` | zqyy_app / test_zqyy_app | 业务库只读角色,供 FDW 用户映射使用 | 权限配置: | 角色 | Schema | 权限 | |------|--------|------| | `etl_user` | `fdw_app` | `USAGE` + `SELECT ON ALL TABLES` + `ALTER DEFAULT PRIVILEGES`(自动授权未来新增外部表) | --- ## 3. 与正向 FDW 的对比 | 维度 | 正向 FDW(`setup_fdw.sql`) | 反向 FDW(`setup_fdw_reverse.sql`) | |------|---------------------------|-------------------------------------| | 执行位置 | `zqyy_app` 业务库 | `etl_feiqiu` ETL 库 | | 数据方向 | 业务库读取 ETL 数据 | ETL 库读取业务库数据 | | 目标 Schema | `fdw_etl`(35 张外部表) | `fdw_app`(1 张外部表) | | 导入方式 | `IMPORT FOREIGN SCHEMA app`(批量) | `CREATE FOREIGN TABLE`(逐表定义) | | 消费方 | 后端 API | ETL DWS 任务 | | 文档 | `BD_Manual_fdw_etl_setup.md` | 本文档 | --- ## 4. 兼容性影响 | 组件 | 影响 | |------|------| | ETL DWS 任务 | 可通过 `fdw_app.member_retention_clue` 只读访问维客线索数据。当前无 DWS 任务直接消费此表(原 `member_birthday_manual` 的生日读取逻辑已移除,生日仅从 `dim_member.birthday` 读取) | | 后端 API | 无影响。后端直接写入 `zqyy_app.public.member_retention_clue`,不经过 FDW | | 小程序 | 无影响。小程序通过后端 API 间接访问 | | 管理后台 | 无影响 | | 正向 FDW(`fdw_etl`) | 无影响。两个方向的 FDW 配置完全独立 | | 业务库 `member_retention_clue` 表 | 无影响。FDW 为只读映射,不修改源表 | ### 幂等性说明 `CREATE FOREIGN TABLE IF NOT EXISTS` 确保重复执行不会报错。如需更新列定义(如添加 `source` 列),需先 `DROP FOREIGN TABLE` 再重建,或使用 `ALTER FOREIGN TABLE ADD COLUMN`。 --- ## 5. 回滚策略 ### 5.1 完整回滚(按逆序执行) ```sql -- 在 etl_feiqiu(生产)中执行: ALTER DEFAULT PRIVILEGES IN SCHEMA fdw_app REVOKE SELECT ON TABLES FROM etl_user; REVOKE SELECT ON ALL TABLES IN SCHEMA fdw_app FROM etl_user; REVOKE USAGE ON SCHEMA fdw_app FROM etl_user; DROP FOREIGN TABLE IF EXISTS fdw_app.member_retention_clue; DROP SCHEMA IF EXISTS fdw_app CASCADE; DROP USER MAPPING IF EXISTS FOR etl_user SERVER zqyy_app_server; DROP SERVER IF EXISTS zqyy_app_server CASCADE; -- 注意:如果其他外部表也使用 postgres_fdw,不要执行 DROP EXTENSION ``` ```sql -- 在 test_etl_feiqiu(测试)中执行: ALTER DEFAULT PRIVILEGES IN SCHEMA fdw_app REVOKE SELECT ON TABLES FROM etl_user; REVOKE SELECT ON ALL TABLES IN SCHEMA fdw_app FROM etl_user; REVOKE USAGE ON SCHEMA fdw_app FROM etl_user; DROP FOREIGN TABLE IF EXISTS fdw_app.member_retention_clue; DROP SCHEMA IF EXISTS fdw_app CASCADE; DROP USER MAPPING IF EXISTS FOR etl_user SERVER test_zqyy_app_server; DROP SERVER IF EXISTS test_zqyy_app_server CASCADE; ``` ### 5.2 仅删除外部表(保留 FDW 基础设施) ```sql DROP FOREIGN TABLE IF EXISTS fdw_app.member_retention_clue; ``` 回滚无数据丢失风险:外部表不存储数据,仅为远程表的映射定义。 --- ## 6. 验证 SQL 以下 SQL 在 ETL 库(`etl_feiqiu` 或 `test_etl_feiqiu`)中执行: ```sql -- 1. 确认 postgres_fdw 扩展已安装 SELECT extname, extversion FROM pg_extension WHERE extname = 'postgres_fdw'; -- 预期:1 行 -- 2. 确认外部服务器存在 -- 生产: SELECT srvname, srvoptions FROM pg_foreign_server WHERE srvname = 'zqyy_app_server'; -- 测试: SELECT srvname, srvoptions FROM pg_foreign_server WHERE srvname = 'test_zqyy_app_server'; -- 预期:1 行,srvoptions 包含正确的 host/dbname/port -- 3. 确认用户映射存在 SELECT um.umid, r.rolname AS local_role, s.srvname, um.umoptions FROM pg_user_mappings um JOIN pg_foreign_server s ON s.srvname = um.srvname JOIN pg_roles r ON r.rolname = um.usename WHERE s.srvname IN ('zqyy_app_server', 'test_zqyy_app_server') AND r.rolname = 'etl_user'; -- 预期:1 行 -- 4. 确认 fdw_app schema 存在 SELECT schema_name FROM information_schema.schemata WHERE schema_name = 'fdw_app'; -- 预期:1 行 -- 5. 确认外部表列结构完整(当前 9 列) SELECT column_name, data_type FROM information_schema.columns WHERE table_schema = 'fdw_app' AND table_name = 'member_retention_clue' ORDER BY ordinal_position; -- 预期:9 行(id, member_id, category, summary, detail, -- recorded_by_assistant_id, recorded_by_name, recorded_at, site_id) -- 6. 确认外部表在 information_schema.foreign_tables 中注册 SELECT foreign_table_schema, foreign_table_name, foreign_server_name FROM information_schema.foreign_tables WHERE foreign_table_schema = 'fdw_app' AND foreign_table_name = 'member_retention_clue'; -- 预期:1 行 -- 7. 确认 etl_user 对 fdw_app 有 USAGE 权限 SELECT has_schema_privilege('etl_user', 'fdw_app', 'USAGE') AS has_usage; -- 预期:true -- 8. 确认外部表可读取(需业务库侧表已存在且网络连通) SELECT COUNT(*) FROM fdw_app.member_retention_clue; -- 预期:返回行数(可能为 0) -- 9. 确认 ALTER DEFAULT PRIVILEGES 已设置 SELECT n.nspname AS schema_name, d.defaclacl AS default_acl FROM pg_default_acl d JOIN pg_namespace n ON n.oid = d.defaclnamespace WHERE n.nspname = 'fdw_app'; -- 预期:1 行,default_acl 包含 etl_user 的 SELECT 权限 ``` --- ## 7. 已知差异与待办 | 项目 | 状态 | 说明 | |------|------|------| | `source` 列缺失 | ⚠️ 待同步 | 业务库侧已有 `source VARCHAR(20) NOT NULL DEFAULT 'manual'`(2026-02-27),FDW 外部表定义未包含。当前无 ETL 任务需要此字段,但未来如需读取线索来源需先更新外部表 | | DWS 任务消费 | 📋 待规划 | 原 `member_birthday_manual` 的 DWS 消费逻辑已移除。维客线索的 DWS 聚合任务尚未规划 | ### source 列同步方法(备用) 如需同步 `source` 列,在 ETL 库中执行: ```sql -- 方法 1:ALTER 追加列(推荐,无需重建) ALTER FOREIGN TABLE fdw_app.member_retention_clue ADD COLUMN source VARCHAR(20); -- 方法 2:DROP + 重建(完整重置) DROP FOREIGN TABLE IF EXISTS fdw_app.member_retention_clue; CREATE FOREIGN TABLE fdw_app.member_retention_clue ( id BIGINT, member_id BIGINT, category VARCHAR(20), summary VARCHAR(200), detail TEXT, recorded_by_assistant_id BIGINT, recorded_by_name VARCHAR(50), recorded_at TIMESTAMPTZ, site_id BIGINT, source VARCHAR(20) ) SERVER zqyy_app_server OPTIONS (schema_name 'public', table_name 'member_retention_clue'); ``` 同步后需更新 `db/fdw/setup_fdw_reverse.sql` 和 `db/fdw/setup_fdw_reverse_test.sql` 中的外部表定义。 --- ## 8. 关联文件 | 文件 | 说明 | |------|------| | `db/fdw/setup_fdw_reverse.sql` | 生产环境部署脚本(在 `etl_feiqiu` 中执行) | | `db/fdw/setup_fdw_reverse_test.sql` | 测试环境部署脚本(在 `test_etl_feiqiu` 中执行) | | `docs/database/BD_Manual_member_retention_clue.md` | 业务库侧 `member_retention_clue` 表结构文档 | | `docs/database/BD_Manual_fdw_etl_setup.md` | 正向 FDW 配置文档(方向相反) | | `docs/database/BD_Manual_app_schema_rls_views.md` | ETL 库 `app` Schema RLS 视图层文档 | | `db/zqyy_app/migrations/2026-02-26__refactor_birthday_to_retention_clue.sql` | 业务库侧建表迁移脚本 |