ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

用information_schema.columns定位全库字段:多租户改造实战

用information_schema.columns定位全库字段:多租户改造实战 前阵子接到一个很常见的需求多租户改造需要把所有包含tenant_id字段的表全部找出来。听起来像是小活等我真在库上一跑才发现事情没那么简单——生产环境光业务表就有上千张横跨十几个 schema如果靠DESC一张张看一上午都不够。后来我用information_schema.columns写了三条模板 SQL十分钟出结果还顺手整理了一份字段资产清单。这篇文章就把这套方法完整拆给你从最基础的查询语法到生产大库下的性能优化、权限坑再到“同时找多个字段”“从存储过程里搜字段”这类进阶需求一次讲透。1. 先从一条 information_schema 查询说起最快路径的写法与执行计划1.1 为什么是 information_schema.columnsinformation_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的字段是不是都是时间类型。就用LIKEWHERE 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) ) ENGINEInnoDB;然后写一个定时任务每天晚上从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加HAVINGSELECT 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.TABLESSELECT 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 SELECTCREATE 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.STATISTICSSELECT 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不用去碰生产库。说实话这套手段不复杂但它能让你在别人还在一张张翻表结构的时候已经把结果甩到群里了。
返回列表