MySQL数据库面试高频问题及解析
以下是汇总一份全面且针对性强的数据库(以 MySQL 为主)问题清单与答案解析。
一、SQL 基础与查询
1. 什么是主键、外键、唯一索引?它们有什么区别?
概念 | 作用 | 是否允许NULL | 一张表数量 |
---|---|---|---|
主键 | 唯一标识一条记录,保证实体完整性 | 不允许 | 一个 |
外键 | 建立和加强两个表数据之间的链接,保证参照完整性 | 允许(除非被引用列是主键) | 多个 |
唯一索引 | 保证数据的唯一性,提高查询速度 | 允许(但只能有一个NULL) | 多个 |
核心答案:主键是唯一的标识符,不允许重复和NULL;外键是关联另一张表主键的字段;唯一索引是保证字段值唯一的索引。主键是一种特殊的唯一索引。
2. WHERE
和 HAVING
子句的区别是什么?
WHERE
:在分组前(GROUP BY) 过滤数据,它后面不能跟聚合函数。它作用于原始数据行。HAVING
:在分组后(GROUP BY) 过滤数据,它后面通常跟聚合函数(如COUNT
,SUM
)。它作用于分组后的结果集。
示例:找出平均分数大于80分的学生ID。
SELECT student_id, AVG(score) as avg_score
FROM scores
GROUP BY student_id
HAVING AVG(score) > 80; -- HAVING在分组后过滤,可以使用聚合函数
3. INNER JOIN
、LEFT JOIN
、RIGHT JOIN
和 FULL JOIN
的区别?
这是一个必考题,用韦恩图来理解最直观。
INNER JOIN
(内连接):只返回两个表中连接条件匹配的记录。交集。LEFT JOIN
(左连接):返回左表的全部记录,以及右表中连接条件匹配的记录。如果右表无匹配,则右表字段为NULL。RIGHT JOIN
(右连接):与左连接相反,返回右表的全部记录。FULL JOIN
(全连接):返回左右两表的全部记录。只要其中某个表存在匹配,就返回行。不匹配的部分用NULL填充。(MySQL不直接支持,但可用UNION
实现)
二、索引
1. 为什么索引能提高查询速度?
核心答案:索引就像一本书的目录。没有索引(全表扫描)就像一页一页地翻书找内容,速度极慢。而通过索引(目录),数据库可以快速定位到数据所在的数据页,极大地减少了需要扫描的数据量。
2. 哈希索引和B+树索引的区别?
特性 | 哈希索引 | B+树索引 |
---|---|---|
结构 | 哈希表 | 多路平衡搜索树 |
查询效率 | 等值查询极快 O(1) | 等值查询和范围查询都很快 O(log n) |
是否有序 | 无序 | 有序(叶子节点链表) |
支持排序 | 不支持 | 支持 |
适用场景 | 仅等值查询(如内存表) | 绝大多数数据库场景 |
结论:数据库(如MySQL的InnoDB)默认使用B+树索引,因为它完美支持了数据库最常用的等值查询和范围查询。
3. 什么情况下索引会失效?
这是一个非常实战化的问题。
在索引列上使用函数或计算:
WHERE YEAR(create_time) = 2023
(失效) vsWHERE create_time BETWEEN '2023-01-01' AND '2023-12-31'
(有效)。使用
!=
或<>
操作符。使用
OR
连接条件,如果OR的某一侧没有索引,可能导致全表扫描。对索引列使用
LIKE
并以通配符%
开头:WHERE name LIKE '%伟'
(失效)vsWHERE name LIKE '张%'
(有效,叫“前缀匹配”)。字符串索引未使用引号,会导致类型转换,索引失效。
复合索引未使用最左前缀。例如索引是
(a, b, c)
,查询条件只有b = 1
和c = 1
则无法使用该索引。
三、事务
1. 谈谈数据库事务的ACID特性。
这是事务的基石,必须烂熟于心。
A - 原子性:事务是一个不可分割的工作单位,事务中的操作要么全部成功,要么全部失败回滚。通过 Undo Log 实现。
C - 一致性:事务执行前后,数据库都必须从一个一致性状态转变到另一个一致性状态(不违反任何完整性约束)。这是事务的最终目的,由其他三大特性共同保证。
I - 隔离性:并发事务之间相互隔离,不应互相干扰。通过锁机制和 MVCC 实现。
D - 持久性:事务一旦提交,它对数据库的改变就是永久性的。通过 Redo Log 实现。
2. 事务的隔离级别有哪些?分别解决了哪些并发问题?
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
读未提交 | ❌ 可能发生 | ❌ 可能发生 | ❌ 可能发生 |
读已提交 | ✅ 解决 | ❌ 可能发生 | ❌ 可能发生 |
可重复读 | ✅ 解决 | ✅ 解决 | ❌ 可能发生 |
串行化 | ✅ 解决 | ✅ 解决 | ✅ 解决 |
脏读:读到了另一个未提交事务修改的数据。
不可重复读:在同一个事务中,两次读取同一条数据,结果不一样(被其他已提交事务修改或删除)。
幻读:在同一个事务中,两次执行相同的查询,返回的记录数不一样(被其他已提交事务插入了新数据)。
注意:MySQL的InnoDB引擎在可重复读级别下,通过 Next-Key Lock 机制在很大程度上避免了幻读。
四、锁
1. 乐观锁和悲观锁的区别?
悲观锁:悲观地认为数据在修改时一定会发生冲突。因此在操作数据之前,会先加锁(如行锁、表锁)。
SELECT ... FOR UPDATE
就是典型的悲观锁。数据库原生支持。乐观锁:乐观地认为数据在修改时不会冲突。因此不会加锁,而是在更新时去判断此期间数据是否被其他线程修改过。通常通过版本号或时间戳实现。需要应用层自己实现。
适用场景:
悲观锁:写操作多,冲突概率高的场景。
乐观锁:读操作多,冲突概率低的场景,能大大提高吞吐量。
2. 行级锁和表级锁的区别?
表级锁:开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低。(如MyISAM引擎)
行级锁:开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度最高。(如InnoDB引擎)
结论:InnoDB的行级锁是现代高并发应用的首选。
五、数据库设计
1. 数据库三大范式是什么?
第一范式:每个字段都是原子性的,不可再分。
第二范式:满足第一范式,且非主键字段必须完全依赖于整个主键,而不是部分主键。(主要针对复合主键)
第三范式:满足第二范式,且非主键字段之间不能有传递依赖,即所有非主键字段必须直接依赖于主键。
简单理解:范式是为了减少数据冗余,保持数据一致性。但并非范式越高越好,有时为了查询性能(减少JOIN),会故意设计反范式的表。
六、实战与场景题
1. 如何定位和优化一条慢SQL查询?
这是一个考察综合能力的问题。
定位:使用MySQL的慢查询日志 或
SHOW PROCESSLIST
命令找到执行慢的SQL。分析:使用
EXPLAIN
命令分析SQL的执行计划。重点关注:type:访问类型,从好到坏有
const
,eq_ref
,ref
,range
,index
,ALL
(全表扫描)。key:实际使用的索引。
rows:预估需要扫描的行数。
Extra:额外信息,如
Using filesort
(需要额外排序),Using temporary
(使用临时表)都是需要优化的信号。
优化:
为
WHERE
条件和ORDER BY
/GROUP BY
的列创建合适的索引。优化SQL语句本身(避免
SELECT *
,避免使用OR
等)。考虑重构表结构或业务逻辑。
2. DROP
、DELETE
和 TRUNCATE
的区别?
操作 | 类型 | 是否可回滚 | 是否触发触发器 | 速度 |
---|---|---|---|---|
DELETE | DML | 可回滚 | 会触发 | 慢(一行行删除) |
TRUNCATE | DDL | 不可回滚 | 不会触发 | 快(直接删除数据页) |
DROP | DDL | 不可回滚 | 不会触发 | 最快(删除整个表) |
下面让我们专注于 SQL 语句本身。这里为你梳理从基础到高级的各种常用 SQL 语句,并附上解释和示例。
一、数据定义语言(DDL)- 定义和管理结构
用于定义、修改和删除数据库、表、索引等对象的结构。
1. CREATE
- 创建
创建数据库:
CREATE DATABASE my_shop;
创建表:
CREATE TABLE users (id INT AUTO_INCREMENT PRIMARY KEY, -- 自增主键username VARCHAR(50) NOT NULL UNIQUE, -- 非空且唯一email VARCHAR(100),age INT CHECK (age >= 0), -- 检查约束created_at DATETIME DEFAULT CURRENT_TIMESTAMP -- 默认值
);
创建索引:
-- 在users表的email字段上创建索引,加快基于邮箱的查询
CREATE INDEX idx_email ON users(email);-- 创建联合索引
CREATE INDEX idx_name_age ON users(username, age);
2. ALTER
- 修改
添加列:
ALTER TABLE users ADD COLUMN phone_number VARCHAR(20);
修改列数据类型:
ALTER TABLE users MODIFY COLUMN email VARCHAR(150); -- 修改长度
删除列:
ALTER TABLE users DROP COLUMN age;
添加主键/外键:
-- 添加主键(如果建表时没加)
ALTER TABLE users ADD PRIMARY KEY (id);-- 添加外键 (orders表的user_id引用users表的id)
ALTER TABLE orders ADD CONSTRAINT fk_user
FOREIGN KEY (user_id) REFERENCES users(id);
3. DROP
- 删除
删除表:
DROP TABLE users; -- 谨慎使用!表和数据都会消失
删除数据库:
DROP DATABASE my_shop; -- 更加谨慎!
删除索引:
DROP INDEX idx_email ON users;
4. TRUNCATE
- 清空表
TRUNCATE TABLE logs; -- 删除表内所有数据,但保留表结构。不可回滚,速度快。
二、数据操作语言(DML)- 操作数据本身
用于对表中的数据进行增、删、改操作。
1. INSERT
- 插入数据
插入单行:
INSERT INTO users (username, email, age)
VALUES ('john_doe', 'john@example.com', 25);
插入多行:
INSERT INTO users (username, email, age) VALUES
('alice', 'alice@example.com', 30),
('bob', 'bob@example.com', 28);
从另一张表插入:
INSERT INTO user_archive (username, email)
SELECT username, email FROM users WHERE created_at < '2020-01-01';
2. UPDATE
- 更新数据
更新特定行:
UPDATE users
SET email = 'new_email@example.com', age = 26
WHERE username = 'john_doe'; -- WHERE子句至关重要!否则会更新所有行。
基于子查询更新:
UPDATE products
SET price = price * 0.9 -- 打九折
WHERE category_id IN (SELECT id FROM categories WHERE name = 'Clearance');
3. DELETE
- 删除数据
删除特定行:
DELETE FROM users
WHERE username = 'inactive_user'; -- WHERE子句至关重要!否则会清空整个表。
清空表(与TRUNCATE对比):
DELETE FROM users; -- 逐行删除,可回滚,速度慢,会触发触发器。
三、数据查询语言(DQL)- 检索数据
核心是 SELECT
语句,用于从表中查询数据。
1. 基础查询
查询所有列:
SELECT * FROM users;
查询特定列:
SELECT username, email FROM users;
使用别名:
SELECT username AS ‘用户名‘,email AS ‘邮箱‘
FROM users;
2. WHERE
- 条件过滤
基本运算符:
SELECT * FROM products WHERE price > 100;
SELECT * FROM users WHERE age BETWEEN 18 AND 65;
SELECT * FROM users WHERE username LIKE 'A%'; -- 以A开头
SELECT * FROM users WHERE email IS NOT NULL;
多条件组合:
SELECT * FROM users
WHERE age > 18 AND (country = 'USA' OR country = 'Canada')AND username NOT LIKE '%admin%';
3. ORDER BY
- 排序
SELECT * FROM products
ORDER BY price DESC; -- 按价格降序SELECT * FROM users
ORDER BY created_at ASC, username DESC; -- 先按创建时间升序,同时间再按用户名降序
4. GROUP BY
与聚合函数
常用聚合函数: COUNT()
, SUM()
, AVG()
, MAX()
, MIN()
-- 统计每个国家的用户数量
SELECT country, COUNT(*) AS user_count
FROM users
GROUP BY country;-- 找出每个分类的平均价格和最高价格
SELECT category_id, AVG(price) AS avg_price,MAX(price) AS max_price
FROM products
GROUP BY category_id;-- HAVING子句用于过滤分组后的结果
SELECT category_id, AVG(price) AS avg_price
FROM products
GROUP BY category_id
HAVING AVG(price) > 50; -- 筛选出平均价格大于50的分类
5. JOIN
- 连接表
内连接:
-- 获取所有订单及其对应的用户信息(只返回有用户的订单)
SELECT o.order_id, o.amount, u.username
FROM orders o
INNER JOIN users u ON o.user_id = u.id;
左外连接:
-- 获取所有用户及其订单(即使用户没有订单也会显示)
SELECT u.username, o.order_id
FROM users u
LEFT JOIN orders o ON u.id = o.user_id;
自连接:
-- 在员工表中查找每个员工及其经理的名字
SELECT e.name AS employee_name, m.name AS manager_name
FROM employees e
LEFT JOIN employees m ON e.manager_id = m.id;
6. 子查询
在WHERE中:
-- 找出价格高于平均价格的产品
SELECT * FROM products
WHERE price > (SELECT AVG(price) FROM products);
在FROM中(派生表):
-- 找出每个分类中价格最高的产品
SELECT p.category_id, p.name, p.price
FROM products p
INNER JOIN (SELECT category_id, MAX(price) as max_priceFROM productsGROUP BY category_id
) AS max_prices ON p.category_id = max_prices.category_id AND p.price = max_prices.max_price;
四、其他关键语句和子句
1. LIMIT
/ OFFSET
- 分页
-- 获取前10条最贵的商品
SELECT * FROM products ORDER BY price DESC LIMIT 10;-- 分页:获取第6到第10条记录(跳过前5条)
SELECT * FROM products ORDER BY id LIMIT 5 OFFSET 5;
-- 或者简写为: LIMIT 5, 5
2. UNION
- 合并结果集
-- 合并来自两个表的用户名,并去重
SELECT username FROM active_users
UNION
SELECT username FROM inactive_users;-- UNION ALL 不去重,性能更高
SELECT city FROM suppliers
UNION ALL
SELECT city FROM customers;
3. CASE
- 条件逻辑
-- 根据价格区间给产品打标签
SELECT name, price,CASE WHEN price < 50 THEN 'Cheap'WHEN price BETWEEN 50 AND 200 THEN 'Moderate'ELSE 'Expensive'END AS price_category
FROM products;
4. DISTINCT
- 去重
-- 找出所有不同的国家
SELECT DISTINCT country FROM users;-- 统计有多少个不同的国家
SELECT COUNT(DISTINCT country) FROM users;