Bootstrap

【数据库】数据库管理(上)事务 视图 索引分类以及生效规则

一、事务处理

数据库事务是数据库管理系统执行过程中的一个逻辑工作单元,它包含了一组数据库操作,这些操作要么全部成功执行,要么全部不执行。事务的主要目的是确保数据的一致性和完整性。

1-1 事务特征

事务具有四个重要的特性,通常称为 ACID 特性:

1、原子性(Atomicity)

事务是一个不可分割的最小工作单位。

如果事务在执行过程中发生错误,系统会回滚到事务开始前的状态,就像事务从未发生过一样。

2、一致性(Consistency)

事务必须使数据库从一个一致状态转换到另一个一致状态,也就是说一个事务执行之前和执行之后都必须处于一致性状态(数据一致)。

事务执行前后,数据库中的数据必须满足所有的完整性约束。例如,如果一个事务将资金从一个账户转移到另一个账户,那么在事务完成后,两个账户的总金额应该与事务开始时相同。

3、隔离性(Isolation)

多个事务并发执行时,每个事务都应该是独立的,不应该受到其他事务的影响。(确保了并发执行的多个事务之间的正确性和一致性。)

  • 脏读(Dirty Read)
    • 当一个事务读取到了另一个事务尚未提交(即可能还会被回滚)的数据时,就会发生脏读。
    • 例如,事务A修改了一行数据但还未提交,此时事务B读取了这行被修改后的数据。如果之后事务A回滚了修改,则事务B实际上读取到了无效的数据。
  • 不可重复读(Non-Repeatable Read)
    • 指的是在一个事务内,多次读取同一数据得到的结果不一致。通常是指其他事务对该数据进行了更新或删除操作,并且这些改变已经提交。
    • 例如,在事务A开始后读取某一行数据;接着另一个事务B更改了这行数据并提交了更改;当事务A再次读取该行时发现数据已经被修改。
  • 幻读(Phantom Read)
    • 发生在一个事务根据某些条件查询数据集合时,随后另一个事务插入了符合这些条件的新记录,并且提交了这个插入操作。因此,当第一个事务重新执行相同的查询时会看到新的“幽灵”记录。
    • 例如,事务A查询所有年龄大于30岁的用户;与此同时,事务B插入了一个年龄为35岁的新用户并且提交了;当事务A再次执行同样的查询时,它将看到之前未出现过的额外一条记录。

事务的隔离级别决定了事务之间的可见性。为了防止这些问题,SQL标准定义了四个级别的事务隔离:

  • 读未提交(Read Uncommitted):最低的隔离级别,允许一个事务读取另一个事务尚未提交的数据。可能导致脏读、幻读和不可重复读。
  • 读已提交(Read Committed):只允许读取已经提交的数据,避免了脏读,但仍可能出现幻读和不可重复读。
  • 可重复读(Repeatable Read):在一个事务内,多次读取同一数据的结果是一致的,避免了脏读和不可重复读,但仍可能出现幻读。
  • 串行化(Serializable):最高的隔离级别,完全避免了脏读、不可重复读和幻读,但性能较低。

每个更高级别的隔离都提供了对低级别问题的保护,但同时也增加了系统开销。选择适当的隔离级别需要权衡数据一致性和性能之间的关系。

4、持久性(Durability)

一旦事务被提交,其结果就是永久性的,即使系统崩溃也不会丢失。这通常通过将事务日志写入磁盘来实现,以确保即使在系统故障后也能恢复数据。

1-2 事务控制语句

BEGIN TRANSACTIONSTART TRANSACTION:开始一个新的事务。在大多数数据库系统中,默认情况下,每个 SQL 语句都是一个单独的事务,但如果需要多个语句作为一个事务执行,则需要显式地开始一个事务。

COMMIT:提交事务,将事务中的所有更改永久保存到数据库中。一旦事务被提交,就不能再回滚。

ROLLBACK:回滚事务,撤销事务中的所有更改,使数据库恢复到事务开始前的状态。

SAVEPOINT:在事务中设置一个保存点,可以在后续的 ROLLBACK TO SAVEPOINT 中回滚到该保存点,而不是回滚整个事务。

SET AUTOCOMMIT:控制事务自动提交模式,手动控制事务的提交。

SET AUTOCOMMIT = {0 | 1 | ON | OFF};

  • 0 或 OFF:关闭自动提交模式。
  • 1 或 ON:默认,开启自动提交模式(MySQL 数据库自动开启了事务提交,所以我们应该先关闭自动提交,改为手动提交)。

假设我们有一个银行转账的例子,涉及两个账户 accountAaccountB。我们需要从 accountA 转账 100 元到 accountB

