尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
MySQL数据库设计实战:从规范建表到索引优化与事务隔离
1. 先想清楚再建库设计规范其实是第一课这篇mysql课堂笔记说白了我整理了很久。最初只是想把自己的学习过程记录下来后来发现很多同学不是不会写SQL而是表结构从一开始就建歪了后面怎么写都别扭。这篇笔记适合三类人刚入门想系统学数据库的工作了两三年但全靠百度拼SQL的以及面试前想快速把知识串一遍的。我会尽量把设计思路、实操细节、踩坑记录都揉在一起不是那种官方文档翻译稿。1.1 范式不是让你死守的重点是理解每一层在防什么上课讲到范式的时候老师举过一个例子我当时笑了半天但印象特别深一个学生表里如果存了“爱好”字段值是“篮球游泳唱歌”这就违反第一范式了因为一个字段里塞了多个值。第二范式说的是消除部分依赖常见场景是联合主键的表里某个字段只依赖其中一列。第三范式是消除传递依赖比如学生表里存班主任姓名而班主任姓名其实是依赖班级编号的。现实中做业务表我很少真的严格按范式把所有表拆到极致。因为查询实在太频繁时适度冗余反而能省掉一堆关联。比如订单表里冗余一个用户昵称虽然理论上应该去用户表join但下单后昵称改了订单里保留当时的昵称反而是合理的——这叫历史快照。课堂笔记里一定要把这个逻辑分清范式解决的是“写入异常”反范式解决的是“读的性能和业务状态冻结”两者不冲突。实际建表时我的习惯是三步走第一步列字段按业务语义思考核心实体需要哪些属性第二步识别主键和唯一键这一步决定了之后索引怎么设计第三步把哪些需要高频读取、哪些只是低频查询、哪些根本不会查区分开再决定冗余和要不要拆宽表。这个顺序反了后面基本要返工。1.2 字段类型选错索引再猛也白搭课堂笔记里必须单独留一个位置给字段类型这块看起来不起眼实际踩坑率极高。整数类型重点说三个TINYINT、INT、BIGINT。状态值用TINYINT就行一个字节省不了多少空间但语义更清晰主键和关联外键建议直接用BIGINT别问我为什么等你的表到了上千万行再迁移主键类型会痛苦到怀疑人生。金额字段用DECIMAL绝不用FLOAT或DOUBLE。浮点数是近似存储运算误差在金额场景不可接受。VARCHAR要按实际最大长度估算再加一点余量但别动不动就255。MySQL的VARCHAR在UTF8MB4字符集下一个字符最多占4字节所以VARCHAR(255)在极端情况下要占用超过1000字节这会影响索引长度上限。日期时间类型的坑更大。DATETIME和TIMESTAMP我建议一律优先DATETIME。TIMESTAMP的范围只到2038年而且会受时区影响DATETIME是字符串语义存进去是什么读出来就是什么。有些旧系统里的时间字段用VARCHAR存排序和范围查询都极其痛苦课堂练习里我也写过这种烂SQL后来被老师指出来说“你这是给自己埋雷”。时间字段该加索引一定加索引范围查询是业务高频场景。还有一个容易被忽视的点是默认值。建表时尽量所有字段都显式给DEFAULT哪怕是NULL也不要让字段处于“未定义”状态。配合NOT NULL尽量约束很多业务异常其实是空值引起的。1.3 字符集和排序规则决定你以后会不会乱码这一节是很多人忽略的重点。MySQL的字符集一定要用utf8mb4不是utf8。utf8在MySQL里其实只是utf8mb3最多存3个字节像emoji、生僻字这些4字节字符直接存不进去轻则报错重则数据被截断。排序规则建议用utf8mb4_unicode_ci或utf8mb4_0900_ai_ci这两个都能处理大小写不敏感的匹配只是具体的排序细节有差异选一个用到底就行。我遇到过最经典的乱码问题是库、表、连接三个层级字符集不一致。数据库是utf8mb4表也是utf8mb4但程序连接串里没指定characterEncoding结果中文写入后变成问号。建库的时候统一规范看好每一层不做此地步后面排查会非常费劲。统一字符集这件事现在不少云数据库默认就是对的但本地练手和旧项目里还得自己检查一遍。2. 课堂高频SQL建表到查询每一步都别想当然2.1 建表和修改表课堂练习里的基本功课堂笔记里最常见的场景就是按题目建表比如商品表、订单表、用户表。建表语句看起来简单但有几个点值得多说两句。CREATE TABLE user ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 主键ID, username VARCHAR(50) NOT NULL DEFAULT COMMENT 用户名, phone VARCHAR(20) NOT NULL DEFAULT COMMENT 手机号, email VARCHAR(100) NOT NULL DEFAULT COMMENT 邮箱, status TINYINT NOT NULL DEFAULT 1 COMMENT 状态1正常 0禁用, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, PRIMARY KEY (id), UNIQUE KEY uk_username (username), KEY idx_phone (phone) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_unicode_ci COMMENT用户表;这个建表样例我从课堂用到项目一直没大变过。几个要点id用BIGINT UNSIGNEDAUTO_INCREMENT主键必备业务字段能加唯一约束就加唯一约束可以让数据层兜底防重复时间字段建表时就配好默认值省得每次插入都手动写时间。修改表结构时要注意ALTER TABLE在数据量大时会锁表。虽然InnoDB在不少操作上支持在线DDL但不代表所有的修改都无痛。比如给大表新增字段很多版本里仍然需要重建表期间写入会被阻塞。课堂上学到的经验是生产环境的表结构变更尽量选择低峰期先用小表演练一遍看看到底耗时多久。2.2 增删改查简单背后的坑不少INSERT的坑主要是批量插入时单条语句过长以及字段类型和值不匹配导致隐式转换。比如phone字段如果定义成VARCHAR却传入了数字MySQL会做隐式类型转换导致索引失效。这一点后面索引章节还要重点说。UPDATE最大的坑就是忘记WHERE条件我敢打赌每个写SQL的人都干过这事。课堂老师讲过一句很吓人的话“UPDATE不加WHERE就是把全表的数据改一遍”。我当时觉得这种低级错误不会犯直到有一次在测试环境跑错才发现这种事故离自己很近。现在我的习惯是写UPDATE之前先写一条SELECT把WHERE条件查出来确认影响行数再把UPDATE语句拼上。DELETE和TRUNCATE的区别也是高频考点。DELETE可以加WHERE走事务删完数据行还在自增ID不会被重置TRUNCATE是直接把整张表清空重建速度快很多但不可按条件删也不能回滚。业务上一般只用DELETE加条件删TRUNCATE只用于清空临时表。2.3 SELECT查询GROUP BY和HAVING别再用混了查询语句是课堂笔记的大头。很多同学写了半年SQL还是分不清WHERE和HAVING。核心逻辑其实一句话WHERE是在分组前过滤行HAVING是在分组后过滤组。比如查订单表里每个用户的订单数先排除已取消的订单这是WHERE再筛选订单数大于10的用户这是HAVING。SELECT user_id, COUNT(*) AS order_cnt FROM orders WHERE status 1 GROUP BY user_id HAVING COUNT(*) 10 ORDER BY order_cnt DESC LIMIT 50;这个SQL是课堂里老师反复讲的经典结构一句话总结先在行级过滤再分组再在组级过滤最后排序和分页。JOIN的坑其实更多。很多人习惯把所有关联条件都塞到WHERE里而不是写在ON里。对于INNER JOIN两者结果一样但对于LEFT JOIN写在WHERE里的右表条件会把LEFT JOIN变成INNER JOIN因为NULL的行不满足WHERE条件最后被过滤掉了。我在项目里排查过这个问题SQL跑出来的数据量莫名其妙少了一半原因就是右表条件放在了WHERE里。课堂笔记里专门用红色标记过LEFT JOIN的右表过滤条件尽量放在ON里除非你确实想过滤掉不匹配的行。2.4 窗口函数课堂笔记里必须补上的现代SQL技能如果课堂笔记里只有传统的GROUP BY那对实际工作帮助有限。窗口函数在现代SQL里真的太重要了我第一次在项目里用窗口函数时直接删掉了三四行自连接的复杂写法。SELECT user_id, order_amount, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY order_amount DESC) AS rn FROM orders;这个SQL的意思是按用户分组组内按订单金额降序编号。就这么一句实现了“每个用户金额最大的订单”这种经典需求。RANK()和DENSE_RANK()的区别面试也爱问RANK会跳号比如两个并列第一下一个就是第三DENSE_RANK不跳号两个并列第一后下一个还是第二。窗口函数的核心优势是既做了聚合又保留了原始行明细这是普通GROUP BY做不到的。3. 索引与性能优化课堂笔记里含金量最高的一节3.1 InnoDB为什么选B树这一节换个角度解读为什么InnoDB的索引底层是B树而不是二叉树不是哈希表也不是普通的B树确实对比着记更牢固。哈希索引的查询单值极快O(1)级别但不支持范围查找也不支持排序。二叉树的查找效率不稳定而且树的高度会随着数据量增长变得很高磁盘IO次数太多。B树和B树的区别在于数据存储位置和叶子节点结构。B树的所有数据都在叶子节点而且叶子节点之间有链表相连做范围查询时顺着链表走就行这对数据库来说至关重要。看执行计划时用EXPLAIN看一眼访问类型和扫描行数就知道索引有没有生效。判断标准就一条SQL能不能快速缩小扫描范围。B树的另一个特点是数据有序存储这也是为什么在索引字段上做排序经常可以直接用索引顺序不需要额外的filesort。3.2 聚簇索引和回表这两个概念直接影响查询效率InnoDB中聚簇索引就是主键索引数据行直接存放在主键索引的叶子节点上。二级索引普通索引的叶子节点存储的是主键值。所以查询时如果用了二级索引要先在二级索引树里找到主键值再回到聚簇索引树里去取整行数据这个过程叫回表。为了减少回表就有了覆盖索引。如果查询的字段全部包含在索引里那就不需要回表直接能从索引里取数。典型的例子一个联合索引(a, b)查询时只查a和b两个字段直接走覆盖索引速度飞快。项目里优化过一个列表接口查询字段只有id和status原来的SQL是SELECT *每次要回表拿全字段后来改成只查需要的列并确保这两个列在联合索引里查询时间下降了一个数量级。课堂笔记里这句话值得大写加粗索引不是越多越好而是业务里筛选和排序用到的列有策略地建联合索引。3.3 联合索引最左前缀必须背下来的规则联合索引(age, city, phone)走索引的条件是查询条件从最左边的age开始且不能跳过中间列。比如WHERE age20 AND city北京 能走索引WHERE city北京则不走索引。这里还有一个容易被忽略的点范围查询会中断索引的后续列。比如WHERE age20 AND city北京age利用到了索引但city无法继续利用同一个联合索引。理解了这个建联合索引时就会把等值查询的字段放在前面范围查询的字段放在后面。我在一个真实报表SQL里踩过这个坑。原表有几十万数据查询条件是三个字段其中一个like范围查询放在了最前面结果全表扫描。调整字段顺序后扫描行数从几十万降到了几千。课堂笔记上写了一句自我提醒索引顺序的设计本质上是把高频等值条件放前面。3.4 EXPLAIN解读重点看这几个字段EXPLAIN输出的一大堆字段真正要看的是type、key、rows和Extra。type从好到差排列大概是system、const、eq_ref、ref、range、index、ALL。只要看到ALL基本就是全表扫描SQL大概率有问题。key表示实际用到的索引如果为NULL说明没走索引。rows是预估扫描行数这个数字一定要小和实际数据量偏离太大时要警惕统计信息过期。Extra里看到Using temporary或者Using filesort意味着查询里有排序或分组操作没用到索引需要优化。一条SQL慢我的排查顺序是先看SQL有没有全表扫描ELIMINATE掉再检查是不是索引失效然后看扫描行数为什么大最后从业务上想能不能减少查询的数据量。课堂笔记里把这些步骤固化成了一个四步排查法看type、看key、看rows、看Extra逐一排除。3.5 索引失效的常见触发点索引失效的触发点非常多课堂笔记里总结了几个高频场景。第一对索引列做函数操作。比如WHERE YEAR(created_at)2024如果created_at上有索引这样写就废了。改写为WHERE created_at2024-01-01 AND created_at2025-01-01就能走索引。第二隐式类型转换。字段是VARCHAR查询条件是数字MySQL会转成数字再比较索引失效。第三LIKE查询以通配符开头比如LIKE %关键词%会导致索引失效。如果业务上确实需要这种搜索建议引搜索引擎或全文本索引。第四在索引列上做计算比如WHERE price*1.1100同样把索引废了。4. 事务与隔离级别并发场景才是真实战场4.1 ACID和四种隔离级别一张表记住事务的四个特性ACID分别是原子性、一致性、隔离性、持久性。课堂笔记里常考的是隔离性也就是多个事务并发执行时如何互不干扰。标准SQL里定义了四个隔离级别从低到高分别是读未提交、读已提交、可重复读、可串行化。每种隔离级别对应会解决或者遗留的问题我用一张表总结隔离级别脏读不可重复读幻读读未提交可能可能可能读已提交不会可能可能可重复读不会不会可能InnoDB下基本解决可串行化不会不会不会MySQL默认使用的是可重复读这和其他数据库不太一样很多数据库默认是读已提交。脏读指的是读到另一个事务未提交的数据这个最吓人不可重复读是指同一查询在事务内两次执行结果不同侧重同一行数据被改写幻读是指同一查询在事务内两次执行结果的行数不同侧重新插入了数据。4.2 MVCC可重复读是怎么做到的如果是加锁实现可重复读那读和写会互相阻塞并发性能会很差。InnoDB实际是用MVCC多版本并发控制。每一行数据都有隐藏的版本链事务读取时会基于自己的快照判断能看到哪个版本的数据。课堂笔记里概念很抽象但落到代码其实不复杂。可重复读级别下第一次SELECT时生成快照之后整个事务里的普通读都从快照取数据。所以两个事务A和B同时启动A先查询到了一条数据B更新了这条数据并提交A再查同一行看到的还是旧版本的值。这在业务上非常重要比如报表生成过程中事务内部多次查询要保持一致。MVCC解决的是普通读快照读下的隔离问题但对于当前读比如UPDATE、DELETE、加锁的SELECT依然要走最新的数据并且加锁。所以可重复读下仍可能出现幻读只是InnoDB通过间隙锁在大多数场景解决了这个问题。这个知识点课堂笔记里标了三颗星面试和工作中都很重要。4.3 锁机制行锁、间隙锁和死锁InnoDB的行锁在RC级别下只锁行但在RR级别下还引入了间隙锁。间隙锁锁的是一个范围防止其他事务在这个范围内插入新数据这就是MySQL解决幻读的主要手段。间隙锁是一把双刃剑。课堂模拟过一个死锁案例事务A先锁了id1的行再想锁id2的行事务B先锁了id2的行再想锁id1的行。两个事务互相等待就死锁了。InnoDB检测到死锁后会回滚其中一个事务并抛异常。业务上减少死锁的方法是多个事务尽量按相同顺序加锁事务内操作尽量少而快避免大事务。我在项目里遇到过一个真正的死锁现场当时两张表的更新出现的锁顺序不一致。排查时用SHOW ENGINE INNODB STATUS看最后死锁的信息能直接看到两条语句和加锁的顺序。修正方式其实很简单把业务里两个更新的顺序固定下来不再交错问题就消失了。这个经验在课堂笔记里专门写了一行死锁出现时别急着改SQL先梳理锁的获取顺序。4.4 大事务的危害和发现方法大事务是隐藏杀手。一个事务执行太久会持有大量锁阻塞其他事务同时undo log不断膨胀磁盘占用飙升binlog也要等事务提交才会落盘主从同步也会跟着延迟。怎么发现大事务课堂上教了一个实用方法从information_schema的INNODB_TRX表里查询当前运行超过一定时间的事务。实际排查时状态字段是RUNNING且时间超过几十秒的事务多半要重点盯一下。再结合SHOW PROCESSLIST看会话执行的SQL基本能定位到是哪条业务逻辑拖住了事务。预防大事务的做法接口逻辑里尽量做到“快进快出”不查太多数据不循环更新不把远程调用放到事务范围里。这些原则放到任何业务场景我都觉得是本质真理。5. 从课堂到项目那些必须写在笔记本上的避坑经验5.1 字符集乱码和时区问题的现场复盘乱码问题在学校练习里顶多是显示难看到了生产环境就是数据事故。我处理过一个中文写入变问号的案例最后定位到三个地方库表字符集是utf8mb4但程序JDBC连接串里没有指定字符集数据库连接池初始化时使用了旧的连接配置部分表用了其他字符集。时区问题同样隐蔽。库表里存DATETIME但应用服务器所在时区与数据库不一致程序读取时间时会偏移数小时。为了避免这种问题团队最好定一个统一规范数据库和应用统一使用UTC存储或北京时间存储时间转换只发生在展示层。东部和西部不是重点重点是规则要统一才能不出幺蛾子。5.2 深分页优化LIMIT 1000000, 20的灾难课堂笔记里写过这样一个SQLSELECT * FROM orders ORDER BY id DESC LIMIT 1000000, 20。这个SQL看起来正常实际上MySQL会扫描前1000020行再丢掉前1000000行最后返回20行。这种深分页在数据量大了之后性能会急剧恶化。常用优化方式有两种。一种是基于主键定位先记下上一页最后一条数据的id下一次直接WHERE id上一页最大id再LIMIT 20。另一种是用子查询先查出目标范围的主键再关联原表取数据。SELECT * FROM orders WHERE id 1000020 ORDER BY id DESC LIMIT 20;这是我项目里实际用过的方式分页接口从慢查询Top级别直接降到了毫秒级。业务上如果允许尽量换成“下一页按钮”而不是“跳到任意页”就能彻底避开深分页问题。5.3 COUNT(*)为什么慢应该怎么数数据量COUNT()在MyISAM里秒回因为它的表元信息里有现成的行数。但在InnoDB里由于MVCC机制同一个表在不同事务里看到的行数可能不一样所以InnoDB必须实时扫描统计这就是为什么大表COUNT()非常慢。课堂笔记里提到过两种替代方案。第一种是COUNT(1)或COUNT(*)其实都差不多别纠结到底用哪个因为在InnoDB里解析后都是找主键统计。第二种是对于“总条数”这种不要求实时准确的统计直接用SQL里定期维护的统计数据表或者用EXPLAIN里的rows做近似估算。说到替代方案业务中如果只是想快速看到总量那用EXPLAIN的rows字段其实是最廉价的近似值。5.4 备份恢复课堂笔记容易漏掉的知识点备份是数据库学习里最容易被忽略的一环却又是生产环境里最重要的一环。mysqldump是逻辑备份适合数据量不大的场景优点是简单缺点是大数据量下慢而且恢复时不够灵活。物理备份工具如XtraBackup适合大数据量和在线备份场景。课堂笔记里强调过一句备份不是拷一份文件就结束一定要定期演练恢复。否则真出事时才发现备份文件是坏的等于白备份。我见过一个案例某团队每天定时备份但从没恢复过。某次磁盘故障后恢复时才发现备份进程早早因为权限问题失败了告警又被忽略了。所以笔记里专门写了一条每周至少做一次恢复演练把备份文件恢复到测试库跑一遍关键SQL确认数据完整。5.5 课堂笔记之外的三个习惯最后一个部分聊点学习习惯。写这篇mysql课堂笔记之后我才发现学数据库最有效的方式不是看文档而是自己动手折腾建一套库往里灌几十万条假数据然后变着法写SQL优化它。第一个习惯是“每次都要看执行计划”。哪怕是一个只查几行的小表也要顺手EXPLAIN一下时间久了对哪些写法会导致走索引的直觉会非常准。第二个习惯是“写SQL前先想数据量”。同样的SQL在1万行和1千万行上的表现天差地别先评估目标表能涨到多大再决定要不要加索引、要不要拆表。第三个习惯是“定期翻一翻慢查询日志”。日志里记录的都是真实业务的糟糕SQL从里面学到的东西比在练习环境里写得再多都值钱。我个人在实际操作中的体会是数据库知识像盖楼设计规范是地基SQL是骨架索引是承重墙事务和优化是装修细节。如果地基没打好后面每一层都要代价。这篇课堂笔记里的东西我是从课堂到项目反复验证过才沉淀下来的希望能帮同样在学MySQL的人少走几步弯路。
RELATED

