MySQL表设计进阶-约束范式连接索引与事务
MySQL 表设计进阶约束、范式、连接、索引与事务会写SELECT之后很快会碰到另一个问题同样的数据为什么有人设计出来的表很好查、很好改有人的表却要靠一堆补丁维持差别往往不在 SQL 写得多花哨而在几个基础决定哪些字段必须有值哪些值不能重复一条信息到底应该放在哪张表表与表之间怎样建立关系哪些查询值得加索引多条写操作怎样保证一起成功或一起失败。下面继续用学生成绩库做例子。环境按 MySQL 8.0 编写重点不只是“语法怎么写”也会说明为什么这样设计。一、先看完整的表关系这个小系统有四张表class班级student学生每个学生属于一个班级course课程score成绩用来连接学生和课程。建表语句如下createdatabaseifnotexistsschool_demodefaultcharactersetutf8mb4collateutf8mb4_0900_ai_ci;useschool_demo;createtableclass(idbigintunsignedprimarykeyauto_increment,namevarchar(30)notnull,uniquekeyuk_class_name(name))engineInnoDB;createtablestudent(idbigintunsignedprimarykeyauto_increment,namevarchar(20)notnull,agetinyintunsigned,gendertinyintnotnulldefault0,class_idbigintunsigned,created_atdatetimenotnulldefaultcurrent_timestamp,constraintck_student_agecheck(ageisnullorage150),constraintck_student_gendercheck(genderin(0,1,2)),constraintfk_student_classforeignkey(class_id)referencesclass(id)onupdatecascadeondeletesetnull,keyidx_student_class_id(class_id))engineInnoDB;createtablecourse(idbigintunsignedprimarykeyauto_increment,namevarchar(50)notnull,creditdecimal(3,1)notnulldefault1.0,uniquekeyuk_course_name(name))engineInnoDB;createtablescore(student_idbigintunsignednotnull,course_idbigintunsignednotnull,scoredecimal(5,2),exam_timedate,primarykey(student_id,course_id),constraintck_score_rangecheck(scoreisnullorscorebetween0and100),constraintfk_score_studentforeignkey(student_id)referencesstudent(id)ondeletecascade,constraintfk_score_courseforeignkey(course_id)referencescourse(id)ondeleterestrict,keyidx_score_course_id(course_id))engineInnoDB;后面的约束、连接和索引都可以从这四张表里找到对应场景。二、约束是数据的最后一道防线1. NOT NULL必须存在的字段namevarchar(20)notnull学生姓名如果允许为NULL这条记录很难再用于点名、查询或展示。对业务上必须存在的字段应该直接在数据库层限制。不过NOT NULL也不要到处加。比如学生暂时没有分班时class_id允许为空就比随便填一个0更清楚。2. DEFAULT常见初始值gendertinyintnotnulldefault0默认值适合“大多数新记录都相同”的字段。它能简化插入语句但不应该掩盖真正缺失的信息。生日未知时保存NULL通常比写一个虚假的1970-01-01更合理。3. UNIQUE不能重复的业务值uniquekeyuk_course_name(name)同一个系统里不允许出现两门同名课程时可以交给唯一约束保证。只在 Java 代码中先查一次再插入并不能完全避免并发请求同时写入重复数据。MySQL 的唯一索引通常允许多个NULL。如果字段要求“必须存在且不能重复”需要一起写phonechar(11)notnullunique4. PRIMARY KEY一行数据的身份证单字段主键idbigintunsignedprimarykeyauto_increment联合主键primarykey(student_id,course_id)score表用(student_id, course_id)做联合主键表达的业务规则很明确一个学生在一门课程下只有一条成绩记录。主键最好稳定、短小并且没有业务含义。手机号、身份证号虽然也可能唯一但它们会变化也涉及隐私不适合作为到处传播的主键。5. AUTO_INCREMENT方便但不保证连续idbigintunsignedprimarykeyauto_increment自增主键只保证生成唯一值不保证严格连续。插入失败、事务回滚、批量预分配都可能留下空洞。因此不要拿自增 ID 推算业务数量也不要因为中间缺了一个编号就手动补回去。6. FOREIGN KEY保证引用关系存在constraintfk_student_classforeignkey(class_id)referencesclass(id)有了这个约束student.class_id不能指向一个不存在的班级。常见外键动作动作含义RESTRICT有子表记录引用时拒绝删除父表记录CASCADE父表更新或删除时子表跟着处理SET NULL父表记录删除后把子表外键设为NULL动作要按业务语义选。删除学生时连带删除成绩通常合理删除课程时直接清空所有历史成绩就未必合理所以示例中课程使用RESTRICT。很多互联网项目会在业务代码中维护关联而不使用物理外键主要是为了分库分表、写入性能和发布灵活性。但即使不用外键也要保留同样的完整性意识并通过代码、任务和监控处理孤儿数据。7. CHECK让取值范围更可信check(scoreisnullorscorebetween0and100)MySQL 8.0.16 及以后版本会真正执行CHECK约束。旧版本可能只解析语法但不生效维护老系统时要先确认版本。三、范式到底在解决什么问题范式不是为了把表拆得越多越好而是减少重复数据和更新异常。1. 第一范式一列只表达一件事不推荐idnameschool1张三吉林大学长春市0431-xxxxschool同时塞了名称、地址和电话后面很难按城市查询也很难单独修改电话。更合理的写法是拆成多个字段或者进一步拆成school表。2. 第二范式非主键字段依赖整个联合主键假设把成绩保存成下面这样学号课程 ID学生姓名课程名学分成绩10011张三MySQL49010012张三Java588主键是(学号, 课程 ID)但学生姓名只依赖学号课程名和学分只依赖课程 ID只有成绩同时依赖学号和课程 ID。所以应该拆成student(id, name) course(id, name, credit) score(student_id, course_id, score)这样修改学生姓名时只改一处不会出现同一个学生在不同成绩记录里名字不一致。3. 第三范式避免非主键字段之间的传递依赖如果学生表同时保存student(id, name, college_id, college_name, college_phone)那么依赖关系是student.id → college_id → college_name、college_phone学院电话只依赖学院不应该在每个学生行里重复。更合理的设计student(id, name, college_id) college(id, name, phone)学院换电话时只改一行。4. 真实项目不一定追求“绝对范式”读多写少、对响应时间很敏感的场景有时会有意保留冗余字段。例如订单明细会保存下单时的商品名称和价格避免商品后来改名、改价后影响历史订单。这不等于范式没用。先按范式把数据关系想清楚再有理由地冗余和一开始就把所有字段堆在一张表里是两回事。四、一对一、一对多和多对多1. 一对一用户基础信息和实名认证信息user(id, username) user_profile(id, user_id, real_name, id_card)在user_profile.user_id上同时加外键和唯一约束user_idbigintunsignednotnullunique2. 一对多一个班级有多个学生一个学生属于一个班级。外键放在“多”的一侧student.class_id → class.id3. 多对多学生可以选择多门课程一门课程也可以被多个学生选择。不要在student表里保存1,2,5这样的课程 ID 字符串而是增加中间表score(student_id, course_id, score)中间表还可以保存这段关系自己的属性例如成绩、考试时间、选课状态。五、多表查询把拆开的数据重新拼起来范式把数据拆到多张表里查询完整信息时就要连接。先插入少量测试数据insertintoclass(name)values(一班),(二班);insertintostudent(name,age,gender,class_id)values(张三,18,1,1),(李四,19,2,1),(王五,18,1,null);insertintocourse(name,credit)values(MySQL,4.0),(Java,5.0);insertintoscore(student_id,course_id,score,exam_time)values(1,1,90,2026-07-20),(1,2,88,2026-07-21),(2,1,82,2026-07-20);1. INNER JOIN只保留匹配成功的数据selects.nameasstudent_name,c.nameasclass_namefromstudent sinnerjoinclass conc.ids.class_id;没有分班的王五不会出现在结果中。2. LEFT JOIN保留左表全部数据selects.nameasstudent_name,sc.scorefromstudent sleftjoinscore sconsc.student_ids.id;即使学生暂时没有成绩也会保留学生这一行右表字段显示为NULL。一个很常见的坑是把右表过滤条件写进WHEREselects.name,sc.scorefromstudent sleftjoinscore sconsc.student_ids.idwheresc.score60;没有成绩的学生会被WHERE过滤掉效果接近内连接。如果需求是“保留所有学生只关联及格成绩”条件应该放在ON中selects.name,sc.scorefromstudent sleftjoinscore sconsc.student_ids.idandsc.score60;3. 多表连接selects.nameasstudent_name,cl.nameasclass_name,co.nameascourse_name,sc.scorefromstudent sleftjoinclass cloncl.ids.class_idleftjoinscore sconsc.student_ids.idleftjoincourse coonco.idsc.course_idorderbys.id,co.id;连接条件用ON最终结果的筛选条件用WHERESQL 会更容易读。4. 自连接员工和上级都在同一张表createtableemployee(idbigintunsignedprimarykey,namevarchar(20)notnull,manager_idbigintunsigned);查询员工及其上级selecte.nameasemployee_name,m.nameasmanager_namefromemployee eleftjoinemployee monm.ide.manager_id;同一张表通过别名扮演了两个角色。5. 子查询查询与张三同班的学生selectid,namefromstudentwhereclass_id(selectclass_idfromstudentwherename张三);查询选修了 MySQL 或 Java 的学生selectid,namefromstudentwhereidin(selectstudent_idfromscorewherecourse_idin(selectidfromcoursewherenamein(MySQL,Java)));子查询不一定比连接慢优化器会做改写。写完后应结合EXPLAIN看真实执行计划而不是只凭语法形式判断。6. UNION 与 UNION ALLselectnamefromstudentunionselectnamefromteacher;UNION会去重UNION ALL直接合并结果通常更快。如果业务上不需要去重优先使用UNION ALL。六、索引少扫数据比“让 SQL 看起来短”更重要可以把索引理解成书的目录。没有目录时为了找一个用户名数据库可能要从第一行扫到最后一行有合适索引时可以快速定位目标范围。1. 为什么 InnoDB 常用 B 树B 树有几个适合磁盘和范围查询的特点分支多、树高低查找需要的页较少数据有序适合BETWEEN、、ORDER BY叶子节点相连连续读取范围数据更方便。Hash 更擅长等值定位但不适合范围和排序普通二叉树分支少数据量大时树会更高磁盘 I/O 更多。2. 聚簇索引和二级索引InnoDB 的主键索引叶子节点保存整行数据所以主键索引也叫聚簇索引。二级索引叶子节点保存的是索引列和主键值。查询二级索引未覆盖的其他列时通常还要拿主键回到聚簇索引再查一次这就是“回表”。3. 创建索引createindexidx_student_nameonstudent(name);createindexidx_score_course_scoreonscore(course_id,score);showindexfromscore;删除索引dropindexidx_student_nameonstudent;4. 联合索引的最左前缀索引createindexidx_student_class_nameonstudent(class_id,name);通常可以支持whereclass_id1whereclass_id1andname张三whereclass_id1andnamelike张%只按name查询时无法利用这棵联合索引的最左列进行有效定位wherename张三联合索引列的顺序要结合实际查询条件、选择性和排序需求设计不能只按字段出现顺序照抄。5. 常见的索引失效或收益很低的写法-- 对索引列做函数计算whereyear(created_at)2026-- 前导模糊匹配wherenamelike%三-- 隐式类型转换例如 phone 是字符串却拿数字比较wherephone13800000001-- 联合索引跳过最左列wherename张三日期查询可以改成范围wherecreated_at2026-01-01andcreated_at2027-01-016. 用 EXPLAIN 验证explainselects.name,sc.scorefromstudent sjoinscore sconsc.student_ids.idwheres.class_id1;重点观察type访问方式key实际选择的索引rows预计扫描行数Extra是否出现临时表、文件排序等信息。索引不是越多越好。每多一个索引插入、更新和删除时就多一份维护成本也会占磁盘空间。低区分度的小表字段建索引后可能几乎没有收益。七、事务让一组操作保持完整转账是最典型的事务场景A 扣款和 B 加款必须一起成功否则数据就不可信。1. ACID特性含义原子性 Atomicity一组操作全部成功或全部回滚一致性 Consistency事务前后都满足业务规则和约束隔离性 Isolation并发事务尽量互不干扰持久性 Durability提交后的结果不会因进程重启而丢失2. 一个更稳妥的转账示例starttransaction;selectid,balancefromaccountwhereidin(1,2)forupdate;updateaccountsetbalancebalance-100whereid1andbalance100;-- 应用程序需要检查上一条 UPDATE 是否确实影响了 1 行updateaccountsetbalancebalance100whereid2;commit;中途发生异常时rollback;FOR UPDATE会对目标记录加锁减少并发转账时互相覆盖的风险。锁定多行时所有事务最好按相同顺序访问账户降低死锁概率。3. 保存点starttransaction;updatescoresetscorescore5wherecourse_id1;savepointafter_mysql;updatescoresetscorescore5wherecourse_id2;rollbacktoafter_mysql;commit;保存点可以只撤销事务后半段但不能替代清晰的业务边界。4. 并发事务的三个问题脏读读到其他事务尚未提交的数据不可重复读同一事务里两次读取同一行结果不同幻读同一条件两次查询记录数量发生变化。5. 隔离级别隔离级别说明READ UNCOMMITTED隔离最弱可能脏读READ COMMITTED只能读已提交数据REPEATABLE READ同一事务内重复读取保持一致SERIALIZABLE隔离最强并发能力最低查看当前隔离级别selecttransaction_isolation;InnoDB 默认通常是REPEATABLE READ。隔离级别不是越高越好要在一致性要求、锁等待和并发量之间取舍。八、三个常见延伸点原稿里还有视图、权限和 JDBC这三部分可以先抓住最实用的规则。1. 视图保存查询逻辑不是复制一份数据createviewv_student_scoreasselects.idasstudent_id,s.nameasstudent_name,c.nameascourse_name,sc.scorefromstudent sjoinscore sconsc.student_ids.idjoincourse conc.idsc.course_id;查询方式与普通表类似select*fromv_student_scorewherescore90;复杂连接、聚合、DISTINCT等视图通常不能直接更新使用前要确认可更新性。2. 权限按最小权限分配createuserreport_user%identifiedbyreplace_with_strong_password;grantselectonschool_demo.*toreport_user%;showgrantsforreport_user%;报表账号只需要读权限就不要直接授予ALL PRIVILEGES。实际部署还要限制允许连接的主机并妥善管理密码。3. JDBC参数一定用 PreparedStatementStringsql select id, name, age from student where class_id ? and name like ? ;try(PreparedStatementpsconnection.prepareStatement(sql)){ps.setLong(1,classId);ps.setString(2,keyword%);try(ResultSetrsps.executeQuery()){while(rs.next()){System.out.println(rs.getLong(id));System.out.println(rs.getString(name));}}}不要用字符串拼接用户输入。PreparedStatement不只是写起来规整更重要的是把 SQL 结构和参数值分开降低 SQL 注入风险。

