MySQL千万级别数据快速查询数据优化,从100多s到1s

389 阅读4分钟

先创建测试数据表

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`),
  KEY `idx_user_id` (`user_id`) USING BTREE,
  KEY `idx_user_amount` (`user_id`,`total_amount`) USING BTREE
) 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`),
  KEY `idx_user_id` (`user_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

再创建存储过程生成千万测试数据

创建用户表的存储过程

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

创建订单表的存储过程

CREATE DEFINER=`root`@`localhost` PROCEDURE `generate_orders`()
BEGIN
    DECLARE i INT DEFAULT 0;
    DECLARE total_users INT DEFAULT 1000; -- 用户数量
    DECLARE total_orders_per_user INT DEFAULT 1000; -- 每个用户的订单数量
    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
        -- 获取用户ID
        SELECT user_id INTO rnd_user_id FROM users LIMIT i, 1;

        WHILE j < total_orders_per_user DO
            -- 生成订单日期和总金额
            SET rnd_order_date = DATE_ADD('2020-01-01', INTERVAL FLOOR(RAND() * 1096) DAY); -- 2020-01-01和2022-12-31之间的随机日期
            SET rnd_total_amount = ROUND(RAND() * 1000, 2); -- 0到1000之间的随机总金额
            -- 将数据插入订单表
            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;

        SET i = i + 1;
    END WHILE;
END

进入测试阶段

1. 不加索引测试查询用户的订单总额

explain
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

测试结果如下

直接全表扫描,类型是all

image.png

image.png

执行结果时间如下,700多s还没结束

image.png

2. 加普通索引

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

-- 添加普通索引
create index idx_orders_user_id on orders (user_id);
create index idx_orders_total_amount on orders(total_amount);

image.png

再执行explain语句看看

type为index,ref,全部有走索引

image.png 但这里没显示使用,可能走回表扫描

image.png

看下执行速度,执行300多s,看来还是

image.png

3. 覆盖索引

覆盖索引就是一个索引包含了要查询的所有列

sql的执行顺序

image.png

image.png

image.png 所以按照select部分,那就是创建联合索引,

-- 删除原来索引
drop index idx_orders_user_id on orders;
drop index idx_orders_total_amount on orders;
-- 添加覆盖索引
create index idx_orders_user_id_total_amount on orders(user_id, total_amount);

这次加索引还挺快的 image.png 再执行explain看看,这次走索引了,extra列也有using index了

image.png

执行看看有没有变快,这次从几百s到10几s

image.png 证明了覆盖索引提升了查询速度,但还是不够快

4. 进行第三步优化,减少数据量

减少数据量就是减少查询不必要的数据量

比如你你要查询的是user_id大于50的用户总金额【具体情况再具体分析】

-- 查询user_id大于50的
explain
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 > 50
group by a.user_id

再执行explain看看,index变成了range

image.png

explain执行结果中,type类型执行效率排序

image.png 再执行下看下速度看看,15s比上次好一点点

image.png

5. 接下来就是小表驱动大表

-- 小表驱动大表
explain
select
a.*,
sum(b.total_amount) as total
from users a
left join (
select
user_id,
total_amount
from orders
where user_id > 60
) b
on a.user_id = b.user_id
where a.user_id > 60
group by a.user_id

执行结果

image.png 看下执行速度,14s多比刚才好一点点

image.png

6. 使用强制索引

-- 强制索引
explain
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 a.user_id in (10,20,30,40,50, 60, 70, 80, 90)
group by a.user_id

使用后和使用前在这里显示不是很大,一般都是有效果的

回表机制

回表就是在查询过程中,命中索引,会找到索引所在行位置,再回到主表去找行的完整数据的过程

总结

  1. 提前命中索引,小表驱动大表
  2. 数据接近千万级,要进行分表
  3. 如果是汇总数据,那平时,做汇总之前的数据,做汇总表会更快
  4. 使用覆盖索引解决回表问题
  5. 千万级in查询索引失效,那可以使用强制索引
  6. 查询select时尽量查询指定字段不要select * 所有字段

image.png