MySQL 学习笔记
MySQL
三大范式
关系数据库中的关系必须满足一定的要求,即满足不同的范式。目前关系数据库有六种范式:第一范式(1NF)、第二范式(2NF)、第三范式(3NF)、第四范式(4NF)、第五范式(5NF)和第六范式(6NF)。满足最低要求的范式是第一范式(1NF)。在第一范式的基础上进一步满足更多要求的称为第二范式(2NF),其余范式以次类推。一般说来,数据库只需满足第三范式(3NF)就行了。
第一范式
1NF 是对属性的原子性,要求属性具有原子性,不可再分解。
表:字段 1、字段 2(字段 2.1、字段 2.2)、字段 3 …
如学生(学号,姓名,性别,出生年月日),如果认为最后一列还可以再分成(出生年,出生月,出生日),它就不是一范式了,否则就是;
第二范式
2NF 是对记录的唯一性,要求记录有唯一标识,即实体的唯一性,即不存在部分依赖。
表:学号、课程号、姓名、学分;
这个表明显说明了两个事务:学生信息,课程信息;由于非主键字段必须依赖主键,这里学分依赖课程号,姓名依赖学号,所以不符合二范式。
可能会存在的问题:
- 数据冗余:每条记录都含有相同信息;
- 删除异常:删除所有学生成绩,就把课程信息全删除了;
- 插入异常:学生未选课,无法记录进数据库;
- 更新异常:调整课程学分,所有行都调整
正确做法:
- 学生:Student(学号,姓名);
- 课程:Course(课程号,学分);
- 选课关系:StudentCourse(学号,课程号,成绩)。
第三范式
3NF 是对字段的冗余性,要求任何字段不能由其他字段派生出来,它要求字段没有冗余,即不存在传递依赖。
表:学号,姓名,年龄,学院名称,学院电话
因为存在依赖传递:(学号) ➡ (学生) ➡ (所在学院) ➡ (学院电话)。
可能会存在问题:
- 数据冗余:有重复值;
- 更新异常:有重复的冗余信息,修改时需要同时修改多条记录,否则会出现数据不一致的情况。
正确做法:
学生:(学号,姓名,年龄,所在学院);
学院:(学院,电话)。
反范式化
一般来说,数据库只需要满足第三范式(3NF)就行了。
没有冗余的数据库设计可以做到。但是,没有冗余的数据库未必是最好的数据库,有时为了提高运行效率,就必须降低范式标准,适当保留冗余数据。具体做法是:在概念数据模型设计时遵守第三范式,降低范式标准的工作放到物理数据模型设计时考虑。降低范式就是增加字段,允许冗余,达到以空间换时间的目的
。
〖例〗:有一张存放商品的基本表,“金额”这个字段的存在,表明该表的设计不满足第三范式,因为 “金额” 可以由 “单价” 乘以 “数量” 得到,说明 “金额” 是冗余字段。但是,增加 “金额” 这个冗余字段,可以提高查询统计的速度,这就是以空间换时间的作法。
在 Rose 2002
中,规定列有两种类型:数据列和计算列。“金额”这样的列被称为 “计算列”,而“单价” 和“数量”这样的列被称为“数据列”。
范式化设计和反范式化设计的优缺点
优点 | 缺点 | |
---|---|---|
范式化 | 1. 可以尽量的减少数据冗余,数据表更新快体积小 < br />2. 范式化的更新操作比反范式化更快 < br />3. 范式化的表通常比范式化更小 | 1. 对于查询需要对多个表进行关联,导致性能降低 < br />2. 更难进行索引优化 |
反范式化 | 1. 可以减少表的关联 < br />2. 可以更好的进行索引优化 | 1. 存在数据冗余及数据维护异常 < br />2. 对数据的修改需要更多的成本 |
事务管理(ACID)
谈到事务一般都是以下四点
原子性(Atomicity)
原子性是指事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生。
一致性(Consistency)
事务前后数据的完整性必须保持一致。
隔离性(Isolation)
事务的隔离性是多个用户并发访问数据库时,数据库为每一个用户开启的事务,不能被其他事务的操作数据所干扰,多个并发事务之间要相互隔离。
持久性(Durability)
持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来即使数据库发生故障也不应该对其有任何影响
举个简单的例子理解以上四点
原子性(Atomicity)
针对同一个事务
这个过程包含两个步骤:
A:800 - 200 = 600
B:200 + 200 = 400
原子性表示,这两个步骤一起成功,或者一起失败,不能只发生其中一个动作
一致性(Consistency)
针对一个事务操作前与操作后的状态一致
操作前 A:800,B:200
操作后 A:600,B:400
一致性表示事务完成后,符合逻辑运算
持久性(Durability)
表示事务结束后的数据不随着外界原因导致数据丢失
操作前 A:800,B:200
操作后 A:600,B:400
如果在操作前(事务还没有提交)服务器宕机或者断电,那么重启数据库以后,数据状态应该为
A:800,B:200
如果在操作后(事务已经提交)服务器宕机或者断电,那么重启数据库以后,数据状态应该为
A:600,B:400
隔离性(Isolation)
针对多个用户同时操作时,主要是排除其他事务对本次事务的影响
事务的隔离级别
数据库事务的隔离性: 数据库系统必须具有隔离并发运行各个事务的能力, 使它们不会相互影响, 避免各种并发问题。
一个事务与其他事务隔离的程度称为隔离级别。数据库规定了多种事务隔离级别, 不同隔离级别对应不同的干扰程度,隔离级别越高, 数据一致性就越好, 但并发性越弱。
脏读
指一个事务读取了另外一个事务未提交的数据。
不可重复读
在一个事务内读取表中的某一行数据,多次读取结果不同。(这个不一定错误,只是某些场合不对)
页面统计查询值
点击生成报表的时候,B 有人转账进来 300(事务已经提交)
虚度(幻读)
是指在一个事务内读取到了别的事务插入的数据,导致前后读取不一致。(一般是行影响,多了一行)
四种隔离级别设置
数据库
set transaction isolation level
设置事务隔离级别
select @@tx_isolation
查询当前事务隔离级别
设置 | 描述 |
---|---|
Serializable | 可避免脏读、不可重复读、虚读情况的发生。(串行化) |
Repeatable read | 可避免脏读、不可重复读情况的发生。(可重复提交) |
Read committed | 可避免脏读情况发生(读已提交)。 |
Read uncommitted | 最低级别,以上情况均无法保证(读未提交)。 |
Java
适当的 Connection 方法,比如 setAutoCommit 或 setTransactionIsolation
设置 | 描述 |
---|---|
TRANSACTION_SERIALIZABLE | 指示不可以发生脏读、不可重复读和虚读的常量。 |
TRANSACTION_REPEATABLE_READ | 指示不可以发生脏读和不可重复读的常量;虚读可以发生。 |
TRANSACTION_READ_UNCOMMITTED | 指示可以发生脏读(dirty read)、不可重复读和虚读(phantom read)的常量。 |
TRANSACTION_READ_COMMITTED | 指示不可以发生脏读的常量;不可重复读和虚读可以发送。 |
mysql 模拟转账测试
开启事务步骤
- 关闭自动提交
- 开启一个事务
- 成功就提交,失败则回滚
- 关闭自动提交
-- 模拟转账:事务
SET autocommit = 0; -- 关闭自动提交
START TRANSACTION; -- 开启事务
-- 1 向 2 转账 200 元
UPDATE account SET money = money - 200 WHERE id = 1;
UPDATE account SET money = money + 200 WHERE id = 2;
COMMIT; -- 成功则提交
ROLLBACK; -- 失败则回滚
SET autocommit = 1; -- 恢复默认值,开启自动提交
索引
MySQL 官方对索引的定义为:索引(Index)是帮助 MySQL 高效获取数据的数据结构。索引的本质其实就是数据结构。
索引的分类
在一个数据表中,主键索引只能有一个,唯一索引可以有多个。
主键索引(PRIMARY KEY)
唯一的标识,主键不可重复,只能有一个列作为主键。
唯一索引(UNIQUE KEY)
避免重复的列出现,唯一索引可以重复,多个列都可以标识为唯一索引
常规索引
默认的索引,用 index、key 关键字来设置
全文索引
在特定的数据库引擎下才有,MyISAM
快速定位数据
基础语法
索引的使用:
1、在创建表的时候给字段增加索引
2、创建完毕后,增加索引
-- 显示 student 表所有的索引信息
SHOW INDEX FROM student;
-- 增加一个全文索引(索引名)列名
ALTER TABLE school.student ADD FULLTEXT INDEX 'studentName'('studentName');
-- EXPLAIN 分析 sql 执行的状况
EXPLAIN SELECT * FROM student; -- 非全文索引
EXPLAIN SELECT * FROM student WHERE MATCH(studentName) AGAINST('刘');
测试索引
新建数据表
CREATE TABLE user100w(
id INT NOT NULL AUTO_INCREMENT,
first_name VARCHAR(10) NOT NULL,
last_name VARCHAR(10) NOT NULL,
sex VARCHAR(5) NOT NULL,
score INT NOT NULL,
copy_id INT NOT NULL,
PRIMARY KEY (`id`)
);
插入 100w 条数据
DROP PROCEDURE IF EXISTS add_user;
DELIMITER //
create PROCEDURE add_user(in num INT)
BEGIN
DECLARE rowid INT DEFAULT 0;
DECLARE firstname CHAR(1);
DECLARE name1 CHAR(1);
DECLARE name2 CHAR(1);
DECLARE lastname VARCHAR(3) DEFAULT '';
DECLARE sex CHAR(1);
DECLARE score CHAR(2);
WHILE rowid < num DO
SET firstname = SUBSTRING('赵钱孙李周吴郑王林杨柳刘孙陈江阮侯邹高彭徐',FLOOR(1+21*RAND()),1);
SET name1 = SUBSTRING('一二三四五六七八九十甲乙丙丁静景京晶名明铭敏闵民军君俊骏天田甜兲恬益依成城诚立莉力黎励',ROUND(1+43*RAND()),1);
SET name2 = SUBSTRING('一二三四五六七八九十甲乙丙丁静景京晶名明铭敏闵民军君俊骏天田甜兲恬益依成城诚立莉力黎励',ROUND(1+43*RAND()),1);
SET sex=FLOOR(0 + (RAND() * 2));
SET score= FLOOR(40 + (RAND() *60));
SET rowid = rowid + 1;
IF ROUND(RAND())=0 THEN
SET lastname =name1;
END IF;
IF ROUND(RAND())=1 THEN
SET lastname = CONCAT(name1,name2);
END IF;
insert INTO user100w (first_name,last_name,sex,score,copy_id) VALUES (firstname,lastname,sex,score,rowid);
END WHILE;
END //
DELIMITER ;
call add_user(1000000);
测试未加索引时的查询速度:
SELECT * FROM user100w WHERE first_name = '刘';
结果:
测试添加索引后的查询速度:
添加索引:
CREATE INDEX 索引名 ON 表名('字段名');
-- id_表名_字段名
-- CREATE INDEX 索引名 ON 表名('字段名');
CREATE INDEX id_user100w_score ON user100w(`score`);
查询:
SELECT * FROM user100w WHERE score=88;
结果:
索引在小数据量的时候,用处不大,但是在大数据量的时候,区别十分明显。
使用日期函数过滤数据注意事项
-
无论什么时候指定一个日期,不管是插入或更新表值还是用 WHERE 字句进行过滤,日期必须为 yyyy-MM-dd;年份应该总是使用 4 位数字的年份,虽然 MySQL 会处理 00-69 为 2000-2069,处理 70-99 为 1970-1999,但使用完整的 4 位数字年份更可靠。
-
使用
SELECT cust_id, order_num FROM orders WHERE order_date = '2005-09-01';
,此 SELECT 语句会正常运行,它会检索一个订单记录,该订单记录的 order_state 为 2005-09-01。但使用WHERE order_date = '2005-09-01'
并不可靠,order_date 的数据类型为 datetime。这种类型存储日期及时间值。样例表中的值全都具有时间值 00:00:00,但实际中很可能并不总是这样。如果用当前日期和时间存储订单日期(因此你不仅知道订单日期,还知道下订单当天的时间),怎么办?比如,存储的 order_date 值为 2005-09-01 11:30:05,则WHERE order_date = '2005-09-01'
失败。即使给出具有该日期的一行,也不会把它检索出来,因为 WHERE 匹配失败。解决办法是指示 MySQL 仅将给出的日期与列中的日期部分进行比较,而不是将给出的日期与整个列值进行比较。为此,必须使用 Date()函数。Date(order_date)指示 MySQL 仅提取列的日期部分,更可靠的 SELECT 语句为:
SELECT cust_id, order_num FROM orders WHERE Date(order_date) ='2005-09-01';
如果要使用的日期,使用 Date()函数,即使知道相应的列只包含日期也是如此。这样,如果由于某种原因表中以后有日期和时间值,SQL 代码也不用改变。
eg:检索出 2005 年 9 月下的所有订单
方式一:使用 BETWEEN 操作符用来把 2005-09-01 和 2005-09-30 定义为一个要匹配的日期范围。
SELECT cust_id, order_num FROM orders WHERE Date(order_date) BETWEEN '2005-09-01' AND '2005-09-30';
方式二:使用 Year()和 Month()函数(不需要记住每个月中有多少天或不需要操作闰年 2 月的办法),Year()是一个从日期(或日期时间)中返回年份的函数,Month()从日期中返回月份。
SELECT cust_id, order_num FROM orders WHERE Year(order_date) = 2005 AND Month(order_date) = 9;
- 但是要注意另外一点,使用日期函数可以更方便的处理时间日期,但也会造成索引失效问题,因此在写 sql 时要综合考虑。
使用聚集函数汇总数据注意事项
函数 | 说明 |
---|---|
AVG( ) | 返回某列的平均值 |
COUNT( ) | 返回某列的行数 |
MAX( ) | 返回某列的最大值 |
MIN( ) | 返回某列的最小值 |
SUM( ) | 返回某列值之后 |
- AVG( )函数只能用来确定特定数值列的平均值,而且列名必须作为函数参数给出;为了获得多个列的平均值,必须使用多个 AVG( )函数。AVG( )函数会忽略列值为 NULL 的行。
- 使用 COUNT()、COUNT(1) 对表中行的数目进行计数,不管表列中包含的是空值(NULL)还是非空值;使用 COUNT(column)对特定列中具有值的行进行计数,忽略 NULL 值;若列名为主键,COUNT(列名)会比 COUNT(1)快;若列名不为主键,COUNT(1)会比 COUNT(列名)快;若表多个列并且没有主键,则 COUNT(1)的执行效率优于 COUNT( * );若表有主键,则 SELECT COUNT(主键)的执行效率是最优的;若表只有一个字段,则 SELECT COUNT()最优。在实际业务中一般用 COUNT(1)比较普遍,但是如果需要聚合多个列,则用 COUNT(列名)比较合适。
- MAX( )、MIN( )函数都会忽略列值为 NULL 的行,虽然 MAX( )、MIN( )一般用来找出最大 / 最小的数值或日期值,但 MySQL 允许将它用来返回任意列中的最大值 / 最小值,包括返回文本列中的最大值 / 最小值。在用于文本数据时,如果数据按相应的列排序,则 MAX( )返回最后一行,MIN( )返回最前面的行。
- SUM( )函数会忽略列值为 NULL 的行,但计算结果也有可能返回 NULL,在使用时应该配合 IFNULL( )函数一起使用。
- 在使用 DISTINCT( )函数时,如果指定列名,则 DISTINCT 只能用于 COUNT( )。DISTINCT 不能用于 COUNT(*),因此不允许使用 COUNT(DISTINCT),否则会产生错误。类似地,DISTINCT 必须使用列名,不能用于计算或表达式。
使用分组注意事项
-
如果分组列中具有 NULL 值,则 NULL 将作为一个分组返回。如果列中有多行 NULL 值,它们将分为一组。
-
GROUP BY 子句必须出现在 WHERE 之后,ORDER BY 子句之前。
-
使用 WITH ROLLUP 关键字,可以得到每个分组以及每个分组汇总级别(针对每个分组)的值
SELECT vend_id, COUNT(*) AS num_prods FROM products GROUP BY vend_id WITH ROLLUP;
结果
+---------+-----------+ | vend_id | num_prods | +---------+-----------+ | 1001 | 3 | | 1002 | 2 | | 1003 | 7 | | 1005 | 2 | | NULL | 14 | +---------+-----------+ 5 rows in set (0.00 sec)
-
SELECT 子句顺序
子句 说明 是否必须使用 SELECT 要返回的列或表达式 是 FROM 从中检索数据的表 仅在从表选择数据时使用 WHERE 行级过滤 否 GROUP BY 分组说明 仅在按组计算聚集时使用 HAVING 组级过滤 否 ORDER BY 输出排序顺序 否 LIMIT 要检索的行数 否
使用联结注意事项
-
应该保证所有联结都有 WHERE 子句,否则 MySQL 将返回比想要的数据多得多的数据。同理,应该保证 WHERE 子句的正确性。不正确的过滤条件将导致 MySQL 返回不正确的数据。目前为止使用的联结称为等值联结,它基于两个表之间的相等测试。MySQL 在运行时关联 指定的每个表以处理联结。这种处理可能是非常耗费资源的,因此应该仔细,不要联结不必要的表。联结的表越多,性能下降越厉害。
-
多做实验。为执行任一给定的 SQL 操作,一般存在不止一种方法。很少有绝对正确或绝对错误的方法。性能可能会受操作类型、表中数据量、是否存在索引或键以及其他一些条件的影响。因此,有必要对不同的选择机制进行实验,以找出最适合具体情况的方法。
-
注意所使用的联结类型。一般我们使用内部联结,但使用外部联结也是有效的;保证使用正确的联结条件,否则将返回不正确的数据;应该总是提供联结条件,否则会得出笛卡尔积;在一个联结中可以包含多个表,甚至对于每个联结可以采用不同的联结类型。虽然这样做是合法的,一般也很有用,但应该在一起测试它们前,分别测试每个联结。这将使故障排除更为简单。
使用全文本搜索注意事项
- 并非所有引擎都支持全文本搜索。MyISAM 支持全文本搜索,InnoDB 不支持。
- 不要在导入数据时使用 FULLTEXT。更新索引要花时间,虽然不是很多,但毕竟要花时间。如果正在导入数据到另一个新表,此时不应该启用 FULLTEXT 索引。应该首先导入所有数据,然后再修改表,定义 FULLTEXT。这样有助于更快地导入数据(而且使索引数据的总时间小于在导入每行时分别进行索引所需的总时间)。
改善性能
- 决不要检索比需求还要多的数据。换言之,不要用 SELECT *(除非你真正需要每个列)。
- 必须索引数据库表以改善数据检索的性能。确定索引什么不是一件微不足道的任务,需要分析使用的 SELECT 语句以找出重复的 WHERE 和 ORDER BY 子句。如果一个简单的 WHERE 子句返回结果所花的时间太长,则可以断定其中使用的列(或几个列)就是需要索引的对象。
- 索引改善数据检索的性能,但损害数据插入、删除和更新的性能。如果你有一些表,它们收集数据且不经常被搜索,则在有必要之前不要索引它们。(索引可根据需要添加和删除。)
- LIKE 很慢。一般来说,最好是使用 FULLTEXT 而不是 LIKE。