数据库导论 关系数据库实现

3. 数据库实现

本节讨论 SQL 语句和部分高级功能, 并介绍 数据库事务 的概念和其处理方式.

3.1 SQL 基础

3.1.1 选择运算: SELECTION

SELECT 语句用于从一个关系中选出 满足选择条件 的元组的一个子集, 我们可以将其视为一个 过滤器. 如果将一个关系视为一张表, 则 SELECT 操作相当于从这张表中按照条件选出一些行, 而丢弃另外一些行.

SELECT column1, column2, ...,  FROM table_name; 

以及

SELECT * FROM table_name;

我们可使用 SELECT 语句查询一个或多个属性, 并且可在语句中对属性进行数值运算:

SELECT u_name, u_score, u_score+10, u_score-10, u_score*2, u_score/2 
FROM T_USER;

此外, 可使用 SELECT-WHERE 执行 条件查询, 在 WHERE 子句中使用下列的运算符:

数据库导论 关系数据库实现_第1张图片

以及使用 与条件 (AND, BETWEEN) :

(查询T_USER 表中成绩大于等于 60 60 60 分且小于等于 80 80 80 分的学生)

SELECT u_name, u_score
FROM T_USER 
WHERE u_score >60 AND u_score <80;

SELECT u_name, u_score
FROM T_USER 
WHERE u_score BETWEEN 60 AND 80; 

使用 或条件 (OR, IN):

# 查询分数大于90分或者小于60分的记录 (用 OR)

SELECT u_name, u_score
FROM T_USER 
WHERE u_score >90 OR u_score <60;

# 查询成绩为100, 98, 65的学生 (用 IN)
SELECT u_name, u_score
FROM T_USER 
WHERE u_score IN (100,98,65);

使用 非条件 (!=, NOT):

# != 写法
SELECT *
FROM T_USER 
WHERE u_name != `Axton Yao`;


# NOT 写法
SELECT *
FROM T_USER 
WHERE u_name NOT LIKE `Ryan Xin`;

如果需要的话还可以为列名称和表名称指定别名:

SELECT `ID` AS `StudentID`, `Name` AS `StudentName` FROM `Students`;

在一些情况下, 我们可能希望从表中选取 不重复 的数据. 我们可以使用 DISTINCT 关键词:

SELECT DISTINCT Company FROM Orders;
3.1.2 投影运算: PROJECTION

SELECT 不同, 投影操作可以视为从表中 选出某些列丢弃某些列.

我们可以使用 SELECT 语句从表中选出特定列作为表的 投影:

数据库导论 关系数据库实现_第2张图片

3.1.3 修改运算: ALTER/UPDATE

我们可以使用 ALTER 语句在 已有的表中 增/删表中属性, 修改属性的域, 修改属性名, 修改表名, 增/删主键:

  1. 在表中添加/删除列:

    # 添加
    ALTER TABLE table_name 
    ADD column_name datatype;
    
    # 删除
    ALTER TABLE table_name 
    DROP COLUMN column_name;
    
  2. 修改属性域 (列的数据类型):

    ALTER TABLE table_name
    ALTER COLUMN column_name datatype;
    
  3. 修改属性名 (列名):

    ALTER TABLE table_name
    CHANGE COLUMN columm_name new_name
    
  4. 修改表名:

    ALTER TABLE table_name
    RENAME TO new_name
    
  5. 增/删主键:

    # 删主键
    ALTER TABLE table_name
    DROP primary key
    
    # 增主键
    ALTER TABLE table_name
    ADD CONSTRAINT pk_name PRIMARY KEY column_name
    

UPDATE 语句用于修改表中的数据:

UPDATE 表名称 SET 列名称=新值 WHERE 列名称=某值;
3.1.4 联合查询: UNION

我们可以使用 UNION 子句求两个不同表中两列的重复元素, UNION 的作用是将 两个 SELECT 语句的查询结果 作为一个 整体 展示出来.

使用 UNION 必须确保各个 SELECT 集合的结果的 列数必须相同, 且每个对应列的 数据类型必须相同, 但 列名可以不同.

数据库导论 关系数据库实现_第3张图片

需要注意的是, UNION 执行的是对两个结果集的 并集 操作, 会筛选掉重复记录.

3.1.5 笛卡尔积运算: CARTESIAN PRODUCT

笛卡尔积可以视为对关系的 乘法, 它的效果是将 分别来自两个数据集合中的行所有可能的方式 进行组合.

我们可以使用下列两种方式执行笛卡尔积:

SELECT * FROM table_1 CROSS JOIN table_2;

