第5章〓事务管理与并发控制 学习目标: 了解什么是事务。 掌握常见的事务操作。 了解事务的隔离级别。 掌握事务的异常处理。 掌握事务的并发控制。 事务管理与并发控制,是数据库管理系统中保证数据完整性和一致性的关键技术。事务被定义为一个或多个数据库操作的集合,这些操作作为一个单一的工作单元执行,要么全部成功,要么全部失败,从而保证了数据库的一致性。本章将针对事务管理与并发控制进行详细讲解。 5.1事务机制 5.1.1事务的概念 事务是数据库管理系统中的一个基本概念,它指的是作为单个逻辑工作单位执行的一系列操作,也可以说是一系列操作的集合,这些操作作为一个整体一起执行,要么全部成功,要么全部失败。 以现实生活中转账为例,转账可以分为转入和转出,只有这两部分都完成才认为转账成功,在数据库中,这个过程是使用两条SQL语句来完成的,如果其中任意一条语句出现异常没有执行,则会导致两个账户的金额不同步,造成错误。此时就可以使用事务来解决这个问题,将两条语句作为一个整体一起执行,要么全部执行成功,要么全部执行失败,以确保账户金额的同步。 事务的核心特点是原子性、一致性、隔离性、持久性,这些特性常被称为ACID属性。 1. 原子性 原子性(Atomicity)意味着事务中的所有操作要么全部成功执行,要么全部不执行。事务作为一个整体被执行,不允许只执行事务中的一部分操作。如果事务中的某个操作失败,整个事务将被回滚(撤销)到事务开始之前的状态。 2. 一致性 一致性(Consistency)确保事务的执行将数据库从一个一致的状态转变到另一个一致的状态。一致状态的定义是基于数据库的完整性约束,如外键约束、唯一约束等。事务执行过程中不应违反这些约束。 3. 隔离性 隔离性(Isolation)是指并发执行的事务之间的操作是隔离的,一个事务的操作不应该被其他事务干扰。数据库系统通过并发控制机制实现事务的隔离性,以避免诸如脏读、不可重复读和幻读等问题。 4. 持久性 持久性(Durability)意味着一旦事务被提交,它对数据库所做的更改就是永久的,即使系统发生故障。提交后的事务结果被持久化存储在数据库中,不会因为系统故障而丢失。 事务的管理对于维护数据库的完整性和一致性至关重要。数据库管理系统提供了事务管理的机制,允许开发者控制事务的开始、执行、提交或回滚,以确保数据的准确性和可靠性。通过使用事务,开发者可以构建出强大且稳定的数据库应用程序,有效地处理并发数据访问,保护数据免受损坏。 5.1.2事务的操作 openGauss是一个开源的关系数据库管理系统,由华为主导开发。它支持SQL标准,并提供了事务管理功能,使得开发者可以在应用程序中利用事务来保证数据的一致性和隔离性。在openGauss中,事务的操作主要涉及以下几方面。 1. 开始事务 在openGauss中,可以使用BEGIN或START TRANSACTION命令来开始一个新的事务。这标志着事务的开始,之后的所有数据库操作都将作为这个事务的一部分。 1.BEGIN; 2.或 3.START TRANSACTION; 2. 提交事务 当事务中的所有操作都成功完成,且想要将这些更改永久保存到数据库中时,可以使用COMMIT命令来提交事务。提交事务会将自事务开始以来进行的所有数据修改永久化到数据库中。 1.COMMIT; 3. 回滚事务 如果在事务执行过程中遇到错误,或者出于某种原因决定放弃事务中所做的所有修改,可以使用ROLLBACK命令来回滚事务。回滚将撤销自事务开始以来所做的所有修改,将数据库恢复到事务开始时的状态。 1.ROLLBACK; 4. 设置保存点 在事务执行的过程中,可以设置一个或多个保存点(SAVEPOINT)。保存点允许在事务内部标记一个特定的点,之后如果需要,可以仅将事务回滚到这个点,而不是完全回滚事务。使用ROLLBACK TO SAVEPOINT命令来回滚到特定的保存点。 1.ROLLBACK TO SAVEPOINT; 5. 事务隔离级别的设置 在openGauss中,可以通过SET TRANSACTION命令来设置事务的隔离级别,具体语句如下。 1.SET TRANSACTION ISOLATION LEVEL READ COMMITTED; 上述语句设置了当前事务的隔离级别为“读已提交”,这是4种标准SQL隔离级别之一。使用事务是保持数据库一致性和完整性的关键机制,尤其是在并发访问的环境中。在openGauss中有效地使用事务,可以帮助开发者确保应用的数据处理逻辑既准确又高效。 下面通过一个例子来演示事务的操作。假设有一个简单的银行账户表accounts,其中包含三个字段id(账户ID)、account_number(卡号)和balance(账户余额)。账户1初始余额1000元,账户2初始余额2000元,然后来模拟转账的过程。 首先,需要创建这个表并插入一些初始数据,具体语句如下。 1.CREATE TABLE accounts ( 2. account_id INT PRIMARY KEY, 3. account_number VARCHAR(20), 4. balance DECIMAL(10, 2) 5.); 6.INSERT INTO accounts (account_id, account_number, balance) VALUES (1, 6214850117715334,1000.00); 7.INSERT INTO accounts (account_id, account_number, balance) VALUES (2, 6214850117715335,2000.00); 然后将通过一个事务来模拟从一个账户向另一个账户转账的过程,具体语句如下。 1.BEGIN; 2.UPDATE accounts SET balance = balance - 100 WHERE account_id = 1; 3.UPDATE accounts SET balance = balance + 100 WHERE account_id = 2; 4.COMMIT; 上述语句执行成功后,可以通过SELECT语句查询accounts表中两个账户的余额情况,查询结果如下。 1.schooldb =# SELECT * FROM accounts; 2.account_id | account_number | balance 3.------------+------------------+--------- 4. 1 | 6214850117715334 |900.00 5. 2 | 6214850117715335 | 2100.00 6.(2 rows) 在上述例子中,首先开始了一个事务,然后执行转账操作,如果语句操作均正常,则执行事务的提交操作,完成转账功能。 在实际应用中,事务可能会涉及更复杂的逻辑和错误处理机制,还需要根据具体的业务逻辑和数据一致性要求来设计和实现事务。 5.1.3事务的异常处置 在数据库操作中,事务异常处置是处理事务执行过程中遇到的错误和异常的过程。这些错误可能是由于多种原因造成的,包括数据冲突、违反约束条件、系统错误等。正确处理这些异常对于保持数据库的完整性和一致性至关重要。以下是一些常见的事务异常处置策略。 1. 回滚事务 回滚(Rollback)是处理事务异常的最直接方式。当事务中的一个操作失败时,可以使用回滚操作撤销事务中所有已执行的操作,使数据库回到事务开始之前的状态。这样可以保证数据库的一致性不被破坏。 1.BEGIN; 2.-- 执行一系列数据库操作 3.DO SOMETHING; 4.-- 如果发生错误 5.ROLLBACK; -- 撤销所有更改 接下来仍以转账为例来演示事务回滚,设置账户1初始余额为1000元,账户2初始余额为2000元,假设账户1想要给账户2转账100元,此时可以开启一个事务,通过UPDATE语句将账户1的100元钱转给账户2,具体语句如下。 1.BEGIN; 2.UPDATE accounts SET balance = balance - 100 WHERE account_id = 1; 3.UPDATE accounts SET balance = balance + 100 WHERE account_id = 2; 上述语句执行完成后,使用SELECT语句查询两个账户的余额,查询结果如下。 1.schooldb =# SELECT * FROM accounts; 2.account_id | account_number | balance 3.--------+--------------+--------- 4. 1 | 6214850117715334 | 900.00 5. 2 | 6214850117715335 | 2100.00 6.(2 rows) 从上述结果可以看出,账户1的余额为900元,账户2的余额为2100元,说明转账成功。然而此时账户1反悔了不想给账户2转账,由于事务还没有提交,就可以将事务回滚,取消本次转账操作,具体语句如下。 1.ROLLBACK; ROLLBACK语句执行成功后,再次使用SELECT语句查询两个账户的余额,查询结果如下。 1.schooldb =# SELECT * FROM accounts; 2.account_id | account_number | balance 3.--------+--------------+--------- 4. 1 | 6214850117715334 | 1000.00 5. 2 | 6214850117715335 | 2000.00 6.(2 rows) 从查询结果可以看出,账户1的金额是1000元且账户2的金额是2000元,并没有完成转账的功能,因此说明当前事务中的操作被取消了并没有执行。 2. 使用保存点 在事务执行过程中设置保存点(Savepoints),可以在事务失败时只回滚到特定的保存点,而不是完全回滚事务。这允许更细粒度的错误恢复,可以在不放弃整个事务的情况下修正错误,具体语句如下。 1.BEGIN; 2.SAVEPOINT savepoint_name; -- 设置保存点 3.-- 执行一系列操作 4.DO SOMETHING; 5.-- 如果某个操作失败 6.ROLLBACK TO savepoint_name; -- 回滚到保存点 7.-- 继续其他操作或最终提交事务 8.COMMIT; 还是以转账为例,设置账户1初始余额为1000元,账户2初始余额为2000元,假设账户1想要给账户2转账100元,此时可以开启一个事务,通过UPDATE语句将账户1的100元钱转给账户2,具体语句如下。 1.BEGIN; 2.SAVEPOINT before_transfer; 3.UPDATE accounts SET balance = balance - 100 WHERE account_id = 1; 4.UPDATE accounts SET balance = balance + 100 WHERE account_id = 2; 上述语句执行完成后,使用SELECT语句查询两个账户的余额,查询结果如下。 1.schooldb =# SELECT * FROM accounts; 2.account_id | account_number | balance 3.--------+--------------+--------- 4. 1 | 6214850117715334 | 900.00 5. 2 | 6214850117715335 | 2100.00 6.(2 rows) 从上述结果可以看出,账户1的余额为900元,账户2的余额为2100元,说明转账成功。假设此时发生错误,如账户2不存在或转账金额不正确等,需要回滚到before_transfer保存点,撤销从账户1扣除的金额,就可以将事务回滚到保存点,具体语句如下。 1.ROLLBACK TO before_transfer; 上述语句执行成功后,再次使用SELECT语句查询两个账户的余额,查询结果如下。 1.schooldb =# SELECT * FROM accounts; 2.account_id | account_number | balance 3.--------+--------------+--------- 4. 1 | 6214850117715334 | 1000.00 5. 2 | 6214850117715335 | 2000.00 6.(2 rows) 从查询结果可以看出,账户1的金额是1000元且账户2的金额是2000元,并没有完成转账的功能。这样当账户2时发生错误,可以使用ROLLBACK TO before_transfer;语句回滚到before_transfer保存点,这样账户1的扣款操作就会被撤销,保持了数据的一致性。 5.1.4事务的隔离级别 事务的隔离级别定义了一个事务可能受到其他并发事务的影响程度。隔离级别的设置是为了在并发访问数据库时,平衡数据的正确性和访问速度。SQL标准定义了4种隔离级别,每个级别在并发性和数据一致性之间提供不同的权衡。以下是这4种隔离级别,从最低到最高。 1. 读未提交(Read Uncommitted) 在这个级别下,一个事务可以读取另一个事务未提交的数据。这种级别的并发性最高,但是它允许“脏读”(即读取到其他事务未提交的更改)。 2. 读提交(Read Committed) 事务只能读取到其他事务已经提交的数据。这个级别防止了脏读,但是仍然允许“不可重复读”,即在同一事务中,两次读取同一记录可能会得到不同的结果,因为其他事务在这两次读取之间进行了更新并提交。 3. 可重复读(Repeatable Read) 保证在同一个事务内,多次读取同一数据的结果是一致的,即避免了不可重复读。但是这个级别仍然可能出现“幻读”,即事务在读取某个范围的记录时,另一个事务插入了新的记录,导致第一个事务再次读取时会发现之前未见过的新记录。 4. 串行化(Serializable) 串行化是事务的最高隔离级别,它通过锁定涉及的所有行来防止脏读、不可重复读和幻读。这保证了完全的隔离,使得并发执行的事务看起来就像是依次串行执行的。 选择合适的隔离级别是在数据一致性需求和系统性能之间做出的重要权衡。较低的隔离级别提高了并发性能,但降低了数据的一致性保障; 而较高的隔离级别虽然提供了更强的数据一致性保护,却以牺牲一定的并发性能为代价。数据库设计者和开发者需要根据具体的应用场景和业务需求,选择最适合的事务隔离级别。 5.2并发控制 5.2.1并发问题介绍 在数据库系统中,当多个事务同时执行时,如果没有适当的并发控制机制,就可能出现各种并发问题。这些问题不仅会影响数据的一致性和完整性,还可能导致数据的不一致性,破坏数据库的可靠性。主要的并发问题如下。 1. 脏读 脏读(Dirty Read)发生在一个事务读取了另一个事务未提交的数据时。如果那个事务回滚,它所做的更改就会消失,这意味着第一个事务读到了根本不存在的数据。 实际上,在openGauss中,即使设置事务的隔离级别为较低的READ UNCOMMITTED,也不会导致脏读。这是因为openGauss使用多版本并发控制(MVCC)来保证即使在最低隔离级别下也不会出现脏读。脏读主要在理论上讨论,以更好地理解事务隔离级别对并发事务处理的影响。在生产环境中,通常应该避免脏读,因为脏读可能导致数据不一致。 介于上述原因,在openGauss中演示脏读可能比较困难,这里仅演示脏读出现的过程。接下来就以前面accounts表中的数据为例来演示脏读出现的过程,假设有两个并发事务T1和T2,具体步骤如下。 T1: 更新账户1的余额但未提交。 1.-- T1开始 2.BEGIN; 3.UPDATE accounts SET balance = balance - 100 WHERE account_id = 1; T2: 读取了T1修改后的账户1余额。 1.-- 设置隔离级别以演示脏读 2.SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; 3.-- T2开始 4.BEGIN; 5.-- 在T1提交或回滚之前读取账户1的余额 6.SELECT balance FROM accounts WHERE account_id = 1; 7.COMMIT; T1: T1回滚更改。 1.ROLLBACK 在上述操作中,如果T2在T1回滚之前读取了账户1的余额,则说明T2就发生了脏读。 2. 不可重复读 不可重复读(Nonrepeatable Read)发生在一个事务中两次读取同一数据集合时,另一个并发事务更新了这些数据并提交,导致第一个事务两次读取的结果不一致,这主要是由于更新操作造成的。 接下来演示不可重复读,首先将事务隔离级别设置为READ COMMITTED(这通常是默认级别,可以省略不写),然后开始一个事务并读取数据,具体步骤如下。 T1: 开始一个新事务,读取数据。 1.-- 设置隔离级别以演示不可重复读 2.SET TRANSACTION ISOLATION LEVEL READ COMMITTED; 3.-- 事务1开始 4.BEGIN; 5.SELECT balance FROM accounts WHERE account_id = 1; -- 假设返回1000.00 查询结果: 1.balance 2.------------- 3.1000.00 4.(1 row) T2: 开始一个新事务,修改同一条记录的数据,然后提交。 1.-- 事务2开始 2.BEGIN; 3.UPDATE accounts SET balance = 900 WHERE account_id = 1; 4.COMMIT; T1: 在T1中再次读取相同的记录。 1.SELECT balance FROM accounts WHERE account_id = 1; -- 在READ COMMITTED级别下,可能返回900 2.COMMIT; 查询结果: 1.balance 2.------------- 3.900.00 4.(1 row) 在上述操作中,如果T1在T2提交更新后,再次读取时发现账户1的余额已经变了,就说明发生了不可重复读。 需要注意的是,由于MVCC的实现,openGauss的REPEATABLE READ以及更高的事务隔离级别提供了对不可重复读的保护,就不会出现不可重复读的现象。 3. 幻读 幻读(Phantom Read)与不可重复读类似,但它涉及插入或删除操作。幻读发生在一个事务重新读取之前查询过的范围时,发现另一个事务插入或删除了符合查询条件的行。这样,第一个事务就会看到之前不存在的“幻影”数据。 接下来演示一下幻读,首先设置事务的隔离级别为REPEATABLE READ,然后再开启一个事务执行插入操作,具体步骤如下。 T1: 设置事务隔离级别为REPEATABLE READ,然后开始一个事务并进行第一次查询。 1.SET TRANSACTION ISOLATION LEVEL REPEATABLE READ; 2.BEGIN; 3.SELECT * FROM accounts WHERE balance > 100; -- 假设返回两行数据 查询结果: 2.account_id | account_number | balance 3.--------+--------------+--------- 4.1 | 6214850117715334 | 900.00 5.2 | 6214850117715335 | 2000.00 6.(2 rows) T2: 开始另一个事务,并插入一条符合之前查询条件的新记录,然后提交。 1.BEGIN; 2.INSERT INTO accounts (account_id, account_number, balance) VALUES (3, 6214850117715336, 1500.00); 3.COMMIT; T1: 再次执行相同的查询。 1.SELECT * FROM accounts WHERE balance > 100; -- 在某些数据库系统中,可能返回三行数据 2.COMMIT; 查询结果: 1.account_id | account_number | balance 2.--------+--------------+--------- 3.1 | 6214850117715334 | 900.00 4.2 | 6214850117715335 | 1000.00 5.3 | 6214850117715336 | 1500.00 6.(3 rows) 在上述操作中,如果T1在T2提交插入操作后再次查询余额大于100的账户,会看到之前未见过的新记录,即发生了幻读。 4. 丢失修改 丢失修改(Lost Update)发生在两个事务都尝试更新同一数据时。一个事务的更新可能会被另一个事务的更新所覆盖,结果是第一个事务的更新就丢失了。 在openGauss中,通过默认的事务隔离级别和锁机制,丢失修改(Lost Update)问题是被防止的,所以无法演示出丢失修改,也仅能演示过程。 T1: 开始一个事务并查询accounts表中account_id=1的余额,假设根据查询结果增加100元。 1.BEGIN; 2.SELECT balance FROM accounts WHERE account_id = 1; T2: 再开启另一个事务,查询同一记录的余额,然后更新这个余额,例如,增加50元。 1.BEGIN; 2.SELECT balance FROM accounts WHERE account_id = 1; 3.UPDATE accounts SET balance = balance + 50 WHERE account_id = 1; 4.COMMIT; T1: 基于最初的查询结果执行更新,假设增加100元。 1.UPDATE accounts SET balance = balance + 100 WHERE account_id = 1; 2.COMMIT; 在上述操作中,T2的更新可能会被T1的更新所覆盖,因为T1没有考虑到在其事务开始后发生的T2更新,这就是典型的丢失修改问题。 通过上述示例说明了并发事务可能带来的问题。在实际应用中,通过设置合适的事务隔离级别,可以有效地控制这些并发问题。具体选择哪种隔离级别取决于应用的需求和对性能的考虑。 5.2.2锁的分类介绍 锁是数据库管理系统中用来实现并发控制的一种机制,旨在管理不同事务对共享数据的访问。锁的基本原理是当一个事务在访问数据时,它会对数据加锁,以防止其他事务同时访问相同的数据,从而避免数据不一致性问题。 提到锁不得不介绍一下悲观锁和乐观锁,乐观锁和悲观锁是两种常见的并发控制机制,用于处理多个事务同时访问和修改同一资源的情况。 乐观锁: 乐观锁假设多个事务在并发执行时不会彼此冲突,直到提交数据时才会检查是否有冲突。如果有冲突,则采取回滚等方式解决。相对悲观锁而言,采取了更加宽松的加锁机制,大多是基于数据版本(Version)记录机制实现,如版本号或时间戳等。 悲观锁: 是指对数据被外界修改持保守态度,因此在整个数据处理过程中总是假设最坏的情况,认为会发生并发冲突,将数据处于锁定状态。悲观锁的实现通常依赖于数据库的锁机制。 了解了乐观锁和悲观锁的原理后,接下来可以根据锁的类型和粒度进行分类,从类型上进行细分,锁可以分为共享锁、排他锁、更新锁; 从粒度上来分,锁可以分为行锁、页锁、表锁、库锁。接下来针对锁的分类进行更详细的讲解。 1. 按锁的类型分类 共享锁(Shared Lock): 也称为读锁,允许多个事务同时读取一个资源,但阻止其他事务写入该资源。共享锁是一种乐观锁的实现,适用于读取操作频繁的场景。可以使用SELECT…FOR SHARE语句来获取共享锁。 排他锁(Exclusive Lock): 也称为写锁,只允许一个事务对数据进行修改(读或写),阻止其他事务读取或者写入,直到排他锁被释放。排他锁是一种悲观锁的实现,适用于写入操作频繁的场景。可以使用SELECT…FOR UPDATE语句来获取排他锁。 更新锁(Update Lock): 更新锁是共享锁和排他锁的混合,用于在读取数据的同时防止其他事务修改数据,直到更新锁被释放。更新锁也属于悲观锁,可以使用SELECT…FOR UPDATE(既是排他锁,也可以理解为更新锁)或 SELECT…FOR NO KEY UPDATE(无键更新锁)语句来获取更新锁。 2. 按锁的粒度分类 行锁(Row Lock): 锁定数据表中的特定行。这是最细的锁粒度,可以最大限度地减少锁冲突,但管理这种锁的开销也最大。 页锁(Page Lock): 锁定数据表中的页,页是数据库存储结构中的一部分,包含多行数据。 表锁(Table Lock): 锁定整个表。这种锁的粒度最大,会锁定表中的所有行,适用于对表执行大量操作的场景。 库锁(Database Lock): 锁定整个数据库,这是最大的锁粒度,用得较少,通常在执行数据库级别的操作时使用。 锁的设计和实现是数据库管理系统中解决并发控制问题的关键。正确使用锁可以在保证数据一致性和完整性的同时,提高并发访问的性能。不同的应用场景和需求可能需要不同类型和粒度的锁来平衡数据的安全性和系统的性能。 5.2.3锁并发控制 锁并发控制是一种通过锁机制来控制多个事务并发访问和修改数据库资源的手段。在openGauss中,锁机制用于确保数据的一致性和隔离性。不同类型的锁具有不同的特性和兼容性,如共享锁允许多个事务同时读取资源,而排他锁则只允许一个事务对资源进行修改。此外,意向锁用于表示事务的锁意向,以便其他事务能够做出相应的反应。锁的粒度决定了锁所覆盖的数据范围,而锁的兼容性则决定了不同锁之间是否可以同时持有。 在实际应用中,锁并发控制对于确保数据的安全性和一致性至关重要。例如,在银行转账场景中,通过锁定用户A的账户,可以防止其他事务在转账过程中修改该账户,从而确保转账的正确性。 需要注意的是,锁的使用也会带来一定的开销,如锁的获取、释放和等待等操作都会消耗系统资源。因此,在设计数据库系统时,需要根据实际应用场景和需求来选择合适的锁策略和粒度,以平衡并发性能和系统开销之间的关系。 接下来会通过案例的形式来演示不同类型锁的使用。 1. 共享锁 示例: 多个事务同时读取同一份数据,但不允许其他事务修改该数据。 T1: 查询账户信息。 1.BEGIN; 2.SELECT * FROM accounts WHERE account_id = 1 FOR SHARE; -- 获取共享锁 3.COMMIT; T2: 查询账户信息。 1.BEGIN; 2.SELECT * FROM accounts WHERE account_id = 1 FOR SHARE; -- 获取共享锁,与事务A兼容 3.COMMIT; T3: 更新账户余额。这里由于UPDATE操作会修改数据,因此openGauss会自动在相应的行上加上排他锁,以防止其他事务同时进行修改。 1.BEGIN; 2.UPDATE accounts SET balance = balance+100 WHERE account_id = 1; -- 等待,因为排他锁与共享锁不兼容 3.COMMIT; 在上述例子中,T1和T2可以同时获取共享锁并读取数据,但T3尝试获取排他锁来修改数据时会被阻塞,直到T1和T2释放它们的共享锁。 2. 排他锁 示例: 一个事务修改数据,阻止其他事务同时读取或修改该数据。 T1: 获取排他锁,更新账户余额。 1.BEGIN; 2.SELECT * FROM accounts WHERE account_id = 1 FOR UPDATE; -- 获取排他锁 3.UPDATE accounts SET balance = balance+100 WHERE account_id = 1 4.COMMIT; T2: 获取排他锁,查询账户信息。 1.BEGIN; 2.SELECT * FROM accounts WHERE account_id = 1 FOR UPDATE; -- 等待,因为排他锁与排他锁不兼容 3.COMMIT; 在上述例子中,T1获取排他锁后修改数据,T2尝试获取同一资源的排他锁时会被阻塞,直到T1释放锁。 3. 更新锁 示例: 一个事务正在读取数据并打算稍后更新,需要阻止其他事务修改这份数据,但允许其他事务继续读取。 T1: 获取更新锁,读取账户余额数据,并对账户余额进行更新。 1.BEGIN; 2.SELECT * FROM accounts WHERE account_id = 1 FOR NO KEY UPDATE; -- 获取更新锁 3.UPDATE accounts SET balance = balance+100 WHERE account_id = 1 4.COMMIT; T2: 获取共享锁,读取账户余额。 1.BEGIN; 2.SELECT * FROM accounts WHERE account_id = 1 FOR SHARE; -- 可以获取共享锁,因为更新锁与共享锁兼容 3.COMMIT; T3: 获取排他锁,更新账户余额。 1.BEGIN; 2.UPDATE accounts SET balance = balance+100 WHERE account_id = 1; -- 等待,因为排他锁与更新锁不兼容 3.COMMIT; 在上述例子中,T1获取更新锁后读取数据并计划修改,T2可以获取共享锁来读取数据,但T3尝试获取排他锁来修改数据时会被阻塞。 5.2.4多版本并发控制 多版本并发控制(Multiversion Concurrency Control, MVCC)是一种广泛使用的并发控制机制,它通过为数据库对象维护不同版本的数据来实现高效的事务隔离。MVCC允许读操作和写操作并发执行,而不必彼此等待,极大地提高了数据库系统的并发性能。它特别适用于读操作远多于写操作的场景。 1. 工作原理 (1) 数据版本: 每当数据被修改时,系统不是直接覆写旧数据,而是创建数据的一个新版本。每个版本都有一个唯一的时间戳或版本号。 (2) 读操作: 当执行读操作时,MVCC允许事务看到数据的一个一致性快照,这通常是事务开始时数据的状态。这意味着读操作可以访问到数据的旧版本,而不会被并发的写操作阻塞。 (3) 写操作: 写操作创建数据的新版本,而不影响旧版本的数据,直到新事务提交。这样,不同的事务可以“同时”看到同一数据的不同版本。 2. 优点 (1) 非阻塞读操作: 读事务不会被写事务阻塞,因为它们可以访问数据的旧版本。 (2) 减少锁争用: 通过减少对共享数据的锁需求,MVCC可以降低锁争用,提高系统的并发性能。 (3) 实现不同隔离级别: MVCC可以灵活实现SQL标准定义的不同事务隔离级别,如读已提交(Read Committed)、可重复读(Repeatable Read)等。 3. 缺点 (1) 空间开销: 因为需要为修改过的数据保留多个版本,MVCC可能会增加数据存储的开销。 (2) 版本管理: 系统必须有效地管理数据的不同版本,包括确定何时可以清理(或“回收”)旧版本的数据,这个过程通常称为垃圾收集或版本清理。 (3) 写操作性能: 虽然MVCC显著提高了读操作的并发性能,但写操作可能因为需要创建新的数据版本和管理这些版本而产生额外的开销。 MVCC被许多现代数据库系统采用,如openGauss、PostgreSQL、MySQL等。每个系统的MVCC实现细节可能不同,但基本原理相似,都是通过为数据对象提供多个版本来支持高效的并发访问。通过使用MVCC,数据库系统能够提供强大的并发性能,同时保持严格的事务隔离,确保数据的一致性和完整性。 需要注意的是,MVCC通常是数据库管理系统内部实现的一部分,而且它的工作原理主要体现在数据版本的管理和访问上,这些都是在数据库的底层操作中自动处理的,无须人为处理。 小结 本章深入探讨了数据库系统中事务管理与并发控制的核心概念,包括事务的ACID属性、事务隔离级别以及如何通过锁机制和多版本并发控制(MVCC)来处理多个事务同时访问数据库时可能出现的并发问题。通过理解这些基本原则和技术,开发者可以设计出既保证数据一致性和完整性,又能高效处理并发请求的数据库应用,满足现代应用对数据处理的复杂需求。 习题 1. 请简要说明事务操作包括哪些。 2. 请简要说明并发问题有哪些。 第6章〓数据库设计 学习目标: 了解什么是数据库设计方法。 掌握数据库范式的使用。 掌握数据库设计流程。 数据库设计是建立高效、可靠数据库系统的基础。它涉及确定数据存储的结构、关系以及如何有效组织、处理和检索这些数据。良好的数据库设计可以最大化性能,确保数据一致性和减少冗余。本章将针对数据库设计进行详细讲解。 6.1数据库设计方法与范式理论 6.1.1数据库设计方法 数据库设计方法是指数据库建模、创建和优化的一系列步骤和技术。良好的数据库设计对于确保数据的完整性、提高查询效率和简化数据库维护工作至关重要。数据库设计通常包括以下几个关键阶段。 1. 需求分析 在设计数据库之前,首先要进行需求分析。这一阶段的目标是收集和分析用户的数据需求,了解系统应支持的业务流程、数据流以及信息的存储和访问需求。需求分析的结果通常以需求规格说明书的形式呈现。 2. 概念设计 概念设计阶段是将需求分析阶段得到的信息需求转换为数据模型的过程。最常用的方法是建立实体关系模型(ER模型)。在这一阶段,设计者识别系统中的实体、实体属性和实体间的关系,并用图形化方式表示出来,从而得到数据库的结构设计。 3. 逻辑设计 在概念设计的基础上,逻辑设计阶段将ER模型转换为具体的数据库模型,如关系模型。这一阶段的关键任务包括确定表结构、字段(属性)、键(主键和外键)等。逻辑设计的结果是一组详细的模式定义,描述了数据库的逻辑结构,但尚未涉及具体的数据库管理系统。 4. 物理设计 物理设计阶段是根据逻辑设计的结果,并考虑到数据库系统的特性,设计数据库的物理存储结构。这包括确定文件的存储方式、索引的建立、数据分区策略等,以优化数据库的性能和存储效率。 5. 数据库实施 在完成设计后,接下来是数据库的实施阶段。这通常涉及使用SQL或特定数据库管理系统提供的工具来创建数据库、表、索引、视图等数据库对象,以及输入初始数据。 6. 数据库维护 数据库设计并不是一次性完成的任务,随着业务需求的变化,数据库设计可能需要进行调整。维护阶段包括监控数据库性能、调整和优化设计、更新和管理数据等活动。 良好的数据库设计应考虑数据的完整性、安全性、性能和可扩展性,确保能够有效支持应用程序和用户的数据需求。 6.1.2范式理论 范式理论是数据库设计中用于评估和改进数据库表结构的一套标准,目的在于减少数据冗余、避免更新异常,并保持数据的一致性。通过将数据库设计到适当的范式,可以提高数据库的逻辑清晰度和操作效率。以下是数据库范式理论中的几个关键范式。 1. 第一范式(1NF) 第一范式是为了确保每个表中的每个字段都是原子的,不能再分解。换句话说,表中的每一列都应该存储不可分割的数据项。其主要目的是消除重复的列,确保每一列的原子性。 2. 第二范式(2NF) 第二范式的前提是表已经处于第一范式的基础上。确保表中的所有非键字段完全依赖于主键。如果存在部分依赖(如非键字段只依赖于复合主键的一部分),则应该分离出不同的表。它的目的是解决部分依赖问题,进一步减少数据冗余。 3. 第三范式(3NF) 第三范式的前提是表已经处于第二范式。确保表中的所有非键字段只依赖于主键,不存在传递依赖,即非键字段依赖于其他非键字段。它的目的是消除传递依赖,确保数据的逻辑独立性。 4. 巴斯科德范式(BCNF) 巴斯科德范式的前提是表已经处于第三范式。在第三范式的基础上更严格,即使在主键是复合的情况下,也要确保表中的每个非键字段都直接依赖于主键,而不是依赖于任何候选键的一部分。它的目的是解决3NF中复合主键带来的问题,确保对任何候选键都没有部分依赖或传递依赖。 5. 更高范式 更高范式包括第四范式(4NF)、第五范式(5NF)等,主要关注多值依赖和更复杂的关系模式。这些高级范式在实际数据库设计中使用较少,通常用于解决特定类型的数据冗余问题。 将数据库设计到适当的范式可以减少数据存储的冗余,提高数据的一致性和完整性,但也可能导致查询性能的下降和数据结构的复杂化。因此,实际应用中需要根据具体情况在范式理论和性能之间做出权衡。在某些情况下,为了优化性能,可能会采取反规范化的措施,有意识地违反范式规则,以减少表的连接操作或提高查询效率。 6.1.3模式分解 模式分解是将一个可能导致数据冗余和更新异常的数据库模式(或表结构),分解成多个较小、符合高一级范式要求的模式的过程。目标是减少数据冗余、避免更新异常、提高查询效率,同时保持数据的一致性和完整性。 分解过程必须谨慎执行,确保分解后的模式可以无损地连接回原始模式。这通常涉及以下步骤。 (1) 识别不良依赖: 分析现有模式中存在的部分函数依赖、传递函数依赖或多值依赖。 (2) 选择分解策略: 根据识别出的不良依赖,选择一个合适的范式作为目标,进行模式分解。 (3) 分解模式: 将原模式分解为多个较小的模式,每个模式满足目标范式的要求。 (4) 验证分解结果: 检查分解是否为无损分解,并保证分解后的模式满足所有业务规则和约束。 假设有一个简单的未规范化图书信息(书名、作者、姓名、年龄),存在传递依赖(书名→作者,姓名→年龄),可以分解为两个3NF的模式: (1) 图书(书名、作者)。 (2) 读者(姓名、年龄)。 这种分解减少了冗余,消除了传递依赖,使得每个模式都满足第三范式的要求。正确的模式分解可以提高数据库的逻辑清晰度、操作效率和数据的一致性。 6.1.4数据完整性 数据完整性是指在数据库中保持数据的准确性和可靠性的一系列约束和规则。它是数据库设计的关键组成部分,确保了数据库系统中数据的正确性、一致性和有效性。数据完整性的维护可以防止数据丢失、错误和不一致性,对于构建高质量的信息系统至关重要。以下是实现数据完整性的几种主要机制。 1. 实体完整性 实体完整性(Entity Integrity)是指每个表都应该有一个唯一的标识符,通常是主键。主键的值必须是唯一的,不允许为空(NULL),确保了表中每条记录的唯一性。这可以防止重复记录的出现,从而保持数据的准确性。 2. 参照完整性 参照完整性(Referential Integrity)是关系数据库中的一个关键概念,它要求外键值必须匹配另一表的主键值或唯一键值,或者完全为空。这种约束确保了表之间的逻辑关系得以维护,防止了因为数据不匹配或丢失而导致的不一致性。 3. 域完整性 域完整性(Domain Integrity)指的是表中每个字段值的有效性。通过定义数据类型、长度限制、默认值和可能的取值范围,可以保证数据符合预期的格式和限制,防止无效数据的输入。 4. 用户定义完整性 用户定义完整性(UserDefined Integrity)指的是根据具体的应用规则设定的数据约束。这些规则可能包括业务规则和条件,如工资不能为负数、员工的入职日期不能晚于离职日期等。这类完整性约束是根据特定业务需求定义的,超出了前面提到的基本完整性约束范畴。 数据库管理系统提供了多种机制来实现和维护数据完整性。 (1) 主外键约束: 确保实体完整性和参照完整性。 (2) 检查约束: 用于实现域完整性,确保数据符合特定的规则。 (3) 触发器(Triggers): 可以自定义复杂的业务逻辑来保持数据的一致性。 (4) 事务控制: 通过事务的ACID属性来维护数据的一致性和完整性。 保持数据完整性对于任何数据库系统都是至关重要的,它直接影响到数据的质量和系统的可靠性。设计和实现有效的数据完整性策略,需要数据库设计者、开发者和管理员之间的紧密合作。 6.2数据库设计流程 6.2.1需求分析 在数据库设计中,需求分析是一个关键的初步阶段,它的目的是明确和理解系统用户的需求,以便设计出满足这些需求的数据库系统。需求分析阶段主要包括以下几个步骤。 (1) 收集需求: 这是需求分析的第一步,涉及与系统的最终用户、管理人员以及其他相关方面的人员进行会谈,了解他们的需要和期望。在这个过程中,可能会使用问卷、访谈、观察等多种方式来收集信息。 (2) 分类和组织需求: 收集到的需求可能是杂乱无章的,需要对其进行分类和组织。可以按照需求的类型来进行分类,如数据需求、功能需求、性能需求等,也可以根据需求的重要性或其他标准来组织。 (3) 需求建模: 将收集到的需求转换成更加形式化的表示,如用例图、ER图等。有助于更清晰地理解需求,并为后续的数据库设计奠定基础。 (4) 需求验证: 确保所收集和整理的需求准确地反映了用户的实际需要。这可能涉及与用户的再次交流,以验证需求的准确性和完整性。 (5) 需求文档化: 将分析和整理好的需求制成文档,这份文档将成为后续数据库设计、实现和测试工作的基础。需求文档应该清晰、详细,易于理解,同时需要得到所有相关方的批准。 需求分析是一个迭代的过程,可能需要多次反馈和调整才能最终确定下来。它为数据库设计的后续步骤提供了方向和依据,是建立数据库系统的基石。 6.2.2概念结构设计 概念结构设计,也称为概念模型设计,是数据库设计过程中的一个关键阶段,紧随需求分析之后。在这个阶段,设计者将基于需求分析阶段收集和分析得到的信息,创建一个独立于任何数据库管理系统和技术细节的高层次数据模型。概念结构设计的主要目标是构建一个全面反映组织信息需求的抽象模型。这个模型通常使用ER(实体关系)图或类图(在面向对象的设计中)来表示。 概念结构设计主要包括以下步骤。 (1) 识别实体: 在这个步骤中,设计者需要识别出系统需求中提到的所有重要的实体。实体是现实世界中可以独立存在的事物,如人、地点、物体或事件。 (2) 确定实体属性: 为每个实体确定属性。属性是实体的特征或性质,例如,一个“读者”实体可能包括“姓名”“性别”“年龄”等属性。 (3) 定义实体间的关系: 确定实体间如何相互关联。这可能包括诸如一对一(1∶1)、一对多(1∶N)、多对多(M∶N)等关系。关系的确定有助于反映实体间的逻辑联系。 (4) 构建概念模型: 使用所识别的实体、属性和关系构建概念模型。最常用的表示方法是ER(实体关系)图,它可以直观地展示实体间的结构关系。 (5) 验证概念模型: 与用户和其他利益相关者一起审查概念模型,确保它准确地反映了需求分析阶段定义的需求。这个步骤可能需要根据反馈进行多次迭代,以修正和完善概念模型。 (6) 优化模型: 在必要时对模型进行优化,以提高其效率和实用性。这可能包括合并相似的实体、重新定义关系、调整属性等。 概念结构设计的结果是一个概念模型,它提供了一个高层次、抽象的视图,展示了系统的数据需求和数据间的关系,但不涉及任何具体的数据库系统或存储细节。这个模型是向逻辑模型和物理设计过渡的基础。 6.2.3逻辑结构设计 逻辑结构设计是数据库设计过程中的第三个阶段,紧随概念结构设计之后。在这个阶段,设计者将概念模型转换为逻辑模型,这涉及选择合适的数据库管理系统和确定数据在数据库中的组织方式。逻辑结构设计阶段的主要任务包括定义数据表、确定字段(属性)、设置键(关键字)和建立表之间的关系。这个阶段的目标是创建一个既能满足需求分析阶段的需求,又能有效利用所选数据库系统。 主要步骤如下。 (1) 选择数据库管理系统: 根据需求分析结果和概念设计的指导原则,选择适合项目需求的数据库管理系统,如openGauss、MySQL、Oracle、SQL Server、MongoDB等。 (2) 数据表设计: 基于概念模型中的实体和实体之间的关系,设计数据表。每个实体通常转换为一个数据表。 (3) 定义字段和数据类型: 为每个数据表确定需要哪些字段,并为每个字段选择合适的数据类型,如整数(INT)、字符串(VARCHAR)、日期(DATE)等。 (4) 设置主键和外键: 为每个数据表定义一个主键,来唯一标识表中的每条记录。同时,根据实体间的关系,设置外键,以实现数据表之间的关联。 (5) 规范化: 通过规范化过程,避免数据库中的数据冗余,提高数据一致性。规范化通常涉及将数据表分解成较小、互相关联的表,直到满足某个特定的规范化级别(如第三范式)。 (6) 定义视图、索引和约束。 ① 视图: 根据需要定义视图,以简化复杂查询,隐藏数据的复杂性。 ② 索引: 为提高查询性能,对一些经常查询的字段创建索引。 (7) 约束: 定义数据完整性约束,以确保数据的准确性和完整性。 逻辑结构设计是确保数据库系统高效、可靠、易维护的关键步骤。通过精心设计,可以确保数据库在性能、安全性和灵活性方面满足用户的需求。 6.2.4物理结构设计 物理结构设计是数据库设计过程中的第四个阶段,紧接着逻辑结构设计之后。在这个阶段,设计者将逻辑数据模型转换为物理存储结构,这涉及数据库的存储方式、文件组织、索引结构选择,以及数据访问路径的优化等。物理结构设计的目标是确保数据库系统在特定的硬件和软件环境下能够以最高效的方式运行。 主要步骤如下。 (1) 确定文件存储结构: 基于所选择的数据库管理系统,决定数据文件的存储方式。这可能包括表空间的划分、数据文件的放置策略,以及考虑不同类型的存储介质,如SSD与HDD。 (2) 设计索引: 确定哪些字段应该建立索引以提高查询性能。选择合适的索引类型,如B树索引、哈希索引、全文索引等,并决定复合索引的策略。 (3) 选择存储参数: 针对不同的数据库对象选择适当的存储参数。这可能包括数据块大小、填充因子、分区策略等,这些参数直接影响数据的存储效率和访问速度。 (4) 数据分区策略: 根据数据访问模式和数据量,决定是否需要对表和索引进行分区,以及选择合适的分区策略,如范围分区、列表分区、散列分区等。 (5) 设计数据安全策略: 确定数据备份和恢复策略,以确保数据的安全和可靠性。此外,还需要设计数据加密和访问控制策略,保护数据不被未授权用户访问。 (6) 优化数据访问路径: 通过分析应用程序的数据访问模式,优化数据访问路径,减少数据访问延迟,提高查询效率。这可能涉及物化视图的创建、查询重写策略的应用等。 (7) 性能调优和测试: 在实际的硬件和软件环境中对数据库进行性能测试,根据测试结果进行调优。这可能包括调整存储参数、重新设计索引结构、优化查询语句等。 物理结构设计是确保数据库性能和可靠性的关键步骤,需要考虑到实际的应用场景和硬件资源,通过合理的设计来实现数据的高效存取。这一阶段的设计决策直接影响到数据库系统的运行效率和维护成本。 6.2.5数据库实施与维护 数据库实施与维护是数据库设计和开发过程的最后阶段,但它是一个持续的过程,随着数据库的使用和组织需求的变化,这个阶段可能会持续进行调整和优化。在这个阶段,数据库从概念模型最终转换为一个完全运行的系统,需要进行安装、配置、加载数据、测试、性能优化和日常维护。以下是数据库实施与维护阶段的关键任务。 1. 数据库实施 (1) 数据库安装和配置: 根据逻辑结构设计和物理结构设计的要求,安装数据库管理系统软件,并进行相应的配置,以确保系统运行在最佳状态。 (2) 创建数据库结构: 根据逻辑结构设计阶段的输出,创建数据库中的表、视图、索引、触发器、存储过程等数据库对象。 (3) 数据迁移和加载: 如果是改进或替换现有系统,可能需要从旧系统迁移数据到新系统。这涉及数据的清洗、转换和加载过程。 (4) 测试: 进行全面的测试,包括单元测试、系统测试和性能测试,以确保数据库系统满足设计要求,并且在各种情况下都能正常运行。 2. 数据库维护 (1) 性能监控和优化: 定期监控数据库的性能,包括查询速度、事务处理能力和存储效率。根据监控结果调整索引、重新组织数据文件和调优查询语句,以保持系统的高性能。 (2) 备份和恢复: 实施定期的数据备份策略,以防止数据丢失。同时,确保有有效的恢复计划,以便在数据丢失或系统故障时迅速恢复。 (3) 安全管理: 管理用户访问权限,确保数据安全。定期更新系统和应用程序,以防止安全漏洞。 (4) 数据完整性和一致性维护: 确保数据的准确性和一致性,包括定期检查和修复数据完整性问题。 (5) 变更管理: 随着业务需求的变化,数据库可能需要进行调整。变更管理包括需求评估、设计变更、测试和部署,确保变更过程有序进行,减少对业务的影响。 数据库实施与维护是确保数据库长期有效支持组织运营的关键环节。通过持续的监控、优化和更新,可以确保数据库系统能够适应不断变化的业务需求和技术环境。 6.3图书借阅管理系统数据库设计 设计一个图书借阅管理系统的数据库涉及定义图书、读者、借阅记录等实体的数据,以及这些实体之间的关系。下面是一个简化的设计流程和数据库结构示例,旨在满足基本的图书借阅管理需求。 1. 需求分析 (1) 存储图书信息,如编号、书名、作者、出版社、出版时间等字段。 (2) 存储读者信息,如编号、姓名、性别、年龄、联系方式等字段。 (3) 存储记录借阅记录,如编号、借阅时间、归还时间、借阅状态等字段。 2. 概念结构设计 在这一步,可以使用ER(实体关系)模型来表示实体及其关系。 实体: 图书、读者、借阅记录。 关系: 读者与图书之间的借阅关系、读者与借阅记录之间的关系、图书与借阅记录之间的关系。 (1) 一本尚未被借阅的图书只可以借阅给一个用户,每个用户可以同时借阅多本尚未被借阅的图书。 (2) 一条借阅记录只记录一本图书的借阅信息,一本图书可以有多条借阅记录。 (3) 一个读者可以有多条借阅记录,一条借阅记录只能记录一个用户的借阅情况。 接下来通过一个ER图来描述实体以及关系,具体如图61所示。 图61ER图 3. 逻辑结构设计 将概念模型转换为逻辑模型,定义数据表及其字段。 1) 图书表(books) 编号: book_id 书名: title 作者: author 出版社: publisher 出版时间: publish_date 2) 读者表(readers) 编号: reader_id 姓名: name 性别: gender 年龄: age 联系方式: contact 3) 借阅记录表(borrowrecords) 编号: borrow_id 借阅时间: borrow_date 归还时间: return_date 状态: status 外键字段: book_id 外键字段: reader_id 4. 物理结构设计 这里主要演示为字段设置索引,原则是选择频繁查询的条件字段作为索引。由于经常需要根据书名来搜索图书,因此可以为图书表的title字段加上索引。同理,也可以为读者表的name字段加上索引。 5. 数据库实施 接下来选择指定的数据库管理系统openGauss创建数据库和数据表,然后设置数据类型和约束,并且插入一些初始数据进行测试。 1) 创建books表 1.CREATE TABLE books ( 2. book_id SERIAL PRIMARY KEY, 3. title VARCHAR(255) NOT NULL, 4. author VARCHAR(100) NOT NULL, 5. publisher VARCHAR(100), 6. publish_date DATE 7.); 8.CREATE INDEX idx_book_title ON books(title);-- 在 title 字段上创建索引 2) 创建readers表 1.CREATE TABLE readers ( 2. reader_id SERIAL PRIMARY KEY, 3. name VARCHAR(100) NOT NULL, 4. gender VARCHAR(10), 5. age INT, 6. contact VARCHAR(20) 7.); 8.CREATE INDEX idx_reader_name ON readers(name);-- 在 name 字段上创建索引 3) 创建borrowrecords表 1.CREATE TABLE borrowrecords ( 2. borrow_id SERIAL PRIMARY KEY, 3. borrow_date DATE, 4. return_date DATE, 5. status VARCHAR(10), 6. book_id INT, 7. reader_id INT, 8. FOREIGN KEY (book_id) REFERENCES Book(book_id), 9. FOREIGN KEY (reader_id) REFERENCES Reader(reader_id) 10.); 4) 向books表添加数据 1.INSERT INTO books (title, author, publisher, publish_date) VALUES 2.('The Great Gatsby', 'F. Scott Fitzgerald', 'Scribner', '1925-04-10'), 3.('To Kill a Mockingbird', 'Harper Lee', 'J.B. Lippincott & Co.', '1960-07-11'), 4.('1984', 'George Orwell', 'Secker & Warburg', '1949-06-08'), 5.('Pride and Prejudice', 'Jane Austen', 'T. Egerton, Whitehall', '1813-01-28'), 6.('The Catcher in the Rye', 'J.D. Salinger', 'Little, Brown and Company', '1951-07-16'); 5) 向readers表添加数据 1.INSERT INTO readers (name, gender, age, contact) VALUES 2.('Alice', 'Female', 25, 'alice@example.com'), 3.('Bob', 'Male', 30, 'bob@example.com'), 4.('Charlie', 'Male', 22, 'charlie@example.com'), 5.('Diana', 'Female', 28, 'diana@example.com'), 6.('Eve', 'Female', 35, 'eve@example.com'); 6) 向borrowrecords表添加数据 1.INSERT INTO borrowrecords (borrow_date, return_date, status, book_id, reader_id) VALUES 2.('2024-03-01', '2024-03-15', 'Returned', 1, 1), 3.('2024-03-05', '2024-03-20', 'Returned', 2, 2), 4.('2024-03-10', NULL, 'Borrowed', 3, 3), 5.('2024-03-15', NULL, 'Borrowed', 4, 4), 6.('2024-03-20', '2024-04-05', 'Returned', 5, 5); 6. 维护和优化 还需要根据实际使用情况对数据库进行监控和优化。同时还需要进行定期备份数据,确保数据安全。随着需求的变化,还会调整数据库结构。 目前学习的数据库设计只是一个基本的框架,在实际开发的过程中,还会根据具体需求调整某些表的结构。同时,在设计数据库时,还应考虑数据完整性、安全性和性能优化。 小结 本章首先针对数据库设计方法以及范式理论进行讲解,然后讲解了数据库设计流程,其中包括需求分析、概念结构设计、逻辑结构设计、物理结构设计、数据库实施与维护等。整体而言,数据库设计不仅是创建一个数据库结构的过程,它也是确保数据正确、安全和高效访问的关键。通过遵循数据库设计的最佳实践和原则,可以构建出既满足当前需求又具有足够灵活性以适应未来变化的数据库系统。 习题 1. 请简要说明数据库范式包括哪些。 2. 请简要说明数据库的设计流程。