资讯详情

批量删除 MySQL 同前缀表:information_schema 与 Python 脚本安全实践

📅 2026/10/14 23:54:10 | 华诺云谱 👁 阅读
批量删除 MySQL 同前缀表:information_schema 与 Python 脚本安全实践
简介这是一份面向MySQL数据库管理者的PHP实用工具用于按指定前缀快速批量删除数据表特别适合开发或测试环境中的表清理与重置场景。工具以PHP源码形式提供可在支持PHP的服务器上直接运行代码结构简单便于开发者根据实际需求调整前缀匹配规则或增加安全校验。压缩包共含3个文件其中killsql.php为主脚本负责连接数据库并执行批量删除说明.htm提供配置步骤与使用注意事项另有chinaz.com.txt文本资料整体压缩包仅3KB轻量易部署。目前已有198人学习或下载对需要频繁清理临时表、重建测试数据的开发人员而言这份工具能显著减少手工操作耗时降低误删风险同时可作为学习PHP与MySQL交互的入门参考。1. 两百张 tmp_ 表摆在面前批量删除 MySQL 同前缀表 v1.0 到底解决什么有一类清理工作放着放着就成了事故现场应用每跑一轮任务就留下几张tmp_开头或者log_2024_开头的表半年过去库里堆了两百多张前缀相同但再也用不上的表。手工DROP TABLE一条条敲敲到第 50 条耐心就没了写个循环一把梭又怕LIKE写宽了把正常业务表一起带走。「批量删除 MySQL 数据库相同前缀的数据表 v1.0」要解决的就是这个场景给定库名和前缀自动查出匹配的表清单确认后分批执行 DROP并对误删风险做前置拦截。它面向的是手里只有业务库账号、经常被临时表和数据残留折磨的后端开发与运维同学而不是专职 DBA。下面按这个 v1.0 的完整实现路径来拆解先对比三条可行路线再给可复现的 Python 脚本最后把参数调优、踩坑和验证方法一次说清。2. 批量删表的三条实现路线information_schema、SHOW TABLES 与存储过程删除相同前缀的表拆开看就两步先拿到「哪些表命中了前缀」再对清单执行 DROP。工具靠不靠谱八成的风险集中在第一步——清单里多一张表就多一次误删的可能。常见的清单来源有三条SHOW TABLES LIKE、information_schema.tables视图、存储过程动态拼接。选型时我只看三点清单能不能过滤视图、返回列结构稳不稳定、执行前能不能看到完整计划。三条路走下来只有一条扛得住生产环境。2.1 SHOW TABLES LIKE能救急但别让它承担批量删除SHOW TABLES LIKE是最直观的入口拿到前缀拼一下就行SHOW TABLES FROM my_app LIKE tmp\_%;这条语句会返回所有以tmp_开头的表优点是不用 JOIN、不用查元数据连上客户端就能跑。但它有三个明显短板第一结果集里分不清表、视图还是临时表视图一旦进入删除清单DROP 会把业务维度的逻辑对象一起清掉第二拿不到存储引擎、表大小、字符集这类信息没法做「先删小表再删大表」的排序也没法评估外键影响面第三结果列的命名在不同版本里不一致早期版本返回的列名就是Tables_in_db脚本按列名解析时会被版本差异坑到。还有一层隐藏问题LIKE里的下划线是通配符。tmp_%中的_会匹配任意一个字符所以必须写成tmp\_%。第一次用这个方案的人十个里有八个会漏掉反斜杠。用来临时确认几张小表可以但要把它做成一个长期复用的删除工具清单不可审计这一点就不合格。v1.0 不采用这条路。2.2 information_schema.tablesv1.0 的清单来源information_schema.tables是 MySQL 元数据的统一出口每张表的表名、类型、引擎、行数、大小都在里面而且对普通账号可读不需要额外授权。v1.0 的清单查询固定成下面这个结构SELECT TABLE_NAME, TABLE_TYPE, ENGINE FROM information_schema.tables WHERE TABLE_SCHEMA my_app AND TABLE_TYPE BASE TABLE AND TABLE_NAME LIKE tmp\_% ORDER BY TABLE_NAME;TABLE_TYPE BASE TABLE把视图和临时表排除在删除范围外这是整个查询里最关键的过滤条件。ENGINE字段用来评估表类型遇到外键依赖时InnoDB 和 MyISAM 的处理策略完全不一样。ORDER BY TABLE_NAME保证每次执行拿到的清单顺序一致脚本跑一半中断后重跑时能稳定跳过已删除的表。相比SHOW TABLES这条路的最大价值是清单可过滤、可统计、可 JOIN。比如想统计匹配到多少张表直接SELECT COUNT(*)想知道哪些表被外键引用可以 JOINKEY_COLUMN_USAGE。同样是拿前缀匹配表名这套查询的列名和语义跨版本稳定适合写进工具里长期使用。2.3 存储过程服务器上没有 Python 时的备选方案如果目标机器上装不了 Python 和客户端库也可以把删除逻辑直接写进数据库用存储过程跑。常见做法是游标遍历information_schema.tables拼出一条 DROP 语句再PREPARE执行DELIMITER $$ CREATE PROCEDURE drop_by_prefix(IN db_name VARCHAR(64), IN prefix VARCHAR(64)) BEGIN DECLARE done INT DEFAULT 0; DECLARE tbl VARCHAR(64); DECLARE cur CURSOR FOR SELECT TABLE_NAME FROM information_schema.tables WHERE TABLE_SCHEMA db_name AND TABLE_NAME LIKE CONCAT(prefix, %); DECLARE CONTINUE HANDLER FOR NOT FOUND SET done 1; SET sql ; OPEN cur; read_loop: LOOP FETCH cur INTO tbl; IF done THEN LEAVE read_loop; END IF; SET sql CONCAT(sql, , tbl, ,); END LOOP; CLOSE cur; IF LENGTH(sql) 0 THEN SET sql CONCAT(DROP TABLE IF EXISTS , LEFT(sql, LENGTH(sql) - 1)); PREPARE stmt FROM sql; EXECUTE stmt; DEALLOCATE PREPARE stmt; END IF; END$$ DELIMITER ;这段过程能跑但我只在「服务器没有外部环境」的场景下用它。它的短板很明显清单没有落盘执行前看不到到底删哪些表白名单、批量大小、跳过逻辑全部写死在过程里想加一个--skip参数就得改存储过程本身出错时只有一条错误日志想定位是哪张表触发的外键约束得自己拼 SQL 去查。结论很明确临时清理用 2.2 的查询手工确认足够想做一个交付给团队复用的工具外部脚本在可观测性和可控性上远胜存储过程。v1.0 选外部脚本路线。提示无论走哪条路线删表前先确认账号有没有 DROP 权限。information_schema对任何账号可读能查出清单不代表能删表。3. 落地 v1.0 脚本连接配置、前缀匹配、分批 DROP 与演练确认v1.0 的定位是「可复现、可审计」的小工具一个 Python 脚本零配置文件依赖只有一个pymysql。处理范围限定在单库单前缀不做正则、不做多库模糊匹配。把边界写死在脚本里比把灵活性留给使用者更安全。下面的实现按「参数解析 → 查清单 → 分批执行 → 演练确认」四段拆开每一段都是完整可跑的代码。3.1 参数设计把密码交给环境变量把选择交给命令行先写参数解析。密码不放在命令行参数里是因为/proc目录会记录完整命令行密码容易泄漏给同机其他进程。统一从环境变量MYSQL_PWD读取# drop_tables_by_prefix.py import argparse import os import logging import pymysql logging.basicConfig(levellogging.INFO, format%(asctime)s [%(levelname)s] %(message)s) logger logging.getLogger(drop_by_prefix) def parse_args(): parser argparse.ArgumentParser(description批量删除 MySQL 同前缀数据表 v1.0) parser.add_argument(--host, default127.0.0.1, helpMySQL 地址) parser.add_argument(--port, typeint, default3306, helpMySQL 端口) parser.add_argument(--user, requiredTrue, help数据库账号需有 DROP 权限) parser.add_argument(--database, requiredTrue, help目标库名例如 my_app) parser.add_argument(--prefix, requiredTrue, help表前缀例如 tmp_) parser.add_argument(--skip, default, help精确跳过的表名逗号分隔) parser.add_argument(--batch-size, typeint, default50, help每批删除的表数量) parser.add_argument(--dry-run, actionstore_true, help演练模式不执行 DROP) parser.add_argument(--yes, actionstore_true, help跳过交互确认生产环境慎用) args parser.parse_args() args.password os.environ.get(MYSQL_PWD, ) if not args.password: parser.error(请通过环境变量 MYSQL_PWD 传入密码避免命令行泄漏) return args这个参数表里最重要的三个开关是--dry-run、--yes和--batch-size。--dry-run保证首次执行只出清单不动数据--yes是给自动化任务准备的手动操作时不建议加--batch-size控制单条 DROP 语句里包含的表数量后面第 4 章会细讲。各参数含义整理如下参数默认值说明--host127.0.0.1MySQL 地址生产环境建议填内网地址--port3306MySQL 端口--user必填账号需对目标库有 DROP 权限--database必填目标库名脚本只允许传一个库--prefix必填表前缀含下划线时脚本会自动转义--skip空精确跳过的表名逗号分隔--batch-size50每批删除表数量生产建议调低--dry-runFalse演练模式只生成 SQL 不执行--yesFalse跳过交互确认适合定时任务3.2 从 information_schema 取清单过滤视图、转义前缀清单查询是 v1.0 的核心直接查information_schema.tables并在 SQL 里固定TABLE_TYPE BASE TABLEdef list_tables(conn, database, prefix, skip_tables): skip_set set(skip_tables.split(,)) if skip_tables else set() sql SELECT TABLE_NAME, TABLE_TYPE, ENGINE FROM information_schema.tables WHERE TABLE_SCHEMA %s AND TABLE_TYPE BASE TABLE AND TABLE_NAME LIKE %s ORDER BY TABLE_NAME # 先转义反斜杠再转义 LIKE 通配符 _ 和 % escaped prefix.replace(\\, \\\\).replace(_, \\_).replace(%, \\%) pattern escaped % with conn.cursor() as cursor: cursor.execute(sql, (database, pattern)) rows cursor.fetchall() kept [r for r in rows if r[0] not in skip_set] logger.info(匹配 %s 张表跳过 %s 张保留 %s 张, len(rows), len(rows) - len(kept), len(kept)) return kept转义顺序有讲究先转义反斜杠本身再转义下划线和百分号否则前缀里原有的反斜杠会把后面的转义序列搞乱。LIKE模式最终变成tmp\_%在 MySQL 默认转义规则下这个模式严格匹配「以 tmp_ 开头」的表名。skip_set在这里做一次精确过滤比先查出全部表再在循环里判断更清晰日志里也能直接看到「匹配、跳过、保留」三个数字。3.3 分批 DROPDDL 不走事务失败后靠重查清单续跑拿到清单后进入执行层。每条 DROP 语句用逗号连接多张表一次提交一批。这里有一个 MySQL 的底层事实要记住DDL 语句隐式提交DROP TABLE一旦执行成功就立即生效rollback()对它没有任何意义。所以异常处理里不用回滚而是中断当前批次重查清单后继续def drop_in_batches(conn, tables, batch_size): total len(tables) for i in range(0, total, batch_size): batch tables[i:i batch_size] # 反引号包住表名防止表名含特殊字符时 SQL 语法错误 names , .join(%s % t[0] for t in batch) sql DROP TABLE IF EXISTS %s % names try: with conn.cursor() as cursor: cursor.execute(sql) logger.info(已删除 %s/%s 张表, min(i batch_size, total), total) except Exception as e: logger.error(本批删除失败: %s, e) breakIF EXISTS不是可有可无的保险脚本执行到一半断网时上一批可能已经删掉一部分表重跑时这些表不再存在于information_schema但DROP TABLE IF EXISTS会让语句安静地跳过它们。反引号同样重要表名里出现-或者中文时不加反引号直接拼接会被 MySQL 当成表达式。失败中断后最稳妥的重试方式不是原地重试而是重新调用list_tables拿最新清单已经删掉的表自然不在里面自动续跑。3.4 演练模式与交互确认不确认就不碰数据最后把主流程串起来。--dry-run模式下不执行任何 DROP而是把完整的删除计划写入drop_plan.sql这份文件既是演练证据也是审计留痕def write_drop_plan(tables, batch_size, path): with open(path, w) as f: for i in range(0, len(tables), batch_size): batch tables[i:i batch_size] names , .join(%s % t[0] for t in batch) f.write(DROP TABLE IF EXISTS %s;\n % names) logger.info(演练完成SQL 已写入 %s, path) def main(): args parse_args() conn pymysql.connect( hostargs.host, portargs.port, userargs.user, passwordargs.password, databaseargs.database, charsetutf8mb4 ) try: tables list_tables(conn, args.database, args.prefix, args.skip) if not tables: logger.info(没有匹配的表结束) return for t in tables[:10]: logger.info(候选: %s | %s | %s, t[0], t[1], t[2]) if len(tables) 10: logger.info(... 共 %s 张仅预览前 10 张, len(tables)) if args.dry_run: write_drop_plan(tables, args.batch_size, drop_plan.sql) return if not args.yes: answer input(确认删除以上 %s 张表? 输入 yes 继续: % len(tables)) if answer ! yes: logger.info(已取消) return drop_in_batches(conn, tables, args.batch_size) finally: conn.close() if __name__ __main__: main()实际使用分两步走。第一次先演练export MYSQL_PWD填入密码 python drop_tables_by_prefix.py --host 127.0.0.1 --port 3306 \ --user app_cleaner --database my_app --prefix tmp_ --dry-run确认drop_plan.sql里的每一行都符合预期后再执行真实删除python drop_tables_by_prefix.py --host 127.0.0.1 --port 3306 \ --user app_cleaner --database my_app --prefix tmp_ --yes这里不建议直接跳过演练。drop_plan.sql不只是一份 SQL 文件它是你删表前最后一次人工复核的机会也是对「到底删了哪些表」的硬证据。脚本本身再稳也替代不了人眼扫一遍清单。4. 批量删表的三个必调参数LIKE 转义、白名单上限、分批大小脚本能跑通只是开始真正决定删表过程安不安全的是三个参数的取值前缀怎么匹配、哪些表必须跳过、每批删多少。v1.0 把这三点都放在命令行参数层方便每个环境按自己的情况调整。这三个参数调不好前面所有代码都等于给事故现场递刀。4.1 前缀匹配的边界下划线是通配符不是表名字符前缀匹配是最容易翻车的一环。假设要清理的表前缀是log_2024_直接拼LIKE log_2024_%时MySQL 会把两个下划线当成「任意单字符」通配符匹配范围瞬间变成logX2024X...。原本只想清日志表结果把logA2024B_user这类命名规则沾边的表也捞了进来。v1.0 的处理方式是在查询前对前缀做转义把_替换成\_把%替换成\%。这样log_2024_就变成严格的前缀字面量只匹配真正以log_2024_开头的表。如果目标环境开启了NO_BACKSLASH_ESCAPESSQL 模式反斜杠转义会失效这时候可以改用更稳妥的等价写法SELECT TABLE_NAME, TABLE_TYPE, ENGINE FROM information_schema.tables WHERE TABLE_SCHEMA my_app AND TABLE_TYPE BASE TABLE AND LEFT(TABLE_NAME, LENGTH(log_2024_)) log_2024_ ORDER BY TABLE_NAME;LEFT()比较虽然不能走索引但information_schema查询本身开销很低几十万张表的元数据全扫一遍也就毫秒级安全优先于性能。调参时的核心原则是命令行传入原始前缀脚本负责转义使用者永远不要自己手动加反斜杠——手动加一次就会有人忘记第二次。4.2 跳过保护表白名单与数量兜底有些表虽然顶着临时前缀但不能删。常见的是定时任务的状态表、给报表用的汇总表、临时上线的补数表。v1.0 里用--skip精确跳过python drop_tables_by_prefix.py --database my_app \ --prefix tmp_ \ --skip tmp_keep_this,tmp_keep_that --dry-run--skip做的是全名精确匹配防止tmp_user_count这种表被误伤。光有白名单还不够还要加一道数量兜底。当前缀写得过宽、匹配到的表远超预期时应该让脚本直接拒绝执行。给 main 函数加一个上限判断parser.add_argument(--max-count, typeint, default0, help匹配表数超过该值直接拒绝执行0 表示不限制)在拿到清单后立刻校验if args.max_count and len(tables) args.max_count: logger.error(匹配表数 %s 超过上限 %s终止执行, len(tables), args.max_count) return这个参数的价值在于它把「人肉核对清单」的最后一道防线自动化了。某次我把前缀看错一位tmp_写成t_如果当时有--max-count 300兜底脚本会在执行前停下来而不是直接对五百多张表发起 DROP。4.3 分批大小与删除顺序稳定比快更重要DROP TABLE t1, t2, t3支持一条语句删多张表但单条语句太长会碰到max_allowed_packet限制。更重要的是MySQL 的 DDL 隐式提交一批语句里只要有一张表处于元数据锁等待整批都会卡住而且无法回滚。所以分批大小不是越大越好--batch-size默认 50生产环境我一般调到 30。删除顺序同样有讲究先删小表再删大表。小表意味着更少的磁盘页释放和更短的锁持有时间即使中途失败中断点之前的表已经删完重跑成本最低。如果想按表大小排序在list_tables里把DATA_LENGTH INDEX_LENGTH也查出来ORDER BY (DATA_LENGTH INDEX_LENGTH) ASC即可。对清理临时表来说按名字顺序删不会出错但按大小顺序删会让整个过程更平滑。5. 批量删表避坑5 个现场让我不敢再一把梭代码写得再小心生产环境也会给出意想不到的回应。这 5 个现场是我在清理临时表时踩过的按「现象→原因→解决」写下来希望对正在做同类清理的人有用。5.1 DROP 执行到一半卡住元数据锁等待与长事务现象脚本停在第 30 张表日志不再输出SHOW PROCESSLIST里看到目标线程的状态是Waiting for table metadata lock。原因某个定时任务或慢查询还持着目标表的元数据锁。DROP TABLE需要拿排他元数据锁拿不到就一直在等。这类锁通常不会自动超时表现就是脚本悬挂在那里看起来像死机。解决先查information_schema.innodb_trx定位长事务SELECT trx_id, trx_state, trx_started, trx_mysql_thread_id, trx_query FROM information_schema.innodb_trx ORDER BY trx_started;找到持有锁的会话后确认是空闲事务再KILL对应线程。之后重跑脚本即可。v1.0 每次执行都会重新查information_schema拿清单已删除的表不会再次进入列表续跑是天然安全的不需要额外做断点记录。5.2 视图混进删除清单没过滤 TABLE_TYPE 的后果现象清单里多出tmp_v_xxx这类视图轻则 DROP 报错打断整批重则把依赖该视图的上游查询全部搞挂。原因第一版图省事用了SHOW TABLES LIKE它不会区分表、视图、临时表凡是名字匹配前缀的全部返回。解决统一走information_schema.tables并且强制带上TABLE_TYPE BASE TABLE条件。v1.0 的清单查询里这个条件不是可选项是写死的。另外预览清单时把TABLE_TYPE一并打印出来人工复核时多看一眼基本能杜绝这类事故。5.3 有外键关联的表批量 DROP报错还是挂起要看引用方向现象批量 DROP 时遇到被其他表引用的父表报ER_ROW_IS_REFERENCED_2。表现可能是整批停在原地也可能是删到一半突然报错。原因目标表被其他表的外键引用MySQL 不允许直接删父表。临时把FOREIGN_KEY_CHECKS关掉再 DROP 是一个流传很广的写法但关掉外键检查删父表会让子表里的引用变成孤儿数据后续业务写入直接失败。解决删表前先查一遍外键依赖把引用关系打印出来SELECT TABLE_NAME, COLUMN_NAME, REFERENCED_TABLE_NAME, REFERENCED_COLUMN_NAME FROM information_schema.KEY_COLUMN_USAGE WHERE TABLE_SCHEMA my_app AND REFERENCED_TABLE_NAME IS NOT NULL AND REFERENCED_TABLE_NAME LIKE tmp\_%;拿到报告后先删子表再删父表。v1.0 的规范做法是把外键报告也落盘和drop_plan.sql放一起删完留档。5.4 删完才想起备份ROW 格式 binlog 也帮不上 DDL 的忙现象误删了一张还带数据的表想用 binlog 做 Flashback结果发现 binlog 里只有一条DROP TABLE记录这张表之前的所有数据变更都无从恢复。原因DROP TABLE是 DDLbinlog 里只记录一条 statementROW 格式的 Flashback 工具只对 DML 有效遇到 DDL 直接断链。解决删表前先把表结构落盘。mysqldump --no-data或者最简单的SHOW CREATE TABLE都可以至少能把表结构和字符集存下来数据层面v1.0 执行前的drop_plan.sql也是一种轻量留痕。真正的重要业务表不应该进批量删除清单它应该出现在--skip白名单里。5.5 权限不足的隐藏现场SELECT 能查清单DROP 却报错现象脚本在查information_schema时一切正常清单也打印出来了执行 DROP 时却报DROP command denied to user app_cleaner%。原因账号对目标库只有SELECT、CREATE权限没有DROP。information_schema对任何账号可读能查出清单不代表有权限删。解决执行前先跑SHOW GRANTS FOR app_cleaner%核对权限给清理任务单独建一个账号只授予目标库的DROP、SELECT权限不要图省事直接把 root 配置进脚本。权限收紧虽然多一道审批但能避免清理账号被用来删掉整个生产库。6. 让 v1.0 不翻车的三个收尾动作清单快照、干跑 DDL、删后核对脚本能跑、参数调完最后还有三个动作能显著降低生产事故概率。它们几乎不花时间但能在误删发生后给你一张后悔药。6.1 清单快照删前落盘、删后 diff执行之前先让 v1.0 以--dry-run输出一份before.txt删除完成后再跑一次--dry-run输出after.txt。两份文件 diff 一下对比结果一目了然python drop_tables_by_prefix.py --database my_app \ --prefix tmp_ --dry-run before.txt python drop_tables_by_prefix.py --database my_app \ --prefix tmp_ --yes python drop_tables_by_prefix.py --database my_app \ --prefix tmp_ --dry-run after.txt diff before.txt after.txtbefore.txt就是审计证据它记录了这次清理到底删了哪些表after.txt如果还有内容说明删除不完整需要人工介入。diff 一行都看不到才算是干净收尾。6.2 干跑 DDL在临时库验证 drop_plan.sql把drop_plan.sql拿到临时库执行一遍能提前发现两类问题表名里包含特殊字符导致反引号拼接不完整以及同一条批量 DROP 语句里混入了被外键引用的表。临时库建议用相同大版本的 MySQL避免语法差异造成的误判。干跑通过不代表生产环境一定没问题但它能过滤掉大部分低级错误把生产执行的风险收敛到元数据锁和权限这两个维度。6.3 删后核对看余量也看释放空间删完最后再做一次核对确认前缀匹配数量归零SELECT COUNT(*) AS remaining FROM information_schema.tables WHERE TABLE_SCHEMA my_app AND TABLE_TYPE BASE TABLE AND TABLE_NAME LIKE tmp\_%;如果还想记录这次清理的成果删除前先统计一次这些表占用的空间删完再查一次做对比SELECT IFNULL(SUM(DATA_LENGTH INDEX_LENGTH), 0) AS reclaim_bytes FROM information_schema.tables WHERE TABLE_SCHEMA my_app AND TABLE_NAME LIKE tmp\_%;我现在删任何一组表都先把 dry-run 的drop_plan.sql留一份再执行删完 diff 一次。之前有次跳过快照直接--yes删完之后才发现有一张tmp_开头的表是定时任务的依赖表恢复结构花了整个下午。从那天起「先落盘、再执行、后核对」成了固定动作。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