# 本质是第一种写法的语法糖
SELECT * FROM table_1, table_2;

数据库导论 关系数据库实现_第4张图片

数据库导论 关系数据库实现_第5张图片

3.1.6 连接运算: JOIN

连接 运算可以视为 在水平方向上 对两个表的 合并, 其基本方法是: 将两个表中在共同数据项上相匹配的那些行合并.

连接运算本质是由 一个笛卡尔积运算一个选取运算 所构成的:

  1. 首先用一个笛卡尔积完成对两个数据集合的乘法, 生成的结果集合包含两个数据集合中所有行的, 所有可能的组合.

  2. 然后对生成的结果集合进行 选取运算 , 确保只把 分别来自两个数据集合 并且 具有重叠部分 的行所 合并在一起.

SQL 提供的多种连接方式之间的区别在于: 它们在连接运算流程的第二步中相互交叠的不同数据集合 中选取 用于连接的行 时所采用的方法不同:

连接类型 定义
内连接 INNER JOIN 包含 Theta 连接, EQUI 连接自然连接, 只连接满足匹配条件的行, 展现出的是两表 共同 的数据.
左外连接 LEFT JOIN 包含左边表中的全部行 (无论是否和右表匹配), 以及右边表中所有与左边表匹配的行.
右外连接 RIGHT JOIN 包含右边表中的全部行 (无论是否和左表匹配), 以及左边表中所有与右边表匹配的行.
全外连接 FULL JOIN 包含左右表的全部行, 不考虑匹配与否, 注意与交叉连接区分.
交叉连接 CROSS JOIN 生成笛卡尔积: 考虑数据集中任意行与另一数据集中任意行的两两组合.

下面详细说明各种连接运算:

  1. 内连接:

    内连接展现的是两表 共同 的数据.

    内连接又因其匹配条件的不同而被区分为Theta 连接, EQUI 连接自然连接.

    1. Theta 连接 是使用 等值以外的条件 (如大于等于, 小于等于, 不等于之类) 来匹配左右两表中行的内连接.

    2. EQUI 连接 是使用 等值 (==) 条件 来匹配左右两表中行的内连接.

    3. 自然连接SQL 中一种预定义的内连接, 可以视为语法糖, 它在匹配左右两表好似 自动判断 两表中相同名称的列, 而后形成匹配.

      我们无法在自然连接中人为指定哪些列被用于匹配, 但用于指定查询结果包含哪些列.

    # Theta连接的例子
    SELECT t1.prop, t2.char FROM table_1 t1 
    INNER JOIN table_2 t2 ON t1.axton != t2.axton;
    
    # EQUI连接的例子
    SELECT t1.prop, t2.char FROM table_1 t1 
    inner JOIN table_2 t2 ON t1.axton=t2.axton;
    
    # 注: 上面的内连接相当于:
    SELECT t1.prop, t2.char FROM table_1 t1, table_2 t2 
    WHERE t1.axton=t2.axton;
    
    # 自然连接的例子
    SELECT t1.prop, t2.char FROM table_1 t1 
    NATURAL JOIN table_2 t2;
    
  2. 外连接:

    外连接不但返回符合连接和查询条件的数据行, 还 返回不符合条件的一些其他行. 外连接分三类: 左连接 LEFT JOIN, 右连接 RIGHT JOIN 和完整外部连接 FULL JOIN:

    1. 左连接除了返回两表中符合连接条件的数据行外, 还返回 左表中不符合连接条件但符合查询条件的数据行.

      如果左表的某行在右表中没有匹配行, 则将右表对应的行处返回 空值 并与其连接.

    2. 右连接除了返回两表中符合连接条件的数据行外, 还返回 右表中不符合连接条件但符合查询条件的数据行.

      如果右表的某行在左表中没有匹配行, 则将左表对应的行处返回 空值 并与其连接.

    3. 完整外部链接返回左表和右表中的所有行. 若某行在另一个表中没有匹配行, 则另一个表的对应列包含 空值, 否则这一整行包含对应的数据值.

    # 左连接
    # 以左表为主表, 右表没数据则为null
    SELECT m.Province,S.Name FROM member m 
    LEFT JOIN ShippingArea s ON m.Province=s.ShippingAreaID;
    
    # 右连接
    # 以右表为主表, 左表没数据则为null
    SELECT m.Province,S.Name FROM member m 
    RIGHT JOIN ShippingArea s ON m.Province=s.ShippingAreaID;
    
    # 全外连接
    SELECT m.Province,S.Name FROM member m 
    FULL JOIN ShippingArea s ON m.Province=s.ShippingAreaID;
    

