MYSQL JOIN 的简单说明

38 阅读3分钟

JOIN 查询详解笔记

1. SQL 执行顺序

SQL 查询按照以下逻辑顺序执行:

  1. FROM 和 JOIN 阶段

    • 处理 FROM 子句中的基础表
    • 按顺序处理 JOIN 操作
    • 根据 ON 条件进行表连接
  2. WHERE 过滤阶段

    • 应用 WHERE 条件过滤记录
  3. SELECT 选择阶段

    • 选择指定的列
    • 执行表达式和函数计算
  4. ORDER BY 排序阶段

    • 对结果进行排序
  5. LIMIT 限制阶段

    • 限制返回的记录数量

⚠️ 注意:虽然 WHERESELECT 之后书写,但执行时 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 值,使用 COALESCEISNULL
  • ✅ 注意一对多关系导致的重复行

可读性维护

  • ✅ 使用表别名(如 dt, dti, udtr
  • ✅ 明确指定列名,避免 SELECT *
  • ✅ 复杂查询添加注释说明业务逻辑

常见陷阱

  • ❌ 隐式 JOIN(逗号分隔的旧式语法)
  • ❌ 忘记 ON 条件导致性能问题
  • ❌ 混淆 LEFT/RIGHT JOIN 的保留表概念

JOIN 中 ON 条件的理解

ON 条件的双重作用

ON 条件确实起到了过滤的作用,但它不仅仅是简单的过滤,而是:

  1. 关联条件定义:定义两个表如何关联
  2. 匹配过滤:过滤出满足关联条件的记录对
  3. 结果集构建:影响最终结果集的构成

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

idname
1A
2B

table2

idtable1_idstatus
11active
21inactive
32inactive
使用 ON 过滤
SELECT t1.name, t2.status 
FROM table1 t1
LEFT JOIN table2 t2 ON t1.id = t2.table1_id AND t2.status = 'active'

结果:

namestatus
Aactive
BNULL
使用 WHERE 过滤
SELECT t1.name, t2.status 
FROM table1 t1
LEFT JOIN table2 t2 ON t1.id = t2.table1_id
WHERE t2.status = 'active'

结果:

namestatus
Aactive

在你的代码中的体现

RIGHT JOIN daily_task dt ON dt.id = dti.task_id AND dt.is_active = 1

这里的 dt.is_active = 1 是在:

  1. 连接时确定哪些 daily_task 记录可以与 daily_task_inventory 关联
  2. 过滤掉非活跃状态的任务(即使它们在右表中存在)

所以你的理解是对的 - ON 确实起到了过滤作用,但更准确地说,它是在连接过程中进行的关联过滤