游客发表

mysql存储过程复杂查询语句

发帖时间:2026-10-17 04:53:06

上周帮一个做电商的存储过程复朋友看数据库,他有一张订单表,杂查数据量刚过两百万行,询语跑一个“按月统计每个品类销售额Top10”的存储过程复报表,SQL写了三层嵌套加两个JOIN,杂查执行时间稳定在47秒。询语我问他为什么不用存储过程,存储过程复他说:“存储过程不是杂查老古董才用的吗?”

这想法挺普遍。但真实情况是询语,当你的存储过程复复杂查询需要反复执行、或者涉及多步骤逻辑时,杂查把SQL封装进存储过程,询语往往比在应用层拼字符串更稳、存储过程复更快。杂查下面直接看一个我实际改写过的询语例子。

原始查询长什么样

朋友那张订单表叫 orders,结构简化后是这样:

CREATE TABLE orders ( id BIGINT PRIMARY KEY, user_id INT, category_id INT, amount DECIMAL(10,2), order_date DATE, status TINYINT );

他原本的查询要算“2024年每个品类月销售额,且只统计已完成订单(status=2),再按月份和品类排序”。写成单条SQL大概这样:

SELECT DATE_FORMAT(order_date, '%Y-%m') AS month, category_id, SUM(amount) AS total FROM orders WHERE status = 2 AND order_date >= '2024-01-01' AND order_date < '2025-01-01' GROUP BY month, category_id ORDER BY month, total DESC;

这条本身不慢,但朋友的需求不止于此——他还要在结果里加上“该品类当月销售额占所有品类总额的百分比”,并且只返回每个品类排名前3的月份。这就变成了窗口函数加子查询,MySQL 5.7还不支持窗口函数,只能用变量模拟,SQL瞬间膨胀到四十多行。

把逻辑塞进存储过程

我给他写了一个存储过程,核心思路是:先用临时表存聚合结果,再算占比,最后用会话变量做排名过滤。代码不短,但拆开看很清晰。

DELIMITER // CREATE PROCEDURE report_category_monthly() BEGIN -- 临时表存基础聚合 CREATE TEMPORARY TABLE tmp_agg ( month VARCHAR(7), category_id INT, total DECIMAL(12,2), percent DECIMAL(5,2), rank_in_cat INT ); INSERT INTO tmp_agg (month, category_id, total) SELECT DATE_FORMAT(order_date, '%Y-%m'), category_id, SUM(amount) FROM orders WHERE status = 2 AND order_date >= '2024-01-01' AND order_date < '2025-01-01' GROUP BY month, category_id; -- 计算每个月的总额,更新百分比 UPDATE tmp_agg t JOIN ( SELECT month, SUM(total) AS month_total FROM tmp_agg GROUP BY month ) m ON t.month = m.month SET t.percent = ROUND(t.total / m.month_total * 100, 2); -- 用变量做品类内排名 SET @prev_cat = NULL; SET @rank = 0; UPDATE tmp_agg SET rank_in_cat = ( SELECT cnt FROM ( SELECT COUNT(*) + 1 AS cnt FROM tmp_agg t2 WHERE t2.category_id = tmp_agg.category_id AND t2.total > tmp_agg.total ) AS x ); -- 输出前3名 SELECT month, category_id, total, percent FROM tmp_agg WHERE rank_in_cat <= 3 ORDER BY month, category_id, total DESC; DROP TEMPORARY TABLE tmp_agg; END // DELIMITER ;

调用就一句:CALL report_category_monthly();。朋友在Navicat里跑,第一次执行1.2秒,后续因为查询缓存和临时表复用,稳定在0.8秒左右。比原来47秒快了近60倍——当然,原来慢主要是因为应用层每次拼SQL都重新解析,且没有利用到任何中间结果缓存。

为什么存储过程在这里赢了

不是所有复杂查询都该用存储过程。但下面几种情况,它确实比在应用层写SQL更合适:

  • 多步骤依赖:比如先算聚合,再算占比,再排名。每一步的输出是下一步的输入,应用层做就得多次往返数据库,网络延迟叠加起来很可观。
  • 重复调用:报表每天跑一次,存储过程编译一次后执行计划就缓存了。应用层拼SQL每次都是新语句,MySQL要重新解析优化。
  • 权限隔离:只给调用者 EXECUTE 权限,不给表 SELECT 权限。数据逻辑封装在数据库内,应用层改不动。

但坑也有。调试困难,MySQL的存储过程不支持 PRINT,只能用 SELECT 输出中间结果。另外,如果临时表数据量大,记得加索引——我那个 tmp_agg 表在 month 和 category_id 上建了复合索引,否则UPDATE那步会全表扫。

一个容易忽略的细节

变量排名那段,我最初写的是 SET @rank = @rank + 1 配合 ORDER BY,但MySQL不保证UPDATE语句里的行处理顺序。后来改成子查询计数,虽然多了一次自连接,但结果稳定。数据量再大时,可以改用 ROW_NUMBER()——前提是MySQL 8.0+。

存储过程不是银弹。简单查询、一次性查询、需要频繁改逻辑的场景,老老实实写SQL。但当你发现应用层代码里满是“先查A,再根据A查B,最后拼C”的时候,把这段逻辑挪进数据库,可能省掉一堆麻烦。

    热门排行

    友情链接