相关文章推荐

我们在很多项目开发中 ,可能会用到多个标签查询一条数据,譬如一条数据有多个用户维护,需要我们按用户id 查询出所属的数据信息,又如 一些文章我们添加了tag标记,我需要把这篇文章相关的其他文章匹配出来,这个时候们就需要用到标签查询,而在这里我主要介绍几种标签查询的比较。

首先我以一个例子来描述这个数据结构的问题。

测试版本:mysql 5.7  deepin 15.6

测试软件: Mysql Workbench

这里我以一个 product 表,表中可以有多个tag 标记结构如下:

product
id   | tags  | price
-----+-------+------
1    | 2,3,4 | 300
2    | 3,4,5 | 400
3    | 4,5,6 | 350


我们需要查询 标签 tag 3 的时候找到 id  1 2 的数据,或者 查询 tag 5 的时候找到 id 2 3 的数据。

这个时候聪明的小朋友们可能就会立马想到 一个关键字  FIND_IN_SET ,是的 这里也会讲到 FIND_IN_SET。

但是有些朋友说 这样不是应该用一个 关联关系表来描述吗,也就是说有如下这个结构的数据出现:

关联关系表 prodoct_tags 就由此被引进了项目。

product_tags
id | tagId | productId
---+-------+----------
1  | 2     | 1
2  | 3     | 1
3  | 3     | 2
4  | 4     | 1
5  | 4     | 2
6  | 4     | 3
7  | 5     | 2
8  | 5     | 3
9  | 6     | 3

这里 可以使用两种方式进行关联,如下:

SELECT * FROM product A where EXISTS(SELECT 1 FROM product_tags WHERE tagId=1000 AND productId=A.Id);
SELECT * FROM product A  LEFT JOIN product_tags ON tagId=1000 AND productId=A.Id;


本文也会讨论比较这种方式,但是数据如果很大,比如 100万数据,反正我是跑了10分钟 还没结束自己手动结束了,聪明的同学可能又会说 干嘛不加索引,是的 应该加一个 tag_pro_index 索引,也就是做一个组合索引 tagId,productId 一起的组合索引。

为了大家能够自己演示,我已经准备好了生成 prodcut 的代码。

生成 100万条数据 代码如下:

请使用命令行模式运行(这样你可以实时看到进度)

ini_set("max_execution_time", "300"); ini_set('memory_limit', '256M'); $sqli = new mysqli('127.0.0.1', 'root', '123456', 'mytest1', 3306); $sqli->query("SET NAMES utf8"); $sqli->query('start transaction'); ob_implicit_flush(1); //生成产品表 $sqli->query('CREATE TABLE IF NOT EXISTS `product` ( `id` int(11) NOT NULL AUTO_INCREMENT, `tags` varchar(255) DEFAULT \'\', `price` int(11) DEFAULT NULL, `addtime` datetime DEFAULT NULL, `updatetime` datetime DEFAULT NULL, FULLTEXT (tags), PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; //设置生成大小 $size = 1000000; echo '---生成产品数据' . PHP_EOL; $stmt = $sqli->prepare('INSERT INTO product(`tags`,`price`,`addtime`,`updatetime`) VALUE (?,?,?,?)'); $stmt->bind_param("siss", $t2name, $price, $addtime, $updatetime); for ($i = 0; $i < $size; $i++) { // echo $i . PHP_EOL; if ($i % 10000 == 0) { if ($i != 0) { $sqli->query('commit'); $sqli->query('begin'); $p = floor($i * 100 / $size); echo $i . '/' . $size . '---' . $p . '%' . PHP_EOL; $tags = []; $tagLen = mt_rand(3, 10); for ($j = 0; $j < $tagLen; $j++) { $tags[] = mt_rand(1, 10000); $t2name = join(',', $tags); $price = mt_rand(1, 10000); $addtime = date('Y-m-d H:i:s', mt_rand(time() - 86400 * 365, time())); $updatetime = date('Y-m-d H:i:s', mt_rand(time() - 86400 * 365, time())); $stmt->execute(); $sqli->query('commit'); $stmt->close(); echo '完成' . PHP_EOL;


如果你也想比较 使用关联表的,我也准备了一份生成关联表的php 代码:

需要先生成 product 表:

ini_set("max_execution_time", "300"); ini_set('memory_limit', '256M'); $sqli = new mysqli('127.0.0.1', 'root', '123456', 'mytest1', 3306); $sqli->query("SET NAMES utf8"); $sqli->query('start transaction'); //产品对应的标签表 $sqli->query('CREATE TABLE IF NOT EXISTS `product_tags` ( `id` int(11) NOT NULL AUTO_INCREMENT, `tagId` int(11) NOT NULL DEFAULT \'0\', `productId` int(11) NOT NULL DEFAULT \'0\', PRIMARY KEY (`id`), KEY `tag_pro_index` (`tagId`,`productId`) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8; echo '---生成产品标签数据' . PHP_EOL; $stmt = $sqli->prepare('INSERT INTO product_tags(`tagId`,`productId`) VALUE (?,?)'); $stmt->bind_param("ii", $tagId, $productId); $result = $sqli->query('SELECT COUNT(*) FROM product'); $count = $result->fetch_row()[0]; $pages = ceil($count / 5000); for ($idx = 0; $idx <= $pages; $idx++) { echo $idx . PHP_EOL; $sqli->query('begin'); $p = floor($idx * 100 / $pages); echo $idx . '/' . $pages . '---' . $p . '%' . PHP_EOL; $start = $idx * 5000; $prolist = $sqli->query('SELECT id,tags FROM product limit ' . $start . ',5000')->fetch_all(MYSQLI_ASSOC); //print_r($prolist); foreach ($prolist as $item) { $productId = $item['id']; $tags = explode(',', $item['tags']); foreach ($tags as $tag) { $tagId = intval($tag); $stmt->execute(); $sqli->query('commit'); $stmt->close(); echo '---生成产品标签数据' . PHP_EOL;


