MySQL JSON复杂查询:从等值判断到深度搜索

函数分类速览表

MySQL 的 JSON 函数库堪称数据库界的“瑞士军刀”,功能强大但容易让人眼花缭乱。下面用一张表格 + 代码示例 + 灵魂比喻,帮你彻底搞懂这些函数!

一、函数分类速览表

类别典型函数比喻核心能力
查询解析JSON_EXTRACT, ->, ->>数据“显微镜”精准提取特定字段
修改更新JSON_SET, JSON_REPLACEJSON“装修队”局部修改不伤整体结构
创建构建JSON_OBJECT, JSON_ARRAY乐高积木组装师手动构造JSON对象/数组
类型检查JSON_VALID, JSON_TYPE大家来找茬验证格式合法性,识别数据类型
工具函数JSON_LENGTH, JSON_KEYS数据“体检仪”测量尺寸、提取键名列表
性能优化JSON_CONTAINS, JSON_SEARCH高速扫描仪快速判断包含关系,定位路径

二、重点函数详解

1. 查询解析三剑客

-- 原始数据
SET @user = '{
  "name": "李四",
  "age": 28,
  "address": {"city": "北京", "zip": "100000"},
  "hobbies": ["coding", "篮球"]
}';

-- (1) JSON_EXTRACT(官方版)
SELECT JSON_EXTRACT(@user, '$.address.city'); -- "北京"(带引号)

-- (2) -> 操作符(语法糖)
SELECT @user->'$.hobbies[0]'; -- "coding"(带引号)

-- (3) ->> 操作符(去引号版)
SELECT @user->>'$.name' AS name; -- 李四(纯文本)

避坑指南:

  • 路径表达式严格区分大小写!$.Name$.name 是不同字段
  • 数组越界返回 NULL,不会报错(像极了程序员的隐式容忍)

2. 修改更新四天王(MySQL 8.0+)

-- (1) JSON_SET:无中生有(新增字段)
SET @user = JSON_SET(@user, '$.salary', 20000); 
-- 结果:添加 salary 字段

-- (2) JSON_REPLACE:偷梁换柱(修改现有字段)
SET @user = JSON_REPLACE(@user, '$.age', 29); 
-- 结果:age 从28变为29

-- (3) JSON_REMOVE:断舍离(删除字段)
SET @user = JSON_REMOVE(@user, '$.hobbies[1]'); 
-- 结果:hobbies数组只剩 ["coding"]

-- (4) JSON_ARRAY_APPEND:数组扩容
SET @user = JSON_ARRAY_APPEND(@user, '$.hobbies', '摸鱼'); 
-- 结果:hobbies变成 ["coding", "摸鱼"]

性能警告

  • 修改函数会生成新JSON对象,频繁操作可能触发内存告警(类似Java的String拼接)
  • 修改后的JSON大小不能超过原值!否则直接报错(防止数据膨胀)

3. 构建函数:手动造JSON

-- (1) 造对象:JSON_OBJECT(键值对自动包装)
SELECT JSON_OBJECT('name', '王五', 'age', 30); 
-- 结果:{"name": "王五", "age": 30}

-- (2) 造数组:JSON_ARRAY(元素自动转JSON类型)
SELECT JSON_ARRAY(1, 'text', NOW(), JSON_OBJECT('a', 10)); 
-- 结果:[1, "text", "2024-02-20 12:00:00", {"a": 10}]

-- (3) 合并对象:JSON_MERGE_PATCH(8.0+覆盖式合并)
SELECT JSON_MERGE_PATCH('{"a":1}', '{"a":2, "b":3}'); 
-- 结果:{"a": 2, "b": 3}

冷知识
JSON_OBJECT 的键名如果是数字,会被强制转字符串(如 JSON_OBJECT(1, 'test') 生成 {"1": "test"})

4. 类型检查与工具函数

-- (1) 合法性验证:JSON_VALID
SELECT JSON_VALID('{"name": "张三"}'); -- 1(合法)
SELECT JSON_VALID('{name: "张三"}');   -- 0(键没引号,非法)

-- (2) 类型检测:JSON_TYPE
SELECT JSON_TYPE(@user->'$.age');    -- INTEGER
SELECT JSON_TYPE(@user->'$.name');  -- STRING
SELECT JSON_TYPE(@user->'$.hobbies'); -- ARRAY

-- (3) 键名提取:JSON_KEYS
SELECT JSON_KEYS('{"a":1, "b":2}'); -- ["a", "b"]

