先给出几个核心结论:ThinkPHP 6.0 本身并不干涉 MySQL 的索引机制,而 JSON 字段的查询性能,完全取决于 MySQL 版本自身的能力。版本最低要求为 5.7.8,若想更加稳妥,强烈建议直接升级到 8.0.13 及以上版本。TP6 仅负责生成标准 SQL 语句,真正起到决定性作用的,是你为 JSON 内部字段所设计的索引策略。

因此,操作思路非常清晰:先搞清楚 MySQL 能提供哪些能力,再考虑 TP6 如何编写代码。
明确 JSON 字段无法直接建立索引
MySQL 不允许直接对 JSON 整列创建 B+Tree 索引。例如 CREATE INDEX idx_json ON table(json_col) 这样的写法看似正确,但实际执行 WHERE json_col->>'$.key' = 'val' 这类查询时,执行计划依然会提示:全表扫描。简而言之,索引并未生效。
必须将目标键值“提取”出来,转化为普通列,再对其建立索引。主流方案有两种:
- 虚拟生成列(MySQL 5.7.8+):不占用磁盘空间,查询时实时计算,适合读多写少、计算量轻的字段。
- 存储生成列(MySQL 5.7.8+):写入时即计算并落盘,查询速度更快,但会增加存储和写入开销。
利用虚拟列 + 索引实现高效查询
假设你有一张 TP6 中常见的日志表 user_logs,其中包含一个 JSON 字段 extra,结构类似 {"action":"login","ip":"192.168.1.100","device":"mobile"}。如果想按 action 快速筛选,首先需要在 MySQL 中执行以下操作:
ALTER TABLE user_logs ADD COLUMN v_action VARCHAR(32) GENERATED ALWAYS AS (JSON_UNQUOTE(JSON_EXTRACT(extra, '$.action'))) VIRTUAL; CREATE INDEX idx_v_action ON user_logs(v_action);
之后在 TP6 中正常查询即可命中索引:
// TP6 查询写法(等价于 SELECT * FROM user_logs WHERE v_action = 'login')
Db::name('user_logs')->where('v_action', 'login')->select();
这里有一个关键点:v_action 是新增的普通字段名,而非 JSON 路径。TP6 不识别 ->> 语法,因此不能写成 where('extra->>''$.action'', 'login')——那样会退化为全表扫描,等于白费功夫。
MySQL 8.0.17+ 支持多值索引(数组场景)
如果 JSON 字段存储的是数组,比如 "tags": ["php", "mysql", "tp6"],需要查询“包含某个 tag”的记录,传统方式很难利用索引。MySQL 8.0.17 起支持多值索引:
ALTER TABLE posts ADD COLUMN v_tags VARCHAR(255) GENERATED ALWAYS AS (JSON_EXTRACT(tags, '$[*]')) STORED; CREATE INDEX idx_tags ON posts(v_tags) USING BTREE;
或者使用函数索引(8.0.13+),写法更简洁:
CREATE INDEX idx_tag_search ON posts((CAST(JSON_EXTRACT(tags, '$[*]') AS CHAR(255))));
查询时结合 JSON_CONTAINS(tags, '"mysql"'),配合该索引可走 range 扫描,性能远优于无索引时的全表遍历。
TP6 开发中需要避开的常见陷阱
- 不要在
where()中直接使用->>表达式:TP6 会原样拼入 SQL,MySQL 无法利用索引。 - 避免高频更新大 JSON 文档:每次更新都会触发生成列重新计算,尤其是存储列,会加重 I/O 压力。
- 数组元素查询慎重使用
JSON_CONTAINS:其内部是字符串匹配,大数据量下仍可能较慢;优先考虑拆分为关联表或使用多值索引。 - 字段长度预估要留有余量:
VARCHAR(32)若实际值超长会被截断,导致索引失效或查不到数据。
归根结底,TP6 只是一个透明通道。JSON 查询是否高效,取决于你是否在 MySQL 层将路径值“具象化”为可索引的列,并配置好对应的索引。操作本身并不复杂,但很容易被忽略。只要这一步做到位,性能和体验都会有质的提升。
