临时表这东西干SQL的都绕不过去。不管你是写存储过程的还是做报表刷数的或是搞ETL的几乎每个项目里都会碰到先建个临时表把中间结果存一下再接着算的操作。但多数人其实只是照着别人的代码抄了个CREATE TABLE #temp或者CREATE TEMPORARY TABLE真问起来临时表在MySQL、SQL Server、Oracle、PostgreSQL里到底有什么区别生命周期是怎么回事连接池里会不会串数据能答清楚的不多。这篇就把SQL创建临时表这件事彻底捋一遍从各数据库的建表语法到使用时的坑全说透适合写SQL的老手也适合刚接触临时表想搞懂原理的初学者。1. 临时表到底解决什么问题1.1 会话隔离数据用完即走的核心价值临时表一开始设计出来是为了解决一个很朴素的需求中间结果集放哪。你写一条复杂查询分了好几步每一步都要依赖上一步的结果总不能每一步都重新扫描一遍大表也不能把中间数据永久落库占空间。这时候你需要的是一张用完即走的表——数据写完就能读读完了会话一关或者事务一结束自动清理不污染正式表。但临时表最容易被忽略的价值其实是会话隔离。默认情况下SQL Server的#临时表、MySQL的TEMPORARY TABLE都只对创建它的那个会话可见。什么意思就是你开了两个查询窗口连同一个数据库窗口A创建的临时表窗口B是看不到的。这个特性在并发场景里非常关键——多个业务线同时跑批处理各算各的互不干扰。我见过有些团队拿普通永久表存中间结果字段名加个日期后缀跑批结束再手动删稍微哪一步忘了删就留下一堆垃圾表整个库跟杂物间一样。用临时表就没这个问题会话结束了数据自动消失。还有一层价值是权限控制。临时表只在你的会话或事务内部有效不会影响其他人的查询也不会被误当成正式业务表被别的同事引用。这对多人共用一个数据库环境、需要频繁做临时分析的情况尤其友好。1.2 临时表和CTE、派生表、表变量怎么选很多初学者会把临时表和CTEWITH子句、派生表、表变量混为一谈觉得它们都是中间结果集。其实它们的使用边界差异很大选错了就直接影响SQL性能和写法复杂度。派生表和CTE本质上是内联视图只在单条SQL语句内有效。你写一条大SQL中间想分步计算可以用CTE串联但这条SQL一结束中间结果就没了后面的语句想再引用就没办法。它们适合中间结果只被当前这条SQL引用的场景。表变量SQL Server特有DECLARE table TABLE (...)本质上是内存中的变量适合数据量很小几千行以内、不需要索引维护、也不想跟事务有太多勾稽关系的场景。但表变量没有统计信息数据量大一点优化器就抓瞎。临时表适合中间结果集被多条后续SQL重复使用、数据量较大、需要加索引来加速后续关联的场景。因为它会真实落盘或落到tempdb可以建索引可以更新数据生命周期可控是复杂批处理场景里最稳的选择。一句话总结单条SQL里的分步计算用CTE小数据量并且语句内临时的用表变量大结果集多次复用的用临时表。这不是偏好问题是性能问题。后面我会专门讲为什么数据量一大CTE和表变量会把你坑得很惨。2. 主流数据库的临时表创建语法盘点2.1 SQL Server井号开头的临时表和表变量SQL Server的临时表创建方式核心就两种语法CREATE TABLE和SELECT INTO。先说第一种最正统的-- 会话级临时表只对当前会话可见 CREATE TABLE #temp_user ( user_id INT, user_name NVARCHAR(100), created_at DATETIME );注意这个#前缀单井号是会话级临时表双井号##是全局临时表。全局临时表对所有会话可见会一直存在到创建它的会话结束且所有引用它的会话都关闭后才被清理所以使用时要格外小心别一建全局临时表就忘删。还有一种高频写法是SELECT INTO一条语句同时完成建表灌数非常实用SELECT user_id, user_name, created_at INTO #temp_user FROM dbo.users WHERE created_at 2024-01-01;这玩意的优势是代码少、执行快不需要先规划字段类型直接从查询结果集推导结构。缺点也很明显它不会自动从源字段带索引、约束、默认值如果后续要用大表关联或者做分组聚合你得手动补索引。稍微进阶一点SQL Server里还可能遇到动态SQL里建临时表的问题。EXEC或sp_executesql里建的临时表和外面不是同一个会话上下文会有临时表不存在的报错这个我放到后面的踩坑部分细说。再补充一个容易混淆的点表变量。它不需要CREATE TABLE用DECLARE声明就行但它不是临时表行为差异很大前面我已经提过它没统计信息的问题后面还会提到事务回滚时的差异。2.2 MySQLCREATE TEMPORARY TABLE的两种姿势MySQL的临时表用关键字TEMPORARY语法和SQL Server差异明显。最常见的写法是CREATE TEMPORARY TABLE temp_orders ( order_id BIGINT PRIMARY KEY, user_id BIGINT, amount DECIMAL(10, 2), created_at DATETIME ) ENGINEInnoDB;新版MySQL 8.0还支持直接指定ENGINETempTable这是MySQL 8.0引入的内存临时表引擎配合internal_tmp_mem_storage_engine参数控制比老版本的MEMORY引擎更好用。另一种高频写法是CREATE TABLE ... AS SELECT简称CTAS。MySQL不像SQL Server那样有SELECT INTO这个能力是靠CTAS覆盖的CREATE TEMPORARY TABLE temp_user_stats AS SELECT user_id, COUNT(*) AS order_cnt, SUM(amount) AS total_amount FROM orders GROUP BY user_id;CTAS建出来的临时表默认不继承索引也不带主键需要的话得在CTAS之后手动加索引。另外要注意的是MySQL的临时表对当前会话可见其他连接看不到如果临时表和已存在的永久表同名那么在当前会话里访问到的永远是临时表DROP掉临时表之后才恢复访问永久表这个坑很多人实际碰到过。MySQL的临时表在会话结束时会自动删除但遇到连接复用连接池场景时建议还是显式DROP TEMPORARY TABLE别依赖自动清理。2.3 Oracle全局临时表的两段式提交选项Oracle的临时表设计和MySQL、SQL Server都不太一样它建的是全局临时表Global Temporary Table, GTT表结构是所有会话共享的但每个会话只能看到自己写入的数据。定义语法CREATE GLOBAL TEMPORARY TABLE temp_orders ( order_id NUMBER, user_id NUMBER, amount NUMBER(10, 2), created_at DATE ) ON COMMIT DELETE ROWS;关键就在这个ON COMMIT选项上它决定了数据的生命周期ON COMMIT DELETE ROWS事务提交或回滚后数据就清空。适用于事务中间过程需要暂存数据的场景跑批事务提交后临时数据落下一次自动清掉。ON COMMIT PRESERVE ROWS数据保留到会话结束跨多个事务持续可用。适用于会话内先塞数据、后续多次利用的场景。我见过不少从MySQL转Oracle的同事第一次看到这个语法会懵觉得为什么还要专门写ON COMMIT。实际上这正是Oracle的临时表设计哲学表结构预先定义好永久存在只是数据是会话私有的。所以这类临时表不会像SQL Server那样在每次创建时都去分配存储空间。Oracle还有一种临时表写法是AS SELECT可以在建表的同时灌入数据CREATE GLOBAL TEMPORARY TABLE temp_order_summary ON COMMIT PRESERVE ROWS AS SELECT order_id, SUM(amount) AS total_amount FROM orders GROUP BY order_id;注意Oracle的GTT需要用户有CREATE GLOBAL TEMPORARY TABLE权限如果用ON COMMIT PRESERVE ROWS但不小心在事务中途用了DDL可能触发隐式提交数据被清掉这个细节后面细说。2.4 PostgreSQL和其他数据库的临时表写法PostgreSQL的临时表跟MySQL比较接近用的是CREATE TEMP TABLE或者CREATE TEMPORARY TABLECREATE TEMP TABLE temp_order_stats ( order_id BIGINT, total_amount NUMERIC(10, 2) ) ON COMMIT DROP;ON COMMIT有几种选项PRESERVE ROWS默认行为事务提交后数据还在会话结束才清。DELETE ROWS事务提交时清空数据类似Oracle的ON COMMIT DELETE ROWS。DROP事务提交时连表结构都删除。适合纯粹在事务内用的临时表提交后彻底消失不用手动清理。PostgreSQL的临时表可以自动清理但要注意它在事务里如果触发回滚临时表数据也会回到事务开始时的状态这一点全数据库通用。至于DB2临时表的关键字是DECLARE GLOBAL TEMPORARY TABLE写法相对特殊DECLARE GLOBAL TEMPORARY TABLE temp_orders ( order_id INTEGER, user_id INTEGER, amount DECIMAL(10, 2) ) ON COMMIT PRESERVE ROWS;DB2的DGTT需要提前创建会话级临时表空间否则会报错这个和别的库是最大区别。另外DB2还有CREATE GLOBAL TEMPORARY TABLE的语法用于定义所有会话共享表结构、数据各自隔离的临时表和Oracle的GTT类似。我把各库用一张表总结一下数据库核心建表语法生命周期关键点跨会话可见性SQL ServerCREATE TABLE #t/SELECT INTO #t会话结束自动清理##t为全局临时表仅本会话#或全部##MySQLCREATE TEMPORARY TABLE/CREATE TEMPORARY TABLE ... AS SELECT会话结束自动清理仅本会话OracleCREATE GLOBAL TEMPORARY TABLE ... ON COMMIT PRESERVE/DELETE ROWS表结构常驻按提交选项清数据表结构共享数据隔离PostgreSQLCREATE TEMP TABLE ... ON COMMIT PRESERVE/DELETE/DROP按提交选项清理数据或结构仅本会话DB2DECLARE GLOBAL TEMPORARY TABLE会话结束自动清理数据隔离需临时表空间3. 临时表创建的关键细节和正确姿势3.1 生命周期控制提交、回滚与连接池临时表看起来很简单但它跟事务、连接池这些基础机制的纠缠比大部分人想象的要深。先说事务回滚。在SQL Server、MySQL、Oracle里对临时表的DML操作INSERT、UPDATE、DELETE是参与当前事务的如果你的语句块最后回滚了临时表里这些数据修改也会一并回滚。很多人写存储过程时前面往临时表塞了一堆数据后面逻辑出错事务回滚临时表里的数据会被清空然后在存储过程的错误捕获里还想再读临时表做排查结果发现数据没了一脸懵。这个行为是数据库一致性要求决定的理解它才能写好事务里的临时表逻辑。再说连接池。这是生产环境最容易踩的雷。连接池是什么概念你的应用请求数据库时不会每次都新建连接而是从池子里借一个之前用过的连接。如果你在代码里建的临时表没有显式删除归还连接后下一个请求复用同一个连接很可能发现刚才的临时表还在。跨了业务的数据就串了。解决方式是三层第一所有临时表用完必须显式DROP第二有条件的话用事务内临时表让数据库在事务结束时自动清理第三从框架层面禁用连接池对临时表的复用风险比如在归还连接时执行清理脚本。SQL Server里还有个细节动态SQL中建临时表的会话上下文问题。你写EXEC(SELECT * INTO #tmp FROM dbo.users); SELECT * FROM #tmp;这样外面是查不到#tmp的因为EXEC里的动态SQL默认在它自己的会话上下文执行动态SQL执行完后里面的临时表对于外层来说已经不存在了。解决办法有两种一种是在外层先建好临时表动态SQL里只INSERT另一种是使用sp_executesql在同一个会话上下文执行但它仍然不会让外层动态创建的表变为外层可见这点不同版本行为还不一样最稳妥的还是外层建表、动态SQL填数。3.2 索引和统计信息别让临时表成为慢查询源头临时表建完就往里灌数据这是常规操作但坑就在灌完不管。以SQL Server为例SELECT INTO建出来的临时表没有索引如果你后续要做大表关联或重复查询全表扫描会让你怀疑人生。正确姿势是灌数据 → 建索引 → 再做关联查询。-- 灌入数据 SELECT order_id, user_id, amount, created_at INTO #orders_tmp FROM dbo.orders WHERE created_at DATEADD(day, -30, GETDATE()); -- 灌完再建索引比先建索引再灌数据快很多 CREATE INDEX ix_tmp_user ON #orders_tmp(user_id); CREATE INDEX ix_tmp_created ON #orders_tmp(created_at); -- 后续关联查询走索引 SELECT u.user_name, SUM(t.amount) AS total_amount FROM #orders_tmp t INNER JOIN dbo.users u ON t.user_id u.user_id GROUP BY u.user_name;为什么要先灌数据再建索引因为边灌边维护索引会产生大量随机写和页分裂性能差尤其是几百万行的数据差距可能是几倍甚至十几倍。一定是数据就绪后再一次性建索引让数据库批量构建索引页效率高得多。统计信息是另一个容易被忽视的点。临时表数据量很大时优化器需要统计信息估算行数和分布如果统计信息缺失或过期执行计划可能是灾难性的。SQL Server里可以在临时表上显式更新统计信息或者建完索引后用UPDATE STATISTICS #temp WITH FULLSCANMySQL的临时表一般不太需要手动管统计信息但如果你发现查询计划里临时表的扫描行数和实际严重不符可以在临时表上先跑一次分析再继续后面的逻辑。再补一个常识不要把大字段TEXT、VARCHAR(MAX)塞满临时表也不要在临时表里为了省事SELECT *只保留后续真正用到的列。临时表数据量一大tempdb或者临时表空间会被撑爆这个锅最后还得运维来背。3.3 命名规范和安全约束临时表命名看着是小事但实际生产里因为命名问题出的BUG不少。SQL Server的临时表名最长限制实际上比你想象的小——#前缀加上之后的名称总长不能超过116个字符超过会截断。MySQL的临时表名跟普通表一样受table_name长度限制。Oracle的GTT命名规则跟普通表一样而且表名在所有会话中都不能重复——这又是一个理解差异全局临时表的表结构是全局唯一的你不能像SQL Server那样每次建一个同名会话临时表。我自己的习惯是这样的会话级临时表统一#tmp_开头后面接业务标识和日期比如#tmp_order_pay_daily全局临时表用##tmp_开头加注释标明为何必须全局MySQL的临时表用tmp_开头不加重叠。命名规范的意义在于临时表不是垃圾场它也是代码的一部分别人看了你的临时表名字就知道这是什么数据、哪个批次的。安全约束方面主要提一句SQL注入。任何跟临时表相关的动态SQL如果拼接了外部输入比如把用户传的查询条件拼进动态SQL去构造临时表都有被注入的风险。这不是临时表独有的问题但临时表经常出现在动态SQL复杂的批处理里出问题的概率也高。能用参数化的地方全部参数化别图省事直接拼字符串。3.4 临时表的性能陷阱临时表本身不会导致SQL变慢导致变慢的往往是使用方式。最常见的性能陷阱有三个。第一个是没有节制地使用大临时表。有些开发把几十个中间步骤都拆成临时表每张临表都几百万行tempdb被塞得满满的SQL Server里临时表全在tempdb里tempdb的IO就成了瓶颈。合理做法是能合并步骤就合并用完的临时表立刻DROP别攒着。第二个是省略DROP的隐患。你以为会话结束自动清理就没问题了但在长事务、长连接场景里临时表会长期占用资源。比如一个存储过程循环处理大批量数据每次循环都建临时表但不删tempdb空间就被慢慢吃满。规范写法-- 先检查并删除旧临时表 IF OBJECT_ID(tempdb..#orders_tmp) IS NOT NULL DROP TABLE #orders_tmp; -- 建表灌数... -- 用完后删掉 DROP TABLE #orders_tmp;第三个是临时表和主表关联时的类型不一致。临时表里字段是从查询结果集推导的类型跟源表可能不完全一样关联时会发生隐式转换导致索引失效。比如源表user_id是INT你在临时表里给他建成了VARCHAR(50)拼一拼也是能跑的但执行计划里全是转换运算符性能直接下雨。建临时表时字段类型尽量显式声明成跟参与关联的源表一致。4. 踩坑实录临时表的典型翻车现场4.1 连接复用时临时表残留这条我在生产环境里真实遇到过症状很诡异。某个定时任务每次跑批前都会建一张临时表往里面塞一批数据然后做关联汇总。某一天突然发现跑批结果经常包含了上一次跑批的冗余数据而且时好时坏跟内部同事排查了半天最后发现是数据库连接池搞的鬼应用代码在方法结束时没有DROP临时表连接归还给连接池后下一次任务从池子里拿到了同一个连接看到上一次的临时表还在直接往里INSERT数据就叠加了。解决方案看起来很简单——代码里DML之后补一个DROP。但很多团队漏这么一句。更稳妥的做法是在方法最开始就做临时表的存在即删除再执行建表方法结束时再DROP。这样即使上一次异常退出没来得及删下一次也会先清掉重来。如果是MySQL还可以直接用CREATE OR REPLACE TEMPORARY TABLEMySQL 8.0支持一句话把建表和重置合并了。4.2 MySQL临时表落盘导致全表查询变慢MySQL的临时表有一个内存优先、超出落盘的机制。默认情况下内存临时表大小超过tmp_table_size或max_heap_table_size的限制后MySQL会把临时表从内存转成磁盘上的临时表。一旦落到磁盘性能下降一个量级都不止。我见过一个报表查询单次跑了17秒把这两个参数调大之后内存能装下临时表查询直接降到2秒。排查方式很简单SHOW GLOBAL STATUS LIKE Created_tmp_disk_tables; SHOW GLOBAL STATUS LIKE Created_tmp_tables;如果Created_tmp_disk_tables占比很大说明临时表频繁落盘了。解决思路有三条一是合理设置tmp_table_size和max_heap_table_size让临时数据尽量留在内存二是优化SQL减少需要生成临时表的排序和去重操作三是避免在临时表里放太多没用的宽字段。注意这个参数是全局性的调整需要考虑服务器内存水位别为了一个查询把整个实例的内存占满。4.3 SQL Server tempdb空间被临时表吃光SQL Server的临时表全部落在tempdb里。大量并发会话同时创建大临时表tempdb文件会不断膨胀。tempdb的自动增长和普通用户库一样但如果磁盘空间不够所有需要用到tempdb的查询都会报错连系统级别的排序、hash join等操作也会受影响整个数据库实例都跟着遭殃。这个问题的防护思路是运维侧的tempdb建议设成按核数分配多个等大小数据文件启用自动增长但初始大小不要设置太小避免频繁增长拖累性能。应用侧的思路才是重点用完临时表立刻删避免长事务里保留大临时表不要在临时表上做超大批量的UPDATE——大批量更新临时表产生的日志量和锁量都很可怕。另一个容易忽略的是SQL Server版本的临时表统计信息更新时间点。临时表创建后第一次插入大量数据统计信息可能是自动更新的但如果这个临时表在存储过程里反复重建统计信息的自动更新阈值未必每次都触发所以存储过程里可以显式加上UPDATE STATISTICS。踩过这个坑的人都知道同一套存储过程数据量小的时候跑得飞快数据量翻倍之后执行计划突然变成哈希匹配全表扫描慢得离谱九成都是统计信息没跟上。4.4 临时表影响执行计划导致存储过程不稳定这又是一个存储过程相关的经典坑。SQL Server的存储过程默认会缓存执行计划。如果存储过程里用到了临时表第一次执行时SQL Server会根据当时的数据分布生成执行计划并把这个计划缓存下来。之后同样的存储过程再执行即使临时表的数据量发生了巨大变化优化器可能依然沿用旧的执行计划导致表现极不稳定。解决手段有几个一是给存储过程加WITH RECOMPILE牺牲一点编译开销来换取每次生成新计划二是把可能导致执行计划差异大的部分SQL拆出来局部使用OPTION(RECOMPILE)。这两种我都实际用过度数据量波动大、敏感查询场景建议局部OPTION(RECOMPILE)代价更小如果整个存储过程本身就轻直接WITH RECOMPILE省心。Oracle用户可能觉得这个问题很陌生——Oracle的优化器是每次解析生成计划和SQL Server的缓存机制差异很大所以我特别强调这个坑是SQL Server特有的。4.5 临时表和永久表重名MySQL的隐藏陷阱MySQL有个有意思的优先级规则如果当前会话里存在临时表和某张永久表同名那么当前会话里所有SQL访问到的都是临时表永久表被遮蔽了。一旦临时表被DROP后续访问才重新落到永久表上。这会造成什么坑比如你为了跑批建了一张tmp_orders临时表库里同时也存在一张tmp_orders永久表很可能是某次历史遗留你往临时表里INSERT数据看起来操作成功但往查询时拿到的数据却跟永久表的字段结构对不上报各种奇怪的错。更糟糕的是如果你以为在操作永久表实际上在操作临时表数据最终永久表里压根没更新。解决方式很直接建临时表前先检查同名表是否存在或者统一给临时表加前缀从命名层面避免和业务永久表撞名。我个人的习惯是MySQL临时表统一用tmp_加业务模块前缀比如tmp_trade_order_stat碰到重名的概率就极低了。5. 一次完整的临时表跑批实操5.1 场景定义和拆解用个具体的例子把所有语法串起来。假设有个电商业务需要做一个每周跑批统计过去7天每个用户的支付总金额、支付单量、平均客单价同时关联用户表拿用户地区信息最后把结果输出到一张汇总表里。这个场景非常适合用临时表拆解因为中间结果集会被多次引用既要做用户维度聚合又要关联用户地区后续可能还要基于中间结果再做其他筛选。拆解思路是三步走第一步把过去7天的订单明细抽出来放临时表里目的是避免后续多次扫描大订单表第二步做用户维度聚合得到统计结果第三步关联用户表补充地区信息。整个过程中临时表被多次读取比每次重新扫描几百万行订单表高效得多。5.2 建表、灌数、索引、关联的完整代码以SQL Server为例完整代码如下-- 1. 清理旧临时表防连接复用 IF OBJECT_ID(tempdb..#tmp_orders) IS NOT NULL DROP TABLE #tmp_orders; -- 2. 抽明细数据到临时表 SELECT order_id, user_id, amount, pay_time INTO #tmp_orders FROM dbo.orders WHERE pay_time DATEADD(day, -7, GETDATE()); -- 3. 临时表补索引按user_id关联用 CREATE INDEX ix_tmp_orders_user ON #tmp_orders(user_id); -- 4. 用户维度聚合 IF OBJECT_ID(tempdb..#tmp_user_stats) IS NOT NULL DROP TABLE #tmp_user_stats; SELECT user_id, COUNT(order_id) AS order_cnt, SUM(amount) AS total_amount, AVG(amount) AS avg_amount INTO #tmp_user_stats FROM #tmp_orders GROUP BY user_id; -- 5. 关联用户表补充地区 SELECT u.user_name, u.user_region, s.order_cnt, s.total_amount, s.avg_amount FROM #tmp_user_stats s INNER JOIN dbo.users u ON s.user_id u.user_id ORDER BY s.total_amount DESC; -- 6. 清理临时表 DROP TABLE #tmp_orders; DROP TABLE #tmp_user_stats;注意第3步建索引的时机先灌数据再建索引聚合和关联都能受益。如果数据量不大索引的作用不明显但数据量到几十万行以上这个索引能让关联从全表扫描变成索引查找差距肉眼可见。第6步的DROP别省前面写的连接池问题就是这么来的。如果把这段代码换成MySQL核心差异在于临时表用CREATE TEMPORARY TABLE加AS SELECT没有SQL Server的IF OBJECT_ID判断和SELECT INTODROP TEMPORARY TABLE IF EXISTS tmp_orders; CREATE TEMPORARY TABLE tmp_orders AS SELECT order_id, user_id, amount, pay_time FROM orders WHERE pay_time DATE_SUB(NOW(), INTERVAL 7 DAY); ALTER TABLE tmp_orders ADD INDEX idx_user(user_id); DROP TEMPORARY TABLE IF EXISTS tmp_user_stats; CREATE TEMPORARY TABLE tmp_user_stats AS SELECT user_id, COUNT(order_id) AS order_cnt, SUM(amount) AS total_amount FROM tmp_orders GROUP BY user_id; -- 后续逻辑 ...这段代码在MySQL里也能直接跑DROP TEMPORARY TABLE IF EXISTS处理了重复运行问题ALTER TABLE补了索引。不同数据库语法差异确实存在但核心思路完全一致明细进临时表、建索引加速、聚合、合并最后清理。5.3 事后清理和监控检查临时表用完之后清理是必须的。除了在代码末尾显式DROP TABLE生产环境里还有两件事值得做一是用系统表或者监控工具定期检查临时对象是否残留SQL Server可以查tempdb.sys.tables看有没有长期不释放的对象MySQL可以查information_schema.innodb_temp_table_info需要开启相关配置二是确认连接池配置确保连接归还后临时表对象不会滞留。这里有个通用的底线结尾的DROP一定写。哪怕你觉得会话结束反正会清理但谁能保证这个会话一定正常结束呢应用服务器断电、连接异常断开、长事务未提交这些异常场景都可能让临时表多活很久白白占着tempdb空间。写完SQL习惯性补上DROP是个好习惯。我自己写存储过程的时候甚至会在一开始就批判性地检查一遍每个临时表是否都在后面被DROP了。我个人在实际操作中最深的体会是临时表本身只是工具真正决定一个SQL好不好用的往往是你有没有提前想清楚数据生命周期和访问模式。是会被复用多次还是一次用完就扔需要在临时表上建索引吗连接的会话生命周期有多长这笔账算清楚了临时表会变成你手里最顺手的中间层工具算不清楚任何一个角落都会变成事故现场。希望这篇总结能帮你把SQL里的临时表真正用到位该快的快该省的省别再被这些隐藏的坑绊住。