百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

第39期:MySQL 时间类分区写 SQL 注意事项

wxin55 2024-11-14 18:44 20 浏览 0 评论

上篇《MySQL 时间类分区具体实现》介绍了时间类分区的实现方法,本篇是对上篇的一个延伸,介绍基于此类分区的相关 SQL 编写注意事项。

对于分区表的检索无非有两种,一种是带分区键,另一种则不带分区键。一般来讲检索条件带分区键则执行速度快,不带分区键则执行速度变慢。这种结论适应于大多数场景,但不能以偏概全,要针对不同的分区表定义来写最合适的 SQL 语句。用分区表的目的是为了减少 SQL 语句检索时的记录数,如果没有达到预期效果,则分区表只能带来副作用。 接下来我列举几个经典的 SQL 语句:

细心的读者在阅读完上篇可能心中就有一些疑问,基于表 ytt_p1 的 SQL 语句如下:

select count(*) from ytt_pt1 where log_date >='2018-01-01' and log_date <'2019-01-01';

同样是分区表 ytt_pt1_month1 ,基于这张表的 SQL 语句如下:

select count(*) from ytt_pt1_month1 where log_date in ('2020-01-01','2020-01-02','2020-01-03','2020-01-04','2020-01-05','2020-01-06','2020-01-07','2020-01-08','2020-01-09','2020-01-10','2020-01-11','2020-01-12','2020-01-13','2020-01-14','2020-01-15');

两张表的检索需求类似,为何写法差异不小? 后者为何要写成列表形式而不继续写成简单的范围检索形式?带着这点疑问,我们继续。

MySQL 针对分区表有一项优化技术叫 partition pruning ,翻译过来就是分区裁剪。其大致含义是 MySQL 会根据 SQL 语句的过滤条件对应的分区函数进行计算,并把计算结果穿透到底层分区表从而减小扫描记录数的一种优化策略。对于时间类型(DATE,TIMESTAMP,TIME,DATETIME),MySQL 仅支持部分函数的分区裁剪:to_days,to_seconds,year,unix_timestamp。那么我们再来看之前的疑问:表 ytt_pt1_month1 分区函数为 month ,MySQL 分区表虽然支持 month 函数,但是分区裁剪技术却不包含这个函数。 接下来,分两部分来介绍本篇内容。

第一、来体验下 MySQL 的分区裁剪技术,新建一张表 pt_pruning:分区函数为 to_days 。

create table pt_pruning (
id int,
r1 int,
r2 int,
log_date date)
partition by range(to_days(log_date))
(
PARTITION p_01 VALUES LESS THAN (to_days('2020-02-01')) ENGINE = InnoDB,
 PARTITION p_02 VALUES LESS THAN (to_days('2020-03-01')) ENGINE = InnoDB,
 PARTITION p_03 VALUES LESS THAN (to_days('2020-04-01')) ENGINE = InnoDB,
 PARTITION p_04 VALUES LESS THAN (to_days('2020-05-01')) ENGINE = InnoDB,
 PARTITION p_05 VALUES LESS THAN (to_days('2020-06-01')) ENGINE = InnoDB,
 PARTITION p_06 VALUES LESS THAN (to_days('2020-07-01')) ENGINE = InnoDB,
 PARTITION p_07 VALUES LESS THAN (to_days('2020-08-01')) ENGINE = InnoDB,
 PARTITION p_08 VALUES LESS THAN (to_days('2020-09-01')) ENGINE = InnoDB,
 PARTITION p_09 VALUES LESS THAN (to_days('2020-10-01')) ENGINE = InnoDB,
 PARTITION p_10 VALUES LESS THAN (to_days('2020-11-01')) ENGINE = InnoDB,
 PARTITION p_11 VALUES LESS THAN (to_days('2020-12-01')) ENGINE = InnoDB,
 PARTITION p_12 VALUES LESS THAN (to_days('2021-01-01')) ENGINE = InnoDB,
 PARTITION p_max VALUES LESS THAN MAXVALUE ENGINE = InnoDB
)

此表包含2020年一整年的数据,大概100W条,此处省略造数据过程。

(localhost:ytt)<mysql>select min(log_date),max(log_date),count(*) from pt_pruning;
+---------------+---------------+----------+
| min(log_date) | max(log_date) | count(*) |
+---------------+---------------+----------+
| 2020-01-02    | 2020-12-31    |  1000000 |
+---------------+---------------+----------+
1 row in set (0.72 sec)

分别执行下面几条 SQL :

SQL 1:求日期包含 '2020-01-02' 的记录条数。

SQL 1: select count(*) from pt_pruning where log_date <= '2020-01-02';

SQL 2 和 SQL 3 : 求2020年1月份的记录条数。

SQL 2: select count(*) from pt_pruning where log_date < '2020-02-01';

SQL 3:  select count(*) from pt_pruning where log_date between '2020-01-01' and '2020-01-31';

