第七章-MySQL 高级特性

——「高性能MYSQL」读书笔记(第七章)

高性能****MYSQL

MYSQL经典书籍,常读常新。

分区表

1.1 分区表概念

​ 分区表就是按照某种规则将一个大表分成若干个小表,规则可能是某一个列的值分区分,或者涉及某个列的表达式。这样大量的数据被分散到不同表中,数据量变小查询性能自然变高。

使用分区表的好处

表数据非常大或者表数据有明显的冷热数据之分。

分区表的数据更容易维护,比如批量删除 可以清除整个分区形式并对独立分区进行优化、检查、修复。

分区表的数据可以维护在不同的物理设备,水平扩展性好。

避免InnoDB的单索引互斥访问,减少锁竞争。

使用分区表的限制

所有分区必须使用相同的存储引擎。

每张表的分区数有限,最大分区数目不能超过1024。

分区表达式只能是整数或者返回整数的表达式(函数)。

分区所用的字段,必须是唯一索引或者主键(主键也是唯一索引)。

无法使用外键、全文索引。

1.2 分区表类型

查看mysql是否支持分区

image

RANGE分区:基于属于一个给定连续区间的列值,把多行分配给分区。

LIST分区:类似于按RANGE分区,区别在于LIST分区是基于列值匹配一个离散值集合中的某个值来进行选择。

HASH****分区:基于用户定义的表达式的返回值来进行选择的分区,该表达式使用将要插入到表中的这些行的列值进行计算。这个函数可以包含MySQL 中有效的、产生非负整数值的任何表达式。

KEY分区:类似于按HASH分区,区别在于KEY分区只支持计算一列或多列,且MySQL服务器提供其自身的哈希函数。必须有一列或多列包含整数值。

1.3 分区表使用

range分区类型

创建分区表