-- (4) 长度测量:JSON_LENGTH
SELECT JSON_LENGTH('["a", "b", "c"]'); -- 3(数组长度)
SELECT JSON_LENGTH('{"a":1, "b":2}');  -- 2(对象键数量)

5. 高级搜索函数

-- (1) JSON_CONTAINS:是否包含某值
SELECT JSON_CONTAINS('[1, 2, 3]', '2', '$'); -- 1(存在)

-- (2) JSON_SEARCH:模糊查找路径
SELECT JSON_SEARCH('{"user": {"name": "李四"}}', 'one', '李四');
-- 结果:"$.user.name"(类似文件搜索的定位功能)

性能优化技巧

  • JSON_CONTAINS 查询建立虚拟列索引(否则全表扫描警告!)
  • JSON_SEARCHone/all 参数控制返回第一个还是所有匹配路径

三、函数使用灵魂法则

  1. 路径表达式要写准:
  • 键名带特殊符号用双引号:$."user-name"
  • 数组索引从0开始:$.hobbies[0]
  1. 类型转换要显式:
-- 错误:字符串和数字比较
SELECT * FROM products WHERE attributes->'$.price' = '100'; 

-- 正确:统一类型
SELECT * FROM products 
WHERE CAST(attributes->'$.price' AS UNSIGNED) = 100;
  1. 索引要针对性:
-- 创建虚拟列 + 索引
ALTER TABLE products 
ADD COLUMN color VARCHAR(20) 
  GENERATED ALWAYS AS (attributes->>'$.color'),
ADD INDEX idx_color (color);

四、一道面试题实战

题目:如何将JSON数组 ["a","b","c"] 转换成关系型数据表?

-- 使用 JSON_TABLE 函数(MySQL 8.0+)
SELECT * FROM JSON_TABLE(
  '["a", "b", "c"]',
  '$[*]' COLUMNS (
    id FOR ORDINALITY,  -- 自动生成行号
    value VARCHAR(10) PATH '$'
  )
) AS t;

输出:

idvalue
1a
2b
3c

终极总结
函数选择口诀: “查用->>,改用SET,建用OBJECT,验用VALID
性能口诀: “路径要短,索引要建,类型对齐,少用通配”

五、JSON对象全等判断:当强迫症遇到数据结构

5.1 精确匹配(键顺序敏感)

-- 案例:查找配置完全相同的设备(键顺序必须一致)
SELECT * FROM device_configs 
WHERE config_json = '{"resolution": "1080p", "brightness": 80}';

-- 陷阱警告:以下两个JSON会被认为不同(键顺序不同)
'{"a":1, "b":2}' vs '{"b":2, "a":1}'

5.2 松散全等判断(键顺序无关)

-- 方法:使用JSON_CONTAINS双向包含 + 长度相同
SELECT * FROM device_configs 
WHERE 
  JSON_CONTAINS(config_json, '{"brightness": 80, "resolution": "1080p"}') 
  AND 
  JSON_CONTAINS('{"brightness": 80, "resolution": "1080p"}', config_json)
  AND
  JSON_LENGTH(config_json) = 2; -- 确保没有多余字段

六、数组的“灵魂拷问”式查询

6.1 数组完全相等(顺序敏感)

-- 查找tags数组严格等于["VIP","北京"]的用户(顺序、数量、元素完全一致)
SELECT * FROM users 
WHERE tags_json = CAST('["VIP","北京"]' AS JSON);

6.2 数组包含所有元素(顺序无关)

-- 查找tags包含"VIP"和"北京"的用户(类似AND条件)
SELECT * FROM users 
WHERE 
  JSON_CONTAINS(tags_json, '"VIP"') 
  AND 
  JSON_CONTAINS(tags_json, '"北京"');

6.3 数组包含任意元素(类似OR条件)

-- 查找tags包含"VIP"或"北京"的用户
SELECT * FROM users 
WHERE 
  JSON_CONTAINS(tags_json, '["VIP"]') 
  OR 
  JSON_CONTAINS(tags_json, '["北京"]');

七、嵌套结构的“掘地三尺”查询

7.1 多层级路径查询

-- 查找住在"北京朝阳区"的用户(嵌套对象查询)
SELECT * FROM users 
WHERE address_json->>'$.city' = '北京' 
  AND address_json->>'$.district' = '朝阳区';

7.2 通配符搜索所有层级

