Mysql

关注公众号 jb51net

关闭
首页 > 数据库 > Mysql > MySQL FIND_IN_SET函数

MySQL FIND_IN_SET字符串函数深度解析

作者:安得小学僧-设计模式之美

FIND_IN_SET是MySQL中处理分隔字符串的重要函数,适合处理标签、分类等多值场景,这篇文章主要介绍了MySQL FIND_IN_SET字符串函数的相关资料,文中通过代码介绍的非常详细,需要的朋友可以参考下

函数概述

FIND_IN_SET() 是 MySQL 提供的一个字符串函数,用于在逗号分隔的字符串集合中查找指定值的位置。这个函数在处理某些特定的数据结构时非常有用,但也容易被误用,导致性能问题和逻辑错误。

函数签名

FIND_IN_SET(str, strlist)

返回值

基本语法与用法

基础示例

-- 基本查找
SELECT FIND_IN_SET('b', 'a,b,c,d');        -- 返回: 2
SELECT FIND_IN_SET('e', 'a,b,c,d');        -- 返回: 0
SELECT FIND_IN_SET('a', 'a,b,c,d');        -- 返回: 1

-- 在表查询中使用
SELECT * FROM users 
WHERE FIND_IN_SET('admin', roles) > 0;

-- 用于条件判断
SELECT 
    name,
    CASE 
        WHEN FIND_IN_SET('vip', user_tags) > 0 THEN 'VIP用户'
        ELSE '普通用户'
    END as user_type
FROM users;

边界情况

-- 空字符串和NULL的处理
SELECT FIND_IN_SET('', 'a,b,c');           -- 返回: 0
SELECT FIND_IN_SET('a', '');               -- 返回: 0
SELECT FIND_IN_SET(NULL, 'a,b,c');         -- 返回: NULL
SELECT FIND_IN_SET('a', NULL);             -- 返回: NULL

-- 特殊字符
SELECT FIND_IN_SET('a,b', 'a,b,c');        -- 返回: 0 (查找的是整个 'a,b' 字符串)
SELECT FIND_IN_SET('a,b', 'a,b,a,b,c');    -- 返回: 3 (找到完整的 'a,b')

工作原理

内部实现逻辑

FIND_IN_SET 的工作原理可以理解为以下步骤:

  1. 字符串分割:将 strlist 按逗号分割成多个子字符串
  2. 逐一比较:将 str 与每个子字符串进行精确匹配
  3. 返回位置:如果匹配成功,返回位置索引(从1开始)
-- 等价的实现逻辑(伪代码)
FUNCTION FIND_IN_SET(needle, haystack):
    IF needle IS NULL OR haystack IS NULL:
        RETURN NULL
    
    items = SPLIT(haystack, ',')
    FOR i = 1 TO LENGTH(items):
        IF items[i] = needle:
            RETURN i
    RETURN 0

字符匹配规则

-- 精确匹配,区分大小写
SELECT FIND_IN_SET('A', 'a,b,c');          -- 返回: 0
SELECT FIND_IN_SET('a', 'A,b,c');          -- 返回: 0

-- 不进行模糊匹配
SELECT FIND_IN_SET('ab', 'a,abc,c');       -- 返回: 0
SELECT FIND_IN_SET('abc', 'a,abc,c');      -- 返回: 2

常见陷阱与问题

1. 索引失效问题

最大的陷阱:无法使用索引

-- 这个查询无法使用索引,即使在 user_roles 字段上有索引
SELECT * FROM users 
WHERE FIND_IN_SET('admin', user_roles) > 0;

-- 执行计划显示全表扫描
EXPLAIN SELECT * FROM users 
WHERE FIND_IN_SET('admin', user_roles) > 0;
-- type: ALL (全表扫描)

2. 数据类型陷阱

-- 数值类型的隐式转换
CREATE TABLE test (
    id INT,
    numbers VARCHAR(100)  -- 存储: '1,2,3,4,5'
);

-- 这些查询的结果可能出乎意料
SELECT FIND_IN_SET(1, '1,2,3');        -- 返回: 1 (正确)
SELECT FIND_IN_SET('01', '1,2,3');     -- 返回: 0 (字符串 '01' != '1')
SELECT FIND_IN_SET(1.0, '1,2,3');      -- 返回: 1 (数值转换)

3. 空值和空字符串陷阱

-- 空字符串在集合中的处理
SELECT FIND_IN_SET('', 'a,,c');        -- 返回: 0 (不匹配空元素)
SELECT FIND_IN_SET('a', 'a,,c');       -- 返回: 1
SELECT FIND_IN_SET('c', 'a,,c');       -- 返回: 3

-- 意外的空元素
INSERT INTO tags VALUES ('tag1,,tag3'); -- 中间有空元素
SELECT FIND_IN_SET('tag2', tags);       -- 可能不是期望的结果

4. 逗号字符陷阱

