1. 数据库约束:数据完整性的守护者
在MySQL中,约束是确保数据完整性的关键机制。作为从业15年的DBA,我见过太多因为约束缺失导致的数据灾难。让我们从实际案例出发,深入理解这些约束的应用场景。
1.1 主键约束的实战陷阱
主键(PRIMARY KEY)看似简单,但隐藏着不少坑。最近处理的一个生产事故就源于对主键的误解:
sql复制CREATE TABLE user_orders (
order_id VARCHAR(32) PRIMARY KEY,
user_id INT,
-- 其他字段...
);
问题出在VARCHAR类型的主键上。当数据量达到千万级时,这种设计导致索引体积膨胀30%,查询性能下降明显。我的经验法则是:
- 自增INT/BIGINT永远是主键首选
- 必须使用业务主键时,考虑用CHAR定长类型
- 复合主键不超过3个字段
注意:在MySQL 8.0中,隐藏式自增主键(_rowid)最大只支持6字节,当主键超过该长度会导致性能下降。
1.2 外键约束的取舍智慧
外键(FOREIGN KEY)是把双刃剑。去年双十一大促前,我们移除了所有外键约束,QPS提升了40%。但这需要应用层严格保证数据一致性。何时使用外键?我的判断标准:
- 金融核心系统必须用
- 高并发秒杀系统避免用
- 开发环境建议用,生产环境谨慎用
外键的级联操作在实际中最有用的是ON DELETE CASCADE,但要注意死锁风险:
sql复制CREATE TABLE orders (
id INT PRIMARY KEY,
user_id INT,
FOREIGN KEY (user_id) REFERENCES users(id)
ON DELETE CASCADE
ON UPDATE RESTRICT
);
1.3 唯一约束的并发问题
唯一约束(UNIQUE)在高并发下可能成为性能瓶颈。去年我们遇到过一个典型案例:
sql复制ALTER TABLE products ADD UNIQUE (product_code);
当每秒200+的INSERT并发时,出现了大量死锁。解决方案是:
- 先查后插的防重逻辑移到应用层
- 使用INSERT IGNORE或ON DUPLICATE KEY UPDATE
- 必要时添加随机后缀破坏唯一性
1.4 检查约束的MySQL特色
MySQL 8.0终于支持标准的CHECK约束了,但有个坑要注意:
sql复制CREATE TABLE employees (
salary DECIMAL(10,2),
CHECK (salary > 0)
) ENGINE=InnoDB;
在5.7及以下版本,这个约束会被静默忽略!替代方案是使用触发器:
sql复制DELIMITER //
CREATE TRIGGER check_salary
BEFORE INSERT ON employees
FOR EACH ROW
BEGIN
IF NEW.salary <= 0 THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'Salary must be positive';
END IF;
END//
DELIMITER ;
1.5 默认值与NOT NULL的最佳实践
新手常犯的错误是过度使用NULL。我的团队规范要求:
- 所有字段必须显式定义NOT NULL
- 没有业务意义的默认值用''或0代替NULL
- 布尔字段永远用TINYINT(1) DEFAULT 0
sql复制CREATE TABLE articles (
title VARCHAR(100) NOT NULL DEFAULT '',
views INT NOT NULL DEFAULT 0,
is_published TINYINT(1) NOT NULL DEFAULT 0
);
这种设计能避免NULL带来的索引问题和业务逻辑复杂性。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 聚合查询:数据分析的利器
2.1 GROUP BY的优化之道
GROUP BY是性能问题的重灾区。上周刚优化了一个慢查询:
sql复制-- 优化前(执行时间4.8s)
SELECT department, COUNT(*)
FROM employees
GROUP BY department;
-- 优化后(0.3s)
SELECT d.name, COUNT(e.id)
FROM departments d
LEFT JOIN employees e ON e.dept_id = d.id
GROUP BY d.id;
关键优化点:
- 改用主键分组
- 减少分组字段数量
- 利用覆盖索引
2.2 聚合函数的隐藏特性
AVG()有个容易被忽略的行为:
sql复制SELECT AVG(salary) FROM employees; -- 包含NULL值
SELECT SUM(salary)/COUNT(*) FROM employees; -- 等价写法
SELECT SUM(salary)/COUNT(salary) FROM employees; -- 排除NULL
COUNT的三种写法性能差异明显:
- COUNT(*)最快
- COUNT(1)次之
- COUNT(column)最慢
2.3 HAVING的巧妙用法
HAVING不只是WHERE的替代品。这个统计活跃用户的写法很实用:
sql复制SELECT user_id, COUNT(*) as action_count
FROM user_actions
WHERE action_time > DATE_SUB(NOW(), INTERVAL 7 DAY)
GROUP BY user_id
HAVING action_count > 5;
2.4 WITH ROLLUP的多维统计
这个功能在报表场景非常强大:
sql复制SELECT
YEAR(order_date) as year,
QUARTER(order_date) as quarter,
SUM(amount) as total
FROM orders
GROUP BY year, quarter WITH ROLLUP;
输出结果会自动包含每年的小计和所有年份的总计。
3. 联合查询:数据关联的艺术
3.1 JOIN算法选择策略
MySQL的JOIN实现有3种算法:
- Nested Loop Join - 默认选择
- Hash Join - MySQL 8.0新增
- BKA (Batched Key Access) - 需要特别启用
通过EXPLAIN可以查看实际使用的算法:
sql复制EXPLAIN FORMAT=JSON
SELECT * FROM orders JOIN customers ON orders.cust_id = customers.id;
在JSON输出的"join_algorithm"字段可以看到具体实现。
3.2 索引对JOIN的影响
没有正确索引时,JOIN可能成为性能杀手。这个案例很典型:
sql复制-- 慢查询(3.2s)
SELECT *
FROM orders o
JOIN order_items i ON o.id = i.order_id;
-- 添加索引后(0.1s)
ALTER TABLE order_items ADD INDEX (order_id);
3.3 子查询优化技巧
MySQL对子查询的处理一直较弱。这个IN子查询:
sql复制SELECT * FROM products
WHERE category_id IN (
SELECT id FROM categories WHERE type = 'electronics'
);
可以改写成JOIN提升性能:
sql复制SELECT p.*
FROM products p
JOIN categories c ON p.category_id = c.id
WHERE c.type = 'electronics';
3.4 UNION的实用场景
UNION在分表查询时特别有用:
sql复制-- 查询2023年所有订单(按月分表)
SELECT * FROM orders_202301
UNION ALL
SELECT * FROM orders_202302
-- ...其他月份
WHERE status = 'completed';
注意UNION ALL比UNION快,因为它不去重。
4. 实战中的高级技巧
4.1 窗口函数的妙用
MySQL 8.0的窗口函数能解决很多复杂查询:
sql复制-- 计算各部门薪资排名
SELECT
name,
department,
salary,
RANK() OVER (PARTITION BY department ORDER BY salary DESC) as dept_rank
FROM employees;
4.2 公用表表达式(CTE)
CTE让复杂查询更清晰:
sql复制WITH regional_sales AS (
SELECT region, SUM(amount) as total
FROM orders
GROUP BY region
),
top_regions AS (
SELECT region
FROM regional_sales
WHERE total > 1000000
)
SELECT * FROM top_regions;
4.3 JSON处理新特性
MySQL 5.7+的JSON功能很实用:
sql复制-- 提取JSON字段
SELECT
id,
JSON_EXTRACT(profile, '$.address.city') as city
FROM users;
-- 更新JSON字段
UPDATE users
SET profile = JSON_SET(profile, '$.age', 30)
WHERE id = 1001;
4.4 执行计划分析实战
学会阅读EXPLAIN是DBA的基本功:
sql复制EXPLAIN
SELECT * FROM orders
WHERE user_id = 100
AND create_time > '2023-01-01';
关键看:
- type列:最好到ref/eq_ref
- key列:是否使用了正确索引
- rows列:预估扫描行数
- Extra列:是否出现Using filesort等警告
我在实际工作中发现,80%的性能问题都能通过正确索引解决。但索引不是越多越好,每个新增索引都需要评估写入成本。