相关新闻

01序列判断:原理、实现与应用场景解析

01序列判断:原理、实现与应用场景解析

1. 项目概述"01序列判断"这个看似简单的概念,实际上在计算机科学和数据处理领域有着广泛的应用场景。作为一名从业多年的程序员,我经常需要在各种场景下处理这类二进制序列的判断问题。无论是网络协议解析、数据校验,还是算法竞赛中…

2026/10/15 7:39:21 阅读更多 →
TMS320C54x DSP时钟配置实战:PLL原理、CLKMD寄存器详解与避坑指南

TMS320C54x DSP时钟配置实战:PLL原理、CLKMD寄存器详解与避坑指南

1. 项目概述与核心价值在嵌入式DSP系统开发中,时钟配置往往是项目启动的第一步,也是最容易踩坑的一步。我接触TMS320C54x系列DSP已经超过十年,从早期的C541到后来的VC549都用过,可以说,一个稳定、高效的时钟系统是整个…

2026/10/14 3:22:12 阅读更多 →
生命涌现的小龙虾技能之【Outdoor Sports Event Risk Analysis Tool | 户外体育赛事风险分析工具】简介

生命涌现的小龙虾技能之【Outdoor Sports Event Risk Analysis Tool | 户外体育赛事风险分析工具】简介

