-- ============================================================================
|
-- (sub_id, scriptid) 去重 + 唯一索引 —— 根治"同一患者同一题堆出多份重复明细"
|
-- 南华附一(nhfy)2026-10-21
|
--
|
-- 【为什么要做】
|
-- 保存答案/补默认值的 upsert 走 isExistServiceSubtaskDetail,历史上只看 del_flag=0;
|
-- 明细一旦被逻辑删除就查不到 → 下一次保存必然 INSERT,副本越积越多
|
-- (2026-09-08 那 10 位患者各堆了 2~6 份,答案完全相同)。
|
-- 代码侧已改为「忽略删除标记取最新一行 + 更新后 reviveServiceSubtaskDetail 复活」,
|
-- 本脚本再从数据库层加一道唯一约束兜底。
|
--
|
-- 【现状(2026-10-21 实测 smartor_nhfy)】
|
-- 明细总数 346,628;sub_id / scriptid 均无 NULL
|
-- 重复组(同 sub_id+scriptid 多于 1 行):5,389 组,涉及 12,680 行
|
-- 若"每组只留 1 行",需要物理删除 7,291 行(绝大多数是 del_flag=1 的历史副本)
|
--
|
-- 【执行顺序】建议低峰期,按 0 → 4 顺序执行;第 3 步是物理删除,务必先跑第 2 步备份
|
-- 【回滚】第 5 步;删掉的行走 bak_ssd_dup_20261021 还原
|
-- ============================================================================
|
|
|
-- ----------------------------------------------------------------------------
|
-- 【0】评估(只读,随时可跑)
|
-- ----------------------------------------------------------------------------
|
SELECT COUNT(*) AS dup_groups,
|
SUM(rows_cnt) AS rows_in_dup_groups,
|
SUM(rows_cnt - 1) AS rows_to_delete_if_keep_one
|
FROM (SELECT sub_id, scriptid, COUNT(*) AS rows_cnt
|
FROM service_subtask_detail
|
WHERE sub_id IS NOT NULL AND scriptid IS NOT NULL
|
GROUP BY sub_id, scriptid
|
HAVING COUNT(*) > 1) x;
|
|
-- 重复最多的 10 组,看看是不是都长一个样
|
SELECT sub_id, scriptid, COUNT(*) AS rows_cnt,
|
SUM(del_flag = 0) AS live_cnt,
|
COUNT(DISTINCT COALESCE(asrtext, '')) AS distinct_answers
|
FROM service_subtask_detail
|
GROUP BY sub_id, scriptid
|
HAVING COUNT(*) > 1
|
ORDER BY rows_cnt DESC
|
LIMIT 10;
|
|
|
-- ----------------------------------------------------------------------------
|
-- 【1】备份"将被删除"的行(约 7,291 行)
|
-- 保留规则:同一 (sub_id, scriptid) 保留 1 行 —— 优先 del_flag=0,其次 id 最大
|
-- ----------------------------------------------------------------------------
|
-- DROP TABLE IF EXISTS bak_ssd_dup_20261021;
|
-- CREATE TABLE bak_ssd_dup_20261021 AS
|
-- SELECT d.*
|
-- FROM service_subtask_detail d
|
-- JOIN (SELECT sub_id, scriptid,
|
-- SUBSTRING_INDEX(GROUP_CONCAT(id ORDER BY (del_flag = 0) DESC, id DESC), ',', 1) AS keep_id
|
-- FROM service_subtask_detail
|
-- WHERE sub_id IS NOT NULL AND scriptid IS NOT NULL
|
-- GROUP BY sub_id, scriptid
|
-- HAVING COUNT(*) > 1) k
|
-- ON k.sub_id = d.sub_id AND k.scriptid = d.scriptid
|
-- WHERE d.id <> k.keep_id;
|
-- SELECT COUNT(*) FROM bak_ssd_dup_20261021; -- 预期 ≈ 7291
|
|
|
-- ----------------------------------------------------------------------------
|
-- 【2】备份自检(确认备份表里没有"每组都应该保留的那一行")
|
-- ----------------------------------------------------------------------------
|
-- SELECT COUNT(*) AS wrong_rows
|
-- FROM bak_ssd_dup_20261021 b
|
-- JOIN (SELECT sub_id, scriptid,
|
-- SUBSTRING_INDEX(GROUP_CONCAT(id ORDER BY (del_flag = 0) DESC, id DESC), ',', 1) AS keep_id
|
-- FROM service_subtask_detail
|
-- GROUP BY sub_id, scriptid) k
|
-- ON k.sub_id = b.sub_id AND k.scriptid = b.scriptid
|
-- WHERE b.id = k.keep_id; -- 预期 0
|
|
|
-- ----------------------------------------------------------------------------
|
-- 【3】物理删除重复行(每组保留 1 行)
|
-- ----------------------------------------------------------------------------
|
-- DELETE d
|
-- FROM service_subtask_detail d
|
-- JOIN (SELECT sub_id, scriptid,
|
-- SUBSTRING_INDEX(GROUP_CONCAT(id ORDER BY (del_flag = 0) DESC, id DESC), ',', 1) AS keep_id
|
-- FROM service_subtask_detail
|
-- WHERE sub_id IS NOT NULL AND scriptid IS NOT NULL
|
-- GROUP BY sub_id, scriptid
|
-- HAVING COUNT(*) > 1) k
|
-- ON k.sub_id = d.sub_id AND k.scriptid = d.scriptid
|
-- WHERE d.id <> k.keep_id;
|
|
|
-- ----------------------------------------------------------------------------
|
-- 【4】加唯一索引
|
-- ⚠️ 前提:第 3 步已跑过(否则必然 ORA/1062 失败)
|
-- ⚠️ 346k 行的 ADD UNIQUE 会短时锁表,建议低峰执行;
|
-- 如果生产版本 MySQL 支持 online DDL,可用 ALGORITHM=INPLACE, LOCK=NONE
|
-- ----------------------------------------------------------------------------
|
-- ALTER TABLE service_subtask_detail
|
-- ADD UNIQUE KEY uk_ssd_sub_script (sub_id, scriptid),
|
-- ALGORITHM = INPLACE, LOCK = NONE;
|
|
-- 校验
|
-- SHOW INDEX FROM service_subtask_detail WHERE Key_name = 'uk_ssd_sub_script';
|
-- SELECT COUNT(*) AS dup_groups_should_be_0 FROM (
|
-- SELECT sub_id, scriptid FROM service_subtask_detail
|
-- GROUP BY sub_id, scriptid HAVING COUNT(*) > 1) x;
|
|
|
-- ----------------------------------------------------------------------------
|
-- 【5】回滚
|
-- ----------------------------------------------------------------------------
|
-- ALTER TABLE service_subtask_detail DROP INDEX uk_ssd_sub_script;
|
-- INSERT INTO service_subtask_detail SELECT * FROM bak_ssd_dup_20261021;
|
-- DROP TABLE bak_ssd_dup_20261021;
|
|
|
-- ============================================================================
|
-- 【加索引之后必须注意】
|
-- 唯一索引生效后,并发保存同一 (sub_id, scriptid) 时"后到的那一条 INSERT"会抛
|
-- DuplicateKeyException。代码侧已经做了两件事降低风险:
|
-- ① isExistServiceSubtaskDetail 改为"忽略 del_flag 取最新一行"→ 正常保存走 UPDATE,不会 INSERT;
|
-- ② /smartor/subtaskAnswer/saveQuestionAnswer 加了 Redis 幂等占位(60 秒,同一 pid+tid+subid 只处理一次)。
|
-- 但如果将来还有别的写入路径直接 INSERT 明细,建议在插入处 catch DuplicateKeyException 后转 UPDATE。
|
-- ============================================================================
|