得从一个真实的场景说起。我早几年接了一个金融客户的报表迁移项目,原系统在业务高峰期要跑一张多门店汇总报表,单条SQL里嵌了六个子查询,其中一个子查询还要跨三张千万级流水表做关联统计。上线时直接把这个"庞然大物"往生产库一丢,结果慢了整整40秒还没出数。后面排查发现根本不是索引问题,而是这条SQL在单次执行里反复扫描同一批中间结果集。后来我把中间结果抽到临时表里,整个查询从40秒降到2秒左右。也就是从那次之后,我意识到"创建临时表"这件事,表面上就是几条语句的事,但选错方式、用错范围、忽略了统计信息和临时库的配置,都可能让性能雪上加霜。
"SQL创建临时表方法总结"这个题目,看着像是一篇新手笔记,真正做久了才发现,里面藏着不少值得掰开揉碎讲的东西。不同数据库的临时表语义差别非常大:SQL Server 的 # 临时表和 @ 表变量完全是两套机制,MySQL 的临时表在连接关闭后自动消失但可能把内存临时表落到磁盘,Oracle 的全局临时表则是结构共享、数据独立。把这些差异搞明白,比单纯背语法重要得多。
这篇文章我不会只给你堆几条 CREATE TABLE 语句,而是把会话级临时表、全局临时表、表变量、CTE 通用表表达式放在一起对比,讲清楚各自的适用场景、隐藏坑点和性能表现。适合正在学 SQL 的初学者,也适合经常写复杂报表SQL、做数据清洗或者维护存储过程的开发者和数据分析师。
1. 会话级临时表:三种主流数据库的创建语法与生命周期
会话级临时表是使用频率最高的一种临时表。它的核心特征是:只在当前会话或当前连接内可见,断开连接后自动销毁,而且在大多数数据库里,会话结束后临时表占用的资源会被回收。
1.1 SQL Server:# 表与 ## 表的经典用法
SQL Server 中创建会话级临时表最常见的方法是 SELECT ... INTO #temp 或者 CREATE TABLE #temp。这两种方式各有用处,不能简单替换。
sql复制-- 方式一:SELECT INTO,边查询边建表
SELECT
a.order_id,
a.customer_id,
b.region_name
INTO #orders_with_region
FROM orders a
LEFT JOIN dim_region b
ON a.region_id = b.region_id;
-- 方式二:CREATE TABLE + INSERT,适合先定义结构再逐步填充
CREATE TABLE #temp_summary (
order_id INT NOT NULL,
total_amount DECIMAL(18, 2),
create_date DATE
);
INSERT INTO #temp_summary (order_id, total_amount, create_date)
SELECT order_id, total_amount, create_date
FROM orders
WHERE create_date >= '2025-01-01';
实际工作中我比较推荐 CREATE TABLE 加 INSERT 的组合。原因很直接:SELECT INTO 虽然写起来快,但它不会自动继承源表的主键、默认值、索引和约束。如果你后续要对临时表做多次关联或过滤,没有索引的临时表在大数据量下会走全表扫描,性能一下就暴露了。你可以在 SELECT INTO 之后手动补索引:
sql复制CREATE INDEX idx_temp_orders_customer ON #orders_with_region(customer_id);
补充一个容易忽略的细节:SQL Server 的 # 临时表存储位置在 tempdb 数据库。如果你在存储过程里频繁创建和销毁临时表,tempdb 会成为争夺热点。这就是为什么 tempdb 的数据文件如果只有一个,高并发时很容易出现 PAGELATCH 等待。后面专门的章节我会再展开。
1.2 MySQL:CREATE TEMPORARY TABLE 与内存临时表的隐藏代价
MySQL 创建临时表的语法:
sql复制CREATE TEMPORARY TABLE temp_user_sales (
user_id INT NOT NULL,
sales_amount DECIMAL(18, 2),
stat_date DATE,
PRIMARY KEY (user_id, stat_date)
) ENGINE = InnoDB;
临时表在 MySQL 里有非常典型的执行计划参与逻辑。当你执行包含派生表(derived table)或 GROUP BY 的复杂查询时,MySQL 优化器会自动创建内部临时表,这种表不在你的控制范围内,完全由优化器决定放在内存还是磁盘。
- 如果临时表数据量小于
tmp_table_size和max_heap_table_size,优化器会优先使用MEMORY存储引擎的内存临时表。 - 一旦临时表大小超过限制,MySQL 会自动把它转换为磁盘上的 InnoDB 临时表。
这个转换过程是个隐形的性能杀手。我曾经处理过一个慢查询,看起来只是对几万行数据做 GROUP BY,但因为每行包含一个很大的 TEXT 字段,内存临时表存不下,最终落盘到临时文件,查询从毫秒级变成了秒级。
所以当你自己手动创建临时表时,需要注意几点:
sql复制-- 查看当前会话的临时表状态
SHOW CREATE TABLE temp_user_sales;
-- 查看临时文件落盘情况(普通用户权限足够)
SHOW GLOBAL STATUS LIKE 'Created_tmp_disk_tables';
SHOW GLOBAL STATUS LIKE 'Created_tmp_tables';
Created_tmp_disk_tables 和 Created_tmp_tables 的比值,直接反映内部临时表落盘的比例。如果这个比值偏高,优先排查是不是查询里带了 TEXT/BLOB 类型字段,或者 GROUP BY / ORDER BY 涉及了大字段。
MySQL 临时表的生命周期分两种:CREATE TEMPORARY TABLE 创建的会话级临时表在会话结束或 DROP TEMPORARY TABLE 时释放;而优化器内部创建的临时表在语句执行完就释放,外部看不到也管不到。
1.3 PostgreSQL:TEMP TABLE 的 ON COMMIT 行为
PostgreSQL 里临时表的使用率非常高,而且它有几个很实用的特性:
sql复制-- 会话级临时表,事务结束时保留数据
CREATE TEMP TABLE temp_orders AS
SELECT * FROM orders WHERE create_date >= CURRENT_DATE - interval '30 days';
-- 事务结束时删除数据,但表结构会继续保留
CREATE TEMP TABLE temp_orders (
order_id INT,
amount NUMERIC
) ON COMMIT PRESERVE ROWS;
-- 事务结束时直接删除表
CREATE TEMP TABLE temp_orders (
order_id INT,
amount NUMERIC
) ON COMMIT DROP;
ON COMMIT DROP 通常用于事务内部的中间结果计算,事务一结束表就消失,不会遗留。ON COMMIT PRESERVE ROWS 则适合在同一个事务里多次使用这批数据。
PostgreSQL 的临时表还有一个非常实用的隐藏福利:临时表会自动创建在各自的会话私有 schema 中,多个会话的同名临时表互不干扰,而且同一个会话里,临时表优先级高于同名的普通表。这意味着你在分析数据时,可以先创建一张临时表 orders,然后在当前会话里写 SELECT * FROM orders 就会命中临时表,完全不用改原有SQL。这是个很巧妙的用法,适合在数据校验场景里用。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 全局临时表与跨会话共享:什么时候值得用,什么时候是坑
2.1 SQL Server 的 ## 全局临时表
SQL Server 的全局临时表用双井号 ## 表示:
sql复制CREATE TABLE ##global_temp_results (
batch_id INT,
total_count INT,
process_time DATETIME
);
全局临时表创建后,所有会话都能查询和修改,直到最后一个引用它的会话断开连接,表才会被自动删除。这里有个非常经典的坑:如果你在会话A创建了 ##表,会话A断开之后,如果会话B还在使用这张表,表不会立即删除,而是等到会话B也断开才删。但如果创建者断开后没有其他会话引用它,表直接就没有了。 这个机制导致全局临时表的行为有点像"共享文件",用的时候都在,谁都不确定它什么时候消失。
我在团队内部多次强调:全局临时表能不用就不用。原因有三个:
- 命名冲突:如果两个存储过程同时创建同名全局临时表,后到的事务会直接报错,因为表已经存在。
- 并发阻塞:多个会话同时写入同一张全局临时表,会造成锁等待,特别是大批量
INSERT时。 - 生命周期不可控:依赖"最后一个会话断开"这个条件,一旦有某个连接异常保持打开,临时表就无法及时清理,占用
tempdb空间。
如果确实需要在多个存储过程或会话间传递中间数据,我更建议用普通表加 SessionId 字段做逻辑隔离,或者直接用表值参数。
2.2 Oracle 的全局临时表:结构与数据分离
Oracle 的全局临时表(GLOBAL TEMPORARY TABLE)和 SQL Server 完全不同:
sql复制CREATE GLOBAL TEMPORARY TABLE temp_employee_sales (
employee_id NUMBER,
sales_amount NUMBER(12, 2),
stat_date DATE
) ON COMMIT DELETE ROWS;
-- 或者 ON COMMIT PRESERVE ROWS
Oracle 的全局临时表是结构共享、数据独立的。表的定义、字段、索引是全局共享的,但每个会话看到的数据只有自己插入的那部分。ON COMMIT DELETE ROWS 表示事务提交后数据清空;ON COMMIT PRESERVE ROWS 表示会话结束才清空。
这个机制有两个好处:一是表结构只需要建一次,所有会话共用,不会反复创建和删除;二是数据对其他会话不可见,从根源上避免了 SQL Server 全局临时表那种互相污染的问题。
不过 Oracle 的全局临时表也有需要注意的地方:它在每个会话中的数据是独占的,所以不适合做"多个会话统一写入一份汇总结果"这种需求。如果真有跨会话共享汇总数据的场景,普通的实体表可能更合适。
2.3 PostgreSQL 和 MySQL 是否有全局临时表?
严格来说,PostgreSQL 和 MySQL 都没有直接对应 SQL Server ## 那种"所有会话共享且自动删除"的全局临时表。PostgreSQL 的临时表始终是会话私有的,MySQL 的 TEMPORARY TABLE 同样只对当前会话可见。要实现跨会话共享临时数据,通常的做法是用普通表加清理定时任务,或者用 UNLOGGED TABLE(PostgreSQL)这种不写 WAL 日志的表来提升性能。
PostgreSQL 里有个容易误用的概念:
sql复制-- 不写 WAL 日志的普通表,不是临时表,但性能和临时表同样好
CREATE UNLOGGED TABLE staging_import_data (
id INT,
payload JSONB
);
UNLOGGED TABLE 在崩溃恢复后无法保证数据完整性,但如果你只是用它做中间层暂存,之后有完整的数据重建流程,这个方案性能明显优于普通表。我在数据导入场景里经常用这个技巧,导入速度能提升不少。
3. 表变量:轻量场景的正确打开方式和误用场景
表变量(Table Variable)是 SQL Server 里一个很有争议的设计。它和临时表看起来很像,但底层机制差异很大。
sql复制DECLARE @temp_table TABLE (
product_id INT NOT NULL,
category_name NVARCHAR(50),
price DECIMAL(18, 2)
);
INSERT INTO @temp_table (product_id, category_name, price)
SELECT product_id, category_name, price
FROM products;
3.1 表变量的核心优点
首先是作用域清晰,它只在当前批处理(Batch)或存储过程作用域内存在,结束后自动释放,不需要显式 DROP。
其次是没有统计信息、不触发重编译,适合数据量小且固定不变的场景。官方文档明确说明,表变量不会创建统计信息,优化器通常预估它只有一行数据。当你往表变量里插入几千行数据再做关联时,优化器可能基于"一行"这个错误假设选择 nested loop,从而导致性能问题。
3.2 表变量和临时表怎么选
我个人的判断标准非常直接:
| 比较维度 | 表变量 | 临时表(#表) |
|---|---|---|
| 作用域 | 当前批处理/存储过程 | 当前会话 |
| 统计信息 | 无 | 有 |
| 索引支持 | 只能通过 PRIMARY KEY / UNIQUE 约束建立 | 可创建多个普通索引 |
| 使用场景 | 小数据集、固定行数 | 大数据集、需要索引、需要多次关联 |
| 事务回滚 | 表变量定义不会回滚,但数据变更会回滚 | 全部跟随事务回滚 |
| 存储位置 | tempdb,但元数据开销更小 | tempdb |
注意:表变量的数据变更也会写
tempdb,但不触发日志记录(在某些情况下),所以轻量小表用它非常合适。一旦数据行数预计超过几千行,或者你要在查询里多次关联、排序、分组,请直接使用临时表。因为表变量没有统计信息这件事,在高数据量下几乎一定会坑你。
例如下面这个场景,我用表变量存了8000个订单ID,然后和订单明细表做关联,执行计划直接走到 nested loop,跑了快20秒。换成临时表并建了索引后,变成 hash join,1秒内完成。原因很简单,优化器不知道表变量里有8000行,以为只有1行,行为自然不可控。
3.3 SQL Server 2022 的改进
值得一提的变化是,SQL Server 2022 引入了表变量延迟编译(Table Variable Deferred Compilation),优化器在执行时能看到表变量的真实行数,不再固定预估1行。这个特性让表变量在小中型场景里的准确性提升了不少。但如果你还在用 SQL Server 2019 或更早版本,上面说的坑依然存在,选型时不要掉以轻心。
4. CTE 通用表表达式:临时表的最佳替代方案
很多读者看到"创建临时表方法总结"这个标题,大概率是想找一个稳定的中间结果存储方式。但我想先说一个容易被忽略的事实:不是所有中间结果都值得建临时表。CTE(Common Table Expression,通用表表达式)在很多场景下比创建临时表更合适,尤其是在你只是想在单个SQL语句内复用一段查询结果时。
4.1 WITH AS 的基本用法
sql复制WITH regional_sales AS (
SELECT
region_id,
SUM(amount) AS total_sales
FROM orders
WHERE create_date >= '2025-01-01'
GROUP BY region_id
),
top_regions AS (
SELECT region_id
FROM regional_sales
ORDER BY total_sales DESC
LIMIT 10
)
SELECT
o.order_id,
o.amount,
tr.region_id
FROM orders o
INNER JOIN top_regions tr
ON o.region_id = tr.region_id;
CTE 并不是"创建物理表",它更像是一个命名的派生表,只在SQL语句执行期间存在。好处很明显:只写一次SQL,不需要 CREATE TABLE、INSERT、DROP 三条语句,阅读起来也很清晰。
4.2 CTE 不会物化,但可能重复执行
很多人对 CTE 有一个误解:认为 WITH 子句只会计算一次,后续引用都用缓存结果。实际上大多数数据库对 CTE 的处理方式是内联视图(inline view),也就是你引用几次,它就可能被展开计算几次。这条规则的例外是:
- PostgreSQL 12 之前的版本默认会物化 CTE。但物化并不是好事,它会导致优化器无法下推谓词,性能反而下降。
- PostgreSQL 12 以上版本中,如果 CTE 在外部查询中引用多次,优化器可能自动物化;如果只引用一次,通常会内联。
- 如果你确定某个 CTE 成本很高,而且要被引用多次,在 PostgreSQL 中可以显式加上
MATERIALIZED:
sql复制WITH heavy_calc AS MATERIALIZED (
SELECT
customer_id,
COUNT(*) AS order_cnt
FROM orders
GROUP BY customer_id
)
SELECT * FROM heavy_calc h
INNER JOIN customers c ON c.customer_id = h.customer_id;
可以用 NOT MATERIALIZED 强制内联,但除非你确定了优化器的行为,否则不建议轻易介入。
4.3 递归 CTE:临时表做不到的事
CTE 还有一个临时表难以优雅实现的功能,就是递归查询:
sql复制WITH RECURSIVE employee_tree AS (
-- 锚点成员
SELECT employee_id, manager_id, employee_name, 1 AS level
FROM employees
WHERE manager_id IS NULL
UNION ALL
-- 递归成员
SELECT
e.employee_id,
e.manager_id,
e.employee_name,
et.level + 1
FROM employees e
INNER JOIN employee_tree et
ON e.manager_id = et.employee_id
)
SELECT * FROM employee_tree;
在组织架构表、BOM 物料清单这类树形结构查询里,递归 CTE 是标准答案。临时表当然也能用循环实现同样效果,但代码量会多不少,而且维护成本高。
4.4 什么时候该用 CTE,什么时候该建临时表
我的经验是:CTE 适合在单条SQL语句内部完成中间结果的复用,适合逻辑上拆解复杂SQL,让代码更可读。但如果你遇到下面这些情况,物理临时表更靠谱:
- 同一个中间结果需要被多条SQL语句引用,或者需要在一个存储过程里反复使用。
- 中间结果数据量很大,且会参与多次 JOIN 和过滤,希望建立索引来提高访问效率。
- 你需要给中间结果添加统计信息,让执行计划更准确。
- 你需要分步调试,把中间结果拿出来肉眼检查。
- 中间结果参与递归处理时,CTE 反而受限,临时表配合循环可以做更复杂的逻辑。
CTE 最大的短板就是"无法加索引"(除非数据库优化器自动物化,但你没有直接控制权)。当你发现一条复杂查询中某个 CTE 被反复引用,执行计划显示它被扫描了好几次时,就应该果断把它改造成临时表。
下面是一个真实的优化案例。最初写法是一个三层嵌套CTE,最内层CTE数据量在200万行,被外层引用了两次,执行计划里出现了两次全量扫描和一次额外的排序,耗时18秒。我把最内层CTE改成临时表,并在 customer_id 上建了索引,最终SQL执行时间降到2.5秒。这个案例非常典型——CTE不是不能用,而是不能把大结果集反复用。
5. 临时表创建后的性能命门:索引、统计信息与临时库/内存设置
这一节是我最想分享的实操经验。很多开发者说"我用了临时表,但还是很慢",问题往往不是临时表本身,而是用临时表的方式忽略了几个关键因素。
5.1 临时表上的索引是决定查询计划的核心
无论是 SQL Server 的 #temp、Oracle 的全局临时表,还是 MySQL 的 TEMPORARY TABLE,如果你只创建表、插入数据,不做任何索引,那后续的关联查询大概率还是全表扫描。大表全表扫描的代价,不会因为你把它改成临时表就自动消失。
sql复制-- SQL Server: 临时表加索引
CREATE TABLE #temp_sales (
order_id INT,
product_id INT,
amount DECIMAL(18, 2),
create_date DATETIME
);
CREATE INDEX ix_temp_sales_product ON #temp_sales(product_id);
CREATE INDEX ix_temp_sales_date ON #temp_sales(create_date);
sql复制-- MySQL: 临时表加索引,语法和普通表一致
CREATE TEMPORARY TABLE temp_sales (
order_id INT,
product_id INT,
amount DECIMAL(18, 2),
create_date DATETIME,
INDEX idx_product (product_id),
INDEX idx_date (create_date)
) ENGINE = InnoDB;
sql复制-- PostgreSQL
CREATE TEMP TABLE temp_sales (
order_id INT,
product_id INT,
amount DECIMAL(18, 2),
create_date TIMESTAMP
);
CREATE INDEX idx_temp_sales_product ON temp_sales(product_id);
索引不是越多越好。临时表本质上是中间结果集,你可以针对后续SQL的 JOIN 列、WHERE 过滤列、ORDER BY 列来精准加索引。三个索引以上就要开始掂量了,因为临时表本身需要写入数据,索引太多会增加写入负担。
5.2 SQL Server 临时表的统计信息与 OPTION (RECOMPILE)
SQL Server 临时表的统计信息是自动创建的,但它的统计更新时间点不一定符合你的预期。如果临时表插入大量数据后,继续查询前最好手动更新统计信息:
sql复制UPDATE STATISTICS #temp_sales;
还有一个比较进阶的优化技巧是 OPTION (RECOMPILE):
sql复制SELECT *
FROM #temp_sales ts
INNER JOIN orders o ON o.order_id = ts.order_id
WHERE ts.create_date >= '2025-01-01'
OPTION (RECOMPILE);
加上 RECOMPILE 后,SQL Server 会在每次执行时基于当前参数和临时表的最新统计信息重新生成执行计划。这个技巧非常适合临时表场景,因为临时表的数据量在每次运行中变化很大,缓存一个基于旧统计信息的执行计划反而有害。
5.3 tempdb 与临时文件:容易被忽视的全局瓶颈
SQL Server 的所有用户临时表都放在 tempdb 中,MySQL 的内部临时表和排序文件也可能落盘到临时目录,PostgreSQL 的临时表同样会写入 temp_buffers 或磁盘临时文件。因此,临时库/临时文件的配置会直接决定临时表的上限。
SQL Server 场景:
tempdb如果只有一个数据文件,高并发下会出现写入瓶颈,建议按 CPU 核心数配置多个等大小的数据文件,并开启即时文件初始化。- 不要把
tempdb和用户数据库放在同一个物理磁盘上,否则tempdb的写入会拖累业务库的 IO。 - 定期监控
tempdb的空间增长,异常爆满往往是因为某个会话的临时表没有被及时释放。
MySQL 场景:
- 查看临时表落盘情况时,重点看
Created_tmp_disk_tables和Created_tmp_tables的比值。 - 如果落盘比例高,调整
tmp_table_size和max_heap_table_size可以缓解,但千万别盲目调大,内存是有限的。更核心的优化是减少临时表里的大字段。 - MySQL 8.0 之后,磁盘临时表默认使用 InnoDB,临时表空间
ibtmp1只会增大不会自动收缩,如果历史峰值很高,磁盘空间会一直被占用。这是生产环境常见问题。
PostgreSQL 场景:
- 临时表会使用每个会话的
temp_buffers内存。如果你的临时表访问量大,可以在会话级别调大:
sql复制SET temp_buffers = '256MB';
- 注意必须要在会话中第一次使用临时表之前设置,否则该值不会生效。
5.4 临时表里的数据倾斜:统计信息救不了的场景
有些场景比统计信息过期更麻烦。比如你把几百万订单行按销售人员ID分组后存入临时表,然后和人员表关联。如果大部分订单集中在少数几个销售身上,数据分布严重倾斜,即使统计信息正常,优化器也可能选错 join 方案。这时候没有办法完全依赖自动统计,需要人工分析分布:
sql复制SELECT salesperson_id, COUNT(*)
FROM #temp_orders
GROUP BY salesperson_id
ORDER BY COUNT(*) DESC;
遇到这种情况,我通常的处理方法是:分析热点 key 之后,把临时表拆成"热点数据"和"非热点数据"两部分分别处理,或者直接换用 hash join hint。这些都是优化器自动能力之外的硬核手段。
6. 实战选型清单:一句话判断该用哪种方式
写到这里,临时表、表变量、CTE、全局临时表其实都覆盖到了。但大部分读者真正需要的是一个能放进日常工作中的判断决策清单。我把自己常用的判断逻辑整理成下面这个规则列表,可以直接抄过去用。
6.1 决策规则
- 如果你的中间结果只在一条SQL语句内复用,优先写 CTE,不要急着创建临时表。代码清晰,执行计划也更容易优化。
- 如果中间结果需要在一个存储过程或脚本的多条语句之间共享,或者后续要对它做多次关联、排序、聚合,用会话级临时表(SQL Server
#/ MySQLTEMPORARY/ PostgreSQLTEMP)。 - 如果中间结果数据量很小(几百行以内)且结构固定,可以用表变量(SQL Server),但数据量超过几千行就换成临时表。
- 跨会话共享中间结果,尽量用普通实体表加业务字段隔离,不要用 SQL Server 的
##全局临时表,也不要在 Oracle 全局临时表上做跨会话汇总。 - 树形结构递归查询,直接用递归 CTE,不要自己写循环加临时表。
- 创建任何临时表之后,如果后续有关联或过滤操作,立刻给关联列和过滤列加索引,这是性价比最高的一步。
- SQL Server 中如果临时表数据量每次差异很大,查询语句后面记得带
OPTION (RECOMPILE)。 - 使用临时表之前,先检查数据库全局配置:SQL Server 的
tempdb文件数、MySQL 的临时表落盘比例、PostgreSQL 的temp_buffers,这三项直接决定临时表的性能上限。
6.2 常见坑位速查表
| 坑点 | 现象 | 解决方案 |
|---|---|---|
| 表变量无统计信息 | 大结果集关联预估1行,执行计划极差 | 换成临时表或升级SQL Server 2022 |
| CTE 被多次引用时重复计算 | 同一CTE在计划中出现多次扫描 | 改造成临时表并加索引 |
| 临时表无索引 | 关联查询走全表扫描 | 创建后立即建索引 |
tempdb 单文件 |
高并发写入大量阻塞 | 按CPU核数拆多个数据文件 |
| MySQL 临时表落盘 | 查询从内存级别变成磁盘级别 | 优化临时表字段、调大临时表内存参数 |
| PostgreSQL 临时表内存不足 | 大量排序落盘,速度骤降 | 会话级别调大 temp_buffers |
| 全局临时表生命周期不可控 | 连接异常时表一直存在 | 尽量不用全局临时表或定期清理 |
6.3 一个综合用例
最后用一个稍微完整的例子把这些原则串起来。假设有一个需求:计算每个区域近30天销量前10的商品,并且还要在之后几步里重复使用这个结果集。
sql复制-- 第一步:建临时表并填充数据
CREATE TABLE #sales_top10 (
region_id INT,
product_id INT,
sales_amount DECIMAL(18, 2),
rank_no INT
);
INSERT INTO #sales_top10 (region_id, product_id, sales_amount, rank_no)
SELECT
region_id,
product_id,
SUM(amount) AS sales_amount,
ROW_NUMBER() OVER (PARTITION BY region_id ORDER BY SUM(amount) DESC) AS rank_no
FROM orders
WHERE create_date >= DATEADD(DAY, -30, GETDATE())
GROUP BY region_id, product_id;
-- 第二步:人工过滤排名,并加索引加速后续关联
DELETE FROM #sales_top10 WHERE rank_no > 10;
CREATE INDEX ix_sales_top10_region ON #sales_top10(region_id);
CREATE INDEX ix_sales_top10_product ON #sales_top10(product_id);
-- 第三步:后续使用,启用 RECOMPILE,确保执行计划适配本批次数据量
SELECT ...
FROM #sales_top10 t
INNER JOIN products p ON p.product_id = t.product_id
WHERE t.region_id = @region_id
OPTION (RECOMPILE);
这个例子演示了从建表、填充、清理、加索引到最终关联的完整链路。看起来没什么特别,但每一步都踩过不少人踩过的坑:如果一上来就只靠一条 SELECT INTO 建表,后续 DELETE 和索引都要额外处理;如果忘了加索引,最后的 INNER JOIN 可能直接跪在大表上。
最后想分享一个我自己的习惯,也算是一个偏方:每次写完临时表逻辑之后,都强制自己回头看一眼执行计划,重点确认两个点——临时表的扫描方式是不是索引 seek,临时表被引用了几次。如果能做到这两点,临时表带来的性能收益就不会被挥霍掉。很多"用了临时表反而更慢"的疑问,追根溯源,基本都是栽在这两个细节上。
