9.代码/SQL语句分析并发
【说明】
某航空售票系统负责所有本地起飞航班的机票销售,并设有多个机票销售网点。以下为E-SQL编写的部分售票代码:
……
EXEC SQL SELECT balance INTO :x FROM tickets WHERE flight=:flightno;
Printf(“航班%s当前剩余机票数为:%d\n请输入购票数:”,flightno,x);
Scanf(“%d”,&a);
EXEC SQL UPDATE tickets SET balance=:x-:a WHERE flight=:flightno;
请根据上述描述,完成下列问题:
【问题1】(5分)
上述售票程序,在并发状态下,可能发生什么错误?产生这种错误的原因是什么?
EXEC SQL SELECT balance INTO :x FROM tickets WHERE flight = :flightno;
-- 把剩余票数读到变量 x
printf("航班%s当前剩余机票数为:%d\n请输入购票数:", flightno, x);
scanf("%d", &a);
-- 用户输入购买数量 a
EXEC SQL UPDATE tickets SET balance = :x - :a WHERE flight = :flightno;
-- 用 x - a 更新剩余票数注意题目关键词,在并发状态下,我们看到的这个执行SQL语句的脚本,它并不是同时只有一个人在操作。而是有多个事务。
在多个人同时购买的情况下,多个事务读取到相同的剩余票数这个初值,然而根据后购买的人计算得出的剩余票数会覆盖掉先购买的人计算得出的剩余票数。产生了丢失更新错误。

