I'm Aron

MySQL 基础(三):函数、约束、表关系与多表查询

2122 字
11 分钟
MySQL 基础(三):函数、约束、表关系与多表查询

1. 常用函数#

1.1 字符串函数#

函数作用示例
CONCAT(s1, s2, ...)拼接字符串CONCAT('Hello', ' MySQL')
LOWER(str)转小写LOWER('MySQL')
UPPER(str)转大写UPPER('mysql')
LPAD(str, n, pad)左侧填充到指定长度LPAD('1', 5, '0')
RPAD(str, n, pad)右侧填充到指定长度RPAD('1', 5, '0')
TRIM(str)去除两端空格TRIM(' abc ')
SUBSTRING(str, start, len)截取字符串;位置从 1 开始SUBSTRING('Hello', 1, 2)
LENGTH(str)返回字节数LENGTH('中国')
CHAR_LENGTH(str)返回字符数CHAR_LENGTH('中国')
REPLACE(str, from, to)替换子串REPLACE('abc', 'b', 'B')

工号补零:

UPDATE emp
SET work_no = LPAD(work_no, 5, '0');

CONCAT 的任一参数为 NULL 时,结果可能为 NULL。需要忽略空值拼接时,可使用 CONCAT_WS 或先用 COALESCE 处理。

1.2 数值函数#

函数作用
CEIL(x) / CEILING(x)向上取整
FLOOR(x)向下取整
ROUND(x, d)四舍五入并保留 d 位小数
ABS(x)绝对值
MOD(x, y)取余
RAND()生成 [0, 1) 范围的伪随机数
POW(x, y) / POWER(x, y)幂运算
SQRT(x)平方根
SELECT CEIL(1.1), FLOOR(1.9), MOD(7, 4), ROUND(2.344, 2);

资料中的六位验证码示例适合演示函数,但不适合安全验证码。安全验证码应由应用层使用密码学安全随机源生成,并设置有效期与重试限制。

1.3 日期函数#

函数作用
CURDATE()当前日期
CURTIME()当前时间
NOW()当前日期和时间
YEAR(date) / MONTH(date) / DAY(date)提取年月日
DATE_ADD(date, INTERVAL n unit)增加时间间隔
DATE_SUB(date, INTERVAL n unit)减少时间间隔
DATEDIFF(date1, date2)date1 - date2 的天数
DATE_FORMAT(date, format)格式化日期时间
SELECT NOW();
SELECT DATE_ADD(CURDATE(), INTERVAL 7 DAY);
SELECT DATEDIFF(CURDATE(), '2026-01-01');
SELECT DATE_FORMAT(NOW(), '%Y-%m-%d %H:%i:%s');

查询员工入职天数:

SELECT
name,
DATEDIFF(CURDATE(), entry_date) AS entry_days
FROM emp
ORDER BY entry_days DESC;

1.4 流程控制函数#

-- IF
SELECT IF(score >= 60, '及格', '不及格');
-- IFNULL:第一个参数为 NULL 时返回第二个参数
SELECT IFNULL(nickname, name) AS display_name
FROM user;
-- COALESCE:返回第一个非 NULL 参数
SELECT COALESCE(nickname, username, '匿名用户') AS display_name
FROM user;

简单 CASE

SELECT
name,
CASE work_address
WHEN '北京' THEN '一线城市'
WHEN '上海' THEN '一线城市'
ELSE '其他城市'
END AS city_level
FROM emp;

搜索 CASE

SELECT
name,
CASE
WHEN score >= 85 THEN '优秀'
WHEN score >= 60 THEN '及格'
ELSE '不及格'
END AS score_level
FROM student_score;

条件从上到下判断,应把更严格的条件放在前面。


2. 约束与表关系#

2.1 常用约束#