SQL 1 和 SQL 2 执行时间为0.04秒,SQL 3 执行时间为0.06秒。 在没有使用索引的条件下效果还是比较理想的。

(localhost:ytt)<mysql> select count(*) from pt_pruning where log_date <= '2020-01-02';
+----------+
| count(*) |
+----------+
|     2621 |
+----------+
1 row in set (0.04 sec)

(localhost:ytt)<mysql>select count(*) from pt_pruning where log_date < '2020-02-01';
+----------+
| count(*) |
+----------+
|    82410 |
+----------+
1 row in set (0.04 sec)

(localhost:ytt)<mysql>select count(*) from pt_pruning where log_date between '2020-01-01' and '2020-01-31';
+----------+
| count(*) |
+----------+
|    82410 |
+----------+
1 row in set (0.06 sec)

所以切记使用 MySQL 分区裁剪技术规定的分区函数来建立分区表,这样写 SQL 就会相对随意些。如果由于历史原因,分区表没有使用以上规定的分区函数,可以有以下两项可能的优化策略:

  1. 手工改 SQL 语句让其达到最优。
  2. 加 HINT 来提示 MySQL 使用具体的分区。

第二、如果分区表使用的分区函数未满足 MySQL 分区裁剪技术的规则,该如何优化此类 SQL 语句?

为避免和上篇内容混淆,建张新表 pt_month,复制表 ytt_pt1_month1 的表定义。表 pt_month 和表 pt_pruning 一样,存放了2020年一整年的记录,总条数也为100W。

(localhost:ytt)<mysql>select min(log_date),max(log_date),count(*) from pt_month;
+---------------+---------------+----------+
| min(log_date) | max(log_date) | count(*) |
+---------------+---------------+----------+
| 2020-01-02    | 2020-12-31    |  1000000 |
+---------------+---------------+----------+
1 row in set (0.72 sec)

再次执行之前的三条 SQL ,并把表名替换为 pt_month :

SQL 1 执行时间为1.26秒,相比之前慢了不少。查看执行计划,发现未使用 MySQL 分区裁剪技术,扫描了不必要的表分区。(这里是全部表分区)

(localhost:ytt)<mysql>select count(*) from pt_month where log_date <= '2020-01-02';
+----------+
| count(*) |
+----------+
|     2621 |
+----------+
1 row in set (1.26 sec)

(localhost:ytt)<mysql>explain 
    -> select count(*) from pt_month where log_date <= '2020-01-02'\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: pt_month
   partitions: p_01,p_02,p_03,p_04,p_05,p_06,p_07,p_08,p_09,p_10,p_11,p_max
...
         rows: 992805
     filtered: 33.33
        Extra: Using where
1 row in set, 1 warning (0.00 sec)

接下来对 SQL 1 进行一个简单的优化:既然是求日期为’2020-01-02‘ 那天的记录,那就不要使用<=来过滤,直接用=过滤:执行时间0.03秒。 查看执行计划,改后的 SQL 直接定位到表分区 p_01 ,达到了分区裁剪的效果。

(localhost:ytt)<mysql>select count(*) from pt_month where log_date = '2020-01-02';
+----------+
| count(*) |
+----------+
|     2621 |
+----------+
1 row in set (0.03 sec)

(localhost:ytt)<mysql>explain 
    -> select count(*) from pt_month where log_date = '2020-01-02'\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: pt_month
   partitions: p_01
         type: ALL
...
         rows: 82522
     filtered: 10.00
        Extra: Using where
1 row in set, 1 warning (0.00 sec)

继续执行 SQL 2 和 SQL 3 :执行时间都是1秒到2秒之间,效率很差,也未使用 MySQL 分区裁剪技术。

(localhost:ytt)<mysql>select count(*) from pt_month where log_date < '2020-02-01';
+----------+
| count(*) |
+----------+
|    82410 |
+----------+
1 row in set (1.35 sec)

(localhost:ytt)<mysql>select count(*) from pt_month where log_date between '2020-01-01' and '2020-01-31';
+----------+
| count(*) |
+----------+
|    82410 |
+----------+
1 row in set (1.93 sec)

来继续优化 SQL 2 和 SQL 3,由于两个需求一致,可以把范围检索改为指定列表检索:执行时间仅为0.04秒。

(localhost:ytt)<mysql>select count(*) from pt_month where log_date in ('2020-01-01','2020-01-02','2020-01-03','2020-01-04','2020-01-05','2020-01-06','2020-01-07','2020-01-08','2020-01-09','2020-01-10','2020-01-11','2020-01-12','2020-01-13','2020-01-14','2020-01-15','2020-01-16','2020-01-17','2020-01-18','2020-01-19','2020-01-20','2020-01-21','2020-01-22','2020-01-23','2020-01-24','2020-01-25','2020-01-26','2020-01-27','2020-01-28','2020-01-29','2020-01-30','2020-01-31');
+----------+
| count(*) |
+----------+
|    82410 |
+----------+
1 row in set (0.04 sec)

