Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >MySQL千万级数据从190秒优化到1秒全过程

MySQL千万级数据从190秒优化到1秒全过程

原创
作者头像
codetrend
发布于 2024-04-24 02:36:00
发布于 2024-04-24 02:36:00
10.5K0
举报
文章被收录于专栏:编程技巧编程技巧

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

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

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

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

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

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

先来个千万级数据

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

表结构如下:

代码语言:sql
AI代码解释
复制
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;

造数据的存储过程如下。

用户数据:

代码语言:sql
AI代码解释
复制
-- 产生用户存储过程,1000个
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

订单数据生成存储过程如下:

代码语言:sql
AI代码解释
复制
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

将users和orders的数据生成分开,这样可以通过多次调用orders存储过程多线程参数数据。

调用一次call create_users(),然后开15个窗口调用orders存储过程call generate_orders()

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

原始SQL

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

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

代码语言:sql
AI代码解释
复制
-- 第一个版本
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;

explain分析如下:

id

select_type

table

partitions

type

possible_keys

key

key_len

ref

rows

filtered

Extra

1

SIMPLE

a

ALL

PRIMARY

1000

100.0

Using temporary

1

SIMPLE

b

ALL

13016086

100.0

Using where; Using join buffer (hash join)

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

用时191s。

第一次优化:普通索引

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

代码语言:sql
AI代码解释
复制
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);

查询sql仍然是第一个版本。

代码语言:sql
AI代码解释
复制
-- 第一个版本
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;

先看看expalin的结果:

id

select_type

table

partitions

type

possible_keys

key

key_len

ref

rows

filtered

Extra

1

SIMPLE

a

index

PRIMARY,idx_users_user_id

PRIMARY

4

1

100.0

1

SIMPLE

b

ref

idx_orders_user_id

idx_orders_user_id

5

test2.a.user_id

13003

100.0

type为index或者ref,全部走的索引。

查询结果却让人失望,这次用的时间更多,用了460+s。也就是说查询变慢了。

推测是由于mysql的回表机制导致查询变得更慢了。所以接下来继续优化索引。

第二次优化:覆盖索引

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

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

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

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

代码语言:sql
AI代码解释
复制
-- 先不删除普通索引
-- 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);

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

查询sql还是第一个版本。

代码语言:sql
AI代码解释
复制
-- 第一个版本
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;

先看看expalin的结果:

id

select_type

table

partitions

type

possible_keys

key

key_len

ref

rows

filtered

Extra

1

SIMPLE

a

index

PRIMARY,idx_users_user_id

PRIMARY

4

1

100.0

1

SIMPLE

b

ref

idx_orders_user_id,idx_orders_user_id_total_amount

idx_orders_user_id_total_amount

5

test2.a.user_id

874

100.0

Using index

可以看到orders表的type从index提升到了ref。

此时的查询时间为从460s+降低到10s了。

结果证明覆盖索引能提升查询速度。

问题就在于这次建的两个覆盖索引,只有 idx_orders_user_id_total_amount 降低了查询时间,而 idx_orders_total_amount_user_id没有。

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

mysql执行顺序如下:

代码语言:shell
AI代码解释
复制
from
on
join
where
group by
having
select
distinct
union (all)
order by
limit

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

代码语言:sql
AI代码解释
复制
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掉相关的多余索引可以发现执行查询时间没有变化,仍然为10s。

索引优化这块差不多就是通过覆盖索引来命中索引。

第三次优化:减少数据量

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

分表就是这个原则。

通过这个方式能把千万的数据量减少到百万甚至几十万的量。提升的查询速度是可以想象的。

代码语言:sql
AI代码解释
复制
-- 第三次优化:减少数据量
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 > 1033
group by a.user_id;

expain结果如下:

id

select_type

table

partitions

type

possible_keys

key

key_len

ref

rows

filtered

Extra

1

SIMPLE

a

range

PRIMARY,idx_users_user_id

PRIMARY

4

685

100.0

Using where

1

SIMPLE

b

ref

idx_orders_user_id_total_amount

idx_orders_user_id_total_amount

5

test2.a.user_id

874

100.0

Using index

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

查询时间从10s降低到7s,减少数据量证明有效。

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

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

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

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

代码语言:sql
AI代码解释
复制
-- 第三个版本,小标驱动大表  没啥效果
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 > 1033
group by a.user_id;

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

expain结果如下:

id

select_type

table

partitions

type

possible_keys

key

key_len

ref

rows

filtered

Extra

1

SIMPLE

a

range

PRIMARY,idx_users_user_id

PRIMARY

4

685

100.0

Using where

1

SIMPLE

c

ref

idx_orders_user_id_total_amount

idx_orders_user_id_total_amount

5

test2.a.user_id

874

100.0

Using where; Using index

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

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

第五次优化:强制索引

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

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

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

