并发调用存储过程变慢主因是多会话在锁、执行计划缓存及临时资源上互相阻塞;典型表现为单次50ms,50并发时升至800ms以上,并出现LCK_M_U或SOS_SCHEDULER_YIELD等待。

为什么并发调用存储过程会变慢
不是因为存储过程本身“慢”,而是多个会话同时执行时,容易在锁、执行计划缓存、临时资源上互相阻塞。典型表现是:单次调用 CALL proc_user_report(101) 耗时 50ms,但 50 个并发调用时,平均响应升至 800ms 以上,且出现大量 wait_type = LCK_M_U 或 wait_type = SOS_SCHEDULER_YIELD。
避免参数嗅探导致的计划复用失效
SQL Server(尤其常见)会基于首次传入参数生成执行计划并缓存;若后续参数选择性差异大(比如一次查用户 ID=1,一次查 ID=999999),复用旧计划可能引发全表扫描。MySQL 8.0+ 和 PostgreSQL 也存在类似问题,只是机制不同。
- SQL Server:加
OPTION (RECOMPILE)强制重编译(适合参数波动剧烈的报表类过程) - SQL Server:用
WITH RECOMPILE创建存储过程(不推荐高频写,仅用于逻辑极不稳定的场景) - MySQL:避免在 WHERE 中对参数做函数包装,例如
WHERE DATE(created_at) = DATE(@dt)→ 改为WHERE created_at >= @dt AND created_at - 通用做法:对高并发核心过程,把关键过滤字段单独建覆盖索引,减少计划对参数值的敏感度
减少锁竞争与事务范围
存储过程里一个 UPDATE 没提交,整个事务就持锁;如果里面还嵌了 SELECT ... FOR UPDATE 或跨多张表更新,锁升级风险陡增。并发一上来,sp_who2 或 sys.dm_exec_requests 里能看到大量 status = suspended。
- 只在真正需要一致性的地方开启事务,避免把日志记录、状态检查等非关键操作包进同一个
BEGIN TRAN - 更新前先用
SELECT ... INTO #tmp预筛选主键,再用INNER JOIN #tmp执行批量更新,比游标或逐行UPDATE持锁时间短得多 - 避免在过程中显式调用
sp_getapplock(SQL Server)或GET_LOCK()(MySQL)——它们本身就是串行化瓶颈 - 读多写少场景下,考虑用快照隔离(
ALTER DATABASE xxx SET READ_COMMITTED_SNAPSHOT ON),让 SELECT 不阻塞 UPDATE
批量操作代替逐行处理
这是最常被忽视、提升最直接的一点。看到 DECLARE cur CURSOR 或循环调用 CALL proc_handle_one_row(...),基本可以判定吞吐量已到天花板。
- 把游标逻辑全部改写成集合操作:例如原先是“查出 1000 条待处理订单,逐条更新状态 + 插入日志”,应改为两个语句:
UPDATE orders SET status = 'processed' WHERE id IN (SELECT id FROM #batch)+INSERT INTO log (...) SELECT ... FROM #batch - MySQL 中慎用
INSERT ... SELECT大量数据,务必确保目标表有合适索引,否则隐式锁表;可配合innodb_lock_wait_timeout缩短等待阈值,快速失败而非卡死 - PostgreSQL 注意
WITHCTE 默认不物化,若反复引用同一子查询,加MATERIALIZED关键字避免重复计算
真正卡住吞吐量的,往往不是某条 SQL 多慢,而是锁持有时间、计划适配偏差、以及“以为在并发实则在排队”的逻辑错觉。检查每个 CALL 的实际锁等待和执行计划复用情况,比盲目加索引更有效。

















