# SQLite SQL 权威指南：从入门到精通

## 第一部分：基础准备：数据库模式与数据类型

在深入探讨结构化查询语言（SQL）的复杂世界之前，建立一个坚实的基础至关重要。这包括定义一个清晰、一致的数据库模式，作为我们所有示例的试验场，并深入理解SQLite独特的数据类型系统。与其他数据库系统相比，SQLite在此处的行为有其独到之处，理解这些差异是避免常见陷阱和充分利用其性能的关键。

### 1.1 示例数据库模式定义

为了在本指南中提供连贯且实用的示例，我们将使用一个模拟公司运营的数据库模式。该模式包含四个核心表：`departments`（部门）、
`employees`（员工）、`projects`（项目）以及一个用于连接员工和项目的关联表 `employee_projects`。

以下是用于创建这些表的完整数据定义语言（DDL）语句。这些语句不仅定义了表的结构，还通过主键、外键和各种约束来确保数据的完整性和一致性。

**`departments` 表:**

```SQL
CREATE TABLE departments
(
    department_id INTEGER PRIMARY KEY,
    name          TEXT NOT NULL UNIQUE
);
```

+ `department_id`: 部门的唯一标识符，作为主键。

+ `name`: 部门名称，不能为空且必须是唯一的。

**`employees` 表:**

```SQL
CREATE TABLE employees
(
    employee_id   INTEGER PRIMARY KEY,
    first_name    TEXT NOT NULL,
    last_name     TEXT NOT NULL,
    salary        REAL CHECK (salary > 0),
    hire_date     TEXT, -- Stored as TEXT in 'YYYY-MM-DD' format
    department_id INTEGER,
    FOREIGN KEY (department_id) REFERENCES departments (department_id)
);
```

+ `employee_id`: 员工的唯一标识符，作为主键。

+ `first_name`, `last_name`: 员工的姓名，不能为空。

+ `salary`: 员工的薪水，必须是正数。

+ `hire_date`: 雇佣日期，以文本格式存储。

+ `department_id`: 员工所属部门的外键，关联到 `departments` 表。

**`projects` 表:**

```SQL
CREATE TABLE projects
(
    project_id INTEGER PRIMARY KEY,
    name       TEXT NOT NULL UNIQUE,
    start_date TEXT, -- Stored as TEXT in 'YYYY-MM-DD' format
    end_date   TEXT  -- Stored as TEXT in 'YYYY-MM-DD' format
);
```

+ `project_id`: 项目的唯一标识符，作为主键。

+ `name`: 项目名称，不能为空且必须是唯一的。

+ `start_date`, `end_date`: 项目的起止日期。

**`employee_projects` 关联表:**

```SQL
CREATE TABLE employee_projects
(
    employee_id INTEGER,
    project_id  INTEGER,
    PRIMARY KEY (employee_id, project_id),
    FOREIGN KEY (employee_id) REFERENCES employees (employee_id),
    FOREIGN KEY (project_id) REFERENCES projects (project_id)
);
```

+ 该表建立员工和项目之间的多对多关系。

+ `employee_id` 和 `project_id` 共同构成复合主键，确保每个员工在一个项目中只被记录一次。

为了更直观地理解这些表之间的关系，可以使用实体关系图（ERD）来表示：

```mermaid
erDiagram
    departments {
        int department_id PK
        string name UK
    }
    employees {
        int employee_id PK
        string first_name
        string last_name
        decimal salary
        date hire_date
        int department_id FK
    }
    projects {
        int project_id PK
        string name UK
        date start_date
        date end_date
    }
    employee_projects {
        int employee_id FK
        int project_id FK
    }

    departments ||--o{ employees : "包含"
    employees ||--o{ employee_projects : "参与"
    projects ||--o{ employee_projects : "分配"

```

这个清晰定义的模式将是我们探索SQLite SQL强大功能的基石。

### 1.2 SQLite 核心概念：存储类与类型亲和性

SQLite的数据类型系统是其最显著的特点之一，也是与MySQL或PostgreSQL等其他关系型数据库最根本的区别所在。大多数数据库采用静态、严格的类型系统，即列的数据类型在表创建时被固定，任何存入该列的数据都必须符合此类型。相比之下，SQLite采用的是一个更灵活的动态类型系统 。

在SQLite中，数据类型是与值本身相关联的，而不是与包含该值的列相关联 。这意味着，除了特殊情况（如

`INTEGER PRIMARY KEY`列），你几乎可以在任何列中存储任何类型的值。为了理解这一机制，必须区分两个核心概念：存储类（Storage
Classes）和类型亲和性（Type Affinity）。

#### 存储类 (Storage Classes)

每个存储在SQLite数据库中的值都属于以下五种存储类之一 ：

1. **`NULL`**: 表示一个缺失或未知的值。

2. **`INTEGER`**: 一个有符号的整数，根据其数值大小，可以存储为1、2、3、4、6或8个字节。

3. **`REAL`**: 一个浮点数，存储为8字节的IEEE浮点数。

4. **`TEXT`**: 一个文本字符串，使用数据库指定的编码（UTF-8, UTF-16BE, or UTF-16LE）进行存储。

5. **`BLOB`**: (Binary Large Object) 一块二进制数据，完全按照输入时的原样存储。

值得注意的是，SQLite没有专门的`BOOLEAN`或`DATETIME`存储类。布尔值通常存储为`INTEGER`类型的0（假）和1（真）。日期和时间可以根据需要存储为
`TEXT`（如'YYYY-MM-DD HH:MM:SS'）、`REAL`（儒略日数）或`INTEGER`（Unix时间戳）。

#### 类型亲和性 (Type Affinity)

尽管SQLite内部使用存储类，但为了最大程度地兼容其他SQL数据库，它引入了“类型亲和性”的概念。当你为表的列声明一个数据类型时，SQLite会为该列分配一种“亲和性”。这种亲和性是该列期望存储的数据类型的
*建议*，而不是强制要求 。当数据插入时，SQLite会尝试将数据转换为该列亲和性的类型。

列的亲和性有以下五种 ：

1. **`TEXT`**: 当数据插入时，如果不是`NULL`或`BLOB`，会被转换为文本。

2. **`NUMERIC`**: 尝试将值存储为`INTEGER`或`REAL`。如果文本值可以无损地转换为整数或浮点数，它就会被转换；否则，它将保持为
   `TEXT`。