相关推荐

HBuilderX编辑器打包成APP

HBuilderX编辑器打包成APP

HBuilderX编辑器打包成APPHBuilderX编辑器打包成APPHBuilderX打包IOS全过程HBuilderX编辑器打包成APP 作者:梦想艳雪 点我跳转链接:https://www.jianshu.com/p/495a87607414 来源:简书 著作权归作者所有。商业转载请联系作者获得授权&#x…

📅 2026/10/16 10:22:50
Load average高不代表CPU忙:从负载分析到性能故障排查全链路指南

Load average高不代表CPU忙:从负载分析到性能故障排查全链路指南

凌晨两点,告警电话把我从床上拽起来:核心业务机的load average飙到了32,平时还不到3。打开监控的第一反应,不是去看代码,也不是直接重启,而是先看这半小时内的数据变化。系统负载与性能排查这件事&#xff…

📅 2026/10/16 10:22:50
有效三角形个数:排序+双指针从O(n³)优化到O(n²)

有效三角形个数:排序+双指针从O(n³)优化到O(n²)

1. 题目拆解:为什么暴力解法是最容易掉进去的坑 1.1 题面描述与三角形的关键条件 力扣611题“有效三角形的个数”,题面很简洁:给定一个包含非负整数的数组 nums ,返回其中可以组成三角形三条边的三元组个数。所谓“可以组成三角…

