06 | 把 meta_config 同步进 MySQL(生成阶段)
06 | 把 meta_config 同步进 MySQL(生成阶段)需要先看清适用场景和关键步骤,避免只记结论却忽略实际限制。
06 | 把 meta_config 同步进 MySQL(生成阶段)
这是一篇系列文,请按顺序阅读。
本文目标
NL2SQL 召回前,要先把业务元数据灌进各个存储:
- MySQL meta 库:结构化元数据(表 / 字段 / 指标 / 关联)
- Qdrant:字段语义向量(后续)
- Elasticsearch:维度值全文检索(后续)
本文完成第 1 步:把 conf/meta_config.yaml 同步到 MySQL meta 库的四张表。
数据源头是 YAML,不是直接扫业务库;业务库(dw)只用来补字段类型和样例值。
1. 整体流程
meta_config.yaml │ ▼conf/sync_db.py │ ├─ sync_table_info() ├─ sync_column_info() ← 会读 dw 库补 type / examples ├─ sync_metric_info() └─ sync_column_metric()│▼ Service → Repository → Mapper → Model│▼ MySQL meta 库
调用链一句话:
2. meta 库要存什么
| 表名 | 作用 | 关键字段 | 状态 |
|---|---|---|---|
table_info | 业务表信息 | id / name / role / description | ✅ |
column_info | 业务字段信息 | id / name / type / role / description / alias / examples / table_id | ✅ |
metric_info | 指标定义 | id / name / description / alias / relevant_columns | ✅ |
column_metric | 字段 ↔ 指标关联 | column_id / metric_id | ✅ |
关系:
table_info 1 ── N column_infocolumn_info N ── N metric_info (通过 column_metric)
配置上做了拆分:
tables:只放业务表(dim/fact)及其columnsmetric_info:单独放在顶级 key,不再塞进tables
这样同步逻辑更清晰,也避免把指标误写进 table_info。
3. 为什么要分层
YAML 格式和数据库表结构不一样,不能「读完 YAML 直接 INSERT」。分层是为了把职责拆开:
| 层级 | 路径 | 职责 | 不关心什么 |
|---|---|---|---|
| Entity | app/entities/ | 纯业务对象(dataclass) | 数据库类型、SQLAlchemy |
| Model | app/models/ | ORM 映射,对应物理表 | YAML 怎么解析 |
| Mapper | app/mappers/ | Entity ↔ Model 转换 | 业务流程 |
| Repository | app/repositories/ | CRUD / 事务 | 业务规则 |
| Service | app/services/ | 编排业务 | SQL 细节 |
| Script | conf/sync_db.py | 读配置、装配依赖、执行同步 | 领域细节 |
相关文件:
app/├── entities/# table_info / column_info / metric_info / column_metric├── models/├── mappers/├── repositories/# 含 dw_db_repository(读业务库)├── services/└── dbs/ # meta_db.py / dw_db.pyconf/├── meta_config.yaml├── app_config.yaml├── get_config.py├── sync_db.py└── ../.env
4. 配置:密码放 .env,YAML 只放占位符
4.1 .env
MYSQL_USER=yanMYSQL_PASSWORD=你的密码LLM_API_KEY=...LLM_BASE_URL=...
4.2 app_config.yaml(片段)
meta_db:host:localhostport:3306user:"${MYSQL_USER}"password:"${MYSQL_PASSWORD}"database:metadw_db:host:localhostport:3306user:"${MYSQL_USER}"password:"${MYSQL_PASSWORD}"database:dw
4.3 get_config.py 做什么
load_dotenv(.env)把环境变量读进进程- 读 YAML 时,把
${MYSQL_PASSWORD}替换成真实值 - 缺少环境变量时直接报错,避免连库时才发现密码为空
依赖:
uv add pyyaml python-dotenv sqlalchemy asyncmy
5. 先实现 table_info:按层搭起来
后面三张表的 Entity / Model / Mapper / Repository / Service 都照这个套路搭,本文只详细展开 table_info。
5.1 Entity
# app/entities/table_info.pyfrom dataclasses import dataclassclassTableInfo:id: strname: strrole: strdescription: str
要点:
id是str,对应数据库varchar(64)主键- YAML 没有单独
id,同步时用物理表名(如dim_region)当稳定主键
5.2 Model
# app/models/base.pyfrom sqlalchemy.orm import DeclarativeBaseclassBase(DeclarativeBase):pass
# app/models/table_info_model.pyclassTableInfoModel(Base):__tablename__ = "table_info"id: Mapped[str] = mapped_column(String(64), primary_key=True)name: Mapped[str | None] = mapped_column(String(128))role: Mapped[str | None] = mapped_column(String(32))description: Mapped[str | None] = mapped_column(Text)
Base 统一持有 metadata,后面的 create_all / drop_all 都靠它。
5.3 Mapper
classTableInfoMapper:defto_entity(self, model: TableInfoModel) -> TableInfo:# ORM → Entity:字段铺开写(ORM.__dict__ 含内部状态)return TableInfo(id=model.id,name=model.name,role=model.role,description=model.description,)defto_model(self, entity: TableInfo) -> TableInfoModel:# Entity → ORM:dataclass.__dict__ 干净,可解包return TableInfoModel(entity.__dict__)
5.4 数据库连接
app/dbs/meta_db.py 负责 meta 库;app/dbs/dw_db.py 同理,只是读 app_config["dw_db"]。
要点:
- 引擎模块级创建一次
- 驱动用
mysql+asyncmy - 密码含特殊字符时要
quote_plus
5.5 Repository / Service
Repository 只做持久化;Service 目前是薄封装,YAML → Entity 的解析先放在 sync_db.py。
add_all 是真正的批量:一次 add_all + 一次 commit。不要在 Service 里循环调单条 add。
6. 同步脚本骨架
conf/sync_db.py 的职责:
- 开发阶段:
drop_all+create_all,方便反复测试 - 依次同步四张表
- 打开 Session,装配
Repository → Service,批量写入
asyncdefreset_meta_tables() ->None:asyncwith async_engine.begin() as conn:await conn.run_sync(Base.metadata.drop_all)await conn.run_sync(Base.metadata.create_all)asyncdefsync_table_info(config: dict) ->list[TableInfo]:table_infos = []for table in config.get("tables", []):name = table["name"]table_infos.append(TableInfo(id=name,name=name,role=table["role"],description=table["description"],))asyncwith MetaAsyncSessionLocal() as session:service = TableInfoService(TableInfoRepository(session))returnawait service.add_all(table_infos)
开发期用 drop_all,是为了避免重复执行时报主键冲突。代价是每次都会清空已有元数据;上线后应改成 upsert。
指标配置单独放在顶级:
metric_info:-name:GMVdescription:全称GrossMerchandiseValue,表示所有订单的成交金额总和。alias: [成交总额, 订单总额]relevant_columns:-fact_order.order_amount
7. 执行与验证
7.1 确认 MySQL 已启动
docker ps --filter name=mysql# 期望看到 Up ... (healthy) 且 0.0.0.0:3306->3306/tcp
7.2 执行同步
uv run python conf/sync_db.py
四张表都写完后,输出大致如下:
已写入 table_info 5 条已写入 column_info 24 条已写入 metric_info 2 条已写入 column_metric 2 条
7.3 查库确认
set -a && source .env && set +adocker exec -e MYSQL_PWD="$MYSQL_PASSWORD" mysql mysql --default-character-set=utf8mb4 -u"$MYSQL_USER" -D meta -e "SELECT * FROM table_info;"
--default-character-set=utf8mb4 要加,否则中文会变成 ?????。
8. 常见问题
| 现象 | 原因 | 处理 |
|---|---|---|
No module named 'app' | 工作目录 / sys.path 不对 | 在项目根执行;脚本已自动把根目录加入 path |
No module named 'asyncmy' | 缺异步驱动 | uv add asyncmy |
Access denied ... using password: NO | 密码没从 .env 展开 | 检查 .env、${MYSQL_PASSWORD}、get_config.py |
Duplicate entry '...PRIMARY' | 重复插入相同主键 | 开发期用 drop_all;或改 upsert |
中文显示 ????? | 客户端字符集不对 | 加 --default-character-set=utf8mb4 |
9. 同步 column_info
YAML 里字段长这样:
columns:-name:provincerole:dimensiondescription:订单所属的省份名称。alias: [省份, 省, 所在省份]sync:true
落库字段:
id / name / type / role / description / alias / examples / table_id
映射规则:
id:{table_name}.{column_name},例如dim_region.provincename/role/description/alias:直接来自 YAMLtable_id:所属业务表名type:从 dw 库的information_schema读取examples:当sync: true时,从 dw 表SELECT DISTINCT取值;否则为空列表
所以要先补业务库只读能力:
# app/repositories/dw_db_repository.pyclassDwDBRepository:asyncdefget_column_type(self, table_name: str, column_name: str) ->str | None:# SELECT COLUMN_TYPE FROM information_schema.COLUMNS ...asyncdefget_column_all_values(self, table_name: str, column_name: str) ->list[str]:# SELECT DISTINCT `column` FROM `table` WHERE `column` IS NOT NULL
Service 只是薄封装。同步核心逻辑:
asyncdefsync_column_info(config: dict) ->list[ColumnInfo]:column_infos: list[ColumnInfo] = []asyncwith DwAsyncSessionLocal() as dw_session:dw_service = DwDBService(DwDBRepository(dw_session))for table in config.get("tables", []):table_name = table["name"]for column in table.get("columns", []):column_name = column["name"]column_type = await dw_service.get_column_type(table_name, column_name)examples = (await dw_service.get_column_all_values(table_name, column_name)if column.get("sync", False)else [])column_infos.append(ColumnInfo(id=f"{table_name}.{column_name}",name=column_name,type=column_type,role=column["role"],examples=examples,description=column["description"],alias=column.get("alias", []),table_id=table_name,))asyncwith MetaAsyncSessionLocal() as meta_session:service = ColumnInfoService(ColumnInfoRepository(meta_session))returnawait service.add_all(column_infos)
Entity / Model / Mapper / Repository / Service 的搭法与 table_info 相同。
10. 同步 metric_info
YAML:
metric_info:-name:GMVdescription:全称GrossMerchandiseValue,表示所有订单的成交金额总和。alias: [成交总额, 订单总额]relevant_columns:-fact_order.order_amount
落库字段:
id / name / description / alias / relevant_columns
其中 id 直接用 name,其余字段原样写入。
asyncdefsync_metric_info(config: dict) ->list[MetricInfo]:metric_infos: list[MetricInfo] = []for record in config.get("metric_info", []):metric_infos.append(MetricInfo(id=record["name"],name=record["name"],description=record["description"],alias=record.get("alias", []),relevant_columns=record.get("relevant_columns", []),))asyncwith MetaAsyncSessionLocal() as session:service = MetricInfoService(MetricInfoRepository(session))returnawait service.add_all(metric_infos)
执行后应看到:
已写入 metric_info 2 条:- GMV- AOV
11. 同步 column_metric
column_metric 是字段与指标的多对多关联表,方便后续按字段反查可参与的指标。
它不单独出现在 YAML 里,而是从 metric_info[].relevant_columns 展开:
column_id = relevant_column # 如 fact_order.order_amountmetric_id = record["name"]# 如 GMV
因为 column_info.id 也用「表名.字段名」,所以这里可以直接对齐。
asyncdefsync_column_metric(config: dict) ->list[ColumnMetric]:column_metrics: list[ColumnMetric] = []for record in config.get("metric_info", []):for relevant_column in record.get("relevant_columns", []):column_metrics.append(ColumnMetric(column_id=relevant_column,metric_id=record["name"],))asyncwith MetaAsyncSessionLocal() as session:service = ColumnMetricService(ColumnMetricRepository(session))returnawait service.add_all(column_metrics)
执行后应看到:
已写入 column_metric 2 条:- fact_order.order_amount -> GMV- fact_order.order_quantity -> AOV
联表验证:
docker exec -e MYSQL_PWD="$MYSQL_PASSWORD" mysql mysql --default-character-set=utf8mb4 -u"$MYSQL_USER" -D meta -e "SELECT cm.column_id, cm.metric_idFROM column_metric cm;"
12. 下一步
MySQL meta 四张表已经齐了。后续继续在 sync_db.py 里补:
- Qdrant:把字段 / 指标的描述与别名向量化,供语义召回
- Elasticsearch:把
sync: true的维度值写入全文索引,供值匹配
入口里已经预留:
sync_to_qdrant(meta_config)# TODOsync_to_elasticsearch(meta_config) # TODO
-
07.31
皇家蓝 Anarkali 人像
-
07.31
OpenAI放大招,将向 10 万名科学家免费开放GPT-5. 6 系列!
-
07.31
低价机票越来越难抢?AI正在改变航空公司的定价逻辑
-
07.31
Waymo 首款从零设计网约车 Ojai 亮相:Gemini 上车,三屏各有分工
-
07.31
《古龙群侠传》副本通关攻略
-
07.31
《诡秘之主》罪恶庭根玩法说明
-
-
-
- 寓言故事一则:狗猛酒酸
- 07.31
-
-
- AI 的玩法,该做减法了
- 07.31
-
-
-
下载
- |
-
-
下载
- 《行尸走肉第一章》免安装中文汉化硬盘版下载
- 单机|436 MB
- 一款以动作冒险为主题的游戏
-
-
下载
- 《街头霸王X铁拳》免安装中文汉化硬盘版下载
- 单机|111MB
- 一款非常好玩的格斗游戏
-
-
下载
- |
-
-
下载
- 《暗黑破坏神3》免安装繁体中文正式版下载
- 单机|7630 MB
- 一款以角色扮演为主题的游戏
-
-
下载
- 《马克思佩恩3》免安装硬盘版下载
- 单机|27033 MB
- 一款以第三人称射击为主题的游戏