3. **`INTEGER`**: 与`NUMERIC`类似，但如果值是浮点数，它会尝试去除小数部分并存储为整数。

4. **`REAL`**: 与`NUMERIC`类似，但更倾向于将值存储为浮点数。

5. **`BLOB`**: 不进行任何类型转换，按原样存储数据。

SQLite根据你在`CREATE TABLE`语句中为列声明的类型名称中的特定子字符串来决定其亲和性。规则按以下顺序应用 ：

1. 如果类型名包含 "INT"，则亲和性为 `INTEGER`。

2. 如果类型名包含 "CHAR"、"CLOB" 或 "TEXT"，则亲和性为 `TEXT`。

3. 如果类型名包含 "BLOB" 或未指定类型，则亲和性为 `BLOB`。

4. 如果类型名包含 "REAL"、"FLOA" 或 "DOUB"，则亲和性为 `REAL`。

5. 否则，默认亲和性为 `NUMERIC`。

下表展示了一些常见的数据类型声明及其在SQLite中对应的亲和性，这对于从其他数据库系统迁移过来的开发者尤其有用 ：

| CREATE TABLE 或 CAST 表达式中的示例类型名 | 最终亲和性 | 确定亲和性的规则 |
| --- | --- | --- |
| `INT`, `INTEGER`, `TINYINT`, `SMALLINT`, `BIGINT` | `INTEGER` | 1 |
| `CHARACTER(20)`, `VARCHAR(255)`, `TEXT`, `CLOB` | `TEXT` | 2 |
| `BLOB`, *未指定数据类型* | `BLOB` | 3 |
| `REAL`, `DOUBLE`, `DOUBLE PRECISION`, `FLOAT` | `REAL` | 4 |
| `NUMERIC`, `DECIMAL(10,5)`, `BOOLEAN`, `DATE`, `DATETIME` | `NUMERIC` | 5 |

Export to Sheets

#### 灵活性的陷阱：对数据完整性和迁移的影响

SQLite的动态类型系统提供了一种强大的灵活性，特别是在快速原型开发和简单应用场景中。然而，这种灵活性也带来了一个潜在的“陷阱”。由于数据库本身不强制执行严格的类型约束，数据验证的重担几乎完全转移到了应用层
。如果应用程序逻辑存在缺陷，就可能导致一个本应只包含数字的列（如

`salary`）被意外地插入了文本字符串。

这种混合类型的数据会给后续的数据查询和分析带来极大的困难。例如，对一个包含文本的`salary`
列计算平均值可能会失败或产生不可预期的结果。更严重的是，这种设计选择对数据库的长期维护和迁移构成了挑战。当需要将一个SQLite数据库迁移到一个严格类型的系统（如PostgreSQL）时，这个过程往往会演变成一个复杂且成本高昂的数据清洗项目
。必须对每一列的数据进行检查、转换和规范化，以满足目标数据库的严格类型要求——而这正是SQLite最初的灵活性所掩盖的问题。

因此，将SQLite的动态类型视为一种架构决策而非简单的技术特性至关重要。它在带来便利的同时，也可能引入一种技术债务。一个成熟的开发实践是，即使SQLite允许，也应在应用程序层面实施严格的数据验证，确保写入数据库的数据类型始终与列的亲和性保持一致。这种审慎的做法能够在享受SQLite灵活性的同时，最大限度地保护数据的长期完整性和可维护性。

## 第二部分：数据定义语言 (DDL) - 构建和管理您的数据骨架

数据定义语言（DDL）是SQL中用于定义和管理数据库对象（如表、索引和视图）的部分。DDL命令能够构建、修改和删除数据库的结构，为存储数据搭建起骨架。本部分将详细介绍SQLite中核心的DDL命令：
`CREATE TABLE`、`ALTER TABLE`和`DROP TABLE`。

### 2.1 创建表 (`CREATE TABLE`) 与数据完整性约束

`CREATE TABLE`
是构建数据库结构的基础。除了定义列名和数据类型（亲和性）外，更重要的是通过约束（Constraints）来强制执行数据规则，从而保证数据的准确性、一致性和可靠性 。

约束是施加在表列上的规则，用于限制可以存入该列的数据类型和范围。如果任何操作违反了约束，该操作将被中止 。SQLite支持多种标准SQL约束。

#### `NOT NULL` 约束

默认情况下，任何列都可以存储`NULL`值。`NOT NULL`约束确保一个列永远不能包含`NULL`
值。这对于那些必须存在的关键数据字段（如ID或名称）至关重要 。

**语法与示例:**

```SQL
CREATE TABLE employees
(
    employee_id INTEGER PRIMARY KEY,
    first_name  TEXT NOT NULL,
    last_name   TEXT NOT NULL, ..
    .
);
```

在此示例中，`first_name`和`last_name`列都不能为`NULL`。

#### `DEFAULT` 约束

`DEFAULT`约束为列提供一个默认值。当`INSERT`语句没有为该列提供值时，将自动使用这个默认值 。

**语法与示例:**

```SQL
CREATE TABLE projects
(
    ..
    .
    status
    TEXT
    DEFAULT
    'Pending'
);
```

如果在`projects`表中插入一条新记录而没有指定`status`，它的值将自动设为'Pending'。

#### `UNIQUE` 约束

`UNIQUE`约束确保一列（或多列组合）中的所有值都是唯一的，不允许重复。一个表可以有多个`UNIQUE`约束 。

**语法与示例:**

```SQL
CREATE TABLE departments
(
    department_id INTEGER PRIMARY KEY,
    name          TEXT NOT NULL UNIQUE
);
```

这确保了每个部门名称都是独一无二的。

#### `PRIMARY KEY` 约束

`PRIMARY KEY`约束是`NOT NULL`和`UNIQUE`
的组合，用于唯一标识表中的每一行记录。每个表只能有一个主键。主键是数据库设计的核心，它不仅保证了记录的唯一性，还常被用作外键，在不同表之间建立关系 。

**语法与示例:**

```SQL
CREATE TABLE employees
(
    employee_id INTEGER PRIMARY KEY, ..
    .
);
```

**SQLite的“主键可为NULL”异常**

