MySQL数据库面试高频问题及解析

以下是汇总一份全面且针对性强的数据库(以 MySQL 为主)问题清单与答案解析。


一、SQL 基础与查询

1. 什么是主键、外键、唯一索引?它们有什么区别?

概念 作用 是否允许NULL 一张表数量
主键 唯一标识一条记录,保证实体完整性 不允许 一个
外键 建立和加强两个表数据之间的链接,保证参照完整性 允许(除非被引用列是主键) 多个
唯一索引 保证数据的唯一性,提高查询速度 允许(但只能有一个NULL) 多个

核心答案:主键是唯一的标识符,不允许重复和NULL;外键是关联另一张表主键的字段;唯一索引是保证字段值唯一的索引。主键是一种特殊的唯一索引。

2. WHEREHAVING 子句的区别是什么?

  • WHERE :在分组前(GROUP BY) 过滤数据,它后面不能跟聚合函数。它作用于原始数据行

  • HAVING :在分组后(GROUP BY) 过滤数据,它后面通常跟聚合函数(如 COUNT, SUM)。它作用于分组后的结果集

示例:找出平均分数大于80分的学生ID。

sql 复制代码
SELECT student_id, AVG(score) as avg_score
FROM scores
GROUP BY student_id
HAVING AVG(score) > 80; -- HAVING在分组后过滤,可以使用聚合函数

3. INNER JOINLEFT JOINRIGHT JOINFULL 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. 什么情况下索引会失效?

这是一个非常实战化的问题。

  1. 在索引列上使用函数或计算WHERE YEAR(create_time) = 2023 (失效) vs WHERE create_time BETWEEN '2023-01-01' AND '2023-12-31' (有效)。

  2. 使用 !=<> 操作符

  3. 使用 OR 连接条件,如果OR的某一侧没有索引,可能导致全表扫描。

  4. 对索引列使用 LIKE 并以通配符 % 开头WHERE name LIKE '%伟' (失效)vs WHERE name LIKE '张%' (有效,叫"前缀匹配")。

  5. 字符串索引未使用引号,会导致类型转换,索引失效。

  6. 复合索引未使用最左前缀 。例如索引是 (a, b, c),查询条件只有 b = 1c = 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查询?

这是一个考察综合能力的问题。

  1. 定位 :使用MySQL的慢查询日志SHOW PROCESSLIST 命令找到执行慢的SQL。

  2. 分析 :使用 EXPLAIN 命令分析SQL的执行计划。重点关注:

    • type :访问类型,从好到坏有 const, eq_ref, ref, range, index, ALL(全表扫描)。

    • key:实际使用的索引。

    • rows:预估需要扫描的行数。

    • Extra :额外信息,如 Using filesort(需要额外排序),Using temporary(使用临时表)都是需要优化的信号。

  3. 优化

    • WHERE 条件和 ORDER BY / GROUP BY 的列创建合适的索引。

    • 优化SQL语句本身(避免 SELECT *,避免使用 OR 等)。

    • 考虑重构表结构或业务逻辑。

2. DROPDELETETRUNCATE 的区别?

操作 类型 是否可回滚 是否触发触发器 速度
DELETE DML 可回滚 会触发 慢(一行行删除)
TRUNCATE DDL 不可回滚 不会触发 快(直接删除数据页)
DROP DDL 不可回滚 不会触发 最快(删除整个表)

下面让我们专注于 SQL 语句本身。这里为你梳理从基础到高级的各种常用 SQL 语句,并附上解释和示例。

一、数据定义语言(DDL)- 定义和管理结构

用于定义、修改和删除数据库、表、索引等对象的结构。

1. CREATE - 创建

创建数据库:

sql 复制代码
CREATE DATABASE my_shop;

创建表:

sql 复制代码
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 -- 默认值
);

创建索引:

sql 复制代码
-- 在users表的email字段上创建索引,加快基于邮箱的查询
CREATE INDEX idx_email ON users(email);

-- 创建联合索引
CREATE INDEX idx_name_age ON users(username, age);
2. ALTER - 修改

添加列:

sql 复制代码
ALTER TABLE users ADD COLUMN phone_number VARCHAR(20);

修改列数据类型:

sql 复制代码
ALTER TABLE users MODIFY COLUMN email VARCHAR(150); -- 修改长度

删除列:

sql 复制代码
ALTER TABLE users DROP COLUMN age;

添加主键/外键:

sql 复制代码
-- 添加主键(如果建表时没加)
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 - 删除

删除表:

sql 复制代码
DROP TABLE users; -- 谨慎使用!表和数据都会消失

删除数据库:

sql 复制代码
DROP DATABASE my_shop; -- 更加谨慎!

删除索引:

sql 复制代码
DROP INDEX idx_email ON users;
4. TRUNCATE - 清空表
sql 复制代码
TRUNCATE TABLE logs; -- 删除表内所有数据,但保留表结构。不可回滚,速度快。

