Skip to content

高性能学习笔记

按读写划分

  1. 共享锁 (Shared Locks, S Lock / 读锁)

    • 获取方式: SELECT ... LOCK IN SHARE MODE;
    • 特点: 多个事务可以对同一行数据加S锁(共享)。S锁会阻塞其他事务对同一行加排他锁 (X锁),但不阻塞其他事务加S锁或进行普通SELECT(快照读)。
  2. 排他锁 (Exclusive Locks, X Lock / 写锁)

    • 获取方式: SELECT ... FOR UPDATE;UPDATEDELETEINSERT(隐式)。
    • 特点: 一个事务对一行数据加X锁后,其他任何事务都不能再对该行加S锁或X锁,直到该事务释放锁。X锁是互斥的。

按颗粒度划分

  1. 全局锁 (Global Lock)

    • 作用范围: 整个MySQL实例的所有数据库、所有表。
    • 实现方式: FLUSH TABLES WITH READ LOCK (FTWRL)
    • 特点: 非常粗粒度,会阻塞所有对表的写操作(DML、DDL),读操作可以执行。常用于做全库逻辑备份(mysqldump在不加--single-transaction时使用),但会严重影响业务。
    • 影响: DDL语句(如ALTER TABLE)即使是对表加读锁也会被全局读锁阻塞。
  2. 表级锁 (Table-level Locks)

    • 作用范围: 整个表。
    • 实现方式:
      • LOCK TABLES ... READ/WRITE:显式加表锁。
      • 元数据锁 (MDL - Metadata Lock):这是InnoDB在访问表时自动加的锁,无需显式操作。它保护表结构(元数据)。
        • 读锁 (MDL_READ): 对表进行SELECT等只读操作时加。多个读锁之间兼容。
        • 写锁 (MDL_WRITE): 对表进行INSERT, UPDATE, DELETE, ALTER等修改数据或结构的操作时加。写锁与读锁、写锁之间互斥。
    • 特点: 粒度较粗,开销小,加锁快,但并发度低。MyISAM引擎主要使用表锁。InnoDB虽然以行锁为主,但在某些操作(如没有索引的全表扫描)或需要表级操作时也会用到表锁或MDL。
    • 常见问题: DDL语句(如ALTER TABLE)需要获取MDL写锁,如果此时有一个长时间运行的SELECT持有MDL读锁,那么DDL会被阻塞,后续所有对该表的请求(包括SELECT)都会被阻塞,导致连接堆积("毛刺"现象)。
  3. 行级锁 (Row-level Locks)

    • 作用范围: 单个数据行。
    • 实现引擎: InnoDB 存储引擎的核心特性。
    • 特点: 粒度最细,并发度最高,但也意味着锁管理开销最大。能最大程度地支持并发访问。

事务

SQL中事务的隔离级别

隔离级别 (Isolation Level)脏读(Dirty Read)不可重复读 (Non-Repeatable Read)幻读 (Phantom Read)特点与说明
读未提交 (Read Uncommitted)可能可能可能最低级别。事务可以读取到其他事务未提交的修改(脏数据)。并发性能最高,但数据一致性最差。极少使用。
读已提交 (Read Committed, RC)不可能可能可能事务只能读取到已提交的修改。解决了脏读问题。在同一个事务中,多次读取同一行数据可能得到不同结果(因为其他事务提交了修改)。Oracle、SQL Server 的默认级别
可重复读 (Repeatable Read, RR)不可能不可能通常不可能发生 (在 MySQL InnoDB 中)保证在同一个事务中,多次读取同一数据结果一致。解决了脏读和不可重复读。MySQL InnoDB 的默认级别。InnoDB 通过 MVCC (多版本并发控制)临键锁 (Next-Key Locks) 机制,在该级别下也解决了幻读问题
串行化 (Serializable)不可能不可能不可能最高级别。事务完全串行执行,避免了所有并发问题。通过强制加锁或队列化事务来实现。并发性能最低,只在对数据一致性要求极高且并发量不大的场景使用。

提示

MySQL默认隔离级别为可重复读

死锁问题

死锁问题的产生:

两个或多个事务相互等待对方持有的锁被释放,导致所有相关事务都无法继续执行,陷入永久等待的状态。

假设有两个事务(事务 A 和 事务 B)和两个数据行(行1 和 行2)。

时间点事务 A (Transaction A)事务 B (Transaction B)锁状态
T1BEGIN;BEGIN;无锁
T2UPDATE table SET ... WHERE id = 1;
(成功获取行1的X锁)
事务A持有行1的X锁
T3UPDATE table SET ... WHERE id = 2;
(成功获取行2的X锁)
事务B持有行2的X锁
T4UPDATE table SET ... WHERE id = 2;
(尝试获取行2的X锁 → 被阻塞,等待事务B释放)
事务A等待行2,事务B持有行2
T5UPDATE table SET ... WHERE id = 1;
(尝试获取行1的X锁 → 被阻塞,等待事务A释放)
事务B等待行1,事务A持有行1