关系数据库理论和标准SQL都明确指出，主键必须是唯一且非空的。然而，SQLite存在一个显著的、与标准行为相悖的特性：由于一个“长期的编码疏忽”，
`PRIMARY KEY`列在技术上可以包含`NULL`值 。

这种异常行为的根源在于SQLite如何处理`NULL`的唯一性。在进行`UNIQUE`约束检查时，SQLite认为`NULL`与任何其他值（包括另一个
`NULL`）都不同 。这意味着，一个单列主键理论上可以存储多个

`NULL`值，这完全违背了主键作为唯一标识符的核心原则。这种情况可能导致严重的数据完整性问题，因为多行记录可能没有有效的唯一标识。

为了避免这种潜在的混乱和数据损坏，开发者在使用SQLite时必须采取一种防御性编程措施。最佳实践是，在定义主键时，*
*总是显式地声明`NOT NULL`约束**，即使理论上`PRIMARY KEY`已经隐含了这一属性。

**正确做法:**

```SQL
CREATE TABLE employees
(
    employee_id INTEGER PRIMARY KEY NOT NULL, -- 显式添加 NOT NULL
    ..
    .
);
```

这种做法不是一种风格偏好，而是在SQLite环境中确保数据行为符合标准预期的必要步骤。它强制主键列遵守其应有的规则，从而保护了数据的完整性。

#### `CHECK` 约束

`CHECK`约束允许你定义一个表达式，用于在插入或更新数据时验证列中的值。只有当表达式的计算结果为真（或`NULL`）时，操作才被允许
。这是一种强大的自定义数据验证工具。

**语法与示例:**

```SQL
CREATE TABLE employees
(
    ..
    .
    salary
    REAL
    CHECK
(
    salary >
    0
),
    end_date TEXT,
    start_date TEXT,
    CHECK (end_date IS NULL OR start_date IS NULL OR end_date >= start_date) -- 表级约束
    );
```

第一个`CHECK`约束确保薪水必须是正数。第二个是表级约束，确保如果`end_date`和`start_date`都存在，则结束日期必须晚于或等于开始日期。

#### `FOREIGN KEY` 约束

`FOREIGN KEY`
约束用于在一个表的列和另一个表的主键之间建立链接，从而强制实现参照完整性。它确保一个表（子表）中的外键值必须在另一个表（父表）的主键中存在 。

**重要提示**: 默认情况下，SQLite不强制执行外键约束。为了激活它，必须在每次连接到数据库后执行以下命令：

```SQL
PRAGMA foreign_keys = ON;
```

**语法与示例:**

```SQL
CREATE TABLE employees
(
    ..
    .
    department_id
    INTEGER,
    FOREIGN KEY (department_id) REFERENCES departments (department_id)
);
```

此约束确保了`employees`表中的`department_id`必须是`departments`表中已存在的`department_id`。

### 2.2 修改表 (`ALTER TABLE`)：限制与变通

在数据库的生命周期中，修改现有表的结构是常见需求。`ALTER TABLE`
命令用于此目的，但SQLite对此命令的支持是有限的，这反映了其设计的简洁性 。

SQLite的`ALTER TABLE`直接支持以下几种操作 ：

1. **`RENAME TO`**: 重命名一个表。
    ```SQL 
    ALTER TABLE old_table_name RENAME TO new_table_name; 
    ```

2. **`ADD COLUMN`**: 向现有表中添加一个新列。新列总是被添加到表的末尾 。

    ```SQL
    ALTER TABLE table_name ADD COLUMN new_column_definition;
   ```

   添加列时存在一些限制，例如新列不能是`PRIMARY KEY`或`UNIQUE`，并且如果带有`NOT NULL`约束，则必须提供一个非`NULL`的
   `DEFAULT`值 。

3. **`RENAME COLUMN TO`**: 重命名一个现有列（自SQLite 3.25.0起支持）。

  ```SQL
  ALTER TABLE table_name RENAME COLUMN old_column_name TO new_column_name;
  ```

4. **`DROP COLUMN`**: 删除一个现有列（自SQLite 3.35.0起支持）。

    ```SQL
   ALTER TABLE table_name DROP COLUMN column_name;
   ```

#### `ALTER TABLE` 限制背后的架构原因

开发者可能会对SQLite `ALTER TABLE`功能的局限性感到困惑，例如无法直接修改列的数据类型或添加`UNIQUE`
约束。这种局限性并非偶然，而是SQLite核心架构的直接体现。

大多数数据库系统（如MySQL、PostgreSQL）将其模式信息以解析后的、结构化的格式存储在多个系统表中 。当执行

`ALTER TABLE`时，数据库引擎只需更新这些系统表中的相应记录。

然而，SQLite的设计哲学追求极致的简洁。它将整个数据库的模式（即所有`CREATE TABLE`、`CREATE INDEX`等语句）作为原始的SQL文本字符串，存储在一个名为
`sqlite_schema`的特殊表中 。因此，SQLite执行

`ALTER TABLE`命令的本质，是对存储在`sqlite_schema`表中的`CREATE TABLE`文本进行*字符串操作*。

+ 重命名表或在末尾添加一列是相对简单的文本编辑。

+ 但是，要从表的中间删除一列、更改列的类型或添加复杂的约束，就需要对原始的`CREATE TABLE`
  语句进行复杂的解析、重写和验证，同时还要更新所有依赖于该表的索引、触发器和视图。这种操作复杂且极易出错。

SQLite选择了一种更简单、更稳健的策略：它不将这种复杂的逻辑内置于核心引擎中，而是将复杂性交由用户通过一个明确的手动流程来处理。

#### 复杂模式变更的变通方案

对于SQLite不直接支持的复杂表结构变更（如修改列类型、添加或删除约束、改变列顺序），需要遵循一个标准的手动流程。这个流程虽然繁琐，但安全可靠 ：

1. **禁用外键约束**: `PRAGMA foreign_keys=OFF;`

2. **开始事务**: `BEGIN TRANSACTION;`

3. **创建新表**: 创建一个具有期望的新结构的新表（例如 `new_table`）。

4. **迁移数据**: 使用`INSERT INTO new_table SELECT... FROM old_table;`将旧表中的数据复制到新表中。在此过程中可能需要进行数据转换。

5. **删除旧表**: `DROP TABLE old_table;`

6. **重命名新表**: `ALTER TABLE new_table RENAME TO old_table;`