-- 查找包含逗号的字符串
SELECT FIND_IN_SET('a,b', 'a,b,c');    -- 返回: 0 (查找整个 'a,b')
SELECT FIND_IN_SET('hello,world', 'hello,world,test'); -- 返回: 1

-- 数据中意外包含逗号
INSERT INTO categories VALUES ('电子产品,手机,iPhone');
-- 如果某个分类名本身包含逗号,会破坏结构

5. 性能陷阱

-- 大数据量时的性能问题
SELECT COUNT(*) FROM orders 
WHERE FIND_IN_SET('completed', status_history); -- 在百万级数据上很慢

-- 复杂查询中的性能叠加
SELECT * FROM products p
JOIN categories c ON FIND_IN_SET(c.id, p.category_ids)
WHERE FIND_IN_SET('sale', p.tags) > 0; -- 双重性能损失

性能分析

时间复杂度

性能测试对比

-- 创建测试数据
CREATE TABLE performance_test (
    id INT PRIMARY KEY,
    tags VARCHAR(1000),
    tag_id INT,
    INDEX idx_tag_id (tag_id)
);

-- 插入100万条测试数据
-- 方法1:FIND_IN_SET (慢)
SELECT COUNT(*) FROM performance_test 
WHERE FIND_IN_SET('target_tag', tags) > 0;
-- 执行时间: ~5-10秒

-- 方法2:规范化表结构 (快)
SELECT COUNT(DISTINCT pt.id) 
FROM performance_test pt
JOIN product_tags pt2 ON pt.id = pt2.product_id
WHERE pt2.tag = 'target_tag';
-- 执行时间: ~0.01-0.1秒

内存使用

-- FIND_IN_SET 需要在内存中处理整个字符串
-- 对于长字符串会消耗更多内存
SELECT FIND_IN_SET('tag', REPEAT('other_tag,', 10000)); -- 高内存消耗

最佳实践

1. 适用场景

✅ 适合使用的场景:

-- 配置项存储(少量、相对固定的值)
SELECT * FROM system_config 
WHERE FIND_IN_SET('email_notifications', enabled_features) > 0;

-- 临时数据处理
SELECT FIND_IN_SET(@user_role, 'admin,manager,supervisor') as has_permission;

-- 小表的简单标签查询
SELECT * FROM articles 
WHERE FIND_IN_SET('featured', flags) > 0
AND created_date > DATE_SUB(NOW(), INTERVAL 1 MONTH);

❌ 不适合使用的场景:

-- 大表的频繁查询
SELECT * FROM products WHERE FIND_IN_SET(@category, categories); -- 避免

-- 复杂的多条件查询
SELECT * FROM orders 
WHERE FIND_IN_SET('express', shipping_methods)
  AND FIND_IN_SET('paid', status_list); -- 避免

-- 需要统计聚合的场景
SELECT category, COUNT(*) FROM products 
GROUP BY FIND_IN_SET(category, available_categories); -- 避免

2. 优化技巧

-- 使用索引友好的辅助字段
ALTER TABLE products 
ADD COLUMN has_sale_tag BOOLEAN AS (FIND_IN_SET('sale', tags) > 0) STORED,
ADD INDEX idx_has_sale_tag (has_sale_tag);

-- 查询时使用辅助字段
SELECT * FROM products 
WHERE has_sale_tag = 1; -- 可以使用索引

-- 结合其他条件减少扫描范围
SELECT * FROM products 
WHERE category_id = 1  -- 先用索引过滤
  AND FIND_IN_SET('hot', tags) > 0; -- 再用FIND_IN_SET

3. 数据验证

-- 确保数据格式正确
DELIMITER //
CREATE TRIGGER validate_tags_format
BEFORE INSERT ON products
FOR EACH ROW
BEGIN
    IF NEW.tags REGEXP '^[^,]+(,[^,]+)*$' = 0 AND NEW.tags != '' THEN
        SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Invalid tags format';
    END IF;
END//
DELIMITER ;

替代方案

1. 规范化表结构(推荐)

-- 原始设计(不推荐)
CREATE TABLE products_bad (
    id INT PRIMARY KEY,
    name VARCHAR(255),
    category_ids VARCHAR(255) -- '1,3,5,7'
);

-- 规范化设计(推荐)
CREATE TABLE products (
    id INT PRIMARY KEY,
    name VARCHAR(255)
);

CREATE TABLE product_categories (
    product_id INT,
    category_id INT,
    PRIMARY KEY (product_id, category_id),
    FOREIGN KEY (product_id) REFERENCES products(id),
    FOREIGN KEY (category_id) REFERENCES categories(id)
);

-- 查询对比
-- 使用FIND_IN_SET(慢)
SELECT * FROM products_bad 
WHERE FIND_IN_SET('3', category_ids) > 0;

-- 使用JOIN(快)
SELECT DISTINCT p.* FROM products p
JOIN product_categories pc ON p.id = pc.product_id
WHERE pc.category_id = 3;

