Files
pj_service_cloud/db/20261022_hard_cs_statistics_verification.md
T
2026-10-22 16:17:46 +08:00

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