7. **重建索引和触发器**: 在新表上重新创建所有必要的索引、触发器和视图。

8. **提交事务**: `COMMIT;`

9. **重新启用外键约束**: `PRAGMA foreign_keys=ON;`

这个流程体现了SQLite的设计权衡：通过牺牲部分便利性来换取核心引擎的极度轻量和可靠。理解这一架构选择有助于开发者更好地规划数据库维护策略，并认识到这个手动流程是SQLite设计理念的逻辑延伸，而非功能缺失。

### 2.3 删除表 (`DROP TABLE`)

`DROP TABLE`命令用于从数据库中永久删除一个表及其所有数据、索引、触发器和视图。这是一个不可逆的操作，应谨慎使用。

**语法:**

```SQL
DROP TABLE table_name;
```

使用可选的`IF EXISTS`子句可以防止在尝试删除一个不存在的表时发生错误。

## 第三部分：数据操作语言 (DML) - 填充与维护您的数据

数据操作语言（DML）是SQL中用于管理数据库中数据的部分。在通过DDL定义了数据结构之后，DML命令被用来插入、更新和删除数据记录。这些是数据库日常操作中最常用的命令。

### 3.1 插入数据 (`INSERT INTO`)

`INSERT INTO`语句用于向表中添加新的数据行。

#### 基本语法

最常见的形式是指定列和要插入的值。

```SQL
INSERT INTO table_name (column1, column2, ...)
VALUES (value1, value2, ...);
```

**示例:** 向`departments`表添加一个新部门。

```SQL
INSERT INTO departments (name)
VALUES ('Human Resources');
```

如果值的顺序与表中列的顺序完全一致，可以省略列名列表。

```SQL
INSERT INTO departments
VALUES (2, 'Engineering');
```

#### 插入多行

可以在一个`VALUES`子句中提供多个元组来一次性插入多行。

```SQL
INSERT INTO projects (name, start_date, end_date)
VALUES ('Project Phoenix', '2023-01-15', '2024-01-15'),
       ('Project Titan', '2023-03-01', NULL),
       ('Project Nebula', '2023-05-20', '2023-12-31');
```

#### 从查询结果插入

`INSERT INTO... SELECT`语句允许你将一个查询的结果集直接插入到另一个表中。这对于数据迁移或汇总非常有用。

```SQL
-- 假设有一个 old_employees 表
INSERT INTO employees (first_name, last_name, salary)
SELECT first_name, last_name, base_salary
FROM old_employees
WHERE status = 'Active';
```

#### 冲突解决

当插入操作违反了约束（如`UNIQUE`或`PRIMARY KEY`）时，默认行为是中止操作并返回错误。SQLite提供了冲突解决子句来改变这种行为。

+ **`INSERT OR REPLACE`**: 如果发生冲突，先删除导致冲突的现有行，然后再插入新行。

+ **`INSERT OR IGNORE`**: 如果发生冲突，则忽略该插入操作，不插入新行，也不返回错误。

+ **`INSERT OR ROLLBACK`**: 如果发生冲突，则回滚当前事务。

+ **`INSERT OR ABORT`**: (默认行为) 中止当前SQL语句并返回错误，但事务保持活动状态。

+ **`INSERT OR FAIL`**: 中止当前SQL语句并返回错误，事务不会回滚。

**示例:**

```SQL
-- 如果 employee_id 为 101 的员工已存在，则忽略此插入
INSERT OR IGNORE INTO employees (employee_id, first_name, last_name)
VALUES (101, 'Jane', 'Doe');
```

### 3.2 更新数据 (`UPDATE`)

`UPDATE`语句用于修改表中已存在的数据行。

#### 基本语法

```SQL
UPDATE table_name
SET column1 = value1,
    column2 = value2, ...
    WHERE condition;
```

`SET`子句指定要修改的列和新值。`WHERE`子句指定要更新哪些行。

**警告**: 如果省略`WHERE`子句，`UPDATE`语句将修改表中的**所有行**。这是一个非常危险的操作，必须极其小心。

**示例:** 更新特定员工的薪水。

```SQL
UPDATE employees
SET salary = 75000.00
WHERE employee_id = 101;
```

**示例:** 给所有工程部门的员工加薪10%。

```SQL
UPDATE employees
SET salary = salary * 1.10
WHERE department_id = (SELECT department_id FROM departments WHERE name = 'Engineering');
```

### 3.3 删除数据 (`DELETE FROM`)

`DELETE FROM`语句用于从表中删除数据行。

#### 基本语法

```SQL
DELETE
FROM table_name
WHERE condition;
```

`WHERE`子句指定要删除哪些行。

**警告**: 与`UPDATE`类似，如果省略`WHERE`子句，`DELETE FROM`语句将删除表中的**所有行**，但表结构本身会保留。这与`DROP TABLE`
不同，后者会删除整个表。

**示例:** 删除一个特定的项目。

```SQL
DELETE
FROM projects
WHERE name = 'Project Titan';
```

**示例:** 删除所有已离职的员工（假设有一个`status`列）。

```SQL
DELETE
FROM employees
WHERE status = 'Terminated';
```

## 第四部分：事务控制语言 (TCL) - 确保数据的原子性与一致性

事务控制语言（TCL）是SQL中用于管理事务的命令集合。事务是一系列操作的逻辑单元，这些操作要么全部成功执行，要么全部失败回滚，从而保证数据库从一个一致的状态转换到另一个一致的状态。在多用户并发环境中，事务对于维护数据完整性至关重要。

### 4.1 事务的核心：ACID 原则

事务的可靠性由四个基本属性来保证，即ACID原则 ：

1. **原子性 (Atomicity)**:
   事务被视为一个不可分割的最小工作单元。事务中的所有操作要么全部完成，要么全部不执行。如果事务在执行过程中发生任何错误，所有已经执行的操作都将被撤销（回滚），数据库将恢复到事务开始前的状态。

2. **一致性 (Consistency)**: 事务必须使数据库从一个一致的状态转移到另一个一致的状态。在事务开始和结束时，数据库的完整性约束（如主键、外键、CHECK约束）都必须得到满足。

3. **隔离性 (Isolation)**: 多个并发执行的事务之间应该相互隔离，一个事务的执行不应被其他事务干扰。隔离性确保了并发事务的最终结果与这些事务按某种顺序串行执行的结果相同。

