ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

多数据库结构分析工具开发实战:解析MySQL、PostgreSQL与SQLite元数据

多数据库结构分析工具开发实战:解析MySQL、PostgreSQL与SQLite元数据 上个月帮朋友迁移一个老项目需要同时核对MySQL和PostgreSQL两套库的表结构。那体验就像一个人同时用两套方言的字典查单词MySQL里翻information_schema还算顺手换到PostgreSQL就得去pg_catalog里对着pg_attribute、pg_class拼查询字段类型一边叫varchar一边叫character varying。半天下来表没看几张人先被方言差异折磨麻了。后来我干脆用trae solo花了一个周末把“多数据库数据结构分析与查询系统”这个练手项目从头到尾做了出来。这个系统做的事情很简单在一个界面里配置好多个数据库连接就能一键列出所有库的表、字段、索引、主外键并且能对其中任意一张表执行只读查询。听起来不复杂但把“多数据库适配”“元数据解析”“查询执行与安全控制”“前端交互”这条完整链路跑通之后你对数据库、对AI编程工具的协作方式都会有一个质的提升。这篇文章我把整个开发过程拆开讲从项目边界怎么定到trae solo环境怎么搭再到各数据库元数据到底藏在哪、查询引擎的安全边界怎么设最后附上我踩过的几个坑和排查链路。适合刚学编程想找练手项目的人也适合那些天天被“帮我导一份数据字典”折磨的开发者。1. 先想清楚这个练手项目要解决什么问题1.1 痛点来源在不同数据库之间来回切换的烦躁每个数据库的“表结构”都藏在各自的系统表或数据字典里。MySQL查information_schema.TABLESPostgreSQL查pg_catalog.pg_tablesSQLite靠sqlite_masterMongoDB压根没有传统意义上的表只有collection和动态文档。你光记住这套差异就够喝一壶的更别说不同库的字段类型定义还五花八门。业务方找你要一份“数据字典”你打开数据库客户端手动复制字段名、类型、注释粘进Excel格式还得对齐。数据库换一个整套操作再重来一遍。这个项目就是想把这些零散操作收敛成一个固定动作填连接信息、点连接、自动出结构树。1.2 项目边界与功能清单不做什么比做什么更重要练手项目最怕的不是功能少而是AI帮你越加越复杂。我第一版给trae的指令里明确写死了边界核心功能就四块功能模块具体内容连接管理支持配置多个数据库连接连接信息存本地配置结构浏览列出库内所有表、视图展示字段名、类型、注释、主外键、索引数据查询对指定表执行只读SELECT结果以表格展示限制返回行数字典导出把表结构导出为Markdown或JSON方便直接贴给业务方不做什么也很重要不做数据写入和DELETE/UPDATE操作不做用户权限系统不做集群监控不接ORM模型同步。这些如果全做进去两周都未必收尾而且偏离了“数据结构分析”这个核心主题。1.3 为什么选“数据结构分析”作为练手载体选这个方向是有私心的。第一它是刚需做完就能用不会白练。第二它虽然看着简单却覆盖了一个完整应用的所有关键环节配置解析、多数据源连接、底层元数据协议差异处理、统一数据模型设计、安全查询、前端展示。麻雀虽小五脏俱全。另外它跟“数据结构”这门计算机核心课程有天然的呼应。热搜里“数据结构”“数据结构与算法”“王道408”这些词常年居高不下说明很多人都在啃这块硬骨头。而这个项目本身就是一套“数据结构”的活学活用你在解析数据库的表结构同时也在设计一套统一的结构描述模型把异构的方言翻译成一套通用语言。2. trae solo开发环境搭建把第一轮对话变成项目骨架2.1 安装trae与使用配额认知trae目前在国内可以直接官网下载装好后登录账号就能开始对话式开发。网上关于“trae积分兑换码”的讨论不少其实日常练手免费额度完全够用完成新手任务还能攒积分兑换更多上下文额度攒积分本身就是个顺手的事不用太纠结。这里想说一个比安装更重要的认知trae不是一个替你写代码的“打字员”而是一个话很多、记性一般、但执行力很强的实习生。你给它一个明确的小任务它能完成得很好你让它“自己看着办”它能把项目复杂度拉爆。所以后面所有工作流都围绕“小步任务、逐步验收”来展开。2.2 solo模式的协作方式与项目上下文设置trae solo模式的核心是你一个人借助AI完成架构、开发、测试整个过程。为了不让AI“失忆”我在项目根目录放了一份PROJECT.md把技术选型、目录约定、功能边界全部写死。每次开启新对话第一句先让它读这份文件再下达具体任务。PROJECT.md我写了这些内容项目定位和技术栈Python 3.10、SQLAlchemy 2.x、PyWebIO前端不单独写Vue/React目录结构约定connectors/放各数据库适配models/放统一数据结构ui/放交互层连接配置格式统一用JSON包含name、type、host、port、database、username、password明确禁止事项不引入ORM模型同步不实现写操作不允许把连接密码硬编码在源码里有了这份文档后面所有对话都锚定在同一个上下文里AI生成的代码风格和结构会稳定很多。2.3 第一轮对话让AI生成项目骨架我的第一轮提示词是这样写的阅读根目录的PROJECT.md然后完成以下任务 1. 生成requirements.txt只包含运行必需依赖 2. 生成config.example.json包含MySQL、PostgreSQL、SQLite三种示例连接 3. 生成项目目录结构创建包目录并补上__init__.py 4. 实现config_loader.py负责读取和校验连接配置不包含真实密码 5. 写一个database.py内置SQLAlchemy engine创建函数支持mysqlpymysql、postgresqlpsycopg2、sqlite三种方言生成完骨架我不会急着往里面塞功能而是先检查三件事依赖有没有多余的、配置文件里有没有把密码占位符写明白、目录结构和PROJECT.md约定是否一致。骨架稳了后面才不容易翻车。这个检查习惯能从根上减少后期重构成本。3. 吃透各数据库的元数据字典为什么表结构信息藏在“系统表”里3.1 MySQL/PostgreSQL/SQLite的元数据查询对比数据库本身也是一个“程序”它需要把自己的表、字段、索引等信息登记在案这些“登记台账”就是系统表。不同数据库的台账格式完全不同这是整个项目最核心的差异点。MySQL的元数据集中在information_schema查表和字段都特别直观-- MySQL列出当前库所有表 SELECT TABLE_NAME, TABLE_COMMENT FROM information_schema.TABLES WHERE TABLE_SCHEMA DATABASE(); -- MySQL查看某张表的字段信息 SELECT COLUMN_NAME, DATA_TYPE, COLUMN_COMMENT FROM information_schema.COLUMNS WHERE TABLE_SCHEMA DATABASE() AND TABLE_NAME users;PostgreSQL的元数据分散在pg_catalog里查询难度直接上一个台阶。你至少得把pg_class表和视图、pg_attribute列、pg_type类型三张系统表join在一起才能拿到一份像样的字段清单-- PostgreSQL列出public schema下的表 SELECT tablename FROM pg_tables WHERE schemaname public; -- PostgreSQL查看某张表的字段 SELECT a.attname AS column_name, t.typname AS data_type FROM pg_attribute a JOIN pg_class c ON a.attrelid c.oid JOIN pg_type t ON a.atttypid t.oid WHERE c.relname users AND a.attnum 0;SQLite更绝它把表结构直接存在sqlite_master这张表里。注意它的字段信息不是单独的行而是藏在一整条CREATE TABLE的SQL语句文本里要解析SQL才能拿到列名和类型SELECT name, sql FROM sqlite_master WHERE type table;MongoDB则没有固定表结构想分析collection的“结构”得先从集合里采样一批文档再推断字段名和类型分布。这四种风格差异就是“多数据库”四个字背后的真实重量。我整理了一张对比表方便你记住重点数据库元数据位置表结构获取方式字段结构获取方式MySQLinformation_schemaTABLES表COLUMNS表PostgreSQLpg_catalogpg_tables/pg_classjoin pg_attribute pg_typeSQLitesqlite_master表内记录解析CREATE TABLE文本MongoDB无系统表listCollections文档采样推断3.2 字段类型、约束与索引的归一化处理不同数据库的定义方式差异很大MySQL叫varcharPostgreSQL叫character varyingSQLite直接叫VARCHARMongoDB里可能只是字符串。归一化的意义是让前端只认识一套类型体系渲染逻辑不用为每个库写分支。我设计了一套简化版统一类型在models/column_info.py里定义from dataclasses import dataclass from typing import Optional dataclass class ColumnInfo: name: str raw_type: str # 数据库原始类型比如 character varying(64) data_type: str # 统一类型string / int / float / decimal / bool / datetime / text nullable: bool default: Optional[str] comment: str is_primary_key: bool False类型映射规则简单粗暴以字段的实际用途为准而不是只看数据库字符。比如MySQL的BIGINT、PostgreSQL的bigint、SQLite的INTEGER统一映射为intVARCHAR、TEXT、character varying统一映射为string或text。判断text和string的区别主要看原始类型里是否包含text关键字。索引和主外键的提取逻辑也类似。MySQL的主键可以从information_schema.KEY_COLUMN_USAGE拿PostgreSQL要靠pg_index和pg_constraintSQLite用PRAGMA index_list(users)和PRAGMA foreign_key_list(users)。各写一套适配代码成本很高所以我把它们收敛到了下一节讲的统一抽象层里。3.3 统一抽象SQLAlchemy Inspector的价值与坑手写四套元数据适配不是不行但维护成本太高。SQLAlchemy的inspect()接口把前面那些差异全部包装成了统一API这是整个项目能“多库共存”的关键。from sqlalchemy import create_engine, inspect engine create_engine(mysqlpymysql://user:passlocalhost/db) inspector inspect(engine) tables inspector.get_table_names() # 所有表 columns inspector.get_columns(users) # 所有字段 pk inspector.get_pk_constraint(users) # 主键 fks inspector.get_foreign_keys(orders) # 外键 indexes inspector.get_indexes(users) # 索引inspector内部会自动识别数据库方言查对应的系统表然后包装成统一结构返回。我不用再关心它是查information_schema还是pg_catalog这一层抽象就把“方言差异”挡在了业务代码之外。但Inspector也不是万能钥匙有几个坑必须知道get_columns()拿不到MySQL的表注释table comment需要另写一句SHOW TABLE STATUS补查。SQLite的get_table_names()会把视图也混进来一部分因为底层都来自sqlite_master需要在适配层过滤。PostgreSQL默认只读publicschema如果业务表建在自定义schema里要先执行inspector.default_schema_name确认再通过table_options传schema参数。MongoDB没有对应方言项目中单独走pymongo的list_collections()加文档采样逻辑。也就是说Inspector帮我解决了80%的通用场景剩下20%的数据库特性需要在适配层里“定制补丁”。这个“通用抽象 个性补丁”的思路在真实业务系统里同样通用。4. 查询执行引擎的设计安全边界比功能更早确定4.1 只读校验与SQL白名单策略结构看完了下一步就要执行查询。但查询引擎有个天然风险它连接的可能是不止一个业务库。如果哪个粗心同事在工具栏里填了一条DELETE FROM orders后果不堪设想。所以安全边界必须排在所有功能前面设计。我采用了三层拦截策略。第一层语句白名单。所有要执行的SQL去掉首尾空格和注释后必须用SELECT或WITH开头否则直接拒绝。这里注意WITH也要允许因为它可能是只读的CTE查询。第二层禁止多语句。如果SQL里出现分号除非是结尾多余的分号否则一律拦截防止SELECT ...; DROP TABLE users这样的拼接攻击。第三层事务只读回滚。执行查询时开启一个事务查完立刻回滚所有操作都不落库。这样即使中间混入写入语句也会被数据库事务机制兜底抹掉。import re def validate_read_only_sql(sql: str): cleaned re.sub(r--.*?$, , sql, flagsre.MULTILINE).strip() if not re.match(r^(select|with)\b, cleaned, re.IGNORECASE): raise PermissionError(仅支持SELECT查询) # 去掉末尾分号后如果还有分号说明是多语句 body cleaned.rstrip().rstrip(;) if ; in body: raise PermissionError(不支持多语句执行) return body4.2 超时、行数限制与连接池配置安全拦截只是第一步查询性能问题同样要命。一张千万行的大表有人手滑执行了SELECT *数据库直接卡死查询系统自己也会被拖垮。我采取的方案是三层限流。第一层自动补行数限制。如果用户的SQL没有LIMIT就在末尾自动拼接LIMIT 200。注意MySQL和PostgreSQL都支持LIMITSQLite也支持这套语法统一性比较好。第二层应用层超时。用asyncio.wait_for或者threading.Timer给查询设置15秒硬超时到点就取消任务并提示。之所以用应用层超时而不是完全依赖数据库的statement_timeout是为了兼容SQLite这种不支持服务端超时的数据库。第三层连接池限制。SQLAlchemy默认连接池无限增长在生产环境很容易把数据库连接数打满。我把连接池上限设为5超额请求排队等待避免查询系统把库里正常的业务连接挤掉。from sqlalchemy import create_engine from sqlalchemy.pool import QueuePool engine create_engine( url, poolclassQueuePool, pool_size5, max_overflow2, pool_timeout10, connect_args{connect_timeout: 5} # 各数据库略有差异 )4.3 统一结果渲染与前端交互前端交互层我选了PyWebIO原因很简单它是Python生态里的交互UI库生成表格、输入框、下拉选择都很快非常适合AI辅助开发不需要再引入前后端分离那套工程。整个界面长这样左侧是连接配置区可以从config.json加载已保存的连接点一下就连上中间是表列表点击任意表名右侧立即展示字段详情、主外键、索引底部是SQL输入区输入SELECT语句后执行结果以表格渲染到下方结构展示部分的核心就是遍历前面inspector返回的columns、indexes、fks把它们整理成PyWebIO的put_table()数据。字段注释显示在列名旁边有注释的字段一眼就能看懂含义。这个交互虽然朴素但恰恰是“练手项目”该有的样子少一点花哨多一点把事办成的踏实感。5. trae solo协作开发中的典型翻车现场与排查链路5.1 事件一数据库连接串方言错误第一个翻车发生在连MySQL的时候。我在配置里写了mysql://user:passlocalhost/test结果运行直接报错ModuleNotFoundError: No module named MySQLdbMySQLdb是旧驱动现代Python项目通常用pymysql或mysql-connector-python。SQLAlchemy需要的连接串格式必须带上方言驱动名写成mysqlpymysql://。这个坑在AI生成代码时特别常见因为它默认的连接串格式往往停留在老教程里。排查链路是这样的先看报错堆栈确认是驱动缺失然后在requirements.txt里补上pymysql最后把连接串改成mysqlpymysql://user:passlocalhost/test。改完后为了让trae以后不再犯同样的错我在PROJECT.md里追加了一句“所有连接串必须用方言驱动格式例如mysqlpymysql、postgresqlpsycopg2”。上下文一更新后面生成的新代码就再没出现过这种低级错误。5.2 事件二SQLite元数据查询不兼容与视图混入第二件事更隐蔽。用trae生成的SQLite适配代码加载出来的“表”列表里混进了几条根本不是表的东西一查才知道是视图。SQLAlchemy的inspect(engine).get_table_names()在SQLite底层依赖sqlite_master它会把视图也一起吐出来需要再用get_view_names()把视图列表取出来然后在展示时过滤掉。排查这个问题的过程是我觉得最有价值的一段。我没有直接改代码而是先让trae打印SQLAlchemy内部实际执行的SQL语句确认它查的是哪张系统表。看到sqlite_master那一刻问题就清楚了视图和表的type字段不一样需要区分。修正方案是在连接适配层里加一段过滤逻辑tables inspector.get_table_names() views set(inspector.get_view_names()) real_tables [t for t in tables if t not in views]同时我在结构浏览界面加了一个“视图”tab把视图单独放在一个标签页里展示而不是简单粗暴地过滤掉。因为视图在分析数据结构时同样有用只是不该和表混在一起。5.3 如何向AI准确描述Bug排查链路总结经历了这几轮翻车我总结出一套向trae描述Bug的模板基本可以避免“AI来回瞎猜”的低效循环Bug描述 在[什么环境/哪个页面]复现 - 期望行为[希望看到什么结果] - 实际行为[实际看到了什么结果] - 日志/报错[贴完整报错不要截断] - 我已经尝试过[说明已做过的操作避免AI重复建议] - 请先输出排查思路不要直接改代码确认思路后再修改[X]文件重点在最后一条。大多数情况下AI拿到bug就急着改代码改出来的方案往往方向不对。让它先输出排查思路你来判断方向合不合理确认后再动手能省一半以上的时间。还有一个小技巧当AI改完代码仍然报错时不要重复描述一遍原始错误。直接把“现在报错变成了什么”发过去并附上新的完整日志。错误信息每变化一次就意味着排查前进了一步让AI基于最新状态继续推理而不是回到起点重跑一遍。6. 实测数据与后续扩展方向6.1 三库加载结构耗时与体验记录项目基本跑通之后我分别连了三个库做了简单压测记录下体感数据数据库表数量加载结构耗时备注MySQL 8.0本地Docker68张约0.8秒有表注释展示效果好PostgreSQL 14本地Docker42张约1.2秒首次加载pg_catalog稍慢但稳定SQLite本地文件97张约0.05秒本地文件几乎是秒开MySQL和PostgreSQL的加载时间都集中在“网络连接查询系统表”上。如果后续接的是远程生产库网络延迟会把耗时放大几倍所以我在连接逻辑里加了缓存同一个连接5分钟内重复查看表结构直接走内存缓存不重复查系统表。SQLite的表现则印证了一个结论本地文件型数据库在结构分析上完全没有性能压力瓶颈全在“你能不能记住怎么查它的元数据”。6.2 扩展方向国产数据库、ER图、数据字典导出练手到一定程度这个项目自然会长出更多实际用途。我自己列了几个可扩展的方向数据字典导出增强目前已经支持Markdown和JSON导出。可以继续增加Word和Excel导出做出来后公司内部做数据治理的人会排队来借你的工具。ER图预览基于外键信息和索引关系用一个轻量级JS库把表关系渲染成ER图。这个功能不复杂但视觉冲击力很强特别适合练手展示。接更偏门的数据库国内单位常见的达梦、人大金仓只要确认SQLAlchemy方言包存在基本可以像其他数据库一样平滑接入。接入时重点测元数据查询和类型映射其余逻辑不用动。结构对比与漂移检测对同一业务库在不同环境测试、预发布、生产的表结构做差异比对。这个功能做出来后数据库变更评审可以直接从“看脚本”变成“看自动对比报告”价值非常大。这些方向里我最推荐先做ER图预览。它把静态的字段列表变成可视化结构图对理解“数据结构”这个概念本身也有帮助而且实现周期短一个周末就能加完。我在实际开发中的体会是这个练手项目最大的收获不是那几千行代码而是学会了跟AI协作的节奏把大目标拆成小任务、把关键决策写进项目文档、用结构化方式描述bug。项目本身后续想怎么扩展方向已经很清楚但更值得珍惜的是这套“一个人 AI 全栈交付”的方法论它以后做任何项目都能复用。最后再分享一个实战小技巧每次让trae开工前先花五分钟把这次的输入条件、期望输出、禁止事项写成一段“提示词备忘录”。别嫌慢这五分钟能帮你省下后面一小时的返工时间。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进