3.2 SQL 进阶

3.2.1 空值: NULL

SQL 中, 任何 未指定 的数据值称为 NULL, 它实际上应当被视为 一种状态 而非属性的值:

数据库导论 关系数据库实现_第6张图片

属性在下列的情况下可能被视为 NULL:

  1. 数据值 未知
  2. 数据值 不可取得
  3. 数据值 被隐藏
  4. 数据值 不适用 (坦白说我也没搞懂不适用是什么意思)

需要注意的是, NULL是独立于 True, False 之外的. 执行逻辑运算时需要遵循下列的 三值逻辑 规则:

数据库导论 关系数据库实现_第7张图片

注意, 我们不可使用 = 检测某个参数是否为 NULL. 要进行这样的检测, 需要使用关键字 IS:

# 返回的查询结果为空
SELECT name FROM VIPs 
WHERE flat = NULL;

# 返回的查询结果是正常的
SELECT name FROM VIPs 
WHERE flat IS NULL;
3.2.2 排序, 取极值, 求和与计数

我们可以使用 ORDER BY 语句对查询的结果集进行 排序:

# ORDER BY 默认按照升序对记录进行排序
SELECT col_1, col_2 FROM table ORDER BY attribute;

# 如果需要按照降序排序记录, 需要使用 DESC 关键字
SELECT col_1, col_2 FROM table ORDER BY attribute DESC;

注意: ORDER BY 子句可 同时使用多个列 作为排序条件, 但是必须确保 作为排序条件的每个列都在表中存在.

我们可以使用 MAX()MIN() 取表中数据列的 极值:

# 取极大值
SELECT MAX(column_name) FROM table_name;

# 取极小值
SELECT MIN(column_name) FROM table_name;

我们使用 SUM() 函数返回 数值列的总数:

SELECT SUM(column_name) FROM table_name;

我们使用 COUNT() 函数返回 查询结果某一列项的数目:

# 若考虑重复结果:
SELECT COUNT(column_name) FROM table;

# 若要求不重复结果的数目:
SELECT COUNT(DISTINCT column_name) FROM table;
3.2.3 聚合函数 GROUP BY

聚合函数/集合函数 (COUNT, MAX, MIN, SUM, AVG) 往往需要搭配 GROUP BY 语句. 它用于 结合合计函数, 根据 一个或多个列 对结果集进行分组:

SELECT column_name, aggregate_function(column_name)
FROM table_name
WHERE column_name operator value
GROUP BY column_name;

对聚合函数的理解如下: 考虑表

数据库导论 关系数据库实现_第8张图片

执行 SQL 语句

SELECT `name` FROM 'test'
GROUP BY `name`

则其查询结果的生成过程可视为:

  1. 首先 “执行” 语句中的 FROM 'test' 得到一张和原表一模一样的新表.

  2. 然后在这张表上执行 GROUP BY 语句进行聚合分组, 在本例中需要基于 name 进行分组, 显然分组依据是将所有在 name 属性相同的行全部 “合并”, 此时执行结果是这样的一个虚拟的表:

    数据库导论 关系数据库实现_第9张图片

  3. 接下来才需要对这个虚拟表执行 SELECT 后面的语句 (在此处是 name).

    在这个例子中, 由于 name 列被用于 GROUP BY, 因此该列必然不存在重复值, 所以简单执行 SELECT name 就不会出错.

    但如果我们需要选择那些在虚拟表中有多个数据的单元格, 如 id, number, 就不能直接选择它们, 而需要使用 以多个数据作为输入, 输出单个数据的 聚合函数, 将这些多数据的单元格作为输入.

    假如我们执行了

     SELECT name, SUM(number) FROM test
     GROUP BY name;
    

    则结果就是在 虚拟表3 的基础上, 对 number 列的每个单元格都执行 SUM() 聚合函数, 返回的表就是

    数据库导论 关系数据库实现_第10张图片

注: 对于 GROUP BY 多个字段的情况, 我们只需要将作为聚合分组依据的多个字段 视为整体字段 即可.

需要注意的是, 用于对表进行筛选的关键字 WHERE 无法和聚合函数 GROUP BY 一起使用. 我们需要使用 HAVING 子句:

SELECT column_name, aggregate_function(column_name)
FROM table_name
WHERE column_name operator value
GROUP BY column_name
HAVING aggregate_function(column_name) operator value;

HAVING 子句的作用是 筛选满足条件的组, 在 分组之后 过滤数据, 而 WHERE 搜索条件在 执行分组操作前 应用.

