Przeglądaj źródła

perf(mdp): 优化销售订单标准层当前态清理迁移

1.0.521 的孤儿清理原本把「取该租户最新一次 sync_batch_id」写成逐行
相关子查询,EXPLAIN 实测最内层是 DEPENDENT SUBQUERY + Using filesort,
即每条 mdp_std_so 行都要对贴源层重排一次。共享库上该判据单独取数 51.8s,
DELETE 本体 123.554s,随贴源增长会撞上
AutoVersionUpdate.MigrationCommandTimeoutSeconds = 600s,
重演 1.0.517 的「启动 → 重试 → 超时 → 停机」死循环。

改法只动查询形状,不动业务判据:把贴源侧「订单行」提成物化一次的 CTE,
「每租户最新批次」与「最新批次含哪些 biz_key」都从这份结果里取。
EXPLAIN 在 8.0.31 与 8.0.44 上计划一致:mdp_stg_so 只扫一次,
CTE 以 ref + auto_key 复用,DEPENDENT SUBQUERY 与逐行 filesort 消失。

取最新批次用 ROW_NUMBER() 而非 GROUP_CONCAT + SUBSTRING_INDEX:
后者要求拼接串不超过 group_concat_max_len(实测 1024,而单租户最坏
414 行 x 45 字符约 18630),必然尾部截断并产生 warning 1260;
虽然只取首元素、结果仍正确,但不应让删除语句的正确性依赖截断位置。

旧写法的 EXISTS 安全闸门未丢:贴源无行的租户在 lb2 里不产生分组行,
JOIN 天然把该租户的标准层整体排除在删除范围外。

等价性与性能实测:
  待删集合 old = new = 386,only_old = 0,only_new = 0,key_mismatch = 0
  DELETE 本体 123.554s -> 0.865s;完整 migration 146.267s -> 约 12-15s
  迁移后 orphans = 0,phantom hits = 0,
  源 <-> mdp_std_so 40/40 逐行一致(only_source/only_mdp/field_mismatch 均 0)

未对 mdp_stg_so 增加索引:该表为 S1/S5/S6/S7 共用贴源表,
索引属共享 MDP 优化,另行决策,不随本次发布。
YY968XX 1 dzień temu
rodzic
commit
442b3fd283
1 zmienionych plików z 38 dodań i 14 usunięć
  1. 38 14
      server/Admin.NET.Web.Entry/UpdateScripts/1.0.521.sql

+ 38 - 14
server/Admin.NET.Web.Entry/UpdateScripts/1.0.521.sql

@@ -49,22 +49,46 @@ UPDATE `mdp_entity`
 -- ── ② 一次性清除标准层已沉淀的孤儿 ──────────────────────────────────────────────────
 -- 判据:该 (tenant_id, source_biz_key) 不在贴源层「该租户最新一次 sync_batch_id」里。
 --
--- ⚠️ 安全闸门 —— `EXISTS (该租户在贴源层确实有数据)`
+-- ⚠️ 安全闸门 —— 「该租户在贴源层确实有数据」
 --    若某租户在 mdp_stg_so 里一行都没有(从未同步 / 贴源被清过),
---    「最新批次」子查询会返回 NULL,NOT EXISTS 对该租户的每一行都成立,
---    没有这道闸门就会把该租户的标准层整个删空。这里宁可不删,也不冒清空的风险。
+--    它在下面的 lb2 里不会产生分组行,JOIN 直接把该租户的标准层整体排除在删除范围外。
+--    这与旧写法的 `EXISTS (...)` 闸门等价,但由 JOIN 天然承担,不必单列一个子查询。
+--    这里宁可不删,也不冒清空的风险。
+--
+-- ── 为什么用派生表而不是逐行相关子查询(2026-09-08 性能修复)─────────────────────────
+-- 原写法把「取该租户最新批次」写成**每一行都重算一次**的相关子查询:
+--   NOT EXISTS(... lb.sync_batch_id = (SELECT ... ORDER BY sync_time DESC, id DESC LIMIT 1))
+-- EXPLAIN 实测(共享库):g 全表扫 38722 行、lb 全表扫 38722 行,
+-- 最内层 x 是 DEPENDENT SUBQUERY + Using filesort —— 即「每条 std 行都对贴源做一次排序」。
+-- 只读实测该判据取数 51.8s / 386 行;一旦贴源继续增长就会撞上
+-- AutoVersionUpdate.MigrationCommandTimeoutSeconds = 600s,重演 1.0.517 的启动死循环。
+--
+-- 改法只动查询形状,不动业务语义:把贴源侧「订单行」这一段提成 CTE `stg` 物化一次,
+-- 「每租户最新批次」与「最新批次里有哪些 biz_key」都从这一份物化结果里取。
+-- EXPLAIN 实测(共享库 8.0.31 与本地 8.0.44 计划一致):mdp_stg_so 只被扫一次,
+-- CTE 被两处以 ref + auto_key 复用,DEPENDENT SUBQUERY 与逐行 filesort 全部消失。
+--
+-- 取「最新批次」用 ROW_NUMBER() 而不是 GROUP_CONCAT + SUBSTRING_INDEX:
+-- 后者要求整个拼接串不超过 group_concat_max_len(实测该值 1024,
+-- 而单租户最多 414 行 × 45 字符 ≈ 18630 字符,必然触发尾部截断并产生 warning 1260)。
+-- 虽然我们只取第一个元素、截断发生在尾部因而结果仍正确,
+-- 但让一条删除语句的正确性依赖「截断位置」太脆弱。ROW_NUMBER 是精确的,没有这个前提。
+-- 等价性已实测:新旧两种写法在共享库与本地库上逐租户取到的 sync_batch_id 完全一致
+-- (mismatch = 0),且待删行集合逐行相同(386 / 386,only_old = 0,only_new = 0)。
+WITH stg AS (
+  SELECT `tenant_id`, `source_biz_key`, `sync_batch_id`,
+         ROW_NUMBER() OVER (PARTITION BY `tenant_id`
+                            ORDER BY `sync_time` DESC, `id` DESC) AS `rn`
+    FROM `mdp_stg_so`
+   WHERE `source_table` = 'crm_seorderentry'
+)
 DELETE s FROM `mdp_std_so` s
+  JOIN (SELECT `tenant_id`, `sync_batch_id` AS `latest_batch`
+          FROM stg WHERE `rn` = 1) lb2
+    ON lb2.`tenant_id` = s.`tenant_id`
  WHERE s.`source_table` = 'crm_seorderentry'
-   AND EXISTS (SELECT 1 FROM `mdp_stg_so` g
-                WHERE g.`source_table` = 'crm_seorderentry'
-                  AND g.`tenant_id` = s.`tenant_id`)
    AND NOT EXISTS (
-        SELECT 1 FROM `mdp_stg_so` lb
-         WHERE lb.`source_table` = 'crm_seorderentry'
-           AND lb.`tenant_id` = s.`tenant_id`
+        SELECT 1 FROM stg lb
+         WHERE lb.`tenant_id` = s.`tenant_id`
            AND lb.`source_biz_key` = s.`source_biz_key`
-           AND lb.`sync_batch_id` = (SELECT x.`sync_batch_id` FROM `mdp_stg_so` x
-                                      WHERE x.`source_table` = 'crm_seorderentry'
-                                        AND x.`tenant_id` = s.`tenant_id`
-                                      ORDER BY x.`sync_time` DESC, x.`id` DESC
-                                      LIMIT 1));
+           AND lb.`sync_batch_id` = lb2.`latest_batch`);