-- 20261004 钱包流水表排序索引优化(可重复执行) -- ⚠️ 本脚本第 4、5 步(前缀索引)已被 20261005_wallet_transaction_biz_type_varchar.sql 接替: -- biz_type 改为 varchar 后重建为全列索引。线上已执行过 20261005 的环境勿再执行本脚本。 -- 背景:/api/wallet/wallet-transaction/page-all 分页查询很慢;索引生效后发现 page-cz/page-jz 仍慢(3~6s)。 -- 原因:查询固定排序 create_time DESC, user_id DESC, id DESC, -- 旧索引 (tenant_id, del_flag, create_time DESC, user_id DESC) 末尾隐式追加的主键 id 为 ASC, -- 与 ORDER BY 最后一段 id DESC 方向不匹配,索引无法提供有序结果, -- 优化器对租户内全部流水做 filesort(执行计划 Using filesort),数据量越大越慢。 -- 方案:建显式包含 id DESC 的 5 列索引,排序完全走索引,LIMIT 只需读取前 N 条; -- 旧 4 列索引被新索引前缀完全覆盖,一并删除以降低流水表写入放大。 -- 已在测试库(123.157.250.162:28816/pjcloudx_wallet)验证:四种查询形态(首页/部门权限包裹/业务类型+时间范围/冷门部门) -- filesort 全部消除;删除旧索引后计划不变。 -- 幂等方式:本环境(OceanBase MySQL 模式)不支持 CREATE INDEX IF NOT EXISTS / DROP INDEX IF EXISTS, -- 改用 information_schema.statistics 判断索引是否存在后经 PREPARE 动态执行; -- 已存在/已删除时输出 skip 提示,重复执行无副作用。 -- 执行顺序:在线 DDL 不锁表,可独立执行;若担心流水表写入放大,可只执行第 1、2 步。 USE pjcloudx_wallet; -- 1. 新建 5 列排序索引(与 Mapper commonWrapperQuery 的 ORDER BY 完全同序) SET @ddl = IF( (SELECT COUNT(*) FROM information_schema.statistics WHERE table_schema = DATABASE() AND table_name = 'wallet_transaction' AND index_name = 'idx_wallet_tx_tenant_del_ct_uid_id') = 0, 'CREATE INDEX idx_wallet_tx_tenant_del_ct_uid_id ON wallet_transaction (tenant_id, del_flag, create_time DESC, user_id DESC, id DESC)', 'SELECT ''skip: idx_wallet_tx_tenant_del_ct_uid_id 已存在'' AS msg'); PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt; -- 2. 删除被完全覆盖的旧 4 列索引(第 1 步成功后执行) SET @ddl = IF( (SELECT COUNT(*) FROM information_schema.statistics WHERE table_schema = DATABASE() AND table_name = 'wallet_transaction' AND index_name = 'idx_wallet_tx_tenant_del_create_user') > 0, 'DROP INDEX idx_wallet_tx_tenant_del_create_user ON wallet_transaction', 'SELECT ''skip: idx_wallet_tx_tenant_del_create_user 不存在'' AS msg'); PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt; -- 3. 部门前置索引:数据权限为"本部门/自定义部门"的操作者,最终 SQL 是外层 dept_id IN (...) 过滤。 -- 部门占比小时按 create_time 顺序扫会翻大量无关行;该索引让优化器按部门范围直取, -- 部门内排序与索引后缀同序同样免 filesort(已验证:冷门部门走此索引,大部门自动走 5 列索引)。 -- 代价是流水表(高写入)多维护一个二级索引,如写入压力敏感可只执行 1、2 步。 SET @ddl = IF( (SELECT COUNT(*) FROM information_schema.statistics WHERE table_schema = DATABASE() AND table_name = 'wallet_transaction' AND index_name = 'idx_wallet_tx_tenant_del_dept_ct') = 0, 'CREATE INDEX idx_wallet_tx_tenant_del_dept_ct ON wallet_transaction (tenant_id, del_flag, dept_id, create_time DESC, user_id DESC, id DESC)', 'SELECT ''skip: idx_wallet_tx_tenant_del_dept_ct 已存在'' AS msg'); PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt; -- 4. 业务类型+金额前缀索引:page-cz / page-jz 固定带 biz_type IN ('1','2','3') + price>0 / price<0。 -- biz_type 为 tinytext 无法整列建 B-tree(只能前缀),price 也不在任何索引中, -- 此前 COUNT 与翻页需对租户内全部行回表校验这两个字段;记账扣款(price<0)仅约 0.5% 行且偏旧, -- page-jz 首页还要按时间倒序翻过大量支付流水。 -- 本索引把 jz 的 COUNT/翻页访问行数从全租户降到 ~0.5%(测试库 40195→443),cz 同理; -- 已验证 page-all 仍走 5 列排序索引、page-xf(biz_type='4') 计划不变,无回归。 -- 注:前缀索引仍需回表核对 biz_type 完整值,线上行数放大后 COUNT 预计降至 1s 内。 -- ⚠️ 已知代价(2026-10-04 线上实测):本索引会使优化器把 page-jz 的翻页查询切换为 -- "全量命中集回表+filesort"(无提前终止),jz 从 6s 恶化到 10s;cz/xf 等不受影响。 -- 根因:biz_type 为 tinytext 只能建前缀索引,前缀列的条件不会下推到索引层(ICP), -- 也永远无法构成覆盖索引,多值 IN(1,2,3) 又无法沿用 biz 前导索引的顺序。 -- 纯索引手段无法同时优化 jz 的 COUNT 与翻页;根治需改代码(UNION 逐 biz_type 等值分支 -- + FORCE INDEX(idx_wallet_tx_biz_ct))或授权将 biz_type 改为 varchar 后建覆盖索引。 SET @ddl = IF( (SELECT COUNT(*) FROM information_schema.statistics WHERE table_schema = DATABASE() AND table_name = 'wallet_transaction' AND index_name = 'idx_wallet_tx_tenant_del_biz_price') = 0, 'CREATE INDEX idx_wallet_tx_tenant_del_biz_price ON wallet_transaction (tenant_id, del_flag, biz_type(2), price)', 'SELECT ''skip: idx_wallet_tx_tenant_del_biz_price 已存在'' AS msg'); PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt; -- 5. biz 前导有序索引:让单值 biz_type 等值/单元素 IN 的翻页(page-tk 退款/page-th 退还等) -- 自动走"biz 前缀定位 + 索引序 + price ICP 过滤",跳过无关业务类型的回表; -- 同时为 page-jz/page-cz 的 UNION 改写(代码修复)预置索引。 -- 注意:本索引对 jz/cz(多值 IN)的翻页无直接改善,仅建议需要提速 tk/th 或配合代码修复时执行。 SET @ddl = IF( (SELECT COUNT(*) FROM information_schema.statistics WHERE table_schema = DATABASE() AND table_name = 'wallet_transaction' AND index_name = 'idx_wallet_tx_biz_ct') = 0, 'CREATE INDEX idx_wallet_tx_biz_ct ON wallet_transaction (tenant_id, del_flag, biz_type(2), create_time DESC, user_id DESC, id DESC, price)', 'SELECT ''skip: idx_wallet_tx_biz_ct 已存在'' AS msg'); PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt;