【mysql】千万级数据MySQL索引优化实例

news2024/12/24 7:26:19

【mysql】千万级数据MySQL索引优化实例

  • 【一】场景描述
  • 【二】生成数千万条记录
  • 【三】原始sql分析
  • 【四】第一次优化:常规索引
  • 【五】第二次优化:覆盖索引
  • 【六】第三次优化:减少数据量
  • 【七】第四次优化:小表驱动大表
  • 【八】第五次优化:强制使用索引
  • 【九】优化策略汇总
  • 【十】索引分析的type含义
  • 【十一】MySQL 的表查找机制

【一】场景描述

针对千万级记录的MySQL有以下几种优化策略:
(1)分片(拆分表和数据库)
(2)创建汇总表
(3)修改查询以使用多个子查询

虑单个 MySQL 表包含数千万条记录的场景,表设计很差,业务规则不允许将 SQL 查询拆分为多个子查询,在这种情况下,开发人员可以尝试通过优化 SQL 来实现他们的查询目标。

当单个 MySQL 表包含数千万条记录时,就会出现一些特殊情况。本篇主要讨论极端情况下的SQL优化策略。

【二】生成数千万条记录

表结构如下:

create table `orders``order_id` int  NOT  NULL AUTO_INCREMENT`user_id` int  DEFAULT  NULL`order_date` date  NOT  NULL`total_amount` decimal102NOT  NULLPRIMARY KEY`order_id`),
  KEY`idx_user_id``user_id`)使用BTREEKEY`idx_user_amount``user_id``total_amount`)使用BTREEENGINE = InnoDB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_general_ci;创建表`users``user_id` int NOT NULL AUTO_INCREMENT`username` varchar50COLLATE utf8mb4_general_ci NOT NULL ,   `email` varchar ( 100 ) COLLATE utf8mb4_general_ci NOT NULL ,   `created_at`时间戳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;
 

生成数据的存储过程如下:

-- 生成 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
         
    

订单数据生成存储过程:

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允许多次调用generate_orders存储过程以利用多线程。

首先调用call create_users(),然后打开15个窗口调用订单存储过程call generate_orders()。

整个过程会产生1000个用户和1510001000,相当于1500万条订单记录。

【三】原始sql分析

这是一个非常简单的 SQL 查询,用于计算每个用户的订单总金额。

如果没有默认创建任何索引,则此查询需要超过 190 秒才能执行。

-- 第一个版本
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”,也就是全表扫描。

执行时间:191秒。

【四】第一次优化:常规索引

在 SQL 条件中使用的所有列上创建索引,包括WHERE、JOIN和SUM。

在订单 (user_id)上创建索引 idx_orders_user_id;在订单(total_amount)
上创建索引 idx_orders_total_amount ;在用户(user_id)
上创建索引 idx_users_user_id ;

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;

让我们检查一下解释结果:
在这里插入图片描述
类型为“index”或“ref”,所有查询都使用索引。

但结果却令人失望,执行时间增加到了460多秒。

这表明由于 MySQL 使用主键来定位关联字段的机制导致查询变慢。

【五】第二次优化:覆盖索引

覆盖索引是包含满足查询所需的所有列的索引,允许直接从索引执行查询而无需访问实际的数据行。

-- 暂时不删除常规索引
-- 删除 INDEX idx_orders_user_id ON orders; --
删除 INDEX idx_orders_total_amount ON orders 
;在orders ( total_amount, user_id ) 上
创建INDEX idx_orders_total_amount_user_id ;在 orders (user_id, total_amount) 上创建 INDEX idx_orders_user_id_total_amount;

对 1500 万条记录创建索引需要 300 多秒,因此应谨慎地进行索引。

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;

检查一下解释结果:
在这里插入图片描述
我们可以看到orders表的类型已经从“index”变为“ref”。

查询时间现已从 460 多秒减少到 10 秒。

这个结果证明了覆盖索引可以提高查询性能。

这里的关键观察是只有索引idx_orders_user_id_total_amount减少了查询时间,而idx_orders_total_amount_user_id没有。

这部分是由于 MySQL 关键字的执行顺序(假设的,因为没有找到明确的来源)。

MySQL执行顺序:

shell from 
on 
join 
where 
group  by 
having 
select 
distinct 
union (全部) 
order  by
 limit

覆盖索引使用函数WHERE之前的子句,与 的索引创建顺序一致。
SELECTSUMidx_orders_user_id_total_amount