-- 创建表
DROP TABLE IF EXISTS `sales`;
CREATE TABLE `sales` (
  `id` int(11) NOT NULL,
  `money` decimal(11,2) NOT NULL,
  `order_date` date NOT NULL,
  PRIMARY KEY (`id`,`order_date`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
 PARTITION BY RANGE (YEAR(order_date))
(PARTITION p_2020 VALUES LESS THAN (2021) ENGINE = InnoDB,
 PARTITION p_2021 VALUES LESS THAN (2022) ENGINE = InnoDB,
 PARTITION p_max VALUES LESS THAN MAXVALUE ENGINE = InnoDB);
复制代码

使用日期范围range 进行分区。,order_date 为分区字段 则必须包含在主键字段内,否则会报如下错误

A PRIMARY KEY must include all columns in the table's partitioning function

对于 MySQL 分区表,无法从数据库层面保证非分区列在表级别的唯一性,只能确保其在分区内的唯一性。

插入数据

-- 插入数据 
INSERT INTO `sales` (`id`, `money`, `order_date`) VALUES('1','123.34','2021-12-01');
INSERT INTO `sales` (`id`, `money`, `order_date`) VALUES('2','12132.33','2022-01-15');
INSERT INTO `sales` (`id`, `money`, `order_date`) VALUES('3','32432.31','2020-03-22');
复制代码

查询分区统计

 -- 查询数据分区分布
SELECT 
  PARTITION_NAME AS "分区",TABLE_ROWS AS "行数"
FROM
  information_schema.partitions 
WHERE table_schema = "cds-temp" 
 AND table_name = "sales"
复制代码

image

list分区类型

-- 创建表
DROP TABLE IF EXISTS `sales`;

CREATE TABLE `sales` (
  `id` INT(11) NOT NULL,
  `money` DECIMAL(11,2) NOT NULL,
  `score` INT(2),
  `order_date` DATE NOT NULL,
  PRIMARY KEY (`id`,`score`)
) ENGINE=INNODB DEFAULT CHARSET=utf8
PARTITION BY LIST(score)
( 
  PARTITION pEven VALUES IN (1,3,5,7,9),
  PARTITION pOdd VALUES IN (2,4,6,8,10)
)
复制代码

插入数据

INSERT INTO sales (id, money, score,order_date) VALUES('1','123.34',1,'2021-12-01');
INSERT INTO sales (id, money, score,order_date) VALUES('2','12132.33',2,'2022-01-15');
INSERT INTO sales (id, money, score,order_date) VALUES('3','32432.31',3,'2020-03-22');
INSERT INTO sales (id, money, score,order_date) VALUES('4','32432.31',4,'2020-03-22');
复制代码

查询分区统计

 -- 查询数据分区分布
SELECT 
  PARTITION_NAME AS "分区",TABLE_ROWS AS "行数"
FROM
  information_schema.partitions 
WHERE table_schema = "cds-temp" 
 AND table_name = "sales"
复制代码

image

HASH分区类型

创建分区表

使用hash函数进行分区,分区数为3 分为p0、p1、p2三个分区。

DROP TABLE IF EXISTS sales;
CREATE TABLE sales (
  id INT(11) NOT NULL,
  money DECIMAL(11,2) NOT NULL,
  score INT(2) NOT NULL,
  order_date DATE NOT NULL,
  PRIMARY KEY (id,score)
) ENGINE=INNODB DEFAULT CHARSET=utf8
PARTITION BY HASH(score) PARTITIONS 3;
复制代码

插入数据

-- 插入数据
INSERT INTO sales (id, money, score,order_date) VALUES('1','123.34',1,'2021-12-01');
INSERT INTO sales (id, money, score,order_date) VALUES('2','12132.33',2,'2022-01-15');
INSERT INTO sales (id, money, score,order_date) VALUES('3','32432.31',3,'2020-03-22');
INSERT INTO sales (id, money, score,order_date) VALUES('4','32432.31',4,'2020-03-22');
复制代码

查询分区统计

 -- 查询数据分区分布
SELECT 
  PARTITION_NAME AS "分区",TABLE_ROWS AS "行数"
FROM
  information_schema.partitions 
WHERE table_schema = "cds-temp" 
 AND table_name = "sales"
复制代码

image

key分区类型

创建分区表

-- 创建表
DROP TABLE IF EXISTS sales;
CREATE TABLE sales (
  id int(11) NOT NULL,
  name varchar(10) NOT NULL,
  money decimal(11,2) NOT NULL,
  order_date date NOT NULL,
  PRIMARY KEY (id,name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
 PARTITION BY key (name) PARTITIONS 3;
复制代码

插入数据

-- 插入数据 
INSERT INTO sales (id,name, money, order_date) VALUES('1','汽车','123.34','2021-12-01');
INSERT INTO sales (id,name, money, order_date) VALUES('2','手机','12132.33','2022-01-15');
INSERT INTO sales (id,name, money, order_date) VALUES('3','相机','32432.31','2020-03-22');
复制代码

查询分区统计

 -- 查询数据分区分布
SELECT 
  PARTITION_NAME AS "分区",TABLE_ROWS AS "行数"
FROM
  information_schema.partitions 
WHERE table_schema = "cds-temp" 
 AND table_name = "sales"
复制代码

image

1.4 分区表原理

分区表是多个底层相关表组成,mysql分区后每个分区成了独立的文件,虽然从逻辑上还是一张表其实已经分成了多张独立的表,从“information_schema.INNODB_SYS_TABLES”系统表可以看到每个分区都存在独立的TABLE_ID, 由于Innodb数据和索引都是保存在".ibd"文件当中(从INNODB_SYS_INDEXES系统表中也可以得到每个索引都是对应各自的分区(primary key和unique也不例外)),所以分区表的索引也是随着各个分区单独存储。

-- 查看创建表的分区情况

SELECT * FROM information_schema.INNODB_SYS_TABLES WHERE NAME LIKE "%sales%"

image

select:查询一个分区表,分区层先打开并锁住所有底层表,优化器先过滤掉部分分区,然后再调用对应的存储引擎访问各个分区的数据。

insert:写入记录,分区层先打开并锁住所有底层,确定某个分区插入数据,释放锁并将记录写入对应的分区。

delete:删除记录,分区层先打开并锁住所有底层,确定数据对应的分区并删除对应分区数据。

update:更新记录,分区层先打开并锁住所有底层,确定需要更新的数据在哪个分区并取出更新,再判断更新后数据需要放在哪个分区并写入,如果更新后不再同一分区还需要将原分区数据删除。

1.5 分区表使用

使用分区表的前提是数据量特别大,在数据量特别巨大的时候,比如10亿条数据,使用索引会产生入下问题:

大数据量的问题

创建和维护索引的成本比较高。

使用索引如果是覆盖索引还好不用回表。回表的可能会产生大量的随机IO。

分区表的两种策略

分区表的全表扫描

​ 使用简单分区方式存放表,不使用其他索引,则该策略假定不依赖于内存数据,则需要控制分区表的数量限制在很小的范围内。

分离冷热数据,使用索引

​ 数据有明显的’冷热’数据之分,只有热数据被频繁访问,其他数据几乎不会被访问,热数据存放到一个分区中且该热数据尽可能的使用内存,大部分查询在该热点分区中,也可以正常使用索引。

分区表的问题

null值问题

Mysql允许分区键值为NULL,分区键可能是一个字段或者一个用户定义的表达式。一般情况下,Mysql在分区的时候会把NULL值当作零值或者一个最小值进行处理。

​ 如果分区键所在列没有notnull约束。

​ range分区表,那么null行将被保存在范围最小的分区。

​ list分区表,那么null行将被保存到list为0的分区。

​ HASH和KEY分区,任何产生NULL值的表达式mysql都视同它的返回值为0。

为了避免这种情况的产生,建议分区键设置成NOT NULL。或者设置一个p_ null的“无用”分区用来存放一些null值字段。

​ null值问题 此处笔者存疑,因为上述分区例子中,分区字段列均为主键,说明其不存在null值字段,即时使用分区表达式也没有输出null的情况

分区列和索引列不匹配

索引列和分区列不匹配是指你的sql语句where条件的包含带有索引的列但不包含分区列的情况,这时候不管分区列有没有索引都不会进行分区过滤。

选择分区成本很高

对应范围分区和list分区,所有的curd操作都需要进行入下符合条件的行属于那些分区,对于查找来说可能需要扫描所有分区才能判断出结果,所以分区数不宜设置的过多,小于100个分区是笔者的建议。HASH和KEY分区不会有该情况。

分区查找锁表成本

​ curd访问分区表时都会出现锁住所有底层表的情况,这是一个硬性开销。该情况在分区过滤之前(所以不能使用分区过滤的形式减少开销),同时也与分区类型无关。

维护分区成本

就像创建索引能提高查询效率,但也增加了索引数据维护的开销,分区也有对应的维护成本,对于频繁的删除或者修改分区会产生一定的性能损耗。

1.6 分区查询

正常使用分区过滤

-- where条件只有带有分区列才能进行分区过滤
EXPLAIN PARTITIONS 
SELECT 
  * 
FROM
  sales 
WHERE order_date > "2022-01-01" 

复制代码
-- 分区表作为关联表的第二张表且关联条件是分区键,查找也是能过来分区的
EXPLAIN [ PARTITIONS ] 
SELECT 
  * 
FROM
  commodity 
  JOIN sales commodity.date = sales.order_date 
WHERE order_date > "2022-01-01" 

复制代码

不能使用分区过滤情况

-- mysql只有在查询条件中分区列本身进行比较 才能过滤分区
-- 如下的使用列的表达式进行筛选 则无法过滤分区,会扫描所有分区
EXPLAIN PARTITIONS 
  SELECT 
    * 
  FROM
    sales 
  WHERE order_date > YEAR("2022-01-01")

复制代码

1.7 合并表

合并表和分区表类似,区别在于分区表时逻辑概念,我们无法直接访问底层表,但是合并表的所有字表都是真实存在的,只是通过使用语言将两张结构一致的表合并在一起处理。

DROP TABLE IF EXISTS `user_one`;
CREATE TABLE `user_one` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(50) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

DROP TABLE IF EXISTS `user_two`;
CREATE TABLE `user_two` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(50) DEFAULT NULL COMMENT '姓名',
  PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
-- 插入数据
INSERT  INTO `user_one`(`id`,`name`) VALUES (1,'张三');
INSERT  INTO `user_two`(`id`,`name`) VALUES (1,'李四');

-- union 必须是多个表结构一致且两个表引擎必须为MyISAM --
-- 合并表
DROP TABLE IF EXISTS `user_mrg`;
CREATE TABLE `user_mrg` (
  `id` INT(11) NOT NULL AUTO_INCREMENT ,
  `name` VARCHAR(50) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=MERGE 
UNION = (user_one,user_two) INSERT_METHOD LAST
;

INSERT INTO user_mrg() VALUE(NULL,"王五");

SELECT * FROM user_mrg

复制代码

image

合并表user_mrg与user_one和user_two表结构定义相同,但是主键出现了重复,说明虽然合并表结构要与子表一致,但是却不受主键限制(有点匪夷所思)。

INSERT_METHOD 设置插入数据时插入到合并表中的哪个字表中,first 插入到第一张表,last 插入数据到最后一张表

视图

2.1 视图概念

MySQL 视图(View)是一种虚拟存在的表,数据库中只存放了视图的定义不存放数据,数据存放在定义视图查询涉及的真实表中。使用视图查询数据时,数据库会从真实表中取出对应的数据。

好处

简单性,简化用户对数据的理解和操作。对于经常查询的sql可以创建视图,简化复杂的sql操作。

安全性,视图可以筛选用户需要的列数据而隐藏用户不需要的列,变向的进行了简单的权限过滤。

-- 创建视图
CREATE VIEW <视图名> [(列名 [, ...n])]
AS
   SELECT语句

-- 修改视图
ALTER VIEW <视图名> [(<列名> [, ...n])]
AS
   SELECT语句
   
-- 删除视图
DROP VIEW <视图名>

复制代码

2.2 视图原理

-- 创建视图
CREATE VIEW sales_view (money,order_date)
AS
  SELECT money,order_date FROM sales WHERE money >500
  
-- 查询视图
SELECT * FROM sales_view WHERE order_date >"2020-05-01";

复制代码

mysql中实现视图的方式有两种方式。以上述视图例子为例

  • 合并算法:所谓合并算法将视图中的查询条件和查询视图追加的条件进行合并转换为完整的sql进行执行,合并算法性能较好,优化器也能对其进行优化,所以此处推荐尽量使用合并算法。上述视图查询使用合并算法
-- 使用合并算法实现视图
SELECT 
  money,
  order_date 
FROM
  sales 
WHERE money > 500   -- 原视图查询条件
  AND order_date > "2020-05-01" ;  -- 查询创建视图 新的查询条件

复制代码
  • 临时表算法:顾名思义,创建视图即为创建一张临时表将视图查询的数据存储到临时表中。基于视图查询操作就对临时表进行查询。
-- 使用临时表实现视图
CREATE TEMPORARY TABLE sale_tmp 
AS 
  SELECT 
    money,
    order_date 
  FROM
    sales 
  WHERE money > 500 
  
SELECT * FROM sale_tmp WHERE order_date >"2020-05-01";

复制代码

image

无法使用合并算法情况

group by、distinct,聚合函数、union、子查询等无法建立原表和视图表1对1映射场景 都会采用临时表形式进行视图实现。使用explain 分析视图 select_type 为DERIVED即使用临时表。

EXPLAIN SELECT * FROM  {view_name}
复制代码

3. 存储代码

mysql****的存储代码有四种类型

触发器:在 insert/update/delete 之前或之后,触发并执行触发器中定义的SQL语句

事件:在MySQL 5.1中新增了一个特色功能事件调度器(Event Scheduler),简称事件。它可以作为定时任务调度器。

存储函数:MySQL存储函数(自定义函数),函数一般用于计算和返回一个值,可以将经常需要使用的计算或功能写成一个函数。

存储过程:特定功能的SQL 语句集经编译后存储在数据库中,用户通过指定存储过程的名字并给出参数来执行它。

好处

存储代码在服务内部执行会节省网络带宽降低网络延迟

存储代码可重复使用,同时对于存储过程/函数,mysql会缓存其对应的执行计划,一次编译多次执行。

将程序开发和和DBA职能分开,专人专职。

缺陷

存储代码功能依赖的函数较少,适于简单逻辑编写,灵活复杂的逻辑还需要依赖应用程序。

存储代码可读性,可维护性差,没有相对完善的开发和调试工具(上百行的存储代码你维护一下试试)。

存储代码没有完备的异常处理机制,一个小错误可能拖死服务。

注:存储代码一般较少使用。

4.字符集和校对

字符集编码 优先级:列>表>数据库

校对规则: _cs、_ci、_bin分别对应 大小写不敏感、大小写敏感、二进制值。

5.全文索引

mysql不支持 中文全文索引,应用其他引擎如 Sphinx等。

6.分布式(XA)事务

在分布式多数据库下仍能保证事务的ACID

注:分布式事务一般在业务侧保证,mysql自带的分布式事务较少使用。

7.查询缓存

-- 概念:缓存select结果,跳过解析、优化、执行阶段。

查询缓存是完全存储在内存中。mysql无法为每一个查询结果精确分配大小刚好配匹的缓存空间。

-- 查询缓存无法命中的原因:包含不确定的函数、未处理过该查询、内存用完被逐出

-- 如何判断查询缓存是否有效:

查看 Qache_hits和Qache_inserts的比值(3:1查询缓存有效,10:1最佳)

-- 配置和维护查询缓存:

query_cache_type: 是否打开查询缓存,设置成ON、OFF、DEMAND(这个仅在明确写明SQL_CACHE下才放入缓存)

query_cache_size:查询缓存使用的 总内存空间(值是1024整数倍)

query_cache_min_res_unit:查询缓存中分配内存块时的 最小单位。

query_cache_limit:MySQL能缓存的 最大查询结果

query_cache_wlock_invalidate:某表被锁住,是否仍然从查询缓存返回结果,默认OFF

减少碎片:命令 FLUSH QUERY CACHE 完成碎片整理

-- InnoDB和查询缓存:表上有任何的锁,该查询结果无法缓存;sql语句有ON DELETE CASCADE,则相关联查询缓存一起失效

通用查询缓存优化:1) 用 多个小表代替一个大表对查询缓存

2) 批量写入时只需要做一次缓存失效

3)缓存 空间太大,服务器可能僵死,办法是控制大小或禁用

4)用SQL_CACHE、SQL_NO_CACHE 控制某个select是否缓存

5)对于 写密集型应用,直接禁用查询缓存更好

注:若需要更高的缓存效率,推荐使用memcached或redis之类

猜你喜欢

转载自juejin.im/post/7182843347520880697