先聊个扎心的事:我见过不少人自称“会 MySQL”,增删改查写得很溜,连表 JOIN、子查询也能拿捏。可一上生产就露馅——一条慢查询拖垮接口,一个锁等待让页面卡死,一次误删数据全员加班。每次遇到这种场面,我都会想起一句话:MySQL 基础从来不是“会不会写 SQL”,而是“你知不知道一条 SQL 进去之后发生了什么、为什么快、为什么慢、为什么锁、为什么挂”。这篇不是教你怎么背命令,而是想把 MySQL 这套地基给捋清楚,让刚入门的人能建立完整框架,也让写了几年 SQL 但一直没系统复盘过的开发,把脑子里那些模糊地带一次补齐。
我尽量用项目实战里踩过的真实案例来讲,不整虚的,每个结论都给出为什么。
1. 存储引擎选型:你连表用什么“发动机”都没选过,凭什么说基础扎实
很多人建表从来不写 ENGINE,默认就 InnoDB,这没错,但属于“瞎猫碰上死耗子”。你真问一句:MyISAM 和 InnoDB 到底差在哪?为什么 MySQL 默认选 InnoDB?不少人会卡壳。这个问题既是面试高频题,也是生产事故的源头。
1.1 行锁和表锁的差距,能直接决定你的并发上限
MyISAM 只支持表级锁,InnoDB 支持行级锁。这句话背出来容易,理解难。我举个例子:一张订单表,两个用户同时下单,分别改不同的订单行。用 InnoDB,两个事务互不干扰,各自锁自己的行;换成 MyISAM,第二个用户必须等第一个用户把整张表的写锁释放后才能操作。
你说这能差多少?一个订单表几百行的时候感觉不明显,等表里有几十万行、QPS 上百的时候,MyISAM 基本就是排队系统——所有写操作串行化,读操作又会被写锁阻塞。我当年接手过一个老报表系统,用的就是 MyISAM,每天凌晨跑批的时候,前端页面查询直接超时,问题就出在跑批任务一把表锁把整张表堵死,所有读请求全部排队。
InnoDB 的行锁不是物理上锁住某一行,而是通过索引来实现的:如果更新语句没走索引,InnoDB 会从行锁升级为锁住所有扫描到的记录,表现上跟表锁差不多。这也是很多“明明选了 InnoDB 却还是卡死”的真相。
1.2 InnoDB 的崩溃恢复能力,才是它成为默认的底牌
很多人以为 InnoDB 默认只是因为支持事务,其实更关键的是崩溃恢复。MyISAM 写数据是直接落盘,写入一半断电,表就坏了,还需要 repai。InnoDB 有 redo log,每次修改先写日志,再异步刷新到数据文件,即使数据库宕机,重启后也能根据 redo log 恢复到最后一次提交的状态。
这对生产环境意味着什么?意味着你不需要每天提心吊胆怕断电、怕 kill -9。我自己的经验是:MyISAM 表一旦损坏,找回数据的成本极高,而 InnoDB 基本上重启就能自愈。
另外 InnoDB 还有一套缓冲池机制,热点数据页缓存在内存里,读请求命中缓冲池就不用走磁盘。MyISAM 也有 key cache,但只缓存索引,不缓存数据行,所以全表扫的时候 InnoDB 的缓存命中率优势非常明显。
1.3 什么时候才需要考虑 MyISAM?
说实话,在 MySQL 8.0 里 MyISAM 已经被边缘化。只有一种情况我还会想起它:某些极端的只读场景下,MyISAM 的压缩表能把占用空间压得很低,查询性能也不差。但仅限归档库、历史数据、完全不需要事务的场景。
我更想强调一个反直觉的结论:不要用“我的表只读,不需要事务”来给 MyISAM 找理由。 InnoDB 的默认配置在只读场景下也有很好的性能,而且你没法保证今天“只读”的表明天不会被业务需求改成可写。我踩过的坑就是:以为一张配置表永远只读,用了 MyISAM,后来业务要支持在线配置热更新,结果每次更新都阻塞读请求,最后还得迁移表引擎。
经验:从 MyISAM 迁移到 InnoDB 一句
ALTER TABLE t ENGINE=InnoDB就能搞定,但线上大表执行会锁表,建议用 pt-online-schema-change 这类工具平滑处理。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 索引不是越多越好:B+树、回表和最左前缀的相爱相杀
索引是 MySQL 性能的核心,也是“基础”里最值得深挖的一块。我知道大家都会背“索引底层是 B+树”,但如果面试官追问“为什么是 B+树,不是二叉树、不是哈希、不是跳表”,很多人就蒙了。这一章我把它讲透,顺便把生产环境里最常见的索引失效场景一起复盘。
2.1 B+树为什么能扛住千万级数据
先给个直觉:B+树是多叉平衡树,每个节点能存很多个 key,树的高度非常低。InnoDB 中一个节点默认 16KB,假设一行数据 1KB,主键是 bigint 占 8 字节,再加指针之类的开销,一个叶子节点能存大约 16 行数据;非叶子节点只存主键和指针,一个节点能存一千多个主键。
三层的 B+树能存多少数据?大概 1000 * 1000 * 16,也就是一千六百万行。这意味着,你查一条数据最多只需要做三次磁盘 IO——根节点常驻内存,实际上一般就两次磁盘 IO。这就是 B+树最恐怖的地方:千万级数据量下,索引查询依旧能保持在毫秒级。
为什么不用哈希?哈希索引对单条等值查询确实快,O(1) 就能定位,但它无法支持范围查询,WHERE id > 100 这种场景哈希直接废掉。为什么不用跳表?跳表在内存数据库里很流行(比如 Redis),但 MySQL 的数据是落盘的,跳表的节点散落在磁盘上,局部性差,IO 次数远高于 B+树。B+树的叶子节点通过双向链表串联,天然就是为范围查询和磁盘顺序读设计的。
2.2 聚簇索引、二级索引和回表:为什么“明明有索引还是很慢”
InnoDB 中,每张表都有一个聚簇索引,通常是主键。聚簇索引的叶子节点直接存整行数据,所以按主键查询是最快的,一次 B+树检索就拿到全部字段。
非主键索引(二级索引)的叶子节点存的是主键值。也就是说,SELECT * FROM user WHERE name='张三',如果 name 上有二级索引,MySQL 会先在二级索引里找到主键 id,再拿 id 去聚簇索引里查整行数据——这个过程就叫回表。
回表不是问题,问题是一次查询要检索两棵 B+树。如果查出来的行数很多,比如 name 匹配一万行,就要回表一万次,性能自然差。应对方案就是覆盖索引:让查询的字段全部包含在索引里,这样直接读二级索引就足够了,不需要回表。
我举个例子,订单表经常要查某个用户的订单状态:
sql复制SELECT order_id, status FROM orders WHERE user_id = 123;
这时候建立联合索引 (user_id, status),那么 user_id、status 两个字段都在索引里,查询走索引就能拿到结果,不需要回表。很多生产环境 SQL 优化,做的就是“消灭回表”这一步。
2.3 联合索引和最左前缀:设计索引时的第一个铁律
联合索引 (a, b, c) 在 B+树里先按 a 排序,a 相同再按 b 排序,b 相同再按 c 排序。所以它能支持查询条件 a、a,b、a,b,c,但不能单独支持 b 或 c 的查询,这就是最左前缀原则。
这意味着:索引字段的顺序直接决定索引能覆盖哪些查询。 设计时要把区分度最高、查询最频繁的字段放最左边。比如订单表高频查询是 WHERE user_id=? AND order_status=?,那联合索引就该是 (user_id, order_status),而不是反过来;如果你把 order_status 放前面,那 user_id 的等值查询就用不上这个索引。
我还见过一个典型错误:对 (a, b, c) 联合索引,用 WHERE b=1 AND c=2 查询,以为三个字段全建了索引就能用上。结果走了全表扫描。你不妨用 EXPLAIN 看一眼,type 从 ref 掉到 ALL 的那一刻就会记住这个教训。
2.4 索引失效的几类高频场景:类型转换是最阴的坑
索引失效是老生常谈,但很多人只记结论,不知道原理。我梳理四个最常踩的:
- 隐式类型转换:索引列是 int 类型,查询条件写
WHERE id = '123',MySQL 会把字符串转成数字,导致无法走索引。反过来的情况更隐蔽:字段是 varchar,条件传数字,MySQL 同样会做类型转换,索引直接失效。 - 函数包裹索引列:
WHERE DATE(create_time) = '2024-01-01',对 create_time 使用函数后,索引失效。正确写法是WHERE create_time >= '2024-01-01' AND create_time < '2024-01-02'。 - LIKE 左侧通配符:
WHERE name LIKE '%张%',最左侧的%使得 B+树无法按前缀比较,只能扫全表。 - OR 连接非索引列:
WHERE a = 1 OR b = 2,即使 a 有索引,b 没有索引,MySQL 也只能全表扫描。
我特别想强调第一个,因为它最阴——你看 SQL 语法完全正常,字段也有索引,EXPLAIN 一出来就是全表扫描。有个热搜词叫 mysql中int+5,说的就是 int 类型做算术运算的坑:如果你在 SQL 里写 WHERE id + 5 > 10,索引列被表达式包裹,索引同样失效。正确做法是把表达式移到常量侧:WHERE id > 5。
2.5 一个索引都没有的表,和索引过多的表,一样危险
索引不是免费的午餐。每建一个索引,写入和更新时都要额外维护一棵 B+树,索引越多,写入越慢。而且索引占磁盘空间,缓冲池也会被索引页挤占。
我一个很深的体会:很多新人接手老项目,一慢就加索引,结果一张 20 个字段的表挂了 15 个索引,写入性能断崖式下跌。正确的做法是:高频查询才建索引,低区分度字段(比如 status 只有 3 个值)别建单列索引。 联合索引能覆盖多个查询条件就尽量用联合索引,而不是每个字段单独建。
3. 一条 SQL 在 MySQL 里经历了什么:从连接器到执行器的完整链路
如果你只把 MySQL 当一个黑盒,那遇到问题的时候只能靠猜。这一章我带你把一条 SQL 从客户端到服务端再到存储引擎的完整路径走一遍,这些都是面试题的常客,也是排查线上问题的基础。
3.1 连接器:为什么应用层一定要有连接池
第一步是建立连接。客户端发起 TCP 连接后,连接器负责验证身份、获取权限信息,然后维持这个连接。这里有个容易忽略的点:权限修改之后,已经存在的连接不会立刻生效,要等下一次新建连接才行。 所以在权限变更后,线上连接池里的旧连接可能还在用旧权限,这是一个安全细节,别忽略。
为什么应用层要用数据库连接池?因为建连的成本很高——TCP 三次握手、MySQL 认证、权限校验,一套下来几毫秒到几十毫秒。如果每条 SQL 都新建连接,大部分时间都耗在建连上。连接池的作用就是复用连接,HikariCP、Druid 这些工具大家不陌生,但要记住一个坑:连接池大小不是越大越好,默认 10 左右通常就够,太大反而会因为上下文切换降低吞吐。
3.2 分析器和优化器:SQL 写法如何影响执行计划
连接建立后,SQL 文本进入分析器,先做词法分析、语法分析,检查 SQL 是否符合语法规则。这一步基本没有优化空间,SQL 写得不规范,直接在这里报 You have an error in your SQL syntax。
接下来是优化器,这是整个链路里最核心的一环。优化器负责决定用哪个索引、JOIN 的顺序怎么定、是否使用临时表等等。注意,优化器选错索引是真实存在的,尤其当表数据分布变化、统计信息过期时,优化器可能选了一个“看起来还行”但实际很烂的执行计划。
这就引出一个实用建议:线上 SQL 变更前,一定要用 EXPLAIN 看执行计划。 重点看三列:type(访问类型,从好到差依次是 system > const > eq_ref > ref > range > index > ALL)、key(实际用的索引)、rows(预估扫描行数)。如果你看到 type=ALL 且 rows 有几十万,这条 SQL 基本就在全表扫。
3.3 执行器和存储引擎的配合
优化器定好方案后,执行器负责真正执行,它会调用存储引擎的接口去读取数据。这里有个经典问题:SELECT * FROM user WHERE name = '张三' 的执行过程是什么?
如果 name 上有索引,执行器会走二级索引找到对应的主键,然后回表拿整行数据;没有索引,就全表扫描每一行,把 name 等于“张三”的行筛出来。扫描完把结果返回给客户端。这个过程听起来简单,但它和“为什么回表慢”“为什么覆盖索引快”是直接对应的。
3.4 版本差异:8.0 移除了查询缓存,5.7 早该淘汰了
在 MySQL 8.0 之前,SQL 执行前会先查一个查询缓存:如果同样的 SQL 之前执行过,直接返回缓存结果。听起来很美好,但实际上查询缓存很容易失效,对写频繁的表,缓存命中率极低,反而需要额外维护。
MySQL 8.0 直接移除了查询缓存这个模块,理由是弊大于利。这也意味着:如果你的应用还在用 MySQL 5.7,建议尽早规划升级到 8.0。 8.0 不光去掉了查询缓存,还引入了窗口函数、公共表表达式(CTE)、默认字符集改为 utf8mb4,以及更安全的认证插件。新项目直接用 8.0,老项目慢慢迁移,但别再停留在 5.7 了。
有个热搜词是 mysql with as 子查询使用临时表,这里顺便一提:8.0 支持 WITH 子句(CTE),能让你写出更清晰的复杂查询,而且有些场景下能避免多层嵌套子查询的重复计算。
4. 事务隔离级别和锁:并发场景下,你的数据是怎么“乱”起来的
事务和锁是 MySQL 基础里最抽象的环节,也是面试重灾区。像 mysql锁表、mysql面试题 这类热搜词背后,基本都是同一拨人:写 SQL 没问题,一遇到并发场景就头大。这一章把四个隔离级别、MVCC、间隙锁一次讲明白。
4.1 四个隔离级别:脏读、不可重复读、幻读是怎么逐步被解决的
SQL 标准定义了四个隔离级别,从低到高:
- 读未提交:一个事务能读到另一个事务未提交的数据。这个最离谱,称为脏读。实际生产中没人用。
- 读已提交:只能读到已提交的数据,解决脏读。但同一个事务里,两次相同的查询可能返回不同结果,这叫不可重复读。Oracle 默认就是这个级别。
- 可重复读:同一个事务里,多次读取同一范围的数据结果一致,解决不可重复读。MySQL 默认就是这个级别。但它还有个残余问题——幻读:事务里两次范围查询,第二次多出了第一次没有的行。
- 串行化:事务完全串行执行,最安全但性能最差。
注意一个细节:MySQL 默认是可重复读,而 Oracle、PostgreSQL 的默认是读已提交。这导致从其他数据库迁到 MySQL 的项目,经常会遇到“明明提交了但另一个事务看不到新数据”的诡异现象,其实不是 bug,是隔离级别差异。
4.2 MVCC 和快照读:为什么 RR 级别下读不到别人刚提交的数据
MySQL 实现可重复读靠的不是加锁,而是 MVCC(多版本并发控制)。简单理解:每个事务开启时会生成一个视图(快照),后续查询基于这个快照读数据。其他事务提交的新数据,只要是在这个快照之后产生的,就“看不见”。
这就是为什么 RR 级别下,事务 A 查到 1 行,事务 B 插入一行并提交,事务 A 再查还是只有 1 行——它读的是自己的快照,不是最新的库。这个机制很好用,但也导致一个常见困惑:“为什么我开了事务,别人插的数据我就是查不到?” 答:因为 RR 的快照读本来就不看别人提交的新数据,这是特性不是缺陷。如果你需要实时看到最新数据,可以把隔离级别改成读已提交,或者用带锁的读(SELECT ... FOR UPDATE / LOCK IN SHARE MODE)。
4.3 行锁、表锁、间隙锁:Next-Key Lock 到底锁了什么东西
InnoDB 的行锁分共享锁(读锁)和排他锁(写锁),行锁只在索引记录上生效。如果条件条件没走索引,InnoDB 会锁住所有扫描到的记录,表现成表锁,这是很多死锁和锁等待的根源。
还有个更隐蔽的存在:间隙锁。RR 级别下,InnoDB 为了防幻读,会在索引记录之间的间隙上锁。比如 SELECT * FROM user WHERE age BETWEEN 20 AND 30 FOR UPDATE,如果 age 上有索引,InnoDB 不仅锁住匹配的记录,还会锁住它们之间的空隙,阻止其他事务往这个区间插数据。行锁加间隙锁合称 Next-Key Lock。
这带来了一个常见事故:两个事务分别在 age=20 和 age=30 的记录上持锁,又都想往间隙里插入一条 age=25 的数据,结果互相等对方释放间隙锁,直接死锁。MySQL 会在检测到死锁时回滚其中一个事务,业务层也能收到死锁错误。排查死锁最直接的命令是 SHOW ENGINE INNODB STATUS,里面会给出最近一次死锁涉及的两条 SQL 和持有锁的情况。
4.4 线上锁表了怎么办:我的排查链路
如果你遇到 Lock wait timeout exceeded; try restarting transaction,别慌,按这个步骤查:
- 执行
SHOW FULL PROCESSLIST;,看有没有State为Waiting for table metadata lock或Updating的长期运行语句。 - 找出阻塞源头,用
SELECT * FROM information_schema.innodb_trx;查看当前活跃事务,重点看trx_started和trx_query,把长时间未提交的事务揪出来。 - 如果确认事务卡死,用
KILL 进程ID;结束掉阻塞源事务。
还有一个元凶常被忽略:在事务里做了 SELECT 之后又做大量业务计算,迟迟不提交。 事务一直持有锁,其他会话只能等。你去看 trx_started 发现事务都开了好几分钟了。解决方式就一条:事务一定要短,提交要快,不在事务里做远程调用和耗时计算。
4.5 事务实践:默认隐式事务是隐形杀手
MySQL 默认是自动提交,也就是每条 SQL 一个事务。但有些 ORM 或代码里,事务是手动开启的,比如 Spring 的 @Transactional。我见过一个典型事故:一个循环里,每条数据更新都开启一个新事务,同时还有查询操作,导致事务重叠,锁等待暴涨。正确的做法是循环里不做数据库写操作,批量拼接成一条 SQL 或批量提交。
另一个高频坑是大事务。一个事务更新一百万行,binlog 会记录所有变更,主从同步延迟也会被拉满。所以不管业务压力多大,尽量把大事务拆成小批次事务,每批一千行提交一次。
5. 从安装到日常运维:那些“以为简单”的步骤,全是有坑的
热搜词里有一大堆是安装相关的:mysql安装教程、mysql windows安装教程、docker安装mysql、配置mysql server8.0、mysql卸载教程。说明安装配置这道坎卡住了很多人。这一章我直接把经验倒出来,包括安装、配置、常用命令和主从同步的基础认知。
5.1 版本选择:别再纠结,新项目直接 8.0
新项目无脑选 MySQL 8.0,理由前面提过:窗口函数、CTE、utf8mb4 默认、查询缓存移除、性能提升。如果你要跑老项目,确认兼容性。5.7 已经结束官方支持生命周期,继续用迟早是隐患。
安装方式上,Windows 用安装包比较省事,Linux 建议用官方 Yum/Apt 仓库安装,或者直接跑 Docker。生产环境物理机/云主机安装时,重点关注初始化参数:character-set-server=utf8mb4、collation-server=utf8mb4_unicode_ci、default-time-zone='+08:00'(按你的业务时区来)。这些参数务必在初始化时就写进 my.cnf,不要等建表了再改,改字符集意味着重建表。
5.2 Docker 安装 MySQL:我最常用也最容易翻车的方式
Docker 装 MySQL 很省事,但也最容易因为几个细节翻车:
bash复制docker run -d \
--name mysql8 \
-p 3306:3306 \
-e MYSQL_ROOT_PASSWORD=your_password \
-e TZ=Asia/Shanghai \
-v /data/mysql/conf:/etc/mysql/conf.d \
-v /data/mysql/data:/var/lib/mysql \
mysql:8.0
几个必须注意的坑:
- 数据目录一定要挂载到宿主机,否则容器删了,数据全没了。
TZ环境变量要设置,否则容器默认 UTC 时间,存进去的时间比北京时间慢 8 小时。- 8.0 默认认证插件是
caching_sha2_password,如果用老版本 JDBC 连不上,需要在创建用户时指定mysql_native_password,或者升级 JDBC 驱动。 - 容器里执行
docker exec -it mysql8 mysql -uroot -p进入命令行,改配置的话,编辑挂载目录下的配置文件再重启容器。
5.3 日常运维必会的一组命令
不管你是不是 DBA,只要开发涉及 MySQL,这组命令建议背下来:
sql复制-- 查看当前连接数,判断是否连接耗尽
SHOW STATUS LIKE 'Threads_connected';
SHOW VARIABLES LIKE 'max_connections';
-- 查看当前正在执行的 SQL,抓慢查询和锁等待
SHOW FULL PROCESSLIST;
-- 查看慢查询日志相关配置
SHOW VARIABLES LIKE 'slow_query_log%';
SHOW VARIABLES LIKE 'long_query_time';
-- 查看 InnoDB 状态,看死锁和锁等待
SHOW ENGINE INNODB STATUS;
-- 查看当前所有事务
SELECT * FROM information_schema.innodb_trx;
导出和导入是最常用的运维操作。mysqldump 导出单表:
bash复制mysqldump -uroot -p --single-transaction --set-gtid-purged=OFF database_name table_name > table_backup.sql
--single-transaction 对 InnoDB 特别重要,它在不锁表的情况下做一致性快照;--set-gtid-purged=OFF 是 8.0 下导出导入到另一套环境时的常见坑,不关掉有时会报错。
5.4 主从同步和 binlog:数据安全的第二道防线
主从复制是 MySQL 高可用的基础。核心原理就三步:主库把变更写入 binlog,从库拉取 binlog 写到自己的 relay log,再从 relay log 回放执行。8.0 以后更推荐用 GTID 模式,gtid_mode=ON,这样每个事务都有全局唯一 ID,主从切换后找同步位点会容易很多。
排查主从延迟最常用的命令是 SHOW SLAVE STATUS\G;,重点看 Seconds_Behind_Master 和 Slave_SQL_Running_State。如果延迟飙升,先看主库是不是有大事务,再看从库机器性能是不是跟不上。
这里提一个热搜词里的场景:mysql/sqlserver/postgresql数据库同步软件 和 datax同步 mysql 可配置参数。如果你要做异构数据同步(MySQL 到 SQL Server、PostgreSQL),或者要全量/增量抽取 MySQL 数据到数据仓库,DataX 是一个很成熟的方案;它对 MySQL 支持配置 where 条件、自定义 split 主键等参数来控制并发。但从基础角度说,先搞清楚 binlog 和主从原理,再去看这些同步工具,会顺畅很多。
5.5 字符集和排序规则:utf8 和 utf8mb4 不是一回事
这是安装配置阶段最经典的坑。MySQL 里的 utf8 其实是 utf8mb3,只支持最多 3 字节的字符,存不了 emoji;utf8mb4 才是完整 UTF-8。所以建库建表时,字符集统一用 utf8mb4,排序规则一般用 utf8mb4_unicode_ci 或 utf8mb4_general_ci。前者校对更精准,后者性能略好,现在 8.0 默认是 utf8mb4_0900_ai_ci,直接用默认就行。
千万别偷懒让字段沿用表的字符集,如果一个表里既有 emoji 又有中文,建表时字段级别尽量统一指定 CHARACTER SET utf8mb4。
6. 实战高频问题复盘:这些“小问题”一次就能坑掉你一下午
最后一章,我把热搜词里那些看起来零零碎碎、但实际项目里特别容易踩的问题集中过一遍。每一个都是真实场景里有人栽过的跟头。
6.1 int 类型和 int+5 的误区:显示宽度和类型转换
热搜词 mysql中int+5 挺有意思,常见误解有两种:一是以为 INT(11) 里的 11 表示能存 11 位数,实际上它只是显示宽度,配合 ZEROFILL 才有意义,不影响取值范围;二是 SQL 里写 WHERE id + 5 = 20,以为挺自然,但索引列被表达式包裹,索引直接失效。
还有一个类型转换的经典场景:字段是 VARCHAR,查询条件写 WHERE phone = 13800001111,数字会被转成字符串再比较,结果索引失效。开发规范里通常要求:字符串类型的字段,查询时一定加引号。
6.2 唯一约束和已有重复数据:怎么给“脏数据”表加唯一索引
热搜词里有一条很典型的求助:给一张已经有重复数据的表设置唯一约束,一直报错。原因很简单:唯一索引要求所有现有数据不重复,在一堆重复数据上建唯一索引必然失败。
处理思路是:先查重复数据,保留一条,删除或合并其余重复行,再建唯一索引。查重复的 SQL 很经典:
sql复制SELECT user_id, COUNT(*) AS cnt
FROM user_login_log
GROUP BY user_id
HAVING cnt > 1;
这里我要说一个开发规范层面的建议:唯一约束应该在一开始建表时就设计好,不要等数据累积到几十万行再补救。 我见过最痛苦的一次迁移,光清理重复数据就花了一整个晚上,而且清理逻辑稍微写错一点,就会误删业务数据。
6.3 存储过程、触发器和分隔符:到底要不要用
热搜词里 mysql存储过程、mysql中触发器中分隔符 都有。我的观点很直接:存储过程和触发器能不用就不用。 原因不是它们不好,而是它们把业务逻辑藏进了数据库里,排查问题、版本管理、水平扩展都变得困难。应用层的代码可以用 Git 管理、可以灰度发布、可以单元测试,存储过程很难做到这些。
但如果你确实要写,有一个必踩的坑:默认情况下 MySQL 用 ; 作为语句分隔符,而存储过程内部也有大量 ;,客户端会把整个存储过程拆成多段去执行,直接语法报错。解决方式是先用 DELIMITER // 把分隔符改成双斜杠,创建完再改回来:
sql复制DELIMITER //
CREATE PROCEDURE proc_name()
BEGIN
SELECT 1;
END//
DELIMITER ;
6.4 深分页为什么慢:LIMIT 100000, 20 的真相
mysql排序 和 LIMIT 分页是日常高频操作。很多人天真地以为 LIMIT 100000, 20 就是只要 20 条,应该很快。实际上 MySQL 会先读取前 100020 条,再把前面的 100000 条丢掉,只返回最后 20 条。前 10 万条都不是白读的,每一条都要参与排序和扫描,当然慢。
优化手段有几种,最推荐的是游标分页(也叫 keyset pagination),利用主键或唯一索引定位上一页最后一条的位置:
sql复制SELECT * FROM orders
WHERE id > 上一页最后一条的id
ORDER BY id ASC
LIMIT 20;
这种写法可以用上主键索引,翻到百万页也很快。缺点是没法直接跳页,但对大多数业务,用户根本不会去点第 5000 页,这个限制完全可接受。如果非要跳页,可以用延迟关联:先查出主键集合,再回表查完整数据。
6.5 误删数据的底线思维:备份是你最后的退路
最后一个话题,也是我每次培训都强调的:任何人在生产环境执行 DELETE 或 UPDATE 之前,都要问自己“这句 SQL 错了怎么办”。 我的习惯是:先 SELECT COUNT(*) 查影响行数,再用 SELECT * 把要删的数据导出来备份,最后才执行操作。
线上备份不能只依赖主从。曾经有人以为主库挂了从库能顶上,结果一条 DELETE 没加 WHERE 条件,把所有行删了,binlog 自动同步,从库也没了——主从复制不会保护你免受逻辑错误。真正有效的是定期全量备份 + binlog 保留,误删之后可以用备份文件加 binlog 把数据恢复到误删前的时间点。
mysqldump 定时全量备份是最简单可行的方案,再配合 binlog 的 ROW 格式保留足够天数,基本能应对 99% 的误删场景。
最后分享一点个人体会
如果你完整读到这里,其实你已经把 MySQL 的一条主线串起来了:存储引擎决定了数据怎么存、怎么锁;索引决定了数据怎么读、读得快不快;事务和锁决定了并发环境下数据怎么保持一致;一条 SQL 的完整链路告诉你在哪个环节可以优化;安装配置和运维命令是落地的保障。
我自己带过不少新人,发现真正能快速成长的,不是背了多少命令,而是遇到问题时会从这条主线上找答案:慢查询出来了,先看执行计划,再回看索引和 SQL 写法;锁等待出现了,先查事务和当前 SQL,再分析隔离级别和锁范围。这套思路比记住任何一条冷门命令都值钱。
MySQL 基础不复杂,但它是一个体系。把这个体系建立起来之后,你再去看什么分库分表、读写分离、分布式事务,真的会轻松很多。