删除订单上的索引idx_orders_user_id ;
删除订单上的索引 idx_orders_total_amount ;
删除订单上的索引 idx_orders_total_amount_user_id ;

删除冗余索引表明执行时间保持不变,仍为 10 秒。

因此这里的优化策略是使用覆盖索引来有效命中索引。

【六】第三次优化:减少数据量

减少数据量包括删除不必要的数据或进行架构调整。实现此目的的一种方法是拆分表。

通过这种方式,数据量可以从几千万级减少到几百万甚至几十万级,大大提高查询速度。

-- 优化三:减少数据量
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;

解释结果如下:
在这里插入图片描述
我们可以看到,该users表的类型是“范围”,过滤了一部分数据。

查询时间由原来的10秒减少到7秒,证明减少数据量是有效的。

【七】第四次优化:小表驱动大表

在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 >  1033 
GROUP  BY a.user_id;

此更改将LEFT JOIN表修改为子查询,提前过滤一些数据。

解释结果如下:
在这里插入图片描述
可以看出,解释结果没有太大变化,实际执行性能保持不变。

虽然“小表驱动大表”策略在这里并不有效,但根据具体的业务逻辑,它仍然可以是一种有效的优化策略。

【八】第五次优化:强制使用索引

当该IN子句用于涉及数千万条记录的查询时,设计不良的索引可能会导致索引效率低下,影响查询性能。

正常情况下,MySQL 优化器会选择最佳的执行计划,包括合适的索引。但是,对于大数据量的IN子句查询,MySQL 可能无法有效利用索引,从而导致全表扫描或索引效率低下。

以下 SQL 演示了这一点,由于IN数据非稀疏,强制索引和不强制索引会产生类似的结果:

-- 第五版,强制索引
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;

在两种情况下,查询时间都少于一秒。

在实际业务场景中,可能会出现这样的情况:尽管按照解释结果命中了索引,但查询仍然很慢。在这种情况下,可以尝试强制索引。

【九】优化策略汇总

(1)尽早命中索引,使用小表驱动大表。
(2)使用强制索引处理IN子句效率低下的问题。
(3)使用覆盖索引来避免表查找。

思路
(1)对于接近数千万的数据量,使用表分区,例如基于用户ID模数。
(2)用汇总表替换子查询来命中索引,例如生成每日或每月的汇总表。
(3)将冗余的连接字段直接放在单表中,方便单表查询。
(4)命中索引,用空间换取时间,这是本文分析的核心场景。

打索引的核心是覆盖索引,对于千万级的数据量,可能还需要强制索引。

【十】索引分析的type含义

在MySQL的EXPLAIN查询结果中,该type字段表示查询所使用的访问类型,代表查询执行时访问的方式。

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

(1)系统:最好的情况,表明查询返回单行结果,通常是通过直接访问PRIMARY KEY或唯一索引。
(2)const:表示 MySQL 在查询中发现一个常量值,该查询是在连接的第一个表中进行的。由于这是一个常量条件,因此 MySQL 只会从表中读取一行数据,例如,通过主键访问一行数据。
(3)eq_ref:与 类似const,但使用索引。此类查询使用唯一索引访问表,其中每个索引键值只有一行匹配。在使用主索引或唯一索引的连接中很常见。
(4)ref:表示查询使用非唯一索引来查找值。返回与单个值匹配的所有行,通常出现在使用非唯一索引或索引前缀的连接中。
(5)range:表示查询使用索引进行范围扫描,常见于具有范围条件的查询,例如,,,,BETWEEN等。IN()><
(6)index:表示 MySQL 将扫描整个索引来查找所需的行,通常是在没有合适索引可用时。
(7)all:表示 MySQL 将执行全表扫描来查找所需的行,这是最糟糕的情况。

通常,type字段值从好到坏的排序为system、const、eq_ref、ref、range、index,all但实际情况取决于具体查询、表结构和索引使用情况。更好的查询性能通常对应更好的type值。

【十一】MySQL 的表查找机制

在MySQL中,表查找(也称为“ref”或“Bookmark Lookup”)是指MySQL首先使用索引找到满足条件的行的位置,然后在主表中查找实际数据行的过程。

此过程通常发生在覆盖索引不能满足所有查询要求的查询中。

当索引不能完全满足查询时,MySQL 需要查找主表以获取更多信息。这通常发生在以下情况下:
(1)非覆盖索引查询:当查询需要返回索引中未包含的额外列时,MySQL 需要在主表中查找这些额外的列。
(2)使用索引范围条件:当使用范围条件(BETWEEN、、等)时,如果索引只能定位范围的开始,则 MySQL 需要检查主表以查找满足范围条件的完整行>。<
(3)使用聚集索引但所需列不在索引中:在具有聚集索引的表中,如果所需列不在聚集索引中,则 MySQL 需要在主表中查找这些列。

