如果你点开这个标题大概率是最近在学后端技术栈或者接了个需要写 SQL 的活儿正想系统地把 MySQL 过一遍。我在工作里和 MySQL 打了快十年交道今天不打算给你贴官方文档而是用做项目的方式把基础阶段真正要掌握的东西捋一遍。MySQL 是当前最流行的开源关系型数据库之一几乎所有互联网公司都在用它存业务数据从用户信息、订单列表到日志统计背后都少不了它。这篇文章适合三类人刚入门编程、想补数据库能力的同学写过一点 SQL 但没系统理解表结构和事务的开发者以及准备技术面试、想把基础概念讲清楚的人。我会从核心概念讲起带大家完成环境搭建、建表、写 SQL、加索引、开事务最后分享一些我在实际项目里踩过的坑。1. 动手之前先明白 MySQL 到底在解决什么问题1.1 为什么后端项目里几乎处处都有 MySQL如果把数据看成一个公司的资产数据库就是存放资产的仓库MySQL 是其中一个做得不错、用的人也最多的仓库管理员。它不只负责把东西“放进去”还负责分类、登记、快速取出、防止丢失以及当两个人同时来存取物品时不至于手忙脚乱出错。MySQL 是开源的关系型数据库管理系统它将数据组织成一个个“表”表里的每一行是一条记录每一列是一个字段通过主键、外键和关联查询可以表达数据之间的复杂关系。它在真实项目里最常出现的场景有两类一类是线上业务系统比如用户注册、下单、支付流水另一类是后台管理系统比如统计报表、权限管理、内容管理。这两类场景几乎占到了普通项目的九成以上。那为什么偏偏是 MySQL我观察到的原因主要有四个第一开源免费社区活跃遇到问题基本能搜到现成答案第二InnoDB 存储引擎提供事务支持和崩溃恢复数据可靠性有保障第三配置和运维相对简单学习成本在数据库里算中等偏下第四几乎所有主流语言都有成熟的连接驱动工程接入非常方便。这几点合在一起让 MySQL 成了大多数人学数据库的默认第一选择。有些团队在超大并发场景会考虑其他的分布式数据库但作为打基础MySQL 的通用性和资料丰富度是最高的。1.2 一张表长什么样用花名册把核心概念讲透关系型数据库里最小也最重要的单位是表。很多人初学的时候把重心放在 SQL 语法上反而忽视了“表”这个核心。其实只要理解一张表数据库里一半的概念都能带出来。拿学校的“学生花名册”来打比方。花名册上有学号、姓名、班级、手机号这些固定栏目每个学生占一行。固定栏目就是字段也叫列每个学生那一行数据就是一条记录整个花名册就是一张数据表把所有班级的花名册集中放在一个文件夹里这个文件夹就是数据库。这张表要组织得规矩必须有一个唯一标识来区分每一行这个标识就是主键。花名册里学号不会重复学号就是这一行的主键。如果两张表要建立联系还会用到外键。比如学生表和班级表学生表里存一个“班级ID”指向班级表里的某一行这样通过这个字段就能知道学生属于哪个班。这种用字段把表串起来的思想是关系型数据库区别于普通文件存储的核心也是后面写 JOIN 查询的基础。这个结构一开始可能有点绕但自己动手画两张表的连线图两三次操作就能熟悉。1.3 SQL 不是编程语言而是一套对话指令SQL 的英文全称是 Structured Query Language结构化查询语言。这里要纠正一个常见误解SQL 不能拿来写业务逻辑它没有循环、变量、对象这些编程概念更像一种“点菜”语言你只要把需求说清楚即可。比如“给我看看 18 岁以上的用户”对应SELECT * FROM user WHERE age 18“把张三的余额加 100”对应UPDATE user SET balance balance 100 WHERE name 张三。每一条需求都可以用一条语句表达语句不求花哨通顺明白最重要。SQL 大体分四类查询类SELECT、操作类INSERT/UPDATE/DELETE、结构类CREATE/ALTER/DROP、权限类GRANT/REVOKE。基础阶段把前两类练熟日常工作基本就够用了。因为 SQL 在开发、数据分析、运维等多个岗位通用所以它的学习曲线虽然平缓但性价比很高值得认真花时间打底。2. 环境搭建别贪多一条路跑通就能一直用2.1 三种常见安装方式怎么选我见过不少初学者在环境这一步卡住其实没必要。先把目标定下来我就是要本地有个能跑的 MySQL能建库、能建表、能敲 SQL。至于性能、部署、集群这些概念后面再学完全来得及。常见选择有三条路。如果你的电脑是 Windows 或 macOS直接在官网下载社区版安装包一路按默认选项安装最省心如果平时用的是 Linux 环境可以用系统自带的包管理器两条命令的事如果你已经装了容器工具用容器把 MySQL 实例拉起来也很快。三种方式最终结果一样区别只在安装路径和启停方式。我自己在项目里最常用容器方式一条命令就能把环境隔离起来想换版本也方便。但说实话新人我不建议把时间耗在容器工具的环境配置上先用最顺手的安装包跑起来更重要。也可以申请一个云厂商提供的免费测试实例好处是不占本地资源登录就能操作但要注意测试实例上别放敏感数据密码也要设置得复杂一点。提示学习阶段的重点是 SQL 本身不是搭建过程。只要有一个能登录的 MySQL 实例哪怕是别人给你开的测试环境也完全够用。2.2 命令行和图形客户端我建议两个都会MySQL 装好后连接方式无非两种。一种是打开终端敲mysql -uroot -p进入自带的命令行客户端另一种是用图形客户端工具连接连接时需要填主机地址、端口、用户名、密码等参数。命令行是基本功图形客户端出任何问题时最后总能靠命令行救回来图形客户端负责提升效率一直手敲 SQL 看结果确实费劲。图形客户端最常见的连接失败原因是三个主机写成了 localhost 但实际需要填服务器 IP端口被防火墙或安全组拦住了用户授权不允许从当前主机登录。遇到这种情况先确认端口通不通再看账号权限八成问题都出在这两处。另外需要注意MySQL 默认端口是 3306有些云环境还要在控制台额外放行这一步新手容易漏。2.3 建库建表一条能反复用的实操路径假设你已经装好并且登录成功。第一步创建自己的练习库注意带上字符集否则后面插入中文会踩乱码的坑CREATE DATABASE learn_mysql DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE learn_mysql;第二步把最经典的 user 表建出来CREATE TABLE user ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 主键, name VARCHAR(50) NOT NULL COMMENT 姓名, age TINYINT UNSIGNED DEFAULT NULL COMMENT 年龄, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表;第三步确认建表结果SHOW TABLES; DESC user;这三步做完一个能练手的环境就铺好了。我建议把这组命令存在本地笔记里以后每次初始化环境都能直接用不用重新翻文档。表结构里出现的关键词下一章专门展开讲。3. 表结构设计是基础里的重点别急着塞数据3.1 字段类型怎么选抓住原则就不会乱建表最常遇到的就是字段类型选择。新人容易犯的一个极端错误是全用 VARCHAR反正什么都能存但后患无穷。类型选对了数据校验、存储空间、查询效率都会好很多。整数、小数、字符串、时间每个大类里都有常用选项。整数从 TINYINT 到 BIGINT按范围选比如年龄用 TINYINT UNSIGNED 就够0 到 255主键如果走自增直接上 BIGINT 更稳。小数里最常踩坑的是 FLOAT 和 DOUBLE存金额会出现精度问题因为浮点数在计算机里是近似值两分钱的账可能对不齐。凡是钱相关字段一律用 DECIMAL它按十进制精确存储金额运算才不会出错。字符串类型主要是 CHAR 和 VARCHAR。CHAR 是固定长度比如存 11 位手机号查询快但浪费空间VARCHAR 是可变长度存多长算多长更省空间是日常默认选择。时间类型有 DATE、DATETIME、TIMESTAMP记住一条带时分秒用 DATETIME只到日期用 DATE希望自动填创建时间就交给 DEFAULT CURRENT_TIMESTAMP。下面是我自己常用的类型清单用途推荐类型原因年龄 / 状态码TINYINT / INT占空间小也利于索引自增主键BIGINT留够扩展余量金额DECIMAL(10,2)精确计算不丢分用户名 / 备注VARCHAR(50~500)可变长度省空间时间DATETIME范围大不受时区影响3.2 约束和主键把脏数据挡在数据库门外数据来自用户输入用户总会犯错。约束就是数据库提供的防错机制常见的有 NOT NULL不能为空、UNIQUE不能重复、DEFAULT给默认值、CHECK做条件校验。它们不是给开发添麻烦而是尽量把非法数据挡在门外。比如手机号字段加 UNIQUE重复注册在数据库这一层就会被拦下来。主键约束是这一套机制里最重要的。主键选择有两种主流方案自增主键和业务唯一键。自增主键就是一个整数列每次插入自动加一简单、无业务含义、稳定业务唯一键比如身份证号、订单号有实际含义但如果业务规则变化改起来很头疼。我的建议是每张表默认都放一个自增主键再在业务字段上加 UNIQUE两套方案一起用。还有一个容易忽略的细节主键类型用 INT 还是 BIGINT。如果表的数据量未来可能接近千万级别建议直接上 BIGINTINT 上限约 21 亿听起来很大但对大表来说并不遥远。我见过订单表主键存满后被迫改表结构的事故来回折腾非常痛苦不如设计阶段就一步到位。3.3 外键能理解但别到处乱用外键的作用是保证两张表之间的引用关系一致。比如订单表里的用户 ID必须真实存在于用户表加外键约束后数据库会自动做检查如果用户被删除但还有订单删除就会失败。这对数据一致性是种保护。但在实际项目里大家对加外键非常谨慎。每次插入、更新、删除都要触发额外校验数据量大之后会变成性能瓶颈表关系一旦复杂外键还会环环相扣线上做数据变更会特别费劲。所以互联网业务里更流行的做法是不在数据库层建外键只把关联字段保留下来通过应用层代码来维护关系也就是常说的“逻辑外键”。对新人我的建议是外键概念必须理解也要会在学习环境里建一次、删一次但别形成“必须用外键”的思维定势。以后进了真实项目先看团队规范再决定要不要使用。3.4 动手设计一张用户表把知识落下来把前面几条串起来我们设计一张更真实的用户表。假设场景是一个社区网站用户需要邮箱、昵称、手机号、性别、生日、注册时间等字段CREATE TABLE user_info ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 主键, email VARCHAR(100) NOT NULL COMMENT 邮箱登录账号, phone VARCHAR(20) DEFAULT NULL COMMENT 手机号可空, nickname VARCHAR(50) NOT NULL COMMENT 昵称, gender TINYINT NOT NULL DEFAULT 0 COMMENT 性别 0未知 1男 2女, birthday DATE DEFAULT NULL COMMENT 生日, status TINYINT NOT NULL DEFAULT 1 COMMENT 账号状态 1正常 0冻结, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 注册时间, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, PRIMARY KEY (id), UNIQUE KEY uk_email (email), KEY idx_phone (phone) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户信息表;这里有几个值得品一下的细节email 加了唯一索引因为登录要用phone 允许为空所以不能加 NOT NULLstatus 用数字存扩展性比字符好create_time 和 update_time 交给数据库自动维护应用代码会省很多事。建表之后再插几行模拟数据后面写 SQL 的素材也就有了。4. 日常 SQL 操作把这套基本套路记牢4.1 增删改查先记住四个动词日常操作里出现频率最高的就是增删改查增是 INSERT删是 DELETE改是 UPDATE查是 SELECT。我建议所有 SQL 学习都从这四个词开始练熟了再谈优化再谈高级写法。插入数据的标准写法是把字段名和值一一对上INSERT INTO user_info (email, nickname, gender, birthday, phone) VALUES (aexample.com, 小A, 1, 1995-06-01, 13800000000);更新数据时最常犯的错是忘写 WHERE。比如UPDATE user_info SET nickname 新昵称 WHERE id 1;这条只改主键为 1 的那一行。如果漏掉 WHERE全表昵称都会被改成“新昵称”测试环境还好生产环境基本是事故。所以我的习惯是先写 WHERE再回头写 SET用顺序强迫自己确认条件范围。删除的威力更猛DELETE 不带 WHERE 会清空整张表的记录。这个操作没有后悔药除非你提前开了事务。所以生产环境的删除命令我基本都会先SELECT COUNT(*) FROM ... WHERE ...看一眼影响范围然后再执行删除能少很多惊吓。4.2 查询三板斧过滤、排序、分页SELECT 的语法看起来自由但实际使用中核心能力就藏在“过滤、排序、分页”三板斧里。过滤靠 WHERE排序靠 ORDER BY分页靠 LIMIT。WHERE 里最常用的操作符分三类比较运算、、、、、!、范围运算BETWEEN、IN、模糊匹配LIKE。比如查找成年用户SELECT name, age FROM user_info WHERE age 18;查找手机号符合特定范围的用户SELECT name FROM user_info WHERE phone IN (13800000000, 13900000000);IN 后面是一个列表也可以接子查询这是后面写复杂查询的基础。模糊匹配 LIKE 张% 表示查姓张的人速度受索引影响这点第五章再细说。排序用 ORDER BY默认升序降序加 DESC。多个排序字段想清楚优先级例如先按注册时间倒序再按昵称正序SELECT * FROM user_info WHERE status 1 ORDER BY create_time DESC, nickname ASC;分页是列表查询经久不变的需求写法很固定SELECT * FROM user_info LIMIT 20, 10;这句的意思是跳过 20 行取 10 行也就是第 21 到第 30 条。LIMIT 的坑在于数据量大以后跳过的行数越多查询反而越慢后面常见问题部分会讲替代方案。4.3 聚合统计从“看数据”变成“看结论”大部分查询是在看明细但业务上更常问的是“总数有多少”“平均数是多少”“最多的是哪个”。这类问题靠的是聚合函数最常用的四个是 COUNT、SUM、AVG、MAX/MIN。统计全表用户数SELECT COUNT(*) FROM user_info;按性别分组统计人数SELECT gender, COUNT(*) AS cnt FROM user_info GROUP BY gender;GROUP BY 的语义是把相同字段值的行合并成一组再用聚合函数对每组计算。所以 SELECT 后面能出现的列要么是分组列要么是聚合函数的结果。这个规则要记牢否则很多数据库会直接报错。GROUP BY 之后如果还想过滤组级别的结果不能用 WHERE得用 HAVING。区别很好记WHERE 过滤的是每一行HAVING 过滤的是每一个小组。比如找出人数超过 10 的性别分组SELECT gender, COUNT(*) AS cnt FROM user_info GROUP BY gender HAVING cnt 10;这种查询在企业报表里非常普遍能把几百上千条明细压缩成一张用于决策的小表格。4.4 多表查询 JOIN把两张表拼在一起看业务数据往往分散在多张表里用户信息在用户表订单在订单表要看某个人的所有订单就得 JOIN。JOIN 的核心思想是基于某个共同字段把两张表的行横向拼接起来。先看最简单的 INNER JOIN只返回两张表能匹配上的行SELECT u.nickname, o.order_no, o.amount FROM user_info u INNER JOIN order_info o ON u.id o.user_id;这里的 u 和 o 是表的别名让 SQL 更简洁。同名字段必须在前面加别名否则数据库分不清到底取哪张表的字段。另一种常用的是 LEFT JOIN返回左表的全部记录右表没匹配到的字段补 NULL。典型场景是“查所有用户以及他们的订单没有订单的用户也要显示出来”SELECT u.nickname, o.order_no FROM user_info u LEFT JOIN order_info o ON u.id o.user_id;JOIN 的重点不是背语法而是想清楚连接条件是什么以及结果集是否可能重复。我排查过很多次“COUNT 突然翻倍”的线上问题最后都是 JOIN 把一对多关系翻成了多对多这个坑印象非常深。5. 索引与性能让查询从“全表扫”变成“按目录翻”5.1 索引为什么快一本书的目录和 B 树数据量小的时候一条 SELECT 走不走索引差别不明显两万行的表全表扫也就几十毫秒。但数据量到了百万、千万级全表扫描就会慢到难以容忍这时索引的威力就体现出来了。最直观的理解是书的目录。没有目录时你想找“事务”这个知识点得从第一页翻到最后一页有了目录先定位章节再跳到具体页几步就到。MySQL 里索引就是数据库给表建的“目录”它基于 B 树这种数据结构组织让查找不再是逐个比较而是层层缩小范围。查询效率从线性级别变成对数级别。但索引也不是万能的。索引会占用额外存储空间写入数据时还要维护索引结构多一次更新就多一份开销所以不能乱建。基础阶段先掌握两个原则第一高频查询的字段才值得建索引第二区分度高的字段才适合建索引。像“性别”这种字段只有两三个取值建索引也过滤不掉多少数据收益很低而邮箱、手机号这种几乎一条一个值收益就会很明显。5.2 索引类型和建索引的基本套路MySQL 常见索引可以按功能分几类普通索引、唯一索引、主键索引、联合索引、全文索引。其中前四类是基础阶段最常接触的。建索引最直接的方式是建表时写在字段定义里也可以用 ALTER TABLE 后来添加ALTER TABLE user_info ADD INDEX idx_nickname (nickname);如果查询经常用 email 和 status 两个条件一起过滤可以建联合索引ALTER TABLE user_info ADD INDEX idx_email_status (email, status);联合索引有一个“最左前缀”规则查询条件必须从联合索引的第一个字段开始依次匹配索引才能被完整使用。拿 idx_email_status 举例查询条件里只有 status 没有 email这个索引就用不上只有 email 一个字段却可以命中。这个细节在面试中高频出现排查实际性能问题时也经常遇到。5.3 用 EXPLAIN 看执行计划索引有没有生效一目了然索引到底有没有走不用靠猜一条命令就能看执行计划EXPLAIN SELECT * FROM user_info WHERE nickname 小A;EXPLAIN 的结果里有几个关键列我建议先看 type 和 key。type 是访问类型从好到差大致是 const、ref、range、index、ALL看到 ALL 通常就是全表扫描。目标是让 type 尽量落在 range 或 ref并且确认 key 里出现你期待的索引名。索引失效的常见原因有三个我遇到最多的一个是对索引列做了函数运算比如WHERE YEAR(create_time) 2024索引当场作废应该改写成create_time 2024-01-01 AND create_time 2025-01-01。第二个是 LIKE 模糊查询时通配符在最前面LIKE %小A没法利用索引。第三个是 OR 连接的多个条件里只要有一个字段没索引整个查询都可能退化成全表扫描可以考虑拆成 UNION 或者给相关字段都补上索引。最后要提醒索引不是加得越多越好。单表索引数量最好控制在五六个以内联合索引尽量合并同类项避免语义重复。我自己有过为了“更快”给一张表建了十几个索引最后导入 100 万条测试数据慢到怀疑人生的经历代价还是很直观的。6. 事务与数据一致性系统崩了钱也要对得上6.1 用转账理解 ACID事务这个概念用银行转账来解释最合适。A 给 B 转 100 块钱数据库要干两件事扣掉 A 的 100加上 B 的 100。如果第一步成功、第二步失败账就算错了。事务就是把这组操作打包成“要么全部成功要么全部失败”的单位。事务有四个特性合起来叫 ACID。原子性指事务里的操作要么全做、要么全不做一致性指事务执行前后数据都符合业务规则不能出现 A 少了钱 B 却没多钱隔离性指多个事务并发执行时互相不能看到对方未提交的中间状态持久性指事务一旦提交即使断电数据也不会丢。四个特性里持久性依赖于日志和刷盘机制隔离性依赖锁和多版本控制学习阶段先理解“它是干什么的”这一层就够了。6.2 隔离级别与并发常见问题并发事务如果隔离不当会出现脏读、不可重复读、幻读三种问题。脏读是一个事务读到了另一个事务未提交的数据万一对方回滚自己拿到的就是脏数据。不可重复读是同一个事务里两条同条件的 SELECT 结果不一样因为中间有其他事务提交了修改。幻读则是同一个事务里两次范围查询行数变了多出来几行像幻觉一样的记录。MySQL 提供了四种隔离级别读未提交、读已提交、可重复读、串行化。默认是“可重复读”它用多版本并发控制的机制解决了脏读和不可重复读配合锁定读也在绝大多数场景下避免了幻读。串行化隔离最强但性能最差基本只在极端场景使用。可以这么记隔离级别从低到高一致性越来越强并发性能越来越弱业务按需取平衡。隔离级别脏读不可重复读幻读说明读未提交可能可能可能极少使用读已提交不会可能可能很多数据库默认可重复读不会不会基本不会MySQL 默认串行化不会不会不会性能代价高6.3 事务怎么开BEGIN、COMMIT、ROLLBACK 实战新手写 SQL 时INSERT 和 UPDATE 一条条单独执行很难意识到“多个操作要作为一个整体”。但在订单、库存、余额这类场景事务必须用起来。MySQL 里事务三步走BEGIN 开始事务COMMIT 提交生效ROLLBACK 回滚取消。BEGIN; UPDATE account SET balance balance - 100 WHERE user_id A; UPDATE account SET balance balance 100 WHERE user_id B; COMMIT;也可以做个回滚实验BEGIN; UPDATE account SET balance balance - 100 WHERE user_id A; ROLLBACK;回滚之后A 的余额等于没动过。这就是原子性的直观表现。另外提醒一点程序里写事务时别把 SELECT 放进事务里长时间持有锁事务越短越好锁的范围越小越好否则高并发场景很容易把别的请求卡死。7. 新手阶段最容易踩的坑和我的几条老经验7.1 我见过最多的几个翻车现场第一个是乱码。建库建表时不指定 utf8mb4插入中文变成一串问号。解决办法是建库时就带DEFAULT CHARACTER SET utf8mb4建好的表也可以再用 ALTER 改字符集。记住utf8mb4 能存表情符号兼容性最好别再用旧版的 utf8。第二个是时间存成字符串。图省事把时间字段定义成 VARCHAR结果按时间范围查询时字符串比较的顺序、格式、时区问题一起冒出来处理起来非常痛苦。正确做法是时间字段就用 DATE、DATETIME 或 TIMESTAMP让数据库自己管格式。第三个是 SELECT * 全字段铺开。调试时用没问题但如果给外部接口返回数据多余字段既浪费带宽也可能把不该暴露的信息带出去。养成“只查自己需要的字段”的习惯是成本最低的性能优化。第四个是分页大偏移。比如LIMIT 2000000, 20MySQL 要先把前两百万行查出来再跳过越到后面的页码越慢。互联网项目里更常见的做法是用“上次游标”或“基于索引条件”的分页方案替代大偏移量基础阶段先记住这个方向。第五个是 DELETE 不带 WHERE前面提过这里再强调一次。任何 DELETE 在生产环境执行前要么先开启事务要么先 SELECT 验证影响范围不要一把梭。实在不行用 UPDATE 把状态置为“已删除”做逻辑删除回滚余地和可追溯性都更好。7.2 打完基础之后下一步怎么走基础阶段收获最大的练习方式不是刷题而是造数据。给自己模拟一个七天的小项目比如“每日记账本”设计三张表用户表、账单表、分类表然后写三十条以上 SQL 去查周报、月报、账单流水和分类统计。动手之后你会发现前面讲的那些概念会自己串成一条线比看十篇教程都管用。因为表结构、索引、事务、聚合这些知识本质上都是围绕一个真实业务场景在转。我个人带新人的时候常有一句话数据库基础不难难的是把数据当资产看待的那根弦。你写的每一条 SQL、建立的每一张表本质上都是在管理资产。所以操作前多想一步多备份多验证MySQL 就会变成非常好用的工具。这个基础阶段就把每一个示例亲手跑一遍弄明白为什么这么设计后面往上走的时候会稳得多。