大量的更新

This commit is contained in:
zeaslity
2025-11-20 16:18:35 +08:00
parent 776c946772
commit fc72a7312e
30 changed files with 8755 additions and 1327 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -56,7 +56,9 @@ erDiagram
```mermaid
flowchart TD
A[1. FROM & JOIN: 构建数据源] --> B[2. WHERE: 行级过滤]
A[1. FROM & JOIN: 构建数据源]
B[2. WHERE: 行级过滤]
A --> B
B --> C[3. GROUP BY: 数据分组]
C --> D[4. HAVING: 分组后过滤]
D --> E[5. SELECT: 选取列/计算表达式]

View File

@@ -9,4 +9,7 @@
## 生成笔记
gemini直接搜索
Elements
markdown markdown-main-panel tutor-markdown-rendering stronger enable-updated-hr-color

Binary file not shown.

View File

@@ -0,0 +1,582 @@
# MySQL 数据类型权威指南:从基础到最佳实践
## 第一章MySQL 数据类型核心概念
### 1.1 数据类型的重要性:性能、存储与数据完整性
在数据库架构设计中,数据类型的选择是一项基础而关键的决策,其影响深远,远超初学者所能想象。它并非简单的占位符声明,而是直接关系到数据库三大核心支柱的基石:性能、存储效率和数据完整性。每一个字段的数据类型定义,都会对整个系统的响应速度、资源消耗和数据可靠性产生连锁反应。
**性能**是数据类型选择最直接的影响领域。数据库的性能瓶颈通常在于I/O操作即从磁盘读取数据到内存的过程。选择更小、更合适的数据类型意味着每一行数据占用的存储空间更少。在InnoDB存储引擎中数据以页Page为单位进行管理通常每页大小为16 KB。更小的数据行意味着单个页可以容纳更多的记录。当执行查询时数据库需要读取的页数量就会减少从而显著降低物理I/O操作提升查询速度。此外更小的数据类型还能更有效地利用InnoDB的缓冲池Buffer Pool这是MySQL缓存数据和索引的核心内存区域。在有限的内存中缓存更多“热”数据可以极大地提高缓存命中率使查询操作更多地在内存中完成而不是依赖缓慢的磁盘 。  
**存储效率**是另一个显而易见的考量因素。随着数据量的爆炸式增长,磁盘空间不再是无限廉价的资源。尤其在云数据库环境中,存储成本与使用量直接挂钩。通过精确选择数据类型,例如使用 `TINYINT` 存储年龄而非 `INT`可以将存储需求降低75%。这种优化在拥有数十亿行记录的大表中累积起来可以节省TB级别的存储空间直接转化为成本的节约 。  
**数据完整性**是数据库的生命线。数据类型是保障数据完整性的第一道防线。它通过定义列可以接受的值的类型、范围和格式,从源头上阻止了非法或无效数据的插入。例如,将日期字段定义为 `DATE` 类型,可以确保只有符合'YYYY-MM-DD'格式的有效日期才能被存储,任何无效的输入(如'2024-02-30')都会被拒绝。这比在应用层进行数据校验更为可靠和高效,因为它将约束直接施加在数据存储的核心层 。  
一个看似微不足道的选择,其影响是系统性的。例如,一个开发者为存储国家代码(如'US', 'CN')的字段选择了 `VARCHAR(255)`。尽管实际存储的值只有2个字符但MySQL在处理变长字段时可能会在内存中为其分配更大的固定大小内存块 。这不仅浪费了内存还增加了行记录的整体长度。更大的行记录导致每页能存储的行数减少。一个需要扫描1000行记录的查询原本可能只需要读取15个数据页现在可能需要读取20个。这额外的5个页不仅增加了本次查询的I/O还可能将缓冲池中其他有用的数据页挤出导致后续其他查询的缓存命中率下降。因此一个字段的草率选择通过增加I/O和降低内存效率最终可能导致整个数据库系统的性能下降。  
### 1.2 MySQL 数据类型分类概览
MySQL提供了一个丰富的数据类型系统以满足各种数据存储需求。这些类型可以被划分为几个主要类别每个类别都针对特定的数据形式进行了优化。理解这些分类是进行高效数据库设计的第一步 。  
+ **数值类型 (Numeric Types)**:用于存储数字数据。这个大类又可细分为:
+ **整数类型**:用于存储没有小数部分的整数,如 `TINYINT`, `SMALLINT`, `INT`, `BIGINT`
+ **定点数类型**:用于存储具有精确小数位的数值,如 `DECIMAL``NUMERIC`,是金融计算的理想选择。
+ **浮点数类型**:用于存储近似的小数值,如 `FLOAT``DOUBLE`,适用于科学计算。
+ **位值类型**`BIT` 类型,用于存储位字段值。
+ **字符串类型 (String Types)**:用于存储文本或二进制数据。这个类别包括:
+ **字符字符串类型**:如 `CHAR`, `VARCHAR`, `TEXT` 系列,用于存储文本数据,并受字符集和排序规则的影响。
+ **二进制字符串类型**:如 `BINARY`, `VARBINARY`, `BLOB` 系列,用于存储原始的字节数据,如图片或文件,其比较和排序是基于字节值的。
+ **枚举和集合类型**`ENUM``SET`,用于从预定义的列表中选择值。
+ **日期和时间类型 (Date and Time Types)**:专门用于存储时间信息。包括 `DATE`, `TIME`, `YEAR`, `DATETIME`, 和 `TIMESTAMP`。这些类型提供了强大的函数支持,用于处理和计算时间数据。
+ **空间数据类型 (Spatial Data Types)**:用于存储地理和空间信息,如点、线和多边形。例如 `GEOMETRY`, `POINT`, `POLYGON` 等。虽然功能强大,但它们属于专业领域,本指南的核心内容不包含对此类的深入探讨 。  
+ **JSON 数据类型 (JSON Data Type)**自MySQL 5.7版本引入允许在单个字段中存储半结构化的JSON文档。这为关系型数据库带来了类似NoSQL的灵活性适用于存储动态或嵌套的元数据 。  
### 1.3 选择正确类型的基本原则
在选择数据类型时,应遵循一个核心哲学:“最小化原则”。即总是选择能够安全、可靠地存储所有当前及未来可能值的最小数据类型 。这一原则是实现高性能和高效率存储的基石。  
具体而言,选择过程应围绕以下几个关键问题展开:
1. **数据的本质是什么?** 首先要明确存储的是什么样的数据。是整数、精确的小数、近似值,还是文本?这个问题的答案将直接将选择范围缩小到一个特定的数据类型类别。
2. **值的范围是多少?** 对于数值类型需要预估其可能的最大值和最小值。例如如果一个字段用于存储用户年龄其值永远不会超过255那么 `TINYINT UNSIGNED` (范围0-255) 就是最佳选择而不是默认使用占用4个字节的 `INT`
3. **是否需要精确性?** 这是一个至关重要的问题。如果数据涉及金融交易、货币计算或任何不允许出现舍入误差的场景,那么必须使用 `DECIMAL` 类型。在这些场景下使用 `FLOAT``DOUBLE` 是一个严重的设计缺陷,可能导致数据不一致和财务损失 。  
4. **时间代表什么?** 对于时间数据,需要区分其含义。它是一个全球统一的、绝对的时间点(例如,一条消息的发送时间),还是一个与特定时区无关的“日历”或“时钟”上的时间(例如,某人的生日或一个本地活动的开始时间)?前者应使用 `TIMESTAMP`,后者则应使用 `DATETIME` 
遵循这些基本原则,可以确保数据库模式从一开始就建立在坚实、高效的基础之上,为未来的扩展和性能优化奠定良好的根基。
## 第二章:数值类型
数值类型是数据库中最基础、最常用的数据类型之一。MySQL提供了全面的数值类型支持涵盖了从微小的整数到高精度的定点数再到大范围的浮点数。正确选择数值类型对于节省存储空间、提升查询性能以及保证数据计算的准确性至关重要。
### 2.1 整数类型 (Integer Types: `TINYINT`, `SMALLINT`, `MEDIUMINT`, `INT`, `BIGINT`)
整数类型用于存储不包含小数部分的数字。MySQL提供了五种不同大小的整数类型以满足不同范围的数据存储需求这完美体现了“最小化原则”的应用。
#### 语法、存储空间与取值范围
每种整数类型都有其固定的存储大小和相应的取值范围。选择时,应根据业务需求预估字段可能的最大值,然后选择恰好能覆盖该范围的最小类型。例如,存储一个班级的学生人数,`TINYINT UNSIGNED` (0-255) 通常就足够了。
下表详细列出了各种整数类型的核心规格  
| 类型名称 | 存储空间 (字节) | 有符号范围 (Signed Range) | 无符号范围 (Unsigned Range) |
| --- | --- | --- | --- |
| `TINYINT` | 1 | \-128 到 127 | 0 到 255 |
| `SMALLINT` | 2 | \-32,768 到 32,767 | 0 到 65,535 |
| `MEDIUMINT` | 3 | \-8,388,608 到 8,388,607 | 0 到 16,777,215 |
| `INT` | 4 | \-2,147,483,648 到 2,147,483,647 | 0 到 4,294,967,295 |
| `BIGINT` | 8 | 9,223,372,036,854,775,808 到 9,223,372,036,854,775,807 | 0 到 18,446,744,073,709,551,615 |
Export to Sheets
#### `UNSIGNED` 和 `ZEROFILL` 属性详解
+ **`UNSIGNED`**:这是一个非常重要的属性。当为整数列指定 `UNSIGNED` 属性时该列将只允许存储非负数零和正数。其效果是将原本用于表示负数的那一半取值范围移动到了正数范围的顶端从而使正数的最大值翻倍。对于那些在业务逻辑上永远不会是负值的字段如自增ID、物品数量、年龄等强烈推荐使用 `UNSIGNED` 属性。这不仅是一种数据约束,确保了数据的有效性,也是一种优化,因为它用同样的存储空间提供了更大的正数表示范围 。  
+ **`ZEROFILL`**:这是一个纯粹的显示格式化属性。当为列指定 `ZEROFILL`MySQL会自动为该列添加 `UNSIGNED` 属性。在显示数值时,如果数字的位数小于在类型定义中指定的显示宽度 `M`(例如 `INT(10)`MySQL会在左侧用零来填充直到达到指定的宽度。例如在一个 `INT(5) ZEROFILL` 的列中存入数字 `123`,查询时会显示为 `00123`。需要特别强调的是,`INT(M)` 中的 `M` **不影响**该整数类型的存储大小或实际可存储的数值范围。`INT(10)``INT(1)` 都能存储从-21亿到+21亿的完整范围并都占用4个字节。`M` 仅在与 `ZEROFILL` 结合使用时才有视觉上的意义。这是一个普遍存在的误解,许多开发者错误地认为 `M` 限制了可存储的数字位数 。  
### 2.2 定点数类型 (Fixed-Point Types: `DECIMAL`, `NUMERIC`)
#### 精确计算的基石
`DECIMAL` 类型是为需要绝对精确计算的场景而设计的,它是存储货币、利率、财务数据以及任何不允许出现舍入误差的数值的唯一正确选择。与 `FLOAT``DOUBLE` 将数值存储为二进制近似值不同,`DECIMAL` 将每个数字作为字符串进行内部存储,从而保证了数值的精确性 。  
`DECIMAL` 的语法是 `DECIMAL(M, D)`,其中:
+ `M` 代表**精度 (precision)**,即数字的总位数(整数部分+小数部分最大值为65。
+ `D` 代表**标度 (scale)**即小数点后的位数最大值为30`D` 必须小于等于 `M`
例如,`DECIMAL(10, 2)` 可以存储最多10位数字其中2位是小数其取值范围为 `-99999999.99``99999999.99`
`NUMERIC` 类型是 `DECIMAL` 的同义词,在功能上完全等同于 `DECIMAL`主要是为了遵循SQL标准 。  
`DECIMAL` 类型的存储空间是可变的,它取决于 `M` 的值MySQL会根据需要打包数字以节省空间 。  
### 2.3 浮点数类型 (Floating-Point Types: `FLOAT`, `DOUBLE`)
#### 近似值的存储与潜在问题
`FLOAT``DOUBLE` 类型用于存储近似的数值。它们遵循IEEE 754标准使用二进制格式来表示浮点数。这种表示方式使得它们能够存储极大或极小的数值但代价是牺牲了精度。
+ **`FLOAT`**单精度浮点数占用4个字节。
+ **`DOUBLE`**双精度浮点数占用8个字节具有比 `FLOAT` 更大的范围和更高的精度。`REAL``DOUBLE` 的同义词。
由于二进制无法精确表示所有十进制小数例如0.1),浮点数在存储和计算过程中可能会引入微小的舍入误差。这些误差在单个数值上可能微不足道,但在大量聚合计算(如 `SUM()`)中会累积,导致结果与预期不符。因此,浮点数适用于科学计算、工程模拟、传感器读数等场景,在这些场景中,数值范围远比绝对精度重要 。  
### 2.4 深度比较:`DECIMAL` vs. `FLOAT`/`DOUBLE`
`DECIMAL``FLOAT`/`DOUBLE` 之间的选择是数据库设计中一个常见且关键的决策点。它们的差异直接关系到数据的准确性和应用的可靠性。
下表清晰地对比了这两种类型的核心特性  
| 特性 | `DECIMAL` | `FLOAT` / `DOUBLE` |
| --- | --- | --- |
| **精度** | **精确**。按原样存储数值,无舍入误差。 | **近似**。存储为二进制浮点数,可能存在微小的舍入误差。 |
| **存储方式** | 内部以变长字符串形式存储,保证精确度。 | 内部以二进制指数形式存储。 |
| **存储空间** | 可变,取决于定义的精度(M)。通常比浮点数占用更多空间。 | 固定。`FLOAT` 为4字节`DOUBLE` 为8字节。 |
| **计算性能** | 由于内部表示复杂,计算速度相对较慢。 | 基于硬件浮点单元,计算速度通常更快。 |
| **适用场景** | **金融、货币、会计**等任何要求精确计算的领域。 | **科学计算、工程、物理模拟**等对数值范围要求高但可容忍微小误差的领域。 |
Export to Sheets
使用 `FLOAT``DOUBLE` 存储货币值是一个极其危险的常见错误。一个简单的例子可以说明问题假设一个商品价格为9.99,在 `FLOAT` 类型中可能被存储为 `9.9900000000000002`。当对一百万笔这样的交易进行求和时,累积的误差可能会导致最终总额出现明显的偏差。这种因数据类型选择不当而导致的业务逻辑错误,其后果可能是灾难性的。
### 2.5 数值类型使用最佳实践与常见误区
+ **最佳实践**
1. **坚持最小化原则**:为年龄字段使用 `TINYINT UNSIGNED`,为文章点赞数使用 `INT UNSIGNED`,而不是一律使用 `INT` 
2. **为非负ID使用 `UNSIGNED`**:所有自增主键和外键都应定义为 `UNSIGNED`这不仅符合逻辑还能将ID的上限提高一倍。
3. **为货币和精确值使用 `DECIMAL`**:这是不可动摇的铁律。
+ **常见误区**
1. **滥用 `INT`**:将 `INT` 作为所有整数需求的默认选择,导致不必要的存储浪费。
2. **误用 `FLOAT`/`DOUBLE` 存储货币**:这是最严重的误区之一,直接威胁到数据的准确性和业务的可靠性 。  
3. **误解 `INT(M)` 的含义**:错误地认为 `M` 限制了数值的范围或存储大小,而实际上它只与 `ZEROFILL` 属性的显示格式有关。
## 第三章:字符串类型
字符串类型是用于存储文本数据的核心构建块从用户名、地址到文章内容无处不在。MySQL提供了多种字符串类型主要分为定长、变长、大文本对象以及特殊的枚举和集合类型。理解它们之间的细微差别尤其是在存储机制、索引能力和性能影响上的差异对于构建高效、可扩展的数据库至关重要。
### 3.1 深度比较:`CHAR` vs. `VARCHAR`
`CHAR``VARCHAR` 是最常用的两种字符类型,它们的核心区别在于长度是固定的还是可变的。
#### 存储机制
+ **`CHAR(M)`**:代表**定长 (Fixed-Length)** 字符串。当你定义一个 `CHAR(10)` 的列时,无论你存入的字符串是 'hello' (5个字符) 还是 'database' (8个字符)它在磁盘上都将占用10个字符的存储空间。对于短于 `M` 的字符串MySQL会在其右侧用空格进行填充以达到指定长度。一个重要的特性是在检索 `CHAR` 值时除非启用了特定的SQL模式 (`PAD_CHAR_TO_FULL_LENGTH`),否则尾部的填充空格通常会被移除 。  
+ **`VARCHAR(M)`**:代表**变长 (Variable-Length)** 字符串。当你定义一个 `VARCHAR(255)` 的列时它存储一个字符串所需的空间是其实际长度加上一个或两个字节的前缀。这个前缀用于记录字符串的字节长度。如果值的字节长度不超过255则使用1个字节的前缀如果超过255则使用2个字节。`VARCHAR` 不会进行空格填充,并且在存储和检索时会保留字符串尾部的空格 。  
下表通过一个实例直观地展示了 `CHAR``VARCHAR` 在存储上的差异  
| 输入值 | 存入 `CHAR(6)` | 存储所需空间 (单字节字符集) | 存入 `VARCHAR(6)` | 存储所需空间 (单字节字符集) |
| --- | --- | --- | --- | --- |
| `''` (空字符串) | `' '` | 6 字节 | `''` | 1 字节 (长度前缀) |
| `'sql'` | `'sql '` | 6 字节 | `'sql'` | 4 字节 (3 + 1) |
| `'mysql '` | `'mysql '` | 6 字节 | `'mysql '` | 7 字节 (6 + 1) |
Export to Sheets
#### 性能影响与适用场景
+ **`CHAR` 的适用场景**`CHAR` 最适合存储那些长度几乎完全固定的数据。典型的例子包括:
+ 国家代码 (如 'US', 'CN'固定为2个字符)。
+ MD5或SHA1哈希值 (固定为32或40个字符)。
+ 性别字段 (如 'M', 'F'固定为1个字符)。 在这些场景下由于行记录的长度是固定的理论上MySQL可以更快地计算行偏移量从而在某些情况下尤其是在较老的存储引擎如MyISAM中获得微弱的性能优势。然而在现代的InnoDB存储引擎中由于其基于页的复杂存储结构这种优势已基本可以忽略不计 。  
+ **`VARCHAR` 的适用场景**`VARCHAR` 是绝大多数文本存储场景的理想选择。用户名、电子邮件、地址、文章标题等,这些数据的长度都是可变的。使用 `VARCHAR` 可以极大地节省存储空间。空间的节省意味着更小的表、更小的索引、更高效的缓冲池利用率最终转化为更少的磁盘I/O和更快的查询性能 。  
### 3.2 深度比较:`VARCHAR` vs. `TEXT`
当需要存储的文本长度可能超过 `VARCHAR` 的常规使用范围,或者非常长时,开发者通常会在 `VARCHAR``TEXT` 之间进行抉择。这个选择对性能有着至关重要的影响。
#### 存储限制与索引策略
+ **`VARCHAR`**其最大长度可以定义到65,535。但需要注意的是这个限制是**字节**数并且受MySQL单行最大65,535字节的限制所有列共享。实际可存储的**字符**数取决于所使用的字符集(例如,`utf8mb4` 字符集一个字符最多可能占用4个字节`VARCHAR` 列可以被完整地索引,这对于查询性能至关重要 。  
+ **`TEXT`**`TEXT` 类型家族用于存储更长的文本数据。
+ `TEXT`:最大长度 65,535 字节 (~64 KB)。
+ `MEDIUMTEXT`:最大长度 16,777,215 字节 (~16 MB)。
+ `LONGTEXT`:最大长度 4,294,967,295 字节 (~4 GB)。 `TEXT` 类型的一个关键限制是,不能直接对其整个列创建常规索引。如果需要索引,必须指定一个**前缀长度**,例如 `CREATE INDEX idx_content ON articles(content(255));`。这意味着索引只能利用内容的前255个字符对于需要全文搜索或精确匹配长内容的查询这种前缀索引的效率有限 。  
#### 对临时表和排序的性能影响
这是 `VARCHAR``TEXT` 之间最关键、也最容易被忽视的性能差异。当一个查询需要使用临时表来处理中间结果时(例如,使用了 `GROUP BY`, `ORDER BY`, `DISTINCT` 等操作MySQL会首先尝试在内存中创建这个临时表。
然而MySQL的内存存储引擎`MEMORY` engine**不支持** `TEXT``BLOB` 类型。如果临时表中需要包含 `TEXT` 类型的列MySQL将被迫放弃使用内存临时表转而在**磁盘**上创建一个基于 `InnoDB``MyISAM` 的临时表 。  
这个转换过程对性能的影响是巨大的:
1. 一个查询,如 `SELECT title FROM articles ORDER BY content;`,需要对 `content` 列进行排序。
2. 优化器决定使用一个临时表来存储 `title``content` 并进行排序。
3. 优化器检查到 `content` 列是 `TEXT` 类型。
4. 由于内存引擎不支持 `TEXT`它不能在RAM中创建临时表。
5. MySQL转而在磁盘上创建临时表。磁盘I/O的速度比内存操作慢几个数量级。
6. 因此,这个原本可能很快的查询,会因为 `TEXT` 类型的存在而变得异常缓慢并产生大量的磁盘I/O可能拖慢整个数据库服务器的性能。
结论是:如果文本数据的长度确定不会超过 `VARCHAR` 的65,535字节限制应**始终优先选择 `VARCHAR` 而不是 `TEXT`**。这可以避免在复杂查询中掉入“磁盘临时表”的性能陷阱 。  
### 3.3 二进制字符串与大对象 (`BINARY`, `VARBINARY`, `BLOB`)
`BINARY`, `VARBINARY`, 和 `BLOB` 分别是 `CHAR`, `VARCHAR`, 和 `TEXT` 的二进制(字节)版本。它们的主要区别在于存储和比较的方式:
+ **存储内容**:它们存储的是原始的字节序列,而不是字符。
+ **比较和排序**:对这些类型的比较和排序是基于字节的数值进行的,因此是**区分大小写**的。它们不受字符集和排序规则collation的影响。例如在二进制比较中字节值 `0x61` ('a') 与 `0x41` ('A') 是不同的。
+ **适用场景**`BINARY``VARBINARY` 适用于存储那些需要精确字节匹配的数据,如加密密钥或定长的二进制标识符。`BLOB` (Binary Large Object) 系列(`TINYBLOB`, `BLOB`, `MEDIUMBLOB`, `LONGBLOB`则用于存储大量的二进制数据如图片、音频或编译后的代码。然而将大文件直接存储在数据库中通常被认为是一种反模式因为它会迅速撑大数据库体积增加备份和恢复的复杂性。更推荐的做法是将文件存储在专门的文件系统或对象存储如S3而在数据库中只存储文件的路径或URL 。  
### 3.4 枚举与集合类型 (`ENUM`, `SET`)
+ **`ENUM`**:枚举类型。允许你从一个预定义的字符串值列表中选择**一个**值。例如 `ENUM('active', 'inactive', 'pending')``ENUM` 在内部存储时非常高效通常只用1或2个字节来存储一个指向值列表的索引而不是存储字符串本身 。  
+ **`SET`**:集合类型。允许你从一个预定义的字符串值列表中选择**零个或多个**值。例如 `SET('read', 'write', 'execute')``SET` 在内部被存储为一个位图bitmap每个预定义的值对应一个位。存储空间取决于列表成员的数量可以是1, 2, 3, 4, 或 8个字节 。  
尽管 `ENUM``SET` 在存储上极为高效,但它们存在一个严重的**维护性陷阱**。这两个类型将允许的值列表硬编码到了表结构schema中。如果业务需求变化需要增加一个新的状态或权限就必须执行 `ALTER TABLE` 语句来修改列定义。在生产环境的大表上,`ALTER TABLE` 通常是一个阻塞性的、耗时很长的操作,可能导致服务中断。因此,在需要频繁变更或扩展值列表的敏捷开发环境中,使用 `ENUM``SET` 往往是不明智的。一个更灵活、可扩展的替代方案是创建一个独立的“查找表”lookup table并通过外键关联来维护这些值 。  
### 3.5 字符串类型使用最佳实践与常见误区
+ **最佳实践**
1. **`VARCHAR` 是首选**对于绝大多数长度可变的文本数据只要在64KB以内`VARCHAR` 都是最佳选择。
2. **精确定义长度**:避免使用 `VARCHAR(255)` 作为“懒人默认值”。根据实际数据预估一个合理的长度,如 `VARCHAR(50)` 用于用户名。这有助于数据校验并且可能为MySQL的内部内存管理带来优化 。  
3. **为真正定长的数据使用 `CHAR`**仅在数据长度严格固定的情况下如MD5哈希才考虑使用 `CHAR`
+ **常见误区**
1. **误用 `TEXT`**:在数据长度完全可以用 `VARCHAR` 容纳的情况下使用 `TEXT`,这会带来潜在的巨大性能风险,尤其是在涉及排序和分组的查询中 。  
2. **滥用 `ENUM`**:为那些可能会随业务发展而变化的值列表(如商品分类)使用 `ENUM`,导致后期维护困难和上线风险 。  
3. **在 `VARCHAR` 中存储CSV**:将多个值用逗号分隔存储在一个 `VARCHAR` 字段中(如 '1,5,23'),这破坏了数据库的第一范式,使得查询、更新和维护这些值变得极其困难和低效。正确的做法是使用一个独立的关联表。
## 第四章:日期与时间类型
在几乎所有的应用中时间信息都是不可或缺的数据维度。MySQL提供了一套完整且功能强大的日期和时间数据类型用于精确记录和处理从年份到微秒的各种时间值。在这些类型中`DATETIME``TIMESTAMP` 的选择尤为关键,它们在时区处理上的根本差异决定了其各自的适用场景。
### 4.1 基础类型:`DATE`, `TIME`, `YEAR`
这三种类型用于存储时间信息的特定部分,用途明确。
+ **`DATE`**:用于存储日期,不包含时间部分。其标准格式为 `'YYYY-MM-DD'`。它占用3个字节支持的范围从 `'1000-01-01'``'9999-12-31'``DATE` 类型是存储生日、纪念日、事件发生日期等场景的理想选择 。  
+ **`TIME`**:用于存储时间,不包含日期部分。其标准格式为 `'HH:MM:SS'`。有趣的是,`TIME` 类型的范围远不止24小时它可以从 `'-838:59:59'``'838:59:59'`。这使得 `TIME` 不仅可以表示一天中的某个时间点,还可以用来记录两个事件之间的时间间隔或持续时长 。  
+ **`YEAR`**用于存储年份。它只占用1个字节可以存储从1901到2155的年份。这是一个空间效率极高的类型但使用场景相对有限 。  
### 4.2 核心对决:`DATETIME` vs. `TIMESTAMP`
`DATETIME``TIMESTAMP` 都可以存储包含日期和时间的完整时间戳,但它们在内部工作机制、存储范围和时区处理上存在本质区别。
#### 存储空间、取值范围与“2038年问题”
+ **`DATETIME`**
+ **范围**:支持的范围非常广,从 `'1000-01-01 00:00:00'``'9999-12-31 23:59:59'` 
+ **存储**自MySQL 5.6.4起,`DATETIME` 的存储空间经过优化需要5个字节加上小数秒所需的额外存储0-3字节 
+ **`TIMESTAMP`**
+ **范围**:其范围相对受限,从 `'1970-01-01 00:00:01'` UTC 到 `'2038-01-19 03:14:07'` UTC 。这个上限被称为\*\*“2038年问题”\*\*因为它在内部是基于一个有符号的32位Unix时间戳从1970年1月1日午夜UTC开始的秒数实现的。当这个32位整数溢出时时间将无法正确表示 。  
+ **存储**需要4个字节加上小数秒所需的额外存储 。  
#### 时区处理的根本差异
这是 `DATETIME``TIMESTAMP` 之间最核心、最决定性的区别。
+ **`DATETIME`:时区无关 (Timezone-Agnostic)** `DATETIME` 存储的是一个**字面量 (literal)** 的日期和时间值。你存入什么,它就记录什么,完全不关心当前数据库服务器或客户端连接的任何时区设置。例如,无论你的服务器在伦敦还是东京,当你执行 `INSERT INTO my_table (dt_col) VALUES ('2024-01-01 10:00:00');` 时,数据库中存储的值就是 `'2024-01-01 10:00:00'`。在任何时区环境下检索这个值,你得到的也永远是这个固定的字符串 。  
+ **`TIMESTAMP`:时区感知 (Timezone-Aware)** `TIMESTAMP` 存储的是一个**绝对的、全球统一的时间点**。它的工作流程如下:
1. **存储时**MySQL获取你提供的时间值例如 `'2024-01-01 10:00:00'`),并根据**当前会话的时区设置**`time_zone`变量),将其转换为**协调世界时 (UTC)** 进行存储。
2. **检索时**:当查询该 `TIMESTAMP` 值时MySQL会从数据库中取出存储的UTC时间并再次根据**当前会话的时区设置**,将其转换回本地时间进行显示。 这个自动转换机制确保了 `TIMESTAMP` 记录的是一个无歧义的时刻。一个在东八区北京时间上午10点插入的 `TIMESTAMP`对于一个在零时区伦敦时间的客户端来说检索出来会显示为凌晨2点两者指向的是同一个宇宙时刻 。  
#### 概念辨析:全球时刻 vs. 本地约定
基于时区处理的差异,可以得出选择 `DATETIME``TIMESTAMP` 的核心指导思想:
+ **使用 `TIMESTAMP` 记录“全球时刻” (Global Moment)**:当需要记录一个事件发生的绝对时间点,且这个时间点需要在全球不同地区被正确地理解时,应使用 `TIMESTAMP`。例如:
+ 用户注册时间
+ 订单创建时间
+ 消息发送时间
+ 文章发布时间
+ **使用 `DATETIME` 记录“本地约定” (Local Appointment)**:当需要记录一个与特定地理位置或文化背景相关的、不应随时区变化而改变的“日历时间”或“墙上时钟时间”时,应使用 `DATETIME`。例如:
+ 某人的生日(一个人的生日不会因为他去了另一个国家而改变)。
+ 一个在纽约举行的会议的开始时间对所有参会者来说都是纽约当地时间上午9点
+ 法定节假日日期。
下表总结了 `DATETIME``TIMESTAMP` 的所有关键区别,为技术选型提供清晰的决策依据  
| 特性 | `DATETIME` | `TIMESTAMP` |
| --- | --- | --- |
| **支持范围** | `'1000-01-01'``'9999-12-31'` | `'1970-01-01'``'2038-01-19'` |
| **存储空间** | 5 字节 + 小数秒存储 (>= 5.6.4) | 4 字节 + 小数秒存储 |
| **时区处理** | **时区无关**。存储和显示字面值,不进行转换。 | **时区感知**。存储时转换为UTC检索时从UTC转回会话时区。 |
| **自动初始化/更新** | 可配置 `DEFAULT CURRENT_TIMESTAMP``ON UPDATE CURRENT_TIMESTAMP` (>= 5.6.5) | 默认行为(在旧版本中)或可配置 `DEFAULT CURRENT_TIMESTAMP``ON UPDATE CURRENT_TIMESTAMP`。 |
| **核心用例** | 生日、本地事件时间、不应随时区变化的“约定时间”。 | 用户注册、订单创建、日志记录等需要全球统一的“绝对时刻”。 |
Export to Sheets
### 4.3 日期时间类型使用最佳实践与常见误区
+ **最佳实践**
1. **服务器时区设置为UTC**强烈建议将MySQL服务器的全局时区设置为UTC (`--default-time-zone='+00:00'`)。这为所有时间存储提供了一个统一、无歧义的基准。应用程序可以根据用户偏好在展示层进行时区转换 。  
2. **明确业务需求**:在选择 `DATETIME``TIMESTAMP` 之前,仔细分析业务场景,问自己:“这个时间值应该随着观察者位置的变化而变化吗?”
3. **考虑未来**:对于需要长期存储或记录未来事件的系统,要警惕 `TIMESTAMP` 的“2038年问题”。如果应用的生命周期可能跨越2038年或者需要存储1970年之前的历史数据那么使用 `DATETIME` 并以UTC标准存储是更安全的选择。
+ **常见误区**
1. **混淆二者**:不理解时区处理的差异,随意选择 `DATETIME``TIMESTAMP`,导致在跨时区的应用中出现时间混乱。
2. **忽略“2038年问题”**:在新项目中使用 `TIMESTAMP` 而没有评估其时间范围是否满足长远需求,为系统埋下了一颗定时炸弹。
3. **在应用层处理时区混乱**:当数据库层面时区策略不一致时(例如,混合使用 `DATETIME``TIMESTAMP`,服务器时区未标准化),应用层需要编写大量复杂的代码来弥补,增加了出错的概率和维护成本。
## 第五章JSON 数据类型
自MySQL 5.7版本起,引入了原生的 `JSON` 数据类型这标志着MySQL在融合关系型数据库的严谨性与NoSQL数据库的灵活性方面迈出了重要一步。`JSON` 类型允许开发者在单个数据库列中存储和操作半结构化的JSONJavaScript Object Notation文档。
### 5.1 JSON 类型简介:关系型数据库的半结构化能力
`JSON` 数据类型为传统的关系型数据模型提供了一种强大的补充。它允许存储包含嵌套对象和数组的复杂数据结构,而无需预先定义一个严格的、扁平化的表结构。这对于以下场景特别有用:
+ \*\* schema 灵活性\*\*:存储那些结构多变或未来可能频繁扩展的元数据,如产品属性、用户配置、标签等。
+ **简化数据模型**:对于一对多的弱关系数据,使用 `JSON` 数组可以避免创建额外的关联表,简化数据库设计。
+ **与现代应用集成**现代Web和移动应用大量使用JSON作为数据交换格式将JSON文档直接存入数据库可以减少应用层的数据解析和转换开销 。  
MySQL对 `JSON` 类型的实现具有一个关键优势:它不仅仅是将其作为普通文本存储。当数据插入 `JSON` 列时MySQL会**校验**其是否为合法的JSON格式。如果格式无效插入操作将失败。此外MySQL会以一种优化的二进制格式存储JSON数据这使得对文档内部元素的读取和访问更加高效 。  
### 5.2 创建、操作与查询 JSON 数据
MySQL提供了一套丰富的内置函数来处理 `JSON` 数据,涵盖了创建、查询、修改等各个方面。
+ **创建JSON值**
+ `JSON_OBJECT(key1, val1, key2, val2,...)`根据键值对创建一个JSON对象。
+ 示例:`SELECT JSON_OBJECT('name', 'Alice', 'age', 30);` -> `{"age": 30, "name": "Alice"}`
+ `JSON_ARRAY(val1, val2,...)`根据给定的值创建一个JSON数组。
+ 示例:`SELECT JSON_ARRAY('apple', 'banana', 123);` -> `["apple", "banana", 123]` 这些函数可以直接在 `INSERT``UPDATE` 语句中使用以编程方式构建JSON文档 。  
+ **查询和提取JSON数据** 查询JSON数据的核心是**JSON路径表达式 (JSON Path)**它是一种类似文件系统路径的语法用于定位JSON文档中的特定元素。路径以 `$` 符号代表整个文档。
+ `JSON_EXTRACT(json_doc, path)`:是最主要的提取函数。
+ 示例:`SELECT JSON_EXTRACT('{"user": {"name": "Bob", "tags": ["dev", "sql"]}}', '$.user.tags');` -> `"dev"`
+ **列路径操作符 (Column Path Operator)**MySQL提供了更简洁的语法糖
+ `column->path`:等价于 `JSON_EXTRACT()`返回的结果是JSON格式的例如字符串会带引号
+ `column->>path`:等价于 `JSON_UNQUOTE(JSON_EXTRACT(...))`它会提取值并移除JSON的引号返回一个常规的SQL字符串。这在 `WHERE` 子句中进行值比较时非常有用 。  
+ **修改JSON数据** MySQL允许对 `JSON` 文档进行原地部分更新,而无需读取整个文档、在应用层修改、再写回整个文档。
+ `JSON_SET(json_doc, path, val,...)`:在指定路径插入或更新值。如果路径已存在,则更新;如果不存在,则创建。
+ `JSON_REPLACE(json_doc, path, val,...)`:仅当指定路径存在时,才更新其值。
+ `JSON_REMOVE(json_doc, path,...)`:移除指定路径的元素 。  
### 5.3 JSON 列的索引策略
`JSON` 类型虽然灵活,但其性能上的一个主要挑战是:**不能直接在 `JSON` 列本身上创建常规索引**。如果不对其进行索引优化那么对JSON内容进行过滤的查询例如 `WHERE profile->>'$.city' = 'New York'`)将会导致全表扫描,在数据量大时性能极差。
为了解决这个问题MySQL提供了**基于生成列 (Generated Columns) 的索引策略**。这是使用 `JSON` 类型时必须掌握的核心性能优化技术。其步骤如下:
1. **创建虚拟或存储生成列**:在表中定义一个新列,其值是根据 `JSON` 列中的某个路径表达式动态计算得出的。
2. **在该生成列上创建索引**:由于生成列具有明确的标量数据类型(如 `VARCHAR``INT`因此可以在其上创建标准的B-Tree索引 。  
下面是一个具体的例子,演示了如何为一个存储用户信息的 `JSON` 列中的 `city` 字段创建索引:
SQL
```
CREATE TABLE user_profiles (
id INT PRIMARY KEY AUTO_INCREMENT,
profile JSON,
-- 步骤1: 创建一个虚拟生成列,用于提取城市信息。
-- 'AS (profile->>"$.address.city")' 定义了该列的值。
-- 'VIRTUAL' 表示该列的值不实际存储,而是在读取时计算。
city VARCHAR(100) AS (profile->>"$.address.city") VIRTUAL,
-- 步骤2: 在这个新创建的生成列上建立索引。
INDEX idx_city (city)
);
```
通过这种方式,当执行如下查询时:
SQL
```
SELECT id, profile FROM user_profiles WHERE city = 'San Francisco';
```
MySQL的查询优化器能够利用 `idx_city` 索引快速定位到符合条件的行,极大地提升了查询性能,避免了对 `user_profiles` 表的全表扫描。
### 5.4 JSON 类型使用最佳实践
+ **明确使用场景**`JSON` 类型最适合用于存储非关键的、结构易变的元数据。核心的、关系紧密的数据,尤其是那些需要频繁进行 `JOIN` 操作或强制实施严格约束的字段,仍然应该使用传统的关系型列来存储。
+ **索引关键路径**:对于 `JSON` 文档中任何需要用于 `WHERE` 子句过滤、排序或分组的字段,都**必须**通过生成列为其创建索引。这是保证 `JSON` 类型高性能查询的前提。
+ **避免“上帝对象”**:切忌将一个应用的所有数据都塞进一个巨大的 `JSON` blob中。这样做无异于将MySQL当作一个简单的键值存储来使用完全丧失了关系型数据库在数据完整性、查询优化和事务处理方面的强大优势。应将结构化数据规范化到独立的列和表中仅将真正半结构化的部分存入 `JSON` 列。
## 第六章:综合选型策略与总结
经过对MySQL主要数据类型的深入剖析我们已经了解了每种类型的特性、优势和潜在的陷阱。本章旨在将这些知识整合为一个整体的、实用的选型策略并总结在数据库设计中最常见的错误以帮助开发者构建出健壮、高效且可维护的数据库模式。
### 6.1 综合选型策略:如何为你的应用设计最佳表结构
设计表结构时,可以遵循一个决策流程,对每个字段进行审慎的考量。这个过程可以被看作是回答一系列关于数据本质的问题:
1. **数据是什么性质的?**
+ 是数字、文本、时间还是复杂的半结构化数据这决定了你将在哪个大的数据类型类别中进行选择数值、字符串、日期时间、JSON
2. **对于数值,它的范围和精度要求是什么?**
+ 它是否永远为非负数?如果是,立即添加 `UNSIGNED` 属性。
+ 预估其可能的最大值是多少?根据这个值,从 `TINYINT`, `SMALLINT`, `MEDIUMINT`, `INT`, `BIGINT` 中选择最小的那个。例如,一个国家的总人口数可能需要 `BIGINT UNSIGNED`,而一个班级的人数 `TINYINT UNSIGNED` 就足够了。
+ 它是否需要绝对精确,如货币?如果是,**必须**使用 `DECIMAL`。任何其他选择都是错误的。如果是科学测量值,`FLOAT``DOUBLE` 可能是合适的。
3. **对于字符串,它的长度特性是什么?**
+ 长度是否严格固定例如MD5哈希值。如果是`CHAR` 是一个合理的选择。
+ 长度是否可变?这是绝大多数情况。选择 `VARCHAR`
+ 预估其最大长度是多少?为 `VARCHAR` 设置一个合理的上限,而不是盲目使用 `VARCHAR(255)`
+ 它是否可能超过64KB如果答案是“是”那么你可能需要 `MEDIUMTEXT``LONGTEXT`,但在此之前,要再次确认是否真的有必要,并充分意识到其对排序和临时表操作的潜在性能影响。
4. **对于时间,它代表什么意义?**
+ 它是一个全球统一的、绝对的时刻吗?(例如,日志条目、创建时间)如果是,选择 `TIMESTAMP`并确保服务器时区设置为UTC。
+ 它是一个与本地时区绑定的“约定”时间吗?(例如,生日、本地会议开始时间)如果是,选择 `DATETIME`
+ 是否需要存储1970年之前或2038年之后的时间如果是`TIMESTAMP` 将无法满足需求,必须使用 `DATETIME`
5. **对于复杂或动态数据,它是否适合 `JSON`**
+ 数据是否是结构化的,但结构经常变化或嵌套层次很深?`JSON` 是一个很好的候选者。
+ 如果选择 `JSON`,哪些内部字段会成为查询条件?为这些字段通过生成列建立索引。
+ 这些数据是否是应用的核心实体?如果是,应优先考虑将其规范化为传统的列和表,而不是全部塞进 `JSON`
### 6.2 常见数据类型选型错误汇总与规避方案
以下是在实际开发中反复出现的典型数据类型选型错误,以及如何规避它们的最终建议。
+ **错误1过度宽容的类型定义 (Over-provisioning Types)**
+ **表现**:为只能容纳几百个值的状态字段使用 `INT`为用户ID使用 `BIGINT`,而 `INT UNSIGNED` 已经可以支持超过40亿用户。
+ **危害**:严重浪费存储空间和内存,降低了缓存效率,拖慢了整个系统。
+ **规避方案**:严格遵循“最小化原则”。仔细分析每个字段的业务边界,选择恰好满足需求的最小数据类型 。  
+ **错误2使用浮点数处理货币 (Using `FLOAT` for Currency)**
+ **表现**:将商品价格、账户余额等字段定义为 `FLOAT``DOUBLE`
+ **危害**:导致不可预测的舍入误差,在聚合计算中误差会被放大,最终造成数据不一致和财务损失。这是一个致命的错误。
+ **规避方案**:任何涉及精确计算的场景,**必须**使用 `DECIMAL``NUMERIC` 类型 。  
+ **错误3无差别使用 `TEXT` (Unnecessary Use of `TEXT`)**
+ **表现**:只要是长文本,就使用 `TEXT`,即使其长度远未达到 `VARCHAR` 的上限。
+ **危害**:当涉及 `TEXT` 列的查询需要排序或分组时可能强制MySQL使用基于磁盘的慢速临时表造成严重的性能瓶颈 。  
+ **规避方案**只要数据长度在65,535字节以内就**始终优先使用 `VARCHAR`**。
+ **错误4混淆 `DATETIME` 和 `TIMESTAMP` 的时区行为**
+ **表现**:在需要全球统一时间戳的应用中使用了 `DATETIME`,或者在需要存储本地“约定”时间时使用了 `TIMESTAMP`
+ **危害**:导致跨时区应用的时间显示混乱和逻辑错误。
+ **规避方案**:深刻理解二者的核心区别——“全球时刻” vs “本地约定”。将服务器时区设为UTC并根据业务需求审慎选择 。  
+ **错误5滥用 `ENUM` 和 `SET` (The `ENUM`/`SET` Maintenance Trap)**
+ **表现**:为那些业务逻辑上可能频繁变更的列表(如商品分类、用户标签)使用 `ENUM``SET`
+ **危害**:每次增加新选项都需要执行 `ALTER TABLE`,这在生产环境的大表上是高风险、高成本的操作,严重影响系统的灵活性和可维护性 。  
+ **规避方案**对于可能变化的列表使用一个独立的查找表lookup table和外键关联是更优的、更具扩展性的设计模式。
+ **错误6在字符串中存储结构化数据 (Storing Structured Data in Strings)**
+ **表现**将多个ID用逗号分隔存储在 `VARCHAR` 中(如 `'1,5,23'`);或者将键值对拼接成字符串。
+ **危害**:完全破坏了关系型数据库的优势,使得查询、更新、数据校验和维护变得极其困难和低效,无法利用索引。
+ **规避方案**:对于多对多关系,使用标准的关联表。对于半结构化的键值对数据,使用 `JSON` 类型并配合生成列索引。
最终数据类型的选择是数据库设计艺术与科学的结合。它要求开发者不仅要理解每种类型的技术规格更要洞察其背后的业务逻辑和数据生命周期。通过遵循本指南中提出的原则和最佳实践可以为构建高性能、高可靠性的MySQL应用打下坚实的基础。

View File

@@ -0,0 +1,700 @@
# MySQL综合能力考核大学信息系统项目
## 导言:大学数据库项目 - 一次实践性考核
欢迎参加本次MySQL综合能力考核。本次考核旨在通过一个完整的、贴近实际的项目全面评估您对MySQL数据库的掌握程度。您将扮演一名数据库工程师负责为一个新成立的大学设计、构建并管理其核心的学生信息系统数据库。
这个项目不仅仅是一系列孤立的SQL命令测试它模拟了数据库开发的整个生命周期从根据业务需求定义数据结构DDL到填充和维护数据DML再到从中提取有价值的信息DQL最后确保复杂操作的数据一致性TCL
### 场景概述与核心业务规则
大学信息系统的核心实体及其业务规则如下:
+ **讲师 (Instructors):** 每位讲师拥有唯一的ID、姓名、电子邮件和入职日期。电子邮件地址必须是唯一的。
+ **课程 (Courses):** 每门课程拥有唯一的ID、课程名称、学分并且由一位讲师负责授课。
+ **学生 (Students):** 每位学生拥有唯一的ID、姓名、电子邮件、出生日期和专业。电子邮件地址必须是唯一的。
+ **选课记录 (Enrollments):** 这是连接学生和课程的关键。它记录了哪个学生选修了哪门课程,以及他们获得的分数。这体现了学生与课程之间的多对多关系。
### 实体关系描述 (ERD)
+ 一名 **讲师 (Instructor)** 可以教授多门 **课程 (Courses)**。(一对多关系)
+ 一名 **学生 (Student)** 可以选修多门 **课程 (Courses)**
+ 一门 **课程 (Course)** 可以被多名 **学生 (Student)** 选修。(学生与课程之间是多对多关系,因此需要`Enrollments`这个中间表来实现)
在开始编写任何SQL代码之前深刻理解这些业务规则和实体间的关系至关重要。例如“电子邮件必须唯一”这一业务规则将直接转化为数据库表中的`UNIQUE`约束;而“学生与课程的多对多关系”则决定了我们必须创建一个独立的`Enrollments`表,并通过外键将其与`Students`表和`Courses`表关联起来。这种从业务需求到数据库逻辑结构的转换,是数据库设计的核心思想,也是本次考核贯穿始终的考察重点。
现在,请根据以下题目,逐步完成这个大学数据库的构建与管理。
* * *
## 第一部分: foundational Architecture - 数据定义语言 (DDL)
本部分将考察您使用DDL创建和管理数据库基本结构的能力。一个设计良好、结构清晰的数据库是所有数据操作的基础。您将从零开始构建起整个大学信息系统的骨架。
1. 创建一个名为 `university_db` 的新数据库,并设置其默认字符集为 `utf8mb4`
2. 切换到 `university_db` 数据库。创建一个名为 `instructors` 的表,用于存储讲师信息。该表应包含以下列:
+ `instructor_id`: 整数类型,作为主键,自动增长。
+ `name`: 变长字符串类型最大长度100不允许为空。
+ `email`: 变长字符串类型最大长度100必须唯一不允许为空。
+ `hire_date`: 日期类型,不允许为空。
3. 创建一个名为 `courses` 的表,用于存储课程信息。该表应包含以下列:
+ `course_id`: 整数类型,作为主键,自动增长。
+ `title`: 变长字符串类型最大长度150不允许为空。
+ `credits`: 整数类型,不允许为空。
+ `instructor_id`: 整数类型。此列将用于关联 `instructors` 表。
4. 创建一个名为 `students` 的表,用于存储学生信息。该表应包含以下列:
+ `student_id`: 整数类型,作为主键,自动增长。
+ `name`: 变长字符串类型最大长度100不允许为空。
+ `email`: 变长字符串类型最大长度100必须唯一不允许为空。
+ `date_of_birth`: 日期类型。
+ `major`: 变长字符串类型最大长度50。
5. 创建最关键的连接表 `enrollments`,用于记录学生的选课情况。该表的设计需要体现多对多关系。它应包含以下列:
+ `enrollment_id`: 整数类型,作为主键,自动增长。
+ `student_id`: 整数类型,不允许为空。
+ `course_id`: 整数类型,不允许为空。
+ `grade`: 小数类型总共3位其中1位是小数例如95.5)。
+ **约束**:
+ `student_id``course_id` 的组合必须是唯一的,以防止同一学生重复选修同一门课程。
+ `student_id` 列应作为外键,引用 `students` 表的 `student_id` 列。
+ `course_id` 列应作为外键,引用 `courses` 表的 `course_id` 列。
6.`students` 表中添加一个新的列 `phone_number`类型为变长字符串最大长度为20。
7. 修改 `courses` 表中的 `credits` 列,为其添加一个 `CHECK` 约束确保其值必须大于0。
8.`students` 表的 `major` 列添加一个 `DEFAULT` 约束,如果插入新学生时未指定专业,则默认为 'Undeclared'。
9. 现在,为 `courses` 表中的 `instructor_id` 列添加一个外键约束,使其引用 `instructors` 表的 `instructor_id` 列。
10. 清空 `enrollments` 表中的所有数据,但保留表结构。请写出执行此操作的命令。
11. 完整地删除 `instructors` 表。
12. 解释 `DELETE FROM table_name;``TRUNCATE TABLE table_name;``DROP TABLE table_name;` 这三个命令在功能、性能和使用场景上的主要区别。
* * *
## 第二部分Data Population and Management - 数据操作语言 (DML)
在数据库结构搭建完毕后我们需要向其中填充数据并根据业务变化进行维护。本部分将考察您使用DML语句进行数据插入、更新和删除的能力。您将亲身体验DDL中设置的约束是如何在DML操作中发挥作用的。
**准备工作:** 如果您在上一部分执行了第11题删除了`instructors`表),请重新创建它,以确保后续操作可以正常进行。
13.`instructors` 表中插入以下三位讲师的数据:
+ 姓名: '张磊', 邮箱: 'zhang.lei@university.edu', 入职日期: '2020-08-15'
+ 姓名: '李静', 邮箱: 'li.jing@university.edu', 入职日期: '2019-07-01'
+ 姓名: '王浩', 邮箱: 'wang.hao@university.edu', 入职日期: '2021-01-20'
14. 使用一条 `INSERT` 语句向 `students` 表中插入以下两位学生的数据:
+ 姓名: '赵辰', 邮箱: 'zhao.chen@student.edu', 生日: '2002-05-10', 专业: 'Computer Science'
+ 姓名: '孙悦', 邮箱: 'sun.yue@student.edu', 生日: '2003-09-22', 专业: 'Data Science'
15. 再向 `students` 表中插入一位学生,但不指定其专业,以测试默认值约束:
+ 姓名: '周毅', 邮箱: 'zhou.yi@student.edu', 生日: '2002-11-30'
16. 根据 `instructors` 表中的数据,向 `courses` 表中插入以下课程。请确保 `instructor_id` 与讲师姓名对应正确假设张磊ID为1李静ID为2王浩ID为3
+ 课程名: 'Introduction to Programming', 学分: 4, 讲师ID: 1
+ 课程名: 'Advanced Databases', 学分: 4, 讲师ID: 2
+ 课程名: 'Web Development', 学分: 3, 讲师ID: 1
+ 课程名: 'Machine Learning', 学分: 3, 讲师ID: 3
17. 为学生选课。向 `enrollments` 表中插入以下记录假设赵辰ID为1孙悦ID为2周毅ID为3课程ID从1开始
+ 赵辰 选修 'Introduction to Programming',成绩为 92.5
+ 赵辰 选修 'Advanced Databases',成绩为 88.0
+ 孙悦 选修 'Introduction to Programming',成绩为 95.0
+ 孙悦 选修 'Machine Learning',成绩为 97.5
18. **(挑战题)** 尝试向 `enrollments` 表中插入一条记录,其中 `student_id` 为 99一个不存在的学生ID`course_id` 为 1。执行该命令并解释为什么会失败。这个失败说明了什么
19. 学生“赵辰”的邮箱地址输入有误,请将其邮箱更新为 'zhao.chen.new@student.edu'。
20. 由于教学计划调整,'Web Development' 这门课程的授课教师需要更换为“李静”假设其ID为2。请更新 `courses` 表。
21. 学校决定将所有 'Computer Science' 专业的学生的专业名称更新为 'Computer Science and Technology'。请执行此批量更新操作。
22. 学生“孙悦”决定退选 'Machine Learning' 这门课。请从 `enrollments` 表中删除对应的选课记录。
23. **(挑战题)** 尝试从 `instructors` 表中删除讲师“张磊”假设其ID为1。执行该命令并解释为什么它很可能会失败。
24. 承接上一题,如果要成功删除讲师“张磊”,需要先执行哪些操作?请描述正确的操作步骤。
* * *
## 第三部分Information Retrieval and Analysis - 数据查询语言 (DQL)
数据库的核心价值在于能够快速、准确地提取所需信息。本部分是考核的重点将全面检验您使用DQL从简单查询到复杂分析的能力。所有查询都基于您在第二部分中插入的数据。
### A. 基础查询 (10道)
25. 查询 `students` 表中的所有学生信息。
26. 查询 `instructors` 表中所有讲师的姓名和邮箱地址。
27. 查询 `courses` 表中学分大于3的所有课程的完整信息。
28. 查询所有专业为 'Data Science' 的学生姓名和出生日期。
29. 查询课程名称中包含 'Introduction' 关键字的所有课程。
30. 查询所有姓“张”的讲师信息。
31. 查询在2020年1月1日之后入职的讲师姓名和入职日期。
32. 查询专业为 'Computer Science and Technology' 或 'Data Science' 的所有学生信息。
33. 查询 `enrollments` 表中所有成绩在85到95分之间包含85和95的选课记录。
34. 查询所有课程信息,并按学分从高到低排序。如果学分相同,则按课程名称的字母顺序排序。
### B. 聚合函数与分组查询 (10道)
35. 统计 `students` 表中共有多少名学生。
36. 计算 `courses` 表中所有课程的平均学分。
37. 找出 `enrollments` 表中记录的最高成绩。
38. 统计每个专业major分别有多少名学生。
39. 计算每门课程的平均成绩。查询结果应显示课程ID和对应的平均分。
40. 统计每位讲师instructor负责教授的课程数量。查询结果应显示讲师ID和课程数量。
41. 找出选修课程数量超过1门的学生的ID。
42. 查询每个专业的学生中,年龄最小的学生的出生日期。
43. 统计所有课程中,总共有多少名学生进行了选课(同一个学生选多门课算多次)。
44. 找出平均成绩最高的课程的ID。
### C. 连接查询 (10道)
45. 使用 `INNER JOIN` 查询所有学生的姓名以及他们选修的课程名称。
46. 查询所有选课记录,并同时显示学生的姓名、课程的名称以及该课程的分数。
47. 查询讲师“张磊”所教授的所有课程的名称和学分。
48. 列出所有学生及其选修课程的授课讲师的姓名。查询结果应包含三列:学生姓名, 课程名称, 讲师姓名。
49. **(数据准备)** 为了测试 `LEFT JOIN`,请先插入一位没有选修任何课程的新学生:
SQL
```
INSERT INTO students (name, email, date_of_birth, major) VALUES ('钱坤', 'qian.kun@student.edu', '2004-01-15', 'Physics');
```
现在,使用 `LEFT JOIN` 查询所有学生的姓名以及他们选修的课程名称。确保那位没有选课的学生(钱坤)也出现在结果中。
50. **(数据准备)** 为了测试 `RIGHT JOIN`,请先插入一门没有任何学生选修的新课程:
SQL
```
INSERT INTO courses (title, credits, instructor_id) VALUES ('Quantum Physics', 4, 2);
```
现在查询所有课程的名称以及选修该课程的学生人数。确保那门没有学生选修的课程也出现在结果中且学生人数为0。
51. 查询所有选修了学分等于4的课程的学生姓名并去除重复的姓名。
52. 查询所有成绩高于90分的学生姓名和他们所选的课程名称。
53. 查询与“赵辰”在同一个专业的所有其他学生的姓名。
54. 列出所有讲师的姓名以及他们所教授课程的平均成绩。
### D. 高阶查询 (含`HAVING`子句与子查询) (6道)
55. 使用 `GROUP BY` 和 `HAVING` 子句找出选修人数超过1人的所有课程的ID和选修人数。
56. 解释 `WHERE` 子句和 `HAVING` 子句在功能和使用时机上的核心区别。
57. 使用子查询找出由讲师“李静”教授的所有课程的ID。然后基于这个ID列表在 `enrollments` 表中找出所有选修了这些课程的学生的ID。
58. 使用子查询,查询选修了 'Advanced Databases' 这门课的所有学生的姓名。
59. 查询比所有 'Data Science' 专业学生的平均年龄都要大的学生姓名。(提示:需要计算 'Data Science' 学生的平均出生日期,然后进行比较)。
60. 查询每门课程中获得最高分的学生的姓名和该课程的名称。
* * *
## 第四部分Ensuring Consistency - 事务控制语言 (TCL)
在真实的数据库应用中一系列操作往往需要被视为一个不可分割的整体。例如银行转账包含“A账户扣款”和“B账户收款”两个步骤必须同时成功或同时失败。本部分将通过场景题考察您使用TCL来保证复杂业务逻辑下数据一致性的能力。
61. **场景一:成功注册** 学生“周毅”ID为3希望注册选修“Web Development”ID为3这门课程。这个注册过程包含两个步骤 a. 在 `enrollments` 表中插入一条新的选课记录成绩暂时为NULL。 b. 假设学校有一个 `student_accounts` 表(我们在此不创建,仅作为逻辑假设),需要更新该学生的账户余额,扣除相应学费。 请编写一个事务将步骤a的操作包含在内。由于操作会成功最终提交该事务。
62. 在上题的事务中,`COMMIT` 命令起到了什么作用?
63. 为什么将这两个步骤(或更多步骤)捆绑在一个事务中是至关重要的?
64. 启动一个事务的SQL命令是什么
65. **场景二:注册失败与回滚** 学生“钱坤”ID为4尝试注册“Machine Learning”ID为4课程但系统检查发现他未满足前置课程要求因此注册失败。整个操作流程如下 a. 开始一个事务。 b. 在 `enrollments` 表中插入一条选课记录。 c. 系统检查前置课程,发现不满足条件,因此决定中止操作。 请编写这个事务,并在操作中止时使用 `ROLLBACK` 命令,确保数据库状态恢复到事务开始之前。
66. 在上题的场景中,`ROLLBACK` 命令具体做了什么?
67. 如果在执行 `ROLLBACK` 之前,没有使用 `START TRANSACTION` 或等效命令开启事务,会发生什么?
68. **场景三:带保存点的部分回滚** 一位管理员正在为学生“孙悦”ID为2批量注册三门课程。流程如下 a. 开始一个事务。 b. 成功注册第一门课 'Advanced Databases' (ID 2)。 c. 创建一个保存点,名为 `savepoint_1`。 d. 成功注册第二门课 'Web Development' (ID 3)。 e. 创建一个保存点,名为 `savepoint_2`。 f. 尝试注册第三门课 'Quantum Physics' (ID 5),但发现该课程已满员,注册失败。 管理员决定保留前两次成功的注册但撤销第三次失败的尝试。请编写SQL序列使用 `SAVEPOINT` 和 `ROLLBACK TO` 来实现这个需求,并最终提交前两次成功的注册。
69. 在上题中,`ROLLBACK TO savepoint_2;` 命令执行后,数据库处于什么状态?
70. 为什么在这种多步操作中,使用 `SAVEPOINT` 比直接使用 `ROLLBACK` 更加灵活?
71. `AUTOCOMMIT` 是MySQL中的一个重要概念。请解释它的含义以及它与我们手动执行 `START TRANSACTION`、`COMMIT`、`ROLLBACK` 之间的关系。
* * *
## 附录:综合能力考核答案
### 第一部分DDL答案
1. SQL
```
CREATE DATABASE university_db CHARACTER SET utf8mb4;
```
2. SQL
```
USE university_db;
CREATE TABLE instructors (
instructor_id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100) NOT NULL,
email VARCHAR(100) NOT NULL UNIQUE,
hire_date DATE NOT NULL
);
```
3. SQL
```
CREATE TABLE courses (
course_id INT PRIMARY KEY AUTO_INCREMENT,
title VARCHAR(150) NOT NULL,
credits INT NOT NULL,
instructor_id INT
);
```
4. SQL
```
CREATE TABLE students (
student_id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(100) NOT NULL,
email VARCHAR(100) NOT NULL UNIQUE,
date_of_birth DATE,
major VARCHAR(50)
);
```
5. SQL
```
CREATE TABLE enrollments (
enrollment_id INT PRIMARY KEY AUTO_INCREMENT,
student_id INT NOT NULL,
course_id INT NOT NULL,
grade DECIMAL(3, 1),
UNIQUE (student_id, course_id),
FOREIGN KEY (student_id) REFERENCES students(student_id),
FOREIGN KEY (course_id) REFERENCES courses(course_id)
);
```
6. SQL
```
ALTER TABLE students ADD COLUMN phone_number VARCHAR(20);
```
7. SQL
```
ALTER TABLE courses ADD CONSTRAINT chk_credits CHECK (credits > 0);
```
8. SQL
```
ALTER TABLE students ALTER COLUMN major SET DEFAULT 'Undeclared';
```
9. SQL
```
ALTER TABLE courses ADD CONSTRAINT fk_instructor
FOREIGN KEY (instructor_id) REFERENCES instructors(instructor_id);
```
10. SQL
```
TRUNCATE TABLE enrollments;
```
11. SQL
```
DROP TABLE instructors;
```
12. **`DELETE FROM table_name;`**:
+ **功能**: 逐行删除表中的数据,可以与 `WHERE` 子句配合使用删除特定行。
+ **性能**: 相对较慢,因为它会为每一行记录删除日志。
+ **特点**: 触发`DELETE`触发器不重置自增ID。 **`TRUNCATE TABLE table_name;`**:
+ **功能**: 快速删除表中的所有行。
+ **性能**: 非常快,因为它通过释放数据页来清空表,而不是逐行删除。
+ **特点**: 不触发`DELETE`触发器通常会重置自增ID不能与`WHERE`子句一起使用。 **`DROP TABLE table_name;`**:
+ **功能**: 完全删除整个表,包括表结构、数据、索引、约束和触发器。
+ **性能**: 非常快。
+ **特点**: 表将不复存在,需要`CREATE TABLE`才能重新使用。这是最具破坏性的操作。
### 第二部分DML答案
13. SQL
```
INSERT INTO instructors (name, email, hire_date) VALUES
('张磊', 'zhang.lei@university.edu', '2020-08-15'),
('李静', 'li.jing@university.edu', '2019-07-01'),
('王浩', 'wang.hao@university.edu', '2021-01-20');
```
14. SQL
```
INSERT INTO students (name, email, date_of_birth, major) VALUES
('赵辰', 'zhao.chen@student.edu', '2002-05-10', 'Computer Science'),
('孙悦', 'sun.yue@student.edu', '2003-09-22', 'Data Science');
```
15. SQL
```
INSERT INTO students (name, email, date_of_birth) VALUES
('周毅', 'zhou.yi@student.edu', '2002-11-30');
-- 查询后会发现他的major字段值为'Undeclared'
```
16. SQL
```
INSERT INTO courses (title, credits, instructor_id) VALUES
('Introduction to Programming', 4, 1),
('Advanced Databases', 4, 2),
('Web Development', 3, 1),
('Machine Learning', 3, 3);
```
17. SQL
```
INSERT INTO enrollments (student_id, course_id, grade) VALUES
(1, 1, 92.5),
(1, 2, 88.0),
(2, 1, 95.0),
(2, 4, 97.5);
```
18. SQL
```
INSERT INTO enrollments (student_id, course_id) VALUES (99, 1);
```
**解释**: 该命令会失败,并报告一个外键约束错误 (`FOREIGN KEY constraint fails`)。 **原因**: 因为我们在 `enrollments` 表的 `student_id` 列上定义了一个外键,它引用了 `students` 表的 `student_id` 列。这意味着任何试图插入到 `enrollments` 表的 `student_id` 的值,都必须首先存在于 `students` 表中。由于 `students` 表中不存在ID为99的学生数据库为了维护引用完整性拒绝了这次插入操作。这完美地展示了DDL中定义的约束是如何保证数据一致性和有效性的。
19. SQL
```
UPDATE students SET email = 'zhao.chen.new@student.edu' WHERE name = '赵辰';
```
20. SQL
```
UPDATE courses SET instructor_id = 2 WHERE title = 'Web Development';
```
21. SQL
```
UPDATE students SET major = 'Computer Science and Technology' WHERE major = 'Computer Science';
```
22. SQL
```
DELETE FROM enrollments WHERE student_id = 2 AND course_id = 4;
```
23. SQL
```
DELETE FROM instructors WHERE instructor_id = 1;
```
**解释**: 该命令很可能会失败,并报告一个外键约束错误。 **原因**: 因为讲师“张磊”ID为1在 `courses` 表中被引用了('Introduction to Programming' 和 'Web Development' 这两门课的 `instructor_id` 都是1。为了保护数据的引用完整性数据库禁止删除一个正在被其他表引用的记录。
24. **正确操作步骤**:
1. 必须先处理掉 `courses` 表中对讲师“张磊”的引用。有两种方式:
+ **方式一(重新分配课程)**: 将他教授的课程的 `instructor_id` 更新为另一位讲师的ID或者设置为 `NULL`如果该列允许为NULL
+ **方式二(删除课程)**: 删除所有由他教授的课程(但这可能会引发连锁问题,因为课程可能已被学生选修)。
2. 在解除了所有外键引用之后,才能安全地从 `instructors` 表中删除讲师“张磊”的记录。 例如,先执行更新:`UPDATE courses SET instructor_id = NULL WHERE instructor_id = 1;` 然后执行删除:`DELETE FROM instructors WHERE instructor_id = 1;`
### 第三部分DQL答案
25. `SELECT * FROM students;`
26. `SELECT name, email FROM instructors;`
27. `SELECT * FROM courses WHERE credits > 3;`
28. `SELECT name, date_of_birth FROM students WHERE major = 'Data Science';`
29. `SELECT * FROM courses WHERE title LIKE '%Introduction%';`
30. `SELECT * FROM instructors WHERE name LIKE '张%';`
31. `SELECT name, hire_date FROM instructors WHERE hire_date > '2020-01-01';`
32. `SELECT * FROM students WHERE major IN ('Computer Science and Technology', 'Data Science');`
33. `SELECT * FROM enrollments WHERE grade BETWEEN 85 AND 95;`
34. `SELECT * FROM courses ORDER BY credits DESC, title ASC;`
35. `SELECT COUNT(*) FROM students;`
36. `SELECT AVG(credits) FROM courses;`
37. `SELECT MAX(grade) FROM enrollments;`
38. `SELECT major, COUNT(*) FROM students GROUP BY major;`
39. `SELECT course_id, AVG(grade) FROM enrollments GROUP BY course_id;`
40. `SELECT instructor_id, COUNT(*) FROM courses GROUP BY instructor_id;`
41. `SELECT student_id FROM enrollments GROUP BY student_id HAVING COUNT(course_id) > 1;`
42. `SELECT major, MAX(date_of_birth) FROM students GROUP BY major;` -- 年龄最小,出生日期最大
43. `SELECT COUNT(*) FROM enrollments;`
44. `SELECT course_id FROM enrollments GROUP BY course_id ORDER BY AVG(grade) DESC LIMIT 1;`
45. `SELECT s.name, c.title FROM students s INNER JOIN enrollments e ON s.student_id = e.student_id INNER JOIN courses c ON e.course_id = c.course_id;`
46. `SELECT s.name, c.title, e.grade FROM students s JOIN enrollments e ON s.student_id = e.student_id JOIN courses c ON e.course_id = c.course_id;`
47. `SELECT c.title, c.credits FROM courses c JOIN instructors i ON c.instructor_id = i.instructor_id WHERE i.name = '张磊';`
48. `SELECT s.name AS student_name, c.title AS course_title, i.name AS instructor_name FROM students s JOIN enrollments e ON s.student_id = e.student_id JOIN courses c ON e.course_id = c.course_id JOIN instructors i ON c.instructor_id = i.instructor_id;`
49. `SELECT s.name, c.title FROM students s LEFT JOIN enrollments e ON s.student_id = e.student_id LEFT JOIN courses c ON e.course_id = c.course_id;`
50. `SELECT c.title, COUNT(e.student_id) AS number_of_students FROM courses c LEFT JOIN enrollments e ON c.course_id = e.course_id GROUP BY c.course_id, c.title;`
51. `SELECT DISTINCT s.name FROM students s JOIN enrollments e ON s.student_id = e.student_id JOIN courses c ON e.course_id = c.course_id WHERE c.credits = 4;`
52. `SELECT s.name, c.title FROM students s JOIN enrollments e ON s.student_id = e.student_id JOIN courses c ON e.course_id = c.course_id WHERE e.grade > 90;`
53. `SELECT s2.name FROM students s1 JOIN students s2 ON s1.major = s2.major WHERE s1.name = '赵辰' AND s2.name!= '赵辰';`
54. `SELECT i.name, AVG(e.grade) AS average_grade FROM instructors i JOIN courses c ON i.instructor_id = c.instructor_id JOIN enrollments e ON c.course_id = e.course_id GROUP BY i.instructor_id, i.name;`
55. `SELECT course_id, COUNT(student_id) AS num_students FROM enrollments GROUP BY course_id HAVING COUNT(student_id) > 1;`
56. **`WHERE` vs `HAVING`**:
+ **作用对象不同**: `WHERE` 子句作用于原始的表数据,在数据分组(`GROUP BY`)之前进行过滤。`HAVING` 子句作用于 `GROUP BY` 之后生成的结果集,对分组后的聚合结果进行过滤。
+ **使用位置不同**: `WHERE` 必须在 `GROUP BY` 之前,`HAVING` 必须在 `GROUP BY` 之后。
+ **可使用函数不同**: `WHERE` 子句中不能使用聚合函数(如 `COUNT()`, `AVG()`),而 `HAVING` 子句专门用于配合聚合函数进行条件判断。
57. SQL
```
SELECT student_id FROM enrollments
WHERE course_id IN (
SELECT course_id FROM courses
WHERE instructor_id = (
SELECT instructor_id FROM instructors WHERE name = '李静'
)
);
```
58. SQL
```
SELECT name FROM students
WHERE student_id IN (
SELECT student_id FROM enrollments
WHERE course_id = (
SELECT course_id FROM courses WHERE title = 'Advanced Databases'
)
);
```
59. SQL
```
SELECT name FROM students
WHERE date_of_birth < (
SELECT AVG(date_of_birth) FROM students WHERE major = 'Data Science'
);
```
60. SQL
```
SELECT s.name, c.title
FROM enrollments e
JOIN students s ON e.student_id = s.student_id
JOIN courses c ON e.course_id = c.course_id
WHERE (e.course_id, e.grade) IN (
SELECT course_id, MAX(grade)
FROM enrollments
GROUP BY course_id
);
```
### 第四部分TCL答案
61. SQL
```
START TRANSACTION;
-- 步骤a: 插入选课记录
INSERT INTO enrollments (student_id, course_id, grade) VALUES (3, 3, NULL);
-- 步骤b: 假设的更新账户操作
-- UPDATE student_accounts SET balance = balance - 100 WHERE student_id = 3;
COMMIT;
```
62. `COMMIT` 命令将事务中执行的所有更改(在此例中是 `INSERT` 语句)永久性地保存到数据库中。一旦提交,这些更改就成为数据库持久状态的一部分,并且对其他用户可见。
63. 事务的重要性在于保证**原子性Atomicity**。注册课程这个业务逻辑包含多个数据库操作,它们必须作为一个整体执行。如果只有插入选课记录成功,而扣费失败,那么数据就会处于不一致的状态。事务确保了这一系列操作要么全部成功(`COMMIT`),要么全部失败(`ROLLBACK`),从而维护了业务逻辑和数据的完整性。
64. `START TRANSACTION;` (或者它的同义词 `BEGIN;`)
65. SQL
```
START TRANSACTION;
-- 尝试插入选课记录
INSERT INTO enrollments (student_id, course_id) VALUES (4, 4);
-- 此时,应用逻辑发现前置课程不满足,决定中止
ROLLBACK;
```
66. `ROLLBACK` 命令会撤销当前事务中自 `START TRANSACTION` 以来所做的所有未提交的更改。在上题中,它会撤销那条 `INSERT` 语句,使得 `enrollments` 表恢复到事务开始前的状态,就好像那条 `INSERT` 语句从未执行过一样。
67. 如果MySQL的 `AUTOCOMMIT` 模式是开启的默认情况那么每一条SQL语句都会被视为一个独立的事务并被立即自动提交。在这种情况下`ROLLBACK` 命令将不起任何作用,因为在它执行之前,前面的 `INSERT` 语句已经永久保存了。因此,要使用 `ROLLBACK`,必须先用 `START TRANSACTION` 显式地开启一个事务。
68. SQL
```
START TRANSACTION;
-- 注册第一门课
INSERT INTO enrollments (student_id, course_id) VALUES (2, 2);
SAVEPOINT savepoint_1;
-- 注册第二门课
INSERT INTO enrollments (student_id, course_id) VALUES (2, 3);
SAVEPOINT savepoint_2;
-- 尝试注册第三门课 (此操作在真实场景中会失败,这里我们用回滚模拟)
-- INSERT INTO enrollments (student_id, course_id) VALUES (2, 5); -- 假设失败
-- 发现失败,回滚到第二个保存点
ROLLBACK TO savepoint_2;
-- 最终提交事务,保留前两次成功的注册
COMMIT;
```
69. 执行 `ROLLBACK TO savepoint_2;` 之后,数据库的状态是:为孙悦注册第二门课的操作(`INSERT` course\_id 3被撤销了但注册第一门课的操作`INSERT` course\_id 2仍然保留在当前事务中等待最终的 `COMMIT` 或 `ROLLBACK`。
70. `SAVEPOINT` 提供了更细粒度的事务控制。在一个长事务中,如果某个中间步骤失败,我们不必撤销整个事务(`ROLLBACK`),而是可以选择性地回滚到某个已知的良好状态(`ROLLBACK TO SAVEPOINT`),然后继续执行其他操作或提交部分结果。这极大地增强了处理复杂业务逻辑时的灵活性。
71. **`AUTOCOMMIT`** 是MySQL的一个系统变量它控制着事务的提交方式。
+ **当 `AUTOCOMMIT = 1` (默认值)**: 每一条SQL语句如 `INSERT`, `UPDATE`, `DELETE`)在执行完毕后都会被立即自动提交,效果等同于在每条语句前后都加上了 `START TRANSACTION` 和 `COMMIT`。
+ **当 `AUTOCOMMIT = 0`**: 自动提交被禁用。你需要手动执行 `START TRANSACTION` 来开启一个事务,并通过 `COMMIT` 或 `ROLLBACK` 来结束它。在手动结束之前,所有的更改都只在当前会话中可见,并且是未提交的状态。 **关系**: 当我们使用 `START TRANSACTION` 时,它会暂时覆盖当前的 `AUTOCOMMIT` 行为,创建一个手动控制的事务块。在这个块内,只有 `COMMIT` 或 `ROLLBACK` 才能结束事务,`AUTOCOMMIT` 的设置被忽略。这使得我们能够将多条语句捆绑在一起实现TCL的核心功能。

View File

@@ -1 +1,11 @@
针对
依照基础[agi_mysql_study.md](agi_mysql_study.md)的要求,修改[0-基础数据结构.md](0-%E5%9F%BA%E7%A1%80%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84.md)中的内容给出完整的MySQL基础数据结构用法的文档
## SQLite 题目
请根据[agi_mysql_study.md](agi_mysql_study.md)的规范,作为一名专业的SQL老师设计从数据格式涵盖DDL DML DQL TCL的一系列题目考察一名初学者对于MySQL学习的掌握能力
每道题目需要有序号
题目设计保持上下文连贯如DQL基于DML插入的数值DDL创建的表结构。如果数据不够请在题目中给出插入数据命令
每个大模块的题目不少于10道
DQL需要给出至少30道题目需要包含inner join, group by, having等高阶查询方法
要求答案放置于最后面,与前面的题目序号一一对应

View File

@@ -0,0 +1,640 @@
# SQLite综合能力考核大学数据库管理实战
## 介绍:大学数据库场景
为了全面考察您对SQLite的掌握程度我们将围绕一个清晰、易于理解的场景来设计所有练习。这种叙事性的方法不仅能提升学习的趣味性还能帮助您理解SQL命令在实际工作中的应用。
**场景描述:** 您将为一所小型大学设计并管理一个数据库。该数据库需要追踪讲师信息、他们教授的课程、注册课程的学生以及学生的选课和成绩详情。这是一个经典的关系模型非常适合用来测试DDL数据定义语言、DML数据操作语言、DQL数据查询语言和TCL事务控制语言的各项概念。
**实体与关系:**
+ **讲师 (Instructors):** 每位讲师拥有姓名、邮箱和办公室编号。
+ **课程 (Courses):** 每门课程拥有课程名称、所属系别、学分,并由一位讲师授课。
+ **学生 (Students):** 每位学生拥有姓名、邮箱和专业。
+ **选课记录 (Enrollments):** 这是一个连接表,用于关联学生和课程,并记录学生在特定课程中获得的成绩。
这个场景经过精心选择,因为它天然地包含了**一对多关系**(一位讲师可以教授多门课程)和**多对多关系**(一名学生可以选修多门课程,一门课程也可以被多名学生选修),为测试连接查询和参照完整性提供了坚实的基础。
* * *
## 第一部分DDL (数据定义语言) - 构建数据库基础
本部分旨在考察您将业务需求转化为逻辑数据库结构的能力。这不仅考验您的DDL命令语法还涉及数据规范化和数据完整性等设计原则。我们将重点考察 `CREATE TABLE`、数据类型与类型亲和性、`PRIMARY KEY``FOREIGN KEY``NOT NULL``UNIQUE``DEFAULT` 以及 `ALTER TABLE` 等命令。
**题目列表:**
1. 在SQLite中外键约束默认是关闭的这可能导致数据完整性问题 。为了确保后续操作的参照完整性,请写出为当前数据库连接开启外键约束强制执行的命令。  
2. 创建一个名为 `Instructors` 的表,用于存储讲师信息。该表应包含以下列:
+ `instructor_id`: 整数类型,作为主键且能自动增长。
+ `first_name`: 文本类型,不能为空。
+ `last_name`: 文本类型,不能为空。
+ `email`: 文本类型,不能为空,且必须保证其值唯一。
3. 创建一个名为 `Students` 的表,用于存储学生信息。该表应包含以下列:
+ `student_id`: 整数类型,作为主键。
+ `first_name`: 文本类型,不能为空。
+ `last_name`: 文本类型,不能为空。
+ `email`: 文本类型,不能为空,且值唯一。
+ `major`: 文本类型,如果未指定,则默认值为 'Undeclared'。
4. 创建一个名为 `Courses` 的表,用于存储课程信息。该表应包含以下列:
+ `course_id`: 整数类型,作为主键。
+ `title`: 文本类型,不能为空。
+ `department`: 文本类型,不能为空(例如 'CSCI')。
+ `credits`: 整数类型。
+ `instructor_id`: 整数类型,该列应作为外键,引用 `Instructors` 表的 `instructor_id` 列。
5. 修改 `Courses` 表中 `instructor_id` 的外键约束。要求当 `Instructors` 表中的某位讲师记录被删除时,`Courses` 表中由该讲师授课的课程的 `instructor_id` 字段应自动被设置为 `NULL` 
6. 创建一个名为 `Enrollments` 的连接表,用于记录学生选修课程的情况。该表应包含:
+ `student_id`: 整数类型。
+ `course_id`: 整数类型。
+ `grade`: 文本类型,用于存储成绩(如 'A', 'B+', 'C' 等)。
+ 将 `student_id``course_id` 的组合设置为主键(复合主键)。  
假设学校规定,每个学生最多只能选修 5 门课程。仅在数据库层面(不依赖应用程序代码),我们如何实现这个约束?
(提示:查阅 SQLite 关于 CHECK 约束和子查询的文档,思考一个创造性的解决方案。)
7.`Enrollments` 表上定义外键约束:
+ `student_id` 列引用 `Students` 表的 `student_id` 列。
+ `course_id` 列引用 `Courses` 表的 `course_id` 列。
8. 修改 `Enrollments` 表的外键约束。要求当 `Students` 表中的某个学生记录被删除时,该学生所有的选课记录也应自动从 `Enrollments` 表中被删除(级联删除)。  
9. 使用 `ALTER TABLE` 命令为 `Instructors` 表添加一个新列,名为 `office_number`,类型为 `TEXT`
10. 使用 `ALTER TABLE` 命令将 `Enrollments` 表重命名为 `Student_Enrollments`
11. 创建一个名为 `Temp_Table` 的临时表(列定义不限),然后写出删除该表的命令。
* * *
## 第二部分DML (数据操作语言) - 填充与修改数据
本部分旨在考察您能否正确地插入、更新和删除数据同时遵守在DDL阶段定义的各项约束。DML操作的成功与否将直接反映您DDL设计的正确性。
**题目列表:**
1.`Instructors` 表中插入以下三位讲师的数据:
+ (1, 'Alan', 'Turing', 'alan.turing@bletchley.edu', 'B-101')
+ (2, 'Grace', 'Hopper', 'grace.hopper@yale.edu', 'C-203')
+ (3, 'John', 'von Neumann', 'john.vn@ias.edu', 'A-305')
2.`Students` 表中插入至少五名学生的数据,确保他们的 `student_id` 从101开始并拥有不同的专业。
SQL
```
INSERT INTO Students (student_id, first_name, last_name, email, major) VALUES
(101, 'Alice', 'Smith', 'alice.smith@university.edu', 'Computer Science'),
(102, 'Bob', 'Johnson', 'bob.johnson@university.edu', 'Physics'),
(103, 'Charlie', 'Brown', 'charlie.brown@university.edu', 'History'),
(104, 'Diana', 'Prince', 'diana.prince@university.edu', 'Computer Science'),
(105, 'Eve', 'Adams', 'eve.adams@university.edu', 'Physics');
```
3. 根据第一步插入的讲师ID向 `Courses` 表中插入以下课程数据:
SQL
```
INSERT INTO Courses (course_id, title, department, credits, instructor_id) VALUES
(201, 'Introduction to CS', 'CSCI', 3, 1),
(202, 'Advanced Compilers', 'CSCI', 4, 2),
(203, 'Game Theory', 'MATH', 3, 3),
(204, 'Databases', 'CSCI', 4, 1),
(205, 'Quantum Mechanics', 'PHYS', 4, NULL);
```
4. 向 `Student_Enrollments` 表注意该表已在DDL部分被重命名中插入以下选课记录
SQL
```
INSERT INTO Student_Enrollments (student_id, course_id, grade) VALUES
(101, 201, 'A'),
(101, 204, 'B+'),
(102, 205, 'A-'),
(103, 203, 'B'),
(104, 201, 'C'),
(104, 202, 'A');
```
5. **测试 `NOT NULL` 约束**:尝试向 `Instructors` 表中插入一条记录,但 `first_name` 字段留空。此操作预期会失败。请写出这条 `INSERT` 语句。
6. **测试 `UNIQUE` 约束**:尝试向 `Students` 表中插入一名新学生,其 `email` 与 'alice.smith@university.edu' 相同。此操作预期会因违反唯一性约束而失败。请写出这条 `INSERT` 语句。
7. **测试 `FOREIGN KEY` 约束**:尝试向 `Courses` 表中插入一门新课程,其 `instructor_id` 为999而这个ID在 `Instructors` 表中并不存在。此操作预期会因违反外键约束而失败。请写出这条 `INSERT` 语句。
8. Alan Turing博士更换了办公室。请使用 `UPDATE` 语句,将他的 `office_number` 更新为 'B-105'。
9. 学校决定将 'CSCI' 系的缩写统一更改为 'COMP'。请使用 `UPDATE` 语句更新 `Courses` 表中所有 'CSCI' 系的课程。
**测试 `连表查询`** 查询学生'Bob Johnson' (student_id=102) 选取的所有课程信息
10. **测试 `ON DELETE CASCADE`**:学生 'Bob Johnson' (student\_id=102) 退学。请从 `Students` 表中删除他的记录。删除后,请查询 `Student_Enrollments` 表,验证他的选课记录是否也已被自动删除。
11. **测试 `ON DELETE SET NULL`**:讲师 'Grace Hopper' (instructor\_id=2) 离职。请从 `Instructors` 表中删除她的记录。删除后,请查询 `Courses` 表,验证她所教授课程的 `instructor_id` 是否已变为 `NULL`。
12. 删除 `Student_Enrollments` 表中所有成绩为 'C' 的记录。
* * *
## 第三部分DQL (数据查询语言) - 查询与分析数据
本部分将全面考察您从数据库中检索和分析数据的能力,从简单的单表查询逐步过渡到复杂的多表聚合查询。
**题目列表:**
#### 基础检索 (1-10)
1. 查询并显示 `Students` 表中的所有列及所有行。
2. 查询所有讲师的姓(`last_name`)和名(`first_name`)。
3. 查询所有属于 'COMP' 系假设已在DML部分更新的课程名称`title`)。
4. 查询所有专业为 'Physics' 的学生信息,并按姓氏(`last_name`)升序排列。
5. 查询学分(`credits`最高的3门课程的所有信息。
6. 查询 `Students` 表中所有不重复的专业(`major`)名称。
7. 查询课程名称(`title`)中包含 'Intro' 关键字的所有课程。
8. 查询专业为 'History' 或 'Physics' 的所有学生信息。
9. 查询所有办公室编号(`office_number`)未被指定的讲师。
10. 查询所有学分在3到4之间包含3和4的课程名称和学分。
#### 聚合与分组 (11-20)
11. 统计 `Students` 表中共有多少名学生。
12. 计算 `Courses` 表中所有课程的平均学分是多少。
13. 统计选修了课程ID为201'Introduction to CS')的学生总人数。
14. 按专业(`major`)分组,统计每个专业的学生人数。
15. 按系别(`department`)分组,统计每个系别开设的课程数量。
16. 查询学生人数超过1人的专业名称以及对应的人数。
17. 按课程(`course_id`)分组,计算每门课程的平均成绩(提示:这在当前数据结构下无法直接计算,但请思考如何统计选课人数)。请统计每门课程的选课人数。
18. 查询开设课程总数超过1门的讲师ID及其开设的课程数。
19. 找出 `Courses` 表中学分最高和最低的课程的学分值。
20. 统计所有 'COMP' 系课程的总学分。
#### 连接查询与复杂查询 (21-32)
21. 使用 `INNER JOIN` 查询所有选修了 'Introduction to CS' 这门课的学生的姓和名。
22. 使用 `INNER JOIN` 连接 `Student_Enrollments` 和 `Students` 表,列出每条选课记录对应的学生全名(姓和名拼接)和成绩。
23. 使用 `INNER JOIN` 连接 `Courses` 和 `Instructors` 表,列出每位讲师的全名以及他们所教授的课程名称。
24. **`LEFT JOIN` 应用**:查询所有讲师的全名,以及他们教授的课程名称。即使某位讲师没有教授任何课程,也需要显示在结果中(其课程名称显示为 `NULL`)。
25. **`LEFT JOIN` 应用**:查询所有没有教授任何课程的讲师的全名。
26. **`LEFT JOIN` 应用**:查询所有学生的姓名,以及他们选修的课程名称。即使某个学生没有选修任何课程,也需要显示在结果中。
27. **`LEFT JOIN` 应用**:查询所有没有选修任何课程的学生的姓名。
28. 使用子查询,查询由 'Alan Turing' 教授的所有课程的名称。
29. 使用子查询,查询选修了至少一门 'COMP' 系课程的所有学生的姓名。
30. 查询选课数量最多的学生的姓名。
31. 使用 `CASE` 语句,查询所有学生的姓名和专业,并添加一列名为 `remark` 的备注。如果专业是 'Computer Science',备注为 'Tech Major';如果专业是 'Physics',备注为 'Science Major';否则备注为 'Arts & Humanities'。
32. **逻辑执行顺序理解**:尝试按专业统计学生人数,并将统计结果列命名为 `student_count`。然后,使用 `WHERE student_count > 1` 来筛选结果。这个查询会失败。请解释为什么会失败,并写出使用 `HAVING` 子句的正确查询 。  
* * *
## 第四部分TCL (事务控制语言) - 保证数据一致性
本部分旨在考察您对事务的理解以及如何使用事务来确保在执行多步操作时数据库的原子性和一致性。ACID原则是数据库管理的核心概念之一 。  
**题目列表:**
1. **基本事务**:开启一个事务,向 `Students` 表中插入一名新学生 (student\_id: 106, name: 'Frank', 'Castle', email: 'frank.castle@university.edu', major: 'Law'),然后提交事务,使更改永久生效。
2. **`ROLLBACK` 场景**:开启一个事务,删除 `Students` 表中所有专业为 'Undeclared' 的学生,然后执行 `ROLLBACK` 命令。最后,查询 `Students` 表,验证被删除的学生记录是否已恢复。
3. **原子操作场景**:学生 'Alice Smith' (student\_id: 101) 决定从 'Databases' (course\_id: 204) 这门课退选,并改选 'Game Theory' (course\_id: 203)。这个操作需要两步:从 `Student_Enrollments` 中删除一条记录,并插入一条新记录。请将这两个操作放在一个事务中,确保它们要么都成功,要么都失败。
4. **约束冲突与自动回滚**:开启一个事务。首先,将学生 'Charlie Brown' (student\_id: 103) 的专业更新为 'Philosophy'。然后,尝试插入一名新学生,但其 `email` 与已存在的学生 'Diana Prince' 冲突。由于第二步违反了 `UNIQUE` 约束会失败,整个事务会自动回滚 。请问,'Charlie Brown' 的专业最终是什么?请通过查询验证。  
5. **`SAVEPOINT` 应用**:在一个事务中,按顺序执行以下操作: a. 插入两名新的 'Art' 专业学生。 b. 创建一个名为 `after_art_students` 的保存点。 c. 插入两名新的 'Music' 专业学生。 d. 回滚到 `after_art_students` 这个保存点。 e. 提交事务。 请问,最终哪些专业的学生被永久保存到了数据库中?
6. **复杂原子操作**为一名学生办理毕业手续。这需要a) 从 `Student_Enrollments` 表中删除该学生的所有选课记录b) 从 `Students` 表中删除该学生的记录。请为学生 'Diana Prince' (student\_id: 104) 编写一个事务来完成此操作,确保数据的一致性。
7. 根据上一题的毕业操作解释ACID属性中的 'A' (Atomicity - 原子性) 是如何体现的。
8. 以 `Student_Enrollments` 表的外键约束为例解释ACID属性中的 'C' (Consistency - 一致性) 是如何保证的。
9. 解释在何种情况下,您可能会选择使用 `BEGIN IMMEDIATE TRANSACTION` 而不是默认的延迟事务(`BEGIN DEFERRED TRANSACTION`)。  
10. 一个脚本需要更新100门不同课程的学分。相比于让SQLite为每条 `UPDATE` 语句自动提交事务为什么将这100条语句包裹在一个单独的事务中会更高效
* * *
## 答案
### 第一部分DDL 答案
1. SQL
```
PRAGMA foreign_keys = ON;
```
2. SQL
```
CREATE TABLE Instructors (
instructor_id INTEGER PRIMARY KEY AUTOINCREMENT,
first_name TEXT NOT NULL,
last_name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE
);
```
3. SQL
```
CREATE TABLE Students (
student_id INTEGER PRIMARY KEY,
first_name TEXT NOT NULL,
last_name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE,
major TEXT DEFAULT 'Undeclared'
);
```
4. SQL
```
CREATE TABLE Courses (
course_id INTEGER PRIMARY KEY,
title TEXT NOT NULL,
department TEXT NOT NULL,
credits INTEGER,
instructor_id INTEGER,
FOREIGN KEY (instructor_id) REFERENCES Instructors(instructor_id)
);
```
5. \-- 需要先删除旧表,再用新的约束创建。在实际操作中,这意味着需要备份数据。
SQL
```
-- 假设表已存在需要先删除再重建或使用ALTER TABLE在新版SQLite中
-- 为简化题目这里提供带ON DELETE SET NULL的完整创建语句
DROP TABLE Courses;
CREATE TABLE Courses (
course_id INTEGER PRIMARY KEY,
title TEXT NOT NULL,
department TEXT NOT NULL,
credits INTEGER,
instructor_id INTEGER,
FOREIGN KEY (instructor_id) REFERENCES Instructors(instructor_id) ON DELETE SET NULL
);
```
6. SQL
```
CREATE TABLE Enrollments (
student_id INTEGER,
course_id INTEGER,
grade TEXT,
PRIMARY KEY (student_id, course_id)
);
```
7. \-- 同样,这里提供带完整外键定义的创建语句
SQL
```
DROP TABLE Enrollments;
CREATE TABLE Enrollments (
student_id INTEGER,
course_id INTEGER,
grade TEXT,
PRIMARY KEY (student_id, course_id),
FOREIGN KEY (student_id) REFERENCES Students(student_id),
FOREIGN KEY (course_id) REFERENCES Courses(course_id)
);
```
8. \-- 提供带ON DELETE CASCADE的完整创建语句
SQL
```
DROP TABLE Enrollments;
CREATE TABLE Enrollments (
student_id INTEGER,
course_id INTEGER,
grade TEXT,
PRIMARY KEY (student_id, course_id),
FOREIGN KEY (student_id) REFERENCES Students(student_id) ON DELETE CASCADE,
FOREIGN KEY (course_id) REFERENCES Courses(course_id)
);
```
9. SQL
```
ALTER TABLE Instructors ADD COLUMN office_number TEXT;
```
10. SQL
```
ALTER TABLE Enrollments RENAME TO Student_Enrollments;
```
11. SQL
```
CREATE TABLE Temp_Table (id INTEGER);
DROP TABLE Temp_Table;
```
### 第二部分DML 答案
1. SQL
```
-- 注意在DDL第9题后Instructors表结构已改变。
-- 为确保数据插入成功需要更新instructor_id=1的记录。
INSERT INTO Instructors (instructor_id, first_name, last_name, email) VALUES
(1, 'Alan', 'Turing', 'alan.turing@bletchley.edu'),
(2, 'Grace', 'Hopper', 'grace.hopper@yale.edu'),
(3, 'John', 'von Neumann', 'john.vn@ias.edu');
UPDATE Instructors SET office_number = 'B-101' WHERE instructor_id = 1;
UPDATE Instructors SET office_number = 'C-203' WHERE instructor_id = 2;
UPDATE Instructors SET office_number = 'A-305' WHERE instructor_id = 3;
```
2. SQL
```
INSERT INTO Students (student_id, first_name, last_name, email, major) VALUES
(101, 'Alice', 'Smith', 'alice.smith@university.edu', 'Computer Science'),
(102, 'Bob', 'Johnson', 'bob.johnson@university.edu', 'Physics'),
(103, 'Charlie', 'Brown', 'charlie.brown@university.edu', 'History'),
(104, 'Diana', 'Prince', 'diana.prince@university.edu', 'Computer Science'),
(105, 'Eve', 'Adams', 'eve.adams@university.edu', 'Physics');
```
3. SQL
```
INSERT INTO Courses (course_id, title, department, credits, instructor_id) VALUES
(201, 'Introduction to CS', 'CSCI', 3, 1),
(202, 'Advanced Compilers', 'CSCI', 4, 2),
(203, 'Game Theory', 'MATH', 3, 3),
(204, 'Databases', 'CSCI', 4, 1),
(205, 'Quantum Mechanics', 'PHYS', 4, NULL);
```
4. SQL
```
INSERT INTO Student_Enrollments (student_id, course_id, grade) VALUES
(101, 201, 'A'),
(101, 204, 'B+'),
(102, 205, 'A-'),
(103, 203, 'B'),
(104, 201, 'C'),
(104, 202, 'A');
```
5. SQL
```
INSERT INTO Instructors (last_name, email) VALUES ('Curie', 'marie.curie@sorbonne.edu');
-- 这将返回一个错误,因为 first_name 违反了 NOT NULL 约束。
```
6. SQL
```
INSERT INTO Students (student_id, first_name, last_name, email, major)
VALUES (106, 'Alex', 'Ray', 'alice.smith@university.edu', 'Biology');
-- 这将返回一个错误,因为 email 违反了 UNIQUE 约束。
```
7. SQL
```
INSERT INTO Courses (course_id, title, department, credits, instructor_id)
VALUES (206, 'Artificial Intelligence', 'CSCI', 4, 999);
-- 这将返回一个错误,因为 instructor_id 违反了 FOREIGN KEY 约束。
```
8. SQL
```
UPDATE Instructors SET office_number = 'B-105' WHERE instructor_id = 1;
```
9. SQL
```
UPDATE Courses SET department = 'COMP' WHERE department = 'CSCI';
```
10. SQL
```
DELETE FROM Students WHERE student_id = 102;
-- 查询验证 (应返回0行)
SELECT * FROM Student_Enrollments WHERE student_id = 102;
```
11. SQL
```
DELETE FROM Instructors WHERE instructor_id = 2;
-- 查询验证 (instructor_id应为NULL)
SELECT * FROM Courses WHERE course_id = 202;
```
12. SQL
```
DELETE FROM Student_Enrollments WHERE grade = 'C';
```
### 第三部分DQL 答案
1. `SELECT * FROM Students;`
2. `SELECT last_name, first_name FROM Instructors;`
3. `SELECT title FROM Courses WHERE department = 'COMP';`
4. `SELECT * FROM Students WHERE major = 'Physics' ORDER BY last_name ASC;`
5. `SELECT * FROM Courses ORDER BY credits DESC LIMIT 3;`
6. `SELECT DISTINCT major FROM Students;`
7. `SELECT * FROM Courses WHERE title LIKE '%Intro%';`
8. `SELECT * FROM Students WHERE major IN ('History', 'Physics');`
9. `SELECT * FROM Instructors WHERE office_number IS NULL;`
10. `SELECT title, credits FROM Courses WHERE credits BETWEEN 3 AND 4;`
11. `SELECT COUNT(*) FROM Students;`
12. `SELECT AVG(credits) FROM Courses;`
13. `SELECT COUNT(*) FROM Student_Enrollments WHERE course_id = 201;`
14. `SELECT major, COUNT(*) FROM Students GROUP BY major;`
15. `SELECT department, COUNT(*) FROM Courses GROUP BY department;`
16. `SELECT major, COUNT(*) FROM Students GROUP BY major HAVING COUNT(*) > 1;`
17. `SELECT course_id, COUNT(student_id) AS num_students FROM Student_Enrollments GROUP BY course_id;`
18. `SELECT instructor_id, COUNT(*) FROM Courses GROUP BY instructor_id HAVING COUNT(*) > 1;`
19. `SELECT MAX(credits), MIN(credits) FROM Courses;`
20. `SELECT SUM(credits) FROM Courses WHERE department = 'COMP';`
21. `SELECT s.first_name, s.last_name FROM Students s INNER JOIN Student_Enrollments se ON s.student_id = se.student_id INNER JOIN Courses c ON se.course_id = c.course_id WHERE c.title = 'Introduction to CS';`
22. `SELECT s.first_name || ' ' | | s.last_name AS full_name, se.grade FROM Students s INNER JOIN Student_Enrollments se ON s.student_id = se.student_id;`
23. `SELECT i.first_name || ' ' | | i.last_name AS instructor_name, c.title FROM Instructors i INNER JOIN Courses c ON i.instructor_id = c.instructor_id;`
24. `SELECT i.first_name, i.last_name, c.title FROM Instructors i LEFT JOIN Courses c ON i.instructor_id = c.instructor_id;`
25. `SELECT i.first_name, i.last_name FROM Instructors i LEFT JOIN Courses c ON i.instructor_id = c.instructor_id WHERE c.course_id IS NULL;`
26. `SELECT s.first_name, s.last_name, c.title FROM Students s LEFT JOIN Student_Enrollments se ON s.student_id = se.student_id LEFT JOIN Courses c ON se.course_id = c.course_id;`
27. `SELECT s.first_name, s.last_name FROM Students s LEFT JOIN Student_Enrollments se ON s.student_id = se.student_id WHERE se.course_id IS NULL;`
28. `SELECT title FROM Courses WHERE instructor_id = (SELECT instructor_id FROM Instructors WHERE first_name = 'Alan' AND last_name = 'Turing');`
29. `SELECT first_name, last_name FROM Students WHERE student_id IN (SELECT student_id FROM Student_Enrollments WHERE course_id IN (SELECT course_id FROM Courses WHERE department = 'COMP'));`
30. `SELECT s.first_name, s.last_name FROM Students s JOIN Student_Enrollments se ON s.student_id = se.student_id GROUP BY s.student_id ORDER BY COUNT(se.course_id) DESC LIMIT 1;`
31. ```sql SELECT first_name, last_name, major, CASE major WHEN 'Computer Science' THEN 'Tech Major' WHEN 'Physics' THEN 'Science Major' ELSE 'Arts & Humanities' END AS remark FROM Students; ```
32. **解释**失败是因为SQL的逻辑执行顺序是`FROM`->`WHERE`->`GROUP BY`->`HAVING`->`SELECT`->`ORDER BY`。`WHERE`子句在`SELECT`子句之前执行,因此在执行`WHERE`时,别名`student_count `尚未被创建。`HAVING`子句在`GROUP BY` 之后执行,用于过滤聚合后的结果,所以可以使用聚合函数。 **正确查询** `sql SELECT major, COUNT(*) AS student_count FROM Students GROUP BY major HAVING COUNT(*) > 1;`
### 第四部分TCL 答案
1. SQL
```
BEGIN TRANSACTION;
INSERT INTO Students (student_id, first_name, last_name, email, major)
VALUES (106, 'Frank', 'Castle', 'frank.castle@university.edu', 'Law');
COMMIT;
```
2. SQL
```
BEGIN TRANSACTION;
-- 假设有一个默认专业为'Undeclared'的学生
INSERT INTO Students (student_id, first_name, last_name, email) VALUES (107, 'John', 'Doe', 'john.doe@university.edu');
DELETE FROM Students WHERE major = 'Undeclared';
ROLLBACK;
-- 查询验证John Doe 应该仍然存在
SELECT * FROM Students WHERE student_id = 107;
```
3. SQL
```
BEGIN TRANSACTION;
DELETE FROM Student_Enrollments WHERE student_id = 101 AND course_id = 204;
INSERT INTO Student_Enrollments (student_id, course_id, grade) VALUES (101, 203, NULL);
COMMIT;
```
4. **最终专业**'History'。因为事务中的第二步失败导致整个事务自动回滚,第一步的 `UPDATE` 操作也被撤销了。 **验证查询**`SELECT major FROM Students WHERE student_id = 103;`
5. **最终结果**:只有 'Art' 专业的学生被永久保存。因为事务回滚到了保存点 `after_art_students`,这撤销了插入 'Music' 专业学生的操作,然后提交了之前的操作。
6. SQL
```
BEGIN TRANSACTION;
DELETE FROM Student_Enrollments WHERE student_id = 104;
DELETE FROM Students WHERE student_id = 104;
COMMIT;
```
7. **原子性Atomicity** 体现在毕业操作的两个步骤(删除选课记录和删除学生记录)被视为一个不可分割的单元。如果其中任何一步失败(例如,由于权限问题),整个事务将回滚,数据库将恢复到操作开始前的状态,从而避免了“学生记录已删除但选课记录仍存在”这种不一致的状态。
8. **一致性Consistency** 保证了事务将数据库从一个有效的状态转换到另一个有效的状态。`Student_Enrollments` 表的外键约束确保了任何插入的选课记录都必须关联一个真实存在的学生和一个真实存在的课程。任何试图破坏这种关系的DML操作如为不存在的学生添加选课记录都会失败从而维护了数据库在事务前后的一致性。
9. 默认的延迟事务(`DEFERRED`)直到第一次实际读写数据库时才加锁。而 `IMMEDIATE` 事务在 `BEGIN` 命令执行时就立即尝试获取一个保留锁(写锁的前置锁)。当您预知即将进行写操作,并希望尽早确保事务不会因为其他写操作而失败(返回 `SQLITE_BUSY`)时,应使用 `IMMEDIATE`。这可以减少事务中途失败的可能性。
10. 将100条 `UPDATE` 语句包裹在单个事务中更高效,主要有两个原因:
+ **减少磁盘I/O**SQLite的默认模式下每次提交事务都需要将日志文件同步到磁盘这是一个相对缓慢的操作。100次自动提交意味着100次磁盘同步。而单个事务只需要在最后 `COMMIT` 时进行一次磁盘同步大大减少了I/O开销。
+ **原子性保证**如果脚本在更新到第50条时失败单个事务可以确保所有已做的更改都被回滚数据库保持一致。而100个独立事务则会导致前49次更新已永久保存数据库处于一个不完整的中间状态。

View File

@@ -0,0 +1,271 @@
PRAGMA forien_keys = ON;
PRAGMA forien_keys;
CREATE TABLE instructors (
instructor_id INTEGER PRIMARY KEY AUTOINCREMENT,
first_name TEXT NOT NULL,
last_name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE
);
CREATE TABLE students (
student_id INTEGER PRIMARY KEY,
first_name TEXT NOT NULL,
last_name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE,
major TEXT DEFAULT "Undeclared"
);
CREATE TABLE courses (
course_id INTEGER PRIMARY KEY,
title TEXT NOT NULL,
department TEXT NOT NULL,
credits INTEGER,
instructor_id INTEGER ,
CONSTRAINT fk_courses_instructor FOREIGN KEY(instructor_id) REFERENCES instructors(instructor_id)
);
-- 修改 Courses 表中 instructor_id 的外键约束。要求当 Instructors 表中的某位讲师记录被删除时Courses 表中由该讲师授课的课程的 instructor_id 字段应自动被设置为 NULL 。
CREATE TABLE courses_back AS SELECT * FROM courses;
DROP TABLE courses;
CREATE TABLE courses (
course_id INTEGER PRIMARY KEY,
title TEXT NOT NULL,
department TEXT NOT NULL,
credits INTEGER,
instructor_id INTEGER ,
CONSTRAINT fk_courses_instructor FOREIGN KEY(instructor_id) REFERENCES instructors(instructor_id) ON DELETE SET NULL
);
INSERT INTO courses SELECT * FROM courses_back;
DROP TABLE courses_back;
-- 创建一个名为 Enrollments 的连接表,用于记录学生选修课程的情况。该表应包含:
--
-- student_id: 整数类型。
--
-- course_id: 整数类型。
--
-- grade: 文本类型,用于存储成绩(如 'A', 'B+', 'C' 等)。
--
-- 将 student_id 和 course_id 的组合设置为主键(复合主键)。
create table enrollments (
student_id INTEGER NOT NULL,
course_id INTEGER NOT NULL,
grade TEXT,
-- 想象一下你的身份证号假设前6位是地区码后8位是个人编码。单看地区码无法确定你是谁一个地区有千万人单看个人编码也可能重复不同地区可能有相同个人编码。但“地区码+个人编码”的组合就能在全国范围内唯一锁定你这个人。这个组合就是“复合主键”。
-- 定义复合主键:一个学生不能多次选修同一门课程
PRIMARY KEY (student_id, course_id),
-- 要求当 Students 表中的某个学生记录被删除时,该学生所有的选课记录也应自动从 Enrollments 表中被删除(级联删除)
FOREIGN KEY (student_id) REFERENCES students(student_id) on delete cascade ,
FOREIGN KEY (course_id) REFERENCES courses(course_id)
);
PRAGMA foreign_keys = ON;
-- 使用 ALTER TABLE 命令为 Instructors 表添加一个新列,名为 office_number类型为 TEXT。
alter table instructors add column office_number TEXT;
-- 使用 ALTER TABLE 命令将 Enrollments 表重命名为 Student_Enrollments。
alter table enrollments rename to student_enrollments;
drop table student_enrollments;
drop table sqlite_master;
-- 创建一个名为 Temp_Table 的临时表(列定义不限),然后写出删除该表的命令。
-- 临时表最大的优点是无需手动管理清理,断开连接后自动消失,避免了垃圾数据累积。 wdd:有何意义?
-- 向 Instructors 表中插入以下三位讲师的数据:
--
-- (1, 'Alan', 'Turing', 'alan.turing@bletchley.edu', 'B-101')
--
-- (2, 'Grace', 'Hopper', 'grace.hopper@yale.edu', 'C-203')
--
-- (3, 'John', 'von Neumann', 'john.vn@ias.edu', 'A-305')
insert into instructors values
(1, 'Alan', 'Turing', 'alan.turing@bletchley.edu', 'B-101'),
(2, 'Grace', 'Hopper', 'grace.hopper@yale.edu', 'C-203'),
(3, 'John', 'von Neumann', 'john.vn@ias.edu', 'A-305');
INSERT INTO students VALUES
(101, 'Alice', 'Smith', 'alice.smith@university.edu', 'Computer Science'),
(102, 'Bob', 'Johnson', 'bob.johnson@university.edu', 'Physics'),
(103, 'Charlie', 'Brown', 'charlie.brown@university.edu', 'History'),
(104, 'Diana', 'Prince', 'diana.prince@university.edu', 'Computer Science'),
(105, 'Eve', 'Adams', 'eve.adams@university.edu', 'Physics');
INSERT INTO courses (course_id, title, department, credits, instructor_id) VALUES
(201, 'Introduction to CS', 'CSCI', 3, 1),
(202, 'Advanced Compilers', 'CSCI', 4, 2),
(203, 'Game Theory', 'MATH', 3, 3),
(204, 'Databases', 'CSCI', 4, 1),
(205, 'Quantum Mechanics', 'PHYS', 4, NULL);
INSERT INTO enrollments (student_id, course_id, grade) VALUES
(101, 201, 'A'),
(101, 204, 'B+'),
(102, 205, 'A-'),
(103, 203, 'B'),
(104, 201, 'C'),
(104, 202, 'A');
-- 测试 NOT NULL 约束:尝试向 Instructors 表中插入一条记录,但 first_name 字段留空。此操作预期会失败。请写出这条 INSERT 语句。
insert into instructors (instructor_id, first_name, last_name, office_number, email) values (233, null, "yes", "CS-1", "ok@qq.com");
-- 测试 FOREIGN KEY 约束:尝试向 Courses 表中插入一门新课程,其 instructor_id 为999而这个ID在 Instructors 表中并不存在。此操作预期会因违反外键约束而失败。请写出这条 INSERT 语句。
insert into courses (course_id, title, department, credits, instructor_id) values (123, "测试课程", "fts3tokenize", 12, 999);
-- Alan Turing博士更换了办公室。请使用 UPDATE 语句,将他的 office_number 更新为 'B-105'。
update instructors set office_number="B-105" where first_name="Alan" and last_name="Turing";
-- 学校决定将 'CSCI' 系的缩写统一更改为 'COMP'。请使用 UPDATE 语句更新 Courses 表中所有 'CSCI' 系的课程。
update courses set title="COMP" where title = "CSCI";
-- 测试 ON DELETE CASCADE学生 'Bob Johnson' (student_id=102) 退学。请从 Students 表中删除他的记录。删除后,请查询 Student_Enrollments 表,验证他的选课记录是否也已被自动删除
delete from students where student_id=102;
-- 连表查询 查询学生'Bob Johnson' (student_id=102) 选取的所有课程信息
select e.student_id, e.course_id, c.title, c.department, c.credits from enrollments e
INNER JOIN courses c on e.course_id = c.course_id where e.student_id = 102;
-- 测试 ON DELETE SET NULL讲师 'Grace Hopper' (instructor_id=2) 离职。请从 Instructors 表中删除她的记录。删除后,请查询 Courses 表,验证她所教授课程的 instructor_id 是否已变为 NULL。
select * from instructors i inner join courses c on i.instructor_id = c.instructor_id where i.instructor_id =2;
delete from instructors where instructor_id=2;
-- 删除 Student_Enrollments 表中所有成绩为 'C' 的记录。
select * from enrollments;
delete from enrollments where grade = 'C';
-- 查询并显示 Students 表中的所有列及所有行。
select * from students;
-- 查询所有讲师的姓last_name和名first_name
select last_name, first_name from instructors;
-- 查询所有属于 'COMP' 系假设已在DML部分更新的课程名称title
select title from courses where department = 'COMP';
-- 查询所有专业为 'Physics' 的学生信息并按姓氏last_name升序排列。
select * from students where major = 'Physics' order by last_name asc ;
-- 查询学分credits最高的3门课程的所有信息。
select * from courses order by credits desc limit 3;
-- 查询 Students 表中所有不重复的专业major名称。
select distinct major from students;
-- 查询课程名称title中包含 'Intro' 关键字的所有课程。
select * from courses where title like '%Intro%';
-- 查询专业为 'History' 或 'Physics' 的所有学生信息。
select * from students where major = 'History' or 'Physics';
-- 查询所有办公室编号office_number未被指定的讲师。
select * from instructors where office_number = '' or null;
-- 查询所有学分在3到4之间包含3和4的课程名称和学分。
select * from courses where credits between 3 and 4;
-- 统计 Students 表中共有多少名学生。
select count(*) from students;
-- 计算 Courses 表中所有课程的平均学分是多少。
select avg(credits) avg_credits from courses;
-- 统计选修了课程ID为201'Introduction to CS')的学生总人数。
select count(student_id) from enrollments where course_id = 201;
-- 按专业major分组统计每个专业的学生人数。
select count(student_id) student_count, major from students group by major;
-- 按系别department分组统计每个系别开设的课程数量。
select count(course_id) course_count, courses.department from courses group by department;
-- 查询学生人数超过1人的专业名称以及对应的人数。
SELECT major, COUNT(*) FROM Students GROUP BY major HAVING COUNT(*) > 1;;
-- 按课程course_id分组计算每门课程的平均成绩提示这在当前数据结构下无法直接计算但请思考如何统计选课人数
--
-- 请统计每门课程的选课人数。
select c.course_id, count(e.student_id), e.grade from courses c inner join enrollments e on c.course_id = e.course_id group by c.course_id;
-- 查询开设课程总数超过1门的讲师ID及其开设的课程数。
select i.instructor_id, count(c.course_id) from instructors i inner join courses c on i.instructor_id = c.instructor_id group by i.instructor_id;
-- 找出 Courses 表中学分最高和最低的课程的学分值。
-- select credits from courses group by credits ;
-- 找出学分最高和最低的值
SELECT
MAX(credits) AS highest_credits,
MIN(credits) AS lowest_credits
FROM courses;
-- 如果想要同时查看这些极值对应的课程信息
SELECT *
FROM courses
WHERE credits = (SELECT MAX(credits) FROM courses)
OR credits = (SELECT MIN(credits) FROM courses);
-- 统计所有 'COMP' 系课程的总学分。
select sum(credits) from courses where department='CSCI';
-- 使用 INNER JOIN 查询所有选修了 'Introduction to CS' 这门课的学生的姓和名。
select * from courses c inner join enrollments e on c.course_id = e.course_id inner join students s on s.student_id = e.student_id where c.title = 'Introduction to CS';
-- 使用 INNER JOIN 连接 Student_Enrollments 和 Students 表,列出每条选课记录对应的学生全名(姓和名拼接)和成绩。
select s.last_name || ' ' || s.first_name as full_name, e.grade from enrollments e inner join students s on s.student_id = e.student_id;
-- 使用 INNER JOIN 连接 Courses 和 Instructors 表,列出每位讲师的全名以及他们所教授的课程名称。
select s.last_name || ' ' || s.first_name as full_name, c.title from instructors s inner join courses c on s.instructor_id = c.instructor_id;
-- LEFT JOIN 应用:查询所有讲师的全名,以及他们教授的课程名称。即使某位讲师没有教授任何课程,也需要显示在结果中(其课程名称显示为 NULL
select s.last_name || ' ' || s.first_name as full_name, c.title from instructors s left join courses c on s.instructor_id = c.instructor_id;
-- LEFT JOIN 应用:查询所有没有教授任何课程的讲师的全名。
select s.last_name || ' ' || s.first_name as full_name from instructors s left join courses c on s.instructor_id = c.instructor_id where c.instructor_id is null;
-- LEFT JOIN 应用:查询所有学生的姓名,以及他们选修的课程名称。即使某个学生没有选修任何课程,也需要显示在结果中。
select s.last_name, s.first_name, c.title from students s left join enrollments e on s.student_id = e.student_id left join courses c on e.course_id = c.course_id;
-- LEFT JOIN 应用:查询所有没有选修任何课程的学生的姓名。
select s.last_name, s.first_name , s.student_id from students s where s.student_id not in (select student_id
from enrollments
);
-- 使用子查询,查询由 'Alan Turing' 教授的所有课程的名称。
select c.title from courses c where instructor_id = (
select instructor_id from instructors where first_name = 'Alan' and last_name = 'Turing'
);
-- 使用子查询,查询选修了至少一门 'COMP' 系课程的所有学生的姓名。
select s.first_name, s.last_name from students s where s.student_id = (
select student_id from enrollments where course_id = (
select course_id from courses where department = 'CSCI'
)
);
select count(student_id) from enrollments where course_id = (
select course_id from courses where department = 'CSCI'
);
select course_id from courses where department = 'CSCI';
-- 查询选课数量最多的学生的姓名。
select students.first_name, students.last_name
from students where student_id = (select student_id from enrollments group by student_id order by count(course_id) limit 1);
-- 使用 CASE 语句,查询所有学生的姓名和专业,并添加一列名为 remark 的备注。如果专业是 'Computer Science',备注为 'Tech Major';如果专业是 'Physics',备注为 'Science Major';否则备注为 'Arts & Humanities'。
select students.first_name, students.last_name
-- 逻辑执行顺序理解:尝试按专业统计学生人数,并将统计结果列命名为 student_count。然后使用 WHERE student_count > 1 来筛选结果。这个查询会失败。请解释为什么会失败,并写出使用 HAVING 子句的正确查询 。

View File

@@ -7,4 +7,13 @@
## SQLite引擎流程
请根据[agi_sqlite_study.md](agi_sqlite_study.md)的规范,请详细讲解SQLite中的事务事务对其他进程的读写产生何种影响请分析极端情况下的事务。SQLite中的读写隔离机制是怎样的。
请根据[agi_sqlite_study.md](agi_sqlite_study.md)的规范,请详细讲解SQLite中的事务事务对其他进程的读写产生何种影响请分析极端情况下的事务。SQLite中的读写隔离机制是怎样的。
## SQLite 题目
请根据[agi_sqlite_study.md](agi_sqlite_study.md)的规范,作为一名专业的SQL老师设计从数据格式涵盖DDL DML DQL TCL的一系列题目考察一名初学者对于SQLite学习的掌握能力
每道题目需要有序号
题目设计保持上下文连贯如DQL基于DML插入的数值DDL创建的表结构。如果数据不够请在题目中给出插入数据命令
每个大模块的题目不少于10道
DQL需要给出至少30道题目需要包含inner join, group by, having等高阶查询方法
要求答案放置于最后面,与前面的题目序号一一对应