-- 从 accountA 减少 100 元
UPDATE accounts
SET balance = balance - 100
WHERE account_id = 'accountA';

-- 向 accountB 增加 100 元
UPDATE accounts
SET balance = balance + 100
WHERE account_id = 'accountB';

如果在执行过程中发生错误:

  • 例如 accountA 的余额不足(回滚事务)
  • 例如在第一个 UPDATE 之后和第二个 UPDATE 之前发生错误,那么 accountA 的余额已经被扣减,但 accountB 的余额没有增加(关闭自动提交模式)
-- 关闭自动提交
SET AUTOCOMMIT = 0;  -- 或者 SET AUTOCOMMIT = OFF;
-- 开始事务
START TRANSACTION;

-- 从 accountA 减少 100 元
UPDATE accounts SET balance = balance - 100 WHERE account_id = 'accountA';

-- 检查 accountA 的余额是否足够
IF (SELECT balance FROM accounts WHERE account_id = 'accountA') < 0 THEN
    -- 回滚事务
    ROLLBACK;
    -- 可以在这里处理错误
ELSE
    -- 向 accountB 增加 100 元
    UPDATE accounts SET balance = balance + 100 WHERE account_id = 'accountB';
    -- 提交事务
    COMMIT;
END IF;
-- 重新开启自动提交
SET AUTOCOMMIT = 1;  -- 或者 SET AUTOCOMMIT = ON;

事务处理步骤

在这里插入图片描述

二、数据库视图

数据库视图(View)是基于一个或多个表的虚拟表,其内容由查询定义。视图本身并不存储数据,而是作为一个查询的结果集存在。视图可以简化复杂的查询、提供额外的安全层以及隐藏底层表的复杂性。语法:

CREATE VIEW view_name AS 
SELECT column1, column2, ... FROM table1;

三、数据库索引

MySQL 索引是对数据库中一列或多列的值进行排序的一种数据结构,用于加快数据库查询的速度和性能。

MySQL 索引的建立对于 MySQL 的高效运行是很重要的,索引可以大大提高 MySQL 的检索速度。

MySQL 索引类似于书籍的索引,通过存储指向数据行的指针,可以快速定位和访问表中的特定数据。拿汉语字典的目录页(索引)打比方,我们可以按拼音、笔画、偏旁部首等排序的目录(索引)快速查找到需要的字。

作用

  • 提高查询速度
  • 确保数据的唯一性
  • 可以加速表和表之间的连接,实现表与表之间的参照完整性
  • 使用分组和排序子句进行数据检索时,可以显著减少分组和排序的时间
  • 全文检索字段进行搜索优化

3-1 主键索引(PRIMARY KEY)

某一个属性或属性的组合能唯一标识一条记录

如:学生表(学号,姓名,班级,性别等) ,学号就是唯一标识的,可作为主键

特点

  • 最常见的索引类型
  • 确保数据记录的唯一性
  • 确定特定数据记录在数据库中的位置
CREATE TABLE `Grade` (
    `GradeID` INT(11) AUTO_INCREMENT PRIMARY KEY,
    # 省略代码……
    # 主键索引也可在字段字义之后,如
    # PRIMARY KEY(`GradeID`)    
)
# 添加主键索引
ALTER table `Grade` add PRIMARY Key(`GradeID`);

3-2 唯一索引(UNIQUE)

作用:避免同一个表中某数据列中的值重复

与主键索引的区别:主键索引只能有一个;唯一索引可有多个

CREATE TABLE `Grade` (
    `GradeID` INT(11) AUTO_INCREMENT PRIMARY KEY,
    `GradeName` VARCHAR(32) NOT NULL UNIQUE
    # 或 UNIQUE KEY `Unique_GradeName` (`GradeName`)
)
# 添加唯一索引(系统设计要求每个年级名称必须是唯一)
ALTER table `Grade` add UNIQUE(`GradeName`);

3-3 常规索引(INDEX)

常规索引(也称为普通索引)是最基本的索引类型,它不强制字段的唯一性,也不限制NULL值。常规索引的主要目的是加快数据检索速度。

创建一个employees表,在last_name列上创建一个常规索引以加速基于姓氏的查询

CREATE TABLE `employees` (
    `employee_id` INT AUTO_INCREMENT PRIMARY KEY,
    `first_name` VARCHAR(50) NOT NULL,
    `last_name` VARCHAR(50) NOT NULL,
    `email` VARCHAR(100),
    `hire_date` DATE,
    INDEX idx_last_name (`last_name`)
);
# 添加普通索引
ALTER TABLE `employees` ADD INDEX idx_last_name(`last_name`);
# 查询示例
SELECT * FROM `employees` WHERE last_name = 'Smith';

