面试官:Mysql千万级大表如何进行深度分页优化?

这篇具有很好参考价值的文章主要介绍了面试官:Mysql千万级大表如何进行深度分页优化?。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

背景

假如有一张千万级的订单表,这张表没有采用分区分表,也没有使用ES等技术,分页查询进行到一定深度分页之后(比如1000万行后)查询比较缓慢,我们该如何进行优化?

数据准备

订单表结构如下:

CREATE TABLE `t_order` (  
    `id` BIGINT ( 20 ) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增主键',  
    `order_no` VARCHAR ( 16 ) NOT NULL DEFAULT '' COMMENT '订单编号',  
    `customer_no` VARCHAR ( 16 ) NOT NULL DEFAULT '' COMMENT '客户编号',  
    `order_status` TINYINT ( 4 ) NOT NULL DEFAULT 0 COMMENT '订单状态',  
    `warehouse_code` VARCHAR ( 16 ) NOT NULL DEFAULT '' COMMENT '发货地仓库编码',  
    `country` VARCHAR ( 16 ) NOT NULL DEFAULT '' COMMENT '收货人国家',  
    `state` VARCHAR ( 32 ) NOT NULL DEFAULT '' COMMENT '收货人州',  
    `city` VARCHAR ( 32 ) NOT NULL DEFAULT '' COMMENT '收货人城市',  
    `street` VARCHAR ( 256 ) NOT NULL DEFAULT '' COMMENT '收货人街道',  
    `zip_code` VARCHAR ( 32 ) NOT NULL DEFAULT '' COMMENT '收货人邮编',  
    `contact_email` VARCHAR ( 128 ) NOT NULL DEFAULT '' COMMENT '收货人邮箱',  
    `contact_name` VARCHAR ( 32 ) NOT NULL DEFAULT '' COMMENT '收货人姓名',  
    `contact_mobile` VARCHAR ( 32 ) NOT NULL DEFAULT '' COMMENT '收货人手机号',  
    `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',  
    `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',  
    `deleted` TINYINT ( 2 ) NOT NULL DEFAULT 0 COMMENT '是否已被删除',  
    PRIMARY KEY ( `id` ),  
    KEY `idx_customer` ( `customer_no`, `deleted` ),  
    KEY `idx_create_time` ( `create_time`, `deleted` )  
) ENGINE = INNODB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8mb4 COMMENT = '销售订单表';

其中Mysql版本为8.0。我们使用Python脚本向表中插入2000万条数据。

import pymysql  
from faker import Faker  
import random  
from datetime import datetime  
from concurrent.futures import ThreadPoolExecutor  
  
# MySQL 连接配置  
db_config = {  
   'host': 'your_database_host', 
   'user': 'your_database_user', 
   'password': 'your_database_password', 
   'database': 'your_database_name'
}  
  
# 创建 MySQL 连接  
conn = pymysql.connect(**db_config)  
cursor = conn.cursor()  
  
# 使用 Faker 生成模拟数据  
fake = Faker() 

# 获取国家下发货仓库编码  
def generate_warehousecode(country):  
    if country == "US":  
        return "US-"+random.choice(["WEST", "EAST", "MIDDLE", "SOUTH", "NORTH"])+"-0" + str(random.choice([1, 2, 3, 4, 5]))  
    else:  
        return country + "00" + str(random.choice([1, 2, 3, 4, 5]))

