MySQL查询性能优化求助:带索引仍耗时,求优化方案
MySQL查询性能分析与优化建议
问题概述
现有一条MySQL查询,已创建索引,但返回5000条数据耗时长达2分13秒,需分析瓶颈并给出优化方案。
查询语句
SELECT up.id post_id, c.id community_id, SUBSTRING(up.description, 1, 30) post_description, c.name community_name, c.is_index is_index, c.slug slug, p.last_modified_on last_modified_on, c.is_shop is_shop FROM user_post up INNER JOIN community c ON up.community_id = c.id and c.is_index = 1 inner join participant pa on c.participant_id = pa.id and pa.is_active = 1 inner join participating_entity p on up.participating_entity_id = p.id left join indexed_post ip on ip.user_post_id = up.id and ip.is_active = 1 WHERE p.is_active = 1 and up.is_spam_post = 0 and ip.id is null and LENGTH(up.description) >= 20 order by up.id limit 358223, 5000\G
执行计划
*************************** 1. row *************************** id: 1 select_type: SIMPLE table: c partitions: NULL type: ref possible_keys: PRIMARY,fk_community_participant1_idx,idx_is_index_participant_id key: idx_is_index_participant_id key_len: 2 ref: const rows: 6702 filtered: 100.00 Extra: Using temporary; Using filesort *************************** 2. row *************************** id: 1 select_type: SIMPLE table: pa partitions: NULL type: eq_ref possible_keys: PRIMARY key: PRIMARY key_len: 8 ref: she.c.participant_id rows: 1 filtered: 10.00 Extra: Using where *************************** 3. row *************************** id: 1 select_type: SIMPLE table: up partitions: NULL type: ref possible_keys: fk_user_post_community1_idx,fk_user_post_entity1_idx,is_spam_post,idx_is_spam_post,idx_community_id_participating_entity_id_is_spam_post,idx_participating_entity_id_community_id key: idx_community_id_participating_entity_id_is_spam_post key_len: 9 ref: she.c.id rows: 338 filtered: 50.00 Extra: Using index condition; Using where *************************** 4. row *************************** id: 1 select_type: SIMPLE table: ip partitions: NULL type: ref possible_keys: idx_user_post_id key: idx_user_post_id key_len: 8 ref: she.up.id rows: 1 filtered: 10.00 Extra: Using where; Not exists *************************** 5. row *************************** id: 1 select_type: SIMPLE table: p partitions: NULL type: eq_ref possible_keys: PRIMARY,idx_is_active key: PRIMARY key_len: 8 ref: she.up.participating_entity_id rows: 1 filtered: 50.00 Extra: Using where
表结构
Community表
PRIMARY KEY (`id`), KEY `fk_community_participant1_idx` (`participant_id`), KEY `fk_community_community_type1_idx` (`community_type_id`), KEY `idx_parent_participant_id` (`parent_participant_id`), KEY `idx_score_latest` (`score_latest`), KEY `slug` (`slug`(191)), KEY `idx_is_index_participant_id` (`is_index`,`participant_id`), CONSTRAINT `_fk_community_community_type1` FOREIGN KEY (`community_type_id`) REFERENCES `community_type` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT `_fk_community_participant1` FOREIGN KEY (`participant_id`) REFERENCES `participant` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION
participant表
CREATE TABLE `participant` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `participant_type_id` bigint(20) NOT NULL, `crdt` timestamp NULL DEFAULT NULL, `created_by` bigint(20) DEFAULT NULL, `last_modified_on` timestamp NULL DEFAULT NULL, `last_modified_by` bigint(20) DEFAULT NULL, `is_active` tinyint(1) DEFAULT '1', `is_deleted` tinyint(1) DEFAULT '0', `partner_id` bigint(20) DEFAULT NULL, `sheroes_deep_link` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL, PRIMARY KEY (`id`), KEY `fk_participant_participation_type1_idx` (`participant_type_id`), KEY `idx_participant_last_modified_on` (`last_modified_on`), KEY `idx_participant_crdt` (`crdt`), KEY `sheroes_deep_link` (`sheroes_deep_link`(191)), CONSTRAINT `fk_participant_participation_type1` FOREIGN KEY (`participant_type_id`) REFERENCES `participant_type` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION )
participating_entity表
PRIMARY KEY (`id`), KEY `fk_entity_entity_type_idx` (`entity_type_id`), KEY `idx_participating_entity_last_modified_on` (`last_modified_on`), KEY `created_by` (`created_by`), KEY `fk_partner_id_idx` (`partner_id`), KEY `crdt` (`crdt`), KEY `idx_category` (`category`), KEY `idx_created_by` (`created_by`), KEY `idx_is_active` (`is_active`), CONSTRAINT `fk_entity_entity_type` FOREIGN KEY (`entity_type_id`) REFERENCES `entity_type` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT `fk_partner_id` FOREIGN KEY (`partner_id`) REFERENCES `api_consumer_partner` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION
indexed_post表
CREATE TABLE `indexed_post` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `user_post_id` bigint(20) NOT NULL, `is_active` tinyint(1) DEFAULT '0', `crdt` timestamp NULL DEFAULT NULL, `created_by` bigint(20) DEFAULT NULL, `last_modified_by` bigint(20) DEFAULT NULL, `last_modified_on` timestamp NULL DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_user_post_id` (`user_post_id`) )
user_post表
PRIMARY KEY (`id`), KEY `fk_user_post_users1_idx` (`users_id`), KEY `fk_user_post_community1_idx` (`community_id`), KEY `fk_user_post_entity1_idx` (`participating_entity_id`), KEY `idx_source_ent_id` (`source_entity_id`), KEY `idx_entity_start_dt` (`entity_start_date`), KEY `idx_rating_for_company` (`rating`), KEY `user_post_source_type` (`source_type`), KEY `is_spam_post` (`is_spam_post`), KEY `idx_is_spam_post` (`is_spam_post`), KEY `idx_is_theme_post` (`is_theme_post`), KEY `idx_meta_title` (`meta_title`), KEY `idx_meta_description` (`meta_description`), KEY `idx_recipient_id` (`recipient_id`), KEY `idx_title` (`title`(191)), KEY `idx_community_id_participating_entity_id_is_spam_post` (`community_id`,`participating_entity_id`,`is_spam_post`), KEY `user_post_is_recommended_IDX` (`is_recommended`), KEY `idx_participating_entity_id_community_id` (`participating_entity_id`,`community_id`), CONSTRAINT `__fk_user_post_community1` FOREIGN KEY (`community_id`) REFERENCES `community` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT `__fk_user_post_entity1` FOREIGN KEY (`participating_entity_id`) REFERENCES `participating_entity` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT `__fk_user_post_users1` FOREIGN KEY (`users_id`) REFERENCES `users` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION
性能瓶颈分析
- 大偏移量分页:
LIMIT 358223, 5000会让MySQL先扫描358223+5000行数据,再丢弃前358223行,仅返回后5000行,磁盘IO和内存开销极大,这是主要耗时原因。 - 临时表与文件排序:执行计划中
community表出现Using temporary; Using filesort,因为查询以community为驱动表,但最终按user_post.id排序,MySQL需要将所有符合条件的数据放入临时表再排序,开销巨大。 - 函数条件无索引:
LENGTH(up.description)>=20无法利用现有索引,需要回表计算每条记录的描述长度,增加CPU和IO开销。 - LEFT JOIN效率问题:
LEFT JOIN indexed_post后判断ip.id IS NULL的方式,相比NOT EXISTS子查询,在数据量大时扫描行数更多,效率更低。
优化建议
1. 分页优化(核心)
替换大偏移量LIMIT为基于主键的范围查询,避免扫描无关数据。例如,记录上一页返回的最大post_id(如示例中的1788267),然后修改查询条件:
WHERE -- 其他条件不变 up.id > 1788267 ORDER BY up.id LIMIT 5000
这种方式直接定位到起始位置,仅扫描5000行数据,耗时会大幅降低。
2. 索引优化
user_post表添加覆盖索引:创建包含过滤条件、排序字段和查询所需字段的覆盖索引,避免回表:
-- 先添加生成列解决LENGTH函数无法索引的问题 ALTER TABLE user_post ADD COLUMN description_len INT AS (LENGTH(description)) STORED; -- 创建覆盖索引 CREATE INDEX idx_user_post_cover ON user_post (is_spam_post, description_len, id, community_id, participating_entity_id, description);该索引包含所有过滤条件(
is_spam_post、description_len)、排序字段(id),以及连接和查询所需的字段,完全无需回表。indexed_post表优化索引:将现有索引升级为复合索引,包含
user_post_id和is_active,加速NOT EXISTS判断:DROP INDEX idx_user_post_id ON indexed_post; CREATE INDEX idx_user_post_active ON indexed_post (user_post_id, is_active);
3. 查询语句重写
用NOT EXISTS替代LEFT JOIN + IS NULL,减少不必要的行扫描;同时调整连接顺序,让user_post作为驱动表,利用其索引排序避免临时表和文件排序:
SELECT up.id post_id, c.id community_id, SUBSTRING(up.description, 1, 30) post_description, c.name community_name, c.is_index is_index, c.slug slug, p.last_modified_on last_modified_on, c.is_shop is_shop FROM user_post up INNER JOIN community c ON up.community_id = c.id INNER JOIN participant pa ON c.participant_id = pa.id INNER JOIN participating_entity p ON up.participating_entity_id = p.id WHERE up.is_spam_post = 0 AND up.description_len >= 20 -- 使用生成列替代LENGTH函数 AND c.is_index = 1 AND pa.is_active = 1 AND p.is_active = 1 AND NOT EXISTS ( SELECT 1 FROM indexed_post ip WHERE ip.user_post_id = up.id AND ip.is_active = 1 ) ORDER BY up.id -- 替换为范围查询:WHERE up.id > [last_post_id] LIMIT 5000 LIMIT 5000;
4. 其他优化
- 定期清理
indexed_post表的无效数据(is_active=0),减少扫描行数。 - 确保
participating_entity表的is_active索引被有效利用,可考虑将PRIMARY KEY查询改为idx_is_active复合索引(如果is_active=1的数据占比低)。
内容的提问来源于stack exchange,提问作者sky ler
相关产品推荐
相关产品推荐