把范围查询改为 IN 列表后,效率得到很大提升,查询计划显示 MySQL 优化器只在分区 p_01 上检索记录。

...
   partitions: p_01
...

除了改造 SQL 语句,还可以给语句加 HINT 的方式来让 MySQL 使用分区裁剪技术:比如给 SQL 2 加上 HINT 后,执行时间为0.04秒,和之前改造后的语句执行效率相当。

(localhost:ytt)<mysql>select count(*) from pt_month partition (p_01) where log_date < '2020-02-01';
+----------+
| count(*) |
+----------+
|    82410 |
+----------+
1 row in set (0.04 sec)

总结:

如果由于历史原因分区表未使用 MySQL 分区裁剪技术,可以按照以下规则来手动对分区表进行裁剪优化:

  1. select * from tbname where partition_key = value;
  2. select * from tbname where partition_key in (value1,value2,...,valueN);
  3. 以上两种规则对于多表 JOIN 依然适用。

关于 MySQL 的技术内容,你们还有什么想知道的吗?赶紧留言告诉小编吧!

相关推荐

ES6中 Promise的使用场景?(es6promise用法例子)

一、介绍Promise,译为承诺,是异步编程的一种解决方案,比传统的解决方案(回调函数)更加合理和更加强大在以往我们如果处理多层异步操作,我们往往会像下面那样编写我们的代码doSomething(f...

JavaScript 对 Promise 并发的处理方法

Promise对象代表一个未来的值,它有三种状态:pending待定,这是Promise的初始状态,它可能成功,也可能失败,前途未卜fulfilled已完成,这是一种成功的状态,此时可以获取...

Promise的九大方法(promise的实例方法)

1、promise.resolv静态方法Promise.resolve(value)可以认为是newPromise方法的语法糖,比如Promise.resolve(42)可以认为是以下代码的语...

360前端一面~面试题解析(360前端开发面试题)

1.组件库按需加载怎么做的,具体打包配了什么-按需加载实现:借助打包工具(如Webpack的require.context或ES模块动态导入),在使用组件时才引入对应的代码。例如在V...

前端面试-Promise 的 finally 怎么实现的?如何在工作中使用?

Promise的finally方法是一个非常有用的工具,它无论Promise是成功(fulfilled)还是失败(rejected)都会执行,且不改变Promise的最终结果。它的实现原...

最简单手写Promise,30行代码理解Promise核心原理和发布订阅模式

看了全网手写Promise的,大部分对于新手还是比较难理解的,其中几个比较难的点:状态还未改变时通过发布订阅模式去收集事件实例化的时候通过调用构造函数里传出来的方法去修改类里面的状态,这个叫Re...

前端分享-Promise可以中途取消啦(promise可以取消吗)

传统Promise就像一台需要手动组装的设备,每次使用都要重新接线。而Promise.withResolvers的出现,相当于给开发者发了一个智能遥控器,可以随时随地控制异步操作。它解决了三大...

手写 Promise(手写输入法 中文)

前言都2020年了,Promise大家肯定都在用了,但是估计很多人对其原理还是一知半解,今天就让我们一起实现一个符合PromiseA+规范的Promise。附PromiseA+规范地址...

什么是 Promise.allSettled()!新手老手都要会?

Promise.allSettled()方法返回一个在所有给定的promise都已经fulfilled或rejected后的promise,并带有一个对象数组,每个对象表示对应的pr...

前端面试-关于Promise解析与高频面试题示范

Promise是啥,直接上图:Promise就是处理异步函数的API,它可以包裹一个异步函数,在异步函数完成时抛出完成状态,让代码结束远古时无限回掉的窘境。配合async/await语法糖,可...

宇宙厂:为什么前端离不开 Promise.withResolvers() ?

大家好,很高兴又见面了,我是"高级前端进阶",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发。1.为什么需要Promise.with...

Promise 新增了一个超实用的 API!

在JavaScript的世界里,Promise一直是处理异步操作的神器。而现在,随着ES2025的发布,Promise又迎来了一个超实用的新成员——Promise.try()!这个新方法简...

一次搞懂 Promise 异步处理(promise 异步顺序执行)

PromisePromise就像这个词的表面意识一样,表示一种承诺、许诺,会在后面给出一个结果,成功或者失败。现在已经成为了主流的异步编程的操作方式,写进了标准里面。状态Promise有且仅有...

Promise 核心机制详解(promise机制的实现原理)

一、Promise的核心状态机Promise本质上是一个状态机,其行为由内部状态严格管控。每个Promise实例在创建时处于Pending(等待)状态,此时异步操作尚未完成。当异步操作成功...

javascript——Promise(js实现promise)

1.PromiseES6开始支持,Promise对象用于一个异步操作的最终完成(包括成功和失败)及结果值的表示。简单说就是处理异步请求的。之所以叫Promise,就是我承诺,如果成功则怎么处理,失败怎...

取消回复欢迎 发表评论: