写在前面

工作当中遇到了一些慢查询,通常我遇到的慢查询基本都是没有命中索引或者全表扫描了

这一次遇到的比较特别

查询语句当中大量出现find_in_set函数,这个函数我平时接触的比较少

字面意思解释应该是在集合当中查找的函数

FIND_IN_SET

含义

FIND_IN_SET() 是 MySQL 中用于在逗号分隔的字符串列表中查找特定值位置的函数

也就是数据表的某一个字段,比如user_ids这样的字段,类型当然是var_char

存储的是一些以id和逗号拼起来的字符串,就像这样

1,2,3,4,5,8,9,15

find_in_set()函数怎么用呢?

比如,我需要查看评论表当中包含user_id=1的评论,但是这个评论表没有user_id的单独字段,而是user_ids这样的字符串字段,就需要这样写SQL语句了

SELECT
    *
FROM
    user_reviews
WHERE
    FIND_IN_SET('1', user_reviews.user_ids);

这样就可以实现这个评论数据的查询了。

劣势

这个函数其实很差劲,在小数据量的时候不明显,甚至可以减少工作量

但是数据量一大,并发一多,就会死得很惨!

性能较差

  1. 全表扫描:无法利用索引,需逐行检查字段。
  2. O (n) 复杂度:列表越长,查询越慢。

数据结构不规范

  1. 违反数据库范式,不利于数据维护(如增删标签需修改整个字符串)。
  2. 无法单独对标签建立索引。

功能受限

  1. 仅支持逗号分隔,无法处理其他分隔符或嵌套结构。
  2. 无法直接排序或聚合标签。

解决方案

方案其实有不少,但是都需要做一个很大的改动

毕竟当时偷了多少懒,现在就得还多少……

出来混迟早要还的

如果要从这些方案当中选一个的话,我认为是选方案一:拆表,是最合适的,后续兼容也更方便

1. 拆表,多表关联

方案来说,就是将原本的关联字段删除

创一个新的表,将关联字段和主键放进去,查询的时候连表查询或提前缓存查询。

比如这里的user_idreview_id放到新表里

后续的方案三可以并行,减少连表的时间损耗。

2. 改造为json结构

这种方案有局限性,MySQL5.7+才支持JSON格式的结构,旧版本直接就麻爪了

而且JSON格式比起拆表的方式性能是略低的,JSON也无法直接用作连表条件

方式如下:

CREATE TABLE articles (
  id INT PRIMARY KEY,
  title VARCHAR(100),
  tags JSON,  -- 存储 ["tag1", "tag2"]
  INDEX (tags)  -- MySQL 5.7+ 支持 JSON 索引
);

-- 查询包含 'mysql' 标签的文章
SELECT *
FROM articles
WHERE JSON_CONTAINS(tags, '"mysql"');

3. 增加多重缓存机制,减少直接数据库查询

采用redismongoDBMemachenosql的方式,提前将相关的条件,常用的查询方式进行存储

请求先进这类数据库进行查询,当缓存穿透时,才进到MySQL里查询

使用内存检索的方式,降低查询耗时,减少MySQL参与查询

4. 全文索引方式

这种方式类似于直接查询关键字

实现如下:

ALTER TABLE articles ADD FULLTEXT(tags);

-- 使用 MATCH AGAINST 查询
SELECT *
FROM articles
WHERE MATCH(tags) AGAINST('mysql' IN NATURAL LANGUAGE MODE);

但是要注意,这种方式有很大的隐患,毕竟查询4,出来45也是合理的

毕竟是关键字匹配,而且还涉及到全表扫描的问题,需要权衡清楚。

5. 引入三方库,如ES建立更多索引查询

这种方式主要是在大数据大体量下才需要,本质其实跟第一种有些类似

给表中的这个ids字段建立各种索引,方便查询,但ES主要用处是在更复杂的json格式数据上

颇有种杀鸡用牛刀的感觉了。

最后修改:2025 年 07 月 25 日
如果您对各种技术博客文章感兴趣,欢迎关注拓行公众号,分享各种专业技术知识~