数据库基础

229 阅读13分钟

基本概念

  • DB:数据库DB是长期存储在计算机内、有组织的、统一管理的相关数据的集合。

  • DBMS:数据库管理系统是位于用户与OS之间的一层数据管理软件,它为用户或应用程序提供了访问DB的方法。

  • 概念模型:表达用户需求观点的DB全局逻辑结构的模型。

  • 逻辑模型:表达计算机实现观点的DB全局逻辑结构的模型。逻辑模型主要有层次、网状、关系和对象模型4种。

  • DDL:定义DB三级结构的语言

  • DML:对数据库进行查询操作的语言

  • 关系模型(relational model)

    • 关系(relation):一个关系对应通常说的一张表
    • 元组(tuple):表中的一行即为一个元组
    • 属性(attribute):表中的一列即为一个属性
    • 码(key):表中可以唯一确定一个元组的某个属性组
    • 域(domain):一组具有相同数据类型的值的集合
    • 分量:元组中的一个属性值
    • 关系模式:对关系的描述,一般表示为 关系名(属性1, 属性2, ..., 属性n)
  • 主键:数据库表中对储存数据对象予以唯一和完整标识的数据列或属性的组合。一个数据列只能有一个主键,且主键的取值不能缺失。

  • 外键:一个表中存在的另外一个表的主键称为此表的外键。

  • 触发器:一个通过事件来触发而被执行的特殊的存储过程,比如对一个表数据的操作会触发另一个表的数据操作。

  • 存储过程:是一个预编译的SQL语句,优点是允许模块化的设计,就是说只需创建一次,以后在该程序中就可以调用多次。

  • 视图:是一种虚拟的表,可以有选择性地展示部分数据,试图通常是有一个表或者多个表的行或列的子集。对视图的修改会影响基本表

  • 临时表: 只在当前连接可见的表,关闭连接后会自动清除表空间。

  • 内连接: 只连接匹配的行。

  • 左外连接: 包含左边表的全部行(不管右边的表中是否存在与它们匹配的行),以及右边表中全部匹配的行

  • 右外连接: 包含右边表的全部行(不管左边的表中是否存在与它们匹配的行),以及左边表中全部匹配的行

  • 全外连接: 包含左、右两个表的全部行,不管另外一边的表中是否存在与它们匹配的行。

  • 交叉连接: 生成笛卡尔积-它不使用任何匹配或者选取条件,而是直接将一个数据源中的每个行与另一个数据源的每个行都一一匹配。

数据库范式

所谓数据库范式就是一张数据表的表结构所符合的某种设计标准的级别。

第一范式

符合1NF的关系,可以把“关系”理解为数据表。此外还有一个“关系模式”的概念。“关系”和“关系模式”的区别,类似于类与对象的区别。”关系“是”关系模式“的一个实例,可以把”关系”理解为一张带数据的表,而“关系模式”是这张数据表的表结构。

1NF的定义为:符合1NF的关系中的每个属性都不可再分。

实际上,1NF是所有关系型数据库的最基本要求,在关系型数据库中一定会设计出1NF的数据库表,所有属性不可再分。

但是仅仅符合1NF的设计,仍然会存在数据冗余过大,插入异常,删除异常,修改异常的问题:

第二范式

**2NF在1NF的基础之上,消除了非主属性对于码的部分函数依赖,即每个非主属性完全依赖于主键。**即要求记录有惟一标识,实体的惟一性。

函数依赖:若在一张表中,在属性(或属性组)X的值确定的情况下,必定能确定属性Y的值,那么就可以说Y函数依赖于X,写作 X → Y。如 学号 → 姓名。但是反过来,因为可能出现同名的学生,所以有可能不同的两条学生记录,它们在姓名上的值相同,但对应的学号不同。

:设 K 为某表中的一个属性或属性组,若除 K 之外的所有属性都完全函数依赖于 K,那么称 K 为候选码,简称为。在实际中通常可以理解为:假如当 K 确定的情况下,该表除 K 之外的所有属性的值也就随之确定,那么 K 就是码。一张表中可以有超过一个码实际应用中为了方便,通常选择其中的一个码作为主码

主属性:包含在任意一个码中的属性称为主属性。

非主属性:不包含在任何一个码中的属性称为非主属性。

表:学号、姓名、课程号、学分、成绩;