二、数据操作语言(DML)- 操作数据本身

用于对表中的数据进行增、删、改操作。

1. INSERT - 插入数据

插入单行:

sql 复制代码
INSERT INTO users (username, email, age)
VALUES ('john_doe', 'john@example.com', 25);

插入多行:

sql 复制代码
INSERT INTO users (username, email, age) VALUES
('alice', 'alice@example.com', 30),
('bob', 'bob@example.com', 28);

从另一张表插入:

sql 复制代码
INSERT INTO user_archive (username, email)
SELECT username, email FROM users WHERE created_at < '2020-01-01';
2. UPDATE - 更新数据

更新特定行:

sql 复制代码
UPDATE users
SET email = 'new_email@example.com', age = 26
WHERE username = 'john_doe'; -- WHERE子句至关重要!否则会更新所有行。

基于子查询更新:

sql 复制代码
UPDATE products
SET price = price * 0.9 -- 打九折
WHERE category_id IN (SELECT id FROM categories WHERE name = 'Clearance');
3. DELETE - 删除数据

删除特定行:

sql 复制代码
DELETE FROM users
WHERE username = 'inactive_user'; -- WHERE子句至关重要!否则会清空整个表。

清空表(与TRUNCATE对比):

sql 复制代码
DELETE FROM users; -- 逐行删除,可回滚,速度慢,会触发触发器。

三、数据查询语言(DQL)- 检索数据

核心是 SELECT 语句,用于从表中查询数据。

1. 基础查询

查询所有列:

sql 复制代码
SELECT * FROM users;

查询特定列:

sql 复制代码
SELECT username, email FROM users;

使用别名:

sql 复制代码
SELECT 
    username AS '用户名',
    email AS '邮箱'
FROM users;
2. WHERE - 条件过滤

基本运算符:

sql 复制代码
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;

多条件组合:

sql 复制代码
SELECT * FROM users 
WHERE age > 18 
   AND (country = 'USA' OR country = 'Canada')
   AND username NOT LIKE '%admin%';
3. ORDER BY - 排序
sql 复制代码
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()

sql 复制代码
-- 统计每个国家的用户数量
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 - 连接表

内连接:

sql 复制代码
-- 获取所有订单及其对应的用户信息(只返回有用户的订单)
SELECT o.order_id, o.amount, u.username
FROM orders o
INNER JOIN users u ON o.user_id = u.id;

左外连接:

sql 复制代码
-- 获取所有用户及其订单(即使用户没有订单也会显示)
SELECT u.username, o.order_id
FROM users u
LEFT JOIN orders o ON u.id = o.user_id;

自连接:

sql 复制代码
-- 在员工表中查找每个员工及其经理的名字
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中:

sql 复制代码
-- 找出价格高于平均价格的产品
SELECT * FROM products
WHERE price > (SELECT AVG(price) FROM products);

在FROM中(派生表):

sql 复制代码
-- 找出每个分类中价格最高的产品
SELECT p.category_id, p.name, p.price
FROM products p
INNER JOIN (
    SELECT category_id, MAX(price) as max_price
    FROM products
    GROUP BY category_id
) AS max_prices ON p.category_id = max_prices.category_id 
                AND p.price = max_prices.max_price;

四、其他关键语句和子句

1. LIMIT / OFFSET - 分页
sql 复制代码
-- 获取前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 - 合并结果集
sql 复制代码
-- 合并来自两个表的用户名,并去重
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 - 条件逻辑
sql 复制代码
-- 根据价格区间给产品打标签
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 - 去重
sql 复制代码
-- 找出所有不同的国家
SELECT DISTINCT country FROM users;

-- 统计有多少个不同的国家
SELECT COUNT(DISTINCT country) FROM users;
相关推荐
DemonAvenger3 小时前
Redis Geo 深度解析:从原理到实战,带你玩转地理位置计算
数据库·redis·性能优化
TDengine (老段)3 小时前
TDengine 数学函数 CEIL 用户手册
java·大数据·数据库·物联网·时序数据库·tdengine·涛思数据
-雷阵雨-3 小时前
MySQL——数据类型
数据库·mysql
LB21123 小时前
Redis 黑马skyout
java·数据库·redis
TDengine (老段)3 小时前
TDengine 浮点数新编码 BSS 用户手册
大数据·数据库·物联网·时序数据库·iot·tdengine·涛思数据
TDengine (老段)4 小时前
TDengine 数学函数 ASIN() 用户手册
大数据·数据库·sql·物联网·时序数据库·tdengine·涛思数据
聪明的笨猪猪9 小时前
Java Redis “持久化”面试清单(含超通俗生活案例与深度理解)
java·经验分享·笔记·面试
聪明的笨猪猪9 小时前
Java Redis “核心基础”面试清单(含超通俗生活案例与深度理解)
java·经验分享·笔记·面试
DIY全栈开发11 小时前
《MCU职位》面试问题
单片机·嵌入式硬件·面试