代码语言:sql
AI代码解释
复制
-- 第五个版本,强制索引 
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

explain结果type的含义

在 MySQL 的 EXPLAIN 查询结果中,type 字段表示了查询使用的访问类型,即查询执行过程中的访问方法。

根据不同的访问类型,MySQL 查询优化器将选择不同的执行计划。以下是 type 字段可能的取值及其含义:

  • system: 这是最好的情况,表示查询只返回一行结果。这通常是通过直接访问表的 PRIMARY KEY 或唯一索引来完成的。
  • const: 表示 MySQL 在查询中找到了常量值,这是在连接的第一个表中进行的。由于这是常量条件,MySQL 只会读取一次表中的一行数据。例如,通过主键访问一行数据。
  • eq_ref: 类似于 const,但在使用了索引的情况下。此类型的查询是通过某个唯一索引来访问表的,对于每个索引键值,表中只有一行匹配。常见于使用主键或唯一索引进行连接操作。
  • ref: 表示此查询使用了非唯一索引来查找值。返回的是所有匹配某个单独值的行。该类型一般出现在联接操作中,使用了非唯一索引或者索引前缀。
  • range: 表示查询使用了索引来进行范围检索,通常出现在带有范围条件的查询语句中,例如 BETWEENIN()>、<等。
  • index: 表示 MySQL 将扫描整个索引来找到所需的行。这通常是在没有合适的索引的情况下,MySQL 会选择使用这种访问类型。
  • all: 表示 MySQL 将扫描全表以找到所需的行,这是最差的情况。这种情况下,MySQL 将对表中的每一行执行完整的扫描。

通常来说,type 字段的排序从最好到最差依次是 systemconsteq_refrefrangeindexall,当然,实际情况取决于查询的具体情况、表结构和索引的使用情况。更好的查询性能通常对应着更好的 type 类型。

mysql的回表机制

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

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

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

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

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

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

  • 创建覆盖索引:确保查询所需的所有列都包含在索引中,从而避免回表操作。
  • 优化查询语句:尽量避免使用范围条件,或者确保所有的过滤条件都可以被索引完全匹配。
  • 考虑表设计:在设计数据库表结构时,可以考虑将常用的查询字段都包含在索引中,以减少回表操作的发生。

关于作者

来自一线全栈程序员nine的探索与实践,持续迭代中。