2. JSON 字段(MySQL 5.7+)

-- 使用JSON存储
CREATE TABLE products_json (
    id INT PRIMARY KEY,
    name VARCHAR(255),
    categories JSON -- ["electronics", "mobile", "smartphone"]
);

-- JSON查询
SELECT * FROM products_json 
WHERE JSON_CONTAINS(categories, '"mobile"');

-- JSON查询可以使用函数索引(MySQL 8.0+)
ALTER TABLE products_json 
ADD INDEX idx_categories ((CAST(categories AS CHAR(255) ARRAY)));

3. 全文索引

-- 对于文本标签搜索
CREATE TABLE articles (
    id INT PRIMARY KEY,
    title VARCHAR(255),
    tags TEXT,
    FULLTEXT(tags)
);

-- 全文搜索
SELECT * FROM articles 
WHERE MATCH(tags) AGAINST('programming' IN BOOLEAN MODE);

4. 位运算方案

-- 对于有限的选项集合
CREATE TABLE user_permissions (
    user_id INT PRIMARY KEY,
    permissions INT -- 使用位运算存储权限
);

-- 权限定义
-- 1: READ (1)
-- 2: WRITE (2)  
-- 4: DELETE (4)
-- 8: ADMIN (8)

-- 检查权限
SELECT * FROM user_permissions 
WHERE permissions & 4 > 0; -- 检查DELETE权限

-- 设置权限
UPDATE user_permissions 
SET permissions = permissions | 8 -- 添加ADMIN权限
WHERE user_id = 123;

实际案例分析

案例1:电商网站商品标签

场景:电商网站需要根据商品标签筛选商品

错误实现

CREATE TABLE products (
    id INT PRIMARY KEY,
    name VARCHAR(255),
    price DECIMAL(10,2),
    tags VARCHAR(500) -- 'hot,sale,new,featured'
);

-- 查询热销商品(性能差)
SELECT * FROM products 
WHERE FIND_IN_SET('hot', tags) > 0
ORDER BY price DESC
LIMIT 20;

正确实现

CREATE TABLE products (
    id INT PRIMARY KEY,
    name VARCHAR(255),
    price DECIMAL(10,2)
);

CREATE TABLE tags (
    id INT PRIMARY KEY,
    name VARCHAR(50) UNIQUE
);

CREATE TABLE product_tags (
    product_id INT,
    tag_id INT,
    PRIMARY KEY (product_id, tag_id),
    FOREIGN KEY (product_id) REFERENCES products(id),
    FOREIGN KEY (tag_id) REFERENCES tags(id)
);

-- 查询热销商品(性能好)
SELECT p.* FROM products p
JOIN product_tags pt ON p.id = pt.product_id
JOIN tags t ON pt.tag_id = t.id
WHERE t.name = 'hot'
ORDER BY p.price DESC
LIMIT 20;

案例2:用户权限系统

场景:检查用户是否具有特定权限

可接受的FIND_IN_SET使用

CREATE TABLE users (
    id INT PRIMARY KEY,
    username VARCHAR(50),
    roles VARCHAR(255) -- 'admin,editor,viewer'
);

-- 小规模用户表,偶尔查询,可以使用
SELECT * FROM users 
WHERE FIND_IN_SET('admin', roles) > 0;

-- 但更好的做法仍然是规范化
CREATE TABLE user_roles (
    user_id INT,
    role_id INT,
    PRIMARY KEY (user_id, role_id)
);

案例3:配置管理

场景:系统配置的启用功能列表

合适的使用场景

CREATE TABLE system_settings (
    id INT PRIMARY KEY,
    setting_key VARCHAR(100),
    setting_value TEXT
);

-- 存储启用的功能列表
INSERT INTO system_settings VALUES 
(1, 'enabled_modules', 'user_management,reporting,notifications');

-- 检查某个模块是否启用
SELECT FIND_IN_SET('reporting', setting_value) > 0 as is_enabled
FROM system_settings 
WHERE setting_key = 'enabled_modules';

总结

核心要点

  1. FIND_IN_SET 不是银弹:它解决特定问题,但不应该是首选方案
  2. 性能影响严重:无法使用索引,大数据量时性能极差
  3. 数据完整性风险:容易出现数据不一致和格式错误
  4. 维护成本高:难以进行复杂查询和数据分析

使用建议

迁移策略

如果已经在使用FIND_IN_SET,可以考虑以下迁移策略:

  1. 渐进式重构:新功能使用规范化设计
  2. 数据迁移:编写脚本将逗号分隔数据迁移到关联表
  3. 性能监控:监控FIND_IN_SET查询的性能影响
  4. 分阶段优化:优先处理性能影响最大的查询

记住:好的数据库设计是性能优化的基础,而FIND_IN_SET往往是设计问题的一个信号。

到此这篇关于MySQL FIND_IN_SET字符串函数的文章就介绍到这了,更多相关MySQL FIND_IN_SET函数内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文