MySQL 里引入原生 JSON 类型之后,很多开发同学把配置、标签、属性扩展一股脑塞进 JSON 字段。这本身没什么问题,但真到查询的时候就会遇到一个很现实的需求:怎么判断一个 JSON 文档里到底有没有某个值?我最早处理这类需求时,第一反应是用 LIKE '%xxx%',结果把 "MySQL优化" 这种标签也匹配进去了,数据一多误报率特别难看。后来切到 JSON_CONTAINS,才发现这是 MySQL 官方给 JSON 场景设计的"包含判断"函数,语义精确,用法也灵活。这篇文章就把 JSON_CONTAINS 从语法到性能、从场景到踩坑一次讲透,适合正在用 MySQL JSON 字段做业务查询的后端开发、数据分析和 DBA。
1. 一个看似简单的搜索需求:JSON 里到底有没有这个值
先看一个最简单的场景。用户表里有一个标签数组字段:
sql复制CREATE TABLE user_profile (
id INT PRIMARY KEY,
name VARCHAR(50),
tags JSON
);
INSERT INTO user_profile VALUES
(1, '张三', JSON_ARRAY('mysql', 'database')),
(2, '李四', JSON_ARRAY('database', 'redis')),
(3, '王五', JSON_ARRAY('MySQL优化'));
运营提了个需求:查出所有打过 mysql 标签的用户。用 LIKE 是这样:
sql复制SELECT * FROM user_profile WHERE tags LIKE '%mysql%';
这条 SQL 会把王五查出来,因为 "MySQL优化" 这个字符串里包含 mysql 子串,但你业务上要的是精确匹配数组元素,不是匹配字符片段。更关键的是,LIKE 无法理解 JSON 数组的结构,万一标签是对象数组 [{"tag":"mysql"}],LIKE 的行为会更加不可控。
换成 JSON_CONTAINS 之后,语义就清楚了:
sql复制SELECT id, name, tags
FROM user_profile
WHERE JSON_CONTAINS(tags, '"mysql"');
结果只返回张三。李四的标签里没有 mysql 元素,王五的标签是 "MySQL优化",不等于 "mysql",都不会命中。这个例子虽然简单,但它是 JSON_CONTAINS 的核心价值:它判断的是"JSON 文档中是否完整包含另一个 JSON 文档",而不是字符串层面的模糊匹配。
JSON_CONTAINS 从 MySQL 5.7 开始支持,适合的场景包括标签筛选、配置项判断、权限角色判断、接口参数校验等。如果你刚开始接触 MySQL JSON,建议先熟悉 JSON_ARRAY、JSON_OBJECT、JSON_EXTRACT,再回来看后面的细节会更好理解。接下来我们把这个函数彻底拆开。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 语法与三个参数的关系:target、candidate、path 缺一不可
2.1 三个参数分别是什么
JSON_CONTAINS 的完整语法是:
sql复制JSON_CONTAINS(target, candidate[, path])
target:被检查的 JSON 文档。可以是 JSON 类型的列、JSON 变量,也可以是JSON_OBJECT(...)之类的表达式结果。candidate:要查找的 JSON 文档片段。注意,它必须是一个合法的 JSON 文本,而不仅仅是一个普通字符串。查字符串mysql,要写成'"mysql"';查数字1,要写成'1';查布尔true,要写成'true'。path:可选,JSON Path 表达式。默认从根节点$开始检查;指定路径后,只在这个路径范围内查找。
返回值是整数:包含返回 1,不包含返回 0。如果 target 本身是 NULL,或者指定的 path 无法定位到值,返回 NULL。三个参数之间的关系,本质上就是"在某个范围内查一个 JSON 片段是否被完整包含"。
举个例子,JSON_CONTAINS('{"name":"张三"}', '"张三"', '$.name') 返回 1,因为 $.name 这个路径下的值是 "张三",candidate 也是 "张三"。如果把路径换成 $.age,返回 NULL,因为路径不存在。
2.2 不同数据结构的命中规则
JSON 里有标量、数组、对象几种基本结构。JSON_CONTAINS 在不同结构下的命中规则不同,我用几条容易测试的结论说明:
target是标量时,candidate 必须是完全相等的同类型标量。JSON_CONTAINS('"abc"', '"abc"')返回 1,JSON_CONTAINS('1', '"1"')返回 0。target是数组,candidate 是标量时,数组中存在一个完全相等的元素就返回 1。JSON_CONTAINS('[1,2,3]', '2')返回 1。target是数组,candidate 也是数组时,candidate 的每一个元素都必须被 target 数组包含,才返回 1。JSON_CONTAINS('[1,2,3]', '[1,2]')返回 1,JSON_CONTAINS('[1,2,3]', '[1,4]')返回 0。target是对象,candidate 是对象时,candidate 的每个键值对都要在 target 中存在,且值相等,返回 1。JSON_CONTAINS('{"a":1,"b":2}', '{"a":1}')返回 1,JSON_CONTAINS('{"a":1,"b":2}', '{"a":2}')返回 0。- 对象作为数组元素时,通常要整体等值才算包含。
JSON_CONTAINS('[{"a":1}]', '{"a":1}')返回 1,但JSON_CONTAINS('[{"a":1,"b":2}]', '{"a":1}')返回 0,因为数组里没有独立的{"a":1}这个元素。
这些规则看起来很琐碎,但决定了一个查询条件写完之后到底能不能命中目标数据。我建议你在本地先建一个测试表,把这几条跑一遍,形成手感。
2.3 path 参数把查询范围收窄
很多 JSON 是嵌套结构。比如用户画像表里有一个 profile 字段:
json复制{
"address": {
"city": "上海",
"district": "浦东"
},
"preferences": {
"theme": "dark",
"notify": ["email", "sms"]
}
}
如果只关心 preferences.theme 是不是 "dark",可以写:
sql复制SELECT *
FROM user_profile
WHERE JSON_CONTAINS(profile, '"dark"', '$.preferences.theme');
路径 $.preferences.theme 把检查范围限定到了 theme 这个键的值,candidate 只需要匹配这个路径下的值。这样能避免同名字段在不同层级造成的干扰。JSON Path 也支持数组下标,比如检查 preferences.notify 数组中第一个元素是不是 "email":
sql复制SELECT *
FROM user_profile
WHERE JSON_CONTAINS(profile, '"email"', '$.preferences.notify[0]');
path 不存在时返回 NULL,不是 0。在 WHERE 里看起来和 0 一样,不会返回数据;但一旦放到反选条件 WHERE NOT JSON_CONTAINS(...) 里,NOT NULL 仍然是 NULL,数据会被意外过滤掉。这个坑后面专门讲。
3. 从场景出发:标签筛选、动态配置、权限校验怎么用
3.1 标签筛选:多条件组合怎么写不累
回到标签场景。业务上的标签需求通常有两类:同时包含多个标签,包含任意一个标签。
同时包含,可以多个 JSON_CONTAINS 用 AND 连接:
sql复制SELECT *
FROM user_profile
WHERE JSON_CONTAINS(tags, '"mysql"')
AND JSON_CONTAINS(tags, '"database"');
也可以用数组作为 candidate,一条 SQL 搞定:
sql复制SELECT *
FROM user_profile
WHERE JSON_CONTAINS(tags, '["mysql", "database"]');
后者表示 ["mysql", "database"] 整体被 tags 包含,也就是两个元素都要存在于 tags 数组中。顺序无所谓,MySQL 判断的是元素集合的包含关系。
包含任意一个标签,在 MySQL 8.0.17 以上用 JSON_OVERLAPS 最直接:
sql复制SELECT *
FROM user_profile
WHERE JSON_OVERLAPS(tags, '["mysql", "database"]');
这个函数只要两个数组有任意一个相同元素就返回 1。5.7 环境只能用多个 OR 组合 JSON_CONTAINS。两种