这个表明显说明了两个事务:学生信息,课程信息;由于非主键字段必须依赖主键,这里学分依赖课程号,姓名依赖与学号,所以不符合二范式。因此需要将表拆分:

学生:Student(学号, 姓名);课程:Course(课程号, 学分);选课关系:StudentCourse(学号, 课程号, 成绩)。

第三范式

3NF在2NF的基础之上,消除了非主属性对于码的传递函数依赖,即任何非主属性不依赖于其他非主属性。要求任何字段不能由其他字段派生出来,它要求字段没有冗余,即不存在传递依赖。

表: 学号, 姓名, 年龄, 学院名称, 学院电话

因为存在依赖传递: (学号) → (学生)→(所在学院) → (学院电话) ,需要将表拆分:

学生:(学号, 姓名, 年龄, 所在学院);学院:(学院, 电话)。

数据库事务

事务

  • 事务:是用户定义的一个数据库操作序列,这些操作要么全做,要么全不做,是一个不可分割的工作单位。
  • 事物的 ACID 特性:原子性、一致性、隔离性、持续性。
  • 恢复的实现技术:建立冗余数据 -> 利用冗余数据实施数据库恢复。
  • 建立冗余数据常用技术:数据转储(动态海量转储、动态增量转储、静态海量转储、静态增量转储)、登记日志文件。

事务在逻辑上,将数据以结构化的形态呈现给用户。支持数据的增、删、改、查,并在过程中保障数据的正确且可靠。

ACID

  • 原子性(Atomicity): 事务要么全部完成,要么全部取消。 如果事务崩溃,状态回到事务之前(事务回滚)。
  • 一致性(Consistency): 只有合法的数据(依照关系约束和函数约束)才能写入数据库。一致性依赖于原子性与隔离性。
  • 隔离性(Isolation): 事务之间互不影响如果2个事务 T1 和 T2 同时运行,事务 T1 和 T2 最终的结果是相同的,不管 T1和T2谁先结束。
  • 持久性(Durability): 一旦事务提交,不管发生什么(崩溃或者出错),数据要保存在数据库中。

原子性、隔离性、一致性的根本问题,是不同的事务同时对同一份数据(A账户)进行写操作(修改、删除、新增),如果事务中都只是读数据的话,那么它们可以随意地同时进行,反正读到的数据都是一样的。

原子性

begin; -- 开始一个事务
update table set A = A - 1亿; 
update table set B = B + 1亿;
-- 其他读写操作
commit; -- 提交事务

要保证上面操作的原子性, 就得等begincommit之间的操作全部成功完成后,才将结果统一提交给数据库保存,如果途中任意一个操作失败,就撤销前面的操作,且操作不会提交数据库保存。

持久性

事务会保证数据不会丢,当数据库因不可抗拒的原因奔溃后重启,它会保证:

  • 成功提交的事务,数据会保存到磁盘
  • 未提交的事务,相应的数据会回滚

隔离性与隔离级别

隔离性要保证事务之间互不影响,这需要通过加锁来实现:

  • 先获得了锁,然后才能修改对应的数据A
  • 事务完成后释放锁,给下一个要修改数据A的事务
  • 同一时间,只能有一个事务持有数据A的互斥锁
  • 没有获取到锁的事务,需要等待锁释放

在事务中更新某条数据获得的互斥锁,只有在事务提交或失败之后才会释放,在此之前,其他事务是只能读,不能写这条数据的。这就是隔离性的关键,针对隔离性的强度,有以下四的级别:

  • 串行化(Serializable,SQLite默认模式):最高级别的隔离。两个同时发生的事务100%隔离,每个事务有自己的"世界",串行执行。
  • 可重复读(Repeatable read,MySQL默认模式):如果一个事务成功执行并且添加了新数据(事务提交),这些数据对其他正在执行的事务是可见的。但是如果事务成功修改了一条数据,修改结果对正在运行的事务不可见。所以,事务之间只是在新数据方面突破了隔离,对已存在的数据仍旧隔离。
  • 读取已提交(Read committed,Oracle、PostgreSQL、SQL Server默认模式):可重复读+新的隔离突破。如果事务A读取了数据D,然后数据D被事务B修改(或删除)并提交,事务A再次读取数据D时数据的变化(或删除)是可见的。这叫不可重复读(non-repeatable read)。
  • 读取未提交(Read uncommitted):最低级别的隔离,是读取已提交+新的隔离突破。如果事务A读取了数据D,然后数据D被事务B修改(但并未提交,事务B仍在运行中),事务A再次读取数据D时,数据修改是可见的。如果事务B回滚,那么事务A第二次读取的数据D是无意义的,因为那是事务B所做的从未发生的修改(已经回滚了)。这叫脏读(dirty read)。