注:

在语句中 同时含有 WHERE, GROUP BY, HAVING子句和聚合函数 时, SQL 语句的执行顺序如下:

  1. 先执行 WHERE 子句查找符合条件的数据.
  2. 执行 GROUP BY 子句对数据进行分组, 并对 GROUP BY 子句执行形成的组运行聚集函数从而计算对应的值.
  3. 最后执行 HAVING 子句: 基于第二步中聚集函数的运算结果, 过滤掉不符合条件的组.

HAVING 子句和 WHERE 子句都可以让查询结果满足一定的条件限制, 但 WHERE 子句中限定的是 , HAVING 子句中限定的是 , 由此 WHERE 子句中 不能使用聚集函数.

3.2.4 嵌套查询语句

嵌套查询 指某个查询语句可以 嵌套 在另外一个查询语句的 WHERE 子句 中. 我们称外层的查询为 主查询, 而内层的查询为 从查询.

在执行含嵌套查询的语句时, 其语句的方向是 由内向外 的: 首先进行内层查询, 而外层查询 利用内层查询的结果集 作为条件进行查询.

如果子查询返回的结果是 单个值, 则它可以在任何涉及到表达式的地方作为常量使用.

若某个表只出现在子查询中而不在外查询中, 则该表的列 无法包含在输出中 (要想提取出子查询的结果集中的数据, 就只能依靠外层查询中的相关语句).

嵌套查询主要分为下列三类:

  1. 简单子查询
    若子查询的结果是 单个值, 则这样的查询为 简单子查询.

  2. IN 嵌套查询
    若查询的 WHERE 语句内包含 IN 关键字 (用于判断查询的表达式是否在多个值的列表中), 则该查询为 IN 嵌套查询:

  3. SOME 嵌套查询
    若查询的 WHERE 语句内包含 SOME 关键字 (用于判断查询的表达式是否满足 SOME 后的某一个条件), 则该查询为 SOME 嵌套查询:

  4. ALL 嵌套查询
    若查询的 WHERE 语句内包含 ALL 关键字 (用于判断查询的表达式是否满足 ALL 后的全部条件), 则该查询为 ALL 嵌套查询:

  5. EXISTS 嵌套查询
    若查询的 WHERE 语句内包含 EXISTS 关键字 (用于判断子查询是否返回了结果集), 则该查询为 EXISTS 嵌套查询:

    # 如果不存在Person_Id的记录, 则子查询没有结果集返回, 主语句不执行
    SELECT * FROM Person WHERE exists (
        SELECT * FROM Person 
        WHERE Person_Id = 114514
    );
    
# IN 嵌套查询
SELECT NAME FROM person 
WHERE countryid IN (
    SELECT countryid FROM country
    WHERE countryname = '下北沢'
);

# SOME 嵌套查询
SELECT NAME FROM person 
WHERE countryid = SOME (
    SELECT countryid FROM country
    WHERE countryname = '下北沢'
);

# ALL 嵌套查询
SELECT NAME FROM person 
WHERE countryid > ALL (
    SELECT countryid FROM country
    WHERE countryname = '下北沢'
);
3.2.5 视图

SQL 中, 视图 是基于 查询结果 的, 可视化的表. 它包含行和列, 且字段实际上来自于被查询的一个或多个数据库中真实存在的表中.

我们可以根据需要向视图中添加 SQL 函数和语句, 视图中的函数和语句不会影响被查询数据库的设计和结构.

# 创建 View 语法
CREATE VIEW view_name AS
SELECT column_name(s)
FROM table_name
WHERE condition

# 在创建后我们可以像查表一样查询这个View中的数据
SELECT * FROM [view_name]

# 视图可以被更新
CREATE OR REPLACE VIEW view_name AS
SELECT column_name(s)
FROM table_name
WHERE condition

# 也可以被撤销
SQL DROP VIEW Syntax
DROP VIEW view_name
3.2.6 存储过程

存储过程 本质是一段 预先编译好, 可供复用 的声明式 SQL 语句.

SQL 语句需要 先编译然后执行, 而存储过程(Stored Procedure) 是一组 经编译后 存储在数据库中, 可完成特定功能的 SQL 语句集, 可通过指定存储过程的名字并给定参数 (若它含有参数) 来调用执行.

存储过程的实质是 函数, 由 SQL语句控制结构 组成. 数据库中的存储过程可以看做是对编程中面向对象方法的模拟, 它允许我们控制数据的访问方式.