约束关键字作用
非空NOT NULL禁止字段为 NULL
唯一UNIQUE禁止出现重复值
主键PRIMARY KEY唯一标识一行,天然非空且唯一
默认值DEFAULT插入时未指定该字段则采用默认值
检查CHECK限制字段值必须满足条件
外键FOREIGN KEY维护父子表之间的引用完整性
CREATE TABLE app_user (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) NOT NULL UNIQUE,
age TINYINT UNSIGNED,
status TINYINT NOT NULL DEFAULT 1,
CONSTRAINT chk_app_user_age CHECK (age IS NULL OR age BETWEEN 1 AND 120),
CONSTRAINT chk_app_user_status CHECK (status IN (0, 1))
);

补充:

  • 一张表只能有一个主键,但主键可以由多个字段组成。
  • AUTO_INCREMENT 常与整数主键配合。
  • MySQL 8.0.16 以后,CHECK 约束才真正执行检查;本资料使用的 8.0.26 会执行。
  • UNIQUENULL 的处理与普通值不同,不应把它当作“必须填写”的替代品;需要必填时同时加 NOT NULL

2.2 外键#

建表时添加:

CREATE TABLE dept (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50) NOT NULL UNIQUE
);
CREATE TABLE emp (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50) NOT NULL,
dept_id BIGINT UNSIGNED,
CONSTRAINT fk_emp_dept
FOREIGN KEY (dept_id) REFERENCES dept(id)
);

已有表添加或删除:

ALTER TABLE emp
ADD CONSTRAINT fk_emp_dept
FOREIGN KEY (dept_id) REFERENCES dept(id);
ALTER TABLE emp
DROP FOREIGN KEY fk_emp_dept;

父表字段和子表外键字段的类型、符号属性应一致;创建外键前,已有数据也必须满足引用关系。

2.3 外键更新与删除行为#

行为说明
RESTRICT / NO ACTION存在子表引用时,拒绝删除或更新父表记录
CASCADE父表更新或删除时,级联更新或删除子表记录
SET NULL父表变化时将子表外键设为 NULL;外键列必须允许 NULL
SET DEFAULTInnoDB 不支持该行为
ALTER TABLE emp
ADD CONSTRAINT fk_emp_dept
FOREIGN KEY (dept_id) REFERENCES dept(id)
ON UPDATE CASCADE
ON DELETE SET NULL;

ON DELETE CASCADE 可能批量删除大量子表数据,应根据业务语义谨慎选择。

2.4 三种常见表关系#

一对多#

例:一个部门有多个员工,一个员工属于一个部门。

实现:在“多”的一方 emp 保存外键 dept_id,指向 dept.id

多对多#

例:学生可以选择多门课程,一门课程可以被多个学生选择。

实现:增加中间表,并用联合唯一约束避免重复选课。

CREATE TABLE student_course (
student_id BIGINT UNSIGNED NOT NULL,
course_id BIGINT UNSIGNED NOT NULL,
selected_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (student_id, course_id),
CONSTRAINT fk_sc_student
FOREIGN KEY (student_id) REFERENCES student(id),
CONSTRAINT fk_sc_course
FOREIGN KEY (course_id) REFERENCES course(id)
);

一对一#

例:用户基本信息和用户教育信息。

实现:在详情表中使用外键,并为外键增加 UNIQUE;也可以直接让详情表主键同时作为外键。

CREATE TABLE user_profile (
user_id BIGINT UNSIGNED PRIMARY KEY,
degree VARCHAR(20),
university VARCHAR(100),
CONSTRAINT fk_profile_user
FOREIGN KEY (user_id) REFERENCES app_user(id)
);


3 多表查询#

3.1 笛卡尔积与连接条件#

SELECT * FROM emp, dept;

emp 有 17 行、dept 有 6 行,会得到 102 种组合。多表查询必须明确连接关系,避免无意义的笛卡尔积。

3.2 内连接#

内连接只返回两表能够匹配的记录。

-- 显式内连接,推荐
SELECT
e.id,
e.name AS employee_name,
d.name AS department_name
FROM emp AS e
INNER JOIN dept AS d ON d.id = e.dept_id;

隐式内连接也能实现,但复杂查询中不够直观:

SELECT e.name, d.name
FROM emp AS e, dept AS d
WHERE e.dept_id = d.id;

