
数据库这行干久了你会发现一个特别普遍的现象大家聊起“数据库”和“SQL”都能说上几句可真到线上出了事、面试追问题目翻车的往往是那些自以为很熟的基础概念。我今天就用一篇长文把数据库的核心原理和 SQL 分类从头捋一遍把“为什么”也一并讲透。这篇内容不挑基础刚入行的开发能当查漏补缺的提纲做了几年的老手也能在里面找到几个平时没细想的点。1. 先从底层把“数据库到底在干什么”讲清楚1.1 数据库不是Excel它解决了三个核心问题很多人第一次接触数据库时都会觉得这不就是个高级 Excel 吗存数据、改数据、查数据Excel 也能干。这么想不能说全错但忽略了数据库真正值钱的地方。数据库解决的核心问题有三个持久化、结构化查询和并发控制。持久化数据在程序重启、机器断电之后还能找回来。Excel 文件确实也能持久化但它的持久化是“整文件覆盖”数据库则是按记录级别的写入和恢复。结构化查询Excel 靠筛选、排序、VLOOKUP 来“查”数据库靠 SQL 这种声明式语言。你告诉它“我要什么”它自己决定“怎么拿”。并发控制这才是数据库和文件最本质的区别。多个用户同时读写同一份数据怎么保证不丢、不错、不互相覆盖Excel 的协作基本上靠“锁文件”数据库靠的是事务、锁、MVCC 这一整套机制。我举个实际场景一个订单系统用户下单要同时扣库存、写订单表、记流水账。假如没有数据库的事务能力扣库存成功了但订单没写进去这单生意就崩了。Excel 做不了这种一致性保证。1.2 关系模型的本质表、行、列、主键、外键关系模型是数据库最主流的数据组织方式它把世界抽象成一张张二维表。表是“关系”行是“元组”列是“属性”主键唯一标识一行外键表达表与表之间的关联。比如用户表和订单表用户表user_id主键、user_name、phone订单表order_id主键、user_id外键、order_amount、create_time通过 user_id 这个外键订单表就能关联到用户表查出“这个订单是谁下的”。这种拆分带来的最大好处是消除冗余——用户姓名只需要在用户表存一份订单表里只放 user_id而不是把整串姓名复制过来。理解关系模型的另一个关键点是“集合思维”。SQL 的查询结果本质上是集合或集合的运算结果JOIN 是笛卡尔积加筛选UNION 是集合并集。你带着这个视角写 SQL很多写法上的困惑会迎刃而解。1.3 事务与ACID为什么银行转账不能出错事务Transaction是数据库里一组不可分割的操作要么全部成功要么全部回滚。它之所以重要是因为它保证了 ACID 四个特性原子性Atomicity事务里的操作全做或全不做。转账时扣款和入账必须打包在一起。一致性Consistency事务执行前后数据都要满足业务规则。比如账户余额不能为负数。隔离性Isolation两个事务并发执行时不能互相干扰。一个事务未提交的数据另一个事务不应该看到脏数据。持久性Durability事务一旦提交结果就要永久保存即使系统崩溃也不能丢。我习惯用一个生活化类比 ACID 就像做饭。备菜、切菜、下锅是一整套动作原子性你不可能只做“下锅”而不做“备菜”做出来的菜必须能吃一致性你和隔壁灶台同时做饭彼此用的锅和铲不能混隔离性做完的菜得盛到盘子里端上桌不能做完就消失持久性。隔离性说起来简单做起来有取舍。完全隔离串行执行性能最差所以数据库提供了多个隔离级别这个我在第 5 节面试题里会展开讲。2. SQL分类面试和实操都绕不开的四种语言SQL 全称 Structured Query Language结构化查询语言。虽然名字叫“查询”但它的能力远不止查数据。按功能划分SQL 被分成四大类DQL、DML、DDL、DCL有人还会把 TCL 单独拎出来当成第五类。这几种分类是数据库面试的必问题也是日常写代码时判断“这条语句会带来什么影响”的基础框架。2.1 DQL数据查询语言日常开发的主力DQLData Query Language只有一个核心语句SELECT。但它恰恰是 SQL 里最值得钻研的部分。很多人写 SELECT 就是“SELECT * FROM 表 WHERE 条件”能用但遇到性能问题、去重统计、分组聚合就抓瞎。要真正掌握 SELECT先记住它的逻辑执行顺序这比背诵语法重要得多FROM确定数据来源先找出要查哪张表、做哪些表关联WHERE对来源数据进行行级过滤GROUP BY按列分组HAVING对分组后的结果做过滤SELECT计算要展示的列和表达式ORDER BY排序LIMIT/OFFSET分页截取执行顺序解释了面试里一个经典问题为什么 WHERE 子句里不能直接用 SELECT 里定义的别名因为 WHERE 在 SELECT 之前执行别名在那个阶段还不存在。而 ORDER BY 在 SELECT 之后执行所以它可以引用别名。比如这条语句SELECT department_id, COUNT(*) AS emp_count FROM employees WHERE salary 5000 GROUP BY department_id HAVING COUNT(*) 10 ORDER BY emp_count DESC;它的执行过程是先从员工表里筛出工资大于 5000 的行再按部门分组统计每组人数只保留人数大于等于 10 的部门最后按人数降序输出。如果你把 HAVING 里的 COUNT(*) 换成 emp_count不少数据库会报错因为 HAVING 也在 SELECT 之前执行。2.2 DML数据操纵语言增删改查的“删改”部分DMLData Manipulation Language对应的是增删改查里的增、删、改主要包括三条语句INSERT、UPDATE、DELETE。增删改查里的“查”是 DQL 的 SELECT所以实际日常说的“增删改查”其实是 DML 加 DQL 的合体。INSERT 的常见写法-- 按列顺序插入 INSERT INTO users (user_id, user_name, phone) VALUES (1, 张三, 13800000000); -- 一次插入多行 INSERT INTO users (user_id, user_name, phone) VALUES (2, 李四, 13900000000), (3, 王五, 13700000000); -- 从另一张表查出来再插入 INSERT INTO user_backup (user_id, user_name, phone) SELECT user_id, user_name, phone FROM users WHERE create_time 2024-01-01;UPDATE 和 DELETE 的使用有一条铁律除非你确实想动全表否则一定要带 WHERE 条件。-- 危险操作会把所有人的手机号都改了 UPDATE users SET phone 10000000000; -- 安全操作只改指定用户 UPDATE users SET phone 13812345678 WHERE user_id 1; -- 删除前先确认条件必要时用 SELECT 预览结果 SELECT * FROM users WHERE user_id 1; DELETE FROM users WHERE user_id 1;我见过不止一次生产事故就是因为同事在测试环境习惯了不带 WHERE 的 UPDATE连到生产库上执行结果全表被改。这种错误不是 SQL 语法问题而是习惯问题。我的经验是手写 UPDATE/DELETE 之前先把同样 WHERE 条件的 SELECT 执行一遍确认影响行数再动手。DELETE 和 TRUNCATE 的区别也是个高频考点DELETE 是 DML逐行删除可以带 WHERE会触发触发器删除后可以回滚TRUNCATE 是 DDL直接释放整张表的存储页速度极快不能回滚。业务上的误删恢复DELETE 还能靠事务救一救TRUNCATE 基本只能靠备份。2.3 DDL数据定义语言结构先行DDLData Definition Language负责定义和修改表结构核心语句是 CREATE、ALTER、DROP。-- 创建表 CREATE TABLE orders ( order_id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, order_amount DECIMAL(10,2) NOT NULL, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, INDEX idx_user (user_id) ); -- 修改表结构加列 ALTER TABLE orders ADD COLUMN pay_time DATETIME; -- 修改表结构加索引 ALTER TABLE orders ADD INDEX idx_create_time (create_time); -- 修改表结构修改列类型 ALTER TABLE orders MODIFY COLUMN order_amount DECIMAL(12,2); -- 删除表 DROP TABLE orders;DDL 一个容易忽视的点是很多 DDL 操作会导致锁表或重建表。比如早期 MySQL 版本中 ALTER TABLE 加索引大表会长时间不可写。生产环境的结构变更不要直接上手敲应该走变更平台或者用 gh-ost、pt-online-schema-change 这类在线工具降低对业务的影响。另外生产库里要约定好表结构的修改要通过脚本纳入版本管理和代码一起提交、评审、发布。否则你很难说清“哪个环境、哪个时刻、是哪次改动把索引删了”。2.4 DCL与TCL权限和事务的开关DCLData Control Language是权限控制语言主要语句是 GRANT 和 REVOKE。-- 授权 GRANT SELECT, INSERT, UPDATE ON mydb.users TO app_user%; -- 收回权限 REVOKE DELETE ON mydb.users FROM app_user%;权限管理最实用的一条经验是“最小权限原则”应用程序账号只给它需要的权限不要随手 GRANT ALL。我看到很多团队图省事给应用账号开了 DROP、DELETE 权限一旦代码被 SQL 注入攻击者能做的事就远不止读数据了。权限收敛到只读或者只增不改很多事故的爆炸半径都会小很多。TCLTransaction Control Language是事务控制语言核心是 COMMIT、ROLLBACK、SAVEPOINT。START TRANSACTION; UPDATE accounts SET balance balance - 100 WHERE account_id 1; UPDATE accounts SET balance balance 100 WHERE account_id 2; -- 检查无误后提交 COMMIT; -- 如果发现问题回滚 -- ROLLBACK;无论是 DCL 还是 TCL在实际工作中都容易被忽略但出问题时它们往往是最后一道防线。理解了这五类 SQL 的边界你在写代码时就会条件反射地意识到这条语句是 DML 会动数据那条是 DDL 会锁表那条是 TCL 需要控制好事务边界。3. SQL使用中的高频细节去重、空值、执行计划日常写 SQL除了弄清分类还有几个细节经常让人踩坑。我把这些年被问得最多、线上也最常见的三个细节单独拿出来讲。3.1 去重到底用 DISTINCT 还是 GROUP BY“SQL 语句去重”是个搜索量非常高的关键词可见大家对这个需求的困扰。去重最常见的两种写法是 DISTINCT 和 GROUP BY-- 查出去重后的部门ID SELECT DISTINCT department_id FROM employees; -- 效果一样 SELECT department_id FROM employees GROUP BY department_id;两种写法在简单去重场景下结果一致但语义有差别DISTINCT 是对整行做去重GROUP BY 是按列分组然后可以做聚合。比如你要统计每个部门的人数只能用 GROUP BYSELECT department_id, COUNT(*) FROM employees GROUP BY department_id;这里 DISTINCT 就没办法直接实现。反过来如果只是列出所有不重复的部门GROUP BY 能实现但会显得语义不明确。我的建议是能用 DISTINCT 表达“去重”意图时用 DISTINCT需要分组聚合时用 GROUP BY。还有一个很多人不知道的细节COUNT(DISTINCT column) 会统计该列的去重非 NULL 值数量而 DISTINCT 关键字后面跟多列时是对多列的组合去重-- 组合去重同一用户同一商品的购买记录算一条 SELECT DISTINCT user_id, product_id FROM order_items; -- 统计去重用户数NULL 不计入 SELECT COUNT(DISTINCT user_id) FROM orders;要注意的是DISTINCT 对大数据量的去重可能比较消耗内存因为它需要把结果集放入临时表排序去重。如果只是要判断某一列是否存在重复值用 GROUP BY HAVING COUNT(*) 1 会更直观。3.2 空值的坑NULL 与空字符串真不一样NULL 在 SQL 里表示“未知”它不是空字符串 也不等于 0。这个概念的混淆会让 SQL 查询结果出现莫名其妙的问题。NULL 参与任何比较运算、、结果都是“未知”WHERE 条件会把它过滤掉COUNT(column) 不会统计 NULL但 COUNT(*) 会统计所有行字符串拼接时任何值和 NULL 拼接结果都是 NULL排序时 NULL 的默认位置因数据库而异MySQL 默认 NULL 最小升序排最前实操里最容易踩的坑是查“用户备注为空”的记录写成WHERE remark 或WHERE remark IS NULL结果不一样。如果表里既有 NULL 又有空字符串两种情况都要覆盖SELECT * FROM users WHERE remark IS NULL OR remark ;聚合时也要注意 NULL-- 如果没有指定默认值AVG(score) 会忽略 NULL 行 SELECT AVG(score) FROM exam_results; -- 想给 NULL 特殊处理用 COALESCE SELECT AVG(COALESCE(score, 0)) FROM exam_results;这两种写法的业务含义完全不同前者是“只统计有成绩的学生的平均分”后者是“没成绩按 0 分算”。设计表结构时能用 NOT NULL DEFAULT 约束尽量约束住因为“空值三态逻辑”会给后续查询带来大量心智负担。3.3 慢SQL优化先看执行计划再动手“慢 SQL 优化”是数据库领域永恒的话题但很多人一上来就“加索引”顺序搞反了。我的优化思路是四步走定位慢查询开启慢查询日志找到真正慢的 SQL查看执行计划用 EXPLAIN 看它为什么慢分析瓶颈是全表扫描、排序临时表、还是索引失效对症下药加索引、改写 SQL、调整表结构EXPLAIN 是每个 DBA 和开发都应该熟练掌握的工具EXPLAIN SELECT * FROM orders WHERE user_id 100 AND create_time 2024-01-01;执行计划里重点看几个字段type访问类型、key实际用到的索引、rows预估扫描行数、Extra额外信息。如果 type 是 ALL说明是全表扫描如果 Extra 里有 Using filesort说明排序没走索引需要优化 ORDER BY 字段的索引。常见的索引失效场景包括对索引列使用了函数如WHERE DATE(create_time) 2024-01-01隐式类型转换如WHERE phone 138...而 phone 列为 VARCHARLIKE 以通配符开头如WHERE name LIKE %张OR 连接非索引列联合索引没遵循最左前缀原则优化时还有个反直觉点SELECT * 不一定是罪魁祸首但它会掩盖很多问题。如果查询只需要两列而表有 20 列SELECT * 会让覆盖索引失效多出大量回表操作。养成只查所需列的习惯慢查询会少很多。4. 数据库落地过程中的常见坑与排查实录综合多年实操经验我把数据库落地过程中几个高频问题整理成“排查实录”这些场景我都在生产环境真真切切遇到过。4.1 连接池被打满现象应用日志报 “Connection pool exhausted” 或 “Too many connections”服务大面积超时。排查步骤先看数据库连接数SHOW PROCESSLIST;看有没有大量 Sleep 状态的连接看应用的连接池配置连接池最大连接数、实际活跃数、等待队列长度检查代码里有没有连接泄漏特别是用了数据库连接但没在 finally 或 try-with-resource 里关闭常见原因就两类连接池配置过小或代码没正确归还连接。我曾经排查过一个案例服务每隔几小时就报连接池耗尽最后定位到是定时任务里的一个查询早退 return 了没关闭 Connection。修复经验连接池不是越大越好。每个连接都占数据库内存过大会拖垮数据库。推荐根据 QPS 和单连接耗时估算连接数 ≈ QPS × 平均耗时秒。给连接池配置连接最大存活时间防止数据库侧主动断开后应用还在用“死连接”。开启连接池的泄漏检测比如 HikariCP 的 leakDetectionThreshold。4.2 执行SQL脚本失败场景把一份几十 MB 的 SQL 脚本导入数据库中途报错中断已执行的语句没有回滚。这类问题要分两步看脚本里包含多条语句每条语句是独立事务一条失败不影响已提交的其他语句所以“整体执行失败”但“部分数据已变更”是很正常的。命令行导入要比图形工具更可靠。MySQL 用 mysql 命令PostgreSQL 用 psql 命令mysql -h127.0.0.1 -uroot -p mydb /data/backup.sql导入前最好先检查脚本里的编码、字符集避免中文乱码。脚本里如果包含 DELIMITER 改写的存储过程用图形工具执行也可能出错。我踩过的坑是从生产库导出的备份脚本在测试机导入时因为目标库里已存在同名表而失败后来习惯在脚本开头加DROP TABLE IF EXISTS或者干脆先备份再重建。4.3 导入导出乱码与丢数据用 Navicat、DataGrip 这类客户端工具导入导出最常见的坑是字符集不统一。导入前检查三处源文件编码、客户端连接编码、目标表字段编码。三处不一致就会出现中文乱码。-- MySQL 查看字符集 SHOW VARIABLES LIKE character_set%; -- 导入前指定客户端编码 SET NAMES utf8mb4;丢数据则往往和 Excel 导入相关。很多人喜欢用 Excel 整理数据再导入数据库Excel 里超过 15 位的数字如身份证号、订单号会变成科学计数法精度直接丢失。正确做法是在 Excel 里把这类列设置为文本格式或者先在数据库建好表再用工具导入时严格按字段类型映射。4.4 常见问题速查表问题典型原因快速排查方向连接池被打满连接泄漏或池太小SHOW PROCESSLIST 看连接状态慢查询缺索引、索引失效、SELECT *EXPLAIN 看扫描行数和 type中文乱码字符集不一致检查文件、连接、字段三处编码数据重复缺少唯一约束查表结构业务唯一键上建唯一索引删除后空间没变小表碎片重建表或使用 OPTIMIZE TABLE事务不回滚自动提交开启检查 autocommit 与代码事务边界这张表不是万能药但能让你在报警发生时快速定位方向。真正的问题排查还是要靠日常对慢日志、错误日志、监控指标的熟悉。5. 面试高频题把数据库和SQL串成一条线既然标题叫“核心概念深度解析”面试题这块绕不开。我梳理了三个最高频、也最能体现基础是否扎实的问题。5.1 索引为什么快B树与Hash的区别索引的本质是“用空间换时间”它让数据库查找数据不再靠全表扫描。主流关系型数据库用的索引结构是 B 树不是二叉树不是跳表。B 树的特点是所有数据都存在叶子节点非叶子节点只存键值叶子节点通过链表相连适合范围查询树的高度通常 2 到 4 层查找一次只需要几次磁盘 IOHash 索引也能快速等值匹配但不支持范围查询也不支持排序所以大部分场景下 B 树是关系型数据库的首选。我平时向新人解释 B 树就用查字典做类比你要查“数据库”这个词不会从第一页翻到最后一页而是先翻目录定位到“数”的大概位置再在那一小片区域里精确查找。索引就是给数据库准备的“目录”。注意索引不是建得越多越好。每个索引都占用额外空间写入时还要同步维护索引结构。一张表索引数量控制在个位数以内比较合理。5.2 事务隔离级别怎么选SQL 标准定义了四个隔离级别从低到高读未提交Read Uncommitted可能读到其他事务未提交的数据脏读读已提交Read Committed不会脏读但可能不可重复读可重复读Repeatable Read不会脏读、不可重复读但可能出现幻读串行化Serializable所有事务串行执行最安全但最慢MySQL 默认是 Repeatable ReadOracle 和 PostgreSQL 默认是 Read Committed。选隔离级别的原则是业务允许的情况下级别越低性能越好但一旦出现数据错乱性能再好也没用。实际经验是大多数业务用 Read Committed 就足够因为不可重复读的场景可以靠业务逻辑规避如果要做金融对账这种强一致性场景才考虑 Serializable 或引入分布式锁。5.3 一条SQL从客户端到存储引擎的完整旅程把一条 SQL 的执行过程讲清楚比背概念更能体现功底。以 MySQL 为例客户端发送 SQL 到连接器连接器负责认证和维持会话查询缓存MySQL 8.0 已移除该模块分析器做词法分析和语法分析把 SQL 拆成“我要从哪张表查哪些列”的语法树优化器决定执行方案走哪个索引、表连接顺序怎么安排、是否用临时表执行器调用存储引擎接口逐行读取并返回结果这条链路解释了“为什么 SQL 是声明式语言”——你只描述“要什么”优化器决定“怎么做”。所以同一个 SQL 写法不同性能可能天差地别就是因为优化器帮不了所有忙你的写法直接影响执行计划。面试官问这条题其实是在考察你有没有站在整体架构上看问题。能答出“优化器选错索引时可以用 FORCE INDEX”“慢查询时排查执行计划”这类实战延伸会明显加分。6. 写在最后的一些实操体会我自己的习惯是面对任何一套数据库系统先把 SQL 分类的边界画清楚再去看它的隔离级别和索引结构。分类清楚你就知道一条语句会引发什么后果原理清楚你就能解释为什么同一个查询换个写法性能完全不同。最后分享一条个人经验不要迷信任何客户端工具的“可视化优化”真正可靠的还是你亲手敲出的 EXPLAIN 和亲手梳理的索引设计。数据库和 SQL 这门基本功没有捷径但只要你把核心概念吃透了无论是面试还是线上排障都会比别人稳那么一点。