3-4 全文索引(FULLTEXT)

全文索引尤其是在进行复杂的文本匹配时可以显著提高对文本字段的搜索效率。对于中文文本,通常需要使用特定的分词器来正确地分割词语。

注意

  • 只能用于 MyISAM、InnoDB 类型的数据表

  • 只能用于 CHAR、VARCHAR、TEXT 数据列类型

  • 适合大型数据集

假设我们有一个名为 case_data 的表,其中包含一个名为 content 的文本字段,并且我们想要在这个字段上创建全文索引并使用ngram分词器。

-- 假设表结构如下
CREATE TABLE `case_data` (
    `id` INT AUTO_INCREMENT PRIMARY KEY,
    `name` VARCHAR(255),
    `content` TEXT
) ENGINE = InnoDB;

-- 添加全文索引
ALTER TABLE `case_data` ADD FULLTEXT `idx_content`(`content`) WITH PARSER ngram;

使用MATCH ... AGAINST语法来进行全文搜索。IN BOOLEAN MODE允许使用布尔操作符来进行更复杂的搜索。

SELECT * FROM `case_data` WHERE MATCH(`content`) AGAINST('*内容*' IN BOOLEAN MODE);

这里的*内容*表示包含“内容”这个词的部分匹配。

使用EXPLAIN关键字来查看查询的执行计划。

EXPLAIN SELECT * FROM `case_data`
WHERE MATCH(`content`) AGAINST('*内容*' IN BOOLEAN MODE);

EXPLAIN会输出关于如何执行该查询的信息,包括是否使用了索引、使用的索引名称、扫描行数等。通过这些信息,你可以评估查询的性能,并根据需要调整索引或查询。

3-5 组合索引(Compound)

也称为复合索引或多列索引,是在表的多个列上创建的索引。

CREATE INDEX idx_name ON table_name (column1, column2, column3);

组合索引的生效规则主要基于以下几点:

一、左前缀原则

  • 左前缀匹配:查询必须从索引的最左边的列开始,并且是连续的。这意味着如果要使组合索引生效,查询条件至少需要包含索引中最左边的列。

    SELECT * FROM table_name WHERE column1 = 'value';
    SELECT * FROM table_name WHERE column1 = 'value' AND column2 > 50;
    
  • 不遵循左前缀的情况:如果查询没有使用索引最左边的列或者跳过了中间的列,则不会使用整个索引

    SELECT * FROM table_name WHERE column2 = 'value';  -- 不使用column1
    SELECT * FROM table_name WHERE column1 = 'value' AND column3 = 'value';  -- 跳过了column2
    

二、范围条件的影响

  • 范围条件限制后续列的使用:一旦在组合索引中遇到了范围条件(如>, <, BETWEEN等),那么该范围条件后的所有列将不再参与索引的选择过程。这是因为数据库引擎在处理范围条件时,无法再有效地利用后续的列来进行精确匹配。
SELECT * FROM table_name WHERE column1 = 'value' AND column2 > 50 AND column3 = 'value';

例如,对于索引idx_name(column1, column2, column3),以下查询会使用索引的一部分,但不会使用到column3

三、覆盖索引

  • 覆盖索引:如果查询的所有列都在索引中定义了,那么数据库可以直接从索引中读取数据,而不需要回表访问原数据行。这称为覆盖索引,它可以极大地提高查询性能。
SELECT column1, column2 FROM table_name WHERE column1 = 'value' AND column2 > 50;

这个查询可以从索引idx_name(column1, column2, column3)中直接获取column1column2的数据,而不需要访问表中的其他列。因为减少了I/O操作,显著提高查询性能。

四、优化器决策

  • 优化器选择:最终是否使用某个索引还取决于数据库查询优化器的判断。优化器会根据统计信息、执行计划成本等因素来决定最佳的执行计划。有时候即使符合以上规则,优化器也可能选择不使用索引,因为可能有更优的执行路径。

创建和查询组合索引示例

-- 创建示例
CREATE INDEX idx_lastname_dept ON `Employees` (`LastName`, `DepartmentID`);
-- 查询示例
SELECT * FROM `Employees` WHERE `LastName` = 'Smith' AND `DepartmentID` = 10;

3-6 空间索引(Spatial)

空间索引是专门用于地理空间数据的索引类型。它主要用于存储和查询具有几何属性的数据,如点、线、多边形等。空间索引可以帮助高效地执行空间查询,如查找某个范围内的所有点、计算两点之间的距离等。