存储过程的优点包括:

  1. 增强 SQL 语言的功能和灵活性.
  2. 可增强 可复用性.
  3. 由于已经预先编译, 因此存储过程具有 较快的执行速度.
  4. 针对数据库对象的操作可以被简化为 对某个存储过程的调用语句, 从而在传输 SQL 指令时 减少对网络带宽的消耗.
  5. 可以通过 限制执行某个存储过程的权限 实现对相应数据 访问权限的控制, 提升 数据安全性.

存储过程的创建语法形如:

DELIMITER //

  CREATE PROCEDURE myproc(
        IN p varchar,
        OUT q int,
        INOUT r boolean)
    BEGIN
      SELECT COUNT(*) INTO q FROM table;
    END //

DELIMITER ;

其删除语法为:

DROP PROCEDURE myproc;

需要注意: MySQL 默认以 “;” 为分隔符, 若未声明分割符, 则编译器会把存储过程视为 SQL 语句, 因此编译过程会报错.

因此, 要事先用 “DELIMITER //” 声明作为 前段分隔符, 让编译器把两个 “//” 之间的内容视为 存储过程的代码, 从而避免执行它们. 最后一行 “DELIMITER ;” 的作用是把 分隔符还原.

根据需要, 存储过程可能会有 输入 (IN), 输出 (OUT)既作为输入又作为输出 (INOUT) 的参数:

  • IN 参数的值必须在调用存储过程时指定, 在存储过程中修改该参数的值不能被返回.
  • OUT 参数的值可在存储过程内部被改变, 并可返回.
  • INOUT 参数的值在调用时指定, 并且可被改变和返回.

我们还可在存储过程中 定义并赋值局部变量:

数据库导论 关系数据库实现_第11张图片

DELIMITER //

CREATE PROCEDURE GetTotalOrder() 
    BEGIN
        # 声明变量时需要同时指定其名称, 数据类型, 默认值
        DECLARE totalOrder INT DEFAULT 0; 

        SELECT COUNT(*)
        INTO totalOrder -- 可使用 INTO 将查询的结果赋值给变量
        FROM orders;
    
        SELECT totalOrder; 
    END //

DELIMITER;

CALL GetTotalOrder();   -- 调用所定义的存储过程

需要注意: 存储过程在数据库中是经过 预编译 后被存储在 information_schema 中的 routines 中的. 我们可以使用下列命令查询数据库中的所有 Stored Procedure:

在本课程中, Slides 里出现了在 Stored Procedure 中定义和使用 局部变量 以及 直接使用用户变量 的例子. 此处简单说明 局部变量用户变量:

  1. 用户变量可以 直接使用, 形如 @var. 其有效生命周期在 单次数据库连接 中, 断开连接时变量消失.
  2. 局部变量需要 先定义, 后使用, 定义时必须 声明变量名和数据类型, 其有效生命周期在函数或存储过程的作用域中.
SELECT `routine_name` FROM `information_schema`.`routines` 
WHERE `routine.type`=`PROCEDURE` AND `routine_schema`=`classicmodels`;

此外, 我们可以在存储过程中应用 条件函数体, 循环函数体 等控制语句:

数据库导论 关系数据库实现_第12张图片

# 使用 IF-THEN 条件语句的存储过程样例
CREATE PROCEDURE GetCustomerLevel( 
    IN pCustomerNumber INT,
    OUT pCustomerLevel VARCHAR(20)
)

BEGIN
    DECLARE credit DECIMAL(10,2) DEFAULT 0;
    SELECT creditLimit INTO credit FROM customers 
    WHERE customerNumber = pCustomerNumber;
    IF credit > 50000 THEN
        SET pCustomerLevel = 'PLATINUM';
    END IF; 
END


# 使用 CASE 条件语句的存储过程片段样例, 注意对默认情形 “ELSE” 的声明
CASE customerCountry 
    WHEN 'USA' THEN
        SET pShipping = '2-day Shipping'; 
    WHEN 'Canada' THEN
        SET pShipping = '3-day Shipping'; 
    ELSE
        SET pShipping = '5-day Shipping'; 
END CASE;

我们可以在存储过程中定义 游标 (Cursor), 它是使我们能够在存储过程中对查询得到的表中的数据 逐行处理 的工具.

游标的一般使用方法是: 声明游标 - 打开游标 - 取出结果 - 关闭游标.

我们一般用 FOR 循环操作游标.

注意; 游标具备下列的性质:

  1. 只读: 在 MySQL 中我们只能使用游标 遍历访问表的每一行, 而不能对这一行的数据进行修改.
  2. 不可滚动: 游标只能 逐行地 遍历表, 不可跳过任意一行.
  3. 数据敏感: 游标所访问的是数据表本身而非表的某个 Buffer, 若游标指向的表中的某个数据被修改, 则该修改会立刻体现在游标提出的数据上.