# 插入 t_order 表数据(多线程并发,每个线程插入1万条,共2000个线程)  
def insert_data_thread(thread_id):  
    # 创建 MySQL 连接  
    conn = pymysql.connect(**db_config)  
    cursor = conn.cursor()  
  
    order_data = []  
    for _ in range(10000):  
        order_no = "OC"+ fake.uuid4()[:12]  # 取前16位  
        customer_no = fake.uuid4()[:16]
        order_status = random.choice([1, 2, 3, 4, 5])  
        country = random.choice(  
            ["CA", "US", "MX", "JP", "UK", "TR", "DE", "ES", "FR", "IT", "NL", "PL", "SE", "BR", "CN"])  
        warehouse_code = generate_warehousecode(country)  
        state = fake.uuid4()[:16]  
        city = fake.uuid4()[:16]  
        street = fake.uuid4()  
        zip_code = fake.uuid4()[:6]  
        contact_email = fake.email()  
        contact_name = fake.name()  
        contact_mobile = fake.phone_number()  
        create_time = fake.date_time_between(start_date=datetime(2019, 1, 1), end_date=datetime.now())  
        update_time = create_time  
        deleted = 0  # 默认未删除  
  
        cursor.execute("""  
            INSERT INTO t_order (                order_no, customer_no, order_status, warehouse_code, country,                state, city, street, zip_code, contact_email, contact_name,                contact_mobile, create_time, update_time, deleted            )            VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)        """, (order_no, customer_no, order_status, warehouse_code, country,  
              state, city, street, zip_code, contact_email, contact_name,  
              contact_mobile, create_time, update_time, deleted))  
  
        order_data.append((cursor.lastrowid, order_no, customer_no, create_time))  # 保存插入的行的 ID  
    # 提交 t_order 数据插入  
    conn.commit()  
    print(thread_id+ "已经跑完10000条数据。。。。。。。。。")  
    # 关闭数据库连接  
    cursor.close()  
    conn.close()  
  
# 使用 ThreadPoolExecutor 并发插入  
with ThreadPoolExecutor(max_workers=10) as executor:  # 可以根据需要调整最大线程数  
    executor.map(insert_data_thread, range(500))

问题复现

导出数据时我们需要按照时间倒序。所以我们先执行以下sql查询前100条

select * FROM t_order ORDER BY create_time desc LIMIT 100;

共花费210ms。执行计划如下:

然后我们继续执行sql,我们从第100万行开始取100条数据:

select * FROM t_order ORDER BY create_time desc LIMIT 1000000,100;

此时耗时3分2秒,耗时明显增加。执行计划如下:


由执行计划看,此时索引已经失效了。。。。

我们继续执行sql,从第1000万行开始取100条数据:

select * FROM t_order ORDER BY create_time desc LIMIT 10000000,100;

此时耗时4分14秒,时间真是太长了,执行计划如下:

后面还有接近1000万条数据没有取出来,直接就废了。

原因分析

当我们使用 LIMIT offset, count 查询语句进行深度分页查询时,例如 LIMIT 10000000,100 ,会发生以下过程:

  1. MySQL首先会根据给定条件从相应的索引树中查找m+n条记录。对于聚集索引来说,它直接找到需要的结果即丢弃前offset条数据,返回count条数据并返回;而对于二级索引,则可能涉及回表操作。
  2. 如果使用的是二级索引,在查到m+n条记录后还需要通过这些记录所关联的主键ID去聚集索引里再次搜索出完整的行数据,然后再丢弃掉前offset条数据,返回count条数据。因此在这个过程中可能会产生大量的“回表”操作,这将导致性能下降。

我们借助B+ Tree Visualization演示一下这张表的索引结构:

  • 聚集索引(主键ID)
  • 二级索引(idx_create_time)

以上述例子来说,当我们查询LIMIT 10000000,100时,它会先从二级索引中查询10000000+100条记录对应的ID,然后再用这些记录的ID去聚集索引中查询ID对应的记录,然后舍弃掉前10000000条数据,返回后100条数据。

所以当offset+count量很大时,Mysql的执行器认为全表扫描的性能更由于使用索引,所以也导致索引失效。所以我们要做的尽可能的减少回表的记录数量。

解决方案

使用子查询

我们改造sql,通过一个子查询按照create_time倒排,获取第offset + 1条记录的最新的create_time,create_time直接从二级索引上可以获取,不会进行回表,然后我们再根据这个create_time传递到主查询时,取100条数据,即回表数据也仅仅只有count条即100条数据,大大减少了回表的记录数量。

