【问题标题】:MySQL query becomes super slow when two conditions are both applied当同时应用两个条件时,MySQL 查询变得超级慢
【发布时间】:2022-10-15 05:10:07
【问题描述】:

我有一个用户具有“bio”字段和“n:n”关系的表通过追随者桌子。因此每个用户 U 可以关注许多其他用户。

问题:我的用户搜索查询非常慢。

观察:

  1. 所有查询都获得前 20 个搜索结果 (limit 20)
  2. 搜索简历中包含“创始人”的用户需要 0.3 秒
  3. 搜索关注X的用户,耗时0.03s
  4. 搜索在个人简介中有“创始人”并关注 X 的用户,需要 118 秒!!!

询问:

这是搜索两个过滤器的最终查询:

select distinct `twitter_user`.`id`
from `twitter_user`
         join `twitter_user_follower`
              on (
                          `twitter_user_follower`.`follower_twitter_user_id` =
                          `twitter_user`.`id`
                      and `twitter_user_follower`.`twitter_user_id` = 4899565692
                      and `twitter_user_follower`.`follower_download_id` = 7064
                  )
where MATCH(twitter_user.description) AGAINST('founder')
limit 20 offset 0

表定义

CREATE TABLE `twitter_user` (
  `id` bigint NOT NULL,
  `name` varchar(128) NOT NULL,
  `email` varchar(128) DEFAULT NULL,
  `screen_name` varchar(128) DEFAULT NULL,
  `location` varchar(256) DEFAULT NULL,
  `description` varchar(512) DEFAULT NULL,
  `url` varchar(256) DEFAULT NULL,
  `is_protected` bit(1) DEFAULT NULL,
  `followers_count` int DEFAULT NULL,
  `is_verified` bit(1) DEFAULT NULL,
  `friends_count` int DEFAULT NULL,
  `created_at` bigint DEFAULT NULL,
  `favourites_count` int DEFAULT NULL,
  `utc_offset` int DEFAULT NULL,
  `time_zone` varchar(128) DEFAULT NULL,
  `statuses_count` int DEFAULT NULL,
  `profile_image_url` varchar(512) DEFAULT NULL,
  `internal_json` json DEFAULT NULL,
  `row_timestamp` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  KEY `twitter_user_username_index` (`screen_name`),
  KEY `twitter_user_ts` (`row_timestamp`),
  FULLTEXT KEY `twitter_user_description_ft_index` (`description`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

CREATE TABLE `twitter_user_follower` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `twitter_user_id` bigint NOT NULL,
  `follower_twitter_user_id` bigint NOT NULL,
  `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `follower_download_id` bigint DEFAULT NULL,
  `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  KEY `twitter_user_follower_twitter_user_id_index` (`twitter_user_id`),
  KEY `twitter_user_follower_follower_download_id_index` (`follower_download_id`),
  KEY `tuf_twitter_user_follower_download_key` (`twitter_user_id`,`follower_download_id`,`follower_twitter_user_id`)
) ENGINE=InnoDB AUTO_INCREMENT=68494675 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

解释输出

+----+-------------+-----------------------+------------+----------+-------------------------------------------------------------------------------------------------------------------------------------+----------------------------------------+---------+----------------------------------------+------+----------+----------------------------------------------------+
| id | select_type | table                 | partitions | type     | possible_keys                                                                                                                       | key                                    | key_len | ref                                    | rows | filtered | Extra                                              |
+----+-------------+-----------------------+------------+----------+-------------------------------------------------------------------------------------------------------------------------------------+----------------------------------------+---------+----------------------------------------+------+----------+----------------------------------------------------+
|  1 | SIMPLE      | twitter_user          | NULL       | fulltext | PRIMARY,twitter_user_username_index,twitter_user_ts,twitter_user_description_ft_index                                               | twitter_user_description_ft_index      | 0       | const                                  |    1 |   100.00 | Using where; Ft_hints: no_ranking; Using temporary |
|  1 | SIMPLE      | twitter_user_follower | NULL       | ref      | twitter_user_follower_twitter_user_id_index,twitter_user_follower_follower_download_id_index,tuf_twitter_user_follower_download_key | tuf_twitter_user_follower_download_key | 25      | const,const,si_data_db.twitter_user.id |    1 |   100.00 | Using index; Distinct                              |
+----+-------------+-----------------------+------------+----------+-------------------------------------------------------------------------------------------------------------------------------------+----------------------------------------+---------+----------------------------------------+------+----------+----------------------------------------------------+

树输出:

    -> Limit: 20 row(s)  (cost=4.77..4.77 rows=1)
    -> Table scan on <temporary>  (cost=2.51..2.51 rows=1)
        -> Temporary table with deduplication  (cost=4.77..4.77 rows=1)
            -> Limit table size: 20 unique row(s)
                -> Nested loop inner join  (cost=2.16 rows=1)
                    -> Filter: (match twitter_user.`description` against (''founder''))  (cost=1.06 rows=1)
                        -> Full-text index search on twitter_user using twitter_user_description_ft_index (description=''founder'')  (cost=1.06 rows=1)
                    -> Limit: 1 row(s)  (cost=1.10 rows=1)
                        -> Covering index lookup on twitter_user_follower using tuf_twitter_user_follower_download_key (twitter_user_id=4899565692, follower_download_id=7064, follower_twitter_user_id=twitter_user.id)  (cost=1.10 rows=1)

更新

根据 Bernd 的建议,我运行了这个仍然很慢的查询:

SELECT `follower`.`follower_twitter_user_id`
FROM (
         SELECT `follower_twitter_user_id`
         FROM `twitter_user_follower`
         WHERE `twitter_user_id` = 4899565692
           AND `follower_download_id` = 7440
     ) AS follower
         JOIN `twitter_user` ON `follower`.`follower_twitter_user_id` =  `twitter_user`.`id`
WHERE MATCH(twitter_user.description) AGAINST('+founder' IN BOOLEAN MODE)
limit 20 offset 0;

解释输出:

+----+-------------+-----------------------+------------+----------+-------------------------------------------------------------------------------------------------------------------------------------+----------------------------------------+---------+----------------------------------------+------+----------+-----------------------------------+
| id | select_type | table                 | partitions | type     | possible_keys                                                                                                                       | key                                    | key_len | ref                                    | rows | filtered | Extra                             |
+----+-------------+-----------------------+------------+----------+-------------------------------------------------------------------------------------------------------------------------------------+----------------------------------------+---------+----------------------------------------+------+----------+-----------------------------------+
|  1 | SIMPLE      | twitter_user          | NULL       | fulltext | PRIMARY,twitter_user_description_ft_index                                                                                           | twitter_user_description_ft_index      | 0       | const                                  |    1 |   100.00 | Using where; Ft_hints: no_ranking |
|  1 | SIMPLE      | twitter_user_follower | NULL       | ref      | twitter_user_follower_twitter_user_id_index,twitter_user_follower_follower_download_id_index,tuf_twitter_user_follower_download_key | tuf_twitter_user_follower_download_key | 25      | const,const,si_data_db.twitter_user.id |    1 |   100.00 | Using index                       |
+----+-------------+-----------------------+------------+----------+-------------------------------------------------------------------------------------------------------------------------------------+----------------------------------------+---------+----------------------------------------+------+----------+-----------------------------------+

解释分析输出:

-> Limit: 20 row(s)  (cost=2.16 rows=1) (actual time=3779.933..91032.297 rows=20 loops=1)
    -> Nested loop inner join  (cost=2.16 rows=1) (actual time=3779.932..91032.285 rows=20 loops=1)
        -> Filter: (match twitter_user.`description` against ('+founder' in boolean mode))  (cost=1.06 rows=1) (actual time=94.166..90001.280 rows=198818 loops=1)
            -> Full-text index search on twitter_user using twitter_user_description_ft_index (description='+founder')  (cost=1.06 rows=1) (actual time=94.163..89909.371 rows=198818 loops=1)
        -> Covering index lookup on twitter_user_follower using tuf_twitter_user_follower_download_key (twitter_user_id=4899565692, follower_download_id=7440, follower_twitter_user_id=twitter_user.id)  (cost=1.10 rows=1) (actual time=0.005..0.005 rows=0 loops=198818)

更新如果这有帮助,用户表是 125GB,追随者表在磁盘上是 5GB。

【问题讨论】:

  • 由于您使用的是 LIMIT 而没有 ORDER BY,因此即使您似乎对此查询有合理的索引,使用附加条件的搜索可能需要更长的时间,我并不感到惊讶。我想知道是否将其重写为IN 查询(没有JOIN)可能会有所帮助,但我只是在推测。
  • 我一直在使用JOININ,但两者都很慢。我认为没有ORDER BYLIMIT 对性能更好,因为DBMS 不需要排序,只需选择它获得的前20 行。
  • 我并不是要暗示添加ORDER BY 会提高性能。我只是说ORDER BY 可能会使具有一种条件的查询在性能上与具有两种条件的查询更相似,所以我并不完全惊讶于您的实际情况中的数字如此不同。

标签: mysql sql performance query-optimization


【解决方案1】:

试试下面的。改变

MATCH(twitter_user.description) AGAINST('founder')

MATCH(twitter_user.description) AGAINST('+founder' IN BOOLEAN MODE)

此外,DISTINCT 可能不是必需的。

奥德库?

埋在评论中,我看到 DELETE + INSERT 在部分表格中造成大量流失。

  • InnoDB 的全文不得在这种情况下高效
  • 如果大多数行没有改变,那么删除+插入是低效的,并且会导致不必要的流失。

查看INSERT ... ON DUPLICATE KEY UPDATE ... 看看是否可以使用它来代替删除+插入。如果大多数行没有改变,那么这可能会更快,并且可能对全文索引等内容的影响更小。

如果该 Delete 确实删除了一些行,那么 IODKU (upsert) 就不够了。使用INSERT ... SELECT ... LEFT JOIN 之类的第二遍可能是插入“新”行的解决方案。 (我在这里在不同的上下文中提到了这一点:Normalization;参见 SQL#1。)

定期(每周?)运行OPTIMIZE TABLE。但是请保留一些时间,看看这一步是否真的有帮助。

2个步骤

首先,我仍然不清楚您每小时收到的数据。它只是关于一个用户的信息吗?它是否包括要删除的行,并带有一些指示它们将被删除而不是更新的指示?等等。

如果是单用户...

  • DELETE 仅需要删除的行。这涉及带有LEFT JOIN 的多表删除以查看缺少的内容。
  • INSERT ... SELECT ... LEFT JOIN ... 插入或更新现有行。

【讨论】:

  • 谢谢@里克詹姆斯。这需要大约 78 秒才能运行,这是一个改进。
  • @Mahdi - 啊! 78s还是很可怕的。我补充了一些想法。
【解决方案2】:

你能试试这个并将解释发布给我们吗?

SELECT `follower`.`follower_twitter_user_id`
FROM (
  SELECT `follower_twitter_user_id`
  FROM `twitter_user_follower`
  WHERE `twitter_user_id` = 4899565692
    AND `follower_download_id` = 7064
) AS follower
JOIN `twitter_user` ON `follower`.`follower_twitter_user_id` =  `twitter_user`.`id`
MATCH(twitter_user.description) AGAINST('+founder' IN BOOLEAN MODE)
limit 20 offset 0;

【讨论】:

  • 这个也花了115秒。我必须更新7064 id,因为新的关注者下载会弃用并删除旧的。
  • 我已经用分析输出更新了这个问题。我定期(每 1 小时)删除所有带有 follower_download_id=X 的行,并插入带有新下载结果的新行。您认为这可能会导致问题吗?
  • @Mahdi - 涉及FULLTEXT 的大删除?这可能是需要OPTIMIZE TABLE 的罕见情况。注意:它可能会在运行时阻止表的使用。
  • @RickJames 这不会发生在 users 表中。它发生在followers 表上。所以我每隔约 1 小时“刷新”一次追随者。结果,大多数行是相同的,有些是新的,有些不再跟随。但我所做的只是删除以前下载的所有内容,然后插入新的。这会导致速度变慢吗?也许通过在物理存储上分割表?
  • 大删除非常密集,因为它需要挂在已删除的行上直到COMMIT 时间。
猜你喜欢
  • 2015-11-25
  • 2021-09-05
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-04-23
  • 1970-01-01
  • 1970-01-01
  • 2014-06-16
相关资源
最近更新 更多