Python SQLite:`JOIN` 子句深度解析
目录
- 引言:关联数据的桥梁——
JOIN子句 - 关系型数据库基础:表、键与关系
- 2.1 主键 (
PRIMARY KEY) - 2.2 外键 (
FOREIGN KEY) - 2.3 关系类型 (一对一、一对多、多对多)
- 2.1 主键 (
JOIN子句基础- 3.1 什么是
JOIN? - 3.2
ON子句:连接条件 - 3.3
USING子句:简化连接条件 - 3.4 隐式连接 vs. 显式连接
- 3.1 什么是
JOIN类型详解- 4.1
INNER JOIN(内连接):只返回匹配的行 - 4.2
LEFT JOIN(或LEFT OUTER JOIN):返回左表所有行,以及右表匹配行 - 4.3
CROSS JOIN(交叉连接):笛卡尔积 - 4.4 SQLite 对
RIGHT JOIN和FULL OUTER JOIN的限制- 4.4.1 模拟
RIGHT JOIN - 4.4.2 模拟
FULL OUTER JOIN
- 4.4.1 模拟
- 4.1
JOIN与其他 SQL 子句的结合- 5.1
WHERE子句:连接后过滤 - 5.2
ORDER BY子句:连接后排序 - 5.3
LIMIT/OFFSET:连接后分页 - 5.4
GROUP BY/HAVING:连接后聚合 - 5.5 多个
JOIN
- 5.1
- 在 Python
sqlite3中使用JOIN- 6.1 执行
JOIN查询 - 6.2 参数绑定:安全传递
WHERE或ON条件中的值 - 6.3 处理连接后的结果
- 6.1 执行
- 性能优化:索引与
JOIN- 7.1 索引的重要性
- 7.2 何时创建索引 (特别是外键)
- 7.3 选择必要的列
- 错误处理
- 8.1
sqlite3.OperationalError
- 8.1
- 最佳实践与注意事项
- 9.1 始终启用外键约束 (
PRAGMA foreign_keys = ON;) - 9.2 参数绑定
- 9.3 使用
with语句管理资源 - 使用
sqlite3.Row提高可读性 - 显式连接 (
INNER JOIN等) 优于隐式连接 ALTER TABLE限制与常见变通方案- 安全警告:避免动态 SQL 中的用户输入
- 9.1 始终启用外键约束 (
- 综合代码示例
- 10.1 数据库结构和数据准备
- 10.2 各种
JOIN查询示例 - 10.3 模拟
RIGHT JOIN和FULL OUTER JOIN
- 总结
1. 引言:关联数据的桥梁——JOIN 子句
在真实世界的应用中,数据通常分散在多个相互关联的数据库表中,而不是存储在一个巨大的单一表中。例如,一个电商系统可能有 customers 表、products 表、orders 表和 order_items 表。customers 表存储客户信息,orders 表存储订单概览,order_items 表存储订单中的具体商品。
为了获取一个客户的所有订单详情,或者查看某个订单包含哪些商品及其价格,我们就需要将这些分散的数据关联起来。SQL 的 JOIN 子句正是为此而生。它允许您根据表之间的逻辑关系,将来自两个或多个表的数据行组合成一个统一的结果集。
本指南将深入探讨 JOIN 子句的各种类型、语法、与其它 SQL 子句的结合、在 Python sqlite3 中的应用,以及性能优化和最佳实践,特别是 SQLite 对 JOIN 类型的一些限制和相应的变通方案。
2. 关系型数据库基础:表、键与关系
理解 JOIN 的前提是理解关系型数据库中的基本概念。
2.1 主键 (PRIMARY KEY)
- 定义: 一个或多个列的组合,其值能唯一标识表中的每一行。
- 特性: 必须包含唯一值,且不能包含
NULL值。 - 作用: 是建立表之间关系的基础。
2.2 外键 (FOREIGN KEY)
- 定义: 一个或多个列的组合,其值引用另一个表(称为“父表”或“被引用表”)中的主键(或唯一键)。
- 特性: 可以包含重复值,也可以包含
NULL值(除非有NOT NULL约束)。 - 作用: 在两个表之间建立和维护引用完整性约束。它确保了相关数据之间的一致性。
2.3 关系类型 (一对一、一对多、多对多)
- 一对一 (One-to-One): 两个表中的每一行都只与另一个表中的一行匹配。不常见,通常可以通过将两个表合并为一个表来简化。
- 一对多 (One-to-Many): 父表中的一行可以与子表中的多行关联,而子表中的一行只能与父表中的一行关联。例如,一个客户可以有多个订单,但一个订单只属于一个客户。这是最常见的关系类型,通过外键实现。
- 多对多 (Many-to-Many): 两个表中的一行都可以与另一个表中的多行关联。例如,一个学生可以选修多门课程,一门课程可以被多个学生选修。这种关系通常通过一个中间的“联结表”或“桥接表”来实现。
3. JOIN 子句基础
3.1 什么是 JOIN?
JOIN 是一种 SQL 操作,它根据两个或多个表之间列的共同值,将这些表中的行组合起来。JOIN 操作的结果是一个新的虚拟表,其中包含来自所有连接表的列。
3.2 ON 子句:连接条件
ON 子句是指定两个表之间连接条件的标准方式。它通常用于指定连接的列,并且可以包含更复杂的条件。
SELECT ...
FROM table1
INNER JOIN table2 ON table1.column_name = table2.column_name;
table1.column_name = table2.column_name: 这是最常见的连接条件,它指定两个表在这些列的值相等时进行匹配。
3.3 USING 子句:简化连接条件
如果两个表之间用于连接的列名相同,可以使用 USING 子句来简化 ON 子句。
SELECT ...
FROM table1
INNER JOIN table2 USING (common_column_name);
这等价于 ON table1.common_column_name = table2.common_column_name。
3.4 隐式连接 vs. 显式连接
-
隐式连接 (Implicit Join): 在
FROM子句中列出多个表,并在WHERE子句中指定连接条件。这种方式虽然能实现连接,但不推荐,因为它将连接逻辑与过滤逻辑混淆,可读性差,且容易出错。-- 隐式连接示例 SELECT c.first_name, o.order_id FROM customers c, orders o WHERE c.customer_id = o.customer_id; -
显式连接 (Explicit Join): 使用
INNER JOIN,LEFT JOIN等关键字明确指定连接类型和ON/USING条件。这是推荐的方式,因为它更清晰、更易读,且能更好地分离连接逻辑与过滤逻辑。-- 显式连接示例 SELECT c.first_name, o.order_id FROM customers c INNER JOIN orders o ON c.customer_id = o.customer_id;
4. JOIN 类型详解
JOIN 子句根据匹配行为分为不同的类型。
4.1 INNER JOIN (内连接):只返回匹配的行
INNER JOIN 是最常见的连接类型。它根据 ON 或 USING 子句中指定的条件,从两个表中返回所有匹配的行。如果一个表中的行在另一个表中没有匹配项,则这些行不会包含在结果集中。
可视化: 两个集合的交集。
示例:
-- 查找所有下过订单的客户及其订单信息
SELECT c.first_name, c.email, o.order_id, o.total_amount
FROM customers AS c
INNER JOIN orders AS o ON c.customer_id = o.customer_id;
4.2 LEFT JOIN (或 LEFT OUTER JOIN):返回左表所有行,以及右表匹配行
LEFT JOIN 返回左表 (FROM 关键字后的第一个表) 中的所有行,以及右表中与左表匹配的行。如果左表中的某行在右表中没有匹配项,则右表中的所有列都将显示为 NULL。
可视化: 左集合 + 两个集合的交集。
示例:
-- 查找所有客户,以及他们可能有的订单信息
-- 即使客户没有下过订单,也会出现在结果中,其订单相关列为 NULL
SELECT c.first_name, c.last_name, o.order_id, o.total_amount
FROM customers AS c
LEFT JOIN orders AS o ON c.customer_id = o.customer_id;
4.3 CROSS JOIN (交叉连接):笛卡尔积
CROSS JOIN 返回两个连接表的笛卡尔积。这意味着第一个表的每一行都与第二个表的每一行组合。如果第一个表有 N 行,第二个表有 M 行,那么 CROSS JOIN 将返回 N * M 行。它通常不包含 ON 子句。
应用场景: 很少直接使用,但在某些需要生成所有可能组合的报告或数据分析场景中可能会用到。
示例:
-- 假设我们有两个表:colors (Red, Green) 和 sizes (S, M)
-- CROSS JOIN 将生成 (Red, S), (Red, M), (Green, S), (Green, M)
-- 这会生成所有客户和所有产品的组合,结果集会非常大
SELECT c.first_name, p.name
FROM customers AS c
CROSS JOIN products AS p
LIMIT 10; -- 通常需要 LIMIT 来避免结果集过大
4.4 SQLite 对 RIGHT JOIN 和 FULL OUTER JOIN 的限制
这是一个重要的限制。SQLite 数据库不直接支持 RIGHT JOIN 和 FULL OUTER JOIN 关键字。您需要通过其他方式来模拟这些连接。
4.4.1 模拟 RIGHT JOIN
RIGHT JOIN 的行为是返回右表的所有行,以及左表中匹配的行。如果右表中的某行在左表中没有匹配项,则左表中的所有列都将显示为 NULL。
由于 SQLite 支持 LEFT JOIN,我们可以通过交换表的顺序,然后使用 LEFT JOIN 来模拟 RIGHT JOIN。
RIGHT JOIN 逻辑: 返回 customers 中与 orders 匹配的,以及 orders 中的所有。
-- 假设需要 RIGHT JOIN customers ON orders
-- 模拟方式:将 orders 放在 FROM 子句,customers 放在 LEFT JOIN 子句
SELECT c.first_name, c.last_name, o.order_id, o.total_amount
FROM orders AS o -- 变成左表
LEFT JOIN customers AS c ON o.customer_id = c.customer_id; -- 变成右表
这将返回所有订单,以及其对应的客户信息。如果某个订单的 customer_id 在 customers 表中找不到匹配项(这在有外键约束时理论上不会发生,但在数据不一致或没有外键时可能会),那么客户相关的列将为 NULL。
4.4.2 模拟 FULL OUTER JOIN
FULL OUTER JOIN 返回两个表中所有匹配和不匹配的行。如果某行在另一个表中没有匹配项,则对应表的列将为 NULL。
模拟 FULL OUTER JOIN 通常需要结合 LEFT JOIN 和 UNION ALL。
- 执行一个
LEFT JOIN。 - 执行一个模拟的
RIGHT JOIN(即LEFT JOIN交换表顺序),并添加WHERE子句来排除已经在LEFT JOIN结果中出现的匹配行。 - 使用
UNION ALL将两个结果集合并。
FULL OUTER JOIN 逻辑: 返回所有客户和所有订单,无论它们是否匹配。
-- 模拟 FULL OUTER JOIN customers AND orders (简化示例)
-- 假设我们有一个 orders 表,其中可能包含一个 customer_id 不存在于 customers 表中的“孤儿订单”
-- (在实际应用中,外键约束ON DELETE RESTRICT/NO ACTION会防止这种情况,CASCADE则会删除订单)
-- 步骤 1: LEFT JOIN (所有客户 + 匹配的订单)
SELECT c.customer_id, c.first_name, o.order_id, o.total_amount
FROM customers AS c
LEFT JOIN orders AS o ON c.customer_id = o.customer_id
UNION ALL
-- 步骤 2: 模拟 RIGHT JOIN (所有订单 + 匹配的客户),但只保留订单在左表中没有匹配的行
SELECT c.customer_id, c.first_name, o.order_id, o.total_amount
FROM orders AS o
LEFT JOIN customers AS c ON o.customer_id = c.customer_id
WHERE c.customer_id IS NULL; -- 排除掉已经在 LEFT JOIN 中匹配的行
注意:上述 WHERE c.customer_id IS NULL 的条件非常关键,它筛选出那些只有 orders 表有记录,而 customers 表没有匹配记录的行,从而避免 UNION ALL 产生重复的匹配行。
5. JOIN 与其他 SQL 子句的结合
JOIN 操作通常与其他 SQL 子句结合使用,以构建更复杂和精确的查询。在 SQL 查询中,子句的逻辑执行顺序通常为:FROM -> ON (JOIN条件) -> WHERE -> GROUP BY -> HAVING -> SELECT -> DISTINCT -> ORDER BY -> LIMIT/OFFSET。
5.1 WHERE 子句:连接后过滤
WHERE 子句在 JOIN 操作完成之后,对连接后的结果集进行过滤。
-- 查找 Alice 的所有已完成订单
SELECT c.first_name, o.order_id, o.total_amount
FROM customers AS c
INNER JOIN orders AS o ON c.customer_id = o.customer_id
WHERE c.first_name = 'Alice' AND o.is_completed = TRUE;
5.2 ORDER BY 子句:连接后排序
ORDER BY 子句在 JOIN 和 WHERE 完成之后,对结果集进行排序。
-- 查找所有订单及其客户信息,按订单日期降序排序
SELECT c.first_name, o.order_id, o.order_date
FROM customers AS c
INNER JOIN orders AS o ON c.customer_id = o.customer_id
ORDER BY o.order_date DESC;
5.3 LIMIT / OFFSET:连接后分页
LIMIT 和 OFFSET 子句在所有其他操作(包括 JOIN, WHERE, ORDER BY)完成后,限制最终结果集的大小和起始位置。
-- 查找最新的 5 个订单及其客户信息
SELECT c.first_name, o.order_id, o.order_date
FROM customers AS c
INNER JOIN orders AS o ON c.customer_id = o.customer_id
ORDER BY o.order_date DESC
LIMIT 5;
5.4 GROUP BY / HAVING:连接后聚合
GROUP BY 子句在 JOIN 和 WHERE 之后执行,用于将具有相同值的行分组。HAVING 子句用于过滤分组后的结果。
-- 查找每个客户的总订单金额和订单数量
SELECT c.first_name, c.last_name, SUM(o.total_amount) AS total_spent, COUNT(o.order_id) AS order_count
FROM customers AS c
LEFT JOIN orders AS o ON c.customer_id = o.customer_id
GROUP BY c.customer_id
HAVING order_count > 1
ORDER BY total_spent DESC;
5.5 多个 JOIN
您可以根据需要连接两个以上的表。
-- 查找 Alice 的所有订单及订单中包含的产品详情
SELECT c.first_name, o.order_id, p.name AS product_name, oi.quantity, p.price, oi.quantity * p.price AS subtotal
FROM customers AS c
INNER JOIN orders AS o ON c.customer_id = o.customer_id
INNER JOIN order_items AS oi ON o.order_id = oi.order_id
INNER JOIN products AS p ON oi.product_id = p.product_id
WHERE c.first_name = 'Alice'
ORDER BY o.order_id, p.name;
6. 在 Python sqlite3 中使用 JOIN
在 Python sqlite3 模块中执行 JOIN 查询与执行其他 SELECT 语句类似。
6.1 执行 JOIN 查询
您只需要将包含 JOIN 子句的完整 SQL 语句作为字符串传递给 cursor.execute() 方法。
import sqlite3
import os
import datetime
db_name = 'join_db.db'
if os.path.exists(db_name):
os.remove(db_name)
with sqlite3.connect(db_name, detect_types=sqlite3.PARSE_DECLTYPES) as conn:
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
cursor.execute("PRAGMA foreign_keys = ON;") # 启用外键约束
# --- 准备数据 (简化版本,仅用于演示) ---
cursor.execute('''
CREATE TABLE IF NOT EXISTS departments (
dept_id INTEGER PRIMARY KEY AUTOINCREMENT,
dept_name TEXT NOT NULL UNIQUE
)
''')
cursor.execute('''
CREATE TABLE IF NOT EXISTS employees (
emp_id INTEGER PRIMARY KEY AUTOINCREMENT,
first_name TEXT NOT NULL,
last_name TEXT NOT NULL,
dept_id INTEGER,
salary REAL,
FOREIGN KEY (dept_id) REFERENCES departments(dept_id)
ON DELETE SET NULL -- 当部门被删除时,员工的部门ID设为NULL
)
''')
cursor.executemany("INSERT INTO departments (dept_name) VALUES (?)", [
("Sales",), ("Marketing",), ("Engineering",), ("HR",)
])
cursor.execute("SELECT dept_id FROM departments WHERE dept_name = 'Sales'")
sales_dept_id = cursor.fetchone()['dept_id']
cursor.execute("SELECT dept_id FROM departments WHERE dept_name = 'Marketing'")
marketing_dept_id = cursor.fetchone()['dept_id']
cursor.execute("SELECT dept_id FROM departments WHERE dept_name = 'Engineering'")
engineering_dept_id = cursor.fetchone()['dept_id']
cursor.executemany("INSERT INTO employees (first_name, last_name, dept_id, salary) VALUES (?, ?, ?, ?)", [
("Alice", "Smith", sales_dept_id, 60000.00),
("Bob", "Johnson", marketing_dept_id, 55000.00),
("Charlie", "Brown", engineering_dept_id, 75000.00),
("David", "Lee", sales_dept_id, 62000.00),
("Eve", "Wang", None, 50000.00), # Eve 没有部门
("Frank", "Green", marketing_dept_id, 58000.00)
])
print("数据库和初始数据已准备好。\n")
print("--- INNER JOIN: 员工及其所属部门 ---")
cursor.execute('''
SELECT e.first_name, e.last_name, d.dept_name
FROM employees AS e
INNER JOIN departments AS d ON e.dept_id = d.dept_id
ORDER BY d.dept_name, e.first_name
''')
for row in cursor.fetchall():
print(f" 员工: {row['first_name']} {row['last_name']}, 部门: {row['dept_name']}")
print("\n--- LEFT JOIN: 所有员工及其所属部门 (包括无部门员工) ---")
cursor.execute('''
SELECT e.first_name, e.last_name, d.dept_name
FROM employees AS e
LEFT JOIN departments AS d ON e.dept_id = d.dept_id
ORDER BY d.dept_name NULLS FIRST, e.first_name
''')
for row in cursor.fetchall():
dept_name = row['dept_name'] if row['dept_name'] else "无部门"
print(f" 员工: {row['first_name']} {row['last_name']}, 部门: {dept_name}")
6.2 参数绑定:安全传递 WHERE 或 ON 条件中的值
与任何其他 SELECT 语句一样,当 JOIN 查询中包含来自外部(例如用户输入)的值时,必须使用参数绑定。这适用于 WHERE 子句中的条件,甚至是在极少数情况下需要动态构建 ON 子句中的值。
# 查找特定部门的员工
target_dept_name = "Sales"
cursor.execute('''
SELECT e.first_name, e.last_name, d.dept_name
FROM employees AS e
INNER JOIN departments AS d ON e.dept_id = d.dept_id
WHERE d.dept_name = ?
ORDER BY e.first_name
''', (target_dept_name,))
print(f"\n--- 部门为 '{target_dept_name}' 的员工 (参数绑定) ---")
for row in cursor.fetchall():
print(f" 员工: {row['first_name']} {row['last_name']}")
6.3 处理连接后的结果
JOIN 查询的结果仍然是一个标准的行集合。您可以使用 cursor.fetchone(), cursor.fetchmany(), cursor.fetchall() 或直接迭代 cursor 来获取结果。
conn.row_factory = sqlite3.Row: 仍然强烈推荐使用,它允许您通过列名访问连接后的结果集中的列,提高可读性。当多个表有同名列时,可以通过AS alias给予别名来区分。detect_types=sqlite3.PARSE_DECLTYPES: 保持自动类型转换的优势。
7. 性能优化:索引与 JOIN
JOIN 操作的性能极大地依赖于表之间连接的列。
7.1 索引的重要性
当数据库执行 JOIN 操作时,它会查找匹配的行。如果在连接条件中使用的列上存在索引,数据库可以更快地定位到匹配的行,而无需进行全表扫描。
7.2 何时创建索引 (特别是外键)
- 外键列: 所有外键列都应该被索引。这是优化
JOIN性能的最重要一步。当您通过外键进行连接时,索引允许数据库快速查找父表中或子表中的匹配项。 - 主键列: 主键列默认会自动被索引,所以通常不需要额外创建。
ON或WHERE子句中频繁使用的列: 如果除了外键,还有其他列频繁用于JOIN的ON条件或WHERE子句中的过滤条件,也应该考虑为这些列创建索引。
示例 (为外键创建索引):
CREATE INDEX idx_employees_dept_id ON employees (dept_id);
7.3 选择必要的列
避免使用 SELECT *,尤其是在 JOIN 查询中。只选择您需要的列可以:
- 减少从磁盘读取的数据量。
- 减少网络传输量。
- 减少应用程序内存消耗。
- 如果所有被查询的列都在索引中(覆盖索引),查询性能会进一步提升。
8. 错误处理
在使用 JOIN 子句执行查询时,最常见的错误是 sqlite3.OperationalError。
8.1 sqlite3.OperationalError
这通常表示:
- SQL 语法错误。
- 引用的表名或列名不存在。
- 连接条件不正确或引用了不存在的列。
try:
# 尝试连接不存在的表
cursor.execute("SELECT * FROM employees INNER JOIN non_existent_table ON employees.dept_id = non_existent_table.id")
results = cursor.fetchall()
except sqlite3.OperationalError as e:
print(f"\n捕获到 OperationalError: {e}")
print("连接失败,请检查表名和列名是否正确。")
9. 最佳实践与注意事项
9.1 始终启用外键约束 (PRAGMA foreign_keys = ON;)
虽然 JOIN 操作本身只是查询,但外键约束是保证数据库数据完整性的核心。有了它,您可以依赖数据之间的逻辑关系,并确保连接操作的结果是基于有效数据的。在每个数据库连接开始时,都应该执行 PRAGMA foreign_keys = ON;。
9.2 参数绑定
再次强调,对于 WHERE 子句或 ON 子句中的任何动态值,始终使用参数绑定。这是防止 SQL 注入漏洞、确保正确类型转换的关键。
9.3 使用 with 语句管理资源
这确保了数据库连接和游标的正确关闭,即使在发生异常时也能自动回滚事务(对于修改数据的操作)。
9.4 使用 sqlite3.Row 提高可读性
当处理 JOIN 查询的结果时,通常会得到一个包含多个表列的行。sqlite3.Row 使得可以通过列名(或别名)方便地访问这些数据,而不是依赖不直观的索引。
9.5 显式连接 (INNER JOIN 等) 优于隐式连接
显式连接 (使用 INNER JOIN, LEFT JOIN 等关键字) 更清晰、更易读,并能更好地分离连接逻辑与过滤逻辑。避免在 FROM 子句中简单地列出多个表并在 WHERE 子句中指定连接条件的隐式连接。
9.6 ALTER TABLE 限制与常见变通方案
(这是针对表结构修改的提醒,与 JOIN 查询本身无关,但作为整个数据库操作的通用最佳实践值得再次提及)SQLite 对 ALTER TABLE 的支持相对有限。例如,它不能直接删除列,也不能直接修改列的数据类型或约束(除了少数例外)。对于更复杂的模式更改(例如,删除列、修改列类型、重新排序列),常见的变通方法是:
- 创建具有所需新结构的新表。
- 将旧表中的数据复制到新表(只复制兼容的列)。
- 删除旧表。
- 将新表重命名为旧表的名称。
理解这些限制对于规划数据库模式演进至关重要。
9.7 安全警告:避免动态 SQL 中的用户输入
如果您的应用程序需要根据用户输入动态地构建 JOIN 子句中的表名或列名,请务必对这些输入进行严格验证和白名单过滤。表名、列名和 SQL 关键字不能通过参数绑定传递。直接拼接未经净化的用户输入到 SQL 语句中可能会导致严重的 SQL 注入漏洞。
10. 综合代码示例
此示例将构建一个更完整的电商数据库模型,并演示各种 JOIN 类型及其与其它 SQL 子句的结合,包括 INNER JOIN, LEFT JOIN, CROSS JOIN,以及模拟 RIGHT JOIN 和 FULL OUTER JOIN。
import sqlite3
import os
import datetime
db_name = 'comprehensive_join_db.db'
if os.path.exists(db_name):
os.remove(db_name)
with sqlite3.connect(db_name, detect_types=sqlite3.PARSE_DECLTYPES) as conn:
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
cursor.execute("PRAGMA foreign_keys = ON;")
# --- 数据库结构和数据准备 ---
cursor.execute('''
CREATE TABLE IF NOT EXISTS customers (
customer_id INTEGER PRIMARY KEY AUTOINCREMENT,
first_name TEXT NOT NULL,
last_name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL,
registration_date DATE DEFAULT CURRENT_DATE
)
''')
cursor.execute('''
CREATE TABLE IF NOT EXISTS products (
product_id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT UNIQUE NOT NULL,
price REAL CHECK(price > 0) DEFAULT 0.0,
stock INTEGER DEFAULT 0 CHECK(stock >= 0)
)
''')
cursor.execute('''
CREATE TABLE IF NOT EXISTS orders (
order_id INTEGER PRIMARY KEY AUTOINCREMENT,
customer_id INTEGER NOT NULL,
order_date DATETIME DEFAULT CURRENT_TIMESTAMP,
total_amount REAL,
is_completed BOOLEAN DEFAULT FALSE,
FOREIGN KEY (customer_id) REFERENCES customers(customer_id)
ON DELETE CASCADE
)
''')
cursor.execute('''
CREATE TABLE IF NOT EXISTS order_items (
item_id INTEGER PRIMARY KEY AUTOINCREMENT,
order_id INTEGER NOT NULL,
product_id INTEGER NOT NULL,
quantity INTEGER CHECK(quantity > 0),
price_at_order REAL, -- 记录下单时的价格
FOREIGN KEY (order_id) REFERENCES orders(order_id)
ON DELETE CASCADE,
FOREIGN KEY (product_id) REFERENCES products(product_id)
ON DELETE RESTRICT
)
''')
# 插入客户
cursor.executemany("INSERT INTO customers (first_name, last_name, email, registration_date) VALUES (?, ?, ?, ?)", [
("Alice", "Smith", "alice@example.com", datetime.date(2023, 1, 10)),
("Bob", "Johnson", "bob@example.com", datetime.date(2023, 1, 15)),
("Charlie", "Brown", "charlie@example.com", datetime.date(2023, 2, 20)),
("David", "Lee", "david@example.com", datetime.date(2023, 3, 5))
])
# 插入产品
cursor.executemany("INSERT INTO products (name, price, stock) VALUES (?, ?, ?)", [
("Laptop", 1200.50, 20),
("Mouse", 25.00, 150),
("Keyboard", 75.99, 80),
("Monitor", 300.00, 30),
("Webcam", 49.99, 100)
])
# 获取 ID 用于订单插入
cursor.execute("SELECT customer_id FROM customers WHERE email = 'alice@example.com'")
alice_id = cursor.fetchone()['customer_id']
cursor.execute("SELECT customer_id FROM customers WHERE email = 'bob@example.com'")
bob_id = cursor.fetchone()['customer_id']
cursor.execute("SELECT customer_id FROM customers WHERE email = 'charlie@example.com'")
charlie_id = cursor.fetchone()['customer_id']
cursor.execute("SELECT product_id, price FROM products WHERE name = 'Laptop'")
laptop_info = cursor.fetchone()
laptop_id, laptop_price = laptop_info['product_id'], laptop_info['price']
cursor.execute("SELECT product_id, price FROM products WHERE name = 'Mouse'")
mouse_info = cursor.fetchone()
mouse_id, mouse_price = mouse_info['product_id'], mouse_info['price']
cursor.execute("SELECT product_id, price FROM products WHERE name = 'Keyboard'")
keyboard_info = cursor.fetchone()
keyboard_id, keyboard_price = keyboard_info['product_id'], keyboard_info['price']
# 插入订单
cursor.execute("INSERT INTO orders (customer_id, order_date, total_amount, is_completed) VALUES (?, ?, ?, ?)",
(alice_id, datetime.datetime(2023, 1, 20, 10, 30), 1250.50, False))
order1_id = cursor.lastrowid
cursor.execute("INSERT INTO order_items (order_id, product_id, quantity, price_at_order) VALUES (?, ?, ?, ?)",
(order1_id, laptop_id, 1, laptop_price))
cursor.execute("INSERT INTO orders (customer_id, order_date, total_amount, is_completed) VALUES (?, ?, ?, ?)",
(bob_id, datetime.datetime(2023, 3, 1, 14, 0), 100.99, True))
order2_id = cursor.lastrowid
cursor.execute("INSERT INTO order_items (order_id, product_id, quantity, price_at_order) VALUES (?, ?, ?, ?)",
(order2_id, mouse_id, 2, mouse_price))
cursor.execute("INSERT INTO order_items (order_id, product_id, quantity, price_at_order) VALUES (?, ?, ?, ?)",
(order2_id, keyboard_id, 1, keyboard_price))
cursor.execute("INSERT INTO orders (customer_id, order_date, total_amount, is_completed) VALUES (?, ?, ?, ?)",
(alice_id, datetime.datetime(2023, 2, 5, 9, 0), 75.99, True))
order3_id = cursor.lastrowid
cursor.execute("INSERT INTO order_items (order_id, product_id, quantity, price_at_order) VALUES (?, ?, ?, ?)",
(order3_id, keyboard_id, 1, keyboard_price))
# 模拟一个没有订单的客户
cursor.execute("INSERT INTO customers (first_name, last_name, email, registration_date) VALUES (?, ?, ?, ?)",
("Eve", "NoOrders", "eve@example.com", datetime.date(2023, 5, 1)))
print("数据库和初始数据已准备好。\n")
# --- 创建外键索引 (最佳实践) ---
cursor.execute("CREATE INDEX idx_orders_customer_id ON orders (customer_id);")
cursor.execute("CREATE INDEX idx_order_items_order_id ON order_items (order_id);")
cursor.execute("CREATE INDEX idx_order_items_product_id ON order_items (product_id);")
print("已创建外键索引。\n")
# --- 演示各种 JOIN 查询 ---
print("--- 1. INNER JOIN: 客户及其订单 (只显示有订单的客户) ---")
cursor.execute('''
SELECT c.first_name, c.last_name, o.order_id, o.order_date, o.total_amount
FROM customers AS c
INNER JOIN orders AS o ON c.customer_id = o.customer_id
ORDER BY o.order_date DESC
''')
for row in cursor.fetchall():
print(f" 客户: {row['first_name']} {row['last_name']}, 订单ID: {row['order_id']}, 日期: {row['order_date'].strftime('%Y-%m-%d %H:%M')}, 金额: ${row['total_amount']:.2f}")
print("\n--- 2. LEFT JOIN: 所有客户及其订单 (包括没有订单的客户) ---")
cursor.execute('''
SELECT c.first_name, c.last_name, o.order_id, o.order_date, o.total_amount
FROM customers AS c
LEFT JOIN orders AS o ON c.customer_id = o.customer_id
ORDER BY c.last_name, c.first_name, o.order_date DESC
''')
for row in cursor.fetchall():
order_info = f"订单ID: {row['order_id']}, 日期: {row['order_date'].strftime('%Y-%m-%d %H:%M')}, 金额: ${row['total_amount']:.2f}" if row['order_id'] else "无订单"
print(f" 客户: {row['first_name']} {row['last_name']}, {order_info}")
print("\n--- 3. 多个 INNER JOIN: 订单详情 (客户、订单、商品信息) ---")
cursor.execute('''
SELECT
c.first_name || ' ' || c.last_name AS customer_name,
o.order_id,
o.order_date,
p.name AS product_name,
oi.quantity,
oi.price_at_order AS unit_price,
(oi.quantity * oi.price_at_order) AS item_total
FROM customers AS c
INNER JOIN orders AS o ON c.customer_id = o.customer_id
INNER JOIN order_items AS oi ON o.order_id = oi.order_id
INNER JOIN products AS p ON oi.product_id = p.product_id
WHERE c.first_name = ?
ORDER BY o.order_id, p.name
''', ('Bob',))
print(" Bob 的订单详情:")
for row in cursor.fetchall():
print(f" 订单 {row['order_id']} ({row['order_date'].strftime('%Y-%m-%d %H:%M')}): {row['product_name']} x{row['quantity']} @${row['unit_price']:.2f} = ${row['item_total']:.2f}")
print("\n--- 4. CROSS JOIN: 所有客户和所有产品组合 (仅前5条) ---")
cursor.execute('''
SELECT c.first_name, p.name
FROM customers AS c
CROSS JOIN products AS p
LIMIT 5
''')
for row in cursor.fetchall():
print(f" {row['first_name']} - {row['name']}")
print("\n--- 5. 模拟 RIGHT JOIN: 所有订单及其客户 (将 orders 作为左表) ---")
# 这个例子等同于 LEFT JOIN orders ON customers,因为订单customer_id在客户表中始终能找到匹配
# 如果有“孤儿订单”(customer_id在客户表中不存在),这个模拟就更有意义
cursor.execute('''
SELECT o.order_id, o.order_date, c.first_name, c.last_name
FROM orders AS o
LEFT JOIN customers AS c ON o.customer_id = c.customer_id
ORDER BY o.order_id
''')
print(" 所有订单及其客户信息 (模拟 RIGHT JOIN):")
for row in cursor.fetchall():
customer_name = f"{row['first_name']} {row['last_name']}" if row['first_name'] else "未知客户"
print(f" 订单ID: {row['order_id']}, 日期: {row['order_date'].strftime('%Y-%m-%d %H:%M')}, 客户: {customer_name}")
# 为了更好地演示 RIGHT JOIN 的效果,我们手动插入一个“孤儿订单”(即 customer_id 不存在)
# 但在有 PRAGMA foreign_keys = ON 的情况下,这种插入会失败。
# 理论上,SQLite会强制外键约束,所以不会有真正意义上的“孤儿订单”
# 如果要演示,需要先 PRAGMA foreign_keys = OFF
# 例如:
# conn.execute("PRAGMA foreign_keys = OFF;")
# conn.execute("INSERT INTO orders (customer_id, order_date, total_amount, is_completed) VALUES (?, ?, ?, ?)",
# (99999, datetime.datetime.now(), 10.00, False)) # 99999 是一个不存在的客户ID
# conn.execute("PRAGMA foreign_keys = ON;")
# 再次运行 LEFT JOIN orders AS o LEFT JOIN customers AS c ON o.customer_id = c.customer_id
# 此时,99999的订单会显示,客户列为NULL
print("\n--- 6. 模拟 FULL OUTER JOIN (客户和订单,无论是否匹配) ---")
cursor.execute('''
SELECT c.customer_id, c.first_name, c.last_name, o.order_id, o.total_amount
FROM customers AS c
LEFT JOIN orders AS o ON c.customer_id = o.customer_id
UNION ALL
SELECT c.customer_id, c.first_name, c.last_name, o.order_id, o.total_amount
FROM orders AS o
LEFT JOIN customers AS c ON o.customer_id = c.customer_id
WHERE c.customer_id IS NULL -- 过滤掉已经在 LEFT JOIN 结果中的匹配项
ORDER BY customer_id NULLS FIRST, order_id NULLS FIRST
''')
print(" 所有客户和订单 (模拟 FULL OUTER JOIN):")
for row in cursor.fetchall():
cust_id = row['customer_id'] if row['customer_id'] else "N/A"
cust_name = f"{row['first_name']} {row['last_name']}" if row['first_name'] else "未知客户"
order_info = f"订单ID: {row['order_id']}, 金额: ${row['total_amount']:.2f}" if row['order_id'] else "无订单"
print(f" 客户ID: {cust_id}, 姓名: {cust_name}, {order_info}")
# 注意上述模拟 FULL OUTER JOIN 的 WHERE c.customer_id IS NULL 在有外键约束时不会返回任何行,
# 因为所有订单都必然有一个匹配的客户。它的作用是处理“孤儿订单”(如果存在)。
# 要看到实际效果,可以先禁用外键,插入孤儿订单,再启用外键。
print(f"\n所有 `JOIN` 子句演示完成,数据库 '{db_name}' 已更新。")
11. 总结
为您详细介绍了 Python SQLite 中 JOIN 子句的各个方面。
核心要点回顾:
JOIN子句是关联和组合来自多个表数据的强大工具,基于表之间的关系 (主键和外键)。- 理解并熟练使用不同的
JOIN类型:INNER JOIN: 返回所有匹配的行。LEFT JOIN: 返回左表所有行和右表匹配行(右表不匹配处为NULL)。CROSS JOIN: 返回笛卡尔积。
- SQLite 不直接支持
RIGHT JOIN和FULL OUTER JOIN,但可以通过交换表顺序 (RIGHT JOIN) 和结合LEFT JOIN与UNION ALL(FULL OUTER JOIN) 来模拟。 JOIN查询可以与WHERE,ORDER BY,LIMIT/OFFSET,GROUP BY/HAVING等所有其他SELECT子句结合使用。- 在 Python
sqlite3中执行JOIN查询,始终使用参数绑定来传递WHERE或ON子句中的动态值,以防止 SQL 注入。 - 为外键列创建索引是优化
JOIN查询性能最关键的一步。 - 遵循最佳实践,包括始终启用外键约束、使用
with语句、使用sqlite3.Row提高可读性,并显式使用JOIN关键字。
通过这些全面而深入的知识,您将能够有效地处理和组合 SQLite 数据库中的关联数据,构建功能丰富、高效和安全的 Python 数据库应用程序。
清理测试文件 (为确保后续测试环境干净)
import os
files_to_clean = [
'join_db.db',
'comprehensive_join_db.db'
]
for f in files_to_clean:
if os.path.exists(f):
os.remove(f)
print("\n所有测试数据库文件已清理。")
更多推荐

所有评论(0)