MySQL 的 InnoDB 存储引擎具备自动死锁检测机制。它不会让死锁永远持续下去。

  1. 检测: InnoDB 会定期检查事务等待图(Wait-for Graph)中是否存在循环等待。
  2. 解决: 一旦检测到死锁,InnoDB 会选择一个代价较小的事务进行回滚 (Rollback)。
    • 代价通常根据事务已经修改的数据量等因素估算,一般选择影响行数较少的事务
  3. 释放: 被回滚的事务会释放它持有的所有锁。
  4. 继续: 另一个(未被回滚的)事务就能获得所需的锁,从而继续执行并最终提交。
  5. 报错:被回滚的事务会收到一个错误提示

提示

死锁不会导致系统崩溃,但会导致其中一个事务失败。

数据类型

优化原则:

  • 更小的通常更好 (Smaller is usually better)

    在满足业务需求的前提下,选择占用存储空间最小的数据类型。

    • 减少磁盘 I/O: 更小的数据意味着单次读取可以加载更多行,提高缓存命中率(如 InnoDB Buffer Pool)。
    • 提高内存效率: 排序、分组、连接等操作需要在内存中进行,更小的数据类型消耗更少的内存。
    • 加快索引速度: 索引也存储数据,更小的列意味着更小的索引,索引树层级更少,查询更快。
  • 简单就好 (Simple is better)

    优先选择 MySQL 内置的、简单的数据类型,避免使用复杂的或需要额外处理的类型。

    • 处理速度快: MySQL 处理整数比处理字符串快得多。
    • 减少计算开销: 内置类型有高度优化的比较、排序算法。
    • 避免应用层处理: 将复杂性留在数据库层处理,简化应用逻辑。
  • 尽量避免使用 NULL

    在设计表结构时,除非业务逻辑明确要求,否则应将列定义为 NOT NULL

    • 存储和性能开销: NULL 值在行格式中需要额外的位(bit)来标记,增加了存储开销。查询时处理 NULL 也比处理普通值更复杂。
    • 索引效率: 对于 NULL 值,某些索引(如唯一索引)的行为可能不符合预期(例如,唯一索引允许有多个 NULL 值)。
    • 查询复杂性: NULL 参与比较(=!=)和计算时会产生意外结果(NULL = NULL 的结果是 UNKNOWN,不是 TRUE)。需要使用 IS NULL / IS NOT NULL 来判断。
    • 聚合函数: COUNT(*) 会统计所有行,但 COUNT(列名) 会忽略 NULL 值。

数值类型 (Numeric Types)

类型存储 (Bytes)有符号范围 (Signed)无符号范围 (Unsigned)适用场景与建议
TINYINT1-128 到 1270 到 255状态码、布尔值 (0/1)。优先使用 TINYINT UNSIGNED
SMALLINT2-32,768 到 32,7670 到 65,535较小的计数器、年份
MEDIUMINT3-8,388,608 到 8,388,6070 到 16,777,215中等规模的 ID 或计数
INT / INTEGER4-2,147,483,648 到 2,147,483,6470 到 4,294,967,295最常用的整数类型。主键、用户 ID、订单量等。除非明确需要更大范围,否则优先选择 INT
BIGINT8-2^63 到 2^63-10 到 2^64-1超大 ID (如分布式系统)、大额金额 (分单位)、时间戳 (毫秒/微秒)。注意:占用空间是 INT 的两倍。
FLOAT(M,D)4单精度浮点数科学计算、对精度要求不高的小数。存在精度丢失问题,不推荐用于金额
DOUBLE(M,D)8双精度浮点数科学计算、对精度要求较高的小数。同样存在精度丢失问题,不推荐用于金额
DECIMAL(M,D)可变精确小数金额、财务数据、需要精确计算的场景M 是总位数,D 是小数位数。虽然性能略低于 FLOAT/DOUBLE,但精确性是第一位的

日期和时间类型 (Date and Time Types)

类型格式范围精度存储 (Bytes)适用场景与建议
DATEYYYY-MM-DD'1000-01-01' 到 '9999-12-31'3只存储日期,如生日、入职日。
TIMEHH:MM:SS[.fraction]'-838:59:59.000000' 到 '838:59:59.000000'微秒3 + 分数部分存储时间或时间间隔
DATETIMEYYYY-MM-DD HH:MM:SS[.fraction]'1000-01-01 00:00:00.000000' 到 '9999-12-31 23:59:59.999999'微秒5 + 分数部分最常用的时间类型。存储精确的日期和时间。与时区无关
TIMESTAMPYYYY-MM-DD HH:MM:SS[.fraction]'1970-01-01 00:00:01.000000' UTC 到 '2038-01-19 03:14:07.999999' UTC微秒4 + 分数部分存储时间戳。存储的是从 1970-01-01 00:00:00 UTC 开始的秒数。受时区影响(存储 UTC,检索时转换为当前会话时区)。常用于 created_at, updated_at
YEARYYYY1901 到 21551只存储年份

