You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

性能瓶颈分析

  1. 大偏移量分页:LIMIT 358223, 5000会让MySQL先扫描358223+5000行数据,再丢弃前358223行,仅返回后5000行,磁盘IO和内存开销极大,这是主要耗时原因。
  2. 临时表与文件排序:执行计划中community表出现Using temporary; Using filesort,因为查询以community为驱动表,但最终按user_post.id排序,MySQL需要将所有符合条件的数据放入临时表再排序,开销巨大。
  3. 函数条件无索引:LENGTH(up.description)>=20无法利用现有索引,需要回表计算每条记录的描述长度,增加CPU和IO开销。
  4. 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.17 20:30:49