4. **持久性 (Durability)**: 一旦事务被成功提交（`COMMIT`），它对数据库的更改就是永久性的，即使后续发生系统崩溃或断电等故障，这些更改也不会丢失。

### 4.2 事务的基本控制：`BEGIN`, `COMMIT`, `ROLLBACK`

SQLite通过三个核心命令来控制事务的边界 ：

+ **`BEGIN TRANSACTION`**: 标志着一个事务的开始。在此命令之后的所有SQL操作都将被视为该事务的一部分。

+ **`COMMIT`**: 标志着一个事务的成功结束。当执行`COMMIT`时，事务期间所做的所有更改都将被永久写入数据库。

+ **`ROLLBACK`**: 用于撤销一个事务。当执行`ROLLBACK`时，事务期间所做的所有更改都将被撤销，数据库将恢复到执行`BEGIN`命令之前的状态。

**示例：银行转账**

一个经典的事务示例是银行转账，它包含两个原子操作：从一个账户扣款，并向另一个账户存款。这两个操作必须同时成功或同时失败。

```SQL
-- 假设有一个 accounts 表，包含 account_id 和 balance 列
BEGIN TRANSACTION;

-- 尝试从账户A（ID=1）扣款100元
UPDATE accounts
SET balance = balance - 100
WHERE account_id = 1;

-- 检查账户A是否有足够余额（此处为简化逻辑，实际应用中应在更新前检查）
--...

-- 尝试向账户B（ID=2）存款100元
UPDATE accounts
SET balance = balance + 100 WHERE account_id = 2;

-- 假设在此时发生错误，或检查发现账户A余额不足
-- 我们可以回滚整个事务
-- ROLLBACK;

-- 如果所有操作都成功，则提交事务
COMMIT;
```

如果在这两个`UPDATE`操作之间发生任何错误（如系统崩溃），数据库在恢复后将自动回滚未完成的事务，确保资金不会凭空消失或产生。

### 4.3 SQLite 事务模式：`DEFERRED`, `IMMEDIATE`, `EXCLUSIVE`

SQLite的`BEGIN`
命令可以接受一个可选的模式修饰符，用于指定事务何时以及如何获取数据库锁。这个选择对并发性能和应用程序的错误处理逻辑有重大影响。这三种模式代表了在并发控制中乐观锁和悲观锁策略的选择 。

1. **`DEFERRED` (延迟模式)**

    + **语法**: `BEGIN DEFERRED TRANSACTION;` 或 `BEGIN;` (这是默认模式)

    + **行为**: 这是最“懒惰”的模式。执行`BEGIN DEFERRED`
      本身不会做任何事情，甚至不访问数据库。它只是将数据库连接置于一个“事务模式”中。直到事务内第一条读或写语句被执行时，SQLite才会去获取相应的锁。

        + 第一条读操作（`SELECT`）会获取一个**共享锁（SHARED lock）**，允许多个读操作并发进行。

        + 第一条写操作（`INSERT`, `UPDATE`, `DELETE`）会尝试获取一个**保留锁（RESERVED lock）**，并最终升级为\*\*独占锁（EXCLUSIVE
          lock）\*\*来完成写入。

    + **策略**: 这是一种**乐观锁**策略。它假设在事务的大部分时间内不会有写冲突。`BEGIN`
      操作本身非常快且不会阻塞。但缺点是，当真正需要写入时，获取写锁的操作可能会失败（返回`SQLITE_BUSY`
      错误），因为此时可能有其他连接已经持有了写锁。这意味着应用程序**必须**准备好处理`SQLITE_BUSY`错误，并实现重试逻辑。

2. **`IMMEDIATE` (立即模式)**

    + **语法**: `BEGIN IMMEDIATE TRANSACTION;`

    + **行为**: 执行`BEGIN IMMEDIATE`会立即尝试获取一个**保留锁（RESERVED lock）**。这个锁允许其他连接继续读取数据库，但阻止任何其他连接发起新的写事务。

    + **策略**: 这是一种**悲观锁**策略。它在事务开始时就确保了“写”的权利。如果`BEGIN IMMEDIATE`
      命令成功执行，那么该事务内的所有后续写操作都保证不会因为锁竞争而失败（虽然仍可能因其他原因失败，如约束冲突）。如果
      `BEGIN IMMEDIATE`命令本身失败（返回`SQLITE_BUSY`），则意味着有其他写事务正在进行。

    + **应用场景**: 对于需要执行写操作的事务，使用`IMMEDIATE`
      模式是一种更健壮的策略。它将并发冲突的处理集中在了事务的起点，简化了应用程序的错误处理逻辑，因为不再需要在每个写操作后都检查
      `SQLITE_BUSY`。

3. **`EXCLUSIVE` (独占模式)**

    + **语法**: `BEGIN EXCLUSIVE TRANSACTION;`

    + **行为**: 执行`BEGIN EXCLUSIVE`会立即尝试获取一个**独占锁（EXCLUSIVE lock）**。一旦成功，任何其他连接（无论是读还是写）都无法访问数据库，直到该事务结束。

    + **策略**: 这是最强的悲观锁策略。它保证了事务执行期间的完全隔离。

    + **应用场景**: 适用于需要对数据库进行大规模修改或需要确保在事务期间数据库状态绝对不被任何其他连接观察到的情况。

**选择合适的事务模式是一个关键的应用程序设计决策**。对于单线程或读密集型的应用，默认的`DEFERRED`
模式通常足够。但对于多线程或写密集型的并发应用，主动使用`BEGIN IMMEDIATE`来启动写事务是一种更可靠的编程范式，它可以显著简化并发控制和错误处理的复杂性。

### 4.4 嵌套事务与保存点：`SAVEPOINT` 与 `RELEASE`

标准的`BEGIN...COMMIT`事务在SQLite中是不能嵌套的。在一个已开始的事务内部再次执行`BEGIN`会导致错误
。为了实现更细粒度的事务控制，例如在复杂操作中进行部分回滚，SQLite提供了

`SAVEPOINT`机制。

+ **`SAVEPOINT savepoint_name;`**: 在当前事务中创建一个命名的保存点。

+ **`RELEASE SAVEPOINT savepoint_name;`**: 销毁一个保存点。这会将该保存点之后的操作合并到其父事务（或上一个保存点）中。