字符串类型 (String Types)

类型可变/定长最大长度存储适用场景与建议
CHAR(M)定长0 到 255 个字符固定 M 字节 (不足用空格填充)存储长度固定的短字符串,如国家代码 (CHAR(2)), 状态码 (CHAR(1)), MD5 值 (CHAR(32))。避免用于可变长文本,会造成空间浪费。
VARCHAR(M)可变长0 到 65,535 字节 (受行大小限制)实际长度 + 1 或 2 字节 (记录长度)最常用的字符串类型。存储可变长度的字符串,如用户名、邮箱、地址。M 是字符数,不是字节数(注意字符集,如 UTF8MB4 一个字符最多 4 字节)。
TEXT可变长65,535 字节 (约 64KB)行外存储 (off-page)存储较长的文本,如文章内容、备注。TEXT 列通常不存储在主记录中,而是存储指针,访问稍慢。
MEDIUMTEXT可变长16,777,215 字节 (约 16MB)行外存储存储超长文本
LONGTEXT可变长4,294,967,295 字节 (约 4GB)行外存储存储极大文本
BINARY(M)定长M 字节固定 M 字节存储二进制数据,长度固定。
VARBINARY(M)可变长M 字节实际长度 + 长度字节存储可变长二进制数据,如小文件、加密数据。比 BLOB 更高效。
BLOB可变长65,535 字节行外存储存储二进制大对象,如图片、文件。不推荐在数据库中直接存储大文件,应存文件路径。

范式化和反范式化

各自的优缺点

范式化 (Normalization)

定义: 将数据表按照范式(1NF, 2NF, 3NF, BCNF 等)的规则进行拆分和组织,以消除数据冗余和数据异常。

优点

  1. 减少数据冗余:
    • 相同的数据(如用户信息、产品类别)只存储一次,大大节省了存储空间。
  2. 保证数据一致性与完整性:
    • 更新数据时,只需修改一处,避免了因多处修改而导致的数据不一致(更新异常)。
    • 通过外键约束等机制,可以有效防止插入无效数据(插入异常)或意外删除关联数据(删除异常)。
  3. 易于维护和扩展:
    • 表结构清晰,职责单一(符合单一职责原则),便于理解和维护。
    • 添加新功能或修改现有逻辑时,通常只需要修改或添加少数几个表,影响范围小。
  4. 数据模型清晰:
    • 实体和关系明确,更贴近现实世界的逻辑结构,便于业务分析和沟通。

缺点

  1. 查询性能可能下降:
    • 为了获取完整信息,经常需要对多个表进行 JOIN 操作。
    • 复杂的 JOIN(尤其是多表 JOIN 或大表 JOIN)会消耗大量的 CPU 和内存资源,导致查询变慢。
  2. SQL 语句复杂:
    • 查询语句变得复杂,需要编写更长的 SELECT 语句,包含多个 JOINON 条件,增加了开发和调试的难度。
  3. 索引优化复杂:
    • JOIN 操作对索引的要求更高,需要为连接字段建立合适的索引,优化难度增加。

反范式化 (Denormalization)

定义: 为了提高查询性能,有意识地引入数据冗余,将原本需要 JOIN 才能获取的数据,预先合并到一张表中。

优点

  1. 显著提升查询性能:
    • 减少了 JOIN 操作,甚至可以做到单表查询,大大加快了读取速度。
    • 特别适用于读操作远多于写操作的场景(如报表、数据分析)。
  2. 简化查询语句:
    • SELECT 语句变得简单,通常只需要查询一张表,降低了开发复杂度。
  3. 减少锁竞争:
    • 减少了跨表操作,降低了因 JOIN 导致的锁等待和死锁风险。

缺点

  1. 增加数据冗余:
    • 相同的数据在多个地方重复存储,占用了更多的磁盘空间。
  2. 可能导致数据不一致:
    • 当需要更新冗余数据时,必须确保所有副本都被同步更新。如果更新逻辑不完善,很容易导致数据不一致。
    • 例如,用户姓名在 orders 表和 order_details 表中都有冗余,修改姓名时需要同时更新两个表。
  3. 增加写操作的复杂性和开销:
    • 为了维护数据一致性,写入(INSERT, UPDATE, DELETE)操作的逻辑变得复杂,可能需要触发器、存储过程或应用层代码来保证所有冗余字段同步。
    • 一次更新可能需要修改多行或多张表,增加了 I/O 开销和事务时间。
  4. 维护难度增加:
    • 数据模型变得不那么“干净”,冗余字段的来源和更新逻辑需要清晰的文档说明,否则容易造成混乱。

