相关子查询是依赖外部查询当前行字段、逐行执行的嵌套查询;它适合余额校验,因需对每笔流水动态计算截至该时间点的累计余额,必须引用t1.account_id和t1.transaction_time来筛选并聚合历史记录。

什么是相关子查询,为什么它适合余额校验场景
相关子查询是指子查询中引用了外部查询的列,每次外部行执行时都会重新计算一次。在资产账户余额校验中,我们需要对每条流水(如交易记录)动态计算“截至该笔流水时间点的累计余额”,这天然依赖当前行的 account_id 和 transaction_time —— 正是相关子查询最典型的使用场景。
不适用普通聚合(如 SUM() OVER (PARTITION BY ... ORDER BY ...))的情况包括:数据库版本过低(如 MySQL 5.7 不支持窗口函数)、需要嵌入到 WHERE 或 CASE 中做条件判断、或需与非标准时间逻辑(如按日切片而非严格时间戳排序)配合。
用相关子查询计算截至某笔流水的实时余额
核心思路是:对每条流水 t1,在子查询中筛选出同一账户下所有 transaction_time 的记录,并求和。
SELECT
t1.account_id,
t1.transaction_time,
t1.amount,
(SELECT SUM(t2.amount)
FROM transactions t2
WHERE t2.account_id = t1.account_id
AND t2.transaction_time <p>注意点:</p>
-
transaction_time必须有索引(联合索引(account_id, transaction_time)效果最佳),否则性能会随数据量陡增 - 若存在并行同秒多笔流水,且业务要求严格序号控制,需额外引入自增
id或seq_no辅助排序 -
amount字段必须为数值类型;若含空值,SUM()会自动忽略,但建议先用COALESCE(amount, 0)显式处理
用相关子查询校验余额是否异常
常见校验逻辑:某笔流水的 expected_balance 字段应等于其截至时间点的累计余额。可用相关子查询直接在 WHERE 中比对:
SELECT
t1.account_id,
t1.transaction_time,
t1.amount,
t1.expected_balance
FROM transactions t1
WHERE t1.expected_balance != (
SELECT COALESCE(SUM(t2.amount), 0)
FROM transactions t2
WHERE t2.account_id = t1.account_id
AND t2.transaction_time <p>容易踩的坑:</p>
- 没加
COALESCE(..., 0):当子查询无匹配行(如首笔流水前无数据),结果为NULL,而NULL != 任何值恒为UNKNOWN,导致该行被 WHERE 过滤掉 —— 看似没报错,实则漏检 - 时间精度不一致:外部表用
DATETIME,子查询条件却用了DATE(transaction_time),破坏相关性,变成非相关子查询,结果全账户误算 - 账户维度遗漏:只写了
t2.transaction_time ,漏掉 <code>t2.account_id = t1.account_id,会导致交叉累加,余额爆炸式增长
性能敏感时的替代方案与边界提醒
相关子查询在万级流水以内响应尚可,但超过 10 万行后,即使有索引,MySQL 或 PostgreSQL 也可能出现明显延迟。此时应考虑:
- 改用窗口函数(如
SUM(amount) OVER (PARTITION BY account_id ORDER BY transaction_time, id)),前提是数据库版本支持且能接受排序稳定性要求 - 将校验逻辑下推到应用层:按
account_id分批拉取有序流水,在内存中递推计算余额,避免重复 SQL 解析与磁盘扫描 - 对历史已闭账账户,预计算并持久化每日终态余额到
account_daily_balance表,校验时只查最近 N 天 + 实时流水
相关子查询不是银弹。它清晰表达了“每行独立快照”的语义,但代价是隐式循环。真正上线前,务必在生产数据量级下 EXPLAIN 验证执行计划,尤其盯住子查询是否走了索引、是否被优化器重写成 JOIN。










