一、什么情况下可以用(有限场景)
1. 静态配置表/字典表
-- 系统配置表,数据极少更新
CREATE TABLE sys_config (
config_key VARCHAR(50),
allowed_users VARCHAR(255) -- "admin,user,guest"
);
-- 低频查询:权限检查
SELECT * FROM sys_config
WHERE FIND_IN_SET('admin', allowed_users) > 0;
-- ✅ 可接受:配置表通常<1000行
2. 管理后台查询
-- 管理员手动查询,不频繁
SELECT * FROM audit_log
WHERE FIND_IN_SET('SUCCESS,FAILED', status_list) > 0
LIMIT 100; -- 必须加LIMIT
3. ETL/报表离线处理
-- 数据仓库,夜间批处理
INSERT INTO daily_report
SELECT * FROM raw_data
WHERE FIND_IN_SET('199', category_path) > 0;
-- ✅ 可接受:离线任务,不要求实时
二、核心优缺点分析
优点
1. 实现简单,SQL直观
2. 避免JOIN操作
3. 保持数据紧凑
4. 查询条件灵活
缺点
1. 性能杀手 - 无法使用索引
2. 数据一致性难保证
3. 查询功能有限
4. 维护困难
5. 类型转换问题
三、如果已经在用,如何优化?(MySQL特供方案)
紧急优化方案(不改表结构)
1. 前置过滤条件法
-- ❌ 原查询(全表扫描)
SELECT * FROM orders
WHERE FIND_IN_SET('199', duty_persons) > 0;
-- ✅ 优化后(先用索引过滤)
SELECT * FROM orders
WHERE status = 'ACTIVE' -- 状态索引
AND YEAR(created_at) = 2024 -- 时间范围
AND FIND_IN_SET('199', duty_persons) > 0; -- 最后执行
2. 生成列+索引法(MySQL 5.7+)
-- 1. 为常用搜索值创建生成列
ALTER TABLE orders
ADD COLUMN has_duty_199 TINYINT(1)
GENERATED ALWAYS AS (FIND_IN_SET('199', duty_persons) > 0) STORED,
ADD COLUMN has_duty_200 TINYINT(1)
GENERATED ALWAYS AS (FIND_IN_SET('200', duty_persons) > 0) STORED,
ADD COLUMN has_duty_201 TINYINT(1)
GENERATED ALWAYS AS (FIND_IN_SET('201', duty_persons) > 0) STORED;
-- 2. 创建索引
CREATE INDEX idx_has_duty_199 ON orders(has_duty_199, status);
CREATE INDEX idx_has_duty_200 ON orders(has_duty_200, created_at);
CREATE INDEX idx_composite ON orders(has_duty_199, has_duty_200, status);
-- 3. 查询使用生成列
SELECT * FROM orders
WHERE has_duty_199 = 1
AND status = 'ACTIVE';
3. 前缀索引优化
-- 为逗号分隔字段创建前缀索引
CREATE INDEX idx_duty_prefix ON orders(duty_persons(20));
-- 结合LIKE前缀匹配
SELECT * FROM orders
WHERE duty_persons LIKE '199,%' -- 在开头
OR duty_persons LIKE '%,199,%' -- 在中间
OR duty_persons LIKE '%,199' -- 在结尾
OR duty_persons = '199' -- 唯一值
AND FIND_IN_SET('199', duty_persons) > 0; -- 最终验证
4. 内存表缓存
-- 1. 创建内存表存储热点数据
CREATE TABLE hot_orders (
id INT PRIMARY KEY,
duty_persons VARCHAR(255)
) ENGINE=MEMORY;
-- 2. 定期同步热点数据
INSERT INTO hot_orders
SELECT id, duty_persons
FROM orders
WHERE updated_at > DATE_SUB(NOW(), INTERVAL 7 DAY)
AND status = 'ACTIVE';
-- 3. 查询内存表
SELECT * FROM hot_orders
WHERE FIND_IN_SET('199', duty_persons) > 0;
-- ✅ 内存操作,速度快
查询优化技巧
1. 分页查询优化
-- ❌ 错误分页
SELECT * FROM orders
WHERE FIND_IN_SET('199', duty_persons) > 0
ORDER BY id DESC
LIMIT 100 OFFSET 10000; -- 越来越慢
-- ✅ 优化分页(基于ID范围)
SELECT * FROM orders
WHERE id < 100000 -- 限制扫描范围
AND FIND_IN_SET('199', duty_persons) > 0
ORDER BY id DESC
LIMIT 100;
2. 批量查询优化
-- ❌ 循环查询
SELECT * FROM orders WHERE FIND_IN_SET('199', duty_persons) > 0;
SELECT * FROM orders WHERE FIND_IN_SET('200', duty_persons) > 0;
SELECT * FROM orders WHERE FIND_IN_SET('201', duty_persons) > 0;
-- ✅ 合并查询
SELECT * FROM orders
WHERE FIND_IN_SET('199', duty_persons) > 0
OR FIND_IN_SET('200', duty_persons) > 0
OR FIND_IN_SET('201', duty_persons) > 0;
3. 结果缓存策略
-- 应用层缓存
SET @cache_key = CONCAT('duty_199_result_', DATE_FORMAT(NOW(), '%Y%m%d'));
SET @result = NULL;
-- 检查缓存
SELECT result INTO @result FROM query_cache
WHERE cache_key = @cache_key
AND expiry_time > NOW();
-- 无缓存则查询
IF @result IS NULL THEN
SET @result = (
SELECT GROUP_CONCAT(id)
FROM orders
WHERE FIND_IN_SET('199', duty_persons) > 0
LIMIT 1000
);
-- 写入缓存
INSERT INTO query_cache VALUES (
@cache_key, @result, DATE_ADD(NOW(), INTERVAL 1 HOUR)
);
END IF;
四、MySQL替代方案推荐
方案1:JSON类型(MySQL 5.7+)
-- 1. 使用JSON数组存储
CREATE TABLE orders_json (
id INT PRIMARY KEY,
duty_persons JSON -- 存储[199, 200, 201]
);
-- 2. 插入数据
INSERT INTO orders_json VALUES
(1, '[199, 200, 201]'),
(2, '[201, 202]');
-- 3. 查询优化
-- 方法A:JSON_CONTAINS
SELECT * FROM orders_json
WHERE JSON_CONTAINS(duty_persons, '199');
-- 方法B:生成列+索引
ALTER TABLE orders_json
ADD COLUMN duty_persons_array VARCHAR(255)
GENERATED ALWAYS AS (
REPLACE(REPLACE(JSON_EXTRACT(duty_persons, '$[*]'), '[', ''), ']', '')
) STORED;
CREATE INDEX idx_duty_array ON orders_json(duty_persons_array(50));
方案2:位运算(适合有限选项)
-- 1. 使用BIGINT存储位掩码
CREATE TABLE orders_bitmask (
id INT PRIMARY KEY,
duty_mask BIGINT UNSIGNED
);
-- 2. 定义位映射
-- 位0: 人员1 (1 << 0 = 1)
-- 位1: 人员2 (1 << 1 = 2)
-- 位7: 人员8 (1 << 7 = 128)
-- 位n: 人员n+1 (1 << n)
-- 3. 设置人员199(第199位)
UPDATE orders_bitmask
SET duty_mask = duty_mask | (1 << 198)
WHERE id = 1;
-- 4. 查询包含人员199
SELECT * FROM orders_bitmask
WHERE duty_mask & (1 << 198) > 0; -- ✅ 高效
方案3:全文索引(MySQL 5.6+)
-- 1. 创建全文索引
ALTER TABLE orders
ADD FULLTEXT INDEX idx_duty_fulltext (duty_persons);
-- 2. 全文搜索
SELECT * FROM orders
WHERE MATCH(duty_persons) AGAINST('+199' IN BOOLEAN MODE);
-- 注意:适用于文本搜索,不适用于精确匹配
五、监控与性能分析
1. 慢查询监控
-- 查看FIND_IN_SET相关慢查询
SELECT
query,
exec_time,
rows_examined,
rows_sent
FROM mysql.slow_log
WHERE query LIKE '%FIND_IN_SET%'
AND exec_time > 1.0
ORDER BY exec_time DESC
LIMIT 20;
2. 执行计划分析
-- 查看查询计划
EXPLAIN
SELECT * FROM orders
WHERE FIND_IN_SET('199', duty_persons) > 0
AND status = 'ACTIVE';
-- 关注:
-- type: ALL = 全表扫描
-- key: NULL = 没用索引
-- rows: 扫描行数
3. 性能影响评估
-- 评估函数调用频率
SELECT
DATE_FORMAT(event_time, '%Y-%m-%d %H:00') as hour,
COUNT(*) as total_queries,
SUM(CASE WHEN argument LIKE '%FIND_IN_SET%' THEN 1 ELSE 0 END) as find_in_set_calls,
AVG(CASE WHEN argument LIKE '%FIND_IN_SET%' THEN timer_wait/1000000000 ELSE NULL END) as avg_time_seconds
FROM performance_schema.events_statements_history_long
WHERE db = 'your_database'
GROUP BY DATE_FORMAT(event_time, '%Y-%m-%d %H:00')
ORDER BY hour DESC
LIMIT 24;
六、决策指南
风险评估矩阵
风险等级 | 数据量 | 查询频率 | 响应要求 | 建议措施
-----------------------------------------------------------------
低风险 | < 1万 | < 10次/天| < 1秒 | 监控即可
中风险 | 1-10万 | 10-100次/天| 0.5-2秒 | 需优化
高风险 | > 10万 | > 100次/天| > 2秒 | 立即重构
优化优先级
-- P0(立即处理):核心接口 + 慢查询
WHERE interface_type = '核心功能'
AND avg_response_time > 2.0
AND call_count > 1000;
-- P1(本周优化):重要功能 + 性能下降
WHERE interface_type = '重要功能'
AND avg_response_time > 1.0
AND growth_rate > 0.5; -- 响应时间增长率>50%
-- P2(本月计划):非核心 + 有优化空间
WHERE interface_type = '辅助功能'
AND avg_response_time > 0.5;
技术选型建议
场景 | 推荐方案 | 备注
---------------------------------------------------------------
权限/角色 | 位运算 | 选项固定且少
标签系统 | JSON + 生成列 | 灵活扩展
分类/层级 | 关联表 | 需要复杂查询
搜索功能 | 全文索引 | 文本匹配
配置参数 | 保留FIND_IN_SET | 数据量极小
最后建议
-
新项目:从设计上杜绝逗号分隔存储
-
老项目:
- 先加生成列索引临时优化
- 监控慢查询,优先处理高频热点
- 业务低峰期逐步重构
-
紧急上线:用内存表+缓存兜底
-
长期规划:根据业务特征选择最合适的替代方案
记住:FIND_IN_SET的性能问题是指数级恶化的。10万行时可能1秒,100万行时可能就是10秒+。越早处理,成本越低。