数据生成可能需要一些时间,大概是5-10分钟这样,如果你机器够快(主要是硬盘 SSD )

下面 我们在真实测试一下
1  使用 FIND_IN_SET
代码:

SELECT * FROM product WHERE FIND_IN_SET('1000',tags) ;
-- 耗时 0.061秒   我机器比较快吧,还能接受

因为没有办法用到索引,所有全表扫描,但是速度不错 0.061 就搞定了。
还有两种方式可以查询,比如 正则查询 和 like 查询:

SELECT * FROM product WHERE tags REGEXP '(^|,)1000(,|$)'; 
--耗时 0.057s
SELECT * FROM product WHERE CONCAT(',',tags,',') LIKE '%,1000,%';
--耗时 0.098s

FIND_IN_SET 也相差不远。

这个时候可能有些朋友迫不及待的需要我测试一下  链表查询的速度了,这个不测没有索引的情况了,就测 product_tags 在添加了组合索引 tag_product_index 的情况。



虽然 product_tags 已经使用了索引,但是 product 表还是依然全表扫描,而且得出的时间 0.644秒 比 like 还要差。

在看一下 left join

SELECT * FROM product A  LEFT JOIN product_tags ON tagId=5201 AND productId=A.Id;



这个结果还是很不错的,而且如果重复查询 时间会更少,第一次查的时候大概 0.0014 秒 -0.0043秒之间。第2次查 就只有 0.00065秒了。
不过 product 还是“全表扫表”了,这里用了引号的全表扫描,是因为下文会说道。

那么 除了上面两种方法 还有没有别的方法呢?
细心的朋友可能会发现 我生成 product 的时候使tags 使用了FullText 索引,是的我们这里要讲另外一种方法是利用 FullText 的,如果你刚刚已经生成了数据,没有将 my.ini 的两个配置项该成如下:

[mysqld]
ft_min_word_len=1
innodb_ft_min_token_size=1

因为我们只是用全文索引来满足我们标签查询,所以分词最小长度需要设置为 1,innodb_ft_min_token_size 是 innodb 的设置。

然后就是重启mysql 服务,还得修复一下原来已经生成的索引。

执行SQL查询语句

REPAIR TABLE product QUICK;


OK,这个时候我们可以测试一下使用 全文索引来检索标签查询的效果了。

SELECT * FROM product WHERE MATCH(tags) AGAINST('1000');


使用全文索引 大概保持在 0.0016 左右,和 left join 的效果差不多,不过不在全表扫描了。

为了更深入的比较 全文索引 和 left join 我特地把数据提升了一倍,product  200W  product_tags 1200万。

SELECT * FROM product A  LEFT JOIN product_tags ON tagId=1000 AND productId=A.Id;
--- 第1次  0.0017 左右
--- 第2次  0.00070s 相同sql 语句
SELECT * FROM product WHERE MATCH(tags) AGAINST('1000');
--- 耗时 0.0021s

而使用另外两个链表方式:


SELECT  * FROM product A ,
(SELECT DISTINCT productId from product_tags where tagId=1000 ) B 
WHERE A.id=B.productId;
-- 第1次 0.0012s 左右
-- 第2次 0.00077s

使用 in 查找

SELECT * FROM product WHERE id In (SELECT DISTINCT productId FROM product_tags WHERE tagId=1000);
---第1次  0.0017-0.0021
---第2次 0.00078秒


最后在测试一下原来的
FIND_IN_SET 正则和 like

SELECT * FROM product WHERE FIND_IN_SET('1000',tags) ;
-- 耗时 0.126s 
SELECT * FROM product WHERE tags REGEXP '(^|,)1000(,|$)'; 
--耗时 0.117s
SELECT * FROM product WHERE CONCAT(',',tags,',') LIKE '%,1000,%';
--耗时 0.182s

基本成线性增长。

比较完以后 发现 left join 其实和 后面两种方式基本一样,所以虽然是“全表扫描”,但是其实mysql 在内部做了相应的优化,是先从 product_tags 找到 productId 然后在从id 中 使用主键 到 product 对应的数据的。其实和 后面这句 IN 的语句极为相识,至于 第2次查找会更快 可能是因为mysql 对一些中间结果做了相应的缓存优化,或者是对索引数据部分做了内存优化。而全文索引中 可能没有相应的优化。


本文为原创文章,未经允许不可转载,请尊重作者劳动成果。

 
推荐文章