+ **`ROLLBACK TO SAVEPOINT savepoint_name;`**: 撤销自指定的保存点创建以来所做的所有更改，但事务本身仍然保持活动状态，保存点之前所做的更改也仍然保留。

**示例:**

```SQL
BEGIN TRANSACTION;
-- 操作 A
INSERT INTO employees (first_name, last_name)
VALUES ('Alice', 'Smith');

SAVEPOINT sp1;
-- 操作 B
UPDATE projects
SET status = 'In Progress'
WHERE project_id = 1;

SAVEPOINT sp2;
-- 操作 C
DELETE
FROM employees
WHERE employee_id = 99;
-- 假设这是一个错误的操作

-- 发现操作C是错误的，回滚到保存点sp2
ROLLBACK TO SAVEPOINT sp2;

-- 现在，操作C被撤销了，但操作A和B仍然是事务的一部分
-- 我们可以继续执行其他操作
--...

-- 如果我们想保留操作A和B，可以提交整个事务
COMMIT;

-- 或者，如果我们想撤销操作A和B，可以回滚整个事务
-- ROLLBACK;
```

`SAVEPOINT`为复杂的业务逻辑提供了强大的控制能力，允许开发者在不放弃整个事务的情况下，从错误中优雅地恢复。

## 第五部分：数据查询语言 (DQL) - `SELECT` 语句的艺术

数据查询语言（DQL）的核心是`SELECT`语句，它是SQL中最复杂也最强大的部分。`SELECT`语句用于从数据库中检索数据，其结果是一个或多个数据行组成的临时表，称为结果集。掌握
`SELECT`语句的各个子句及其执行逻辑，是进行任何有效数据分析和应用开发的基础。

本部分将按照`SELECT`语句的逻辑处理顺序，而不是其书写顺序，来详细解析每个子句的功能。逻辑处理顺序大致如下：`FROM` ->
`JOIN` -> `WHERE` -> `GROUP BY` -> `HAVING` -> `SELECT` -> `DISTINCT` -> `ORDER BY` -> `LIMIT`。

### 5.1 查询的基石：`SELECT` 与 `FROM`

`SELECT`和`FROM`是所有查询语句最基本的组成部分。`FROM`子句指定了数据源（一个或多个表），而`SELECT`子句指定了要从这些数据源中检索哪些列。

#### 基本语法

```SQL
SELECT column1,
       column2, ...
    FROM table_name;
```

**示例：** 从`employees`表中检索员工的姓名和薪水。

```SQL
SELECT first_name, last_name, salary
FROM employees;
```

#### 选择所有列 (`*`)

星号（`*`）是选择表中所有列的快捷方式 。

```SQL
SELECT *
FROM projects;
```

虽然在交互式查询和调试中非常方便，但在生产应用程序代码中，**强烈建议明确列出所需的列**。这样做有几个好处 ：

+ **可读性和可维护性**: 代码清晰地表明了需要哪些数据。

+ **性能**: 数据库只需读取和传输必要的列，减少了I/O和网络开销。

+ **稳定性**: 如果表的结构发生变化（例如，添加或删除列），明确指定列的查询不会受到影响，而`SELECT *`可能会导致应用程序因列数不匹配而崩溃。

#### 使用别名 (Alias)

可以使用`AS`关键字为列或表指定一个临时的别名，以提高查询的可读性。

```SQL
SELECT first_name AS "姓",
       last_name  AS "名",
       salary     AS "薪水"
FROM employees AS e;
```

### 5.2 关联的艺术：`JOIN` 子句详解

当需要的数据分散在多个表中时，`JOIN`
子句就派上了用场。它根据表之间的关联条件，将多个表的行组合起来。SQLite中的所有连接操作都基于左表和右表数据集的笛卡尔积，然后根据连接条件进行筛选 。

#### `INNER JOIN` (内连接)

`INNER JOIN`返回两个表中连接字段相匹配的行。如果某一行在另一个表中没有匹配的行，那么它将不会出现在结果集中 。这是最常用的连接类型。

```mermaid
graph TD
    subgraph A
        d1(Row 1)
        d2(Row 2)
        d3(Row 3)
    end
    subgraph B
        d4(Row A)
        d5(Row B)
        d6(Row C)
    end
    subgraph INNER JOIN Result
        d7(Row 2 - Row A)
        d8(Row 3 - Row B)
    end
    d2 -- matches --> d4
    d3 -- matches --> d5
```

**语法:**

```SQL
SELECT...
    FROM table1
    INNER JOIN table2
ON table1.column_name = table2.column_name;
```

`ON`子句指定了连接条件。

**示例：** 查询每个员工及其所在的部门名称。

```SQL
SELECT e.first_name,
       e.last_name,
       d.name AS department_name
FROM employees AS e
         INNER JOIN
     departments AS d ON e.department_id = d.department_id;
```

#### `LEFT JOIN` (左连接)

`LEFT JOIN` (或 `LEFT OUTER JOIN`) 返回左表中的**所有行**，以及右表中与左表匹配的行。如果右表中没有匹配的行，则结果集中右表的列将显示为
`NULL` 。

```mermaid
graph TD
    subgraph Left Table A
        d1(Row 1)
        d2(Row 2)
        d3(Row 3)
    end
    subgraph Right Table B
        d4(Row A)
        d5(Row B)
    end
    subgraph LEFT JOIN Result
        d6(Row 1 - NULL)
        d7(Row 2 - Row A)
        d8(Row 3 - Row B)
    end
    d2 -- matches --> d4
    d3 -- matches --> d5
```

**语法:**

```SQL
SELECT ...
    FROM table1 -- 左表
    LEFT JOIN table2
ON table1.column_name = table2.column_name;
```

**示例：** 列出所有部门，并显示每个部门的员工数量（即使某个部门没有员工）。

```SQL
SELECT d.name,
       COUNT(e.employee_id) AS number_of_employees
FROM departments AS d
         LEFT JOIN
     employees AS e ON d.department_id = e.department_id
GROUP BY d.name;
```

对于没有任何员工的部门，`number_of_employees`将为0。

#### `CROSS JOIN` (交叉连接)

`CROSS JOIN`返回两个表的笛卡尔积，即左表中的每一行都与右表中的每一行进行组合。它不需要`ON`子句 。在

