查看mysql 用户的连接权限
1 | mysql> use mysql; |
可见允许root用户在任何IP段下连接数据库实例
mysql连接方式
1 | 1. TCP/IP方式:本地或远程登录 |
InnoDB 存储结构:B+树
MyIsam引擎是默认存储引擎,对应磁盘上三个文件:.frm(存储表定义) .MYD(存储数据) .MYI(存储索引)
InnoDB引擎,对应磁盘上两个文件:.frm(存储表定义).ibd(存储聚集索引,叶子节点即数据)
1 | InnoDB的一张表的数据和索引都存在表空间,一个表空间对应一个ibd文件 |
索引
1 | 1. 聚集索引: |
查看索引(Cardinality:基数值越大,区分度越高,适合建立索引)
1 | show index from table_name; |
添加索引
1 | // 普通索引 |
explain/desc 查看执行计划:
±—±-----------------±--------±-------------±--------±-------------------±------±-----------±-----±-------±---------±-----------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
±—±-----------------±--------±--------------±-------±--------------------±------±-----------±-----±-------±---------±----------+
id: 查询序号
id相同:执行顺序由上至下
id不同:如果是子查询,id的序号会递增,id值越大优先级越高,越先被执行
id相同又不同(两种情况同时存在):id如果相同,可以认为是一组,从上往下顺序执行;在所有组中,id值越大,优先级越高,越先执行
select_type:查询类型
SIMPLE:简单的select查询,查询中不包含子查询或者union
PRIMARY:查询中包含任何复杂的子部分,最外层查询则被标记为primary
SUBQUERY:在select 或 where列表中包含了子查询
DERIVED:在from列表中包含的子查询被标记为derived(衍生),mysql或递归执行这些子查询,把结果放在临时表里
table:查询的数据表,当从衍生表中查数据时会显示
<derivedx>
,x
表示对应的执行计划id
partitions:表分区,表创建的时候可以指定通过那个列进行表分区
type:访问类型,性能由好到坏依次是:
system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL
system:表只有一行记录
const:表示通过索引一次就找到了,且只有一条匹配记录;const用于比较primary key 或者 unique索引
eq_ref:唯一性索引或主键扫描
ref:非唯一性索引扫描,返回匹配某个单独值的所有行
range:只检索给定范围的行,使用一个索引来选择行
index:Full Index Scan,遍历索引
ALL:Full Table Scan,遍历全表
possible_keys:查询涉及到的字段上存在索引,则该索引将被列出,但不一定被查询实际使用
key:实际使用的索引,如果为NULL,则没有使用索引
key_len:查询中使用到的索引的最大可能长度
ref:显示使用到的索引字段或常量查询(const)
rows:估算找到所需的记录所需要读取的行数
extra:额外信息
Using filesort :使用非索引列进行排序时出现,非常耗性能
Using temporary: 使用临时表保存中间结果,常见于order by 和 group by 、join子查询
Using index:使用了覆盖索引,避免了访问聚集索引数据行,效率高
Using where:使用了where子句来过滤结果集
Limit查询
mysql通过limit实现分页查询,语法如下:
1 | // 返回前10条记录 |
当offset过大时,分页查询性能急剧下降(抛开mysql的查询缓存),为了提升limit查询的性能,优化方向有以下种:
-
使用覆盖索引子查询,查询所需offset的最小主键值,然后通过主键范围查询的where子句来得到结果,例如:
1
2// 覆盖索引子查询 + where 范围查询 + limit count查询
select * from user where user_id >= (select user_id from user order by user_id limit 50000, 1) limit 10; -
使用覆盖索引子查询,查询所需offset的最小主键值,然后通过JOIN联表查询的where子句来得到结果,例如:
1
2// 覆盖索引子查询 + JOIN + where + limit count 查询
select * from user as t1 inner join (select user_id from user order by user_id limit 50000, 1) as t2 where t1.user_id >= t2.user_id limit 10;1
2
3select t1.* from user_task t1 where t1.task_no in (100, 110, 120, 130, 140) limit 46000, 10; --- 百万数据查询耗时2~3秒
select t1.* from user_task t1 inner join (select id from user_task where task_no in (100, 110, 120, 130, 140) order by id asc limit 46000, 10) t2 on t1.id=t2.id; --- 百万数据查询耗时0.1秒左右
---性能提升20~30倍 -
业务优化,根据业务逻辑确定其他字段的值来代替limit的offset值
优化
- 查看sql具体执行时间,具体查看官方文档
1 | mysql> set profiling=1; |
- mysql 性能监控performance_schema,具体查看官方文档
1 | mysql> show databases; |
事务查询
1 | mysql> show full processlist; |
InnoDB在Repeatable Read事务隔离级别下,使用快照读,快照读不会占用和等待表锁
应该显示的使用事务,不要使用autocommit
数据库更新丢失问题
并发事务中进行查询并更新可能会导致更新丢失。说白了就是查询并更新两条sql不是一个原子操作,而且更新语句中使用了查询出来的旧值,或者更新语句是根据查询语句的旧值进行业务逻辑的更新,此时其他事务的更新才会被覆盖。如果更新语句和查询语句的结果不要建立关系,则没问题,比如 update bank set money = money - 100 where id = 123;
例如:mysql 事务级别Repeatable Read下:
SET AUTOCOMMIT=0;
CREATE TABLE bank
(
id
int(10) NOT NULL AUTO_INCREMENT,
money
int(10) NOT NULL,
PRIMARY KEY (id
)
);
INSERT INTO bank
VALUES (1, 900);
事务1(存100元) | 事务2(取100元) |
---|---|
begin; | |
begin; | |
select * from bank where id=1;(900) | |
select * from bank where id=1; (900) | |
update bank set money=1000 where id=1; | |
update bank set money=800 where id=1;(阻塞直到事务1commit) | |
commit; | |
commit; (事务1的更新被覆盖) |
解决办法1:悲观锁(select for update)
事务1(存100元) | 事务2(取100元) |
---|---|
begin; | |
begin; | |
select * from bank where id=1 for update; | |
select * from bank where id=1 for update;(阻塞直到事务1commit)(1000) | |
update bank set money=1000 where id=1; | |
update bank set money=900 where id=1;(基于1000减去100) | |
commit; | |
commit; |
解决办法2:乐观锁-旧值判断
事务1(存100元) | 事务2(取100元) |
---|---|
begin; | |
begin; | |
select * from bank where id=1; | |
select * from bank where id=1; | |
update bank set money=1000 where id=1 and money=900; | |
update bank set money=800 where id=1 and money=900;(阻塞直到事务1commit,但是此条更新会失败) | |
commit; | |
commit; |
解决办法3:乐观锁-表上增加版本字段
CREATE TABLE bank
(
id
int(10) NOT NULL AUTO_INCREMENT,
money
int(10) NOT NULL,
timeStamp
int(10) DEFAULT NULL,
PRIMARY KEY (id
)
);
INSERT INTO bank
VALUES (1, 900, 1571129028);
事务1(存100元) | 事务2(取100元) |
---|---|
begin; | |
begin; | |
select * from bank where id=1; | |
select * from bank where id=1; | |
update bank set money=1000, timeStamp=1571129243 where id=1 and timeStamp=1571129028; | |
update bank set money=800, timeStamp=1571129246 where id=1 and timeStamp=1571129028;(阻塞直到事务1commit,但是此条更新会失败) | |
commit; | |
commit; |
一致读vs当前读
可重复读的核心就是一致性读(consistent read);而事务更新数据的时候,只能用当前读。如果当前的记录的行锁被其他事务占用的话,就需要进入锁等待。
而读提交的逻辑和可重复读的逻辑类似,它们最主要的区别是:
- 在可重复读隔离级别下,只需要在事务开始的时候创建一致性视图,之后事务里的其他查询都共用这个一致性视图;
- 在读提交隔离级别下,每一个语句执行前都会重新算出一个新的视图。
清空表
1 | truncate vs delete vs drop |
分库分表
分库database:分库的目的是降低单台服务器负载,切分原则是根据业务紧密程度拆分,缺点是跨数据库无法联表查询
分表table:当单表数据量超大的时候,B-Tree索引就无法起作用了。除非是覆盖索引查询,否则数据库服务器需要根据索引扫描的结果回表(回主键索引查询叶子节点行记录),查询所有符合条件的记录,如果数据量巨大,这将产生大量随机I/O,随之,数据库的响应时间将大到不可接受的程度。另外,索引维护(磁盘空间、I/O操作)的代价也非常高
分表分为:垂直分表和水平分表
垂直分表:就是表的字段太多,将字段拆分到不同的表中。
水平分表:每个表的字段一样,存储的记录不一样,可以根据业务逻辑进行拆分(user_id,地区,时间,hash等)
水平分表后的问题:
怎么设置全局唯一主键(现成方案:snowflake等)
怎么保证数据均匀存储,没有热点问题(根据业务逻辑来)
怎么方便扩容和缩容(一致性hash)
分区
分表存在的问题是需要服务端修改,引入中间件来屏蔽分库分表带来的差异
分区相当于mysql层的分表,对服务端是透明的,服务端不需要做任何修改
而且分区的扩容和缩容很容易,直接增加分区,删除分区即可,对数据完整性没影响
所以不存在单机负载问题的情况下,可以用分区来代替分表,简化服务端的更改
mysql repication
主从复制备份,读写分离
mysql fabric
在replication的基础上,提供故障检测和自动Failover、请求路由和分片(sharding)功能
mysql cluster
面向大规模数据的存储解决方案,Cluster的架构思想与Hadoop非常类似,它设计的前提是“认为每个Node都是易于出错的”、集群规模巨大、多租户,所以它提供了数据备份机制、自动迁移、自动Failover等特性,来保证可用性、健壮性。它的核心特性为:数据集并不是存储某个特定的MySQL实例上,而是被分布在多个Data Nodes中,即一个table的数据可能被分散在多个物理节点上,任何数据都会在多个Data Nodes上冗余备份。任何一个数据变更操作,都将在一组Data Nodes上同步以保证数据的一致性。
DUAL表
Mysql里面很多内置函数的用法如下:
1 | mysql> select rand(); |
其实他们都等同于如下语句:
1 | mysql> select rand() from dual; |
DUAL表是一张虚拟表,纯粹是为了满足 select from where这一语法习惯而设置的。
实际应用中可以用于,条件查询不存在则插入的操作:
比如新建一个角色表
1 | -- ---------------------------- |
条件插入语句可以如下:
先select * from role where name=‘xxoo’;判断是否存在,不存在,再insert into role (id, name, no, type, remark ) values (…);
使用DUAL表一条语句搞定:
insert into role (id, name, no, type, remark ) select 123, ‘test-dual’, 456, 1, ‘test-dual’ from dual where not exists (select * from role where name=‘test-dual’);
日志
Mysql日志类型:
-
redo log: 在事务开始的时候先写redo log,以防发生故障时,在恢复的时候根据redo log进行重做日志,保证数据的一致性
-
undo log:在事务开始前将当前数据库版本生成undo log,用于事务回滚操作
-
bin log:用于主从复制,从库连接到主库,读取bin log进行主从同步。在事务提交的时候会刷新bin log
-
relay log:用于主从复制,从库将主库读到的binlog复制到从库的relay log中,然后回放relay log以达到主从同步
-
error log:
1
2[mysqld]
log-error = /var/log/mysql/error.log -
query log
1
2
3[mysqld]
general_log_file=/var/log/mysql/query.log
general_log=ON -
slow query log
1
2
3
4
5
6
7
8#1. 查看mysql配置文件
mysql --help | grep my.cnf
/etc/my.cnf /etc/mysql/my.cnf /usr/etc/my.cnf ~/.my.cnf
#2. 修改配置文件:vim /etc/my.cnf
slow_query_log=ON
slow_query_log_file=/var/log/slowmysql.log
long_query_time=10
查看某个操作的语法
1 | mysql> ? create table |
字符集和校对规则
字符集指定字符的编码方式。比如Mysql推荐使用的字符集为:utf8mb4
校对规则制定字符如何排序及比较大小。比如Mysql推荐使用的校对规则为:utf8mb4_unicode_ci (ci结尾表示case insensitive)
Mysql对字符集的转换处理流程如下:
- 客户端请求数据库数据,发送的数据使用character_set_client字符集
- MySQL实例收到客户端发送的数据后,将其转换为character_set_connection字符集
- 进行内部操作时,将数据字符集转换为内部操作字符集:
- 使用每个数据字段的character set设定值
- 若不存在,使用对应数据表的default character set设定值
- 若不存在,使用对应数据库的default character set设定值
- 若不存在,使用character_set_server设定值
- 将操作结果值从内部操作字符集转换为character_set_results
以防中文显示乱码,将character_set_client、character_set_connection、character_set_results、character_set_server都设置为utf8mb4;
查看默认字符集和校对规则
1 | mysql> SHOW VARIABLES LIKE "%char%"; |
设置字符集和校对规则
- 启动mysql时设置:修改配置文件
1 | [mysqld] |
- 创建数据库时指定
1 | CREATE DATABASE myDb |
- 创建表时指定
1 | CREATE TABLE myTb () ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 |
mysql默认字符集为latin,如果不存储中文字符,使用默认即可,节省内存,如果要存储中文,可以对存储中文的特定字段设置字符集
SQL注入
在输入SQL参数的地方输入了特殊的SQL语句。防止SQL注入:对参数进行检查转义。
Mybatis的参数引用方式:#{param}
和${param}
,其中#{}
形式的SQL语句会创建一个预编译语句PreparedStatement,参数会用?占位符来标识,PreparedStatement里会对输入参数进行转义处理。而${}
形式的SQL语句会直接用输入参数进行字符串替换。
另外PreparedStatement的预编译功能是依赖Mysql数据库服务器的。Mysql的预编译功能如下:
1 | mysql> prepare selectVodById from 'select * from vods where vodid = ?'; |
Mysql JDBC Connector驱动从5.0开始默认关闭了服务器预编译功能,就是说PreapredStatement的执行效果和Statement在服务器上是一样的。只有显式开启了服务器预编译功能,PreparedStatement才会被预编译。直接在数据库url中添加useServerPrepStmts=true即可:
1 | jdbc:mysql://localhost:3306/test?useServerPrepStmts=true |
Druid连接池+监控
一: 配置数据库密码加密
- 执行命令,生成密码加密后的的privateKey、publicKey和加密后的password
1 | java -cp druid-1.1.17.jar com.alibaba.druid.filter.config.ConfigTools 12345678 |
1 | privateKey:MIIBVQIBADANBgkqhkiG9w0BAQEFAASCAT8wggE7AgEAAkEAjdRL/KlKXM5TjMHSRZC/Gq45a/DvEBXIRBDUHodCmQrs4HkB4kMsEuYJ5i6ID45XUZw8ibyyCNrcx9FNkj6NSwIDAQABAkEAgbMO+jNmSZB1X1cwD2XbHW8OG+Ps+uywg25QTMqs4H55W+D9rThzDpgWBHbj1o8hupKOm1aoeeZf0itIi8TggQIhANrioKxjzAHRpNTTr5KsJWMRjIzhmn3KZCwpyNkZItoxAiEApeDSq/268bRIEsLapBHYfZA6Sy7M4JJCKPxv6pfFhDsCIQCLZk3BvIUOm3+Ic5CbrrrYzzJd/sgvWJhXb/0UFmgV4QIgHCh2+rU+p8sXtP+Yx+MzodT64EpYgwKw8m4vvV34LIMCIGh979qmx/BZk5ZpsOf4Y3H8V9G8ApTIHUCORs5gbIDn |
- 将publicKey和加密后的password写入配置文件,如下
1 | spring: |
二:配置监控
- 配置文件添加具体的filter,如下
1 | spring: |
- 启动应用,访问/druid/index.html
GroupBy+Having使用场景
比如员工表employee如下:
“id” “name” “dept” “salary” “hire_date”
“1” “张三” “开发部” “1200000” "1/10/2019 17:34:05"
“2” “李四” “开发部” “1500000” "21/4/2020 17:34:48"
“3” “王五” “设计部” “900000” "4/8/2020 17:35:23"
“4” “刘坡” “测试部” “1500000” "20/10/2020 17:36:16"
“5” “钱八” “销售部” “800000” "1/4/2019 17:36:48"
“6” “孙久” “销售部” “950000” "28/4/2020 17:37:29"
“7” “麻子” “设计部” “1100000” "15/10/2020 17:38:10"
“8” “曹弯” “开发部” “2200000” “20/7/2020 17:39:03”
查询自2020年以来入职的员工中哪个部门的最低工资是最低的:
1 | select dept, min(salary) as minimum from employee where hire_date > '2020-01-01' group by dept having minimum>1000000 order by minimum limit 1; |