欢迎关注或者点个小红心~

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
从 12s 到 200ms,MySQL 两千万订单数据 6 种深度分页优化全解析
那晚,大约晚上 11 点,我与 Chaya 在丽江的洱海酒店享受两人世界的快乐,电商平台的运维大群突然炸开了锅。
码哥字节
2025/02/25
3530
从 12s 到 200ms,MySQL 两千万订单数据 6 种深度分页优化全解析
交付工程师准备工作
交付工程师(Delivery Engineer)是在软件开发项目中,负责将软件产品交付给客户并确保其顺利运行的工程师。他们的工作主要涵盖以下几个方面:
GeekLiHua
2025/01/21
1000
交付工程师准备工作
mysql┃explain 都不会用?怎么优化?
现在的java开发人员越来越多,竞争也越来越激烈,moon在某钩招聘网站上发布了一个岗位需求,不到短短1天就收到20多份简历,大部分都是应届一年两年的,新鲜血液越来越多,我们也要不断的提升自己才能够不被挤下去,大家可以看下各大网站的java岗位3年以上的招聘需求:
moon聊技术
2021/07/28
6600
mysql┃explain 都不会用?怎么优化?
MySQL怎样优化千万级数据
这里讨论的情况是在MySQL一张表的数据达到千万级别。表设计很烂,业务统计规则又不允许把sql拆成多个子查询。
闻说社
2024/05/21
1740
MySQL怎样优化千万级数据
10道常考SQL笔试题
题目:在 Employees 表中,获取每个部门(department)薪资最高的员工的姓名、部门和工资。表结构如下:
SQL数据库开发
2024/09/24
1780
10道常考SQL笔试题
【收藏】MySQL 超全优化清单(可执行系列)
先从一般的语句优化开始,其实对于很多规范大家并不陌生,可就是在用的时候,无法遵从,希望今天大家再过一遍,可以养成一种良好的数据库编码习惯。
lyb-geek
2024/07/17
2490
【收藏】MySQL 超全优化清单(可执行系列)
MySQL Hints:控制查询优化器的选择
MySQL Hints是一组特殊的注释或指令,可以直接嵌入到SQL查询中,以改变MySQL优化器的默认行为。这些Hints通常被用于解决性能问题,或者当开发者比优化器更了解数据分布和查询特性时,来指导优化器选择更好的查询计划。
公众号:码到三十五
2024/06/05
5960
MySQL Hints:控制查询优化器的选择
MySQL中的SQL优化建议那么多,该如何有的放矢
今天早上看到同事的一个优化需求,优化的时间其实不多,但是对于这条SQL的优化思考了很多,希望有一些参考。
jeanron100
2019/11/15
6940
最强总结!十大数据库索引类型详解!!
数据库索引是数据库性能优化的重要基础。选择正确的索引类型对提升查询性能至关重要。本文将通过理论讲解+案例分析的方式,帮您深入理解各类索引的原理和应用!
SQL数据库开发
2024/12/24
7150
最强总结!十大数据库索引类型详解!!
数据库优化 – SQL优化[通俗易懂]
前面一篇文章从实例的角度进行数据库优化,通过配置一些参数让数据库性能达到最优。但是一些“不好”的SQL也会导致数据库查询变慢,影响业务流程。本文从SQL角度进行数据库优化,提升SQL运行效率。
全栈程序员站长
2022/10/04
3.6K0
数据库优化 – SQL优化[通俗易懂]
工作中数据库优化技巧
内容整理于网络 一、EXPLAIN 做MySQL优化,我们要善用 EXPLAIN 查看SQL执行计划。 EXPLAIN 输出格式 EXPLAIN 命令的输出内容大致如下: mysql root@localhost:youdi_auth> explain select * from auth_user\G; ***************************[ 1. row ]*************************** id | 1 select_type |
若与
2018/04/25
7820
工作中数据库优化技巧
MySQL上亿数据查询优化:实践与技巧
随着大数据时代的到来,数据库管理系统需要处理越来越多的数据。MySQL作为一种流行的关系型数据库管理系统,被广泛应用于各类业务场景。然而,当数据量达到上亿级别时,查询性能可能会显著下降,严重影响应用的响应速度和用户体验。本文将详细介绍MySQL在处理上亿数据时的查询优化技巧,并通过实践案例展示如何有效提升查询性能。
洛秋_
2024/07/13
7060
MySQL的索引是什么?怎么优化?
索引类似大学图书馆建书目索引,可以提高数据检索的效率,降低数据库的IO成本。MySQL在300万条记录左右性能开始逐渐下降,虽然官方文档说500~800w记录,所以大数据量建立索引是非常有必要的。MySQL提供了Explain,用于显示SQL执行的详细信息,可以进行索引的优化。
吴生
2018/04/03
1.5K0
MySQL的索引是什么?怎么优化?
MySQL实战面试题(附案例答案+建表语句+模拟数据+案例深度解析),练完直接碾压面试官
使用YEAR()和MONTH()函数从signup_date字段中提取年份和月份,并匹配给定的条件。
小白的大数据之旅
2024/11/20
1610
8 种常见SQL错误用法,你千万别犯!
分页查询是最常用的场景之一,但也通常也是最容易出问题的地方。比如对于下面简单的语句,一般 DBA 想到的办法是在 type, name, create_time 字段上加组合索引。这样条件排序都能有效的利用到索引,性能迅速提升。
好好学java
2020/03/02
1K0
MySQL通过索引优化-这里可能有你不知道的索引优化细节(二)
用执行计划分别测试一下union all、in和or,发现union all分两步执行,而in和or只用了一步,效率高一点。
行百里er
2020/12/02
8960
MySQL通过索引优化-这里可能有你不知道的索引优化细节(二)
【随笔】MySQL 索引测试和性能优化指南
在数据库查询优化的过程中,索引扮演着至关重要的角色。合理使用索引不仅能大幅提升查询效率,还能降低数据库的负载。然而,不同类型的索引在不同场景下的表现可能存在较大差异,因此深入理解各类索引的特性以及 EXPLAIN 关键字的查询分析能力尤为重要。
框架师
2025/02/21
1680
【随笔】MySQL 索引测试和性能优化指南
MySQL 覆盖索引与延迟关联
本期来谈谈覆盖索引与延迟关联。在此之前,我们先简单建立一个订单表 Orders 用于举例说明。表中共包含 3 个字段:
江不知
2020/09/08
1.7K0
MySQL 覆盖索引与延迟关联
最强总结!数据库优化完全指南!!
数据库优化是提升应用性能的关键环节。本文将从多个维度系统地介绍数据库优化的方法和实践经验。
SQL数据库开发
2024/11/21
2290
最强总结!数据库优化完全指南!!
MySQL数据库基础与实战应用
MySQL是一种开源的关系型数据库管理系统,广泛应用于各种规模的应用程序和网站中。它以其稳定性、高性能和可扩展性而闻名,成为许多开发者和企业首选的数据库解决方案。本文将介绍MySQL数据库的基础知识,并探讨其在实际应用中的应用场景。
海拥
2023/09/01
2540
推荐阅读
相关推荐
从 12s 到 200ms,MySQL 两千万订单数据 6 种深度分页优化全解析
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档