`FROM`子句中用逗号分隔多个表，效果与`CROSS JOIN`相同。

**语法:**

```SQL
SELECT...FROM table1 CROSS JOIN table2;
-- 等效于
SELECT...FROM table1, table2;
```

**`CROSS JOIN`作为查询优化器覆盖工具**

从数学角度看，`INNER JOIN`、逗号连接和`CROSS JOIN`在没有`ON`子句的情况下都产生相同的结果——笛卡尔积。那么为什么需要
`CROSS JOIN`这个看似冗余的语法呢？

答案在于一个SQLite特有的、非标准的行为。通常，数据库的查询优化器会根据统计信息自由地重新排列`JOIN`
的顺序，以找到最高效的执行计划（例如，先连接小表）。然而，当开发者使用明确的`CROSS JOIN`关键字时，SQLite的查询优化器会**禁用连接重排
** 。

这提供了一个“逃生舱口”，允许对数据分布有深入了解的专家用户手动强制一个特定的连接顺序，从而可能覆盖优化器不够理想的决策。这是一种高级性能调优技术，虽然牺牲了可移植性，但在特定场景下，它是解决复杂查询性能问题的强大工具。因此，
`CROSS JOIN`在SQLite中不仅仅是一种语法变体，更是一个为专家级用户提供底层控制的有意设计。

### 5.3 精准过滤：`WHERE` 子句

`WHERE`子句在`FROM`和`JOIN`子句之后执行，用于根据指定的条件过滤行。只有满足`WHERE`子句中条件的行才会被传递到后续的处理阶段（如
`GROUP BY`）。

**常用操作符:**

+ 比较操作符: `=`, `!=` (或 `<>`), `>`, `<`, `>=`, `<=`

+ 逻辑操作符: `AND`, `OR`, `NOT`

+ 范围操作符: `BETWEEN... AND...`

+ 列表操作符: `IN (...)`, `NOT IN (...)`

+ 模式匹配: `LIKE` (配合 `%` 匹配任意字符序列，`_` 匹配单个字符)

+ 空值检查: `IS NULL`, `IS NOT NULL`

**示例：** 查询薪水在50000到80000之间，且在2022年之后雇佣的员工。

```SQL
SELECT first_name, last_name, salary, hire_date
FROM employees
WHERE salary BETWEEN 50000 AND 80000
  AND hire_date >= '2022-01-01';
```

### 5.4 数据聚合：`GROUP BY` 与聚合函数

`GROUP BY`子句通常与聚合函数一起使用，用于将具有相同值的行分组到摘要行中 。例如，按部门对员工进行分组，然后计算每个部门的平均薪水。

**常用聚合函数:**

+ `COUNT()`: 计算行数。

+ `SUM()`: 计算数值列的总和。

+ `AVG()`: 计算数值列的平均值。

+ `MAX()`: 找出列中的最大值。

+ `MIN()`: 找出列中的最小值。

**语法:**

```SQL
SELECT column1, aggregate_function(column2)
FROM table_name
WHERE condition
GROUP BY column1;
```

**示例：** 计算每个部门的员工人数和平均薪水。

```SQL
SELECT d.name               AS department,
       COUNT(e.employee_id) AS employee_count,
       AVG(e.salary)        AS average_salary
FROM departments AS d
         JOIN
     employees AS e ON d.department_id = e.department_id
GROUP BY d.name;
```

#### “裸列”的危险与效用

标准SQL有一个严格的规则：`SELECT`列表中的任何非聚合函数列都必须出现在`GROUP BY`子句中。否则，数据库无法确定对于一个聚合组，应该显示哪个具体行的值。

SQLite打破了这一规则，允许在`SELECT`列表中出现既不在聚合函数中也不在`GROUP BY`子句中的列，这些列被称为“裸列”（bare
columns）。在这种情况下，SQLite会从该组的

**任意一行**中选取该裸列的值。这种不确定性行为是潜在错误的来源，因为查询结果可能看起来正确，但实际上是随机的，每次执行都可能不同。

然而，这个特性有一个非常重要且有用的例外。如果一个聚合查询中**只包含一个`min()`或`max()`聚合函数**
，那么SQLite保证所有裸列的值都将取自包含该最小值或最大值的**同一行** 。

这个例外将“裸列”从一个危险的反模式转变为一个高效的工具。考虑这个需求：找出每个部门中薪水最高的员工的完整信息。在标准SQL中，这通常需要使用子查询或窗口函数，语法较为复杂。但在SQLite中，可以利用这个特性写出非常简洁的查询：

```SQL
-- 找出每个部门薪水最高的员工的姓名和薪水
SELECT d.name        AS department,
       e.first_name,               -- 裸列
       e.last_name,                -- 裸列
       MAX(e.salary) AS max_salary -- 唯一的 max() 函数
FROM employees AS e
         JOIN
     departments AS d ON e.department_id = d.department_id
GROUP BY d.name;
```

这个查询不仅简洁，而且执行效率通常更高。因此，在使用SQLite时，应谨慎对待裸列：在一般情况下避免使用以防止不确定性，但在符合
`min()`/`max()`单一聚合的特定场景下，可以将其作为一种强大的优化技巧。

### 5.5 聚合后过滤：`HAVING` 子句

`HAVING`子句与`WHERE`子句类似，都用于过滤数据。但它们的作用阶段完全不同：

+ `WHERE`在**聚合前**过滤行。

+ `HAVING`在**聚合后**过滤分组 。

因此，`HAVING`子句中可以使用聚合函数，而`WHERE`子句中不能。

**示例：** 找出平均薪水超过60000的部门。

```SQL
SELECT d.name,
       AVG(e.salary) AS average_salary
FROM departments AS d
         JOIN
     employees AS e ON d.department_id = e.department_id
GROUP BY d.name
HAVING AVG(e.salary) > 60000;
```

### 5.6 结果排序：`ORDER BY` 子句

`ORDER BY`子句用于对最终的结果集进行排序。如果不指定`ORDER BY`，结果的顺序是未定义的 。

**语法:**

```SQL
ORDER BY column1, column2,...;
```

+ `ASC`: 升序（默认）。

+ `DESC`: 降序。

+ 可以按多个列排序，优先级从左到右。