SELECT * FROM t_order  
WHERE create_time <= (
SELECT create_time FROM t_order ORDER BY create_time desc LIMIT 1000000,1
)
ORDER BY create_time desc LIMIT 100;

查询第100万时耗时556毫秒。

执行结果,执行计划


可以看出主查询以及子查询都使用到了索引,回表查询的数据记录数也大大减少。

继续查询到第1000万行时耗时接近6秒。
执行结果,执行计划


可以看出主查询以及子查询都使用到了索引,回表查询的数据记录数也大大减少。

这种方式需要create_time 的分布是相对均匀的,否则可能会导致某个时间段内的数据较多,影响查询性能。

INNER JOIN

我们改造sql,create_timeid都存储在二级索引中,我们获取这两列值不需要回表,所以我们创建一个偏移量为offset,个数为count并且包含create_time以及id的临时表,临时表中数据不需要回表。然后再跟自身通过主键ID进行关联,仅需要回表count条数据,大大减少了回表的记录格式。同时也使用了主键索引关联,效率也大大提高。

SELECT torder.* FROM  t_order torder
	INNER JOIN (
	SELECT id FROM t_order ORDER BY create_time DESC LIMIT 1000000,100 
	) tmp ON torder.id = tmp.id 
ORDER BY
	create_time DESC

查询第100万时耗时260毫秒。
执行结果,执行计划。

继续查询到第1000万行时耗时接近2秒
执行结果,执行计划

这种方式要保证INNER JOIN使用了合适的索引。

SEARCH AFTER

每次查询都保留上次的最小的create_time,然后下次查询只查询比上一页的create_time小的数据。单表查询,并且使用索引,回表数据少,不需要子查询以及关联查询,查询效率高。类似ES的SEARCH AFTER的查询方式。

-- 我们模拟连续分页到第1000000页,最小的一条数据的create_time
SELECT * FROM t_order  
ORDER BY create_time ASC  LIMIT 1000000, 1

SELECT * FROM t_order  
WHERE create_time <= '2023-01-22 00:00:00' 
ORDER BY create_time desc LIMIT 100;

查询第100万时耗时142毫秒。

执行结果,执行计划

继续查询到第1000万行时耗时244毫秒

执行结果,执行计划

当然该种方式缺点也很明显:只能支持连续分页,不能支持获取随意页的数据。

其他方案

  • 限制查询范围: 在需求层面,可以限制只能查询前100页数据,或者规定只能获取某个时间段内的数据,从而避免深度分页。
  • 水平分表:考虑将数据按照某个维度进行水平分表,以减小单表的数据量
  • 使用ES,Hive,ClickHouse等OLAP方案

本文已收录于我的个人博客:码农Academy的博客,专注分享Java技术干货,包括Java基础、Spring Boot、Spring Cloud、Mysql、Redis、Elasticsearch、中间件、架构设计、面试题、程序员攻略等文章来源地址https://www.toymoban.com/news/detail-825188.html

到了这里,关于面试官:Mysql千万级大表如何进行深度分页优化?的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

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

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

