MySQL千万数据查询优化之路

这篇具有很好参考价值的文章主要介绍了MySQL千万数据查询优化之路。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

MySQL千万数据查询优化之路

本文主要针对 MySQL 在千万级别数据的分页查询性能进行优化, 下面是整个优化的过程.

先说结论

先说结论, MySQL 在千万级别数据的分页查询性能主要受到 2 个因素的影响:

  • 查询的偏移量
  • 查询的数据量

查询的偏移量优化

当 MySQL 执行查询语句分页 LIMIT 时, 有 2 个步骤需要先按照指定的排序规则对数据进行排序, 然后跳过指定的偏移量。

如果查询的偏移量比较大, 那么排序的时间就会比较长(B+树 索引可以极大优化该阶段性能)

但是 B+树 在跳过指定的偏移量时, 需要顺序遍历, O(n) 的复杂度, 千万级的偏移量也是比较慢

优化思路:

  • 给排序的字段加上B+树索引
  • 使用子查询确定查询范围(比如, 主键的范围, BETWEEN 等)
  • 连表查询, 小表驱动大表, 通过小表的索引来确定大表的范围, 减少偏移量

查询的数据量优化

  • 指定列替代 SELECT *
  • 减少不需要的列, 特别是大字段
  • 一次尽可能按需查询较少的数据条数
  • 缓存查询结果 (比如 redis) 来减少查询次数

准备数据

建表