-- 查找任意层级包含"error_code":500的日志(递归搜索)
SELECT * FROM service_logs 
WHERE JSON_SEARCH(log_json, 'all', '500', NULL, '$**.error_code') IS NOT NULL;

7.3 深度过滤数组对象

-- 查找订单中有商品ID=100且数量>2的订单(数组对象过滤)
SELECT * FROM orders 
WHERE JSON_EXISTS(
  items_json, 
  '$[*]?(@.product_id == 100 && @.quantity > 2)'
);

八、混合条件综合查询

8.1 JSON字段 + 关系字段联合查询

-- 查找2023年后注册,且扩展信息中device_type="iOS"的用户
SELECT * FROM users 
WHERE 
  register_time > '2023-01-01' 
  AND 
  ext_info->>'$.device_type' = 'iOS';

8.2 多JSON字段关联查询

-- 查找购物车总价>1000且包含"急件"标签的订单
SELECT * FROM orders 
WHERE 
  CAST(cart_info->>'$.total_price' AS DECIMAL) > 1000 
  AND 
  JSON_CONTAINS(tags_json, '"急件"');

8.3 动态条件生成查询

-- 根据前端传入的JSON过滤条件动态查询(PHP示例)
$filters = '{"status":"pending","price":{"$gt":100}}';
$where = [];
foreach(json_decode($filters, true) as $key => $value){
  if(is_array($value)){
    $where[] = "data_json->>'$.$key' > ".$value['$gt'];
  }else{
    $where[] = "data_json->>'$.$key' = '$value'";
  }
}
$sql = "SELECT * FROM products WHERE ".implode(' AND ', $where);

九、性能优化黑科技

9.1 虚拟列 + 索引加速

-- 为常用查询条件创建虚拟列索引
ALTER TABLE users 
ADD COLUMN city VARCHAR(20) 
  GENERATED ALWAYS AS (address_json->>'$.city'),
ADD INDEX idx_city (city);

9.2 函数索引(MySQL 8.0+)

-- 直接为JSON路径表达式创建索引
CREATE INDEX idx_price ON products ((CAST(data_json->>'$.price' AS DECIMAL)));

9.3 查询重写优化

-- 原查询(性能差)
SELECT * FROM logs 
WHERE JSON_EXTRACT(log_data, '$.request.time') > '2023-01-01';

-- 优化后(提取时间到独立字段 + 索引)
ALTER TABLE logs ADD COLUMN request_time DATETIME 
  GENERATED ALWAYS AS (JSON_UNQUOTE(JSON_EXTRACT(log_data, '$.request.time')));
CREATE INDEX idx_request_time ON logs(request_time);

十、经典踩坑案例

10.1 隐式类型转换陷阱

-- 错误:字符串与数字比较导致索引失效
SELECT * FROM products 
WHERE data_json->>'$.id' = 100; -- $.id值是字符串"100"

-- 正确:显式类型转换
SELECT * FROM products 
WHERE CAST(data_json->>'$.id' AS UNSIGNED) = 100;

10.2 通配符滥用灾难

-- 错误:左模糊查询全表扫描
SELECT * FROM articles 
WHERE content_json->>'$.text' LIKE '%重要通知%';

-- 正确:使用全文索引或专用搜索引擎(如Elasticsearch)

10.3 大JSON修改雪崩

-- 错误:频繁更新大JSON字段导致IO飙升
UPDATE user_activities 
SET log_json = JSON_ARRAY_APPEND(log_json, '$', '新事件') 
WHERE user_id = 1001;

-- 正确:拆分成关系表或分片存储

十一、超硬核面试题

题目:如何高效实现JSON数组的交集查询?
示例:查找tags数组同时包含["VIP","北京","90后"]的用户
参考答案

-- 方法1:JSON_CONTAINS链式调用
SELECT * FROM users 
WHERE 
  JSON_CONTAINS(tags_json, '"VIP"') 
  AND JSON_CONTAINS(tags_json, '"北京"')
  AND JSON_CONTAINS(tags_json, '"90后"');

-- 方法2:利用JSON_TABLE展开后统计(MySQL 8.0+)
SELECT user_id 
FROM users, JSON_TABLE(
  tags_json,
  '$[*]' COLUMNS(tag VARCHAR(10) PATH '$')
) AS tags 
WHERE tag IN ('VIP', '北京', '90后')
GROUP BY user_id
HAVING COUNT(DISTINCT tag) = 3;
关于我
loading