**处理`NULL`值**: 默认情况下，SQLite在排序时认为`NULL`值比任何其他值都小。可以使用`NULLS FIRST`或`NULLS LAST`来明确控制
`NULL`值的排序位置 。

**示例：** 按薪水降序排列员工，薪水相同时按姓氏升序排列。

```SQL
SELECT first_name, last_name, salary
FROM employees
ORDER BY salary DESC, last_name ASC;
```

### 5.7 数据去重：`DISTINCT` 关键字

`DISTINCT`关键字用于从结果集中移除重复的行 。

**示例：** 查询所有有员工的部门ID。

```SQL
SELECT DISTINCT department_id
FROM employees
WHERE department_id IS NOT NULL;
```

`DISTINCT`会作用于`SELECT`列表中的所有列，只有当所有列的值都相同时，才被视为重复行。

### 5.8 结果分页：`LIMIT` 与 `OFFSET`

`LIMIT`和`OFFSET`子句用于实现结果集的分页，这在Web应用中非常常见 。它们是查询中最后被应用的子句。

+ `LIMIT N`: 限制结果集最多返回 N 行。

+ `OFFSET M`: 从结果集的开头跳过 M 行。

**语法:**

```SQL
SELECT...
    ORDER BY ...
    LIMIT < count >
OFFSET <skip>;
```

**示例：** 查询薪水排在第11到第20位的员工。

```SQL
SELECT first_name, last_name, salary
FROM employees
ORDER BY salary DESC
LIMIT 10 OFFSET 10;
```

#### `LIMIT M, N` 兼容性语法

除了标准的`LIMIT... OFFSET...`语法，SQLite还支持一种替代形式：`LIMIT <skip>, <count>` 。

```SQL
-- 与上面的示例等效
SELECT first_name, last_name, salary
FROM employees
ORDER BY salary DESC
LIMIT 10, 10; -- 注意：第一个数字是OFFSET，第二个是LIMIT
```

这种语法的参数顺序是反直觉的，与`OFFSET`关键字的用法相反。它存在的唯一原因是为了“最大化与遗留SQL数据库系统的兼容性”，特别是为了兼容MySQL，后者广泛使用
`LIMIT <offset>, <count>`这种形式 。

这个细节揭示了SQLite的一个重要设计哲学：实用主义优先于纯粹性。为了方便开发者从其他流行系统迁移，SQLite愿意采纳一个虽然不一致但被广泛使用的语法。尽管如此，为了代码的清晰性和可移植性，
**强烈建议始终使用明确的`OFFSET`关键字**。

### 5.9 高级查询技术

除了基本子句，`SELECT`语句还支持多种高级技术，以处理更复杂的查询需求。

#### 子查询 (Subqueries)

子查询是嵌套在另一个查询（主查询）中的`SELECT`语句。子查询可以出现在`SELECT`列表、`FROM`子句、`WHERE`子句等多个位置。

+ **标量子查询**: 返回单个值（一行一列），可以用在需要单个值的地方。

   ```SQLSELECT first_name, salary,
         (SELECT AVG(salary) FROM employees) AS avg_company_salary
  FROM employees;
  ```

+ **多行子查询**: 返回多行，常与`IN`, `EXISTS`等操作符一起使用。

   ```SQLSELECT * FROM employees
  WHERE department_id IN (SELECT department_id FROM departments WHERE name LIKE 'Eng%');
  ```

+ **派生表**: 子查询出现在`FROM`子句中，其结果集被当作一个临时表。

   ```SQL
  SELECT * FROM (
      SELECT department_id, AVG(salary) as avg_sal
      FROM employees
      GROUP BY department_id
  ) AS dept_salaries
  WHERE avg_sal > 55000;
  ```

#### 公用表表达式 (Common Table Expressions - `WITH` Clause)

公用表表达式（CTE）通过`WITH`
子句定义，它允许你创建一个临时的、命名的结果集，可以在后续的查询中引用。CTE可以极大地提高复杂查询的可读性和模块化程度 。

**示例：** 使用CTE重写上面的派生表示例。

```SQL
WITH dept_salaries AS (SELECT department_id, AVG(salary) as avg_sal
                       FROM employees
                       GROUP BY department_id)
SELECT d.name, ds.avg_sal
FROM dept_salaries AS ds
         JOIN departments AS d ON ds.department_id = d.department_id
WHERE ds.avg_sal > 55000;
```

CTE还可以是递归的，用于处理层次结构或图形数据，例如组织结构图或物料清单。

#### 集合运算 (`UNION`, `UNION ALL`, `INTERSECT`, `EXCEPT`)

集合运算符用于合并两个或多个`SELECT`语句的结果集 。

+ **`UNION`**: 合并两个结果集，并自动去除重复行。

+ **`UNION ALL`**: 合并两个结果集，但保留所有行，包括重复行。性能通常优于`UNION`。

+ **`INTERSECT`**: 返回同时存在于两个结果集中的行。

+ **`EXCEPT`**: 返回存在于第一个结果集中，但不存在于第二个结果集中的行。

**要求**: 参与集合运算的所有`SELECT`语句必须具有相同数量的列，并且对应列的数据类型必须兼容。

#### 窗口函数 (`WINDOW` 与 `OVER` 子句)

窗口函数是一种强大的分析工具，它对与当前行相关的行集（称为“窗口”）进行计算。与聚合函数不同，窗口函数不会将多行折叠为一行，而是在保留原始行数的同时，为每一行计算一个值。

窗口由`OVER()`子句定义，其中可以包含：

+ **`PARTITION BY`**: 将行分成多个分区（组），窗口函数在每个分区内独立计算。

+ **`ORDER BY`**: 定义分区内行的排序方式。

+ **`frame_spec`**: 定义窗口的范围（例如，“当前行及之前的所有行”）。

**示例：** 计算每个员工的薪水，并显示其所在部门的平均薪水，以及薪水在部门内的排名。

```SQL
SELECT first_name,
       last_name,
       d.name   AS department,
       salary,
       AVG(salary) OVER (PARTITION BY department_id) AS avg_dept_salary,
       RANK() OVER (PARTITION BY department_id ORDER BY salary DESC) AS salary_rank_in_dept
FROM employees e
         JOIN
     departments d ON e.department_id = d.department_id;
```

这个查询无需`GROUP BY`，就能为每一位员工提供丰富的上下文信息，展示了窗口函数的强大威力。