当 MySQL 执行表查找时,会发生额外的磁盘访问,因为它需要从主表读取数据,这可能会降低性能,尤其是在大型表或高并发环境中。

为了最大限度地减少表查找,请考虑以下事项:
(1)创建覆盖索引:确保查询所需的所有列都包含在索引中,以避免表查找。
(2)优化查询语句:尽量避免使用范围条件,或者保证所有的过滤条件都能被索引完全匹配。
(3)考虑表设计:在索引中包含经常查询的列以减少表查找的需要。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2171508.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

蓝桥杯模块二:数码管的静态、动态实现

模块二训练 1.静态显示 一、数码管电路图 二、电路分析 1.数码管电路分析 端口分公共端和段码&#xff0c;先用公共端控制一个数码管&#xff0c;再用段码实现显示数字。共阳数码管公共端输入高电平&#xff0c;段码输入低电平实现点亮 2.锁存器 Y7控制段码&#xff0c;Y6控…

机器学习学习笔记-20240927

文章目录 一些简单的指令数据操作广播机制 标量&#xff0c;向量&#xff0c;矩阵的相互求导1. 标量对标量的求导2. 标量对向量的求导3. 向量对标量的求导4. 向量对向量的求导5. 矩阵对标量的求导6. 矩阵对向量的求导 链式求导法则YYDS求出损失函数偏导为0时的最优解w*1. 损失函…

卷轴模式商城APP开发指南

卷轴模式商城APP的开发是一项融合了技术创新、用户体验优化与商业策略实施的综合性工程。本文将从程序员的角度出发&#xff0c;详细介绍该类型应用的开发流程&#xff0c;涵盖从需求分析到后期维护的各个环节。 一、需求分析 首先&#xff0c;明确APP的核心功能需求&#xff…

从0-1搭建海外社媒矩阵,详细方案深度拆解

做买卖&#xff0c;好的产品质量固然是关键&#xff0c;但如何让更多的消费者知道&#xff1f;营销推广是必不可少的。在互联网时代&#xff0c;通过社交媒体推广已经成为跨境电商卖家常用的广告手段。 如何通过海外社交媒体矩阵扩大品牌影响力&#xff0c;实现营销目标&#…

又一篇Nature!可解释GNN今年持续发力,创新思路有时候就这么简单!

最近发现了一篇优秀的Nature子刊论文&#xff0c;作者提出了一种基于可解释GNN癌症基因分析新框架&#xff0c;在预测任务中实现了卓越的性能表现。 除此之外&#xff0c;还出现了很多可解释GNN的新研究&#xff0c;其中顶会不少&#xff0c;可见无论在学术界和工业界&#xf…

AES CCM详解

AES CCM是一种对数据进行加密及完整性检查的算法&#xff0c;主要用到AES中的CBC(完整性检查)和CTR(对明文进行加密)&#xff0c;除此之外&#xff0c;还涉及到对数据的格式化(本文着重阐述)。 文章目录 加密过程STEPS 解密及校验过程STEPS 格式化B0的构成B0解析举例AAD的格式化…

企业微信扫码登录

请求url 可以看到如下结果&#xff1a; 请求的URL是 reqauth.aspx&#xff0c;这是发起认证的第一步&#xff0c;这个请求的返回结果是一个 XML 数据&#xff0c;包含一个 ReqID&#xff0c;用户授权的地址 AuthUrl 以及查询结果的地址 ResultUrl。 如果直接访问这个地址&…

sysctl 命令:Linux内核参数管理

一、命令简介 ​sysctl​是一个用于查看、设置和管理内核参数的命令行工具。 ‍ 二、命令参数 sysctl [选项] [内核参数]选项: ​-a​, --all​: 显示所有参数及其当前值。​-n​: 仅显示值&#xff0c;不显示参数名。​-w​: 设置参数的值。 ‍ 三、命令示例 查看所有…

理解:基础地理实体相关概述

理解&#xff1a;基础地理实体相关概述 地理实体 geo-entity 现实世界中占据一定且连续空间位置和范围、单独具有同一属 性或完整功能的地理对象。 基础地理实体 fundamental geo-entity 通过基础测绘采集和表达的地理实体&#xff0c;是其他地理实体和相关 信息的定位框架与…

