9.7 KiB
商户交易汇总优化与验证记录
2026-10-22,接口 POST /hard/hard-total/page-list-cs。线上数据库由用户确认为 PolarDB MySQL 8 兼容版;本次直接连接的测试数据库为 MySQL Community 8.0.46。本次没有连接生产数据库,也没有重启正在运行的服务。
查询链路与瓶颈
前端商户统计页面默认查询近三个月,返回整个商户汇总列表。接口名称虽然带 page,实际响应为 List<HardOrderCsVo>,没有分页 COUNT SQL。
请求依次经过子商户展开、用户可见商户类型处理、支付汇总、退款汇总、Java 合并、商户及部门信息补全、平台抽成,最后由前端计算表尾合计。商户信息已按 200 个 ID 分批查询;钱包服务补全商户时还会查询部门,因此接口总耗时不能直接等同于两条 SQL 的耗时。
原查询存在以下开销:
- 未筛选钱包类型时,支付和退款仍逐条关联
wallet_manage。 - 售后表只有
(order_id,del_flag,id)索引,无法直接按退款统计时间定位。 - 已有支付时间窄索引需要读取订单主表;钱包、用户部门和用餐时段筛选也需要额外字段。
- 实测发现优化器可能为商户分组选择商户索引,扫描全部历史订单;增加售后索引后,也可能改为从全部历史订单逐条查售后。仅加索引或仅删关联并不稳定。
- Java 合并退款及匹配抽成商户反复遍历列表;前端挂载和切换子表有多个查询触发点。
最终实现
HardOrderMapper.xml 仅在筛选钱包类型时关联钱包表,移除 VO 不使用的 max(user_dept_id),支付聚合使用 NO_GROUP_INDEX(a) 避免为分组顺序扫描整个历史商户索引。退款有日期范围且未指定订单、商户、用户或设备时使用 JOIN_PREFIX(r),先扫描售后日期范围;定向筛选保留优化器自行选择驱动表的能力。
配套脚本 db/20261022_hard_cs_statistics_indexes.sql 建立两个索引,并识别同名或异名的等效索引,避免重复创建:
hard_order (
tenant_id, del_flag, pay_time, order_status, merchant_type,
merchant_id, merchant_dept_id, pay_price, wallet_id, user_dept_id, hour_type
)
hard_after_sale (
tenant_id, del_flag, status, audit_time, order_id, refund_price
)
支付索引同时覆盖页面常用筛选,退款索引覆盖售后时间、金额及订单关联字段。新增索引会增加磁盘占用和写入维护成本,脚本保留所有既有索引,未自动删除窄支付时间索引。
HardOrderTotalServiceImpl 使用保留插入顺序的 Map 合并商户,抽成匹配也改为 Map,合并开销为 O(支付商户数 + 退款商户数)。保留每 200 个 ID 的远程查询边界。Controller 和 Service 对超过一秒的请求记录子商户解析、权限、支付查询、退款查询、合并、商户 RPC、部门 RPC 的分段耗时。
前端统一在异步子表挂载后查询,移除重复的 mounted/tab-click 触发,日/月切换每次只请求一次,并保留手动刷新和部门筛选。
同时修复两个边界问题:整个区间只有退款时原实现会漏掉全部商户;指定设备类型时原支付 SQL 引用了未关联的 m 别名。现在退款商户能显示负净额,设备类型筛选同时作用于支付和退款。
统计口径与正确性
支付仍使用状态 5、20、10 和订单 pay_time;退款仍使用完成状态 50 和原 SQL 的售后 audit_time,独立于订单支付时间。一笔订单多次退款按退款记录分别计次。支付与退款分别汇总后按商户相减,金额使用 BigDecimal,抽成计算及取舍位置保持原实现。
金额单位、时间范围的包含边界、商户类型排序规则、商户子级展开、用户部门和商户部门的包含/排除、缺失或已删除钱包的历史行为均有回归覆盖。继续使用现有租户及 DataScope 插件;没有将原先分组后的部门权限改为不同的行级语义。
保留优化前两条 SQL 作为测试对照,36 组筛选逐项比较支付及退款结果。MySQL 测试库现有业务数据另做 9 组对比。HTTP 回归实际执行 Controller、Service、Mapper 和权限插件,外部服务使用替身;没有将该测试当作真实网关或远程服务的性能测量。
还比较了“退款先按订单归集再关联”的候选。在一单一退款分布、已建立退款索引时,交替测量中直接关联约 175ms、预聚合约 274ms;最终没有将该物化方案用于业务查询。测试资源保留候选,供以后重新评估其他数据分布。
百万级 MySQL 测试
连接内临时表复制真实业务表的 SHOW CREATE TABLE,包括原有索引,再生成 100 万订单、100 万售后及合成钱包数据。数据覆盖两年、两个租户、997 个商户、不同状态及软删除标记,包含日期与支付状态过滤。最终验收使用一笔订单一条售后,避免多次退款的缓存复用使测试过于乐观;多次部分退款另有功能回归及前轮百万级数据验证。
最终 SQL 和索引下,每个场景先与旧 SQL 比较完整商户金额/次数,再测量三次取中位数。下表时间只包含 Mapper 查询及 SQL,不包含真实权限服务、商户/部门 RPC、网关和浏览器网络。
| 场景 | 支付中位数 | 退款中位数 | 两条查询合计 |
|---|---|---|---|
| 三个月,全部商户 | 144.52ms | 354.35ms | 498.87ms |
| 两天范围 | 5.27ms | 10.78ms | 16.05ms |
| 指定一个商户 | 4.80ms | 7.12ms | 11.92ms |
| 商户部门筛选 | 39.63ms | 593.03ms | 632.66ms |
| 用户部门筛选 | 39.51ms | 279.40ms | 318.91ms |
| 用餐时段筛选 | 58.90ms | 374.20ms | 433.10ms |
| 钱包类型筛选 | 143.37ms | 745.15ms | 888.52ms |
同一合成数据生成规则的原索引、旧 SQL 阶段,三个月支付约 1461.51ms、退款约 566.49ms,合计约 2028ms。不同轮次受本机并发负载、缓存和临时表统计信息影响,以上为离线阶段对比,不能推算线上接口必然从 5s 降至 0.5s。
最终实际执行计划:
支付:Covering index range scan on a using idx_hard_order_cs_pay_cover
日期范围扫描 111182 条索引记录,筛选后 106348 笔支付,997 个商户
退款:Covering index range scan on r using idx_hard_after_sale_cs_audit
日期范围 101079 条售后,再按 hard_order PRIMARY 查对应订单
钱包筛选:订单仍使用支付覆盖索引,之后按 wallet_manage PRIMARY 检查钱包类型
后端验收共 44 项:43 项执行通过,0 失败、0 错误;1 项与本次无关的旧 H2 性能用例默认跳过。本次 MySQL 百万级性能用例已实际执行。前端 19 项测试通过,其中新增 3 项执行页面真实 setup、异步子组件挂载及切换生命周期;页面 SFC 脚本和模板编译通过。
索引迁移在独立测试 schema 中连续执行,并重命名为异名等效索引再次执行,均未重复建索引。百万级临时表随连接关闭清理,独立 schema 已删除。验收后业务表仍为 833 条订单、158 条售后,业务表没有新增本次候选索引。
PolarDB 发布与验收
PolarDB 官方 Statement Outline 文档明确提供 JOIN_PREFIX 并说明支持 MySQL 8.0 的 Hint 类型;DDL 指南列明 8.0.1/8.0.2 创建二级索引支持并发 DML。脚本使用 ALGORITHM=INPLACE LOCK=NONE,不支持时会失败,不会自动降级为锁写入的 COPY。实际建索引开始/结束仍有元数据锁,并消耗 I/O 和 CPU,应安排低峰执行。
建议先灰度后端查询改造,再在同一发布窗口建立两个配套索引,随后发布前端。不要只给旧服务加索引:本次实测旧 SQL 曾因新增索引改变驱动表,明显变慢。不要自动回滚或删除其他既有索引。
在实际 PolarDB 节点上记录内核小版本及路由节点,用大租户近三个月、商户、钱包和部门条件执行 EXPLAIN,紧接 SHOW WARNINGS 检查提示是否被忽略;确认默认支付为日期覆盖范围扫描、默认退款先走售后时间索引。EXPLAIN ANALYZE 会实际执行查询,应选择验收时段。若该内核支持 EXPLAIN DDL,也可先预览建索引的算法和并发 DML 行为。
灰度时比较同一筛选的商户数、支付/退款金额及次数、负净额、表尾合计;结合新增分段日志记录端到端 P50/P95、数据库扫描行数和并发负载。线上执行计划、端到端性能和真实 RPC 的验证仍需在发布环境完成;本次没有使用生产连接或执行生产数据库操作。
复现后端测试前设置提供的 MYSQL_HOST、MYSQL_PORT、MYSQL_USER、MYSQL_PWD 环境变量,不将密码写入文件。在仓库根目录运行:
HARD_CS_MYSQL_TEST=true HARD_CS_MYSQL_BENCHMARK=true mvn \
-pl pjcloud-hardware/pjcloud-hardware-biz -am \
-Dtest=HardOrderCsSqlTest,HardOrderCsServiceTest,HardOrderCsMySqlTest,HardBatchExportQueryTest,HardBatchExportFileTest,HardOrderStatisticsSqlTest,HardOrderStatisticsServiceTest,HardOrderStatisticsScopeTest,HardOrderStatisticsControllerTest \
-Dhard.cs.benchmark.finalOnly=true \
-Dsurefire.failIfNoSpecifiedTests=false -DfailIfNoTests=false test
不设置 hard.cs.benchmark.finalOnly 可重跑原索引、只加退款索引、完整索引三个阶段,并交替比较预聚合候选,耗时更长。管理端仓库执行下列命令可复现本次 19 项前端回归:
node --test tests/batch-export.test.cjs tests/hard-sh-query-lifecycle.test.cjs \
tests/hard-third-order.test.cjs tests/wallet-ledger-exports.test.cjs \
tests/wallet-manage-export.test.cjs