首页 / 资讯中心 / 文章详情

MySQL临时表为什么可以重名?从会话隔离到内部命名机制

MySQL临时表为什么可以重名?从会话隔离到内部命名机制 ★ FEATURED ARTICLE
先纠正一个容易误解的印象很多人在刚接触 MySQL 临时表时会觉得“同一个库、同一套数据文件怎么可能允许两张同名表同时存在” 我第一次被人问到这个问题时也愣了一下。正因为所有普通表都在共享的数据字典里第二个CREATE TABLE t必然会收到 1050 错误可换成CREATE TEMPORARY TABLE t两个不同会话各建各的居然完全不冲突。这一讲的价值就是把这件事背后的“会话隔离”机制讲透了——临时表为什么可以重名以及这个设计和复制、备份、执行计划之间是什么关系。这篇内容适合所有正在系统学习 MySQL 内部机制的开发者尤其适合被“临时表重名”现象困惑过、或者用临时表做过数据分批处理但说不清原理的人。你不需要先读完整本书只需要有一个能跑 MySQL 的环境跟着文中的实验走一遍就能把临时表的生命周期、内部命名、主从复制行为和性能参数串成一条完整的知识线。1. 重名不是巧合是“会话私有表”在物理上的必然设计1.1 两个连接各建一张同名临时表会发生什么先做一个最直观的验证。开两个 MySQL 终端会话 A 和 B分别执行下面这条语句-- 会话 A CREATE TEMPORARY TABLE t_tmp (id INT PRIMARY KEY, name VARCHAR(20)) ENGINEInnoDB; INSERT INTO t_tmp VALUES (1, from A); -- 会话 B注意这里用的是同一个库、同一张表名 CREATE TEMPORARY TABLE t_tmp (id INT PRIMARY KEY, name VARCHAR(20)) ENGINEInnoDB; INSERT INTO t_tmp VALUES (2, from B);两条CREATE TEMPORARY TABLE都会成功不会出现“Table t_tmp already exists”的报错。接着在 A 里查SELECT * FROM t_tmp只看到(1, from A)在 B 里查只看到(2, from B)。再开第三个会话 C直接查SELECT * FROM t_tmp会收到 1146 错误Table xxx.t_tmp doesnt exist。从这里能得出三个明确结论临时表是会话级别的只有创建它的会话能看到。不同会话之间的同名临时表彼此完全无感。临时表不会出现在“公共的数据库目录”里否则 C 会话不可能查不到。1.2 真正的表名早就被换掉了内部名与线程 ID关键问题是MySQL 底层怎么做到“同名不冲突”答案不是靠什么全局锁而是靠“名字映射”。你在 SQL 里写的t_tmp只是一个逻辑别名。当 MySQL Server 层决定创建一张临时表时传给 InnoDB 存储引擎的实际内部名并不是t_tmp而是一个自动生成的、带前缀和线程标识的名字常见形式类似#sqlxxxx_N。其中#sql前缀用于标识“这是一张临时表”xxxx是一段随机或内部生成的标识结尾的N通常和创建它的连接线程 ID 相关。这样做的效果是即便 A 和 B 都执行了同名t_tmp落到 InnoDB 层时一张可能被映射成#sql_aa11_31另一张被映射成#sql_bb22_47。存储引擎看到的是两个完全不同的对象自然不会发生命名冲突。Server 层则通过“线程 ID 内部表标识”维护了一张会话级的临时表目录这个目录只有当前会话自己能访问。如果你用的是 8.0可以在创建临时表之后查一下information_schema.INNODB_TEMP_TABLE_INFOSELECT TABLE_ID, NAME, N_COLS, SPACE FROM information_schema.INNODB_TEMP_TABLE_INFO;输出里的NAME一列不会是你写的t_tmp而是#sqlxxxx_xx之类的内部名。这个现象就是“为什么临时表可以重名”最底层的答案重名的只是你敲进去的逻辑名物理对象早就被隔离了。1.3 临时表优先同会话里的名字遮蔽规则“会话隔离”解决了不同会话之间的冲突那同一个会话里临时表和普通表同名又会怎样这里有一条很实用的规则临时表优先。假如库里本来就有一张普通表test.t_user然后你在同一个会话里执行CREATE TEMPORARY TABLE t_user (id INT) ENGINEInnoDB;随后这个会话里所有对t_user的读写走到的都是临时表而不是原来那张普通表。原来的普通表被“遮住”了直到你把这个临时表DROP掉之后对t_user的访问才重新落回普通表。这个行为也解释了为什么官方文档强调要用显式的DROP TEMPORARY TABLE来删除临时表DROP TEMPORARY TABLE IF EXISTS t_user;在临时表和普通表同名的情况下直接写DROP TABLE t_user是有歧义的。虽然 MySQL 内部能够区分但作为使用方最好别让自己的表名状态变成“同名两表并存”这种需要靠规则才能说清的情况。尤其是SHOW CREATE TABLE在这种状态下显示的是临时表的定义SHOW TABLES又不会列出临时表维护的人很容易被搞糊涂。提示SHOW TABLES不显示临时表这是设计如此不是 bug。想确认当前会话有哪些临时表可以查看INNODB_TEMP_TABLE_INFO或者直接看SHOW CREATE TABLE的输出。2. 临时表和普通表的三处关键差异理解之后才不会用错2.1 生命周期差异随会话结束自动清理重启必清普通表是持久化对象建一次只要不删它就一直躺在数据目录里。临时表则完全相反它的生命周期绑定在会话上会话正常QUIT或连接断开MySQL 会自动清理当前会话创建的所有临时表。会话执行DROP TEMPORARY TABLE立即删除。MySQL 实例重启所有临时表消失因为临时表不参与正常的数据字典持久化。所以临时表非常适合做“即用即走”的中间数据容器。我常见的一个用法是先把需要处理的大批 ID 捞进临时表再分批JOIN回业务表更新避免一个事务里扫全表。整个过程里临时表只活在当前会话中不需要担心残留数据污染下一次执行。但这也意味着一个反面教训不要把临时表当成跨会话的数据传递工具。如果你在一端创建临时表然后在另一个连接里想读取它必然失败。跨会话共享中间结果应该用普通表、缓存中间件而不是临时表。2.2 元数据不进入共享字典SHOW TABLES 查不到普通表的元数据会进入数据字典所以你在任何会话里SHOW TABLES、SELECT * FROM information_schema.TABLES都能看到它。临时表则不同它的元数据只挂在当前会话内部不进共享字典因此在别的会话里看不到SHOW TABLES也查不到。这个设计带来一个很实际的便利临时表名可以在不影响任何现有对象名的情况下随意复用。比如一个存储过程内部创建tmp_result临时表多个会话同时调用这个存储过程每个会话各建各的tmp_result互不干扰。哪怕原来的库里已经有了一张叫tmp_result的普通表也只是在当前会话中暂时被遮蔽不会发生物理覆盖。如果你要排查“磁盘上怎么多了很多 #sql 文件”或者怀疑会话里还有未释放的临时表可以通过INNODB_TEMP_TABLE_INFO查SELECT * FROM information_schema.INNODB_TEMP_TABLE_INFO;这个视图能列出当前实例里所有 InnoDB 临时表的信息包括内部名和对应的表空间 ID比到处翻数据目录里的.ibd文件高效得多。2.3 存储位置独立临时表空间与普通表空间分开临时表不仅逻辑上隔离物理存储上也和普通表拉开了距离。8.0 里 InnoDB 会使用专门的临时表空间默认位置在数据目录下的#innodb_temp目录里5.7 里则常见于共享临时表空间文件如ibtmp1。这么做有两个好处临时表的数据不会和普通表的数据文件混在一起备份、恢复普通表时可以忽略临时表。实例重启时临时表空间可以被重建天然满足“临时表生命周期不超过会话”的设定。如果你在 5.7 上遇到过ibtmp1文件涨到几十 GB 的情况原因多半是某个大查询触发了大量磁盘临时表而共享临时表空间自动扩展后不会自动收缩直到实例重启才会重建。8.0 里临时表空间管理更灵活但长会话里一直持有大临时表同样会占用空间。排查思路是一样的先用INNODB_TEMP_TABLE_INFO找到内部表名和空间号再去SHOW PROCESSLIST里看对应会话正在执行什么 SQL。3. 主从复制里为什么不会乱套binlog 如何描述临时表3.1 主库上的临时表操作如何进入 binlog很多人担心一个问题临时表既然是会话私有的那主从复制怎么处理它答案比想象中简单主库在执行CREATE TEMPORARY TABLE时会把这个 DDL 写进 binlog在会话结束或显式DROP时也会把DROP TEMPORARY TABLE必要时带IF EXISTS写进 binlog。比如主库上执行CREATE TEMPORARY TABLE t_tmp (id INT PRIMARY KEY) ENGINEInnoDB; INSERT INTO t_tmp VALUES (1); DROP TEMPORARY TABLE IF EXISTS t_tmp;在 statement 或 mixed 格式下binlog 里能看到对应的CREATE TEMPORARY TABLE和DROP TEMPORARY TABLE语句。从库回放这些语句时复制线程同样是以一个独立会话的身份执行临时表只存在于从库的复制线程上下文里。于是“临时表可以重名”的隔离规则在主从两个方向上同时成立主库的多个会话互不干扰从库的复制线程也不会和从库上的其他业务会话产生表名冲突。还有一个很关键但容易忽略的细节如果主库会话异常断开临时表还没来得及显式删除MySQL 会在 binlog 里自动补记一条 DROP TEMPORARY TABLE从库才能跟着清理。这个自动补删机制保证了从库不会留下主库已经消失的临时表残留。3.2 从库回放时的会话隔离以及 row 格式的差异从库回放CREATE TEMPORARY TABLE后这张临时表同样只对复制线程可见。假设从库数据库里有一张同名普通表复制线程创建的同名临时表会遮蔽它但不会覆盖它等到DROP TEMPORARY TABLE回放完毕普通表又恢复可见。正因为 MySQL 在最底层的内部命名上做了线程隔离这个过程无论主库并发建多少张同名临时表都不会在从库炸出重复定义错误。如果你用的是 row 格式的 binlog临时表上的INSERT/UPDATE/DELETE通常不会产生行事件。原因很直接临时表的改变是会话级的不会持久化复制到从库没有实际意义。真正需要复制的是临时表的创建和删除以及“从临时表读出数据并写入普通表”时对普通表产生的行变更。这个边界理清楚之后主从延迟排查时就不会把临时表的 DML 当成可疑对象。3.3 使用临时表时的复制注意点虽然临时表在主从复制下是安全的实际使用中还是有几个注意点值得记住在存储过程或触发器里使用临时表时要避免依赖“当前会话看到的临时表数据”产生不确定的语句顺序否则 statement 格式下可能出现回放结果不一致。临时表和普通表同名时binlog 里的语句顺序、DROP行为会更复杂建议业务上避免这种同名设计降低排查成本。不要因为临时表是会话私有的就在一个长连接里大量创建后不删除。连接不释放临时表就一直在尤其是连接池里的复用连接问题会更明显。4. 执行计划里的“隐式临时表”可重名机制同样在帮优化器兜底4.1 哪些 SQL 会触发内部临时表除了用户主动用CREATE TEMPORARY TABLE创建的临时表MySQL 优化器在执行某些 SQL 时也会在内部生成临时表这类表常被称为“内部临时表”或“隐式临时表”。EXPLAIN输出里如果Extra字段出现了Using temporary就说明这条 SQL 走了内部临时表。常见触发场景包括-- 去重合并UNION 会先建临时表再过滤重复行 SELECT id FROM t1 UNION SELECT id FROM t2; -- GROUP BY 无法走索引且需要临时表辅助分组 SELECT category, COUNT(*) FROM t1 GROUP BY category; -- DISTINCT 无法走索引消重 SELECT DISTINCT name FROM t1; -- NOT IN / NOT EXISTS 转成 anti join 时可能用临时表辅助判断 SELECT * FROM t1 WHERE id NOT IN (SELECT id FROM t2); -- 窗口函数排序计算通常需要临时表暂存中间结果 SELECT id, ROW_NUMBER() OVER (ORDER BY id) FROM t1;这些由优化器生成的内部临时表同样使用#sql开头的内部名字同样是会话级隔离。所以一个数据库实例里几十个连接同时执行相同的UNION大查询每个连接的内部临时表都能独立存在不会互相撞名。这是“临时表可以重名”机制最广泛的体现它不仅服务于用户手动建的表还在整个执行引擎内部默默兜底。4.2 如何确认临时表是内存行还是磁盘行内部临时表有两种形态内存临时表和磁盘临时表。内存临时表又有两种引擎实现5.7 及之前常见的是MEMORY引擎数据会整块放在内存中。8.0 默认使用TempTable引擎处理变长字段时比MEMORY更省内存例如 VARCHAR 按实际长度分配而不是按最大长度固定分配。如果内存临时表撑不住了MySQL 会自动把数据转存到磁盘转存之后 SQL 的执行性能会明显下降。检查磁盘临时表是否出现过最简单的方式是看状态变量SHOW GLOBAL STATUS LIKE Created_tmp%;两个主要指标是Created_tmp_disk_tables和Created_tmp_tables。如果前者占比持续偏高说明你的实例经常因为内存临时表容量不足而落盘。配合SHOW PROCESSLIST看正在跑的 SQL通常能快速定位是哪类GROUP BY、ORDER BY或UNION引发的。4.3 阈值参数与磁盘暴涨的排查控制内存临时表容量的参数主要是下面这几个不同 MySQL 版本的默认值略有差异具体要以SHOW VARIABLES的输出为准参数常见默认值作用tmp_table_size16MB用户创建的内存临时表与部分内部内存临时表的上限之一max_heap_table_size16MB约束 MEMORY 引擎用户临时表的最大大小temptable_max_ram1GB8.0 TempTable 引擎内存临时表的上限internal_tmp_mem_storage_engineTempTable8.0选择内存临时表引擎internal_tmp_disk_storage_engineInnoDB磁盘临时表引擎8.0 固定为 InnoDB对 MEMORY 引擎来说内存临时表实际能涨到多大取决于MIN(tmp_table_size, max_heap_table_size)。一旦大小超过这个上限优化器就会把临时表转到磁盘。另外MEMORY 引擎不支持TEXT、BLOB等类型只要 SQL 里的临时表需要容纳这些列就会直接使用磁盘临时表根本没机会留在内存里。磁盘临时表暴涨是生产环境里最容易遇到的一类问题。5.7 下如果ibtmp1越来越大先确认是不是有大查询正在构造超大临时表。可以考虑SHOW PROCESSLIST;看到State长时间停在“converting to heap”或“copy to tmp table”基本就是临时表在批量拷贝数据。再配合执行计划确认 SQL 是否必须用临时表尝试加索引、改写 SQL、拆分查询降低临时表数据量。注意tmp_table_size和max_heap_table_size调整只影响后续新建的内存临时表已经存在的临时表不会因为参数调大而自动扩容。这也是很多人改了参数却没看到效果的原因。5. 自己动手验证一组 5 分钟的复现实验5.1 实验一验证两个连接的同名临时表互不可见打开三个终端按下面的顺序操作终端 AUSE test; CREATE TEMPORARY TABLE t_tmp (id INT PRIMARY KEY); INSERT INTO t_tmp VALUES (1); SELECT * FROM t_tmp; -- 看到 1终端 BUSE test; CREATE TEMPORARY TABLE t_tmp (id INT PRIMARY KEY); INSERT INTO t_tmp VALUES (2); SELECT * FROM t_tmp; -- 看到 2终端 CUSE test; SELECT * FROM t_tmp; -- 报错Table test.t_tmp doesnt exist这个实验 5 分钟做完但信息量很大它同时验证了临时表的会话隔离、重名合法性和不可跨会话访问三个特性。做的时候建议再顺手查一下INNODB_TEMP_TABLE_INFO你会看到两个名称不同的内部表这样“物理名不同”的印象会更深刻。5.2 实验二验证临时表遮蔽普通表先建一张普通表USE test; CREATE TABLE t_tmp (id INT PRIMARY KEY); INSERT INTO t_tmp VALUES (100);然后在同一个会话里创建同名临时表USE test; CREATE TEMPORARY TABLE t_tmp (id INT PRIMARY KEY); INSERT INTO t_tmp VALUES (1); SELECT * FROM t_tmp; -- 看到 1而不是 100此时SELECT读到的是临时表的数据执行DROP TEMPORARY TABLE t_tmp之后再查SELECT * FROM t_tmp看到的又是 100。这个实验能帮你建立“保护性和遮蔽性”的直觉临时表在会话里把同名普通表暂时藏起来但不会真的动它。5.3 连接池与存储过程最容易出现的临时表残留我在实际项目里踩过最典型的一个坑是连接池里的临时表残留。某业务方法里执行过CREATE TEMPORARY TABLE tmp_batch (id INT PRIMARY KEY);代码结束时忘了显式删除临时表。框架把连接归还给连接池后连接并没有真正关闭临时表还挂在这个物理连接上。下一次业务请求复用了同一个连接再执行同样逻辑时CREATE TEMPORARY TABLE tmp_batch直接报“表已存在”错误。排查了很久才发现是上一个请求留下的临时表没清理。解决方式非常朴素用完立刻删最好放在finally或等价清理代码里DROP TEMPORARY TABLE IF EXISTS tmp_batch;存储过程里也有类似问题。虽然存储过程执行完会话如果还活着临时表可能继续存在。所以存储过程内创建临时表的规范做法是用完后显式DROP TEMPORARY TABLE IF EXISTS或者依赖每次连接新建的短生命周期会话。尽量不要把临时表的生命周期和“业务方法的结束”划等号MySQL 只知道连接会话不知道业务方法在哪里结束。5.4 用临时表做数据分批处理的实用套路既然临时表会话私有、自动命名隔离完全可以直接拿来承载“中间结果集”。我处理大量数据更新时常用这个套路-- 第一步把要处理的目标 id 集合放进度临时表 CREATE TEMPORARY TABLE tmp_ids (id INT PRIMARY KEY) ENGINEInnoDB; INSERT INTO tmp_ids SELECT id FROM big_table WHERE update_time 2024-01-01 AND status pending; -- 第二步分批取 id小事务更新 SET batch_size 5000; REPEAT UPDATE big_table AS b JOIN ( SELECT id FROM tmp_ids ORDER BY id LIMIT batch_size ) AS t ON b.id t.id SET b.status done; -- 删除本次已处理的 id DELETE t FROM tmp_ids AS t JOIN ( SELECT id FROM tmp_ids ORDER BY id LIMIT batch_size ) AS d ON t.id d.id; UNTIL ROW_COUNT() 0 END REPEAT;这里的核心思想就是把大任务拆成多个小事务每个事务只碰少量行减少锁竞争和 binlog 体积。临时表的优越性在于它是会话私有的多个实例、多个连接并行跑同一套脚本时不会因为tmp_ids这个名字产生任何冲突。6. 最后一点个人体会把“为什么临时表可以重名”彻底搞懂之后再看 MySQL 的临时表感觉会完全不一样。很多人遇到重名问题时的第一反应是加全局唯一名但真正机制是每个会话都维护了一套独立命名空间物理对象再通过#sql前缀和线程 ID 做映射。理解了这一层再去解释“连接池里临时表残留”“两个会话执行同一个存储过程为什么互不影响”“主从复制里临时表 DDL 为什么安全”就都是一条线下来的事了。我个人现在的实践习惯是凡是只在一个会话里用的中间数据一律用临时表凡是需要跨会话共享的中间结果优先考虑普通表或者独立的中间存储绝不用临时表硬撑。每次创建完临时表也会下意识在收尾处补一句DROP TEMPORARY TABLE IF EXISTS。这两个小习惯看着不起眼但在高并发连接池场景里真的能帮你省掉不少凌晨排查问题的时间。
阅读完成 · 觉得有帮助?
咨询建站