Spring Security 是一个强大的和高度可定制的身份验证和访问控制框架。它是 Spring 项目家族的一员,用于构建安全的 Java 应用程序。

Spring Security 是一个强大的和高度可定制的身份验证和访问控制框架。它是 Spring 项目家族的一员&#xff0c;用于构建安全的 Java 应用程序。Spring Security 提供了全面的安全服务&#xff0c;从基本的登录认证到复杂的访问控制&#xff0c;几乎涵盖了所有与安全相关的需求…

线程与线程安全,生产消费者模型

线程与进程 2390. 从字符串中移除星号 给你一个包含若干星号 * 的字符串 s 。 在一步操作中&#xff0c;你可以&#xff1a; 选中 s 中的一个星号。移除星号 左侧 最近的那个 非星号 字符&#xff0c;并移除该星号自身。 返回移除 所有 星号之后的字符串**。** 注意&#xff1a…

ubuntu 设置静态IP

一、 ip addresssudo nano /etc/netplan/50-cloud-init.yaml 修改前&#xff1a; 修改后&#xff1a; # This file is generated from information provided by the datasource. Changes # to it will not persist across an instance reboot. To disable cloud-inits # ne…

深度解析与解决方案:U盘有盘符但无法打开的困境

引言&#xff1a;U盘困境初现 在日常工作与生活中&#xff0c;U盘作为便携式存储设备&#xff0c;扮演着数据传输与备份的重要角色。然而&#xff0c;不少用户会遇到这样一个棘手问题&#xff1a;U盘在插入电脑后能够正常显示盘符&#xff0c;但尝试打开时却遭遇拒绝访问或提示…

巧用时间换空间:解读 ArcGraph 如何灵活应对有限内存下的图分析

导读&#xff1a;ArcGraph 是一款云原生架构、存查分析一体化的分布式图数据库。本文将详细解读 ArcGraph 如何灵活应对有限内存下的图分析。 01 引言 在图分析技术广泛应用的当下&#xff0c;学术界和各大图数据库厂商热衷于提升图分析技术的高性能指标。然而&#xff0c;追求…

STM32嵌入式编程学习到提高:【5】delay函数

------------------------------------------------------------------------------------------------------------------------- 工程文件&#xff1a;放在百度云盘里&#xff0c;需要的自行下载&#xff01;&#xff01;&#xff01; 链接:http:// https://pan.baidu.com/s…

【3D分割】Segment Anything in 3D with Radiance Fields

论文链接&#xff1a;Segment Anything in 3D with Radiance Fields 代码链接&#xff1a;GitHub - Jumpat/SegmentAnythingin3D: Segment Anything in 3D with NeRFs (NeurIPS 2023) 作者&#xff1a;Jiazhong Cen, Jiemin Fang, Zanwei Zhou, Chen Yang, Lingxi Xie, Xiaop…

DataLight(V1.4.5) 版本更新,新增 Ranger、Solr

DataLight&#xff08;V1.4.5&#xff09; 版本更新&#xff0c;新增 Ranger、Solr DataLight 迎来了重大的版本更新&#xff0c;现已发布 V1.4.5 版本。本次更新对平台进行了较多的功能拓展和优化&#xff0c;新增了对 Ranger 和 Solr 服务组件的支持&#xff0c;同时对多项已…

微服务的优点及在云原生时代的合理落地方式

云计算de小白 那么&#xff0c;微服务到底能给业务带来什么好处&#xff1f;在云原生时代&#xff0c;如何更合理地实现微服务&#xff1f; 架构没有好坏之分&#xff0c;只有适合与不适合。然而&#xff0c;当我们对比微服务架构与单体架构时&#xff0c;可以发现微服务有以…

【【通信协议之UDP协议】】

通信协议之UDP协议 UDP &#xff08;user Datagram Protocol )用户数据报协议 整个的UDP数据格式 如下所示 TCP协议与UDP协议的区别 TCP协议面向连接&#xff0c;是流传输协议&#xff0c;通过连接发送数据&#xff0c;而 UDP 协议传输不需要连接&#xff0c;是数据包协议 …

[3]Opengl ES着色器

术语&#xff1a; VertexShader&#xff1a;顶点着色器&#xff0c;用来描述图形图像位置的顶点坐标&#xff1b; FragmentShader&#xff1a;片元着色器&#xff0c;用来给顶点指定的区域进行着色&#xff1b; Vertex&#xff1a;顶点 Texture&#xff1a;纹理…