(实在是写不下去了, 游标剩下的部分不写了, 开摆!)

3.2.7 触发器

触发器的实质是一段预先存储的 SQL 程序, 在监听到发生对应的数据库操作 (如插入, 表更新和表删除) 时会被唤起调用 (Invoked), 一般在 SQL 查询执行前或执行后 运行.

同时, 触发器也可被编程于执行 计划任务 (Scheduled Tasks).

由于其先天具备 Watchdog 的特性, 触发器常被用于检验和审核数据库中执行的操作, 维护数据库的完整性, 避免异常发生.

触发器的创建语法如下:

CREATE TRIGGER 
    # 标识触发器名称, 用户自行指定
    trigger_name

    标识触发时机, 取值为 BEFORE 或 AFTER;    
    trigger_time

    # 标识触发事件, 取值为 INSERT, UPDATE 或 DELETE;    
    trigger_event 

ON tbl_name -- 标识建立触发器的表名, 即在哪张表上建立触发器.

FOR EACH ROW
BEGIN
    trigger_stmt    -- 触发器程序体, 可以是一句SQL语句, 或者由 BEGIN 和 END 包络的语句段.
END

触发器的信息查询和删除如下:

# 查询触发器信息
SHOW TRIGGERS;

# 删除某个触发器
DROP TRIGGER trigger_name;

需要注意:

  1. 和存储过程类似, 为了避免 SQL 将在 BEGINEND 间, 每一行由分号 ; 结束的语句段错误解析导致触发器被视为不完整的 SQL 语句, 需要使用分隔符 DELIMITER 标记整个 Trigger 语句段.

  2. 触发器一般被分为三类:

    • INSERT 型触发器

      插入某一行时激活触发器, 可能通过 INSERT, LOAD DATA, REPLACE 语句触发.

    • UPDATE 型触发器

      更改某一行时激活触发器, 可能通过 UPDATE 语句触发.

    • DELETE 型触发器

      删除某一行时激活触发器, 可能通过 DELETE, REPLACE 语句触发.

  3. INSERT 型触发器中, NEW 用来表示将要(BEFORE) 或已经 (AFTER) 插入的新数据.

    UPDATE 型触发器中, OLD 用来表示将要或已经被修改的原数据, NEW 用来表示将要或已经修改为的新数据.

    DELETE 型触发器中, OLD 用来表示将要或已经被删除的原数据.

  4. 我们可以使用 FOLLOWSPRECEDES 实现触发器的 菊链式调用:

    FOLLOWS 允许新触发器在旧的触发器 执行完成后 被激活.

    PRECEDES 允许新触发器在旧的触发器 执行之前 被激活.

  5. 我们也可以对触发器进行编程, 令其在被激活时调用某个 存储过程.

    CREATE TRIGGER before_accounts_update
    BEFORE UPDATE
    ON accounts FOR EACH ROW
        BEGIN
            CALL CheckWithdrawal ( 
                OLD.accountId, 
                OLD.amount - NEW.amount 
            );
        END
    

3.3 数据库事务的概念和处理

数据库事务是 构成某个单一逻辑单元一系列操作 组成的 集合. 一个典型的数据库事务形如:

BEGIN TRANSACTION   # 某个事务的开始
    sql_clause_1;
    sql_clause_2;
    ...
    sql_clause_n;
COMMIT/ROLLBACK     # 事务执行完毕, 提交对数据库进行的修改或回滚至原来的状态

需要注意:

  1. 数据库事务中包括的一条或多条数据库操作构成了一个 逻辑上的整体.
  2. 由于它们在逻辑上被组织成一个整体, 这些数据库操作要么 全部成功, 要么 全部不执行.
  3. 构成数据库事务的所有操作要么 全部对数据库产生影响, 要么 全都不产生影响.
  4. 上述性质恒成立, 无论数据库是否出现故障或是否存在并发事务.

事务的执行过程可以被简化为:

数据库导论 关系数据库实现_第13张图片

  1. 系统为每个事务开辟一个 私有的缓冲区 .
  2. 事务读操作 将数据从主存中复制到缓冲区内.
  3. 事务写操作 修改缓冲区内的数据.
  4. 若执行 COMMIT (提交), 则缓冲区内的数据 覆盖主存中的对应原版 .
  5. 若执行 ROLLBACK (回滚), 则 缓冲区内的数据被丢弃.

