JOIN 查询详解笔记
1. SQL 执行顺序
SQL 查询按照以下逻辑顺序执行:
-
FROM 和 JOIN 阶段
- 处理
FROM子句中的基础表 - 按顺序处理
JOIN操作 - 根据
ON条件进行表连接
- 处理
-
WHERE 过滤阶段
- 应用
WHERE条件过滤记录
- 应用
-
SELECT 选择阶段
- 选择指定的列
- 执行表达式和函数计算
-
ORDER BY 排序阶段
- 对结果进行排序
-
LIMIT 限制阶段
- 限制返回的记录数量
⚠️ 注意:虽然
WHERE在SELECT之后书写,但执行时WHERE过滤发生在SELECT之前
2. 坐标系理解法
两表 JOIN(二维坐标系)
Y轴 (右表)
↑
| ● ● ← INNER JOIN: 只显示匹配点
| ● ○ ← LEFT/RIGHT JOIN: 无匹配用○表示NULL
| ●
|● ●
+------------→ X轴 (左表)
三表 JOIN(三维坐标系)
Z轴
↑ ●
| /|\
| / | \ ← 三个表都匹配的空间点
| ●--+--●
|/ | /
●----+-- ← 投影到各平面的连接关系
+----●----→ X轴
3. JOIN 类型详解
INNER JOIN
- 只返回两个表中都有匹配的记录
- 最严格的连接方式
SELECT * FROM table1 t1
INNER JOIN table2 t2 ON t1.id = t2.table1_id;
LEFT JOIN
- 返回左表所有记录,右表无匹配则为 NULL
- 保留左表完整性
SELECT * FROM table1 t1
LEFT JOIN table2 t2 ON t1.id = t2.table1_id;
RIGHT JOIN
- 返回右表所有记录,左表无匹配则为 NULL
- 保留右表完整性
SELECT * FROM table1 t1
RIGHT JOIN table2 t2 ON t1.id = t2.table1_id;
4. 实际应用示例
业务场景表结构
// 任务库存表
data class DailyTaskInventory(
val taskId: Int,
val inventoryDate: Date,
val totalInventory: Int,
val usedInventory: Int
)
// 任务表
data class DailyTask(
val id: Int,
val taskName: String,
val isActive: Boolean,
val minUserLevel: Int,
val maxUserLevel: Int
)
// 用户任务记录表
data class UserDailyTaskRecord(
val taskId: Int,
val userId: Int,
val taskStatus: Int,
val completeDate: Date
)
三表连接查询示例
@Select("""
SELECT
dt.id as task_id,
dt.task_name,
dti.total_inventory,
dti.used_inventory,
CASE
WHEN udtr.task_status = 1 THEN '已完成'
WHEN udtr.task_status = 0 THEN '待审核'
ELSE '未领取'
END as task_status
FROM daily_task_inventory dti
RIGHT JOIN daily_task dt ON dt.id = dti.task_id AND dt.is_active = 1
LEFT JOIN user_daily_task_record udtr ON dt.id = udtr.task_id
AND udtr.user_id = #{userId}
AND udtr.complete_date = CURDATE()
WHERE dti.inventory_date = CURDATE()
""")
fun getAvailableTasks(userId: Int): List<TaskInventoryItem>
5. 重要注意事项
性能优化
- ✅ 在
JOIN条件列上创建索引 - ✅ 将小表放在
JOIN的左侧(某些数据库) - ✅ 避免笛卡尔积(缺少
ON条件)
数据准确性
- ✅ 明确
JOIN条件,避免错误连接 - ✅ 处理 NULL 值,使用
COALESCE或ISNULL - ✅ 注意一对多关系导致的重复行
可读性维护
- ✅ 使用表别名(如
dt,dti,udtr) - ✅ 明确指定列名,避免
SELECT * - ✅ 复杂查询添加注释说明业务逻辑
常见陷阱
- ❌ 隐式 JOIN(逗号分隔的旧式语法)
- ❌ 忘记
ON条件导致性能问题 - ❌ 混淆 LEFT/RIGHT JOIN 的保留表概念
JOIN 中 ON 条件的理解
ON 条件的双重作用
ON 条件确实起到了过滤的作用,但它不仅仅是简单的过滤,而是:
- 关联条件定义:定义两个表如何关联
- 匹配过滤:过滤出满足关联条件的记录对
- 结果集构建:影响最终结果集的构成
ON vs WHERE 的区别
ON 条件(连接时过滤)
SELECT * FROM table1 t1
LEFT JOIN table2 t2 ON t1.id = t2.table1_id AND t2.status = 'active'
- 在连接过程中应用
- 影响连接的结果(右表无匹配时返回 NULL)
WHERE 条件(连接后过滤)
SELECT * FROM table1 t1
LEFT JOIN table2 t2 ON t1.id = t2.table1_id
WHERE t2.status = 'active'
- 在连接完成后应用
- 可能过滤掉左表的记录
实际示例对比
假设有以下数据:
table1
| id | name |
|---|---|
| 1 | A |
| 2 | B |
table2
| id | table1_id | status |
|---|---|---|
| 1 | 1 | active |
| 2 | 1 | inactive |
| 3 | 2 | inactive |
使用 ON 过滤
SELECT t1.name, t2.status
FROM table1 t1
LEFT JOIN table2 t2 ON t1.id = t2.table1_id AND t2.status = 'active'
结果:
| name | status |
|---|---|
| A | active |
| B | NULL |
使用 WHERE 过滤
SELECT t1.name, t2.status
FROM table1 t1
LEFT JOIN table2 t2 ON t1.id = t2.table1_id
WHERE t2.status = 'active'
结果:
| name | status |
|---|---|
| A | active |
在你的代码中的体现
RIGHT JOIN daily_task dt ON dt.id = dti.task_id AND dt.is_active = 1
这里的 dt.is_active = 1 是在:
- 连接时确定哪些
daily_task记录可以与daily_task_inventory关联 - 过滤掉非活跃状态的任务(即使它们在右表中存在)
所以你的理解是对的 - ON 确实起到了过滤作用,但更准确地说,它是在连接过程中进行的关联过滤。