🏃 Outdoor Sports Event Risk Analysis Tool | 户外体育赛事风险分析工具 智能分析中枢 图片/视频智能分析 结构化报告 历史报告云端查询 🧭 技能概览 | Overview 模块内容🏷️ 技能名称户外体育赛事风险分析工具🎯 核心目标…

2026/10/2 13:53:06 阅读更多 →

最新新闻

基于集群划分的分布式光伏配电网电压协调控制与Matlab实现

基于集群划分的分布式光伏配电网电压协调控制与Matlab实现

分布式光伏一多起来,配电网最先暴露问题的往往是电压。光照一强、负荷又轻的时候,馈线末端电压能一路冲到1.06甚至1.07标幺值,逆变器要么降出力、要么直接脱网保护,好好的发电量白白丢了。传统的变电站有载调压和固定电容器组又反…

2026/10/15 7:39:00 阅读更多 →
MEDS:基于Docker Compose的本地开发环境标准化方案

MEDS:基于Docker Compose的本地开发环境标准化方案

1. 项目概述:MEDS不是医疗系统,而是开发者私藏的“效率加速器”最近在几个技术社区里频繁刷到“MEDS”这个词,有人问“MEDS开源项目值不值得学”,也有人发帖说“搭完MEDS后本地开发流速翻倍”。但翻遍GitHub热门榜单和主流技术媒体…