为了确保关键操作的必然可靠性, 数据库事务的存在是必要的. 考虑下列的例子:

AxtonDerek 转账 114514 114514 114514 元, 这个操作在形式上显然是一个单一的操作, 但在银行的数据库系统中会被至少 拆分为两个步骤:

  1. Axton 的账户余额减少 114514 114514 114514 元.
  2. Derek 的账户余额增加 114514 114514 114514 元.

而显然在这两个单独的过程中可能会出现下列的问题:

  1. 第一步执行成功, 但第二步执行失败或系统在执行第二步之前突然崩溃, 由此导致 Derek 并没有成功收款.
  2. 系统在第二步刚刚执行成功时崩溃, 而重启时崩溃前的转账记录丢失.
  3. AxtonDerek 转账的同时, 还有多人也在向 Derek 转账, 由此导致 Derek 的账户余额出现异常.

其中, 我们一般称第二类 导致对数据库执行的修改丢失 的问题为 暂更新问题 (Temporary Update Problem):

数据库导论 关系数据库实现_第14张图片

而称第三类 导致对数据的更新异常的 问题为 丢更新问题 (Lost Update Problem):

数据库导论 关系数据库实现_第15张图片

而若将上述的转账流程涉及的所有操作包含在同一个数据库事务中, 则会:

  1. 若数据库操作失败或系统崩溃, 系统能够 以事务为单位进行数据恢复 , 从而避免了第一, 二类问题;
  2. 若有多个用户同时操作数据库, 数据库能够 以事务为单位进行并发控制, 从而将多个转账操作进行隔离, 避免了第三类问题.

为了实现对上述问题的预防和避免, 数据库事务需要具备所谓的 ACID 特性:

  1. A: Atomicity 原子性:

    事务中的所有操作要作为一个整体, 如同原子一般不可再分: 要么全部执行成功, 要么全部执行失败.

  2. C: Consistency 一致性:

    事务的执行结果必须使数据库 从某个一致性状态 转化到 另一个一致性状态. 而一致性状态指:

    • 满足 数据库的完整性约束
    • 正常反映数据库所描述的现实世界的真实状态, 如转账前后两个账户的金额总和需保持不变.

  3. I: Isolation 隔离性:

    并发执行的事务被彼此隔离, 不会相互影响: 它们对数据库的影响和它们一个接一个地串行执行时相同.

  4. D: Durability 持久性:

    事务一旦提交, 它对数据库的更新就是 持久的, 任何故障和意外事件都不会导致数据丢失.

事务的 ACID 特性中, C, 数据库一致性, 是 事务的根本追求. 一般地, 对一致性的破坏主要来源于事务的并发执行或事务/系统故障. 在本课程中, 我们只关心防止并发执行破坏数据库一致性的基本原理和方法:

在并发执行数据库事务时, 必须通过 调度 (Scheduling) 解除不同并发执行的事务之间可能造成的冲突, 否则就会出现各种异常, 破坏事务间的隔离性, 影响数据库的一致性.

一般来说, 异常被分为下列四种:

  1. Lost Update 丢更新异常
  2. Temporary Update 暂更新异常
  3. Dirty Write 脏写异常

在本课程中, 我们介绍了前两种异常:

Lost Update 丢更新异常的成因是: 在某个调度中, 事务 T 1 T_1 T1, T 2 T_2 T2 读入相同的数据值 X X X, 进行不同的计算, 并先后修改并提交对 X X X 的改动. 则此时后提交的事务会将先提交的事务对 X X X 进行的修改覆盖掉.

Temporary Update 暂更新异常的成因是: 在某个调度中两个数据库事务共享一片 Database Buffer, 对同一个数据进行读写操作.

若第一个事务 T 1 T_1 T1 对 数据 X X X 的修改 被第二个事务 T 2 T_2 T2 读取, 进一步修改并提交, 而 T 1 T_1 T1 所做更改在 T 2 T_2 T2 读取了 X X X 后回滚, 则会出现 “ T 2 T_2 T2 读取了本不该出现的, 被 T 1 T_1 T1 修改过的数据” 的情况.

定义 (调度, Schedule)

调度 定义为对数据库不同事务中语句的 交错穿插安排.

下面考虑 包含 n n n 个事务 T 1 , ⋯   , T n T_1, \cdots, T_n T1,,Tn 的调度 S S S.

S S S 可以进一步地表示为调度指令执行的简写序列, 其中:

  1. b: begin, 开始一系列事务的调度
  2. r: read, 读操作.
  3. w: write, 写操作.
  4. e: end, 表示事务调度的终止.
  5. c: commit, 表示提交对缓存中数据的修改到主存.
  6. a: abort, 终止调度序列.

