简介:一份完整的 SQL 数据库图书管理系统课程设计文档,面向数据库初学者、高校信息管理相关专业学生,尤其适合正在完成课程设计或毕业设计的读者。资源以图书馆真实管理场景为背景,围绕读者信息、图书信息、操作员信息三大模块,覆盖图书借阅、还书、超期罚款等业务流程,系统讲解从数据库存储设计、E-R 图、数据字典、关系模式设计到 SQL 语句实现的完整链路。文档还包含了详细的查询描述、关系代数表达和部分 SQL 查询结果,便于理解图书管理系统的表结构逻辑与增删改查写法。资源仅 1 个 doc 文件,压缩包大小 709KB,轻量易下载,打开即可对照学习和二次开发。目前已吸引 2317 人学习下载,是课程设计报告撰写的实用参考。
1. 图书管理系统背后的SQL数据库设计:一份课程设计报告能复现什么
图书馆管理系统是数据库课程设计里出现频率最高的题目,但大部分流传出来的资料只有零散脚本,能跑通的不多。这份《SQL数据库图书管理系统(完整代码)》是一份完整的设计报告外加可执行代码,来自广西交通职业技术学院信息工程系的课程设计,包含六张业务表、完整的外键约束、初始化的借阅数据和罚款记录,覆盖了从E-R图设计到SQL实现再到结果验证的全流程。对正在做数据库课程设计、或者想快速搭一套图书借阅管理原型的人来说,这份资源的价值在于:它把系统分析、数据字典、关系模式、SQL建库建表、数据初始化、查询验证这条路完整走了一遍,拿到手可以直接在SQL Server里执行,不需要自己从头设计表结构。适合三类人:一是正在写数据库课程设计报告的学生,二是想参考小型业务系统表结构设计的开发新手,三是需要一份带外键约束和业务状态管理的SQL范例的从业者。我把这套代码完整拆了一遍,下面是表结构、建表语句、初始化脚本和踩坑点的逐项分析。
2. 六张表怎么设计出来的:E-R图、数据字典与关系模式
2.1 实体划分与E-R图背后的业务推导
图书管理系统这个问题的难点不在SQL语法,而在业务实体怎么划分。这份报告把系统拆成六个实体:书籍类别、读者、书籍、借阅记录、还书记录、罚款记录。这个划分对应的是图书馆日常业务的三条主线——管书、管人、管借还。
书籍类别和书籍是主从关系,一个类别下面挂多本书,所以书籍表里放了一个外键bookstyleno指向类别表。读者和借阅记录、还书记录是一对多的关系,一个读者可以借多本书。罚款记录本质上是由还书超期派生出来的业务数据,注意看它的字段设计:readerid、readername、bookid、bookname、bookfee、borrowdate,这里冗余了读者姓名和书籍名称,这种冗余在小系统里是合理的,因为罚款单打印时需要直接显示这些信息,省去每次关联查询。
从这个设计能看出来,做数据库设计第一步不是画表,而是把业务过程捋清楚:读者登记、借书、还书、超期罚款,每个环节产生什么数据,数据之间怎么关联。E-R图在这里起的作用是沟通工具,让人一眼看清实体之间的关系,而不是为了凑报告页数。
2.2 数据字典里值得注意的字段设计
数据字典定义了每张表的字段、类型、是否为空和主外键约束。我挑选几个关键点:
| 表名 | 字段 | 类型 | 约束 | 设计意图 |
|---|---|---|---|---|
| book_style | bookstyleno | varchar(30) | 主键 | 类别编号,业务上可能是"1"、"2"这种短编码 |
| system_books | bookid | varchar(20) | 主键 | 书籍编号,示例数据用"901"、"902"三位数 |
| system_books | isborrowed | varchar(2) | 非空 | 是否被借出,用"1"和"0"表示,字符类型而非bit |
| system_readers | readerid | varchar(9) | 主键 | 借书证编号,学生号、教师号、管理号前缀不同 |
| borrow_record | bookid | varchar(20) | 主键兼外键 | 一本书同一时间只能有一条借出记录 |
| return_record | bookid | varchar(20) | 主键兼外键 | 还书记录,同样是一本书一条 |
| reader_fee | bookfee | varchar(30) | 无约束 | 罚款金额,注意这里用了字符类型 |
有两个细节值得展开说。
第一个是borrow_record和return_record的主键设计。两张表都用bookid做主键,这意味着同一本书在同一张表里只能出现一次。从业务角度理解,这个设计隐含了一个假设:一本书同一时间只能被一个人借走,所以借阅记录表里一本书一条记录就够了。还书也一样,一本书归还一次就销掉一条记录。这在小型图书馆场景下是成立的,但如果做大型系统,同一本书有多个副本,就需要用流水号或者复合主键来区分。
第二个是isborrowed字段的设计。这个字段在报告的数据字典里标的是varchar(2),初始化的数据里直接用"1"表示已借出,"0"表示可借。用字符类型而不是bit类型,这种选择在课程设计里很常见,因为教学环境里bit类型的显示和操作对学生来说不够直观,而且varchar(2)写起来灵活,做演示效果更直观。我会在后面的初始化数据部分详细说这个字段是怎么配合更新逻辑的。
2.3 关系模式与关系代数的落地价值
报告里给出了六个关系模式,形式上就是表结构的抽象描述。在实际开发中,关系模式最大的用途是检查逻辑是否闭环。比如罚款表的关系模式里出现了两个借书证编号,一个是读者信息,一个是借阅信息,这其实指向借阅时间的获取来源——罚款金额需要根据借书日期和还书日期计算超期天数,所以borrowdate在罚款表里是必要的。
报告里专门提到用关系代数进行运算得到所需结果。关系代数的选择、投影、连接操作,对应到SQL里就是select、where、join。对于这份资源来说,关系代数部分主要用于报告撰写,上机实现看的是SQL语句。我的建议是:如果时间紧张,优先把SQL跑通,关系代数可以放在文档里对照着写,因为评分看的是逻辑正确性,不是看你用了哪种运算符号。
3. 建库建表与外键约束:从CREATE DATABASE到六张核心表的落地
3.1 创建数据库的参数细节
这份资源的可执行脚本从建库开始,以下是完整代码:
USE master GO CREATE DATABASE tangzhangsentsg ON ( NAME = librarysystem, FILENAME = 'c:\librarysystem.mdf', SIZE = 10, MAXSIZE = 50, FILEGROWTH = 5 ) LOG ON ( NAME = 'library_log', FILENAME = 'c:\librarysystem_log.ldf', SIZE = 5MB, MAXSIZE = 25MB, FILEGROWTH = 5MB ) GO这段脚本有几个参数需要实际动手时调整。SIZE = 10表示初始大小10MB,MAXSIZE = 50限制最大50MB,FILEGROWTH = 5表示每次自动增长5MB。对课程设计这个量级的数据来说,10MB初始空间绰绰有余,即便插入几百条记录也用不到1MB。但要注意FILENAME的路径,原文写的是'c:',这在大部分机器上会因为权限问题建库失败,我一般会改成当前实例的数据目录,或者直接用默认路径。
另一个坑是数据库名。tangzhangsentsg是作者名字拼音加缩写命名,这个库名在实际项目里没法复用,建议改成library_db之类有业务含义的名字。CREATE DATABASE语句里的ON和LOG ON分别定义数据文件和日志文件的属性,数据文件存表数据,日志文件存事务日志。如果只写ON不写LOG ON,SQL Server会默认创建一个1MB的日志文件,但显式指定大小更可控。
数据库创建完成后,后续所有建表语句都要确保当前数据库是tangzhangsentsg,否则表会建到master库里。常见做法是在建表脚本最前面加USE tangzhangsentsg,或者手动在SSMS左上角下拉框切换。
3.2 六张表的建表语句与外键链
以下是书籍类别表、书籍表、读者表、借阅记录表、还书记录表、罚款记录表的完整建表代码:
-- 书籍类别表:主键是类别编号,类别名称非空 CREATE TABLE book_style ( bookstyleno varchar(30) PRIMARY KEY, bookstyle varchar(30) ) GO -- 书籍表:主键是书籍编号,外键关联类别表 CREATE TABLE system_books ( bookid varchar(20) PRIMARY KEY, bookname varchar(30) NOT NULL, bookstyleno varchar(30) NOT NULL, bookauthor varchar(30), bookpub varchar(30), bookpubdate datetime, bookindate datetime, isborrowed varchar(2), FOREIGN KEY (bookstyleno) REFERENCES book_style(bookstyleno) ) GO -- 读者表:主键是借书证编号,姓名和性别非空 CREATE TABLE system_readers ( readerid varchar(9) PRIMARY KEY, readername varchar(9) NOT NULL, readersex varchar(2) NOT NULL, readertype varchar(10), regdate datetime ) GO -- 借阅记录表:书籍编号是主键也是外键,读者编号是外键 CREATE TABLE borrow_record ( bookid varchar(20) PRIMARY KEY, readerid varchar(9), borrowdate datetime, FOREIGN KEY (bookid) REFERENCES system_books(bookid), FOREIGN KEY (readerid) REFERENCES system_readers(readerid) ) GO -- 还书记录表:结构与借阅记录表对应 CREATE TABLE return_record ( bookid varchar(20) PRIMARY KEY, readerid varchar(9), returndate datetime, FOREIGN KEY (bookid) REFERENCES system_books(bookid), FOREIGN KEY (readerid) REFERENCES system_readers(readerid) ) GO -- 罚款记录表:书籍编号是主键也有外键约束 CREATE TABLE reader_fee ( readerid varchar(9) NOT NULL, readername varchar(9) NOT NULL, bookid varchar(20) PRIMARY KEY, bookname varchar(30) NOT NULL, bookfee varchar(30), borrowdate datetime, FOREIGN KEY (bookid) REFERENCES system_books(bookid), FOREIGN KEY (readerid) REFERENCES system_readers(readerid) ) GO先说书名为首的system_books表。它的外键指向book_style表,建表顺序有讲究,必须先建被引用的表book_style,再建引用它的system_books,否则SQL Server会报外键引用的对象不存在。同理,borrow_record和return_record都引用了system_books和system_readers,所以这两张表必须排在读者表和书籍表之后。reader_fee表虽然字段最多,但它依赖的system_books和system_readers已经建好,放在最后没有问题。这个建表顺序是这类资源的第一个隐藏考察点,很多人拿到脚本按从上到下执行,前面没问题,到borrow_record就报错,其实是前面的表还没建。
再看字段类型的选择。所有编号字段都是varchar,不是int或bigint。这个设计符合业务习惯——借书证编号可能有前缀字母Q、GL、201005这样的混合内容,用数字类型会丢信息。书籍编号"901"看起来是数字,但未来如果要加字母编号,varchar才能兼容。日期字段用datetime,金额字段bookfee用varchar(30),这个后面在避坑章节我会专门说。
外键约束方面,borrow_record的readerid允许为空,因为SQL Server默认允许外键列存NULL。这在业务里意味着可以插入一条只有bookid、没有readerid的借阅记录,这在逻辑上说不通。实际项目里应该把readerid改成NOT NULL,至少加个检查约束。这是这个设计的薄弱点,但不影响课程设计的演示效果。
3.3 主键选择的取舍:为什么用业务主键而不是自增ID
这里值得思考一个问题:为什么所有表都用业务字段做主键,而不是加一个自增的id列?比如书籍表用bookid做主键,读者表用readerid做主键,借阅记录表用bookid做主键。
用业务主键的好处是查询直观,不需要额外的索引查找。比如要知道一本书的借阅历史,直接where bookid = '901'就能命中主键索引,速度很快。对于数据量在几千条级别的课程设计系统,这种方式没有任何性能问题。
坏处是业务主键对变化不敏感。如果一本书的编号规则变了,比如从三位数变成带字母的五位编码,主键改动会影响所有外键引用它的表。自增ID的好处是物理主键和业务编号解耦,无论业务编号怎么变,主键都不用动。但在课程设计里,业务主键展示起来更直观,评分老师一眼能看懂bookid就是书籍编号,不需要解释id=1对应哪本书。
我给一个实用建议:如果你后续要把这个系统扩展成真正可用的图书管理系统,书籍表要加副本数量和唯一标识码,比如ISBN或者馆藏条码,这时候再用图书id做唯一主键更合适。课程设计阶段保持原样没问题,理解这个取舍就够了。
4. 数据初始化与借阅状态维护:INSERT和UPDATE的配合逻辑
4.1 基础档案数据的加载
表建好后第一步是加载基础数据。先往book_style表插入七种书籍类别,再往system_books表插入八本示例图书。代码如下:
INSERT INTO book_style(bookstyleno, bookstyle) VALUES('1', '修真小说') INSERT INTO book_style(bookstyleno, bookstyle) VALUES('2', '穿越小说') INSERT INTO book_style(bookstyleno, bookstyle) VALUES('3', '恐怖小说') INSERT INTO book_style(bookstyleno, bookstyle) VALUES('4', '都市小说') INSERT INTO book_style(bookstyleno, bookstyle) VALUES('5', '科幻小说') INSERT INTO book_style(bookstyleno, bookstyle) VALUES('6', '仙侠小说') INSERT INTO book_style(bookstyleno, bookstyle) VALUES('7', '言情小说') GO INSERT INTO system_books( bookid, bookname, bookstyleno, bookauthor, bookpub, bookpubdate, bookindate, isborrowed ) VALUES ( '901', '飘渺之旅', '1', '萧潜', '鲜网', '2005-09-01', '2013-05-25', '1' ) INSERT INTO system_books( bookid, bookname, bookstyleno, bookauthor, bookpub, bookpubdate, bookindate, isborrowed ) VALUES ( '902', '唐朝好男人', '2', '多一半', '新星出版社', '2008-05-09', '2013-05-26', '1' ) -- 后续书本记录的插入格式相同,这里省略中间四条 INSERT INTO system_books( bookid, bookname, bookstyleno, bookauthor, bookpub, bookpubdate, bookindate, isborrowed ) VALUES ( '908', '步步惊心', '2', '桐华', '民族出版社', '2006-06-20', '2013-05-30', '1' ) GObook_style的插入没有悬念,就是七条固定记录。system_books插入时需要特别注意isborrowed字段,初始值全部是'1',表示这批书默认都是已借出状态。为什么初始状态是已借出而不是可借?因为后面的借阅记录表里马上会插入对应的借书记录,这些书确实都已经被借走了,所以初始值是'1'是符合业务事实的。
日期字段的值用字符串格式'2005-09-01'直接插入datetime类型列,SQL Server会自动做隐式转换。这个转换依赖数据库的日期格式设置,在中文版SQL Server默认配置下没有问题。如果遇到日期转换报错,常见做法是用CONVERT函数显式转换,格式如CONVERT(datetime, '2005-09-01', 120)。
4.2 借阅记录与状态同步的UPDATE联动
接下来是这套系统里最核心的一个逻辑:插入借阅记录后,同步把书籍表里的isborrowed字段从'1'改成'0'。代码原文如下:
INSERT INTO borrow_record(bookid, readerid, borrowdate) VALUES('901', 'Q001', '2013-01-18 12:20') UPDATE system_books SET isborrowed = '0' WHERE bookid = '901' AND isborrowed = '1'这里有个反直觉的地方:初始时isborrowed = '1'表示已借出,插入借阅记录后UPDATE system_books SET isborrowed = '0',那'0'表示什么?
我按这个逻辑推一遍:首先往system_books插入图书时把isborrowed设为'1',紧接着往borrow_record插入借阅记录,然后update把isborrowed改为'0'。如果'1'表示已借出,'0'表示未借出,那么在插入借阅记录之前的瞬间,这本书的isborrowed是'1',状态是已借出——但此时borrow_record里并没有对应的记录。这说不通。
再换一个理解方向:'1'表示"这本书在馆内(未被借出)",插入借阅记录后改成'0'表示"这本书已借出"。初始全部为'1'表示所有书都在馆内,然后依次插入借阅记录,每借出一本书就把状态改成'0'。这个解释和代码行为完全吻合。即isborrowed = '1'代表未借出(可借),isborrowed = '0'代表已借出(不可借),和注释里写的"将已借出的借阅标记置0"对应。
理解这个逻辑很重要,因为后面做查询时,如果你想查"哪些书还在馆内",条件是isborrowed = '1'而不是'0'。这个字段的语义和直觉相反,我在排错部分会再提到一次,这是这套代码里最容易搞反的地方。
UPDATE语句带AND isborrowed = '1'这个条件,作用是防止重复更新。如果一本书的isborrowed已经是'0',说明它已经被借出,此时再插入一条借阅记录然后执行UPDATE,条件不满足,不会更新。这算是一个简易的幂等保护,虽然它不能阻止重复的借阅记录插入,但至少状态不会被二次改写。实际项目里应该用事务把INSERT和UPDATE包起来,保证两步操作要么都成功要么都失败,课程设计代码没有用事务,这是一个可以改进的点。
4.3 读者数据的多样性和借阅记录的分布
读者表的数据体现了借书证编号的多样性:Q001、Q002这种纯字母加数字的面向学生,201005、201006纯数字的面向教师,GL001带管理含义的面向管理员。这个设计说明借书证编号是分前缀规则的,有了readertype字段做辅助说明,学生、教师、管理三种角色区分清楚。
借阅记录也按照不同读者分布:学生读者借了前五本,教师读者借了后两本,管理员的GL001没有借阅记录。这样设计的好处是后续做查询演示时,可以用读者类型做分组统计,比如按readertype统计借阅数量,结果会自然呈现出学生比教师借书多的数据分布。
到这里,基础数据和业务数据都加载完了。下一章我直接说坑点,因为这套代码的执行过程有几个地方第一次跑的时候非常容易翻车。
5. 避坑指南:外键、主键和初始化数据里的四个典型翻车点
5.1 数据库文件路径导致建库失败
现象:执行CREATE DATABASE语句时报错,错误信息类似"操作系统拒绝了对路径的访问"或者"文件无法创建"。
原因:原文中FILENAME指定为'c:',在Windows Vista之后的系统上,普通用户对C盘根目录没有写权限,SQL Server服务账户无法在C盘根目录创建.mdf和.ldf文件。
解决:把FILENAME改成SQL Server默认数据目录。可以先执行SELECT physical_name FROM sys.master_files WHERE name = 'master'查出主数据文件的路径,然后把建库脚本里的路径改成这个目录下,例如FILENAME = 'C:\Program Files\Microsoft SQL Server\MSSQL15.MSSQLSERVER\MSSQL\DATA\librarysystem.mdf'。如果不想手改路径,更省事的做法是直接删掉FILENAME这一行,让SQL Server用默认路径建库。
5.2 表依赖顺序导致外键创建失败
现象:按文档顺序执行建表脚本,到borrow_record表时报错"外键'FK__borrow_re__booki__...'引用了无效的表system_books"或者类似提示。
原因:SQL Server要求外键引用的父表必须先存在。borrow_record的bookid外键依赖system_books,readerid外键依赖system_readers,如果这两张表还没创建就执行borrow_record的建表语句,必然报错。
解决:严格执行创建顺序——先建book_style,再建system_books和system_readers,然后建borrow_record和return_record,最后建reader_fee。如果你用的是SSMS的脚本执行窗口,建议把这些建表语句分段执行,每段执行后用SELECT name FROM sys.tables确认表已经创建成功再继续。还有一种粗暴但有效的办法:如果已经建了一些表但报错了,可以先把所有表drop掉重新来,用DROP TABLE IF EXISTS逐张清理,注意先删子表再删父表。
5.3 isborrowed字段语义反转导致查询结果错乱
现象:按直觉写SELECT * FROM system_books WHERE isborrowed = '1'想查"已借出的书",结果查出来的是全部八本书,跟预期完全相反。
原因:这套代码里isborrowed的语义是'1'表示未借出(在库),'0'表示已借出。初始插入时为'1',插入借阅记录后UPDATE改为'0'。很多人按字段字面意思理解,以为'1'是已借出,结果查出来的数据恰好是反的。
解决:在查询前先做一步数据验证。执行SELECT bookid, bookname, isborrowed FROM system_books,对照borrow_record里的记录检查:有借阅记录的书,isborrowed应该是'0';没有借阅记录的书,isborrowed应该是'1'。如果一致,说明状态维护正常。这个确认动作花不了十秒钟,但能避免后续所有查询都基于错误理解。我在第一次跑这套代码时就翻了车,用'1'查在架图书结果查出了全部书籍,后来对着borrow_record逐条比对才反应过来。
5.4 NOT NULL约束挡住非法数据
现象:向system_readers表插入一行没有姓名的读者记录时报错,提示"不能将值NULL插入列readername"。
原因:readername字段在建表时定义了NOT NULL约束,业务上要求每个读者必须有姓名。课程设计报告里对读者信息的定义就是借书证编号、读者姓名、读者性别三项必填。
解决:按约束补全必填字段。这也是数据库设计的目的之一——在应用层没做校验的情况下,数据库约束是最后一道防线。如果你在做自己的系统,建议在应用层也做同样的非空校验,让用户在界面上就填不了空值,而不是等到数据库报错。另外注意,readertype和regdate两个字段允许为空,如果插入时省略这两个值不会报错,比如INSERT INTO system_readers(readerid, readername, readersex) VALUES('X001', '张三', '男')是可以执行的。
6. 用查询验证系统闭环:把报告里的SQL变成可检查的功能点
文档的最后一个部分是结果数据处理,用单表查询演示。实际复现时,我建议不只是跑一句SELECT * FROM book_style看结果,而是把整个系统的业务逻辑用查询串起来验证一遍,确认六张表的数据是自洽的。
最值得先跑的是借阅状态验证查询:
SELECT b.bookid, b.bookname, b.isborrowed, br.readerid, br.borrowdate FROM system_books b LEFT JOIN borrow_record br ON b.bookid = br.bookid WHERE b.isborrowed = '0'这条语句能查出所有已借出的书籍及其借阅人、借阅日期。如果join出来的结果和borrow_record里的记录一一对应,说明前面的INSERT和UPDATE联动没有漏执行。LEFT JOIN保证即使某本书没有借阅记录也会出现在结果集里,便于排查数据初始化的疏漏。
第二个建议跑的是多表关联查询,按书籍类别统计在架图书数量:
SELECT bs.bookstyle, COUNT(b.bookid) AS total_books, SUM(CASE WHEN b.isborrowed = '1' THEN 1 ELSE 0 END) AS available_books FROM book_style bs LEFT JOIN system_books b ON bs.bookstyleno = b.bookstyleno GROUP BY bs.bookstyle这个查询同时验证了外键关联关系和数据状态字段的正确性。比如穿越小说类目下有两本书,编号902和908,两本都有借阅记录,所以total_books是2、available_books是0。如果查询结果和预期一致,说明从建表到初始化再到状态更新,整条链路都是通的。
验证的方式,我习惯在SSMS里把上面两条查询和文档里的原始查询都跑一遍,然后手动数一遍borrow_record的行数,再数一遍isborrowed为'0'的书数,两者必须相等。这个习惯救了我好几次,因为手动执行的SQL脚本一旦漏掉某个UPDATE,数据状态就错位了。
文档里还提到罚款信息管理,这部分在原报告里没有给完整的计算SQL。要补全的话,需要根据还书日期和应还日期算出超期天数,乘以每日罚金,再写入reader_fee表。这类SQL在数据量大之后性能会明显下降,因为日期计算没法走索引。不过课程设计的数据量完全不用担心,先把业务逻辑跑通更重要。
从那以后我每次拿到一份数据库课程设计代码,都会先执行一遍状态自洽验证再动查询。这套资源最值得学习的地方不在于SQL技巧有多高深,而在于它完整展示了"设计文档——建库建表——数据初始化——业务验证"的闭环。照着复现一遍,跑通了,你再去看E-R图和关系模式,会清晰很多。希望帮到你。
本文还有配套的精品资源,点击获取