索引

索引分类

按数据结构分类

  • B+Tree 索引:这是最常见的索引类型,InnoDB 和 MyISAM 存储引擎都使用 B+Tree 来实现索引。它支持范围查询、排序和部分匹配。
  • 哈希索引(Hash Index):仅在 MEMORY 存储引擎中默认支持。哈希索引提供了快速的等值查找,但不支持范围查询或排序。
  • 全文索引(Full-Text Index):用于全文搜索,适用于大文本字段的搜索。MyISAM 和 InnoDB(5.6版本后)都支持这种类型的索引。
  • R-Tree 索引(空间索引):用于存储和查询地理空间数据。MyISAM 支持 R-Tree 索引,而 InnoDB 自从 MySQL 5.7 开始也支持空间索引。

按物理存储(与数据的关系)

  • 聚簇索引(Clustered Index):在 InnoDB 中,表的数据行按照主键的顺序存储,这就是聚簇索引。每个表只能有一个聚簇索引,通常就是主键索引。
  • 非聚簇索引(Non-Clustered Index/Secondary Index):除聚簇索引之外的所有其他索引都是非聚簇索引。在 InnoDB 中,非聚簇索引的叶子节点包含的是指向聚簇索引的指针,因此查询时可能需要额外的一次查找操作(回表)。

按逻辑功能/使用特性划分

  • 普通索引(Index/KEY):最基本的索引类型,没有任何唯一性约束,主要用于加速查询。
  • 唯一索引(Unique Index):确保索引列中的值是唯一的,但允许存在多个 NULL 值。
  • 主键索引(Primary Key):一种特殊的唯一索引,不允许 NULL 值,并且每个表只能有一个主键索引。
  • 前缀索引(Prefix Index):对字符串类型的列创建索引时,可以指定只对前 N 个字符建立索引以节省空间。
  • 组合索引/联合索引(Composite Index):多列联合索引,遵循“最左前缀原则”,即查询条件必须包含索引中最左边的列才能有效利用该索引。
  • 覆盖索引(Covering Index):当一个索引包含了查询所需的所有列时,称为覆盖索引。这样数据库可以直接从索引中获取数据,不需要访问表中的数据行。
存储引擎主要索引类型底层算法/结构特点
InnoDB (默认)B+Tree 索引B+Tree支持事务、行锁,数据聚簇存储
MyISAM(旧)B+Tree 索引B+Tree不支持事务,表锁,非聚簇
MEMORYHASH / B+TreeHASHB+Tree内存表,快但数据不持久

查询优化

切分查询(拆分长SQL)

当一个SQL查询过于复杂时,可以考虑将其拆分成几个更小、更简单的查询。这样做有几个好处:

  • 简化逻辑:更易于理解和维护。
  • 提高可读性:有助于其他开发者理解代码意图。
  • 便于缓存:某些中间结果集可以通过缓存来加速多次查询。
  • 错误隔离:如果一个查询失败,只需重新运行出错的部分,而不是整个大查询

分解关联查询(拆分JOIN语句)

JOIN操作有时会导致性能瓶颈,尤其是在处理大量数据时。拆分JOIN语句通常意味着减少每个查询中的JOIN数量,或者避免不必要的JOIN。

优化:

  • 延迟JOIN:只在必要时才执行JOIN操作。例如,首先筛选出主表的数据,然后再根据这些数据进行JOIN。
  • 使用子查询代替JOIN:有时候,子查询可能比JOIN更高效,特别是当你只需要访问主表的一部分记录时。
  • 增加索引:确保JOIN条件涉及的列上有适当的索引,以加快连接速度。
  • 限制返回的数据量:在JOIN之前使用WHERE子句过滤数据,以减少需要合并的数据量。
sql
SELECT FROM tag
JOIN tag_post ON tag_post.tag_id=tag.id
-JOIN post ON tag_post.post_id=post.id
-WHERE tag.tag='mysql';

分解后:

sql
mysq1>SELECT FROM tag WHERE tag='mysql';
mysql>SELECT FROM tag_post WHERE tag id=1234;
mysql>SELECT FROM post WHERE post.id in (123,456,567,9098,8904);

特定语句优化:

count():

统计行数时可以使用count(*),MySQL会忽略所有的列直接统计所有行数。

数据量较大时可以采用逆向思维查询,例如:

优化前:

sql
SELECT COUNT(*)FROM world.city WHERE ID>5

优化后:

sql
SELECT (SELECT COUNT(*)FROM world.city)-COUNT(*)
-FROM world.City WHERE ID <5
最近更新