📅 2026/10/16 10:22:50
MORE NEWS

更多资讯

📰

面向对象核心:类与对象的区别、原理与实战

我经常被初学者问到一个问题:类和对象到底有什么区别?带新同学入门的时候,几乎每个人都会在这个地方卡一下。这个问题看似基础,其实特别值得花时间想透,因为后面所有东西——继承、多态、封装、设计模式——全都站在这…

📰

WGS84转ENU坐标转换从原理到实战,避免那些坑

在户外测量、无人机航线规划、自动驾驶路测或者机器人室内外定位的时候,你一定遇到过这种场景:手里拿到的是GPS设备输出的经纬度——也就是WGS84坐标系下的数据,但你要用这套数据去控制车辆或者飞机,去跟激光雷达点云做对齐&#…

📰

giflib 4.1.6 64位静态库在Windows MSVC工程中的配置与避坑指南

简介:面向需要在 osg 3.2.1 中编译 Plugin gif 的开发者,这份资源提供了 4.1.6 版本的 64 位静态链接库 giflib.lib 及配套头文件,解决了 GIF 图像编解码库与 OpenSceneGraph 集成时的链接难题。库内包含 giflib.lib、giflibd.lib&#xff08…

📰

从爬虫到Flutter影视App:移动端全栈实践完整指南

电影全栈系统的前几篇,我们把爬虫、后端接口、数据库、推荐服务都搭了起来,服务端的数据管道已经能稳定输出。但整个系统始终缺一个真正面向用户的东西,数据在接口里躺着,后台能看到,却没有任何一个贴近日常使用场景的…

📰

Go指针完全指南:从地址、多级指针到运算边界

先聊一个老生常谈的话题:指针。很多刚接触Go的程序员,写代码时对指针能避则避,总觉得这玩意儿是历史遗留的麻烦,又难理解又容易出bug。但只要你认真写过几个项目,就一定会撞上需要指针的时刻——比如修改实参、自定义数…

📰

Graphviz DOT语言从入门到精通:语法、布局引擎与工程实践

1. 为什么值得花时间啃透 DOT 这门“画图语言”第一次接触 Graphviz 的人,多半是被一段.dot文本劝退的:满屏的digraph、->、node [shapebox],看起来像某种上古配置文件,跟“画图”这件事八竿子打不着。但只要你真正用它画过一张…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