注意: r, w 操作涉及对数据库项的操作, 在实际表示时常常使用下标表明该操作是属于哪个事务的.

举例, 调度序列

S a : r 1 ( X ) ;   w 1 ( X ) ;   r 2 ( X ) ;   w 2 ( X ) ;   r 1 ( Y ) ;   a 1 ; S_a: r_1(X);~ w_1(X);~r_2(X);~w_2(X);~r_1(Y);~a_1; Sa:r1(X); w1(X); r2(X); w2(X); r1(Y); a1;

表示如下表所示的调度:

数据库导论 关系数据库实现_第16张图片

定义 (冲突, Conflict)

称调度序列中的某两个操作是 冲突(Conflict) 的, 若它们满足下列特征:

  1. 这两个操作分属 不同的事务.
  2. 这两个操作访问了 同样的数据.
  3. 这两个操作中至少有一个是 写操作.

由于调度中的冲突操作会导致 数据不一致, 我们需要构造 完备调度:

定义 (完备调度, Complete Schedule)

称满足下列条件的调度为 完备调度:

  1. 调度 S S S 中的指令恰好是 n n n 个事务 T 1 , ⋯   , T n T_1, \cdots, T_n T1,,Tn 中的.
  2. T 1 , ⋯   , T n T_1, \cdots, T_n T1,,Tn 里任何事务中的任意一对指令的 相对顺序 S S S 中均 保持.
  3. 调度中任意两个冲突操作都 不能同时被执行.

构造完备调度最简单的方法是不考虑穿插, 让所有事务 串行执行:

数据库导论 关系数据库实现_第17张图片

但为了避免过大的性能损失, 在实际情况下我们必须穿插事务指令, 构造并行调度 (Allow Interleaving). 由此, 我们需要明确: 如何确定某个并行调度不会导致异常:

定义 (可序列化调度, Serializable Schedule)

称某个包含 n n n 个事务的调度 S S S可序列化的, 若它 等价于 (一般认为是冲突等价) 某个包含同样 n n n 个事务的串行调度.

为了确保我们将要构造的并行调度是可序列化的, 我们还要引入 冲突等价 的概念: (显然, 如果只考虑输出值等价的话很容易出现问题)

定义 (冲突等价, Conflict Equivalence)

称两个不同顺序, 包含相同的 n n n 个事务的调度是 冲突等价 的, 若对任意一对冲突操作, 这对冲突操作在两个不同调度中的 相对顺序 相同, 反之则称其为 冲突不等价 (Not Conflicting Equivalence) 的.

因此, 我们现在可以给出 真正可用的
可序列化调度 的定义: 若某个调度 S S S 和某个串行调度 冲突等价, 则称 S S S可序列化 的.

本课程中教授了如下的, 快速判断调度是否为可序列化的:

使用有向图判断: 只考虑读写操作, 依次检查调度中涉及的每个事务 T i T_i Ti.

  1. 将每个事务用一个节点表示,

  2. 对调度中涉及的每一对冲突操作 o 1 , o 2 o_1, o_2 o1,o2, 画一条从 更早发生的操作所属事务节点更晚发生的操作所属的事务节点有向边.

最后检查构造的图. 若图是 无圈的, 则该调度为 可序列化的.

数据库导论 关系数据库实现_第18张图片

REFERENCE:

SQL中SELECT语句详解

SQL WHERE 子句

SQL总结(一)基本查询

SQL ALTER TABLE 语句

SQL的alter用法总结

sql中union和union all的用法

SQL查询cross JOIN的用法(笛卡尔积)

left join 和 left outer join 的区别

SQL的四种连接-左外连接, 右外连接, 内连接, 全连接

sql 内连接, 外连接, 自然连接等各种连接

Understanding cartesian product in SQL

SQL VIEW(视图)

SQL中MAX()和MIN()函数的使用

SQL COUNT DISTINCT 函数

可以这样去理解group by和聚合函数

超实用的SQL语句之嵌套查询

SQL Having的用法

Advantages and Disadvantages of Using Stored Procedures – SQL

MySQL存储过程:存储过程简介

MySQL数据库:触发器Trigger

MYSQL之触发器(Trigger)操作

数据库事务的概念及其实现原理

数据库】序列化

Wikipedia: Serializability

MySQL中全局变量、会话变量、用户变量和局部变量的区别

你可能感兴趣的:(Database,数据库,sql,database)