1. 为什么我们需要数据库约束?
记得刚入行时,我负责维护一个电商系统的用户表。有天运营同事慌慌张张跑来说,后台出现了大量手机号相同的用户订单。检查后发现,由于没有设置唯一约束,用户注册时手机号字段竟然可以重复插入。这个事故让我深刻理解了约束的重要性。
数据库约束本质上是对表中数据的限制规则,就像交通规则保证道路秩序一样。在MySQL中,约束主要分为以下几类:
1.1 非空约束(NOT NULL)
创建表时,在字段后直接加上NOT NULL即可:
sql复制CREATE TABLE users (
id INT PRIMARY KEY,
username VARCHAR(50) NOT NULL,
email VARCHAR(100)
);
这里username字段必须填写,而email可以为NULL。实际项目中,我建议关键业务字段都应设为NOT NULL,因为:
- 避免业务逻辑中出现NPE(空指针异常)
- 空值会占用额外存储空间(1bit标记位)
- 包含NULL的列会使索引、索引统计和值比较更复杂
注意:对于历史原因必须允许NULL的字段,在应用层要统一处理NULL的情况,比如COALESCE(email, '')函数。
1.2 唯一约束(UNIQUE)
确保某列或多列的值唯一:
sql复制-- 单列唯一
ALTER TABLE users ADD UNIQUE (email);
-- 多列组合唯一
ALTER TABLE orders ADD CONSTRAINT uk_user_product
UNIQUE (user_id, product_id);
我在实际项目中遇到过这样的坑:明明加了UNIQUE约束,还是出现了重复数据。后来发现是因为:
- NULL值在UNIQUE约束中会被视为不同的值(可以有多条NULL记录)
- 字符类型末尾空格可能导致唯一性失效(建议用BINARY属性)
1.3 主键约束(PRIMARY KEY)
主键是特殊的唯一约束,区别在于:
- 不允许NULL值
- 每个表只能有一个
- 默认会建立聚簇索引(InnoDB)
自增主键的常见误区:
sql复制CREATE TABLE products (
id INT AUTO_INCREMENT PRIMARY KEY,
...
);
很多人不知道AUTO_INCREMENT会在重启后重置为MAX(id)+1。在分布式系统中,建议使用雪花算法等分布式ID生成方案。
1.4 外键约束(FOREIGN KEY)
确保数据引用完整性:
sql复制CREATE TABLE orders (
id INT PRIMARY KEY,
user_id INT,
FOREIGN KEY (user_id) REFERENCES users(id)
);
外键虽然能保证数据一致性,但在高并发场景下要谨慎:
- 影响写入性能(需要检查引用约束)
- 级联删除可能导致意外数据丢失
- 分库分表环境下难以使用
1.5 检查约束(CHECK)
MySQL 8.0+才真正支持CHECK约束:
sql复制CREATE TABLE employees (
salary DECIMAL(10,2) CHECK (salary > 0)
);
低版本可以通过触发器实现类似功能。我在财务系统中就用触发器实现了复杂的业务规则校验。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 聚合查询:从基础统计到高级分析
2.1 五大基础聚合函数
- COUNT:统计记录数
sql复制-- 统计总用户数(包含NULL)
SELECT COUNT(*) FROM users;
-- 统计有邮箱的用户数(不统计NULL)
SELECT COUNT(email) FROM users;
- SUM/AVG:求和与平均值
sql复制-- 计算订单总金额和平均金额
SELECT
SUM(amount) AS total_sales,
AVG(amount) AS avg_order
FROM orders;
浮点数计算要注意精度问题,建议用DECIMAL类型。
- MAX/MIN:极值查询
sql复制-- 查找最贵和最便宜的商品
SELECT
MAX(price) AS max_price,
MIN(price) AS min_price
FROM products;
2.2 GROUP BY的实战技巧
按部门统计薪资:
sql复制SELECT
department,
COUNT(*) AS emp_count,
AVG(salary) AS avg_salary
FROM employees
GROUP BY department;
我经常看到开发者犯的GROUP BY错误:
- SELECT中的非聚合列必须出现在GROUP BY中
- WHERE条件在GROUP BY之前执行,HAVING在之后
- GROUP BY性能优化:尽量在分组字段上建立索引
2.3 WITH ROLLUP和小计功能
生成分层小计:
sql复制SELECT
YEAR(order_date) AS year,
MONTH(order_date) AS month,
SUM(amount) AS sales
FROM orders
GROUP BY YEAR(order_date), MONTH(order_date)
WITH ROLLUP;
结果会包含每月小计、每年总计和全局总计。
2.4 窗口函数:聚合的进阶用法
MySQL 8.0+支持强大的窗口函数:
sql复制-- 计算每个部门的薪资排名
SELECT
name,
department,
salary,
RANK() OVER (PARTITION BY department ORDER BY salary DESC) AS dept_rank
FROM employees;
这在制作报表时特别有用,避免了繁琐的子查询。
3. 联合查询:连接多个数据源
3.1 内连接(INNER JOIN)核心原理
获取用户及其订单信息:
sql复制SELECT u.username, o.order_date, o.amount
FROM users u
INNER JOIN orders o ON u.id = o.user_id;
执行过程相当于双重循环:
- 遍历users表的每条记录
- 对于每个user,在orders表中查找匹配的user_id
- 返回所有匹配的组合
性能优化建议:
- 连接字段要有索引
- 小表驱动大表(MySQL会自动优化)
- 避免连接超过3个表,复杂查询考虑拆分成多个步骤
3.2 外连接(LEFT/RIGHT JOIN)的陷阱
查找所有用户(包括没有订单的):
sql复制SELECT u.username, o.order_date
FROM users u
LEFT JOIN orders o ON u.id = o.user_id;
常见问题:
- WHERE条件放在ON子句会导致不同结果
- 右表匹配不到的记录会用NULL填充
- 多表外连接时逻辑容易混乱
3.3 自连接:处理层级数据
查找员工及其经理:
sql复制SELECT e.name AS employee, m.name AS manager
FROM employees e
LEFT JOIN employees m ON e.manager_id = m.id;
这种模式适合处理树形结构数据,如组织架构、评论回复等。
3.4 联合查询(UNION)的妙用
合并多个查询结果:
sql复制-- 合并活跃和非活跃用户
SELECT id, username, 'active' AS status FROM active_users
UNION ALL
SELECT id, username, 'inactive' FROM inactive_users;
UNION vs UNION ALL:
- UNION会去重并排序,性能较差
- UNION ALL直接合并,优先考虑使用
4. 实战中的复杂查询优化
4.1 多表连接的性能调优
分析执行计划:
sql复制EXPLAIN SELECT ...
重点关注:
- type列:最好达到ref或eq_ref
- possible_keys/key列:是否用到了合适的索引
- rows列:预估扫描行数
4.2 子查询 vs 连接查询
不好的写法:
sql复制SELECT name
FROM products
WHERE category_id IN (
SELECT id FROM categories WHERE type = 'electronics'
);
更好的写法:
sql复制SELECT p.name
FROM products p
JOIN categories c ON p.category_id = c.id
WHERE c.type = 'electronics';
MySQL对JOIN的优化通常优于子查询。
4.3 分页查询的优化方案
常规分页的性能问题:
sql复制SELECT * FROM large_table LIMIT 1000000, 10;
优化方案:
- 使用覆盖索引
- 记录上次查询的最大ID:
sql复制SELECT * FROM large_table
WHERE id > 1000000
ORDER BY id LIMIT 10;
4.4 临时表与Common Table Expressions
MySQL 8.0+的CTE语法:
sql复制WITH regional_sales AS (
SELECT region, SUM(amount) AS total_sales
FROM orders
GROUP BY region
)
SELECT region, total_sales
FROM regional_sales
WHERE total_sales > 100000;
这比子查询更清晰,也便于复用。
5. 真实案例:电商系统查询优化
最近优化过一个商品搜索查询,原始SQL:
sql复制SELECT p.*, c.name AS category_name
FROM products p
LEFT JOIN categories c ON p.category_id = c.id
WHERE p.price BETWEEN 100 AND 500
AND p.status = 1
ORDER BY p.sales DESC
LIMIT 0, 20;
优化步骤:
- 建立复合索引:(status, price, sales)
- 改为内连接(不需要NULL品类)
- 只查询必要字段
- 使用延迟关联处理大文本字段
最终优化效果:查询时间从1200ms降到80ms。
在MySQL查询优化的路上,我最大的体会是:理解执行过程比记住优化技巧更重要。每次遇到慢查询,先看EXPLAIN,再结合业务特点制定优化方案,这才是可持续的优化之道。
