Files
group_fqcd_jr/alembic/versions/20260911_risk_rule_index.py
lzf_0626 df96275ab5 给 trigger_rule_codes 加 JSON 多值索引;登记 P3 处理结果(docs/25 P3 #21)
实测确认 #21 描述准确:fin_risk_alert 只有 11 个普通 BTREE 索引 + 主键 + alert_no
唯一键,规则命中的 JSON_CONTAINS 查询 EXPLAIN 为 type=ALL、possible_keys=NULL,
即全表扫。MySQL 8.0.27 支持多值索引,故新增迁移 20260911_risk_rule_index:

    ADD INDEX idx_fin_risk_alert_trigger_rule_codes
      ((CAST(`trigger_rule_codes` AS CHAR(16) ARRAY)))

迁移幂等(先查 information_schema.STATISTICS),upgrade/downgrade 往返已验证。
生效后 EXPLAIN 变为 access_type=range 且 key 命中该索引,原始证据留档在
docs/evidence/risk-index-probe.json(由 tools/probe_risk_index.py 生成,只读探查)。

只解决一半,另一半如实记为限制:若干 like(f"%{keyword}%") 全表扫无法用 B-tree 索引,
根治需全文索引 + 中文分词组件(部署依赖),本轮不做。

revision 名刻意压到 32 字符以内 —— alembic_version.version_num 是 VARCHAR(32),
超长会在写版本号时报 1406,而 DDL 是非事务的,那时索引已经建好了。

docs/25 追加"P3 处理结果"表,逐条登记 17-25 的状态:#19 是协议级重做(keyset 分页)
不单方面改,#21 部分修复,#25 前半段不成立,其余已修。
2026-09-11 14:19:05 +08:00

81 lines
3.6 KiB
Python
Raw Permalink 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}`")