持续更新,不重要的我不收集
MySQL 基础知识
什么是关系型数据库?
关系型数据库 RDB,就是一种建立在关系模型的基础上的数据库。
关系模型表明了数据库中所存储的数据之间的联系,例如一对一、一对多、多对多
关系型数据库中,我们的数据都被存放在了各种表中(比如用户表),表中的每一行就存放着一条数据(比如一个用户的信息)。
大部分关系型数据库都使用 SQL 来操作数据库中的数据。并且,大部分关系型数据库都支持事务的四大特性(ACID)。
有哪些常见的关系型数据库呢?
MySQL、PostgreSQL、Oracle、SQL Server、SQLite
什么是 SQL?
SQL 是一种结构化查询语言(Structured Query Language),专门用来与数据库打交道,目的是提供一种从数据库中读写数据的简单有效的方法。
几乎所有的主流关系数据库都支持 SQL ,适用性非常强。并且,一些非关系型数据库也兼容 SQL 或者使用的是类似于 SQL 的查询语言。
什么是元组, 码, 候选码, 主码, 外码, 主属性, 非主属性?
翻译:
元组:关系数据库中的基本单位,也就是一行记录。
每个元组包含了一个实体的完整信息。也就说的是一行记录包含这个实体的完整信息
码:字段,唯一标识关系中元组的一个或多个属性的集合。保证数据完整性唯一性
- 候选码:能唯一标识一行数据的最小字段集合。不止主键,只要能唯一标识一行数据的都算是候选码,例如学生的学号肯定也能唯一标识
- 主码:主键,每个关系只能有一个主码,但可以有多个候选码。
- 外码:外键,它对应另一个关系的主码。例如,在选课表中的”学号”如果引用学生表的主码”学号”,则选课表中的”学号”就是外码。
主属性:包含在任何一个候选码中的属性。例如,学生关系(学号,身份证号,姓名),那么学号和身份证号就是主属性
非主属性:不包含在任何候选码中的属性
数据库三大范式是什么?实际项目中如何平衡范式化和反范式化?
1NF 第一范式:要求表中的每个字段都是原子性的,不可再分,每个字段只包含一个值。
- 要求:字段不能是集合、数组、复合结构。
- 例子:不能把 “姓名 + 电话” 放一个字段,必须分开。
2NF 第二范式:在满足 1NF 基础上,且所有非主键字段都完全依赖于主键,消除部分函数依赖。
- 要求:非主键字段必须完全依赖整个主键,而不是依赖主键的一部分。
- 例子:联合主键表中,不能有只跟其中一个字段相关的数据。
3NF 第三范式:在满足 2NF 基础上,消除传递依赖,非主键字段不能依赖于其他非主键字段。
- 要求:非主键字段之间不能互相推导,只能直接依赖主键。
- 例子:订单表不要存 “客户姓名、客户地址”,只存客户 ID,通过关联查询获取。
范式化的优势是减少数据冗余、保证数据一致性、避免更新异常、便于维护,但会导致表结构复杂、查询需要多表连接、关联多、查询慢、join复杂。
反范式化正好相反,通过增加冗余数据来简化查询、提升性能,但会增加存储空间、维护复杂度上升、增加数据不一致风险。
实际项目中需要在数据一致性和查询性能之间找平衡。
OLTP 业务(增删改多,如订单、用户),优先考虑范式化设计,保证数据完整性和事务安全;
OLAP 业务(查询多,报表、统计分析、大数据量查询这种),它们查询通常比较复杂,而且数据更新频率较低,所以可适当的反范式化,搞点冗余字段、宽表、汇总表,简化查询逻辑,提升查询效率,优化查询性能
混合系统可以采用部分反范式化策略,在关键查询路径上增加冗余字段,以提升查询性能,同时保持大部分表结构的范式化。这种方法能够在数据一致性和查询性能之间找到合适的平衡点。例如,可以在订单表中冗余存储客户名称,以避免每次查询都需要连接客户表。
常用平衡范式化和反范式化的手段有这些
- 冗余少量常用字段:如订单表冗余存用户名,避免每次都 join 用户表。
- 中间表 / 统计表
- 用缓存替代反范式:热点数据放 Redis,既不破坏范式,又提升速度
1NF 是字段原子不可再分;
2NF 是消除部分依赖,非主键字段完全依赖主键;
3NF 是消除传递依赖,非主键字段只依赖主键。
实际项目里,我会优先按 3NF 设计,保证数据清晰、减少冗余、方便维护。
但在查询多、关联复杂、性能瓶颈的场景,会适度反范式,比如冗余常用字段、做宽表、建统计中间表,减少 join 提升速度。
整体原则是先范式保证结构合理,再根据性能适度反范式,不盲目冗余。
DML 语句和 DDL 语句区别是
- DML
数据库操作语言,是指对数据库中表记录的操作,主要包括表记录的插入、更新、删除和查询
- SELECT 查询一般单独拿出来叫 DQL
- DDL 数据库定义语言,是对数据库内部的对象进行创建、删除、修改的操作语言。它和 DML 语言的最大区别是 DML 只是对表内部数据的操作,而不涉及到表的定义、结构的修改,更不会涉及到其他对象。
额外还有 TCL,事务控制语言用于管理数据库中的事务,核心指令是
COMMIT、ROLLBACK。还有DCL 数据控制语言,控制用户的访问权限为主,它可以控制特定用户账户对数据表、查看表、预存程序、用户自定义函数等数据库对象的控制权。
MySQL有哪些常见存储引擎?InnoDB为什么成为默认引擎?如何选择合适的存储引擎?
MySQL常见的存储引擎包括InnoDB、MyISAM、Memory、Archive等。可以通过
SHOW ENGINES 命令来查看 MySQL 支持的所有存储引擎。
InnoDB 成为默认引擎是因为其完整支持事务处理、崩溃恢复、行级锁等企业级特性,能够保证数据一致性和并发安全。相比 MyISAM 的表级锁,InnoDB 的行级锁大大提升了并发性能,适合高并发的 OLTP 应用场景。同时 InnoDB 的 MVCC 机制支持非阻塞读操作,进一步提升了系统性能。
MySQL 的存储引擎是数据库系统的核心组件,它决定了数据的存储方式、索引结构、事务处理机制以及并发控制策略。不同的存储引擎针对不同的应用场景进行了优化
InnoDB之所以成为 MySQL 的默认引擎,主要得益于其在企业级应用方面的全面支持。
InnoDB 存储引擎是 MySQL 5.5 版本后的默认引擎
它采用了B+树索引结构和聚簇索引的设计理念。InnoDB 的数据文件本身就是索引文件,主键索引的叶子节点直接存储完整的数据行,这种设计使得基于主键的查询非常高效。
所有的存储引擎中只有 InnoDB 是事务性存储引擎,也就是说只有 InnoDB 支持事务。而且 InnoDB 对事务处理的支持还非常好,完整实现了ACID特性,能够保证数据的一致性、隔离性、持久性和原子性,通过MVCC(多版本并发控制)机制实现了非阻塞的读操作,而且 InnoDB 的两阶段提交能够保证主从分布式情况下的数据一致性,同时使用行级锁来保证并发安全。
InnoDB还提供了崩溃恢复功能,通过redo log、undo log 和 binlog 这三大日志来保证数据的一致性和持久性。
而且 InnoDB 支持外键约束,能够在数据库层面保证数据的引用完整性和数据一致性。外键约束会带来一定的性能开销。而且通常很多情况下,不建议在实际生产项目中使用外键的,在业务代码中进行约束即可。
阿里的《Java 开发手册》也是明确规定禁止使用外键的。
MyISAM存储引擎是MySQL早期版本的默认引擎,它采用了非聚簇索引的设计,数据文件和索引文件是分离的。MyISAM不支持事务处理,使用表级锁来控制并发访问,这在高并发场景下会成为性能瓶颈。但是MyISAM在全文索引方面有优势,对于需要全文搜索的应用场景仍然有其价值。MyISAM的存储结构相对简单,在只读或读多写少的场景下性能表现良好。
Memory存储引擎将数据完全存储在内存中,这使得它的读写速度非常快,但同时也带来了数据持久性的问题。Memory引擎不支持事务处理,使用哈希索引来加速查询。Memory引擎适合用作缓存表或临时表,比如存储会话信息、临时计算结果等。需要注意的是,Memory引擎的表大小受限于可用内存,重启后数据会丢失。
Archive存储引擎专门用于数据归档,它使用zlib压缩算法来压缩数据,压缩比可以达到1:10甚至更高。Archive引擎只支持INSERT和SELECT操作,不支持UPDATE和DELETE操作,这符合归档数据的特点。Archive引擎适合存储历史数据、日志数据等不需要频繁修改的数据。
选择合适的存储引擎需要综合考虑多个因素,包括应用类型、性能要求、数据一致性需求、存储空间、维护成本等。
但是,对于读密集的情况下是否使用 MyISAM 就更好?《MySQL 高性能》上面有一句话这样写到:
不要轻易相信“MyISAM 比 InnoDB 快”之类的经验之谈,这个结论往往不是绝对的。在很多我们已知场景中,InnoDB 的速度都可以让 MyISAM 望尘莫及,尤其是用到了聚簇索引,或者需要访问的数据都可以放入内存的应用。
MyISAM 和 InnoDB 有什么区别?
是否支持行级锁(是否支持 MVCC)
MyISAM 只有表级锁,而 InnoDB 支持行级锁和表级锁,默认为行级锁。也就说,MyISAM 一锁就是锁住了整张表,这也是为什么 InnoDB 在并发写的时候,性能更好!
毕竟 MyISAM 连行级锁都不支持。MVCC 可以看作是行级锁的一个升级,可以有效减少加锁操作,提高性能。
是否支持事务
MyISAM 不提供事务支持,InnoDB 提供事务支持,实现了 SQL 标准定义了四个隔离级别
是否支持外键
MyISAM 不支持,而 InnoDB 支持。
是否支持数据库异常崩溃后的安全恢复
MyISAM 不支持,而 InnoDB 通过 redo log 支持。
索引实现不一样。
虽然 MyISAM 引擎和 InnoDB 引擎都是使用 B+Tree 作为索引结构,但是两者的实现方式不太一样。
InnoDB 引擎中,其数据文件本身就是索引文件。相比 MyISAM,索引文件和数据文件是分离的,其表数据文件本身就是按 B+Tree 组织的一个索引结构,树的叶节点 data 域保存了完整的数据记录。
性能差别。
InnoDB 的性能比 MyISAM 更强大,不管是在读写混合模式下还是只读模式下,随着 CPU 核数的增加,InnoDB 的读写能力呈线性增长。MyISAM 因为读写不能并发,它的处理能力跟核数没关系
数据缓存策略和机制实现不同。
InnoDB 使用缓冲池(Buffer Pool)缓存数据页和索引页,MyISAM 使用键缓存(Key Cache)仅缓存索引页而不缓存数据页。
SQL
连接查询有哪几种?详细介绍一下他们的具体查询过程?项目中如何使用它们
连接查询的核心是基于两个或多个表的关联字段,筛选出满足条件的记录组合
内连接
INNER JOIN只返回两个表中满足连接条件的交集记录,不满足条件的记录会被过滤掉
1
2
3
4
5-- 查询用户表和订单表中,用户ID匹配的用户信息+订单信息
SELECT u.id, u.name, o.order_no, o.amount
FROM users u
INNER JOIN orders o
ON u.id = o.user_id;业务要求 必须有关联数据才展示,比如有任务进行中的小组,有库存的商品
内连接使用
INNER JOIN的表达的时候较少,大家都通常使用隐式内连接,使用,代替INNER JOIN1
SELECT u.id, u.name, o.order_no, o.amount FROM users u,orders o ON u.id = o.user_id;
外连接
OUTER JOIN外连接的核心是保留某一张表的全部记录,另一张表匹配不上则补 NULL,分为左外连接、右外连接、全外连接。
左外连接
LEFT OUTER JOIN保留左表的所有记录,右表匹配上则返回对应记录,匹配不上则右表字段补
NULL一般直接写
LEFT JOIN1
2
3
4
5-- 查询所有用户(包括无订单的用户),以及他们的订单信息(无订单则订单字段为NULL)
SELECT u.id, u.name, o.order_no, o.amount
FROM user u
LEFT JOIN order o
ON u.id = o.user_id;要求 展示主表全部数据,关联表可选 的时候用
右外连接
RIGHT OUTER JOIN保留右表的所有记录,左表匹配上则返回对应记录,匹配不上则左表字段补
NULL,同样直接写RIGHT JOIN与左连接相反,先取右表全部记录,遍历右表每条记录匹配左表,匹配不上则左表字段填
NULL1
2
3
4
5-- 查询所有订单(包括用户表中已删除的用户的订单),以及对应的用户信息(无用户则用户字段为NULL)
SELECT u.id, u.name, o.order_no, o.amount
FROM user u
RIGHT JOIN order o
ON u.id = o.user_id;较少直接使用,因为可通过调换表顺序转为左连接
全外连接
FULL OUTER JOIN保留左表和右表的所有记录,匹配不上的一侧字段补
NULL,MySQL 不原生支持,需用UNION模拟,Oracle/SQL Server 支持所以说一般是先执行左连接,再执行右连接,合并两步结果,去重后返回。
1
2
3
4
5
6SELECT u.id, u.name, o.order_no, o.amount
FROM user u
LEFT JOIN order o ON u.id == o.user_id
UNION
SELECT u.id, u.name, o.order_no, o.amount
RIGHT JOIN order o ON u.id == o.user_id;UNION 用于合并两个或多个 SELECT 查询的结果集,并默认去除重复行;若使用 UNION ALL 则保留重复行。
UNION 联合查询的多张表列数必须保持一致,字段类型也需要保持一致
交叉连接
CROSS JOIN返回两个表的笛卡尔积,即左表每条记录和右表每条记录组合,无连接条件,结果条数 = 表 A 行数 × 表 B 行数,慎用容易炸
1
2
3
4-- 生成用户表和商品表的笛卡尔积(慎用,数据量大时会爆)
SELECT u.id, u.name, p.id, p.product_name
FROM user u
CROSS JOIN product p;自连接
SELF JOIN不是独立的连接类型,而是表与自身进行连接,本质是内连接 / 左连接的特殊用法
将同一张表拆分为 虚拟表 A 和 虚拟表 B,按普通连接逻辑执行,所以需给表起不同别名
1
2
3
4
5-- 查询员工表中,每个员工的直属领导(领导也是员工,存在同一张表)
SELECT e.id AS emp_id, e.name AS emp_name, m.name AS manager_name
FROM employee e
LEFT JOIN employee m
ON e.manager_id = m.id;
分组是什么?GROUP BY 是如何进行分组的?GROUP BY 分组后,SELECT 字段有什么限制?为什么?
GROUP BY 是 SQL
中将数据集按照指定字段或者表达式的相同值,划分为多个子集的操作。
分组的核心目的是对「每个分组」而非「整个数据集」做聚合计算。
确定分组依据的数据源
先执行
FROM/JOIN得到原始数据集过滤数据
WHERE对原始数据集做 前置过滤,只保留符合条件的行
分组核心操作
GROUP BYMySQL 会先对分组字段进行排序,相同值的行被归为一组;
每个唯一的分组值对应一个「分组桶」,所有行被分配到对应的桶中。
分组聚合,过滤(聚合函数 /
HAVING)
GROUP BY
用于将查询结果按一个或多个字段分组,把相同值的行合并为一组,通常配合聚合函数
COUNT,SUM,AVG 做统计。
1 | -- 例如统计每个部门的人数和平均工资 |
要求 SELECT 后的字段必须是 GROUP BY
中的分组字段,或被聚合函数包裹,例如SELECT dept_id, COUNT(*) FROM emp GROUP BY dept_id,要是写成SELECT dept_id, name FROM emp GROUP BY dept_id
执行顺序:WHERE > GROUP BY > 聚合函数 > HAVING > ORDER BY > LIMIT
分组字段加索引是优化 GROUP BY 性能的关键,一般是给
GROUP BY 字段创建单列索引,或给
WHERE+GROUP BY
字段创建联合索引也可以,不过注意最左前缀原则,而且分组前常用
WHERE 提前筛选无效数据,避免对全表分组
MySQL 默认会对 GROUP BY 结果排序,可加 ORDER BY NULL
关闭排序,提升性能
WHERE 和 HAVING 的区别?HAVING 可以单独使用吗?
首先要记住 SQL 的执行流程
1 | FROM/JOIN → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT |
所以说 WHERE是分组(GROUP
BY)之前筛选,筛选的对象也就是分组之前的原始行数据
HAVING是分组(GROUP
BY)之后筛选,筛选对象也就是分组后的聚合结果,一般都是和
GROUP BY 连用。
WHERE不能使用聚合函数,因为还没分组,无聚合值,HAVING可以,它针对分组后的聚合结果
而且 WHERE可用于
SELECT/UPDATE/DELETE,HAVING需要配合GROUP BY用于
SELECT
例如,筛选 2024 年下单且总金额超过 1000 元的用户,先分组再筛聚合结果
1 | SELECT user_id, SUM(order_amount) AS total_amount |
语法上 HAVING 可以单独用,此时默认整表为一个分组,但业务上无意义、效率低,实际开发中必须配合 GROUP BY 使用;
DISTINCT 的作用?DISTINCT 和 GROUP BY 去重的区别?
两者都能去重,DISTINCT 等价于
GROUP BY 所有 SELECT 字段且无聚合函数;所以说,DISTINCT
用于返回唯一不同的值。它作用于所有列,也就是说所有列的值都相同才算相同。
GROUP BY 是分组 + 聚合统计,DISTINCT
就是去重
简单去重 DISTINCT 更简洁,优化器处理逻辑与
GROUP BY 基本一致;
1 | -- 等价的去重逻辑 |
EXISTS和IN子查询有什么区别?性能如何?如何选择合适的子查询方式?
EXISTS 和 IN 都是用于子查询的运算符,但执行逻辑不同。
EXISTS 关注的是 存在性,只要子查询返回至少一行结果就返回 TRUE,具有短路特性;IN 关注的是 包含性,检查主查询的值是否在子查询结果集中。
EXISTS 对外表的每一行执行子查询,IN 先执行子查询得到结果集再判断。
性能差异主要体现在执行方式和数据量影响上。EXISTS 通常在大表关联小表时性能更好,因为具有短路特性,找到匹配就停止;IN 在小表关联大表时可能更优,但需要处理 NULL 值。性能主要取决于数据量大小、索引使用情况、NULL 值处理等因素。
选择合适的子查询方式需要考虑数据量大小、索引情况、NULL 值处理需求。大表关联小表优先使用 EXISTS,小表关联大表可考虑 IN,但要注意 NULL 值问题。对于复杂查询,可以考虑使用 JOIN 替代子查询,或者使用临时表优化性能。
内连接和外连接有什么区别?ON 和 WHERE 条件在连接查询时候有什么不同?
内连接(INNER JOIN)只返回两个表中都存在匹配记录的行,基于连接条件进行数据匹配。
外连接包括左外连接(LEFT JOIN)、右外连接(RIGHT JOIN)和全外连接(FULL JOIN),会保留主表的所有记录,从对应的另一侧获取内容,即使从表中没有匹配的记录也会返回 NULL 值。
ON 条件在连接过程中执行,用于指定表之间的连接条件,决定哪些行会被连接。WHERE 条件在连接完成后执行,用于过滤连接后的结果集。这个执行时机的差异会导致不同的查询结果,特别是在外连接中,ON 和 WHERE 的位置会影响NULL值的处理,因为 NULL 值参与比较时结果总是 NULL
连接查询的性能优化需要合理选择连接类型、优化连接条件、使用适当的索引。内连接通常性能较好,外连接需要额外的 NULL 值处理。连接条件的列应该有索引,避免全表扫描。对于大表连接,可以考虑使用子查询或临时表优化。
COUNT (*)、COUNT (1)、COUNT (字段)
的区别?COUNT (NULL)
的结果是什么?(COUNT函数有哪些使用方式?性能有什么区别?如何优化COUNT查询?)
COUNT 函数是 SQL
中最常用的聚合函数之一,它用于统计表中的行数或特定列的非 NULL
值数量。COUNT
函数支持COUNT(*)、COUNT(1)、COUNT(主键)、COUNT(非主键)、COUNT(DISTINCT)等使用方式
COUNT(*)和COUNT(1)统计所有行数,包括 NULL
值,而且二者性能相同;(两者的执行机制在MySQL中是相同的,它们都会统计表中的所有行数,包括
NULL 值。而且这两种方式都会使用主键索引来获取行数)
COUNT(主键)统计非 NULL
的主键值,执行机制与COUNT(*)类似,但由于主键列不允许 NULL
值,所以不需要检查 NULL 值,性能略优于COUNT(*)
在InnoDB中,主键索引是聚簇索引,数据行直接存储在索引的叶子节点中,这使得基于主键的统计非常高效。
COUNT(非主键)只统计该列非 NULL
的行数,需要检查指定列是否为 NULL 值,性能稍差
COUNT(DISTINCT)统计去重后的非 NULL
值数量,它需要先收集所有不重复的值,然后统计这些值的数量,性能肯定最差了。
IN 和 EXISTS 的区别?性能如何?什么时候用 IN,什么时候用 EXISTS?
EXISTS 和 IN 都是用于子查询的运算符,但是执行逻辑不同。
EXISTS 关注的是 存在性,只要子查询返回至少有一行结果就返回 TRUE,因为其布尔判断所以具有短路特性,一旦找到匹配的值就会停止然后返回,这样它在大表关联小表的时候性能好
IN 关注的是包含性,先执行子查询得到结果集,通常去重,再检查主查询的值是否在子查询的结果集中。IN 还需要处理 NULL 值,如果子查询的结果集中包含 NULL,整个 IN 表达式的结果就可能是 NULL 而不是 TRUE 或者 FALSE。IN 的子查询不关联外表字段,因此也叫非相关子查询;
EXISTS 对外表每一行都执行子查询。IN 先执行子查询得到结果集再判断。
所以说,性能这一块
内表大外表小,子查询结果集大 → 优先用 EXISTS
IN 会先把大的子查询结果集加载到内存,性能损耗大
EXISTS 对外表(小表)逐行执行子查询,范围小,而且且有短路特性,只要找到匹配项就停止,性能好
1
2
3
4
5
6
7-- 外表user(100行),子查询order(100万行)
SELECT * FROM user u
WHERE EXISTS (SELECT 1 FROM ORDER o WHERE o.user_id = u.id);
-- 不推荐 IN(子查询先查100万行order的user_id,生成大结果集)
SELECT * FROM user u
WHERE u.id IN (SELECT user_id FROM order o);内表小外表大,子查询结果集小 → 优先用 IN
IN 对内表子查询,结果集小,后续外表(大表)遍历就轻松,性能好
若用 EXISTS,对外表(大表)逐行执行子查询,即使有短路,次数也因为外表大而非常多
1
2
3
4
5
6
7
8-- 外表order(100万行),子查询user(100行,仅筛选VIP用户)
-- 推荐 IN
SELECT * FROM order o
WHERE o.user_id IN (SELECT id FROM user WHERE is_vip = 1);
-- 不推荐 EXISTS(需执行100万次子查询)
SELECT * FROM order o
WHERE EXISTS (SELECT 1 FROM user u WHERE u.id = o.user_id AND u.is_vip = 1);子查询结果集有 NULL → IN 会出逻辑问题,EXISTS 不受影响
IN:若子查询结果集包含 NULL,
字段 IN (结果集)会返回 NULL 而非 FALSE,导致该行被丢弃,逻辑错误;EXISTS:仅判断「是否有结果」,子查询结果含 NULL 不影响存在性判断;
EXISTS 子查询中的关联字段必须做索引,否则走全表性能很差
IN 子查询的返回字段建议加索引,加速子查询执行;外表的判断字段也建议加索引,加速包含判断。
呃呃,EXISTS 子查询里写 SELECT * 和
SELECT 1 没区别,查一条和查全部,SELECT 1
更直观我更习惯罢了
而 NOT IN
性能极差,无法短路,需遍历全部结果集,且子查询含 NULL
时会返回全空,NOT EXISTS
就很好了,确保子查询关联字段有索引就能保证较好性能
SQL 中 select、from、join、where、group by、having、order by、limit 的执行顺序是什么?
SQL 的执行顺序和书写顺序完全不一样
执行顺序是:FROM → JOIN → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT
所以说,WHERE 里不能用 SELECT 里定义的别名,因为此时还没有进行返回
UNION 和 UNION ALL有什么区别?性能如何?什么时候使用哪种方式?两者对 NULL 的处理是怎么样的?
UNION 和 UNION ALL 都是用于合并多个查询结果集的集合操作符。UNION 会自动去重,对结果集进行去重和排序操作;UNION ALL 保留所有记录包括重复的,直接合并结果集。
由于 UNION 需要额外的去重和排序处理,性能相对较差;UNION ALL 直接合并,性能更好。
选择 UNION 还是 UNION ALL 主要取决于业务需求。如果业务逻辑允许重复记录存在,应该使用 UNION ALL 获得更好的性能;如果必须去除重复记录,则使用 UNION。
对于对 NULL 的处理,UNION 视两个 NULL 为相等,会合并重复的 NULL,UNION ALL 就还是直接合并,保留所有 NULL,不做任何去重操作
MySQL 数据类型
整数类型的 UNSIGNED 属性有什么用?
MySQL 中的整数类型可以使用可选的 UNSIGNED 属性来表示不允许负值的无符号整数。在不需要存储复数值的情况下,使用 UNSIGNED 属性可以将正整数的上限提高一倍。
对于从 0 开始递增的 ID 列,使用 UNSIGNED 属性可以非常适合,因为不允许负值并且可以拥有更大的上限范围,提供了更多的 ID 值可用
NULL
和 ’’ 的区别是什么?为什么 MySQL 不建议使用 NULL
作为列默认值?
NULL 和 '' (空字符串)
是两个完全不同的值,它们分别表示不同的含义,并在数据库中有着不同的行为。
NULL 代表缺失或未知的数据,而 ''
表示一个已知存在的空字符串。它们的主要区别如下:
语义上:
NULL代表一个不确定的值,意味着缺失或未知的信息,它不等于任何值,包括它自身。因此,
SELECT NULL = NULL的结果是NULL,而不是true或false,意思是两个未知的值无法判定是否相等。 虽然NULL不等于任何值,但在某些操作中,数据库系统会将NULL值视为相同的类别进行处理,例如:DISTINCT,GROUP BY,ORDER BY。需要注意的是,这些操作将NULL值视为相同的类别进行处理,并不意味着NULL值之间是相等的。 它们只是在特定操作中被特殊处理,以保证结果的正确性和一致性。 这种处理方式是为了方便数据操作,而不是改变了NULL的语义。''表示一个空字符串,它是一个已知的值,仅代表内容为空,空字符串会被视为普通值,受唯一约束限制。
存储空间:
NULL的存储空间占用取决于数据库的实现,通常需要一些空间来标记该值为空。''的存储空间占用通常较小,因为它只存储一个空字符串的标志,不需要存储实际的字符。
比较运算:
- 任何值与
NULL进行比较(例如=,!=,>,<等)的结果都是NULL,表示结果不确定。要判断一个值是否为NULL,必须使用IS NULL或IS NOT NULL。 ''可以像其他字符串一样进行比较运算。例如,'' = ''的结果是true。
- 任何值与
聚合函数:
- 大多数聚合函数(例如
SUM,AVG,MIN,MAX)会忽略NULL值。 COUNT(*)会统计所有行数,包括包含NULL值的行。COUNT(列名)会统计指定列中非NULL值的行数。- 空字符串
''会被聚合函数计算在内。例如,SUM会将其视为 0,MIN和MAX会将其视为一个空字符串
- 大多数聚合函数(例如
至于 MySQL 不建议使用 NULL 作为列默认值,因为
- 开发中若列默认值为 NULL,写 SQL
时必须额外处理
IS NULL/IS NOT NULL,若疏忽用了= NULL,会导致查询结果为空,出现不易排查的逻辑错误;而默认值为空字符串,可直接用常规比较运算符,代码和 SQL 更简洁,降低出错概率。 - 而且 InnoDB 的索引中会存储 NULL 值的标记,若索引列包含大量 NULL,会增大索引体积,降低索引的缓存命中率和查询效率;对于复合索引,若某列默认值为 NULL,该列的选择性会变得非常差,会导致索引的选择性下降,优化器可能就因此放弃使用索引,走全表扫描
- 因聚合函数会忽略 NULL,若列默认值为
NULL,做统计时需用
IFNULL/COALESCE函数做兜底处理,让 SQL 更冗长;而默认值为 ’’,可直接使用聚合函数,无需额外处理。 - 若联表的关联列包含 NULL,因
NULL JOIN NULL的结果为 NULL,会导致关联查询丢失部分数据,需额外做 NULL 值的兜底匹配,而默认值为 空字符串 的关联列,联表规则清晰,无此问题。
在实际数据库设计中,要尽量用确定值替代 NULL
正确处理NULL值需要使用专门的函数和操作符。IS NULL/IS NOT NULL用于判断NULL值,COALESCE
返回第一个非 NULL 值,IFNULL
提供默认值,NULLIF
用于条件判断。在设计表结构时应该明确字段是否允许
NULL,在查询时正确处理 NULL 值。
CHAR和VARCHAR有什么区别?如何选择字符串类型?存储空间如何计算?
CHAR是定长字符串类型,无论实际内容多长都会分配固定的存储空间,不足部分用空格填充。VARCHAR是变长字符串类型,只分配实际内容所需的存储空间,但需要额外的长度前缀来记录字符串长度。
CHAR查询性能稍好但可能浪费存储空间,VARCHAR存储效率高但有长度计算开销。
CHAR适用于长度固定或变化很小的字符串,如国家代码、性别、状态标识等。VARCHAR适用于长度变化较大的字符串,如用户名、地址、描述信息等。选择哪种类型需要考虑数据长度特点、查询频率、存储空间成本等因素。如果某个字段经常用于查询条件或排序操作,应该优先考虑使用CHAR,因为CHAR的查询性能更好。如果某个字段主要用于存储,很少用于查询,可以考虑使用VARCHAR来节省存储空间。
具体的性能特点如下
CHAR的性能特点主要体现在查询性能方面。由于CHAR是定长存储,数据在磁盘上的位置是固定的,读取时不需要额外的长度计算,因此查询性能相对较好。CHAR还适合进行字符串比较操作,因为所有字符串都是相同长度,比较操作更加高效。但是,CHAR可能会浪费存储空间
VARCHAR的性能特点主要体现在存储效率方面。VARCHAR只存储实际需要的空间,能够有效节省存储空间。对于长度变化较大的字符串,VARCHAR的存储效率明显优于CHAR。但是,VARCHAR在查询时需要额外的长度计算,可能会影响查询性能。VARCHAR还可能导致行碎片,因为变长存储可能导致数据在磁盘上的存储不连续。
存储空间计算方面,CHAR固定分配指定长度的空间,VARCHAR需要额外1-2字节的长度前缀。CHAR在内存中按最大长度分配空间,VARCHAR按实际长度分配。对于短字符串,CHAR可能更高效;对于长字符串,VARCHAR通常更节省空间。
CHAR的存储空间计算相对简单,存储空间等于定义的长度乘以字符集编码的字节数。例如,CHAR(10)在UTF8字符集下占用30个字节(10个字符 × 3字节/字符),无论实际存储的内容是什么。CHAR的存储空间是固定的,不会因为实际内容的变化而变化。
VARCHAR的存储空间计算相对复杂,需要考虑实际字符串长度、长度前缀和字符集编码。VARCHAR的存储空间等于长度前缀的字节数加上实际字符串内容的字节数。长度前缀的字节数取决于字符串长度:如果长度小于等于255,使用1个字节;如果长度大于255,使用2个字节。实际字符串内容的字节数等于字符数乘以字符集编码的字节数。
为什么不推荐使用 TEXT 和 BLOB?如果需要存储对应的内容,可以采用什么方案
TEXT(长文本)和 BLOB(二进制大对象)是 MySQL 用于存储大尺寸数据的类型
TEXT/BLOB 存储的是大尺寸数据,会快速增大单表数据量,提前到达数据库瓶颈,这可能导致表上的 DML 操作变慢,而且 TEXT/BLOB 增删改操作会产生大量的磁盘 IO 和网络带宽,不仅导致自身的检索效率低,而且还会拖慢数据库整体性能
而且使用 TEXT/BLOB 字段,在做联表查询、分页查询时也会使得数据库压力非常大,还会让运维成本倍增。
InnoDB 中,小尺寸的 TEXT/BLOB 会存在行内,大尺寸会被溢出到行外的溢出页,查询时需要先查主表再跳转到溢出页读取数据,触发随机 IO,相比 VARCHAR/CHAR 的顺序 IO,查询效率骤降;且这类字段无法被高效缓存,会占用大量缓冲池(Buffer Pool),挤占普通字段的缓存空间,导致整个数据库的缓存命中率降低
TEXT/BLOB 无法直接创建普通索引,必须指定前缀长度来创建前缀索引,而前缀索引的选择性差,查询时回表率高,慢 SQL 概率大幅增加;而且它们也不支持覆盖索引,即使查询字段包含在索引中,也必须回表读取完整的 TEXT/BLOB 数据,进一步降低查询效率;
这其实也从另一方面映射了,为什么在给数据库做索引的时候,会避免给大字段建索引。
TEXT/BLOB 还有一些其他的缺点,包括不能有默认值,在使用临时表时无法使用内存临时表,只能在磁盘上创建临时表
如此多的缺点,决定了在面临问题的时候绝对有更多更好的解决方案
存储二进制文件这种对象文件,我的做法是使用了用对象存储 + 数据库存文件引用,我的项目中二进制文件全部存储在 COS 或者 MinIO 中,MySQL/PostgreSQL 中仅存储文件的各种元数据
存储超长结构化文本,我的做法是用MongoDB + 数据库存关联 ID,如果还涉及到对这些超长文本进行全文检索的需求,就按照需求的情况考虑是否引入 ES ,把检索逻辑剥离出关系库,提升查询效率。
Boolean 类型如何表示?
MySQL 中没有专门的布尔类型,而是用 TINYINT(1)
类型来表示布尔值。TINYINT(1) 类型可以存储 0 或 1,分别对应
false 或 true
PostgreSQL 中 内置了 boolean(也可简写为
bool)类型,是专门用于存储布尔值的原生类型,所以不用像
MySQL 那样用整数模拟。
而 PostgreSQL 的布尔类型仅占用 1 个字节,和 MySQL 的
TINYINT(1) 存储效率一致,但语义更清晰。如果从 MySQL
迁移数据,0/1 可以直接插入 PostgreSQL
的布尔字段,会自动转为
false/true,无需额外转换。但是注意布尔类型允许为
null,判断时需注意 is null/is not null
在 MySQL 中存储金额数据,应该使用什么数据类型?
金额存储业界主流两种方案:DECIMAL 或 BIGINT 存分。绝对不能用 FLOAT/DOUBLE,浮点数有精度丢失问题。
DECIMAL 是定点数,精度可控,比如 DECIMAL(18,2) 表示最多
18 位数字,其中 2 位小数。而且能再 Java 代码里用 BigDecimal
直接对应,天然支持高精度运算。
BIGINT 存分是把金额乘以 100 变成整数存储,比如 99.99 元存成 9999 分,这样就能以整数形式存储。Java 代码里用 long 对应,只不过在展示 / 计算时别忘了转换
DATETIME 和 TIMESTAMP 的区别是什么?如何选择?为什么说不要用字符串存储日期?
DATETIME 类型没有时区信息,TIMESTAMP
和时区有关。
TIMESTAMP 只需要使用 4 个字节的存储空间,但是
DATETIME 需要耗费 8
个字节的存储空间。但是,这样同样造成了一个问题,TIMESTAMP
表示的时间范围更小。
DATETIME:1000-01-01 00:00:00.000000到9999-12-31 23:59:59.999999TIMESTAMP:1970-01-01 00:00:01.000000 UTC到2038-01-19 03:14:07.999999 UTC
TIMESTAMP 的核心优势在于其内建的时区处理能力,数据库负责
UTC 存储和基于会话时区的自动转换,简化了需要处理多时区应用的开发。
如果应用需要处理多时区,或者希望数据库能自动管理时区转换,TIMESTAMP
自然是很好的选择。
如果应用场景不涉及时区转换,或者希望应用程序完全控制时区逻辑,并且需要表示
2038 年之后的时间,DATETIME 是更稳妥的选择。
尝试使用字符串,如 VARCHAR
类型来存储日期和时间,甚至一度认为这是一种简单直观的方法。毕竟,YYYY-MM-DD HH:MM:SS
这样的格式看起来清晰易懂。
但是,这是不正确的做法,主要会有下面两个问题:
- 空间浪费:明明有 MySQL 内建的日期时间类型,VARCHAR 通常会更长,占用更多的存储空间来表示相同的时间信息。
- 查询与计算效率低下:
- 比较操作复杂且低效:基于字符串的日期比较需要按照字典序逐字符进行,这不仅不直观,而效率远低于 MySQL 原生日期时间类型进行的数值或时间点比较。
- 计算功能受限:无法直接利用数据库提供的丰富日期时间函数进行运算
- 索引性能不佳:基于字符串的索引在处理范围查询时,其效率和灵活性通常不如原生日期时间类型的索引
手机号存储用 INT 还是 VARCHAR?
存储手机号,强烈推荐使用 VARCHAR 类型,而不是 INT 或 BIGINT。主要原因如下:
格式兼容性与完整性:
电话号可能存在一些 INT 类型存储困难的情况,例如某些地区的固话区号(前导零),国家代码前缀(
+86),如果是座机或传真还会有分隔符(-)。INT 或 BIGINT 这种数字类型会自动丢失这些重要的格式信息(比如前导零会被去掉,+和-无法存储)。VARCHAR可以原样存储各种格式的号码,无论是国内的 11 位手机号,还是带有国家代码的国际号码,都能完美兼容。非算术性:
手机号虽然看起来是数字,但我们从不对它进行数学运算(比如求和、平均值)。它本质上是一个标识符,更像是一个字符串。用
VARCHAR更符合其数据性质。查询灵活性:
- 业务中常常需要根据号段(前缀)进行查询,例如查找所有 “138”
开头的用户。使用 VARCHAR 类型配合
LIKE '138%'这样的 SQL 查询既直观又高效(前缀索引)。 - 如果使用数字类型,进行类似的前缀匹配通常需要复杂的函数转换(如
CAST或SUBSTRING),或者使用范围查询(如WHERE phone >= 13800000000 AND phone < 13900000000),这不仅写法繁琐,而且可能无法有效利用索引,导致性能下降。
- 业务中常常需要根据号段(前缀)进行查询,例如查找所有 “138”
开头的用户。使用 VARCHAR 类型配合
符号加密存储和数据脱敏的要求:
出于数据安全和隐私合规的要求,手机号这类敏感个人信息通常必须加密存储在数据库中。加密后的数据(密文)是一长串字符串。INT 或 BIGINT 类型根本无法存储这种密文。只有 VARCHAR、TEXT 等字符串类型可以。
MySQL 实际应用
我们要进行几张表的联查,那么如何进行联查,联查过程中如何筛选?联查的性能如何保证?
多表联查的本质是基于关联字段将多张表的数据拼接成逻辑上的一张表,一般是 JOIN 联查和子查询两类
连接查询
各种各样的连接查询,使用
JOIN,INNER JOIN,LEFT JOIN,RIGHT JOIN,FULL JOIN(LEFT JOIN + UNION + RIGHT JOIN)等连接查询来做,其中使用ON设置关联条件,指定两张表的关联字段,WHERE是筛选条件用于过滤最终结果子查询
子查询是将一个查询结果作为另一个查询的条件 / 数据源,分为「相关子查询」和「非相关子查询」
1
2
3
4
5-- 非相关子查询:先查所有VIP用户ID,再查这些用户的订单(执行一次子查询)
SELECT * FROM order WHERE user_id IN (SELECT id FROM user WHERE is_vip = 1);
-- 相关子查询:逐行关联(性能差,慎用),查每个用户的最新订单
SELECT o.* FROM order o WHERE o.create_time = (SELECT MAX(create_time) FROM order WHERE user_id = o.user_id);子查询多数情况下性能不如 JOIN,尤其是相关子查询,优先用 JOIN。
而性能优化的核心逻辑是减少数据库的 数据扫描量 和 数据处理量
给关联字段 / 筛选字段加索引是最重要的优化方式
可以考虑添加联合索引,注意覆盖的情况,尽量使用覆盖索引,避免 回表查询。
SQL 优化
优先用 INNER JOIN,避免 LEFT JOIN,而且优先用 JOIN,慎用子查询
减少联查表的数量,拆分复杂联查
其他层次缓存热点数据等等啊
为什么不推荐使用外键与级联?
对于外键和级联,阿里巴巴开发手册这样说到:
【强制】不得使用外键与级联,一切外键概念必须在应用层解决。
说明: 以学生和成绩的关系为例,学生表中的 student_id 是主键,那么成绩表中的 student_id 则为外键。如果更新学生表中的 student_id,同时触发成绩表中的 student_id 更新,即为级联更新。外键与级联更新适用于单机低并发,不适合分布式、高并发集群;级联更新是强阻塞,存在数据库更新风暴的风险;外键影响数据库的插入速度
- 增加了复杂性:每次更新或者删除都要考虑外键约束,虽然可以设置成一个相对简单的级联情况,但是也还是会牵扯
- 不容易调整:外键的主从关系是定的,假如哪天需求有变化,数据库中的这个字段根本不需要和其他表有关联的话就会增加很多麻烦。
- 增加了额外工作:数据库需要增加维护外键的工作,比如当我们做一些涉及外键字段的增,删,更新操作之后,需要触发相关操作去检查,保证数据的一致性和正确性,这样会不得不消耗数据库资源,性能也会受损。如果在应用层面去维护的话,可以减小数据库压力;
- 对分库分表不友好:因为分库分表下外键是无法生效的
但是,不要一股脑的就抛弃了外键这个概念,既然它存在就有它存在的道理,如果系统不涉及分库分表,并发量不是很高的情况还是可以考虑使用外键的
数据库设计当中外键的作用,有一对一的关系吗?
外键是数据库表之间的约束关系,用来保证数据的完整性、一致性,防止无效 或者 脏数据,保证数据完整性。
保证引用完整性:
子表中的数据必须依赖主表存在,不能出现 “无中生有” 的关联数据。
防止误删 / 脏数据:
如果主表记录被其他表引用,数据库可以使用外键约束会阻止删除,避免业务逻辑错乱。
明确表关系:
让表与表之间的业务关联一目了然,方便团队协作、维护、文档化。
支持级联操作
可以配置
ON DELETE CASCADE等,主表删除时自动清理子表关联数据。
数据库中一对一的关系非常常见,A 表的一条记录,最多对应 B 表的一条记录;B 表一条也最多对应 A 表一条。
外键主要是用来保证数据库的引用完整性和数据一致性,防止产生无效关联数据,也能防止误删被引用的数据,同时明确表之间的业务关系,支持级联操作简化维护。
一对一关系是存在的,而且很常用。它表示一张表的一条记录最多对应另一张表的一条记录,例如,项目表 project 和任务表 task 是一对多,我用外键保证任务必须归属某个项目,避免出现无主任务,保证数据一致性。用户表 user 和用户详情表 user_info,我设计成一对一关系。
什么是存储过程?为什么不推荐使用存储过程?
存储过程是预编译并存储在数据库中的一组 SQL 语句集合,,形成一个可重复调用的数据库对象,它可以接收参数、执行逻辑、返回结果,本质是数据库端的可编程函数 / 脚本
对于预编译,首次执行后,执行计划缓存到数据库,后续调用无需重新解析;
1 | -- 创建存储过程:查询指定部门的员工数 |
在传统企业级应用中,存储过程具有一定的实用价值。当业务逻辑复杂时,需要执行大量 SQL 语句才能完成一个业务操作,此时可以将这些语句封装成存储过程,简化调用过程。由于存储过程在创建时就已经编译并存储在数据库中,执行时无需重新编译,因此相比动态 SQL 语句具有更好的执行性能。同时,一旦存储过程调试完成,其运行相对稳定可靠。
然而,在现代互联网架构中,存储过程的使用越来越少。它调试困难,修改业务逻辑直接涉及到数据库对象,移植性差,不同数据库系统的存储过程语法差异较大,无法协作,无法水平扩展,分库分表不兼容,云原生不友好
将业务逻辑放在应用层实现,保持数据库的简单和高效是我们的常用做法
什么是分库分表,MySQL何时分库分表?
分库分表是 MySQL 应对大数据量、高并发的核心水平扩展方案,是为了打破单库单表的存储和性能瓶颈,将原本存储在一个数据库或者数据表中的数据,按照指定规则拆分到多个数据库、多张数据表中,这样能够很好的分散数据存储压力和查询压力,提升系统的吞吐量和响应速度。
- 分库:把一个数据库的内容拆分到多个独立的数据库(可部署在不同服务器),解决单库的存储上限、IO 瓶颈、连接数限制问题;
- 分表:把一个大表的内容拆分到同一个数据库的多张表中,解决单表数据量过大导致的索引失效、查询缓慢、DDL 操作阻塞等问题;
拆分规则主流为水平拆分(按数据属性,如用户 ID 哈希、时间范围拆分,最常用),少量场景用垂直拆分(按业务模块,如把订单表拆分为订单基础表、订单物流表)。
单库单表的性能 / 存储达到 MySQL 自身瓶颈时,就需要考虑分库分表,具体需要根据明确的量化指标和业务场景触发点
- 单表数据量触达阈值:InnoDB 引擎下,单表数据量超过 500 万 - 1000 万行,会出现索引查询效率骤降、分页 / 联表查询缓慢、DDL操作长时间阻塞的问题
- 单库存储或者性能达到瓶颈:单库磁盘很大十几几十个G,或者日常出现高 IO、高 CPU、连接数占满,常规优化达到上限水平,需要考虑分库分表
- 业务高并发压力:单库单表面对每秒数千甚至上万的读写请求(尤其是写请求),出现事务提交慢、请求超时、数据不一致的情况;
- 业务快速扩张:提前做架构规划,比如面向百万 / 千万级用户的 ToC 产品,预判后续数据量和并发会快速增长,避免后期单库瓶颈导致的架构重构。
分库分表是最后一步的优化方案,在此之前必须先做基础优化(索引优化、SQL 调优、读写分离、缓存 Redis 热点数据、主从复制),基础优化无效后再考虑分库分表。
结合项目就这样回答
我在主导智研科研项目管理平台时,目前暂未直接落地分库分表,但在项目架构设计阶段,我已经做了分库分表的前置规划和技术预留
- 项目现阶段是面向高校科研团队、心理健康小程序的垂直领域产品,用户量和数据量尚未触达 MySQL 单库单表的瓶颈,通过基础优化即可满足性能需求:比如用 Redis 缓存热点数据,做慢 SQL 优化,Spring Cache ,Sentinel 做流量控制
- 虽然未落地,但我在项目中做了关键的技术预留,为后续业务扩张后的分库分表铺路,比如设计数据库的时候使用了分布式ID,表现就是雪花算法生成ID,表设计时考虑了部分反范式化来避免了跨表的强联表查询,降低后续拆分的复杂度。
分库分表的拆分规则是什么
拆分规则核心分水平拆分和垂直拆分
水平拆分
按数据属性进行拆分,表结构一致,将同一表的不同数据行拆分到不同库 / 表
有这样几种方式
- 哈希分片(取模,一致性哈希):例如按用户 ID、项目 ID、业务主键等唯一标识做哈希取模,这样数据分布均匀,分散读写压力很明显,一般对用户中心、项目管理、社区互动等无明显时间特征、读写均衡的业务,分片键选查询高频的唯一键,一般是ID,比如按用户 ID 拆分后,查询某用户的所有记录无需跨库。
- 时间范围分片:按创建时间、业务时间拆分,符合业务按时间查询的习惯,一般是对日志表、操作记录表、AI 交互记录这种写多读少的业务
垂直拆分
按业务模块或者字段属性拆分,表结构不同,分库级垂直拆分和表级垂直拆分,通过拆分低耦合的业务或者字段,减少单表字段数和单库业务压力:
- 表级垂直拆分:将一张大表按字段 冷热 拆分,比如订单表拆为订单基础表(订单 ID、金额、状态,高频查询,属于热表)和订单详情表(收货地址、备注,低频查询,属于冷表);
- 库级垂直拆分:按业务模块拆分数据库,一般是微服务架构中的做法,例如将 用户中心、项目管理、AI 交互 拆为独立数据库
能水平不垂直,能分表不分库,最小化拆分,仅对核心大表做水平拆分
分库分表会涉及到什么问题
分库分表是一把双刃剑,拆分后会引入分布式场景的共性问题
分布式主键冲突:需要考虑分布式ID
在分库分表环境下,传统的自增ID无法保证全局唯一性,需要专门的全局ID生成策略。雪花算法是最流行的方案,它基于时间戳、机器ID和序列号生成64位的唯一ID。雪花算法生成的ID天然有序,便于数据库索引,但需要解决时钟回拨问题。
分布式事务一致性:跨库 / 跨表操作时也会导致分布式事务,需要引入Seata等分布式事务解决方案
TCC模式(Try-Confirm-Cancel)是一种更为灵活的方案,它将事务分为尝试、确认、取消三个阶段。应用需要为每个操作实现对应的三个方法,虽然增加了开发复杂度,但提供了更好的性能和可控性。
本地消息表和分布式消息方案基于最终一致性思想,通过消息机制来保证分布式事务。这类方案的优势是性能好、可扩展性强,但需要业务能够容忍短暂的数据不一致。
Saga模式是长事务的解决方案,它将长事务拆分为多个短事务,每个短事务都有对应的补偿操作。当某个步骤失败时,执行补偿操作来保证数据一致性
跨库查询问题
传统的JOIN查询在分库分表环境下变得极其复杂,因为相关数据可能分布在不同的数据库实例中。应用层关联是最常用的解决方案,通过多次查询获取数据后在应用层进行关联。虽然增加了代码复杂度,但给了应用更多的控制权,可以根据业务需求进行优化。
数据冗余是另一种有效方案,通过在不同的分片中冗余关联数据来避免跨库查询。比如在订单表中冗余用户的基本信息,避免每次查询都要关联用户表。这种方案提高了查询性能,但需要解决数据一致性问题,通常采用最终一致性模型。
对于复杂的多维度查询,搜索引擎如 Elasticsearch 成为了重要的补充。将需要复杂查询的数据同步到搜索引擎中,利用其强大的查询能力解决跨库查询问题。这种方案特别适合报表查询、全文搜索等场景。
数据迁移复杂,运维复杂度提升
MySQL的读写分离该如何实现?
MySQL 读写分离的核心是「读请求走从库,写请求走主库」,通过主从复制同步数据,实现读写负载分担;
实现方式分 “应用层方案” 和 “中间件方案”,前者简单易上手,后者适合高并发场景。
但读写分离会引入数据一致性、主从延迟、故障切换等问题,需要针对性解决。
应用层实现
基于代码 + 数据库驱动实现
- 搭建主从复制的架构
- 主库(Master)开启 binlog 日志,记录所有写操作;
- 从库(Slave)通过 IO 线程读取主库 binlog,SQL 线程重放日志,同步数据;
- 配置:主库
my.cnf开启log-bin,从库配置master_host/master_log_file等参数。
- 搭建主从复制的架构