-- 创建空间索引
CREATE SPATIAL INDEX idx_coordinates ON `Locations` (`Coordinates`);
-- 查询示例:找到所有位于特定矩形区域内的位置
# 定义一个矩形区域
SET @rectangle = ST_GeomFromText('POLYGON((10 10, 20 10, 20 20, 10 20, 10 10))', 4326);
# 查询在矩形区域内的所有位置
SELECT * FROM `Locations` WHERE MBRContains(@rectangle, `Coordinates`);

管理索引

创建索引可以在创建表时添加也可以在建表后追加

删除索引
DROP INDEX 索引名 ON 表名
ALTER TABLE 表名 DROP INDEX 索引名
ALTER TABLE 表名 DROP PRIMARY KEY
查看索引
SHOW INDEX(KEYS) FROM 表名
添加正确的索引
  • 在 WHERE、ORDER BY 子句中经常使用的字段
  • 字段的值是多个(例如性别字段则不适合)
  • 字段内容不是经常变化的(经常变化的字段,添加索引反而降低性能)
  • 不宜过多添加索引(每添加一条索引都会占用磁盘空间)
索引失效

某些语法或查询条件可能会导致索引不生效。以下是一些常见的情况,其中索引可能不会被使用:

1、使用函数

如果在列上应用了函数,索引通常不会生效。例如:

SELECT * FROM employees WHERE UPPER(last_name) = 'SMITH';

2、不等操作符

使用不等操作符(如!=, <>)通常会导致索引不生效。例如:

SELECT * FROM employees WHERE salary != 50000;

在这种情况下,数据库引擎可能会选择全表扫描。

3、LIKE 通配符

LIKE语句以通配符开头时,索引通常不会生效。例如:

-- `LIKE`语句以通配符开头数据库引擎无法利用索引来快速定位匹配的行
SELECT * FROM employees WHERE first_name LIKE '%John%';
-- 可改写查询为:
SELECT * FROM employees WHERE first_name LIKE 'John%';

4、OR 条件

当使用OR条件时,如果其中一个条件不能使用索引,那么整个查询可能都不会使用索引。例如:

-- 如果`salary`列上有索引但`last_name`列没有索引,那么整个查询可能不会使用索引
SELECT * FROM employees WHERE (salary > 50000 OR last_name = 'Smith');
-- 通过创建复合索引来解决这个问题:
CREATE INDEX idx_salary_last_name ON employees (salary, last_name);

5、范围条件后的列

在组合索引中,一旦遇到范围条件(如>, <, BETWEEN),后续的列将不会被用于索引的选择过程。例如:

SELECT * FROM orders WHERE customer_id = 100 AND order_date > '2023-01-01' AND total_amount = 500;

在这个查询中,order_date上的范围条件使得total_amount这一列不会被用于索引选择。

6、数据类型不匹配

如果查询中的数据类型与列的数据类型不匹配,索引可能不会生效。例如:

-- 如果`employee_id`是整数类型,而查询中使用了字符串,这会导致索引失效
SELECT * FROM employees WHERE employee_id = '123';
-- 正确的做法:
SELECT * FROM employees WHERE employee_id = 123;

7、隐式类型转换

隐式类型转换也可能导致索引失效。例如:

-- 如果`hire_date`是日期类型,而查询中使用了字符串,这会导致隐式类型转换从而可能使索引失效
SELECT * FROM employees WHERE hire_date = '2023-01-01';
-- 正确的做法:
SELECT * FROM employees WHERE hire_date = DATE '2023-01-01';

8、大量结果集

如果查询返回的结果集非常大(例如超过表的一定比例),数据库优化器可能会选择全表扫描而不是使用索引。这是因为全表扫描在这种情况下可能更高效。

9、使用 NOT INNOT EXISTS

使用NOT INNOT EXISTS子查询时,索引可能不会生效。例如:

SELECT * FROM employees WHERE department_id NOT IN (SELECT department_id FROM departments WHERE location = 'New York');

这种情况下,可以考虑使用LEFT JOIN来替代NOT IN,以便更好地利用索引。

10、使用 ORDER BYGROUP BY

如果ORDER BYGROUP BY中的列不在索引中,或者顺序与索引顺序不一致,索引可能不会生效。例如:

-- 如果只有`last_name`上有索引,而没有包含`first_name`,那么 `ORDER BY`可能不会使用索引
SELECT * FROM employees ORDER BY last_name, first_name;
-- 可以创建一个复合索引来解决这个问题:
CREATE INDEX idx_last_name_first_name ON employees (last_name, first_name);
;