假设有下面两个事务同时执行

begin; -- 事务1
insert into table1 (somevalue);
update table2 set aa = aa + 1 where id = 1;
commit;

begin; -- 事务2
select count(*) from table1; -- 第一次读count
select aa from table2 where id = 1; -- 第一次读aa
-- 假设在这个点 事务1成功提交
select count(*) from table1; -- 第二次读count
select aa from table2 where id = 1; -- 第二次读aa
commit;

事务2执行到一半时,事务1 成功提交,出现

  • 事务2中 第二次读count得到的值和第一次读count得到的值不一样(因为事务1新增了一条数据),这叫幻读,不隔离新增的数据。
  • 可重复读:事务2中 第一次读aa 和第二次读aa得到的值是一样的,对刚更新的值不可见,隔离已经存在的数据。 可以重复读,读到的数据都是一样的。
  • 读已提交:能读取到已提交的数据,事务2中第一次读aa 和第二次读aa得到的值是不一样的,对刚提交的值可见,不隔离已经存在的数据。 不可以重复读,读到的数据是不一样的(如果成功修改)。
  • 读未提交:能读取到未提交的数据,事务2中第一次读aa 和第二次读aa得到的值是不一样的(事务1未提交),对最新版本的值可见,不隔离已经存在的数据。 不可以重复读,读到的数据是不一样的。如果此时事务1因为其他原因回滚了,事务2第二次读到的数据是无意义的,因为修改没有发生(回滚了),这叫脏读

锁机制

在对数据进行读写操作时数据库会自动加锁,因此大多数情况下不用考虑数据库锁的问题。

共享锁

共享锁(读锁、S锁) :事务A对对象T加共享锁,其他事务也只能对T加共享锁,多个事务可以同时读,但不能有写操作,直到A释放共享锁。

特点:多个事务可封锁同一个共享页;任何事务都不能修改该页;该页被读取完毕,共享锁立即被释放。

排它锁

互斥锁(排它锁、写锁、X锁) :事务A对对象T加互斥锁以后,其他事务不能对T加任何锁(即其他事物进入阻塞状态),只有事务A可以读写对象T直到A释放互斥锁。

特点:仅允许一个事务封锁此页;其他任何事务必须等到互斥锁被释放才能对该页进行访问;互斥锁一直到事务结束才能被释放。

更新锁

更新锁(U锁) :更新锁用来预定要对此对象施加互斥锁,它允许其他事务读,但不允许再施加更新锁或互斥锁,当被读取的对象将要被更新时,则升级为互斥锁。

更新锁主要是用来防止死锁的,因为使用共享锁时,修改数据的操作分为两步,首先获得一个共享锁,读取数据,然后将共享锁升级为互斥锁,然后再执行修改操作。这样如果同时有两个或多个事务同时对一个对象申请了共享锁,在修改数据的时候,这些事务都要将共享锁升级为互斥锁。这些事务都不会释放共享锁而是一直等待对方释放,这样就造成了死锁。如果一个数据在修改前直接申请为更新锁,在数据修改的时候再升级为互斥锁,就可以避免死锁。

特点:用来预定要对此页施加互斥锁,它允许其他事务读,但不允许再施加更新锁或互斥锁;当被读取的页要被更新时,则升级为互斥锁。更新锁一直到事务结束时才能被释放。

锁的粒度

数据库能够确定那些行需要锁的情况下使用行锁,如果不知道会影响哪些行的时候就会使用表锁。

假设有一个用户表user,有主键id和用户生日birthday。当使用update … where id=?这样的语句时,数据库明确知道会影响哪一行,它就会使用行锁;当你使用update … where birthday=?这样的的语句时,因为事先不知道会影响哪些行就可能会使用表锁。

  • 行锁:行锁的作用范围是行级别。
  • 表锁:表锁的作用范围是整张表。