比如图中,两个人同时读取到剩余票数16,T1先购买一张,剩余15,T2也是基于初值16购买2张剩余14。最终剩余的数量是14,凭空丢了一张票。
【问题2】(6分)
代码封装事务/加锁分析
若将上述代码封装成一个完整的事务,则:
(1)在并发请求下的响应效率会存在什么问题?
(2)分析产生效率问题的原因。
并发响应效率低,大量事务阻塞等待。
原始代码有三条独立的 SQL,每条语句都是一个自动提交的独立事务。"封装成一个完整的事务"指的就是:
BEGIN TRANSACTION;
SELECT balance INTO :x ...;
// 用户输入
UPDATE tickets SET ...;
COMMIT;隐含的意思就是:为了解决问题1(丢失更新),我们将代码封装成事务,并加上适当的锁
丢失更新的问题是,多个事务读到的都是初始值,那么基于这个初始值做计算,肯定会出问题。我们只给UPDATE加写入的锁,是没用的。
所以我们必须要阻止其他人在你"计算期间"读到旧值。不可以加S锁,不可以让其他人读,必须加X锁。这个事物已经持有X锁了,后续就不用考虑什么加锁的问题了。
我们前面已经学习分析过,BEGIN TRANSACTION;之后,要么是commit释放,要么是rollback释放。锁一直不释放,事务持有锁时间包括了用户操作,过于长。其他售票网点的操作被阻塞。
(3)给出解决方案。
将事务适当划分,用户查询置放于外部事务,该事务只负责购票。
查询阶段 SELECT → 显示给用户 → 用户输入 ❌ 不在事务里
更新阶段 SELECT FOR UPDATE → 校验 → UPDATE ✅ 在事务里
第一次 SELECT 只是给用户看看查询时大概还剩多少,第二次 SELECT FOR UPDATE 才是真正锁住、确认、更新的依据。
【问题3】(4分)
下面是改写的存储过程,其中flightno为航班号;a为购票数;result为执行状态:1表示成功,0表示失败;表tickets中的剩余机票数balance具有大于等于零约束。请补充完整。
CREATE PROCEDURE buy_ticket(char[] flightno IN, (a) , int result OUT)
AS
BEGIN
.........
UPDATE tickets SET balance= (b)
WHERE flight= flightno;
if (SQLcode<>SUCCESS) { //SQLcode为SQL语句的执行状态
(c) ;
result = 0; return;
}
COMMIT;
(d)
ENDCREATE PROCEDURE buy_ticket(char[] flightno IN, (a) , int result OUT)
题目:flightno为航班号;a为购票数;result为执行状态。从整个题目中都没有找到(a)这个缺失的传入参数,从业务需求分析应该是购票数。
注意:不要忘了存储过程的参数,有参数类型、参数名、参数模式。
还有的题顺序不一样,这里我们参考其他选项。
所以 (a) 填:int a IN
购票数没有传出、返回的需求
UPDATE tickets SET balance=(b)
不论里面的判断条件是什么,这里都是要更新剩余票数
所以 (b) 填:balance - a
1表示成功,0表示失败
if (SQLcode<>SUCCESS) { //SQLcode为SQL语句的执行状态
(c) ;
result = 0; return;
执行失败了,要能联想到回滚事务,所以 (c) 填:ROLLBACK
观察程序结构,已有commit,已有rollback,既然有执行失败的result = 0,还差执行完毕的result = 1。且其他语句有return的题这里也需要添加。
(d)result=1; return;
10.范式分析
【说明】
一汽车厂商对配件进行统一管理,设计了相应的数据库,其中一个表记录了维修配件的使用信息。其表结构如下:
维修配件使用表(车牌号,维修时间,配件编码,配件名称,配件供应商,配件仓库编码,仓库地址,维修配件数量)
其中,车牌号和配件编码满足唯一性。假设同一辆车在同一次维修情况下可能需要多种维修配件;一种配件只能存放于一个配件仓库,一种配件只能由一个配件供应商提供。维修时间精确到秒。
【问题1】(7分)
题中给出的维修配件使用表存在数据冗余,请给出具体的冗余属性并说明因此会出现哪些异常?
"冗余属性"这个概念正是以范式理论为标尺来定义的。
只要是不符合范式设计要求的,都应该列为数据冗余。
所有范式分析题,先搞清楚表是描述什么的,然后确定依赖。
这题要先读完搞明白这个表是做什么用的,它是记录了某一次维修的信息:某车在某时间维修的时候用了什么配件,用了多少...
京A12345, 2025-05-10, P001, 刹车片, ...
京A12345, 2025-05-10, P002, 机油滤芯, ...首先一眼能看出来的依赖有:
配件编码→(配件名称,配件供应商,配件仓库编码)
“一种配件只能存放于一个配件仓库,一种配件只能由一个配件供应商提供”
配件仓库编码→仓库地址
题目有个不严谨的地方,题目说:车牌号和配件编码满足唯一性,很显然同一辆车用同一个配件,存在不同时间多次维修的情况。
应该认为(车牌号, 维修时间, 配件编码)具有唯一性,是主键
还剩维修配件数量,这应该是一个数值,我们精确到某一次维修,用了多少配件数量才是不重复的,(车牌号, 维修时间, 配件编码)
(车牌号,维修时间,配件编码)→维修配件数量
接下来就可以分析依赖问题和冗余属性了,非主属性有:
配件名称,配件供应商,配件仓库编码,仓库地址,维修配件数量
补充:主属性指的是候选键里的属性,不是仅指主键里的属性。
配件编码 → 配件名称,是非主属性对候选键(主键)的部分依赖
配件编码 → 配件供应商,部分依赖
配件编码 → 配件仓库编码 部分依赖
配件编码 → 配件仓库编码→ 仓库地址
(车牌号, 维修时间, 配件编码) → 维修配件数量 完全依赖✅
因此冗余属性:配件名称,配件供应商,配件仓库编码,仓库地址
插入异常:有新人(新数据)没“主键”领不进来(主键缺失)。
删除异常:删掉特定数据,顺带把其他信息也删没了(信息丢失)。
更新异常:改一个事实,要改 N 个地方,漏了就乱(修改复杂、数据不一致)。
只要在分析中发现了部分依赖或传递依赖,就可以立刻“锤定”该表存在插入、删除和更新三种异常,无一例外。
出现的异常有:存在插入异常,修改异常和删除异常。
【问题2】(8分)
维修配件使用表是否满足BCNF?如果不满足,请对其进行模式分解,使分解后的关系模式满足BCNF,并标记出主键和外键。
由于存在传递依赖和部分依赖,不满足
对于模式拆分的题目,我们统一需要搞清楚:主键是什么?依赖是什么?这些我们在第一问中已经分析。
配件编码 → (配件名称, 配件供应商, 配件仓库编码)
括号里的三个属性,都只由 配件编码 决定,“一种配件只能由一个配件供应商提供”、“只能存放于一个配件仓库”,这也叫1对1关系。
如果还出现了其他的东西,那就说明出现了部分依赖,那就说明出现了冗余,我们拆分模式就是要把冗余丢出去
模式1️⃣:把决定因素 配件编码 作为新表的主键,其余属性都由这个主键决定
配件表:配件(配件编码, 配件名称, 配件供应商, 配件仓库编码)
主键:配件编码
外键:配件仓库编码,它是引用下面仓库表
依据:配件仓库编码 → 仓库地址 这个依赖。
把决定因素 配件仓库编码 作为新表的主键,
仓库(配件仓库编码, 仓库地址)
主键:配件仓库编码,没有外键
原来的大表只剩下了主键和被主键完全决定的属性。
(车牌号,维修时间,配件编码)→维修配件数量
维修记录表(车牌号, 维修时间, 配件编码, 维修配件数量)
主键是(车牌号,维修时间,配件编码),外键是配件编码,引用自配件表