1. 数据库约束:数据完整性的守护者
在数据库设计中,约束(Constraint)是确保数据完整性和一致性的关键机制。作为一名长期与MySQL打交道的开发者,我深刻体会到合理使用约束能避免80%以上的低级数据错误。让我们从实战角度剖析六种核心约束类型。
1.1 主键约束:数据表的身份证系统
主键(PRIMARY KEY)相当于数据行的身份证号,必须同时满足唯一性和非空性。在电商系统中,我习惯采用自增主键设计:
CREATE TABLE products ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL, price DECIMAL(10,2) UNSIGNED );关键经验:使用UNSIGNED防止负数价格,AUTO_INCREMENT让数据库自动分配ID。实际项目中,主键最好使用与业务无关的代理键(surrogate key),避免业务规则变更影响数据关系。
1.2 外键约束:表关系的粘合剂
外键(FOREIGN KEY)维护表间的引用完整性。在用户订单系统中,没有外键保护可能导致"幽灵订单":
CREATE TABLE orders ( order_id INT PRIMARY KEY, user_id INT, FOREIGN KEY (user_id) REFERENCES users(user_id) ON DELETE CASCADE ON UPDATE RESTRICT );这里设置了级联删除(DELETE CASCADE),但更新限制(UPDATE RESTRICT)。根据业务需求,还有SET NULL、NO ACTION等选项。我在金融系统中会禁用级联操作,改为应用层控制,避免误删关键数据。
1.3 唯一约束:防重复的精密滤网
唯一约束(UNIQUE)允许NULL值但禁止重复值,适合邮箱、手机号等字段:
ALTER TABLE employees ADD CONSTRAINT uk_email UNIQUE (email);注意与主键的区别:一个表可以有多个UNIQUE约束,但只能有一个PRIMARY KEY。在用户表中,我通常同时设置用户名UNIQUE和邮箱UNIQUE。
1.4 检查约束:数据质量的守门员
MySQL 8.0+终于原生支持CHECK约束,可验证数据范围:
CREATE TABLE students ( id INT PRIMARY KEY, age TINYINT CHECK (age BETWEEN 15 AND 30), gender ENUM('M','F') NOT NULL );早期版本可通过触发器实现类似功能。我在物流系统用CHECK确保重量>0,避免后续计算出错。
1.5 非空与默认约束:字段的默认行为
NOT NULL与DEFAULT常配合使用:
CREATE TABLE articles ( id INT PRIMARY KEY, title VARCHAR(200) NOT NULL, status ENUM('draft','published') DEFAULT 'draft', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP );踩坑提醒:TIMESTAMP类型在MySQL 5.6+支持自动更新,但DATETIME需要显式设置默认值。我曾因此浪费两小时排查为什么创建时间没自动记录。
2. 聚合查询:数据统计的瑞士军刀
2.1 聚合函数深度解析
COUNT()、SUM()等聚合函数是数据分析的基石。几个易错点:
COUNT(*)计算所有行,COUNT(column)忽略NULL- SUM/AVG对NULL自动过滤,与Excel行为不同
- 聚合函数可嵌套:
SELECT MAX(AVG(salary))需配合GROUP BY使用
统计不同状态订单数量的正确姿势:
SELECT status, COUNT(*) AS total, SUM(amount) AS revenue, AVG(amount) AS avg_order FROM orders GROUP BY status;2.2 GROUP BY的玄机
GROUP BY的黄金法则:SELECT中的非聚合字段必须出现在GROUP BY中。这个错误我见过无数次:
-- 错误示例(user_name不在GROUP BY中) SELECT user_id, user_name, SUM(amount) FROM orders GROUP BY user_id; -- 正确写法 SELECT user_id, SUM(amount) FROM orders GROUP BY user_id; -- 或包含所有非聚合字段 SELECT user_id, user_name, SUM(amount) FROM orders GROUP BY user_id, user_name;2.3 HAVING与WHERE的区别
WHERE在分组前过滤原始数据,HAVING在分组后过滤结果。统计高消费用户时:
SELECT user_id, SUM(amount) AS total_spent FROM orders WHERE status = 'completed' -- 先筛选已完成订单 GROUP BY user_id HAVING total_spent > 1000; -- 再筛选消费大户性能提示:WHERE能利用索引,HAVING不行。大数据量时先用WHERE减少处理量。
3. 联合查询:多表协作的艺术
3.1 连接类型选择指南
- 内连接(INNER JOIN):只返回匹配行(交集)
- 左连接(LEFT JOIN):保留左表所有记录
- 右连接(RIGHT JOIN):保留右表所有记录
- 全连接(FULL JOIN):MySQL需用UNION模拟
查询用户及其订单的经典模式:
SELECT u.user_id, u.name, o.order_id, o.amount FROM users u LEFT JOIN orders o ON u.user_id = o.user_id;3.2 连接性能优化
多表连接时注意:
- 小表驱动大表(小表放在JOIN左侧)
- 确保连接字段有索引
- 避免SELECT *,只取必要字段
我曾优化过一个5表连接查询,通过添加复合索引将执行时间从12秒降到0.2秒。
3.3 自连接的特殊场景
查询同一表中的关联数据,如员工与其经理:
SELECT e.name AS employee, m.name AS manager FROM employees e LEFT JOIN employees m ON e.manager_id = m.emp_id;4. 子查询与合并查询实战技巧
4.1 子查询的四种形态
- WHERE子查询:
WHERE col > (SELECT...) - FROM子查询:
FROM (SELECT...) AS temp - SELECT子查询:
SELECT (SELECT...) AS col - EXISTS子查询:
WHERE EXISTS (SELECT...)
查找没有订单的用户:
SELECT user_id, name FROM users WHERE NOT EXISTS ( SELECT 1 FROM orders WHERE orders.user_id = users.user_id );4.2 UNION的妙用
合并查询结果时:
- UNION:去重排序(有性能开销)
- UNION ALL:简单合并(推荐优先使用)
合并不同年份的销售数据:
SELECT '2023' AS year, SUM(amount) FROM sales_2023 UNION ALL SELECT '2022' AS year, SUM(amount) FROM sales_2022;重要限制:各查询的列数、类型必须兼容。我曾因一个TINYINT和SMALLINT不匹配调试半天。
5. 避坑指南与性能优化
5.1 索引使用原则
- 为所有JOIN、WHERE、GROUP BY字段建索引
- 多列索引遵循最左前缀原则
- 避免在索引列上使用函数:
WHERE YEAR(create_time)=2023会使索引失效
5.2 执行计划解读
使用EXPLAIN分析查询:
EXPLAIN SELECT * FROM users WHERE id = 1;重点关注type列(最好到ref)、rows列(扫描行数)和Extra列(是否使用临时表)。
5.3 分页查询优化
低效写法:
SELECT * FROM large_table LIMIT 1000000, 10;高效方案:
SELECT * FROM large_table WHERE id > 1000000 ORDER BY id LIMIT 10;在大数据量分页时,这种"记住上次位置"的方法可将查询时间从秒级降到毫秒级。