必须用子查询而非简单select,因为单条select无法原子性判断“是否足够扣减”,而“读-改-写”存在并发间隙;子查询可将库存查询与条件校验合并进一条语句,配合update where或select for update实现原子约束,避免超卖。

库存超卖检查为什么必须用子查询而不是简单 SELECT
因为单条 SELECT 无法在读取库存的同时原子性地判断“是否足够扣减”,而应用层先查再更新(即“读-改-写”)必然存在并发间隙,导致多个请求同时读到相同剩余库存并都通过校验。子查询能把“查库存”和“判断是否可扣减”压进一条语句里,配合 UPDATE ... WHERE 或 SELECT ... FOR UPDATE 实现原子约束。
用 UPDATE + 子查询做扣减前校验(推荐主流程)
这是最常用、最安全的方式:把库存检查和扣减合并为一条 UPDATE,靠 WHERE 条件里的子查询拦截超卖。数据库只会在满足条件时真正修改数据,否则影响行为为 0 行变更。
示例(MySQL):
UPDATE products SET stock = stock - 1 WHERE id = 123 AND (SELECT stock FROM products WHERE id = 123) >= 1;
注意点:
-
UPDATE的 WHERE 中不能直接写stock >= 1—— 因为该行可能被其他事务锁住但尚未提交,此时读到的是旧值;子查询会触发一次独立的当前读(consistent read 或加锁读,取决于隔离级别),更可靠 - 务必确保
id字段有索引,否则子查询会全表扫描,性能雪崩 - 执行后检查
ROW_COUNT()(MySQL)或pg_affected_rows(PostgreSQL),若返回 0,说明库存不足,不是异常而是预期结果
SELECT ... FOR UPDATE + 子查询组合用于复杂校验逻辑
当校验不止看单个商品库存,还要关联订单、SKU 组合、仓库分仓等时,子查询常嵌在 SELECT ... FOR UPDATE 的 WHERE 里,提前锁定关键行。
例如:检查某 SKU 在指定仓库的可用库存是否 ≥ 订单数量
SELECT id, stock FROM warehouse_stock WHERE sku_id = 456 AND warehouse_id = 789 AND stock >= (SELECT qty FROM order_items WHERE order_id = 999 AND sku_id = 456) FOR UPDATE;
关键细节:
- 子查询必须返回单值,否则报错
Subquery returns more than 1 row -
FOR UPDATE锁的是warehouse_stock行,不是子查询里的order_items行 —— 后者只读,不锁 - 如果子查询涉及未索引字段(如
order_items.order_id没索引),整个语句会变慢甚至锁表
子查询里用 JOIN 还是嵌套?性能差异在哪
纯库存检查场景下,优先用标量子查询(即返回单值的 (SELECT ...)),比 JOIN 更轻量、更易控制锁范围。但遇到“检查该用户历史 30 天内是否买过同类商品”这类跨表逻辑,就必须用 EXISTS 或 IN 子查询。
对比示例:
-- 推荐:EXISTS 判断是否存在购买记录(不取数据,只判真伪)
SELECT 1 FROM products p
WHERE p.id = 123
AND EXISTS (
SELECT 1 FROM orders o
JOIN order_items oi ON o.id = oi.order_id
WHERE o.user_id = 555 AND oi.product_id = p.id AND o.created_at > NOW() - INTERVAL 30 DAY
);
<p>-- 不推荐:LEFT JOIN + IS NOT NULL(会构造临时结果集,可能扫更多行)</p>
容易踩的坑:
-
IN (SELECT ...)在 MySQL 5.6 及更早版本中可能不走索引,换成EXISTS更稳 - 子查询里别用
ORDER BY ... LIMIT 1除非真需要排序后取首行——这会强制排序,拖慢响应 - PostgreSQL 对子查询优化更强,但若子查询含窗口函数或 CTE,仍可能生成计划外的物化步骤,需用
EXPLAIN ANALYZE验证
真正难的不是写出子查询,而是想清楚哪一行该被锁、哪一列必须索引、以及子查询返回空时业务逻辑怎么兜底。这些不画 ER 图、不跑压测,光看 SQL 是看不出来的。