推荐使用显式 JOIN ... ON ...,将“表怎么关联”和“业务如何过滤”分开。

3.3 外连接#

左连接保留左表全部记录;右表未匹配的列补 NULL

SELECT e.id, e.name, d.name AS department_name
FROM emp AS e
LEFT JOIN dept AS d ON d.id = e.dept_id;

查询所有部门,包括没有员工的部门:

SELECT
d.id,
d.name,
COUNT(e.id) AS employee_count
FROM dept AS d
LEFT JOIN emp AS e ON e.dept_id = d.id
GROUP BY d.id, d.name;

这里应使用 COUNT(e.id),不能用 COUNT(*);对于没有员工的部门,左连接仍产生一行占位结果,COUNT(*) 会错误地计为 1。

3.4 ONWHERE 的位置#

对内连接而言,很多过滤条件放在 ONWHERE 中结果相同;对外连接而言,两者可能完全不同。

保留所有部门,只连接在职员工:

SELECT d.name, e.name
FROM dept AS d
LEFT JOIN emp AS e
ON e.dept_id = d.id
AND e.status = 1;

如果把 e.status = 1 放入 WHERE,右表为 NULL 的行会被过滤,结果可能退化得像内连接。

3.5 自连接#

同一张表扮演两个角色时必须使用不同别名:

SELECT
e.name AS employee_name,
m.name AS manager_name
FROM emp AS e
LEFT JOIN emp AS m ON m.id = e.manager_id;

3.6 UNION 与 UNION ALL#

SELECT id, name FROM emp WHERE salary < 5000
UNION ALL
SELECT id, name FROM emp WHERE age > 50;
  • UNION ALL 直接合并,保留重复行,通常更快。
  • UNION 合并后去重。
  • 各查询的列数必须相同,对应列的数据类型应兼容。

MySQL 没有直接的 FULL OUTER JOIN 语法。需要完整保留两边时,可用左连接与“仅右表未匹配部分”组合:

SELECT a.id AS a_id, b.id AS b_id
FROM table_a AS a
LEFT JOIN table_b AS b ON b.a_id = a.id
UNION ALL
SELECT a.id AS a_id, b.id AS b_id
FROM table_a AS a
RIGHT JOIN table_b AS b ON b.a_id = a.id
WHERE a.id IS NULL;

3.7 子查询分类#

分类返回结果常见操作符
标量子查询一行一列=<>>>=<<=
列子查询多行一列INNOT INANYSOMEALL
行子查询一行多列=<>INNOT IN
表子查询多行多列放在 FROM 后作为派生表,或配合多列 IN

标量子查询:

SELECT *
FROM emp
WHERE salary > (SELECT AVG(salary) FROM emp);

列子查询:

SELECT *
FROM emp
WHERE dept_id IN (
SELECT id
FROM dept
WHERE name IN ('研发部', '市场部')
);

行子查询:

SELECT *
FROM emp
WHERE (salary, manager_id) = (
SELECT salary, manager_id
FROM emp
WHERE name = '张无忌'
);

表子查询:

SELECT e.name, d.name AS department_name
FROM (
SELECT *
FROM emp
WHERE entry_date > '2006-01-01'
) AS e
LEFT JOIN dept AS d ON d.id = e.dept_id;

3.8 相关子查询与 EXISTS#

相关子查询会引用外层查询字段:

-- 查询低于本部门平均工资的员工
SELECT e.id, e.name, e.salary, e.dept_id
FROM emp AS e
WHERE e.salary < (
SELECT AVG(e2.salary)
FROM emp AS e2
WHERE e2.dept_id = e.dept_id
);

EXISTS 关注子查询是否至少返回一行:

-- 查询至少有一名员工的部门
SELECT d.id, d.name
FROM dept AS d
WHERE EXISTS (
SELECT 1
FROM emp AS e
WHERE e.dept_id = d.id
);

SELECT 1 表示只关心是否存在,不依赖子查询实际返回哪些列。


评论区

文章目录