相关文章

  • 阿里二面:千万级、亿级数据,如何性能优化? 教科书级 答案来了

    在尼恩指导了几百个小伙伴的面试,在这些过程中, 非常、非常高频的一个面试题: 千万级数据,如何做性能优化? 亿级数据,如何做性能优化? 最近,有个小伙伴阿里二面,又遇到了这个问题。 其实,尼恩一直想梳理一个教科书式的答案, 但是由于千万级数据、亿级数

    2024年02月02日
    浏览(48)
  • kafka千万级数据积压原因以及解决方案

    一、原因 kafka作为消息队列,其中数据积压也是经常遇到的问题之一。 我们都知道,数据积压的直接原因,一定是系统中的某个部分出现了性能问题,来不及处理上游发送的数据,才会导致数据积压。 那么我们就需要分析在使用kafka时,如何通过优化代码以及参数配置来最大

    2024年02月12日
    浏览(45)
  • 千万级数据深分页查询SQL性能优化实践

    如何在Mysql中实现上亿数据的遍历查询?先来介绍一下系统主角:关注系统,主要是维护京东用户和业务对象之前的关注关系;并对外提供各种关系查询,比如查询用户的关注商品或店铺列表,查询用户是否关注了某个商品或店铺等。但是最近接到了一个新需求,要求提供查

    2024年02月11日
    浏览(59)
  • ElasticSearch + Canal 开发千万级的实时搜索系统【转】

    我们总结一下新搜索系统需要解决的几个问题: 海量请求。几百万的请求毫无压力,上千万上亿也要可以扛得住。 实时搜索。指的是当一个用户修改了其数据之后,另一个用户能实时地搜索到改用户。 海量请求。要扛得起海量的搜索请求,可以使用ElasticSearch来实现,它是在

    2024年04月09日
    浏览(39)
  • 千万级数据并发解决方案(理论+实战) 高并发解决思路 方案

    课程地址 项目地址 秒杀 高并发 新闻系统 超大数据量 一般的网站 写入的少 读取的次数多 模糊查询 数据量少的时候可以用 like 数据量多的时候用 Elasticsearch搜索引擎 占用磁盘空间比较大 在laravel中 创建 要提前配置好数据库 在Tests文件中写入 运行命令 php artisan app:tests 生成

    2024年02月08日
    浏览(86)
  • Java中处理千万级数据的最佳实践:性能优化指南

    在今天的数字化时代,处理大规模数据已经成为许多Java应用程序的核心任务。无论您是构建数据分析工具、实现实时监控系统,还是处理大规模日志文件,性能优化都是确保应用程序能够高效运行的关键因素。本指南将介绍一系列最佳实践,帮助您在处理千万级数据时提高

    2024年02月03日
    浏览(51)
  • 千万级数据的表,我把慢sql优化后性能提升30倍!

    背景:系统中有一个统计页面加载特别慢,前端设置的40s超时时间都加载不出来数据,因为是个统计页面,基本上一猜就知道是mysql的语句有问题,遗留了很久没有解决,正好趁不忙的时候,下定决心一定把它给搞定! (mysql5.7) 执行一下问题sql,可以看到单表查就需要61s 这

    2024年02月14日
    浏览(50)
  • 数据量超过亿级别,MySQL大表迁移该如何做?

    MySQL 作为当前应用最广泛的开源关系型数据库之一,具有高性能、稳定性和易用性等特性,是许多网站、应用和商业产品的主要数据存储。在一些场景中,如果出现单表行数上亿的情况,就可能需要开发和 DBA 对大表进行优化:分表、归档或扩容操作,而在这些操作中都涉及

    2024年02月11日
    浏览(43)
  • 权威答案!灵犀医疗引入 Zilliz Cloud,千万级向量数据库赋能医学 AIGC 平台

    “医疗行业是一个信息差较大的行业,术语体系庞杂且知识门类较多,如何能搜索到最精准的医学知识并采用最合理方式进行总结,这是我们医学 AIGC 平台 EviMed 所遇见的最主要的技术问题。 传统的数据库和全文检索方式难以满足我们的技术要求,结合了 Zilliz Cloud 向量数据

    2024年04月25日
    浏览(41)
  • EMQX+阿里云飞天洛神云网络 NLB:MQTT 消息亿级并发、千万级吞吐性能达成

    随着物联网技术的发展与各行业数字化进程的推进,全球物联网设备连接规模与日俱增。一个可靠高效的物联网系统需要具备高并发、大吞吐、低时延的数据处理能力,支撑海量物联网数据的接入与分析,从而进一步挖掘数据价值。 于今年五月发布的 EMQX 5.0 版本全球首个实

    2023年04月15日
    浏览(47)

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

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

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

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

二维码1

领取红包

二维码2

领红包