前阵子接到一个很常见的需求:多租户改造,需要把所有包含tenant_id字段的表全部找出来。听起来像是小活,等我真在库上一跑,才发现事情没那么简单——生产环境光业务表就有上千张,横跨十几个 schema,如果靠DESC一张张看,一上午都不够。后来我用information_schema.columns写了三条模板 SQL,十分钟出结果,还顺手整理了一份字段资产清单。这篇文章就把这套方法完整拆给你,从最基础的查询语法,到生产大库下的性能优化、权限坑,再到“同时找多个字段”“从存储过程里搜字段”这类进阶需求,一次讲透。
1. 先从一条 information_schema 查询说起:最快路径的写法与执行计划
1.1 为什么是 information_schema.columns
information_schema是 MySQL 里的“元数据库”,里面存的是关于数据库本身的元数据:哪些库存在、每张表叫什么、每个字段叫什么、类型是什么。其中COLUMNS表记录了所有库、所有表的字段级信息,一条 SQL 就能把全库的“字段清单”拉出来。
很多人一上来就去翻工具里的表列表,或者写脚本循环SHOW FULL COLUMNS FROM 表名,这在小库没问题,但表一多基本上属于“手工时代”。直接查information_schema.COLUMNS,本质就是让数据库自己把目录翻开给你看,效率和准确度都高一个量级。
最基础的一条查询长这样:
SELECT TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME, COLUMN_TYPE, COLUMN_COMMENT FROM information_schema.COLUMNS WHERE COLUMN_NAME = 'tenant_id' AND TABLE_SCHEMA NOT IN ('mysql', 'information_schema', 'performance_schema', 'sys') ORDER BY TABLE_SCHEMA, TABLE_NAME;这条 SQL 干了三件事:
- 过滤字段名:
WHERE COLUMN_NAME = 'tenant_id',定位目标字段。 - 排除系统库:MySQL 自带的那几个库全是系统内部对象,业务排查时基本不需要。
- 按库和表排序:结果一眼就能看全,方便后续复制表名去核查。
1.2 三种最常用的匹配模板
实际工作中,字段名很少能精确记住,更多是“大概叫这个名字”。所以我把模板扩成了三种,覆盖绝大多数场景。
精确匹配:字段名完全一致,适合tenant_id、created_at这类命名规范、约定统一的字段。
WHERE COLUMN_NAME = 'tenant_id'模糊匹配:字段名记不全,或者想知道所有包含某个关键词的字段,比如所有带id的字段是不是都有索引、所有含time的字段是不是都是时间类型。就用LIKE:
WHERE COLUMN_NAME LIKE '%tenant%'注意这里前后的%会带来两个问题:一是查询变慢,二是有可能误匹配。比如搜%user%会把sys_user、user_name、end_user_type全捞出来。推荐先用精确模板查,再用模糊模板去查“疑似相关”的候选字段,分两步走,结果才可控。
按注释匹配:很多系统字段名是英文缩写,但注释是中文全称。比如CUSTNO的注释可能是“客户编号”,这时候你记不清字段名,只记得业务含义,就用注释来搜:
WHERE COLUMN_COMMENT LIKE '%客户编号%'三种模板可以任意叠加。比如“查所有注释包含‘金额’且字段类型带 decimal 的字段”,直接拼条件就行。
1.3 结果列里哪些信息值得看
搜索结果不只给你“哪个库哪张表”这一个答案,information_schema.COLUMNS里还有几个关键列,能帮你做初步判断,省得再回表里确认:
ORDINAL_POSITION:字段在表里的第几列,数字越小越靠前,一般主键在1位。COLUMN_TYPE:完整类型,包括长度,比如varchar(64)、decimal(18,2)。IS_NULLABLE:是否允许 NULL,改造字段、加唯一索引时特别重要。COLUMN_KEY:这个字段是否是索引的一部分。空字符串表示没索引,PRI表示主键,UNI表示唯一索引,MUL表示非唯一索引。看到PRI直接就能确认“这就是主键”。COLUMN_COMMENT:字段注释,经常比字段名还重要。
所以实际排查时,我一般会把查询写成只保留这几列,多一列都不要。字段少、网络传输小、结果也干净。
2. 生产级大库不能蛮干:查询变慢的根因与三个提速手段
2.1 为什么几千张表时查询会卡到怀疑人生
写 SQL 谁都会,但生产环境动辄几千张表、几十万个字段,直接跑上面那条 SQL,结果死活不出来,很多人第一反应是“数据库出问题了”。其实这是 MySQL 版本特性导致的。
MySQL 5.7 及更早版本里,information_schema.COLUMNS的数据来源是扫描每个表的元数据文件(InnoDB 表对应.frm文件)。表少的时候感受不到,表一多,每次查询都相当于把整个数据目录翻一遍,把所有表的字段定义读一遍,再拼成结果给你。我见过一个实例,光业务表就有4000多张,裸查COLUMNS表花了将近40秒。40秒对于一条查询来说,已经算“事故级”了。
MySQL 8.0 改成了事务性数据字典,字段定义直接存在数据字典里,内存读取,速度比 5.7 快很多。但生产环境想升级不是一朝一夕的事,5.7 仍然大量存在。所以下面的提速方案,都假设你活在真实世界里,可能用的是 5.7。
2.2 提速手段一:把查询挪到从库,并固定在低峰期执行
这个手段成本最低,效果最直接。
information_schema的查询虽然不碰业务数据,但扫描元数据文件需要消耗 CPU 和 IO。在主库上跑一次 40 秒的全库扫描,赶上业务高峰,等于给数据库追加了一次不大不小的 IO 压力。所以我的习惯是:所有“找字段”“统计字段”这类元数据排查,一律去从库执行。从库同步数据,元数据结构完全一致,查出来的结果和主库没有任何差别。
如果用的是 MySQL 5.7,还要注意一个细节:不要在从库正在追大量事务日志的时候跑全库扫描,那时候从库本身 IO 就在高水位,元数据库扫描会和 SQL 线程抢资源。建议选在凌晨或业务低峰期,配合定时任务去跑,第二天早上看结果就行。
2.3 提速手段二:物化一份字段清单到业务库,秒级出结果
如果“找字段”不是一次性需求,而是每周都要做,那每次都去扫information_schema就太笨了。更聪明的做法是:把字段清单同步到自己的元数据管理库里,之后所有搜索都在本地表上进行,速度从几十秒降到零点几秒。
我自己的做法是建一张column_inventory表,结构很简单:
CREATE TABLE metadata.column_inventory ( id INT PRIMARY KEY AUTO_INCREMENT, table_schema VARCHAR(64) NOT NULL, table_name VARCHAR(64) NOT NULL, column_name VARCHAR(64) NOT NULL, ordinal_position INT NOT NULL, column_type VARCHAR(255) NULL, is_nullable VARCHAR(3) NULL, column_key VARCHAR(3) NULL, column_comment VARCHAR(1024) NULL, collected_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_schema_table_column (table_schema, table_name, column_name) ) ENGINE=InnoDB;然后写一个定时任务,每天晚上从information_schema.COLUMNS全量刷新这张表:
TRUNCATE TABLE metadata.column_inventory; INSERT INTO metadata.column_inventory (table_schema, table_name, column_name, ordinal_position, column_type, is_nullable, column_key, column_comment) SELECT TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME, ORDINAL_POSITION, COLUMN_TYPE, IS_NULLABLE, COLUMN_KEY, COLUMN_COMMENT FROM information_schema.COLUMNS WHERE TABLE_SCHEMA NOT IN ('mysql', 'information_schema', 'performance_schema', 'sys');你可能会担心全量刷新太慢。实际上这一层数据量并不夸张,一个5000张表的库,平均每张表30个字段,总共也就15万行,TRUNCATE加INSERT一条 SQL 十几秒搞定。有了这张表之后,查字段就是普通的业务查询:
SELECT table_schema, table_name, column_name, column_comment FROM metadata.column_inventory WHERE column_name = 'tenant_id';这个方案还有个额外好处:可以把元数据查询权限发给开发同学,他们不用碰生产information_schema,直接在你们的元数据库里查就行,权限管理也更干净。
2.4 提速手段三:MySQL 8.0 里调整元数据统计缓存
如果你已经上了 MySQL 8.0,有一个参数需要知道:information_schema_stats_expiry,默认值是86400,也就是一天的秒数。
这个参数控制的是information_schema里部分统计信息的缓存时长,比如TABLES表的TABLE_ROWS、AUTO_INCREMENT、AVG_ROW_LENGTH这些统计列。默认情况下,这些统计值一天内不会重新计算,你查到的是昨天的快照。
这对“找字段”影响不大,因为字段定义本身是实时的。但如果你查询时还顺带关心“这张表大概多少行”,那就要注意统计值可能滞后。想拿最新的统计值,可以在当前会话里执行:
SET SESSION information_schema_stats_expiry = 0;再查information_schema.TABLES,就能拿到实时统计。注意是SESSION级别的,改全局会影响所有查询的性能,不建议在生产随便动。
3. 实战复盘:一次完整的字段定位与资产整理
3.1 场景设定:多租户改造,主动找所有带租户标识的表
方法讲完了,走一遍完整案例。
假设系统要做多租户改造,需要知道“哪些表已经有租户字段,哪些表还没有”。租户字段在系统里可能叫tenant_id,也可能叫org_id或者app_id,不同团队开发习惯不一样。这时候靠猜没用,得全库摸底。
第一步,先跑一个字段出现频率的统计,看看租户相关字段在整个库里到底有几种叫法:
SELECT COLUMN_NAME, COUNT(*) AS table_count FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = 'business' AND (COLUMN_NAME LIKE '%tenant%' OR COLUMN_NAME LIKE '%org_id%' OR COLUMN_NAME LIKE '%app_id%') GROUP BY COLUMN_NAME ORDER BY table_count DESC;结果里可能有tenant_id、tenant_code、org_id、app_id好几种叫法。这一步能帮你先建立全局认知:哪些叫法用得最多,哪些叫法只是零星出现在某几张表里。
第二步,把所有这些候选字段完整列出来,确认每张表的归属:
SELECT TABLE_NAME, COLUMN_NAME, COLUMN_COMMENT FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = 'business' AND (COLUMN_NAME IN ('tenant_id', 'tenant_code', 'org_id', 'app_id')) ORDER BY TABLE_NAME, COLUMN_NAME;拿到这个清单后,可以按表名分组去核对,也可以直接导出成 Excel。这一步的结果就是最原始的改造范围清单。
3.2 组合条件锁定“同时包含多个字段”的表
改造时经常遇到另一种需求:找“同时包含created_at和updated_at的表”,这种表多半是规范化的审计表,或者找“同时包含status和type的表”,判断哪些表需要同步修改状态逻辑。
SQL 写法用GROUP BY加HAVING:
SELECT TABLE_SCHEMA, TABLE_NAME, GROUP_CONCAT(COLUMN_NAME ORDER BY ORDINAL_POSITION SEPARATOR ', ') AS column_list FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = 'business' AND COLUMN_NAME IN ('created_at', 'updated_at') GROUP BY TABLE_SCHEMA, TABLE_NAME HAVING COUNT(DISTINCT COLUMN_NAME) = 2 ORDER BY TABLE_SCHEMA, TABLE_NAME;关键在HAVING COUNT(DISTINCT COLUMN_NAME) = 2,表示两张表字段都存在才算命中。如果只要“包含其中任意一个”,把HAVING去掉就行。GROUP_CONCAT把命中的字段名拼在一行里,方便肉眼快速核查,尤其是命中字段多、想直接看完整字段列表时,很省事。
3.3 区分基表和视图:JOIN TABLES 表过滤掉干扰项
生产库里除了业务表,还有大量视图,有时候还有临时表。找字段时,视图会混在结果里,影响判断。区分方法很简单,information_schema.COLUMNS里没有表类型信息,需要 JOIN 一下information_schema.TABLES:
SELECT c.TABLE_NAME, c.COLUMN_NAME, t.TABLE_TYPE FROM information_schema.COLUMNS c JOIN information_schema.TABLES t ON t.TABLE_SCHEMA = c.TABLE_SCHEMA AND t.TABLE_NAME = c.TABLE_NAME WHERE c.COLUMN_NAME = 'tenant_id' AND c.TABLE_SCHEMA = 'business' AND t.TABLE_TYPE = 'BASE TABLE' ORDER BY c.TABLE_NAME;只看BASE TABLE,视图和系统临时表自然被过滤掉。这个方法在统计基表范围时特别有用,否则结果里混着十几个视图,工程量会翻倍。
3.4 把结果落成资产清单:CREATE TABLE AS SELECT 或定时同步
排查结果不能只停留在 SQL 结果集里,我习惯把它直接落地成一张表,方便后续反复查询和团队共享。
最省事的方式是CREATE TABLE AS SELECT:
CREATE TABLE metadata.tenant_column_audit AS SELECT c.TABLE_SCHEMA, c.TABLE_NAME, c.COLUMN_NAME, c.COLUMN_TYPE, c.COLUMN_COMMENT, t.TABLE_TYPE FROM information_schema.COLUMNS c JOIN information_schema.TABLES t ON t.TABLE_SCHEMA = c.TABLE_SCHEMA AND t.TABLE_NAME = c.TABLE_NAME WHERE c.COLUMN_NAME IN ('tenant_id', 'tenant_code', 'org_id', 'app_id') AND t.TABLE_TYPE = 'BASE TABLE' ORDER BY c.TABLE_SCHEMA, c.TABLE_NAME;以后想查“哪些表有 tenant_id 但没建索引”,直接在这张表上二次过滤就行。配合前面说的定时刷新,这张资产清单表就成了团队里的“字段字典”,新人来了也能自己查,不用到处问人。
3.5 用注释列还原字段的业务含义
字段名是英文、注释是中文是常态。第三层筛选时,我一般会把COLUMN_COMMENT带出来看一遍。很多字段名看起来相似但业务意思完全不同,比如amt和amount,单看字段名以为重复,其实注释一个写“余额”、一个写“总额”。注释能帮你排除大量“同名不同义”的干扰项。
特别是在日语、韩语等非英语项目里,注释可能是本地语言,字段名反而不直观,这时候按注释搜索甚至比按字段名搜索更靠谱。
4. 进阶玩法:索引定位、复合字段组、存储过程搜索
4.1 判断字段是主键还是索引的一部分
前面提到COLUMN_KEY字段能区分PRI、UNI、MUL,但要注意:MUL只能告诉你“这个字段是某个非唯一索引的一部分”,不能告诉你它在索引里的第几个位置。
复合索引场景下,这个信息很重要。比如有个联合索引idx_tenant_time (tenant_id, created_at),你搜created_at时,COLUMN_KEY也是MUL,但它在索引里排第二,单独拿created_at做条件时可能用不上索引。
想查字段在复合索引里的位置,用information_schema.STATISTICS:
SELECT TABLE_NAME, INDEX_NAME, SEQ_IN_INDEX, COLUMN_NAME FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = 'business' AND COLUMN_NAME = 'created_at' ORDER BY TABLE_NAME, INDEX_NAME, SEQ_IN_INDEX;SEQ_IN_INDEX显示字段在索引中的顺序。排查某个字段是否适合单独建索引时,这一步能避免误判——你以为它已经在索引里了,实际可能只是某个复合索引的附属字段。
4.2 同时找多个字段,且要求“完全同时出现”
这个在 3.2 节已经提过,这里补充一个变体:要求“至少包含任意一个”和“必须同时包含全部”两种语义要分清楚。
“至少包含任意一个”直接用WHERE COLUMN_NAME IN (...),不需要GROUP BY。“必须同时包含全部”必须用GROUP BY + HAVING COUNT(DISTINCT COLUMN_NAME) = N。N 就是你要找的字段个数,如果你要同时找 5 个字段,就写= 5。
特别注意COUNT(DISTINCT COLUMN_NAME)而不是COUNT(COLUMN_NAME),否则同一字段在表里重复出现时(正常不会,但历史遗留表有可能),计数会虚高。
4.3 在存储过程、函数、触发器里搜索字段
有些字段不一定只出现在表结构里,还可能出现在存储过程、函数、触发器的定义中。如果改字段名,这些代码也是要一起改的。
information_schema.ROUTINES表存了存储过程和函数的定义,直接搜定义文本:
SELECT ROUTINE_SCHEMA, ROUTINE_NAME, ROUTINE_TYPE FROM information_schema.ROUTINES WHERE ROUTINE_DEFINITION LIKE '%tenant_id%';注意两点。
第一,ROUTINE_DEFINITION可能很长,模糊匹配会扫描所有存储过程,库多定义多时会慢,建议加上ROUTINE_SCHEMA = 'business'条件缩小范围。
第二,这个视图对权限要求比较高,普通开发账号可能查不到定义内容,需要用有权限的账号执行。触发器类似,在information_schema.TRIGGERS里搜索ACTION_STATEMENT LIKE '%字段名%'就行。
4.4 可视化工具能做什么,不能做什么
总有人问:用 Navicat、DataGrip 这类工具找字段不就行了,为什么还要写 SQL?
我的回答是:小库、开发库,工具完全够用。DataGrip 的全局搜索(双击 Shift 搜表名/字段名)、Navicat 的“查找表/视图”功能,都能快速找字段。但生产大库,工具反而不好用——它要先拉取全库元数据缓存到本地,几千张表时首次加载卡到没法看,而且工具一般只能找表名和字段名,想跑“同时包含多个字段”“按注释搜索”“在存储过程里搜索”这类高级条件,工具就无能为力了。
所以我的习惯是:日常开发用工具,生产排查用 SQL 脚本。工具负责快速浏览,脚本负责严谨排查。
5. 容易踩的坑与生产环境安全红线
5.1 权限不同导致搜索结果“缺数据”
这个坑我踩过一次。当时用开发账号执行information_schema.COLUMNS查询,结果只返回了几张表,我当时以为业务库这些表都没有目标字段,后来用管理员账号一查,发现搜出来的表数量翻了十倍。
原因很简单:MySQL 的information_schema会按当前账号权限过滤数据。普通账号只能看到自己有权限访问的对象,没有权限的库和表,元数据也不会展示。
排查时务必确认账号权限和实际库表数量一致。验证方法:
SHOW DATABASES; SELECT COUNT(*) FROM information_schema.TABLES;如果数量和 DBA 提供的库表总数对不上,说明账号权限不够,要么换账号执行,要么找 DBA 临时授权。用错账号查出来的“没找到”,可能只是权限屏蔽,不是真的没有。
5.2 字段名是关键字:上反引号保平安
“mysql 表中字段为关键字”这类问题,网上问的人特别多。比如有个老表里有个字段叫order,或者desc,写 SQL 时必须要用反引号包起来:
SELECT `order`, `desc` FROM business.orders_old;在information_schema.COLUMNS里按条件过滤时不受影响,因为WHERE COLUMN_NAME = 'order'里order是字符串值,不是标识符。
但如果你想用GROUP_CONCAT拼出一段 DDL 去生成ALTER TABLE语句,比如“把所有含 tenant_id 且没有索引的表生成加索引语句”,那拼出来的字段名必须加反引号:
SELECT CONCAT('ALTER TABLE `', TABLE_SCHEMA, '`.`', TABLE_NAME, '` ADD INDEX `idx_', COLUMN_NAME, '` (`', COLUMN_NAME, '`);') AS ddl FROM information_schema.COLUMNS WHERE COLUMN_NAME = 'tenant_id' AND TABLE_SCHEMA = 'business' AND COLUMN_KEY = '';这样拼出来的 DDL 里,order、desc、group这类关键字字段名也不会执行时报错。这个小技巧,能让排查结果直接变成可执行的改造脚本。
5.3 表名大小写:Linux 上不能含糊
MySQL 的字段名不区分大小写,所以COLUMN_NAME = 'TENANT_ID'和'tenant_id'结果一样。但表名在 Linux 环境下默认区分大小写,Tenant_Info和tenant_info是两张完全不同的表。
所以按表名过滤、排序、拼接 DDL 时,统一小写管理更稳妥。如果业务系统跨平台部署,还要确认lower_case_table_names参数设置,这个参数不一致会导致数据文件命名规则不同,备份迁移时特别容易踩坑。
5.4 高峰期不要跑大范围LIKE '%xxx%'
前面说information_schema查询不碰业务数据,但“不碰业务数据”不等于“没有成本”。在 MySQL 5.7 里,全库模糊匹配字段名,底层要做全量元数据文件扫描,IO 压力和磁盘扫描类似,高峰期跑照样会拖慢实例。
我的红线是:
- 高峰期只跑
COLUMN_NAME = 'xxx'这种精确匹配,速度可控。 COLUMN_NAME LIKE '%xxx%'、ROUTINE_DEFINITION LIKE '%xxx%'这类模糊搜索,一律在低峰期或从库执行。- 超过 30 秒没出结果的元数据查询,直接终止,改成物化表方式夜里跑。
这条经验在 5.7 大库上特别重要,8.0 会好很多,但谨慎一点总没错。
5.5 分区表不用逐个分区查
还有一个常见误解:以为分区表在information_schema里会按分区显示多张表。实际上,分区表在COLUMNS和TABLES里就是一张逻辑表,字段列表是统一的,不需要逐个分区去查字段。只有在information_schema.PARTITIONS里,你才能看到每个分区的具体信息,比如分区名、行数、数据大小。这和“找字段”是两码事,不用混在一起。
最后分享两个我一直在用的小习惯。一是把这套查询模板固化下来,放到团队 Wiki 里,遇到“找字段”这种需求直接复制改个字段名就行,不用现想 SQL。二是更推荐打造自己的字段资产表,让定时任务每晚从information_schema同步一次到元数据库,整个团队查字段就是一条秒回的 SQL,不用去碰生产库。说实话,这套手段不复杂,但它能让你在别人还在一张张翻表结构的时候,已经把结果甩到群里了。