Files
group_fqcd_jr/alembic/versions/20260911_risk_rule_index.py
T
张胜宇 e239eb778b docs: 品牌全量口径统一为「南方基金」+ 作废文档清理
1) 客服 Agent 四份交付文档 + 构建脚手架:品牌由包装占位 XX科技 / 旧名 南方财富
   统一为南方基金(热线 400-889-8899 / 官网 nffund.com),系统名改为「智能服务系统」;
   同步追加 §0.4 修订记录行,工程记录行保留原占位字面以支撑硬编码扫描验收。
2) 开发文档:清理 28 份已作废/残留文档(14 份移出归档 + 14 份仓库副本),
   新增《文档规整方案与开发前待决事项-2026-09-17》。
3) 客服agent 四份交付文档首次纳入本分支。
2026-09-17 15:15:22 +08:00

81 lines
3.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""给 `fin_risk_alert.trigger_rule_codes` 补 JSON 多值索引(docs/25 P3 #21)。
修订原因:规则命中查询走的是 `JSON_CONTAINS(trigger_rule_codes, '"RW-018"')`
(`risk_repository.py` 的 `contains`),而 `docs/25` 第 21 条实测该表**没有**任何可用于
该表达式的索引 —— `information_schema.STATISTICS` 里 `fin_risk_alert` 只有 11 个普通
BTREE 索引(`status`、`customer_id`、`created_at` 等)+ 主键 + `alert_no` 唯一键,
`EXPLAIN` 显示 `type=ALL`、`possible_keys=NULL`,即每次按规则码筛选都是全表扫描。
为什么用**多值索引**而不是普通索引:`trigger_rule_codes` 是 `json` 列,存的是数组
(实测取值形如 `["RW-007", "RW-002", "RW-012"]`)。MySQL 8.0.17 起支持在 JSON 数组上
建多值索引,且优化器在 `JSON_CONTAINS` / `MEMBER OF` / `JSON_OVERLAPS` 上可以使用它 ——
这正是本条要解决的那个谓词。当前实例版本 8.0.27,满足条件。
`CAST(... AS CHAR(16) ARRAY)` 中的 16:规则码是 `RW-###`(5 字符),留足余量而不至于让
索引项过大。**注意**这不是"截断匹配"—— 多值索引要求列上所有取值都能装进声明长度,
否则 `ALTER` 会直接失败(报 3903),而不是悄悄少索引几行。
前置条件已在 `tools/probe_risk_index.py` 里验证并留档
(`docs/evidence/risk-index-probe.json`):3 行数据全部是 JSON 数组,
`non_array_rows = 0`。
**本条只解决一半**:另一处索引失效是若干 `like(f"%{keyword}%")` 全表扫 —— 前后都有
通配符的模糊匹配在 B-tree 上无法索引,唯一出路是全文索引(中文需要分词组件,属于部署
依赖)。因此这里**不**假装解决它,只把它记为已知限制,见迁移提交说明与 `docs/25`。
基线合规(`AGENTS.md` 第 2/3/4 条):本迁移只**新增一个索引**,不建表、不加列、不改列,
不重命名、不删除任何已有表或字段,也不改变任何已有字段的类型、可空性与业务含义。
`docs/00-新数据库基线设计.md` 未修改。
幂等性:先查 `information_schema.STATISTICS` 再决定是否 `ALTER`,重复执行不会报 1061。
`downgrade` 与之对称,回退后结构与迁移前完全一致。
"""
from sqlalchemy import text
from sqlalchemy.engine import Connection
from alembic import op
# revision 名必须 ≤ 32 字符:`alembic_version.version_num` 是 VARCHAR(32),
# 超长会在写版本号时报 1406 Data too long —— 而 DDL 是非事务的,那时索引已经建好了。
revision = "20260911_risk_rule_index"
down_revision = "20260910_drop_review_separation"
branch_labels = None
depends_on = None
TABLE = "fin_risk_alert"
INDEX = "idx_fin_risk_alert_trigger_rule_codes"
EXPRESSION = "CAST(`trigger_rule_codes` AS CHAR(16) ARRAY)"
def _index_exists(bind: Connection) -> bool:
found = bind.execute(
text(
"""
SELECT 1
FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = :table
AND INDEX_NAME = :name
LIMIT 1
"""
),
{"table": TABLE, "name": INDEX},
).first()
return found is not None
def upgrade() -> None:
bind = op.get_bind()
if _index_exists(bind):
return
# 表名/索引名/表达式都是本模块常量,不含外部输入。
op.execute(f"ALTER TABLE `{TABLE}` ADD INDEX `{INDEX}` (({EXPRESSION}))")
def downgrade() -> None:
bind = op.get_bind()
if not _index_exists(bind):
return
op.execute(f"ALTER TABLE `{TABLE}` DROP INDEX `{INDEX}`")