2026/10/15 7:39:00 阅读更多 →
多Agent系统面试攻略:4种经典架构详解与避坑指南

多Agent系统面试攻略:4种经典架构详解与避坑指南

多 Agent 系统面试怎么答?我总结了 4 种经典架构这两年只要面试 AI 应用岗位,十个里面有八个会被问到“多 Agent 系统怎么设计”。我作为经常端坐在对面的人,说实话最怕听到的回答是:“这个简单,用 LangGraph 编排几个…

2026/10/15 7:39:00 阅读更多 →
MEDS:面向开发者的模块化本地工作台与服务编排工具

MEDS:面向开发者的模块化本地工作台与服务编排工具

1. 项目概述:MEDS不是医疗系统,而是开发者日常提效的“数字工作台”最近在几个技术社区和开源协作平台刷到一个叫MEDS的项目,标题里带“开源项目推荐”,但点进去发现它既不卖药也不做电子病历,和医院信息系统&#xff…

2026/10/15 7:39:00 阅读更多 →
Excel一键生成炫彩二维码:IMAGE函数与VBA实战教程

Excel一键生成炫彩二维码:IMAGE函数与VBA实战教程

上个月帮朋友准备展板物料,需求看着不复杂:几十个二维码,每个对应不同链接,但每个都得配合当天的主题色来展示,不能清一色黑乎乎。他那边既没有Python环境,也不方便一个个打开在线生成器,最后发…