CREATE TABLE `big_tables` (
  `id` bigint unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(256DEFAULT NULL,
  `age` bigint DEFAULT NULL,
  `data` longblob,
  PRIMARY KEY (`id`)
ENGINE=InnoDB AUTO_INCREMENT=10010001 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

导入数据

我使用的是 golang gorm 来导入数据, 代码如下:

var (
 configFile = "../config-dev.yaml"
 DB         *gorm.DB
)

type BigTable struct {
 ID   uint64 `gorm:"column:id;primary_key;auto_increment"`
 Name string `gorm:"column:name"`
 Age  int    `gorm:"column:age"`
 Data []byte `gorm:"column:data"`
}

func TestBitTable_InsertData(t *testing.T) {
 var err error

 DB.AutoMigrate(&BigTable{})

 // 关闭日志
 DB.Logger = logger.Default.LogMode(logger.Silent)

 // 批量插入 1000w 条数据, 每次插入 10000 条
 batches := 1000
 size := 10_000
 bigTables := make([]*BigTable, 0, size)
 for i := 0; i < batches; i++ {
  for i := 0; i < size; i++ {
   bigTables = append(bigTables, &BigTable{
    Name: utils.RandString(10),
    Age:  utils.RandInt(10),
    Data: utils.RandBytes(10),
   })
  }
  var task = func(idx int, db *gorm.DB) {
   err = db.CreateInBatches(bigTables, size).Error
   if err != nil {
    t.Error(err)
   }
   log.Printf("批次: %v, 完成 \n", idx)
  }

  task(i, DB)

  // 清空
  bigTables = bigTables[:0]
 }

 log.Printf("\n插入完成\n")
}

一分钟左右就可以导入 1000w 条数据

查看导入的数据

mysql> select count(*) from big_tables;
+----------+
| count(*) |
+----------+
| 10010000 |
+----------+
1 row in set (1.27 sec)

普通查询

查询 offset 为 1w, 10w, 100w, 1000w 的 1 条数据

命令

select * from big_tables limit 100001;
select * from big_tables limit 1000001;
select * from big_tables limit 10000001;
select * from big_tables limit 100000001;

结果

mysql> select * from big_tables limit 100001;
+-------+------------+-----+------------+
| id    | name       | age | data       |
+-------+------------+-----+------------+
| 10001 | I6pC5NBFD9 |   7 | x4zXHhnPnW |
+-------+------------+-----+------------+
1 row in set (0.10 sec)

mysql> select * from big_tables limit 1000001;
+--------+------------+-----+------------+
| id     | name       | age | data       |
+--------+------------+-----+------------+
| 100001 | PzpzEZDX9G |   0 | B48IvBLlWo |
+--------+------------+-----+------------+
1 row in set (0.13 sec)

mysql> select * from big_tables limit 10000001;
+---------+------------+-----+------------+
| id      | name       | age | data       |
+---------+------------+-----+------------+
| 1000001 | 4niiNSTHtx |   5 | tdCK9VuVWJ |
+---------+------------+-----+------------+
1 row in set (0.52 sec)

mysql> select * from big_tables limit 100000001;
+----------+------------+-----+------------+
| id       | name       | age | data       |
+----------+------------+-----+------------+
| 10000001 | R0HWlAyf7R |   0 | kHDTpsmtcg |
+----------+------------+-----+------------+
1 row in set (5.86 sec)

表格:

偏移量 查询时间
1w 0.10s
10w 0.13s
100w 0.52s
1000w 5.86s

可以看到 1w 到 10w 的查询时间基本不变, 但是 100w 到 1000w 的查询时间基本线性增长

因为 B+树 在跳过指定的偏移量时, 需要顺序遍历, O(n) 的复杂度

查询 offset 为 10, 100, 1000, 10000 条数据

命令

select * from big_tables limit 10000010;
select * from big_tables limit 100000100;
select * from big_tables limit 1000001000;
select * from big_tables limit 10000010000;

结果

mysql> select * from big_tables limit 10000010;
# 数据太多, 省略
10 rows in set (0.21 sec)

mysql> select * from big_tables limit 100000100;
# 数据太多, 省略
100 rows in set (0.35 sec)

mysql> select * from big_tables limit 1000001000;
# 数据太多, 省略
1000 rows in set (1.93 sec)

mysql> select * from big_tables limit 10000010000;
# 数据太多, 省略
10000 rows in set (21.20 sec)

表格

数据量 查询时间
10 0.21s
100 0.35s
1000 1.93s
10000 21.20s

可以看到, 数据量越大, 查询时间越长. 数据 1000-10000 的查询时间基本线性增长 (这里我的 MySQL 就在本机上, 如果是远程 MySQL 网络 IO 产生的时间将更长)

但是一般查询的数据量不会太大, 一般都是 10 条左右

优化方案如下:

  • 指定列替代 SELECT *
  • 减少不需要的列, 特别是大字段
  • 一次尽可能按需查询较少的数据条数
  • 缓存查询结果 (比如 redis) 来减少查询次数

优化方案比较简单, 容易理解, 后面就不再赘述了

优化: 偏移量导致的查询慢

1. 子查询

先查询 id 的位置, 然后再根据 id 的位置查询数据

命令

select * from big_tables where id >= (
    select id from big_tables limit 100000001
limit 01;

结果

mysql> select * from big_tables where id >= (
    select id from big_tables limit 100000001
limit 01;
+----------+------------+-----+------------+
| id       | name       | age | data       |
+----------+------------+-----+------------+
| 10000001 | R0HWlAyf7R |   0 | kHDTpsmtcg |
+----------+------------+-----+------------+
1 row in set (2.69 sec)

表格

是否使用子查询 偏移量 查询时间
1000w 2.69s
1000w 5.86s

可以看到, 使用子查询后, 查询时间减少了50%以上.

但是还是在秒级别, 达不到毫秒级别的业务需求

2. 子查询 EXPLAIN 分析

子查询命令

explain select * from big_tables where id >= (
    select id from big_tables limit 100000001
limit 01;

结果

mysql> explain select * from big_tables where id >= (
    select id from big_tables limit 10000000, 1
limit 0, 1;
+----+-------------+------------+------------+-------+---------------+---------+---------+------+---------+----------+-------------+
| id | select_type | table      | partitions | type  | possible_keys | key     | key_len | ref  | rows    | filtered | Extra       |
+----+-------------+------------+------------+-------+---------------+---------+---------+------+---------+----------+-------------+
|  1 | PRIMARY     | big_tables | NULL       | range | PRIMARY       | PRIMARY | 8       | NULL |   19290 |   100.00 | Using where |
|  2 | SUBQUERY    | big_tables | NULL       | index | NULL          | PRIMARY | 8       | NULL | 9750719 |   100.00 | Using index |
+----+-------------+------------+------------+-------+---------------+---------+---------+------+---------+----------+-------------+
2 rows in set (3.20 sec)

子查询(第二行):

  • 子查询的 typeindex, 表示使用了索引扫描全表
  • 子查询的 keyPRIMARY, 表示使用了主键索引
  • 子查询的 rows9750719, 表示扫描了 9750719 行数据 (粗略计算的, 因为 MySQL 每页 16KB)
  • 子查询的 ExtraUsing index, 表示使用了覆盖索引

主查询(第一行):

  • 主查询 typerange, 表示使用了索引范围扫描
  • 主查询 keyPRIMARY, 表示使用了主键索引
  • 主查询 rows19290, 表示扫描了 19290 行数据
  • 主查询 ExtraUsing where, 表示使用了 where 条件

从上面可以看出: 子查询使用了索引扫描全表, 扫描行数量千万级别, 所以查询时间很长

给主键加上唯一索引

给主键加上 B+树 的唯一索引

命令

# add unique index
alter table big_tables add unique index id(idusing btree;
# query2
select * from big_tables where id >= (
    select id from big_tables limit 100000001
limit 01;
# explain query2
explain select * from big_tables where id >= (
    select id from big_tables limit 100000001
limit 01;

结果

mysql> # add unique index
alter table big_tables add unique index id(id) using btree;
Query OK, 0 rows affected (35.82 sec)
Records: 0  Duplicates: 0  Warnings: 0

mysql> # query2
select * from big_tables where id >= (
    select id from big_tables limit 10000000, 1
limit 0, 1;
+----------+------------+-----+------------+
| id       | name       | age | data       |
+----------+------------+-----+------------+
| 10000001 | R0HWlAyf7R |   0 | kHDTpsmtcg |
+----------+------------+-----+------------+
1 row in set (1.25 sec)
mysql> # explain query2
explain select * from big_tables where id >= (
    select id from big_tables limit 10000000, 1
limit 0, 1;
+----+-------------+------------+------------+-------+---------------+---------+---------+------+---------+----------+-------------+
| id | select_type | table      | partitions | type  | possible_keys | key     | key_len | ref  | rows    | filtered | Extra       |
+----+-------------+------------+------------+-------+---------------+---------+---------+------+---------+----------+-------------+
|  1 | PRIMARY     | big_tables | NULL       | range | PRIMARY,id    | PRIMARY | 8       | NULL |   19290 |   100.00 | Using where |
|  2 | SUBQUERY    | big_tables | NULL       | index | NULL          | id      | 8       | NULL | 9750719 |   100.00 | Using index |
+----+-------------+------------+------------+-------+---------------+---------+---------+------+---------+----------+-------------+
2 rows in set (1.47 sec)

表格

状态 偏移量 查询时间
不用子查询 1000w 5.86s
子查询 1000w 2.69s
子查询 + 唯一索引 1000w 1.25s

可以看到, 给主键加上唯一索引后, 查询时间减少了50%以上

如果主键不是递增的, 比如是字符串, 需要用 IN 查询

因为某些 mysql 版本不支持在 in 子句中使用 limit, 所以这里多嵌套了一层子查询

select * from big_tables where id in (
    select id from (
        select id from big_tables limit 100000001
    ) as t
limit 01;

如果主键是线性递增, 可以使用 WHERE 优化

上面我们知道, 查询的消耗主要是在索引遍历的过程中, 如果id是连续递增的, 可以使用 WHERE 来优化

# query3
select * from big_tables where id >= 10000000 limit 01;
# explain query3
explain select * from big_tables where id >= 10000000 limit 01;

结果

mysql> # query3
select * from big_tables where id >= 10000000 limit 0, 1;
+----------+------------+-----+------------+
| id       | name       | age | data       |
+----------+------------+-----+------------+
| 10000000 | Hey8TWX966 |   7 | kSjxDkL1qj |
+----------+------------+-----+------------+
1 row in set (0.08 sec)

mysql> # explain query3
explain select * from big_tables where id >= 10000000 limit 0, 1;
+----+-------------+------------+------------+-------+---------------+---------+---------+------+-------+----------+-------------+
| id | select_type | table      | partitions | type  | possible_keys | key     | key_len | ref  | rows  | filtered | Extra       |
+----+-------------+------------+------------+-------+---------------+---------+---------+------+-------+----------+-------------+
|  1 | SIMPLE      | big_tables | NULL       | range | PRIMARY,id    | PRIMARY | 8       | NULL | 19298 |   100.00 | Using where |
+----+-------------+------------+------------+-------+---------------+---------+---------+------+-------+----------+-------------+
1 row in set (0.13 sec)

性能从原来的 5.86s 降低到了 0.08s, 提升 73 倍.

因为 rows 扫描行数不再是千万级别, 而只有一页的大小

为什么给主键加上唯一索引查询更快

在 MySQL 中,新增的唯一索引需要查询 2 次,第一次是查询索引树,第二次是查询数据页,而主键索引是唯一索引,所以查询主键索引时,只需要查询一次索引树即可。

但是在测试中, 1000万的偏移量的查询下, 再给主键加上唯一索引查询更快, 这是为什么呢?

参考:

  • https://dba.stackexchange.com/questions/290617/why-does-mysql-workbench-let-me-add-unique-indexes-to-primary-keys
  • https://stackoverflow.com/questions/75937219/why-add-unique-index-to-primary-index-is-faster

本文由 mdnice 多平台发布文章来源地址https://www.toymoban.com/news/detail-408585.html

到了这里,关于MySQL千万数据查询优化之路的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包赞助服务器费用

相关文章

  • 【MySQL进阶之路丨第八篇】MySQL数据的插入及查询

    【MySQL进阶之路丨第八篇】MySQL数据的插入及查询

    在上一篇中我们介绍了数据表的创建和删除;在开发中,了解数据库中数据的运行方式、运行逻辑是十分重要的。 这一篇我们使用命令行方式来帮助读者掌握MySQL数据的插入及查询。 上一篇链接: MySQL | 数据表的创建与删除 在MySQL数据库中,使用 INSERT INTO 语句来插入数据。

    2024年02月08日
    浏览(12)
  • Mysql如何优化数据查询方案

    Mysql如何优化数据查询方案

    mysql做读写分离 读写分离是提高mysql并发的首选方案。 Mysql主从复制的原理 mysql的主从复制依赖于binlog,也就是记录mysql上的所有变化并以二进制的形式保存在磁盘上,复制的过程就是将binlog中的数据从主库传输到从库上。 主从复制过程详细分为3个阶段: 第一阶段:主库写

    2024年02月21日
    浏览(8)
  • 【性能优化】MySql数据库查询优化方案

    【性能优化】MySql数据库查询优化方案

    了解系统运行效率提升的整体解决思路和方向 学会MySQl中进行数据库查询优化的步骤 学会看慢查询、执行计划、进行性能分析、调优 ​关于这个问题,我们通常首先考虑的是硬件升级,毕竟服务器的内存、CPU、磁盘IO速度 、网络速度等都是制约我们系统快慢的首要因素。硬

    2024年02月03日
    浏览(9)
  • 面试官:一千万的数据,你是怎么查询的

    面试官:一千万的数据,你是怎么查询的

    面试官:来说说,一千万的数据,你是怎么查询的? B哥:直接分页查询,使用limit分页。 面试官:有实操过吗? B哥:肯定有呀 此刻献上一首《凉凉》 也许有些人没遇过上千万数据量的表,也不清楚查询上千万数据量的时候会发生什么。 今天就来带大家实操一下,这次是基

    2024年02月03日
    浏览(7)
  • 面试官:一千万的数据,你是怎么查询的?

    面试官:一千万的数据,你是怎么查询的?

    对于1千万的数据查询,主要关注分页查询过程中的性能 针对偏移量大导致查询速度慢: 先对查询的字段创建唯一索引 根据业务需求,先定位查询范围(对应主键id的范围,比如大于多少、小于多少、IN) 查询时,将第2步确定的范围作为查询条件 针对查询数据量大的导致查

    2023年04月20日
    浏览(7)
  • MySQL 大数据in查询该怎么优化

    对于 MySQL 中的大数据 IN 查询,可以使用以下方法进行优化: 尽可能使用索引。如果使用 IN 操作符的字段有索引,MySQL 将会使用索引来加速查询。 尽量使用常量作为 IN 操作符中的参数,而不是使用子查询或者表达式。因为 MySQL 在优化器中使用常量比较容易,因此常量可以提

    2024年02月11日
    浏览(7)
  • MySQL 联表查询重复数据并删除(子查询删除记录) SQL优化

    数据库表介绍: table_a :主表(小表,表数据不可重复) table_b :流水表(大表,记录审核流水数据) 注:两表表结构大致一致,流水表增加一个审核状态的字段 业务逻辑: 主表保存唯一数据,流水表记录审核流水数据,用于后续展示,并在审核成功后插入主表,在插入流

    2023年04月08日
    浏览(11)
  • MYSQL中1000万条数据你是怎么查询的,查询非常慢怎么优化

    MYSQL中1000万条数据你是怎么查询的,查询非常慢怎么优化

     数据量越大,花费时间越长   偏移量越大,花费时间越长(浅层分页到深层分页) SQL优化: 1.MySQL自身 2.网络IO 3.SQL自身 --原SQL --避免使用select *  全表扫描,查询效率慢 --按需查找字段    使用索引扫描,主键索引,进行提升 --覆盖索引 查询字段索引覆盖,通过辅助索引提

    2024年02月06日
    浏览(7)
  • MySQL大数据量分页查询方法及其优化

    ---方法1: 直接使用数据库提供的SQL语句 ---语句样式: MySQL中,可用如下方法: SELECT * FROM 表名称 LIMIT M,N ---适应场景: 适用于数据量较少的情况(元组百/千级) ---原因/缺点: 全表扫描,速度会很慢 且 有的数据库 结果集返回不稳定 (如某次返回1,2,3,另外的一次返回2,1,3). Limit限制的是从

    2024年02月15日
    浏览(11)
  • MySQL数据库索引优化指南:提升查询效率的利器

    MySQL数据库索引优化指南:提升查询效率的利器

    本文将详细探讨MySQL数据库索引的概念、作用以及不同类型的索引,包括主键索引、唯一索引和普通索引。通过实际案例分析,我们将深入理解索引的工作原理,并提供实用的优化建议,帮助读者提升数据库性能。

    2024年02月09日
    浏览(36)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包