无论如何,为了提高 SQL 查询的性能,以按标签匹配计数查找具有顺序的行
Posted
技术标签:
【中文标题】无论如何,为了提高 SQL 查询的性能,以按标签匹配计数查找具有顺序的行【英文标题】:Anyway to improve performance of SQL query to find rows with order by tag match count 【发布时间】:2021-06-20 13:40:25 【问题描述】:服务器:10.3.27-MariaDB-log
我有项目表和多对多项目标签表。在搜索表单中,我列出了我想要查找的所有标签,并希望收到按降序排列的标签计数的项目 ID 的有序列表。没有什么不寻常的。
CREATE TABLE `item` (
`id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
...
PRIMARY KEY (`id`)
)
COLLATE='utf8mb4_unicode_ci'
ENGINE=InnoDB
AUTO_INCREMENT=2283235
;
Item 表包含超过 2M 行。
CREATE TABLE `item_tag` (
`item_id` INT(10) UNSIGNED NOT NULL,
`tag_id` INT(10) UNSIGNED NOT NULL,
INDEX `fk_item_tag_tag_idx` (`tag_id`),
INDEX `fk_item_tag_item_idx` (`item_id`),
INDEX `tid_iid_idx` (`tag_id`, `item_id`),
INDEX `iid_tid_idx` (`item_id`, `tag_id`)
)
COLLATE='utf8mb4_unicode_ci'
ENGINE=InnoDB
;
Item_tag 表目前包含超过 60M 行。
经过一番努力寻找最佳查询后,我最终找到了 2 个在速度方面几乎相同的解决方案:
SELECT
i.`id`,
counter.cnt
FROM `item` i
RIGHT JOIN (
SELECT
it.item_id,
COUNT(it.item_id) AS cnt
FROM
item_tag it
WHERE
it.tag_id IN (3733, 5203, 5202, 1234) << --- test TAG_IDs
GROUP BY
it.item_id
ORDER BY NULL
) counter ON counter.item_id = i.id
ORDER BY
counter.cnt DESC, i.id DESC
LIMIT 50;
在 4,118 秒内执行。
和
SELECT
it.item_id,
COUNT(*) AS cnt
FROM item_tag it
INNER JOIN item i ON i.id = it.item_id
WHERE
it.tag_id IN (3733, 5203, 5202, 1234)
GROUP BY
it.item_id
ORDER BY
cnt DESC,
it.item_id DESC
LIMIT 50;
在 3,386 秒内执行。
查询的执行时间很大程度上取决于指定标签的频率。以前的时间是针对以下标签和计数:
| tag_id | counter (number of items) |
| -------- | ------- |
| 3733 | 457357 |
| 5203 | 14300 |
| 5202 | 13803 |
| 1234 | 0 |
但如果我重复这些查询以获得更流行的标签,我的查询执行时间会飙升至 40-50 秒:
| tag_id | counter (number of items) |
| -------- | ------- |
| 3927 | 497732 |
| 4189 | 472916 |
| 3714 | 505325 |
| 3702 | 369115 |
第一种查询说明:
id|select_type|table|type|possible_keys|key|key_len|ref|rows|Extra
--|------|-----|-----|-------|---|-------|--|---|------
1|PRIMARY|<derived2>|ALL|\N|\N|\N|\N|3271866|Using temporary| Using filesort
1|PRIMARY|i|eq_ref|PRIMARY|PRIMARY|4|counter.item_id|1|
2|DERIVED|it|range|fk_item_tag_tag_idx,tid_iid_idx|tid_iid_idx|4|\N|3271866|Using where| Using index| Using temporary
第二类查询说明:
id|select_type|table|type|possible_keys|key|key_len|ref|rows|Extra
--|------|-----|-----|-------|---|-------|--|---|------
1|SIMPLE|it|range|fk_item_tag_tag_idx,fk_item_tag_item_idx,tid_iid_idx,pid_kid_idx|tid_iid_idx|4|\N|3271866|Using where| Using index| Using temporary| Using filesort
1|SIMPLE|i|eq_ref|PRIMARY|PRIMARY|4|lm2.it.item_id|1|Using index
正如您可能已经猜到的 :) 这一次对我来说是不可接受的。我想知道可以做哪些类型的优化来减少查询执行时间?
更新 2 还有另一个表'TAG':
CREATE TABLE `tag` (
`id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
`tag` VARCHAR(45) NOT NULL COLLATE 'utf8mb4_unicode_ci',
`last_assigned` TIMESTAMP NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE INDEX `id_UNIQUE` (`id`),
UNIQUE INDEX `tag_UNIQUE` (`tag`)
)
COLLATE='utf8mb4_unicode_ci'
ENGINE=InnoDB
AUTO_INCREMENT=372469;
我已经从 ysth 的答案中测试了额外的查询,并且执行时间从 44.563 秒 -> 38.922 -> 33.931 下降。
id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | |
---|---|---|---|---|---|---|---|---|---|---|
1 | SIMPLE | it | range | fk_item_tag_tag_idx,tid_iid_idx | tid_iid_idx | 4 | \N | 3271866 | Using where | Using temporary |
1 | SIMPLE | t | eq_ref | PRIMARY,id_UNIQUE | PRIMARY | 4 | lm2.it.tag_id | 1 | Using index |
id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | |
---|---|---|---|---|---|---|---|---|---|---|
1 | PRIMARY | ALL | \N | \N | \N | \N | 2 | Using temporary | ||
1 | PRIMARY | it | ref | fk_item_tag_tag_idx,tid_iid_idx | tid_iid_idx | tid_iid_idx | 4 | t.id | 105 | Using index |
2 | DERIVED | \N | \N | \N | \N | \N | \N | \N | No tables used | |
3 | UNION | \N | \N | \N | \N | \N | \N | \N | No tables used | |
4 | UNION | \N | \N | \N | \N | \N | \N | \N | No tables used | |
5 | UNION | \N | \N | \N | \N | \N | \N | \N | No tables used |
无论如何,在我通过添加新条件缩小搜索范围之前,似乎无法加快执行时间。我想不出更好的方法来缩小搜索范围。
按时间列限制搜索查询(即显示去年的项目) 不允许用户一次搜索多个最受欢迎的标签 ... ?【问题讨论】:
顺便说一句,请注意没有人使用 RIGHT JOIN 对count()
的结果进行排序很难改进...我尝试创建FIDDLE 但结果太好了,无法改进... ????
您的时间不包括获取此列表:it.tag_id IN (3733, 5203, 5202, 1234)
;我的答案消除了这种开销。
select version();
说什么?
10.3.27-MariaDB-log
【参考方案1】:
查询将必须读取您选择的标签的所有 item_tag 记录,因此对于更受欢迎的标签需要更长的时间;没有办法。
除非您在 item_tag 中有 item_id 值不在您需要排除的项目中,否则根本不需要加入项目。
您可能会看到使用标签表的一些改进(我假设存在,因为 fk_item_tag_tag_idx 索引);这应该将 item_tag 上的索引查找从范围更改为参考:
select it.item_id, count(*)
from tag t
join item_tag it on t.id=it.tag_id
where t.id in (3733, 5203, 5202, 1234)
group by it.item_id order by count(*) desc, it.item_id desc limit 50
如果没有标签表,您可以使用 ad-hoc 表:
select it.item_id, count(*)
from (select 3733 id union all select 5203 union all select 5202 union all select 1234) t
join item_tag it on t.id=it.tag_id
group by it.item_id order by count(*) desc, it.item_id desc limit 50
【讨论】:
谢谢。实际上,还有一个 TAG 表。我只是没有提到它,因为我认为这无关紧要。我用新的测量值更新了原始帖子。您的查询确实减少了执行时间,我不明白为什么。为什么使用附加表会使查询运行得更快? 如果-> 38.922 -> 33.931.
之后的两个解释来自我的两个查询,看起来第一个的改进只是不必阅读项目表;它仍然首先从 item_tag 读取范围,而不是先读取标签,然后再读取 item_tag 和引用。尝试将 straight_join 添加到我的第一个查询 select straight_join it.item_id, count(*) ...
'SELECT straight_join' 查询在 49 分 44 秒内完成 :) vs 42 秒。这里解释一下:id|select_type|table|type|possible_keys|key|key_len|ref|rows|Extra 1|SIMPLE|t|index|PRIMARY,id_UNIQUE|id_UNIQUE|4|\N|330728|使用索引|使用临时|使用文件排序 1|SIMPLE|it|ref|fk_item_tag_tag_idx,tid_iid_idx|tid_iid_idx|tid_iid_idx|4|lm2.t.id|104|使用 where|使用索引【参考方案2】:
删除这些,它们会损害性能:
INDEX `fk_item_tag_tag_idx` (`tag_id`),
INDEX `fk_item_tag_item_idx` (`item_id`),
它们妨碍优化器意识到复合索引之一更好。
PRIMARY KEY
是什么?每个 InnoDB 都必须有一个 PK,最好明确声明 PK。如果是(tag_id, item_id)
的组合,那就PK吧。 (但不要费心让 (item_id, tag_id)
独一无二。)
标签是什么样的?它们是短字符串吗?如果是这样,请不要为每个标签使用id
。只需将字符串放在此表中即可。
进一步讨论:many:many 而且,我是根据标记经验发言。
另外,摆脱嵌套的SELECT
。找到最常见的标签很简单:
SELECT tag, COUNT(*) AS counter
FROM tags
WHERE tag IN (...)
GROUP BY tag
ORDER BY counter DESC
LIMIT 50;
(是的,您可以将tag
添加到ORDER BY
。)
【讨论】:
我按照您的建议删除了单列索引,但在速度方面没有任何改变。我在这张表上没有 PK,但我确实覆盖了索引 tid_iid_idx(标签,项目)和 iid_tid_idx(项目,标签)。不一样吗?无论如何我会尝试创建PK。创建 PK 后是否必须删除覆盖索引?标签只是文字,有些很短,有些很长。 PK 可以是“复合的”。 PK 是唯一的并且是一个索引。 “标签可能很长”——即使有些标签是 100 个字符,我的建议仍然有效。以上是关于无论如何,为了提高 SQL 查询的性能,以按标签匹配计数查找具有顺序的行的主要内容,如果未能解决你的问题,请参考以下文章