数据库
全部内容请访问GitHub仓库
1. 彻底理解数据库事物
一:事物的定义:
事务(Transaction),一般是指要做的或所做的事情。在计算机术语中是指访问并可能更新数据库中各种数据项的一个程序执行单元(unit)。在计算机术语中,
事务通常就是指数据库事务。
通俗的讲:事务,由若干条语句组成的,指的是要做的一系列操作。
二:事物的目的:
1、为数据库操作提供了一个从失败中恢复到正常状态的方法,同时提供了数据库即使在异常状态下仍能保持一致性的方法。
2、当多个应用程序在并发访问数据库时,可以在这些应用程序之间提供一个隔离方法,以防止彼此的操作互相干扰。
解释:当一个事务被提交给了DBMS(数据库管理系统),则DBMS需要确保该事务中的所有操作都成功完成且其结果被永久保存在数据库中
,如果事务中有的操作没有成功完成,则事务中的所有操作都需要被回滚,回到事务执行前的状态(要么全执行,要么全都不执行);同时,
该事务对数据库或者其他事务的执行无影响,所有的事务都好像在独立的运行。
但在现实情况下,失败的风险很高。在一个数据库事务的执行过程中,有可能会遇上事务操作失败、数据库系统/操作系统失败,
甚至是存储介质失败等情况。这便需要DBMS对一个执行失败的事务执行恢复操作,将其数据库状态恢复到一致状态(数据的一致性得到保证的状态)。
为了实现将数据库状态恢复到一致状态的功能,DBMS通常需要维护事务日志以追踪事务中所有影响数据库数据的操作。
三:事物的特性:
并非任意的对数据库的操作序列都是数据库事务,事务应该具有4个属性:原子性、一致性、隔离性、持久性,这四个属性通常称为ACID特性。
原子性(Atomicity):事务作为一个整体被执行,包含在其中的对数据库的操作要么全部被执行,要么都不执行。
一致性(Consistency):事务应确保数据库的状态从一个一致状态转变为另一个一致状态,一致状态的含义是数据库中的数据应满足完整性约束。
隔离性(Isolation):多个事务并发执行时,一个事务的执行不应影响其他事务的执行。
持久性(Durability):一个事务一旦提交,他对数据库的修改应该永久保存在数据库中。
四:举例:
用一个常用的“A账户向B账号汇钱”的例子来说明如何通过数据库事务保证数据的准确性和完整性。熟悉关系型数据库事务的都知道从帐号A到帐号B需要6个操作:
1、从A账号中把余额读出来(500);
2、对A账号做减法操作(500-100);
3、把结果写回A账号中(400);
4、从B账号中把余额读出来(500);
5、对B账号做加法操作(500+100);
6、把结果写回B账号中(600)。
原子性:
保证1-6所有过程要么都执行,要么都不执行;一旦在执行某一步骤的过程中发生问题,就需要执行回滚操作;
假如执行到第五步的时候,B账户突然不可用(比如被注销),那么之前的所有操作都应该回滚到执行事务之前的状态。
一致性:
在转账之前,A和B的账户中共有500+500=1000元钱。在转账之后,A和B的账户中共有400+600=1000元;
也就是说,数据的状态在执行该事务操作之后从一个状态改变到了另外一个状态。同时一致性还能保证账户余额不会变成负数等。
隔离性:
在A向B转账的整个过程中,只要事务还没有提交(commit),查询A账户和B账户的时候,两个账户里面的钱的数量都不会有变化;
如果在A给B转账的同时,有另外一个事务执行了C给B转账的操作,那么当两个事务都结束的时候,B账户里面的钱应该是A转给B的钱加上C转给B的钱再加上自己原有的钱。
持久性:
一旦转账成功(事务提交),两个账户的里面的钱就会真的发生变化(会把数据写入数据库做持久化保存)!
原子性和一致性:
一致性与原子性是密切相关的,原子性的破坏可能导致数据库的不一致,数据的一致性问题并不都和原子性有关;
比如刚刚的例子,在第五步的时候,对B账户做加法时只加了50元。那么该过程可以符合原子性,但是数据的一致性就出现了问题。
因此,事务的原子性与一致性缺一不可!
五:MySQL隔离级别:
隔离性不好,事务的操作就会互相影响,带来不同严重程度的后果;
首先看看隔离性不好,带来哪些问题:
- 更新丢失Lost Update
事务A和B,更新同一个数据,它们都读取了初始值100,A要减10,B要加100,A减去10后更新为90,B加
100更新为200,A的更新丢失了,就像从来没有减过10一样。 - 脏读
事务A和B,事务B读取到了事务A未提交的数据(这个数据可能是一个中间值,也可能事务A后来回滚事
务);事务A是否最后提交并不关心,只要读取到了这个被修改的数据就是脏读。 - 不可重复读Unrepeatable read
事务A在事务执行中相同查询语句,得到了不同的结果,不能保证同一条查询语句重复读相同的结果就是不可
以重复读。
例如,事务A查询了一次后,事务B修改了数据,事务A又查询了一次,发现数据不一致了。
注意,脏读讲的是可以读到相同的数据的,但是读取的是一个未提交的数据,而不是提交的最终结果。 - 幻读Phantom read
事务A中同一个查询要进行多次,事务B插入数据,导致A返回不同的结果集,如同幻觉,就是幻读;
数据集有记录增加了,可以看做是增加了记录的不可重复读。
有了上述问题,数据库就必须要解决,提出了隔离级别。
隔离级别由低到高,如下:
READ UNCOMMITTED 读取到未提交的数据
READ COMMITTED 读已经提交的数据,ORACLE默认隔离级别
REPEATABLE READ 可以重复读,MySQL的 默认隔离级别
SERIALIZABLE 可串行化,事务间完全隔离,事务不能并发,只能串行执行
隔离级别越高,串行化越高,数据库执行效率低;隔离级别越低,并行度越高,性能越高;隔离级别越高,当前事务处理的中间结果对其它事务不可见程度越高。
其中:
SERIALIZABLE,串行了,解决所有问题,但是效率极低;
REPEATABLE READ,事务A中同一条查询语句返回同样的结果,就是可以重复读数据了。例如语句为(select *
from user)。实现可重复读的办法有:
1、对select的数据加锁,不允许其它事务删除、修改的操作;
2、第一次select的时候,对最后一次确切提交的事务的结果做快照;
解决了不可以重复读,但是有可能出现幻读;因为另一个事务可以增删数据。
READ COMMITTED,在事务中,每次select可以读取到别的事务刚提交成功的新的数据,因为读到的是提交后的
数据,解决了脏读,但是不能解决 不可重复读 和 幻读 的问题。因为其他事务前后修改了数据或增删了数据。
READ UNCOMMITTED,能读取到别的事务还没有提交的数据,完全没有隔离性可言,出现了脏读,当前其他问题
都可能出现。
六:事物语法:
START TRANSACTION
或BEGIN
开始一个事务,START TRANSACTION
是标准SQL的语法;
使用COMMIT
提交事务后,变更成为永久变更。
ROLLBACK
可以在提交事务之前,回滚变更,事务中的操作就如同没有发生过一样(原子性);
SET AUTOCOMMIT
语句可以禁用或启用默认的autocommit
模式,用于当前连接。SET AUTOCOMMIT = 0
禁用自
动提交事务;如果开启自动提交,如果有一个修改表的语句执行后,会立即把更新存储到磁盘。
参考博客:彻底明白事物
2. 数据库和数据仓库的区别
本质上来说没有区别,都是存放数据的地方;但是数据库关注数据的持久化、数据的关系,为业务系统提供支持,事务支持;
数据仓库存储数据的是为了分析或者发掘而设计的表结构,可以存储海量数据。
数据库存储在线交易数据OLTP(联机事务处理OLTP,On-line Transaction Processing);数据仓库存储历史数据
用于分析OLAP(联机分析处理OLAP,On-Line Analytical Processing)。
数据库支持在线业务,需要频繁增删改查;数据仓库一般囤积历史数据支持用于分析的SQL,一般不建议删改。
3. 数据库的安装
关系型数据库以安装MariaDB为例(centos7环境):
[root@centos7 ~]# yum list | grep mariadb
mariadb-libs.x86_64 1:5.5.60-1.el7_5 @anaconda
mariadb.x86_64 1:5.5.60-1.el7_5 base
mariadb-bench.x86_64 1:5.5.60-1.el7_5 base
mariadb-devel.i686 1:5.5.60-1.el7_5 base
mariadb-devel.x86_64 1:5.5.60-1.el7_5 base
mariadb-embedded.i686 1:5.5.60-1.el7_5 base
mariadb-embedded.x86_64 1:5.5.60-1.el7_5 base
mariadb-embedded-devel.i686 1:5.5.60-1.el7_5 base
mariadb-embedded-devel.x86_64 1:5.5.60-1.el7_5 base
mariadb-libs.i686 1:5.5.60-1.el7_5 base
mariadb-server.x86_64 1:5.5.60-1.el7_5 base
mariadb-test.x86_64 1:5.5.60-1.el7_5 base
安装mariadb服务,会自动安装mariadb
[root@centos7 ~]# yum install mariadb-server
[root@centos7 ~]# systemctl start mariadb.service
[root@centos7 ~]# ss -tanl
LISTEN 0 50 *:3306 *:*
开机启动
[root@centos7 ~]# systemctl enable mariadb.service
为了安全设置Mysql服务
[root@centos7 ~]# mysql_secure_installation
数据库密码登陆
[root@centos7 ~]# mysql -u root -p
mysql> show databases;
+--------------------+
| Database |
+--------------------+
| information_schema |
| mysql |
| performance_schema |
+--------------------+
3 rows in set (0.00 sec)
创建并授权用户
mysql> grant all on *.* to 'root'@'%' identified by 'daneil11';
mysql> flush privileges;
4. SQL语句概述
SQL是结构化查询语言Structured Query Language,1987年被ISO组织标准化。
所有主流的关系型数据库都支持SQL,NoSQL也有很大一部分支持SQL。
SQL语句分类:
DDL数据定义语言,负责数据库定义、数据库对象定义,由CREATE
、ALTER
与DROP
三种语句组
成;
DML数据操作语言,负责对数据库对象的操作,CRUD
增删改查;
DCL数据控制语言,负责数据库权限访问控制,由 GRANT
和 REVOKE
两个指令组成;
TCL事务控制语言,负责处理ACID
事务,支持commit
、rollback
指令;
语言规范:
SQL
语句大小写不敏感
一般建议,SQL
的关键字、函数等大写
SQL
语句末尾应该使用分号结束
注释
多行注释 /*注释内容*/
单行注释 -- 注释内容
MySQL
注释可以使用#
使用空格或缩进来提高可读性
命名规范
必须以字母开头
可以使用数字、#、$和_
不可使用关键字
DCL
GRANT
授权、REVOKE
撤销;
GRANT ALL ON employees.* TO 'root' IDENTIFIED by 'daneil11';
REVOKE ALL ON *.* FROM 'duanming'
*
为通配符,指代任意库或者任意表; *.*
所有库的所有表; employees.*
表示employees库下所有的表;%
为通配符,它是SQL语句的通配符,匹配任意长度字符串;
DDL
- 创建数据库,所有数据按照数据模型组织在数据库中;
CREATE DATABASE IF NOT EXISTS test CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
CREATE DATABASE IF NOT EXISTS test CHARACTER SET utf8;
CHARACTER SET
指定字符集; utf8mb4
是utf8
的扩展,支持4字节utf8mb4
,需要MySQL5.5.3+
; COLLATE
指定字符集的校对规则,用来做字符串的比较的;例如a、A谁大?
- 删除数据库;
DROP DATABASE IF EXISTS test;
- 删除用户;
DROP USER duanming;
创建表
表也称为关系,分为行和列,MySQL是行存数据库。数据是一行行存的,列必须固定多少列。
行Row,也称为记录Record,元组。
列Column,也称为字段Field、属性。
字段的取值范围叫做 域Domain。例如gender字段的取值就是M或者F两个值。
- 选择创建到的数据库
use table_name;
- 创建表
CREATE TABLE `employees` (
`emp_no` int(11) NOT NULL,
`birth_date` date NOT NULL,
`first_name` varchar(14) NOT NULL,
`last_name` varchar(16) NOT NULL,
`gender` enum('M', 'F') NOT NULL,
`hire_date` date NOT NULL,
PRIMARY KEY (`emp_no`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
其中,反引号标注的名称,会被认为是非关键字,使用反引号避免冲突。
DESC
查看列信息:
{DESCRIBE | DESC} tbl_name [col_name | wild]
DESC employees;
DESC employees '%name';
5. 什么是主键、外键、关系等概念
关系
关系是什么?
在关系数据库中,关系就是二维表,由行和列组成。
二维表中的术语?
行Row
,也称为记录Record
,元组;
列Column
,也称为字段Field
、属性;
字段的取值范围叫做 域Domain。例如gender字段的取值就是M或者F两个值;
维数:关系的维数指关系中属性的个数;
基数:元组的个数。
注意在关系中,属性的顺序并不重要;理论上,元组顺序也不重要,但是由于元组顺序与存储相关,会
影响查询效率。
候选键
关系中,能唯一标识一条元组的属性或属性集合,称为候选键。
候选键,表中一列或者多列组成唯一的key,通过这一个或者多个列能唯一的标识一条记录。
表中可能有多个候选键。
PRIMARY KEY主键
从候选键中选择出主键。
主键的列不能包含空值null
。主键往往设置为整型、长整型
,可以为自增AUTO_INCREMENT
字段。
表中可以没有主键,但是,一般表设计中,往往都会有主键,以避免记录重复。InnoDB的表要求
使用主键。
Foreign KEY外键
严格来说,当一个关系中的某个属性或属性集合与另一个关系(也可以是自身)的候选键匹配时,就称
作这个属性或属性集合是外键。
索引Index
可以看做是一本字典的目录,为了快速检索用的;空间换时间,显著提高查询效率。
可以对一列或者多列字段设定索引。
主键索引
,主键会自动建立主键索引,主键本身就是为了快速定位唯一记录的。
唯一索引
,表中的索引列组成的索引必须唯一,但可以为空,非空值必须唯一。
普通索引
,没有唯一性的要求,就是建了一个字典的目录而已。
联合索引,多个字段组合创建索引,使用条件查询时,先匹配左边字段。
全文索引,MyISAM使用,对Char、Varchar、TEXT类型使用。
空间索引,SPATIAL,基本不用。
在MySQL
中,InnoDB
和MyISAM
的索引数据结构可以使用Hash
或BTree
,innodb
默认是BTree
。
Hash
时间复杂度是O(1)
,但是只能进行精确匹配,也就是Hash
值的匹配,比如范围匹配就没办法了,
hash
值无序所以无法知道原有记录的顺序。Hash
问题较多。
BTree
索引,以B+树为存储结构。
虽然,索引可以提高查询所读,但是却影响增删改的效率,因为需要索引更新或重构。频繁出现在
where
子句中的列可以考虑使用索引。要避免把性别这种字段设索引。
B+树节点组织成一棵树; 节点分为内部节点
和叶子节点
。
内部节点
内部节点不存储数据,叶子节点不存储指针。
每个leaf node保存数据,所有的leaf node组织成链表。假设读取16到22的数据,找到18后,顺着链表
往后遍历读取即可。
InnoDB中,数据文件本身就是按主键索引存储的,叶子节点中保存的就是数据记录。
如果在其他字段上定义B+Tree索引,叶子节点的数据记录的是主键,这种称为辅助索引。
6. 约束
为了保证数据的完整正确,数据模型还必须支持完整性约束。
- “必须有值约束”约束
某些列的值必须有值,不许为空NULL。 - 域约束Domain Constraint
限定了表中字段的取值范围. - 实体完整性Entity Integrity
PRIMARY KEY约束定义了主键,就定义了主键约束。主键不重复且唯一,不能为空。
7. 引用完整性
引用完整性Referential Integrity
外键定义中,可以不是引用另一张表的主键,但是,往往实际只会关注引用主键。
外键:在表B中的列,引用了表A中的主键,表B中的列就是外键。
A表称为主表,B表称为从表。
插入规则
不需要指定。
如果在表B插入一条记录,B的外键列插入了一个值,这个值必须是表A中存在的主键值。
更新规则
定义外键约束时指定该规则。
删除规则
定义外键约束时指定该规则。
外键约束的操作
设定值 说明
CASCADE
级联,从父表删除或更新会自动删除或更新子表中匹配的行
SET NULL
从父表删除或更新行,会设置子表中的外链列为NULL,但必须保证子表列没有指定NOT NULL,也就是说子表的字段可以为NULL才行
RESTRICT
如果从父表删除主键,如果子表引用了,则拒绝对父表的删除或更新操作
NO ACTION
标准sql的关键字,在MySQL中与RESTRICT相同,拒绝对父表的删除或更新操作外键约束,是为了保证数据完整性、一致性,杜绝数据冗余、数据错误。
8. MyISAM和InnoDB
MyISAM
适合于一些需要大量查询的应用,但其对于有大量写操作并不是很好。甚至你只是需要update一个字段,整个表都会被锁起来,而别的进程,就算是读进程都无法操作直到读操作完成。另外,MyISAM
对于 SELECT COUNT(*)
这类的计算是超快无比的。
InnoDB
的趋势会是一个非常复杂的存储引擎,对于一些小的应用,它会比 MyISAM
还慢。他是它支持“行锁” ,于是在写操作比较多的时候,会更优秀。并且,他还支持更多的高级应用,比如:事务。
参考博客园:MySQL数据库引擎详解
参考segmentfault: MySQL数据库引擎
9. 乐观锁和悲观锁
悲观锁:假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作;
乐观锁:假设不会发生并发冲突,只在提交操作时检查是否违反数据完整性。
10. 数据库编程
驱动
封装好MySQL协议的包,习惯上称为驱动程序;与MySQL通信就是典型的CS模式
,Server就是服务器端,使用客户端先建立连接,数据库编程时,这个客户端变成了程序。无论是任何一个封装好MySQL协议的包都是基于TCP协议
之上开发,传输的数据必须遵循MySQL的协议。
MySQL的驱动
MySQLdb是最有名的库,对MySQL的C Client封装实现,支持Python 2,现在不更新了,所以不支持Python3
;
mysqlclient在MySQLdb的基础上,增加了对Python 3的支持;安装的时候使用pip install mysqlclient
进行安装,使用的时候还使用import MySQLdb
进行导入使用。
pymysql是语法兼容MySQLdb,使用纯Python写的MySQL客户端库,支持Python 3,在CPython 2.7 、3.4+之后
才可以使用,并且要求MySQL版本是MySQL 5.5+、MariaDB 5.5+
。
pymysql的使用
安装
pip install pymysql
连接
# conn.json文件中:
{
"host":"127.0.0.1",
"user":"wayne",
"password":"wayne",
"database":"test",
"port": 3306
}
import pymysql
import simplejson
conf = simplejson.load(open('conn.json'))
print(conf)
conn = None
try:
conn = pymysql.connect(**conf)
print(type(conn), conn)
conn.ping(False) # ping不同通异常,True重连
finally:
if conn:
conn.close()
游标
操作数据库,必须使用游标,需要先获取一个游标对象;Connection.cursor(cursor=None)
方法返回一个新的游标对象。
连接没有关闭前,游标对象可以反复使用。
cursor参数,可以指定一个Cursor类。如果为None,则使用默认Cursor类
,Cursor类的实例,可以使用execute()
方法执行SQL语句,成功返回影响的行数
。
一般使用流程
- 建立连接
- 获取游标
- 执行SQL
- 提交事务
- 释放资源
import pymysql
from pymysql.cursors import DictCursor
conn = pymysql.connect(host='192.168.116.66', user='root', password='dmneil11', database='test', port=3306, cursorclass=DictCursor) # port可以省略
# pymysql.connect跟进去看源码发现是Connection类,进行实例化的时候参数有一个cursorclass=Cursor,默认是Cursor类的实例,我们可以修改这个参数,
# 可以修改为DictCursor,这个类的签名是 class DictCursor(DictCursorMixin, Cursor), 这个DictCursor返回的是一个字典,返回一行,是一个字典。
# 返回多行,放在列表中,元素是字典,代表一行。
# 同时还有个参数是autocommit: Autocommit mode. None means use server default. (default: False),相当于set aotocommit=0
# 同时Connection类的实例有绑定方法__enter__和__exit__,所以可以使用上下文,但是这里的上下文不是像文件,socket那样用来关闭资源的
# 这里的__enter__返回一个Cursor类实例,__exit__不是关闭这个Connection实例释放资源,而是用来提交这次事务,如果有错误就rollback,没有
# 错误就commit
# Cursor实例也支持上下文,这里就很正常了,在__enter__中返回自身,__exit__中关闭这个cursor资源。
# Cursor实例在执行了execute方法之后可以使用Cursor类获取查询结果集的方法,有fetchone(), fetchmany(size=None),fetchall()方法,
# 同时还有rownumber属性返回当前行号,其实就是修改游标指针的位置,可以修改这个属性,并且可以修改为负数,还有rowcount属性,这个属性返回返回的总行数
with conn as cursor: # conn的__enter__返回的是当前设置的Cursor类的实例
with cursor: # cursor类的__enter__返回的是自身
result = cursor.execute("select * from employees")
print(result) # 受影响的行数
print(1, cursor.fetchone()) # 拿到一行,此时指针已经指到第二个位置了
print(2, cursor.fetchmany(4)) # 从指针指的第二个位置开始拿三个,拿完之后指针向后指4个
print(3, cursor.fetchall()) # 从当前指针位置一直读取到全部,读完之后指针指到末尾
print(4, cursor.fetchall()) # 此时已经拿不到东西了
cursor.rownumber = 1
print(5, cursor.fetchone()) # 重新调整指针的位置,再拿一个,又能拿到了
# 注意fetch操作的是结果集,结果集是已经查询之后保存在客户端里了。
print(6,'结果集的行数', cursor.rowcount) # 结果集的行数
# cursor的__exit__会关闭,不用管了
# conn的__exit__只会提交或回滚,不会关闭,所以要手动关一次
conn.close()
解释
pymysql.connect
跟进去看源码
发现是Connection
类,进行实例化的时候参数有一个cursorclass=Cursor
,默认是Cursor
类的实例,我们可以修改这个参数,可以修改为DictCursor
,这个类的签名是 class DictCursor(DictCursorMixin, Cursor)
, 这个DictCursor
返回的是一个字典,返回一行,是一个字典。
同时还有个参数是autocommit: Autocommit mode. None means use server default. (default: False)
,相当于set aotocommit=0
;
同时Connection类的实例有绑定方法__enter__
和__exit__
,所以可以使用上下文,但是这里的上下文不是像文件,socket那样用来关闭资源的这里的__enter__
返回一个Cursor类实例,__exit__
不是关闭这个Connection
实例释放资源,而是用来提交这次事务,如果有错误就rollback
,没有错误就commit
;
Cursor
实例也支持上下文,这里就很正常了,在__enter__
中返回自身,__exit__
中关闭这个cursor
资源。
Cursor
实例在执行了execute
方法之后可以使用Cursor
类获取查询结果集的方法,有fetchone(), fetchmany(size=None),fetchall()
方法,同时还有rownumber
属性返回当前行号,其实就是修改游标指针的位置,可以修改这个属性,并且可以修改为负数,还有rowcount
属性,这个属性返回返回的总行数。
pip install mysqlclient
命令安装的MySQLdb
和pymysql
的使用方法基本一致,但是MySQLdb
的connect
实例不支持上下文。
11. MySQL索引和页的关系
B-tree,B是balance
,一般用于数据库的索引。使用B-tree结构可以显著减少定位记录时所经历的中间过程,从而加快存取速度。而B+tree是B-tree的一个变种,MySQL就普遍使用B+tree实现其索引结构。
一般来说,索引本身也很大
,不可能全部存储在内存中,因此索引往往以索引文件的形式存储的磁盘上。这样的话,索引查找过程中就要产生磁盘I/O消耗,相对于内存存取,I/O存取的消耗要高几个数量级,所以评价一个数据结构作为索引的优劣最重要的指标就是在查找过程中磁盘I/O操作次数的渐进复杂度。换句话说,索引的结构组织要尽量减少查找过程中磁盘I/O的存取次数。
为了达到这个目的,磁盘按需读取,要求每次都会预读的长度一般为页的整数倍。而且数据库系统将一个节点的大小设为等于一个页,这样每个节点只需要一次I/O就可以完全载入。每次新建节点时
,直接申请一个页的空间
,这样就保证一个节点物理上也存储在一个页里,加之计算机存储分配都是按页对齐
的,就实现了一个node只需一次I/O
。并把B-tree中的m值设的非常大,就会让树的高度降低,有利于一次完全载入。
12. MySQL索引的B+树、聚簇索引和非聚簇索引
B+Tree结构都
可以用在MyISAM和InnoDB上,mysql中,不同的存储引擎对索引的实现方式不同,大致说下MyISAM和InnoDB两种存储引擎。
MyISAM
的是非聚簇索引
,B+Tree的叶子节点上的data,并不是数据本身,而是数据存放的地址
。主索引和辅助索引没啥区别,只是主索引中的key一定得是唯一的。这里的索引都是非聚簇索引。非聚簇索引的两棵B+树看上去没什么不同,节点的结构完全一致只是存储的内容不同而已,主键索引B+树的节点存储了主键,辅助键索引B+树存储了辅助键。表数据存储在独立的地方
;
这两颗B+树的叶子节点都使用一个地址指向真正的表数据
,对于表数据来说,这两个键没有任何差别。由于索引树是独立
的,通过辅助键检索无需访问主键的索引树。InnoDB的数据文件本身就是索引文件,B+Tree的叶子节点上的data就是数据本身,key为主键,这是聚簇索引
。聚簇索引,叶子节点上的data是主键(所以聚簇索引的key,不能过长)。
InnoDB
使用的是聚簇索引
,将主键组织到一棵B+树中,而行数据
就储存在叶子节点上
,若使用where id = 14
这样的条件查找主键,则按照B+树的检索算法即可查找到对应的叶节点,之后获得行数据。若对Name列进行条件搜索,则需要两个步骤:第一步
在辅助索引
B+树中检索Name,到达其叶子节点获取对应的主键
。第二步使用主键在主索引B+树
种再执行一次B+树检索操作,最终到达叶子节点即可获取整行数据。
总结就是非聚簇索引
的辅助索引和主键索引的B+树内部结点存储的分别是辅助键和主键,叶子结点都存的是表数据真实独立的地方;而聚簇索引则是辅助索引B+树的内部结点是辅助键
,叶子结点存的是主键
,主键索引的B+树的内部结点存的是主键
,叶子结点存的是真的表数据
。
聚簇索引
的数据的物理存放顺序
与索引顺序
是一致
的,即:只要索引是相邻
的,那么对应的数据
一定也是相邻
地存放在磁盘上的。聚簇索引要比非聚簇索引查询效率高很多
。聚集索引这种主+辅索引
的好处是,当发生数据行移动
或者页分裂
时,辅助索引树
不需要更新,因为辅助索引树存储的是主索引的主键关键字
,而不是数据具体的物理地址。
非聚集索引
,类似于图书的附录
,那个专业术语出现在哪个章节,这些专业术语
是有顺序
的,但是出现的位置
是没有顺序的。每个表只能有一个聚簇索引,因为一个表中的记录只能以一种物理顺序存放。但是,一个表可以有不止一个非聚簇索引。
13. 索引的优缺点
优点:
优点:
第一,通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性。
第二,可以大大加快数据的检索速度,这也是创建索引的最主要的原因。
第三,可以加速表和表之间的连接,特别是在实现数据的参考完整性方面特别有意义。
第四,在使用分组和排序子句进行数据检索时,同样可以显著减少查询中分组和排序的时间。
第五,通过使用索引,可以在查询的过程中,使用优化隐藏器,提高系统的性能。
缺点:
第一,创建索引和维护索引要耗费时间,这种时间随着数据量的增加而增加。
第二,索引需要占物理空间,除了数据表占数据空间之外,每一个索引还要占一定的物理空间,如果要建立聚簇索引,那么需要的空间就会更大。
第三,当对表中的数据进行增加、删除和修改的时候,索引也要动态的维护,这样就降低了数据的维护速度。
14. 建立索引的时机
该建立索引:
(1)在作为主键的列上,强制该列的唯一性和组织表中数据的排列结构;
(2)在经常用在连接的列上,这些列主要是一些外键,可以加快连接的速度;
(3)在经常需要根据范围进行搜索的列上创建索引,因为索引已经排序,其指定的范围是连续的;
(4)在经常需要排序的列上创建索引,因为索引已经排序,这样查询可以利用索引的排序,加快排序查询时间;
(5)在经常使用在WHERE子句中的列上面创建索引,加快条件的判断速度。
不该建立索引:
(1)对于那些在查询中很少使用或者参考的列不应该创建索引。这是因为,既然这些列很少使用到,因此有索引或者无索引,并不能提高查询速度。相反,由于增加了索引,反而降低了系统的维护速度和增大了空间需求。
(2)对于那些只有很少数据值的列也不应该增加索引。这是因为,由于这些列的取值很少,例如人事表的性别列,在查询的结果中,结果集的数据行占了表中数据行的很大比例,即需要在表中搜索的数据行的比例很大。增加索引,并不能明显加快检索速度。
(3)对于那些定义为text, image和bit数据类型的列不应该增加索引。这是因为,这些列的数据量要么相当大,要么取值很少。
(4)当修改性能远远大于检索性能时,不应该创建索引。这是因为,修改性能和检索性能是互相矛盾的。当增加索引时,会提高检索性能,但是会降低修改性能。当减少索引时,会提高修改性能,降低检索性能。因此,当修改性能远远大于检索性能时,不应该创建索引。
15. sql语句优化
什么是’执行计划’
执行计划
是数据库根据SQL语句和相关表的统计信息作出的一个查询方案
,这个方案是由查询优化器
自动分析产生的,比如一条SQL语句如果用来从一个 10万条记录的表中查1条记录,那查询优化器会选择“索引查找”方式,如果该表进行了归档,当前只剩下5000条记录了,那查询优化器就会改变方案,采用 “全表扫描”方式。可见,执行计划并不是固定的,它是“带有相当个性的”。
写出统一的sql语句
对于以下两句SQL语句,很多人人认为是相同的,但是,数据库查询优化器认为是不同的。
select * from dual
select * From dual
虽然只是大小写不同,查询分析器就认为是两句不同的SQL语句,必须进行两次解析。生成2个执行计划
。所以作为程序员,应该保证相同的查询语句在任何地方都一致,多一个空格都不行!
不要把sql语句写的太过冗余
一般
,将一个Select语句的结果作为子集
,然后从该子集中
再进行查询,这种一层嵌套语句还是比较常见的,但是根据经验,超过3层嵌套,查询优化器就很容易给出错误的执行计划。因为它被绕晕了。像这种类似人工智能的东西,终究比人的分辨力要差些,如果人都看晕了,我可以保证数据库也会晕的。
另外
,执行计划是可以被重用
的,越简单的SQL语句被重用的可能性越高。而复杂的SQL语句
只要有一个字符发生变化就必须重新解析
,然后再把这一大堆垃圾塞在内存里。可想而知,数据库的效率会何等低下。
使用临时表
简化SQL语句的重要方法就是采用临时表暂存中间结果,但是,临时表的好处远远不止这些,将临时结果暂存在临时表,后面的查询就在tempdb中了,这可以避免程序中多次扫描主表,也大大减少了程序执行中“共享锁”阻塞“更新锁”,减少了阻塞,提高了并发性能。
OLTP系统SQL语句必须采用绑定变量
select * from orderheader where changetime >’2010-10-20 00:00:01′
select * from orderheader where changetime >’2010-09-22 00:00:01′
以上两句语句,查询优化器认为是不同的SQL语句,需要解析两次。如果采用绑定变量 select * from orderheader where changetime >@chgtime
, @chgtime
变量可以传入任何值,这样大量的类似查询可以重用该执行计划了,可以大大降低数据库解析SQL语句的负担。一次解析,多次重用,是提高数据库效率的原则,同时也会有效的解决sql注入攻击。
倾斜字段不要采用绑定变量,会造成绑定变量窥测
事物都存在两面性
,绑定变量对大多数OLTP
处理是适用的,但是也有例外。比如在where条件中的字段是“倾斜字段
”的时候。
“倾斜字段
”指该列中的绝大多数的值
都是相同
的,比如一张人口调查表,其中“民族
”这列,90%以上都是汉族
。那么如果一个SQL语句要查询30岁的汉族人口有多少,那“民族”这列必然要被放在where条件中。这个时候如果采用绑定变量@nation会存在很大问题。
试想如果@nation传入的第一个值是“汉族”,那整个执行计划
必然会选择表扫描。然后,第二个值传入的是“布依族”,按理说“布依族”占的比例可能只有万分之一,应该采用索引查找。但是,由于重用了
第一次解析的“汉族”
的那个执行计划
,那么第二次也将采用表扫描
方式。这个问题就是著名的“绑定变量窥测
”,建议对于“倾斜字段
”不要采用绑定变量。
只在必要的情况下才使用begin tran
SQL Server
中一句SQL语句默认
就是一个事务
,在该语句执行完成后也是默认commit
的。其实,这就是begin tran
的一个最小化的形式,好比在每句语句开头隐含
了一个begin tran,结束时隐含
了一个commit。
有些情况下,我们需要显式
声明begin tran,比如做“插、删、改”操作需要同时修改几个表,要求要么几个表都修改成功,要么都不成功
。begin tran 可以起到这样的作用,它可以把若干SQL语句套在一起执行,最后再一起commit。 好处是保证了数据的一致性,但任何事情都不是完美无缺的。Begin tran付出的代价
是在提交之前,所有SQL语句锁住的资源都不能释放
,直到commit掉。
可见,如果Begin tran套住的SQL语句太多,那数据库的性能就糟糕了。在该大事务提交之前,必然会阻塞别的语句,造成block很多。
Begin tran使用的原则是,在保证数据一致性的前提下,begin tran 套住的SQL语句越少越好
!有些情况下可以采用触发器同步数据
,不一定要用begin tran。
部分SQL查询语句加上nolock
在SQL语句中加nolock
是提高SQL Server并发性能
的重要手段,在oracle中并不需要这样做,因为oracle的结构更为合理,有undo表空间保存“数据前影
”,该数据如果在修改中还未commit,那么你读到的是它修改之前的副本,该副本放在undo表空间
中。这样,oracle的读、写可以做到互不影响,这也是oracle 广受称赞的地方。SQL Server 的读、写是会相互阻塞的,为了提高并发性能
,对于一些查询,可以加上nolock
,这样读的时候可以允许写,但缺点是
可能读到未提交的脏数据
。使用 nolock有3条原则。
- 查询的结果用于“插、删、改”的不能加nolock !
- 查询的表属于频繁发生页分裂的,慎用nolock !
使用临时表一样可以
保存“数据前影”,起到类似oracle的undo表空间
的功能,能采用临时表提高并发性能的,不要用nolock 。
聚集索引要建在表的顺序字段上,否则表容易发生页分裂
比如订单表,有订单编号orderid
,也有客户编号contactid
,那么聚集索引应该加在哪个字段上呢?对于该表,订单编号
是顺序添加
的,如果在orderid上加聚集索引,新增的行都是添加在末尾,这样不容易经常产生页分裂
。然而,由于大多数查询
都是根据客户编号
来查的,因此,将聚集索引
加在contactid上才有意义。而contactid
对于订单表而言,并非顺序字段
。
比如“张三”的“contactid”是001,那么“张三”的订单信息必须都放在这张表的第一个数据页上,如果今天“张三”新下了一个订单,那该订单信息不能放在表的最后一页,而是第一页!如果第一页放满了呢?很抱歉,该表所有数据都要往后移动为这条记录腾地方。
SQL Server的索引和Oracle的索引是不同的
,SQL Server的聚集索引
实际上是对表按照聚集索引字段的顺序进行了排序,相当于oracle的索引组织表
。SQL Server的聚集索引就是表本身
的一种组织形式
,所以它的效率是非常高的。也正因为此,插入
一条记录,它的位置不是随便放的,而是要按照顺序
放在该放的数据页
,如果那个数据页没有空间
了,就引起了页分裂
。所以很显然,聚集索引
没有
建在表的顺序字段
上,该表容易发生页分裂。
曾经碰到过一个情况,一位哥们的某张表重建索引后,插入的效率大幅下降了。估计情况大概是这样的。该表的聚集索引可能没有建在表的顺序字段上,该表经常被归档,所以该表的数据是以一种稀疏状态存在的。比如张三下过20张订单,而最近3个月的订单只有5张,归档策略
是保留3个月数据,那么张三过去的 15张订单已经被归档,留下15个空位,可以在insert发生时
重新被利用。在这种情况下由于有空位
可以利用,就不会发生页分裂
。但是查询性能
会比较低
,因为查询时必须扫描
那些没有数据的空位
。
重建聚集索引后情况改变了,因为重建聚集索引
就是把表中的数据重新排列一遍,原来的空位没有
了,而页的填充率
又很高,插入数据经常要发生页分裂,所以性能大幅下降。
对于聚集索引
没有建在顺序字段上的表,是否
要给与比较低的页填充率?是否
要避免重建聚集索引?是一个值得考虑的问题!
加noblock查询要小心
加nolock后查询经常发生页分裂的表,容易产生跳读或重复读;
加nolock后可以在“插、删、改
”的同时
进行查询
,但是由于同时发生“插、删、改”,在某些情况下,一旦
该数据页满了
,那么页分裂
不可避免,而此时nolock的查询
正在发生,比如在第100页
已经读过的记录,可能会因为
页分裂而分到第101页
,这有可能使得nolock查询在读101页时重复读到该条数据,产生“重复读
”。同理,如果在100页上
的数据还没被读到就分到99页
去了,那nolock查询有可能会漏过
该记录,产生“跳读
”。
上面提到的哥们,在加了nolock后一些操作出现报错
,估计有可能因为nolock查询产生了重复读
,2条相同
的记录
去插入别的表,当然会发生主键冲突
。
合理使用模糊查询
有的时候会需要进行一些模糊查询
比如:
select * from contact where username like ‘%yue%’
关键词 %yue%
,由于yue前面用到了“%”,因此该查询必然走全表扫描
,除非必要
,否则不要
在关键词前加%。
注意数据类型的隐式转换对查询的影响
sql server2000
的数据库,我们的程序在提交sql语句的时候,没有使用强类型
提交这个字段的值,由sql server 2000自动转换
数据类型,导致传入的参数与主键字段类型不一致
,这个时候sql server 2000可能就会使用全表扫描
。Sql server2005上没有发现这种问题,但是还是应该注意一下。
sql server版本对表连接的影响
SQL Server 表连接的三种方式:
Merge Join
Nested Loop Join
Hash Join
SQL Server 2000
只有一种join
方式——Nested Loop Join
,如果A结果集较小,那就默认作为外表,A中每条记录都要去B中扫描一遍,实际扫过的行数相当于A结果集行数x B结果集行数。所以如果两个结果集都很大,那Join的结果很糟糕。
SQL Server 2005
新增了Merge Join
,如果A表和B表的连接字段正好
是聚集索引
所在字段,那么表的顺序
已经排好
,只要两边拼上
去就行了,这种join的开销相当于A表的结果集行数加上
B表的结果集行数,一个是加
,一个是乘
,可见merge join 的效果要比Nested Loop Join好多了。
16. MySQL数据库的主从复制
MySQL 的主从复制是一个异步
的复制过程(虽然一般情况下感觉是实时的),数据将从一个 MySQL 数据库(Master)复制到另一个 MySQL 数据库(Slave),在 Master
和 Slave
之间实现整个主从复制的过程是由三个线程
参与完成的,其中有两个线程(SQL 和 I/O )在 Slave 端
,另一个线程(I/O)在 Master 端
。
要实现 MySQL
的主从复制,首先必须打开 Master
端的 binlog
记录功能
,否则就无法实现,因为整个复制过程实际上就是 Slave
从 Master
获取 binlog 日志
,然后再在 Slave
上以相同顺序
执行获取的 binlog 日志中所记录的各种 SQL 操作
。MySQL 的 binlog
功能在 /etc/my.cnf
中的 [mysqld]
模块下增加 log-bin
参数来实现。
① 主从复制是异步的逻辑的 SQL 语句级的复制;
② 复制时,主库有一个 I/O 线程,从库有两个线程,及 I/O 和 SQL 线程;
③ 实现主从复制的必要条件是主库要开启记录 binlog 的功能;
④ 作为复制的所有 MySQL 节点的 server-id 都不能相同;
⑤ binlog 文件只记录对数据内容有更改的 SQL 语句,不记录任何查询语句。
17. varchar和char的使用场景
varchar字符长度可变,但是不能超过设置的长度,char字符长度不可变,固定长度,一般我们设置登录名可以使用varchar,而加密后的密码一般设置为char,因为加密算法的结果是固定长度的。
18. 数据库连接池
官方:数据库连接池(Connection pooling)是程序启动时
建立足够的数据库连接,并将这些连接组成一个连接池,由程序动态地
对池中的连接进行申请,使用,释放。
个人理解:创建数据库连接
是一个很耗时
的操作,也容易对数据库造成安全隐患。所以,在程序初始化的时候,集中创建多个数据库连接,并把他们集中管理,供程序使用,可以保证较快
的数据库读写速度
,还更加安全可靠
。
运行机制:
(1) 程序初始化时创建连接池;
(2) 使用时向连接池申请可用连接;
(3) 使用完毕,将连接返还给连接池;
(4) 程序退出时,断开所有连接,并释放资源。