2026/10/15 7:39:00 阅读更多 →
CentOS 7.6搭建DNS正向解析服务:BIND配置实战

CentOS 7.6搭建DNS正向解析服务:BIND配置实战

平时做运维的朋友肯定都有过这种经历:公司内部新起了一套系统,IP地址越来越多,今天要连这个、明天要连那个,总不能天天翻Excel表格。更尴尬的是,内网服务想用一个好记的域名访问,结果发现公共DNS根本不认识…

2026/10/15 7:37:59 阅读更多 →

日新闻

Docker Compose 容器化部署 ChirpStack LoRaWAN 网络服务器实战

Docker Compose 容器化部署 ChirpStack LoRaWAN 网络服务器实战

1. 为什么选择容器化方式部署 LoRa 网络服务LoRa 物联网项目落地时,网络服务器是整个链路的核心枢纽。它负责接收网关上报的数据包、完成去重与解密、再把有效载荷分发给上层应用。早期大家习惯把网络服务器直接装在物理机上,依赖一堆系统库和特定版本的…

2026/10/15 0:00:46 阅读更多 →
共享LLM服务下的跨模型自动扩缩容:Kubernetes实战与避坑指南

共享LLM服务下的跨模型自动扩缩容:Kubernetes实战与避坑指南

1. 共享推理服务里,扩缩容到底难在哪做过大模型推理服务部署的人,大概都经历过这样的场景:一个团队共用一套推理集群,A 业务跑的是 7B 的小模型,B 业务跑的是 72B 的大模型,C 业务白天流量高、晚上几乎归零…

2026/10/15 0:00:46 阅读更多 →
Systemd 启动阶段根文件系统只读之谜:fstab 与 remount-fs 服务深度解析

Systemd 启动阶段根文件系统只读之谜:fstab 与 remount-fs 服务深度解析

1. 引子:一次内核恐慌引发的追查先从一个真实场景说起。某次我给一台老旧的嵌入式设备折腾系统裁减,为了把启动时间压进两秒以内,我手贱地把一串自认为“没用”的 systemd 服务给 disable 了,其中就包括systemd-remount-fs.servic…

2026/10/15 0:00:46 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/14 3:30:28 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/14 16:30:29 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/14 3:29:58 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 0:14:36 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/14 22:45:47 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/14 7:53:34 阅读更多 →