MySQL千万级数据从10min优化到250ms_覆盖索引的魅力

300 阅读12分钟

首先要声明的就是,千万级数据对于MySQL来说就是不太合理的一个存在。

优化MySQL千万级数据策略还是比较多的。

  • 分表分库
  • 创建中间表,汇总表
  • 修改为多个子查询

这里讨论的情况是在MySQL一张表的数据达到千万级别。表设计很烂,业务统计规则又不允许把sql拆成多个子查询。

在这样的情况下,开发者可以尝试通过优化SQL来达到查询的目的。

当MySQL一张表的数据达到千万级别,会出现一些特殊的情况。这里主要是讨论在比较极端的情况下SQL的优化策略。

init千万级数据

通过存储过程传递函数制造1000万条数据。 sql+存储过程 脚本

CREATE TABLE `orders` (
  `order_id` int NOT NULL AUTO_INCREMENT,
  `user_id` int DEFAULT NULL,
  `order_date` date NOT NULL,
  `total_amount` decimal(10,2) NOT NULL,
  PRIMARY KEY (`order_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

CREATE TABLE `users` (
  `user_id` int NOT NULL AUTO_INCREMENT,
  `username` varchar(50) COLLATE utf8mb4_general_ci NOT NULL,
  `email` varchar(100) COLLATE utf8mb4_general_ci NOT NULL,
  `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`user_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;


CREATE TABLE `orders` (
  `order_id` int(11) NOT NULL AUTO_INCREMENT,
  `user_id` int(11) DEFAULT NULL,
  `order_date` date NOT NULL,
  `total_amount` decimal(10,2) NOT NULL,
  `task_number` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '提总任务单code',
  `base_unit_id` bigint(19) DEFAULT NULL COMMENT '基本单位id',
  `base_unit_code` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '基本单位code',
  `base_unit_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '基本单位名称',
  `base_planned_qty` decimal(15,3) unsigned DEFAULT NULL COMMENT '基本单位计划数量',
  `base_allocated_qty` decimal(15,3) unsigned DEFAULT NULL COMMENT '基本单位分配数量',
  `base_picked_qty` decimal(15,3) unsigned DEFAULT NULL COMMENT '基本单位拣货数量',
  `base_loaded_qty` decimal(15,3) unsigned DEFAULT NULL COMMENT '基本单位分拣数量',
  `base_shipped_qty` decimal(15,3) unsigned DEFAULT NULL COMMENT '基本单位发运数量',
  `base_diffied_qty` decimal(15,3) unsigned DEFAULT NULL COMMENT '基本单位差异数量',
  `planned_cross_qty` decimal(15,3) unsigned DEFAULT NULL COMMENT '计划越库数量',
  `real_cross_qty` decimal(15,3) unsigned DEFAULT NULL COMMENT '实际越库数量',
  `base_replenish_qty` decimal(15,3) unsigned DEFAULT NULL COMMENT '计划补货数量',
  `is_replenish` bit(1) DEFAULT NULL COMMENT '是否已补货(1:已补货;0:未补货)',
  `diffied_man` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '差异人',
  `diffied_time` datetime DEFAULT NULL COMMENT '差异时间',
  `is_occupy` bit(1) DEFAULT NULL COMMENT '是否占用',
  `occupy_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '占用人员',
  `reason_intercept` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '拦截原因',
  `occupy_time` datetime DEFAULT NULL COMMENT '占用时间',
  `print_mark` bit(1) DEFAULT b'0' COMMENT '打印标识',
  `revision` int(11) DEFAULT NULL COMMENT '乐观锁',
  `create_user` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '创建人',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `update_user` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '更新人',
  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
  `tenant_code` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL,
  `review_qty` decimal(15,3) unsigned DEFAULT NULL COMMENT '复核数量',
  `review_status` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '复核状态',
  `create_nick_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '创建人姓名',
  `update_nick_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '更新人姓名',
  `intercept_man` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '拦截人',
  `intercept_time` datetime DEFAULT NULL COMMENT '拦截时间',
  `instance_code` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '实例编码',
  PRIMARY KEY (`order_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;






DELIMITER $$

CREATE DEFINER=`root`@`localhost` PROCEDURE `generate_orders`()
BEGIN
    DECLARE i INT DEFAULT 0;
    DECLARE total_users INT DEFAULT 1000; -- Number of users
    DECLARE total_orders_per_user INT DEFAULT 1000; -- Orders per user
    DECLARE rnd_user_id INT;
    DECLARE rnd_order_date DATE;
    DECLARE rnd_total_amount DECIMAL(10, 2);
    DECLARE rnd_task_number VARCHAR(20);
    DECLARE rnd_base_unit_id BIGINT;
    DECLARE rnd_base_unit_code VARCHAR(20);
    DECLARE rnd_base_unit_name VARCHAR(50);
    DECLARE rnd_base_planned_qty DECIMAL(15, 3);
    DECLARE rnd_base_allocated_qty DECIMAL(15, 3);
    DECLARE rnd_base_picked_qty DECIMAL(15, 3);
    DECLARE rnd_base_loaded_qty DECIMAL(15, 3);
    DECLARE rnd_base_shipped_qty DECIMAL(15, 3);
    DECLARE rnd_base_diffied_qty DECIMAL(15, 3);
    DECLARE rnd_planned_cross_qty DECIMAL(15, 3);
    DECLARE rnd_real_cross_qty DECIMAL(15, 3);
    DECLARE rnd_base_replenish_qty DECIMAL(15, 3);
    DECLARE rnd_is_replenish BIT;
    DECLARE rnd_diffied_man VARCHAR(50);
    DECLARE rnd_diffied_time DATETIME;
    DECLARE rnd_is_occupy BIT;
    DECLARE rnd_occupy_name VARCHAR(50);
    DECLARE rnd_reason_intercept VARCHAR(50);
    DECLARE rnd_occupy_time DATETIME;
    DECLARE rnd_print_mark BIT;
    DECLARE rnd_revision INT;
    DECLARE rnd_create_user VARCHAR(50);
    DECLARE rnd_create_time DATETIME;
    DECLARE rnd_update_user VARCHAR(50);
    DECLARE rnd_update_time DATETIME;
    DECLARE rnd_tenant_code VARCHAR(32);
    DECLARE rnd_review_qty DECIMAL(15, 3);
    DECLARE rnd_review_status VARCHAR(30);
    DECLARE rnd_create_nick_name VARCHAR(50);
    DECLARE rnd_update_nick_name VARCHAR(50);
    DECLARE rnd_intercept_man VARCHAR(50);
    DECLARE rnd_intercept_time DATETIME;
    DECLARE rnd_instance_code VARCHAR(32);
    DECLARE j INT DEFAULT 0;

    WHILE i < total_users DO
        -- Fetch user ID
        SELECT user_id INTO rnd_user_id FROM users LIMIT i, 1;

        WHILE j < total_orders_per_user DO
            -- Generate random order date and total amount
            SET rnd_order_date = DATE_ADD('2020-01-01', INTERVAL FLOOR(RAND() * 1096) DAY); -- Random date between 2020-01-01 and 2022-12-31
            SET rnd_total_amount = ROUND(RAND() * 1000, 2); -- Random total amount between 0 and 1000
            
            -- Generate random values for the remaining fields
            SET rnd_task_number = CONCAT('TASK', FLOOR(RAND() * 10000)); -- Random task number
            SET rnd_base_unit_id = FLOOR(RAND() * 1000000000); -- Random base unit ID
            SET rnd_base_unit_code = CONCAT('UNIT', FLOOR(RAND() * 10000)); -- Random base unit code
            SET rnd_base_unit_name = CONCAT('UnitName', FLOOR(RAND() * 100)); -- Random base unit name
            SET rnd_base_planned_qty = ROUND(RAND() * 1000, 3); -- Random planned quantity
            SET rnd_base_allocated_qty = ROUND(RAND() * 1000, 3); -- Random allocated quantity
            SET rnd_base_picked_qty = ROUND(RAND() * 1000, 3); -- Random picked quantity
            SET rnd_base_loaded_qty = ROUND(RAND() * 1000, 3); -- Random loaded quantity
            SET rnd_base_shipped_qty = ROUND(RAND() * 1000, 3); -- Random shipped quantity
            SET rnd_base_diffied_qty = ROUND(RAND() * 1000, 3); -- Random diffied quantity
            SET rnd_planned_cross_qty = ROUND(RAND() * 1000, 3); -- Random planned cross quantity
            SET rnd_real_cross_qty = ROUND(RAND() * 1000, 3); -- Random real cross quantity
            SET rnd_base_replenish_qty = ROUND(RAND() * 1000, 3); -- Random replenish quantity
            SET rnd_is_replenish = IF(RAND() > 0.5, b'1', b'0'); -- Random replenish status
            SET rnd_diffied_man = CONCAT('DiffiedMan', FLOOR(RAND() * 100)); -- Random diffied man name
            SET rnd_diffied_time = NOW(); -- Use current time as the diffied time
            SET rnd_is_occupy = IF(RAND() > 0.5, b'1', b'0'); -- Random occupy status
            SET rnd_occupy_name = CONCAT('OccupyName', FLOOR(RAND() * 100)); -- Random occupy person
            SET rnd_reason_intercept = CONCAT('InterceptReason', FLOOR(RAND() * 100)); -- Random intercept reason
            SET rnd_occupy_time = NOW(); -- Use current time as occupy time
            SET rnd_print_mark = IF(RAND() > 0.5, b'1', b'0'); -- Random print mark
            SET rnd_revision = FLOOR(RAND() * 10); -- Random revision number
            SET rnd_create_user = CONCAT('User', FLOOR(RAND() * 100)); -- Random creator user
            SET rnd_create_time = NOW(); -- Current time as create time
            SET rnd_update_user = CONCAT('User', FLOOR(RAND() * 100)); -- Random updater user
            SET rnd_update_time = NOW(); -- Current time as update time
            SET rnd_tenant_code = CONCAT('Tenant', FLOOR(RAND() * 100)); -- Random tenant code
            SET rnd_review_qty = ROUND(RAND() * 1000, 3); -- Random review quantity
            SET rnd_review_status = IF(RAND() > 0.5, 'Pending', 'Reviewed'); -- Random review status
            SET rnd_create_nick_name = CONCAT('NickName', FLOOR(RAND() * 100)); -- Random creator nickname
            SET rnd_update_nick_name = CONCAT('NickName', FLOOR(RAND() * 100)); -- Random updater nickname
            SET rnd_intercept_man = CONCAT('InterceptMan', FLOOR(RAND() * 100)); -- Random intercept person
            SET rnd_intercept_time = NOW(); -- Current time as intercept time
            SET rnd_instance_code = CONCAT('Instance', FLOOR(RAND() * 10000)); -- Random instance code

            -- Insert data into orders table
            INSERT INTO orders (
                user_id, order_date, total_amount, task_number, base_unit_id, base_unit_code, base_unit_name, 
                base_planned_qty, base_allocated_qty, base_picked_qty, base_loaded_qty, base_shipped_qty, 
                base_diffied_qty, planned_cross_qty, real_cross_qty, base_replenish_qty, is_replenish, 
                diffied_man, diffied_time, is_occupy, occupy_name, reason_intercept, occupy_time, 
                print_mark, revision, create_user, create_time, update_user, update_time, tenant_code, 
                review_qty, review_status, create_nick_name, update_nick_name, intercept_man, intercept_time, instance_code
            ) VALUES (
                rnd_user_id, rnd_order_date, rnd_total_amount, rnd_task_number, rnd_base_unit_id, rnd_base_unit_code, 
                rnd_base_unit_name, rnd_base_planned_qty, rnd_base_allocated_qty, rnd_base_picked_qty, rnd_base_loaded_qty, 
                rnd_base_shipped_qty, rnd_base_diffied_qty, rnd_planned_cross_qty, rnd_real_cross_qty, rnd_base_replenish_qty, 
                rnd_is_replenish, rnd_diffied_man, rnd_diffied_time, rnd_is_occupy, rnd_occupy_name, rnd_reason_intercept, 
                rnd_occupy_time, rnd_print_mark, rnd_revision, rnd_create_user, rnd_create_time, rnd_update_user, rnd_update_time, 
                rnd_tenant_code, rnd_review_qty, rnd_review_status, rnd_create_nick_name, rnd_update_nick_name, rnd_intercept_man, 
                rnd_intercept_time, rnd_instance_code
            );

            SET j = j + 1;
        END WHILE;
        
        SET j = 0; -- Reset inner loop counter
        SET i = i + 1;
    END WHILE;
END $$

DELIMITER ;

-- 产生用户存储过程,1000DELIMITER $$

CREATE DEFINER=`root`@`localhost` PROCEDURE `create_users`()
BEGIN
    DECLARE i INT DEFAULT 0;
    DECLARE total_users INT DEFAULT 1000;
    DECLARE rnd_username VARCHAR(50);
    DECLARE rnd_email VARCHAR(100);

    WHILE i < total_users DO
        SET rnd_username = CONCAT('User', FLOOR(1 + RAND() * 10000000));
        SET rnd_email = CONCAT(rnd_username, '@example.com');
        INSERT INTO users (username, email) VALUES (rnd_username, rnd_email);
        SET i = i + 1;
    END WHILE;
END $$

DELIMITER ;

- 1000*1000


DELIMITER $$

CREATE DEFINER=`root`@`localhost` PROCEDURE `generate_orders`()
BEGIN
    DECLARE i INT DEFAULT 0;
    DECLARE total_users INT DEFAULT 1000; -- Number of users
    DECLARE total_orders_per_user INT DEFAULT 1000; -- Orders per user
    DECLARE rnd_user_id INT;
    DECLARE rnd_order_date DATE;
    DECLARE rnd_total_amount DECIMAL(10, 2);
    DECLARE j INT DEFAULT 0;

    WHILE i < total_users DO
        -- Fetch user ID
        SELECT user_id INTO rnd_user_id FROM users LIMIT i, 1;

        WHILE j < total_orders_per_user DO
            -- Generate random order date and total amount
            SET rnd_order_date = DATE_ADD('2020-01-01', INTERVAL FLOOR(RAND() * 1096) DAY); -- Random date between 2020-01-01 and 2022-12-31
            SET rnd_total_amount = ROUND(RAND() * 1000, 2); -- Random total amount between 0 and 1000
            -- Insert data into orders table
            INSERT INTO orders (user_id, order_date, total_amount) VALUES (rnd_user_id, rnd_order_date, rnd_total_amount);

            SET j = j + 1;
        END WHILE;
        
        SET j = 0; -- Reset inner loop counter

        SET i = i + 1;
    END WHILE;
END $$

DELIMITER ;

将users和orders的数据生成分开,这样可以通过多次调用orders存储过程多线程参数数据。调用一次
call create_users() 
然后开15个窗口调用orders存储过程
call generate_orders() 


整个过程会产生1000个用户
11*1000*1000也就是1500万条订单数据。

优化过程

数据量

select count(*) from orders 11000000

select count(*) from users 1000

原始SQL:无索引

这是一个很简单的sql,统计每个用户的订单总额。

在默认情况下,什么索引都没有创建,需要花费10min 的时间。

    
    -- 第一个版本
    SELECT a.*,sum(b.total_amount) as total from users a left join orders b  on a.user_id = b.user_id
    group by a.user_id;
    
    -- sql耗时 10min

image.png

可以看到什么索引也没使用,type为all,直接全表扫描。

  • 用时10min+。 explain分析如下: image.png

第一次优化:普通索引

把查询条件用到的sql条件都创建索引。也就是where和join、sum涉及到的知道。

sql
CREATE INDEX idx_orders_user_id ON orders (user_id);
CREATE INDEX idx_orders_total_amount ON orders (total_amount);
CREATE INDEX idx_users_user_id ON users (user_id);

加这三个索引耗时30s

type为index或者ref,全部走的索引。查询结果好了很多, 用了 12.6s+。也就是说查询变快了。 但是是由于mysql的回表机制接下来继续优化索引。

  • 在看看expalin的结果:

image.png

第二次优化:覆盖索引

覆盖索引是指一个索引包含了查询所需的所有列,从而可以满足查询的要求,而不需要访问实际的数据行。

通常情况下,数据库查询需要根据索引定位到对应的数据行,然后再从数据行中获取所需的列值。

而当索引中包含了查询所需的所有列时,数据库引擎可以直接通过索引就能够满足查询的要求,无需访问实际的数据行,这样就可以提高查询性能。

这也是普通索引添加了还是查询慢的原因,因为普通索引命中了还是会去找主键,通过主键找到关联字段的值做过滤。


-- 先删除普通索引
-- drop INDEX idx_orders_user_id ON orders;
-- drop INDEX idx_orders_total_amount ON orders;

-- 在创建覆盖索引
CREATE INDEX idx_orders_total_amount_user_id ON orders (total_amount,user_id);
CREATE INDEX idx_orders_user_id_total_amount ON orders (user_id,total_amount);

 -- sql耗时 33+s

1100万数据创建索引就花费了33+s。所以创建索引得适度。

sql
-- 查询sql还是第一个版本。
SELECT a.*,sum(b.total_amount) as total from users a left join orders b  on a.user_id = b.user_id
group by a.user_id;

-- sql耗时 579ms

先看看expalin的结果: image.png

可以看到orders表的type从index提升到了ref。此时的查询时间为从12.6s+降低到579ms了。 结果证明覆盖索引能提升查询速度。

问题就在于这次建的两个覆盖索引,

  • 只有 idx_orders_user_id_total_amount 降低了查询时间,
  • 而 idx_orders_total_amount_user_id没有。

这个和mysql的关键词执行顺序有一定关系(推测,没找到资料)。

mysql执行顺序如下:

    from
    on
    join
    where
    group by
    having
    select
    distinct
    unionallorder by
    limit

可以看到在覆盖索引使用过程先是where,再是到select的sum函数。这也是 idx_orders_user_id_total_amount 索引的创建顺序。

    drop INDEX idx_orders_user_id ON orders;
    drop INDEX idx_orders_total_amount ON orders;
    drop INDEX idx_orders_total_amount_user_id ON orders;

drop掉相关的多余索引可以发现执行查询时间没有变化,仍然为579ms。 索引优化这块差不多就是通过覆盖索引来命中索引。

第三次优化:减少数据量

减少数据量在业务上来说就是移除不必要的数据,或者可以在架构设计这块做一些工作。分表就是这个原则。

  • 通过这个方式能把千万的数据量减少到百万甚至几十万的量。提升的查询速度是可以想象的。
    -- 第三次优化:减少数据量
    SELECT a.*,sum(b.total_amount) as total from users a left join orders b  on a.user_id = b.user_id
    where a.user_id > 900
    group by a.user_id;
    
    -- sql耗时 250ms
  • expain结果如下: image.png

  • 可以看到users表的type为range。能过滤一部分数据量。

  • 查询时间从579ms降低到250ms,减少数据量证明有效。

第四次优化:小表驱动大表

在 MySQL 中,通常情况下,优化器会根据查询条件和表的大小选择合适的驱动表(即主导表)。

小表驱动大表是一种优化策略,它指的是在连接查询中,优先选择小表作为驱动表,以减少连接操作所需的内存和处理时间。

在第三次优化的结果上,可以尝试使用小表驱动大表优化策略。


    -- 第三个版本,小标驱动大表  没啥效果
    SELECT a.*,sum(b.total_amount) as total from users a
    left join (select user_id,total_amount from orders c where c.user_id > 1033 ) b  on a.user_id = b.user_id
    where a.user_id > 900
    group by a.user_id;
    
  -- sql耗时 250ms+

    

将left join的表修改为子查询,能提前过滤一部分数据量。

expain结果如下: image.png

可以看到explain没什么变化。实际执行效果也没啥变化。

小表驱动大表在这里无效,但是可以结合具体的业务进行优化sql。这个策略是没问题的。

第五次优化:强制索引

当 MySQL 中的 IN 子句用于查询千万级数据时,如果未正确设计和使用索引,可能导致索引失效,从而影响查询性能。

通常情况下,MySQL 的优化器会根据查询条件选择最优的执行计划,包括选择合适的索引。然而,对于大数据量的 IN 子句查询,MySQL 可能无法有效使用索引,从而导致全表扫描或索引失效。

查询sql如下,由于in的数据量不是很稀疏,实际查询强制索引和普通索引效果一致

sql

-- 第五个版本,强制索引 
SELECT a.*,sum(b.total_amount) as total from users a left join orders b force index (idx_orders_user_id_total_amount)  on a.user_id = b.user_id
where b.user_id in (1033,1034,1035,1036,1037,1038)
group by a.user_id;


-- 第五个版本,不走强制索引 
SELECT a.*,sum(b.total_amount) as total from users a left join orders b  on a.user_id = b.user_id
where b.user_id in (1033,1034,1035,1036,1037,1038)
group by a.user_id;

查询时间都是零点几秒。

笔者在实际业务中是遇到过这种场景的,业务sql更加复杂。这里由于临时创建的订单用户表没复现。

当你发现explain都是命中索引的,但是查询依然很慢。这个强制索引可以试试。

优化策略

  • 提前命中索引,小表驱动大表
  • 千万级数据in索引失效,进行强制索引
  • 使用覆盖索引解决回表问题

下次该怎么优化SQL

  • 数据接近千万级,需要分表,比如按照用户id取模分表。
  • 用汇总表代替子查询来命中索引,比如把小时表生成日表、月表汇总数据。
  • 关联字段冗余、直接放到一张表就是单表查询了。
  • 覆盖索引,空间换时间,这也是本文分析的场景。

关于命中索引核心点就是覆盖索引,再者是千万数据产生的特有场景需要走强制索引。

tips mysql的回表机制

在 MySQL 中,回表("ref" or "Bookmark Lookup" in English)是指在使用索引进行查询时,MySQL 首先通过索引找到满足条件的行的位置,然后再回到主表(或称为数据表)中查找完整的行数据的过程。

这个过程通常发生在某些查询中,特别是涉及到覆盖索引无法满足查询需求时。

当一个查询不能完全通过索引满足时,MySQL 就需要回到主表中查找更多的信息。这种情况通常出现在以下几种情况下:

  • 非覆盖索引查询: 如果查询需要返回主表中未包含在索引中的其他列的数据时,MySQL 就需要回到主表中查找这些额外的列数据。
  • 使用索引范围条件: 当查询中使用了范围条件(例如 BETWEEN>< 等),而索引只能定位到范围起始位置时,MySQL 需要回到主表中检查满足范围条件的完整行。
  • 使用了聚簇索引但需要查找的列不在索引中: 在使用了聚簇索引的表中,如果需要查询的列不在聚簇索引中,MySQL 需要回到主表中查找这些列的数据。

当 MySQL 需要执行回表操作时,会发生额外的磁盘访问,因为需要读取主表中的数据。这可能会导致性能下降,特别是在大型数据表中或者在高并发环境中。

为了尽量减少回表操作的发生,可以考虑以下几点:

  • 创建覆盖索引:确保查询所需的所有列都包含在索引中,从而避免回表操作。

  • 优化查询语句:尽量避免使用范围条件,或者确保所有的过滤条件都可以被索引完全匹配。

  • 考虑表设计:在设计数据库表结构时,可以考虑将常用的查询字段都包含在索引中,以减少回表操作的发生。