阅读视图

面试笔记:MySQL 相关03 – SQL语法与查询优化

感谢订阅陶其的个人博客!

SQL语法与查询优化

回目录: 《面试笔记:MySQL 相关目录》
上一篇: 《面试笔记:MySQL 相关02 – 索引》


01. 子查询与连接(join)查询

子查询:是嵌套在主查询中的查询,按“返回结果类型”和“是否依赖主查询”分类,核心是 “子结果驱动主查询”;
连接查询:是多表关联,核心是 “通过关联条件合并表数据”,按“是否保留不匹配行”分类,底层依赖驱动表和被驱动表的关联逻辑。

以下给出了4种核心子查询和5种核心连接查询,以及一种内部优化查询。

01. 标量子查询

返回单个值。

  • 写法格式:子查询返回 1 行 1 列,可作为主查询的字段或条件值;
    -- 示例:查询订单数最多的用户名称(子查询返回最大订单数)
    SELECT username FROM user WHERE user_id = (SELECT MAX(user_id) FROM order WHERE status = 1);
  • 性能开销:极低,仅执行 1 次子查询,结果直接代入主查询;
  • 适用场景:主查询需依赖单个聚合值(如最大值、平均值)或特定单行数据。

02. 列子查询

返回单列多行。

  • 写法格式:子查询返回 1 列 N 行,主查询用IN/NOT IN/ANY/ALL匹配;
    -- 示例:查询有未支付订单的用户(子查询返回未支付订单的用户ID列表)
    SELECT username FROM user WHERE user_id IN (SELECT DISTINCT user_id FROM order WHERE status = 0);
  • 性能开销:中低,MySQL 5.7 + 会将其优化为 “半连接”(避免逐行匹配),子查询结果会被物化(存入临时表);
  • 适用场景:主查询需匹配多个候选值(如 “在某个集合中”),子查询结果集不宜过大(建议 1 万行内)。

03. 行子查询

返回多行多列。

  • 写法格式:子查询返回 N 行 N 列(通常 1-2 列),主查询用IN匹配行数据;
    -- 示例:查询与“用户张三的上海地址”完全匹配的用户(返回id和city两列)
    SELECT username FROM user WHERE (user_id, city) IN (SELECT user_id, city FROM user_addr WHERE username = '张三' AND city = '上海');
  • 性能开销:中,子查询结果集需按行匹配,建议结果集控制在 1 千行内;
  • 适用场景:主查询需匹配 “多字段组合条件”(如联合主键、多维度筛选)。

04. 关联子查询

依赖主查询字段。

  • 写法格式:子查询用主查询的字段作为条件(EXISTS是典型用法),需用表别名区分;
    -- 示例:查询有有效订单的用户(子查询依赖主查询的user_id,找到匹配即停止)
    SELECT username FROM user u WHERE EXISTS (SELECT 1 FROM order o WHERE o.user_id = u.user_id AND o.status = 1);
  • 性能开销:中高,主查询每扫描 1 行,子查询触发 1 次(类似 “嵌套循环”);若子查询关联字段无索引,会导致全表扫描(如 10 万行主表触发 10 万次子查询);
  • 适用场景:主查询需 “存在性校验”(如 “是否有相关数据”),优先用EXISTS(找到匹配行立即停止)替代IN(需遍历全量结果集)。

05. 内连接

INNER JOIN,默认连接方式。

  • 写法格式:仅保留两表中 “关联条件完全匹配” 的行(取交集),INNER可省略;
    -- 示例:查询用户及其有效订单(仅返回有订单的用户和匹配的订单)
    SELECT u.username, o.order_no FROM user u
    INNER JOIN order o ON u.user_id = o.user_id  -- 关联条件(必须写ON),INNER 可省略
    WHERE o.status = 1;
  • 性能开销:低,MySQL 会自动选择 “小表作为驱动表”,若关联字段有索引,被驱动表可快速匹配(避免全表扫描);
  • 适用场景:需获取多表 “交集数据”(如用户 + 关联订单、商品 + 关联分类),是业务中最常用的连接方式。

06. 左外连接

LEFT JOIN / LEFT OUTER JOIN

  • 写法格式:保留左表所有行,右表无匹配时用NULL填充,OUTER可省略;
    -- 示例:查询所有用户及其订单(无订单的用户也会显示,订单字段为NULL)
    SELECT u.username, o.order_no FROM user u
    LEFT JOIN order o ON u.user_id = o.user_id
    WHERE o.status = 1 OR o.order_no IS NULL;  -- 注意:过滤右表需包含NULL
  • 性能开销:中低,驱动表是左表(需全扫左表),右表关联字段有索引则性能接近内连接;无索引则性能下降明显;
  • 适用场景:需保留主表全量数据(如用户表),关联从表(如订单表)的可选数据(如 “统计所有用户的订单情况,包括无订单用户”)。

07. 右外连接

RIGHT JOIN / RIGHT OUTER JOIN

  • 写法格式:保留右表所有行,左表无匹配时用NULL填充,逻辑与左连接相反;
    -- 示例:查询所有订单及其所属用户(无匹配用户的订单也显示,用户字段为NULL)
    SELECT u.username, o.order_no FROM user u
    RIGHT JOIN order o ON u.user_id = o.user_id
    WHERE o.create_time > '2024-01-01';
  • 性能开销:与左连接一致,驱动表是右表(需全扫右表);
  • 适用场景:需保留从表全量数据(如订单表),关联主表(如用户表)的可选数据(如 “统计所有近 3 个月订单,包括已删除用户的订单”)。

08. 全外连接

FULL JOIN / FULL OUTER JOIN

  • 写法格式:保留左右两表所有行,无匹配时用NULL填充;MySQL 不直接支持FULL JOIN,需用UNION合并左连接和右连接;
    -- 示例:查询所有用户和所有订单的关联数据(保留双方无匹配的行)
    SELECT u.username, o.order_no FROM user u
    LEFT JOIN order o ON u.user_id = o.user_id
    UNION  -- 去重合并
    SELECT u.username, o.order_no FROM user u
    RIGHT JOIN order o ON u.user_id = o.user_id;
  • 性能开销:高,需执行两次连接 + 去重操作,数据量大时效率低;
  • 适用场景:极少用,仅需 “完整保留两表所有数据” 的特殊场景(数据对账、全量统计)。

09. 交叉连接

CROSS JOIN,笛卡尔积

  • 写法格式:无关联条件,两表数据完全组合(行数 = 左表行数 × 右表行数),CROSS JOIN可省略;
    -- 示例:用户表(3行)和商品表(2行)交叉连接,结果为6行
    SELECT u.username, p.product_name FROM user u
    CROSS JOIN product p;
  • 性能开销:极高,行数呈指数级增长(1 万行 ×1 万行 = 1 亿行),几乎不用;
  • 适用场景:仅用于 “全组合场景”(如生成所有用户 + 所有商品的测试数据),必须配合WHERE过滤(否则会撑爆内存)。

10. 半连接

半连接不是独立的查询语法,而是 MySQL 5.7 + 默认启用的对列子查询(如IN子查询)的内部优化手段,目的是减少子查询与主查询的匹配开销。

用于优化“主查询匹配子查询结果集”的场景(如IN/EXISTS列子查询)。

核心逻辑是 “只判断匹配与否,不返回子查询的完整数据”,避免逐行匹配的低效问题。

简单说:普通子查询是 “子查询返回所有结果→主查询逐行比对”,半连接是 “主查询与子查询表直接关联→找到匹配行就停止”,本质是将子查询转化为类似JOIN的关联逻辑,提升性能。

触发条件:子查询是“列子查询”(返回单列多行),且子查询表与主查询表无关联(非关联子查询),例如WHERE id IN (SELECT uid FROM t2 WHERE status=1)

写法格式:无特殊语法,仍用普通IN/EXISTS子查询写法,MySQL 会自动触发半连接优化(可通过EXPLAIN查看type列是否有SIMPLE/HASH JOIN,而非SUBQUERY)。

-- 示例:查询有未支付订单的用户(MySQL会自动用半连接优化)
SELECT username FROM user u WHERE u.user_id IN (SELECT DISTINCT o.user_id FROM order o WHERE o.status = 0);

性能开销

  • 低 – 中:远优于未优化的子查询(避免子查询结果集物化后逐行匹配)。
  • 优化逻辑:MySQL 会选择以下方式之一执行半连接:
    • 哈希半连接:对小表(子查询表)建哈希表,主表逐行匹配哈希键(适合大表)。
    • 嵌套循环半连接:小表驱动大表,找到匹配行立即停止扫描(适合小表)。
    • 物化半连接:子查询结果存入临时表并建索引,主表关联临时表(适合子查询结果集较小)。

适用场景

  • 主查询需判断 “字段是否在子查询结果集中”(如IN/EXISTS)。
  • 子查询表数据量适中(1 万行内最佳),且子查询有过滤条件(如WHERE status=1),能减少结果集大小。
  • 替代场景:若子查询结果集极大(10 万行 +),建议手动改写为JOIN(半连接优化效果会下降)。

注意项:

  • 半连接是 MySQL 自动优化,无需专门手动去写语法,但需知道 “IN子查询在 5.7 + 会被优化为半连接”,避免面试官问 “INJOIN哪个快” 时只说表面结论。
  • 实践中,若EXPLAIN显示子查询类型为SUBQUERY(未触发半连接),可通过 “子查询加DISTINCT” 或 “改写为JOIN” 强制优化(如上述示例加DISTINCT让结果集唯一,更易触发半连接)。

总结

1. 子查询 vs 连接查询:

  • 简单场景(如单值匹配、存在性校验)用子查询(写法简洁);
  • 复杂多表关联(如 3 表以上、需筛选多字段)用连接查询(性能更优,易优化);
  • 关联子查询尽量改写为JOIN(避免嵌套循环,如EXISTS适合小表,JOIN适合大表)。

2. 性能优化关键:

  • 连接查询:关联字段必须建索引(如order.user_id),小表驱动大表(减少循环次数);
  • 子查询:避免NOT INNULL 值会导致结果异常),用NOT EXISTS替代;
  • 外连接:右表过滤条件写在ON(关联时过滤),左表过滤条件写在WHERE(关联后过滤)。

3. Java 开发实践场景:

  • 列表查询(如用户列表 + 订单数):用LEFT JOIN + GROUP BY
  • 详情查询(如订单详情 + 用户信息):用INNER JOIN
  • 存在性校验(如判断用户是否有未支付订单):用EXISTS子查询。

02. 子查询与join性能对比

子查询与join性能对比及适用场景

关键结论:

  • 简单场景(单表校验、单值匹配)用子查询(简洁);
  • 多表关联、复杂聚合、主表数据量大时,优先用 JOIN(性能更稳定)。

03. 复杂查询的执行逻辑

1. group by/having

GROUP BYHAVING 是 MySQL 中用于数据分组聚合的核心语法,执行逻辑: “数据过滤→分组→聚合→二次过滤” 。

1. 核心执行逻辑

GROUP BY 的作用是 “按指定字段将数据分组”;
HAVING 则是 “对分组后的结果进行过滤”。

整体执行流程分四步,顺序不可颠倒:

1) 原始数据过滤(WHERE 子句);

  • 先通过 WHERE 筛选符合条件的原始数据(如 WHERE status = 1 过滤无效数据),减少后续处理的数据量。
  • 原理:WHERE 是 “分组前过滤”,仅保留满足条件的行,不涉及聚合操作,可利用索引快速筛选(如 status 字段有索引时,直接定位有效行)。

2) 分组操作(GROUP BY 子句);

  • GROUP BY 后的字段(如 GROUP BY user_id)将上一步过滤后的数据分组,相同值的行被归为一组。
  • 底层处理:
    • 若分组字段有索引,MySQL 会直接按索引顺序分组(无需额外排序,效率高);
    • 若无索引,会创建临时表存储分组数据,或对数据进行文件排序(Using temporaryUsing filesort,可通过 EXPLAIN 查看)。

3) 聚合计算(聚合函数);

  • 对每个分组执行聚合函数(如 COUNT(*) 统计行数、SUM(amount) 计算总和),生成每个分组的聚合结果。
  • 原理:聚合函数仅作用于分组内的数据,每个分组最终输出一行结果(包含分组字段和聚合值)。

4) 分组结果过滤(HAVING 子句);

  • HAVING 筛选聚合后的分组(如 HAVING COUNT(*) > 5 保留行数超 5 的组),最终返回符合条件的分组。
  • 原理:HAVING 是 “分组后过滤”,可直接使用聚合函数(因聚合结果已生成)。

2. 关键区别与实践注意

1) WHERE 与 HAVING 的核心区别

  • WHERE 作用于分组前的原始数据,不能使用聚合函数(如 WHERE COUNT(*) > 5 会报错);
  • HAVING 作用于分组后的结果,可以使用聚合函数(如 HAVING SUM(amount) > 1000 合法)。

2) 性能优化实践(重点)

  • 优先用 WHERE 过滤:尽量在分组前通过 WHERE 减少数据量(如 GROUP BY user_id HAVING user_id > 100 可改为 WHERE user_id > 100 GROUP BY user_id,减少分组计算量)。
  • 分组字段加索引:避免临时表和文件排序(如 GROUP BY create_time 时,给 create_time 建索引,EXPLAIN 中无 Using temporary 即为优化生效)。
  • 避免 SELECT 非分组字段:MySQL 5.7+ 默认开启 ONLY_FULL_GROUP_BY 模式,SELECT 后只能出现 GROUP BY 字段或聚合函数(如 SELECT user_id, username, COUNT(*)username 未在 GROUP BY 中,会报错,需规范写法)。

3. 典型场景举例

  • 统计高频用户:查询 “订单数超 10 单的用户及其总消费”,用 GROUP BY user_id HAVING COUNT(*) > 10
  • 按时间分组分析:查询 “每月订单金额超 10 万的月份”,用 GROUP BY month(create_time) HAVING SUM(amount) > 100000

2. limit 分页

LIMIT 分页是 MySQL 中用于从查询结果中截取指定范围数据的核心语法(如 LIMIT offset, size 取第 offset + 1offset + size 行),其执行逻辑围绕 “全量查询→排序→截取” 展开,实际使用中需重点关注性能问题。

1. 基本执行逻辑

LIMIT 本身不参与数据筛选,而是对 “查询 + 排序后” 的结果集进行截取,核心流程分三步:

1) 执行主查询获取原始数据

  • 先执行 WHEREJOIN 等条件筛选,得到所有符合条件的行(如 SELECT * FROM order WHERE status=1 筛选有效订单)。

2) 排序处理(若有 ORDER BY

  • 若包含 ORDER BY(分页几乎必带,否则结果无序),MySQL 会按指定字段排序:
    • 若排序字段有索引(如 ORDER BY create_timecreate_time 有索引),直接利用索引顺序获取有序数据(效率高);
    • 若无索引,需在内存或磁盘中进行 “文件排序”(Using filesort,可通过 EXPLAIN 查看),排序过程会消耗额外 CPU/IO。

3) 截取指定范围数据

  • 从排序后的完整结果集中,跳过前 offset 行,返回接下来的 size 行(如 LIMIT 20, 10 即跳过前 20 行,返回 10 行)。

2. 核心性能问题:offset 过大导致效率低下

offset 很大时(如 LIMIT 100000, 10),性能会急剧下降,原因是:

  • MySQL 必须先扫描并排序前 100010 行数据,然后丢弃前 100000 行,仅返回最后 10 行,大量计算被浪费;
  • 若排序无索引(依赖文件排序),会产生临时文件,进一步加剧性能损耗(10 万行数据排序可能耗时数百毫秒)。

3. 实践优化方案(重点,体现工程经验)

针对 offset 过大的问题,结合 Java 开发中常见的分页场景(如列表页、历史记录查询),优化手段如下:

1) 基于 “主键 / 唯一索引” 分页(最常用)

利用主键或唯一索引的有序性,通过 WHERE 条件直接定位起始位置,避免全量扫描:

-- 代替 LIMIT 100000, 10(低效)
SELECT * FROM order 
WHERE id > 100000  -- 上一页最后一条数据的id
ORDER BY id 
LIMIT 10;

原理:主键索引是有序的,id > 100000 可直接定位到起始行,无需扫描前 10 万行,性能提升 10 倍以上。

2) 避免 SELECT *,只查必要字段

减少数据传输量和内存占用,尤其大表(如包含 text 字段的表):

-- 只查需要的字段(如订单号、金额、时间)
SELECT order_no, amount, create_time FROM order 
WHERE id > 100000 
LIMIT 10;

3) 确保排序字段有索引

分页必须带 ORDER BY,且排序字段需建索引(如 create_time 索引),避免文件排序:

-- 给 create_time 建索引:CREATE INDEX idx_order_create_time ON order(create_time)
SELECT * FROM order 
WHERE status=1 
ORDER BY create_time DESC  -- 利用索引排序
LIMIT 20, 10;

4) 限制最大分页页数

业务上避免允许用户访问过大页数(如 “只显示前 100 页”),超过则提示 “数据过多,请缩小范围”(如电商平台常见做法),从源头减少大 offset 场景。

04. order by 的排序原理

MySQL 的ORDER BY用于对查询结果按指定字段排序,其核心原理是 “利用索引有序性直接取数” 或 “无索引时通过内存 / 磁盘排序”,性能差异主要源于是否能借助索引避免额外排序操作。

1. 核心排序原理

1. 利用索引排序

Using index,高效。

若排序字段(或多字段排序的前缀)有有序索引(如 B + 树索引,本身按字段值有序存储),MySQL 会直接沿索引顺序读取数据,无需额外排序,这是最优情况。

  • 原理:B + 树索引的叶子节点按索引字段值升序(或降序,取决于建索引时的指定)排列,ORDER BY字段与索引顺序一致时,可直接通过索引定位并返回有序数据,避免排序开销。
  • 示例:
    订单表order有索引idx_create_timecreate_time升序),执行SELECT id, create_time FROM order ORDER BY create_time时,MySQL 直接沿idx_create_time的叶子节点顺序取数,无需排序(EXPLAINExtra列显示Using index)。

2. 文件排序

Using filesort,低效。

若排序字段无索引,或排序顺序与索引顺序不一致(如索引是升序,ORDER BY用降序且无对应降序索引),MySQL 会触发 “文件排序”:先将数据加载到内存 / 临时文件,完成排序后再返回结果。

  • 执行步骤:

    1. 从表中读取符合WHERE条件的行(无索引时全表扫描);
    2. 将 “排序字段值 + 行指针(指向原始数据位置)” 存入sort_buffer(排序缓冲区);
    3. sort_buffer装不下所有数据,会将数据分块,先在内存中排序,再写入临时文件(磁盘);
    4. 最后合并所有分块的排序结果,得到全局有序的结果集,再通过行指针取原始数据返回。
  • 示例:
    订单表ordercreate_time无索引,执行SELECT * FROM order ORDER BY create_time时,MySQL 会先扫描全表,将create_time和行指针存入sort_buffer,排序后再取完整数据(EXPLAINExtra列显示Using filesort)。

2. 影响排序性能的关键因素

  1. 是否使用索引:索引排序性能远高于文件排序(毫秒级 vs 秒级,数据量大时差距更大)。
  2. sort_buffer大小:由sort_buffer_size参数控制(默认 256KB),若数据量超过该值,会触发磁盘临时文件排序(IO 开销剧增)。
  3. 查询字段多少SELECT *会导致sort_buffer中存储更多字段(增加内存占用),而只查必要字段可减少sort_buffer压力,加速排序。

3. 实践优化建议

1. 给排序字段建合适的索引:

  • 单字段排序:直接给排序字段建索引(如ORDER BY create_time → 建idx_create_time)。
  • 多字段排序(如ORDER BY a, b DESC):建联合索引(a, b DESC),索引顺序与排序顺序完全一致(避免索引失效)。

2. 避免SELECT *,只查必要字段:

  • 例如,列表页只需idnamecreate_time,则SELECT id, name, create_time ...,减少sort_buffer中存储的数据量,避免磁盘排序。

3. 控制排序数据量:
先用WHERE过滤无效数据(如WHERE status=1),减少进入排序阶段的行数(如从 100 万行滤到 1 万行,排序效率提升 100 倍)。

4. 大结果集排序用分页配合索引:
如 “按时间排序分页查询订单”,建(create_time, id)联合索引,用WHERE create_time > ?定位分页起点,避免全量排序。

4. 总结

ORDER BY的优化核心是 “能用上索引就避免文件排序”:索引排序依赖 B + 树的有序性,高效低耗;文件排序需内存 / 磁盘排序,性能差。实际开发中,通过 “建匹配索引 + 精简查询字段 + 提前过滤数据” 可显著提升排序性能,这也是处理订单列表、用户日志等需排序场景的常规优化手段。

05. 慢查询的定位与分析

MySQL 的慢查询(执行时间超过阈值的 SQL)是性能优化的核心切入点,定位与分析需结合日志工具、执行计划和业务场景。

1. 慢查询的定位

慢查询的定位:通过日志捕获目标 SQL。

定位慢查询的核心是开启慢查询日志,记录执行时间超标的 SQL,为后续分析提供依据。

1. 慢查询日志配置(5.7 版本)
通过修改my.cnf(或my.ini)配置,开启并定制日志规则:

slow_query_log = 1  # 开启慢查询日志(默认:0,关闭)
slow_query_log_file = /var/log/mysql/slow.log  # 日志文件路径(需MySQL有写入权限)
long_query_time = 1  # 慢查询阈值(单位秒,默认10秒,建议生产设1-2秒)
log_queries_not_using_indexes = 1  # 记录未使用索引的查询(即使未超阈值,可选)
log_output = FILE  # 日志输出方式(FILE/table,5.7默认:FILE)

配置后重启 MySQL 生效,也可通过SET GLOBAL动态开启(无需重启,适合临时排查):

SET GLOBAL slow_query_log = 1;
SET GLOBAL long_query_time = 1;

2. 日志收集工具

  • mysqldumpslow(自带工具):简单统计慢查询,适合快速定位高频问题:

    # 查看访问次数最多的10条慢查询
    mysqldumpslow -s c -t 10 /var/log/mysql/slow.log
    # 查看总执行时间最长的10条慢查询
    mysqldumpslow -s t -t 10 /var/log/mysql/slow.log

    优势:轻量,适合初步筛选;
    不足:无法分析趋势或复杂统计。

  • pt-query-digest(Percona 工具包):生产环境常用,可按执行时间、频率、用户等维度分析,输出 SQL 模板、平均耗时、扫描行数等关键信息:

    pt-query-digest /var/log/mysql/slow.log > slow_analysis.txt

    优势:能识别重复 SQL(如参数不同但结构相同的查询),定位 “隐形高频慢查询”(单条耗时 0.8 秒,但每秒执行 100 次,总耗时 80 秒)。

2. 慢查询的分析

慢查询的分析:通过执行计划定位根因。

捕获慢查询后,需用EXPLAIN分析其执行逻辑,重点关注是否全表扫描、是否用对索引、是否有额外排序 / 临时表。

1. 核心分析工具:EXPLAIN

对慢查询执行EXPLAIN,查看关键字段:

  • type:访问类型(性能从好到差:const > eq_ref > ref > range > ALL)。ALL表示全表扫描(需优先优化)。
  • key:实际使用的索引(NULL表示未用索引)。
  • rows:预估扫描行数(值越大,效率越低)。
  • Extra:额外信息(关键警告:Using filesort(需排序且无索引)、Using temporary(需创建临时表)、Using where; Using filesort(全表扫描后排序,性能极差))。

2. 常见慢查询原因及分析(结合EXPLAIN

  • 全表扫描(type=ALLkey=NULL
    • 原因:查询条件无索引(如:WHERE status=1status无索引),或索引失效(如:WHERE name LIKE '%abc'左模糊匹配导致索引失效)。
  • JOIN 关联无索引(rows极大)
    • 原因:被驱动表的关联字段无索引(如t1 JOIN t2 ON t2.uid = t1.idt2.uid无索引),导致被驱动表全表扫描。
  • 排序无索引(Extra=Using filesort
    • 原因:ORDER BY字段无索引,或排序顺序与索引不一致(如索引是create_time ASC,但查询用ORDER BY create_time DESC且无降序索引)。
  • 子查询嵌套(type=DEPENDENT SUBQUERY
    • 原因:关联子查询导致主表每行触发一次子查询(如WHERE EXISTS (SELECT 1 FROM t2 WHERE t2.uid = t1.id)),无索引时性能骤降。

3. 慢查询的优化方向

分析出原因后,针对性优化:

1. 加索引

  • 为查询条件、JOIN 关联字段、排序字段建索引(如:WHERE+ORDER BY字段建联合索引,如idx_status_create_time (status, create_time))。

2. 改写 SQL

  • 子查询改 JOIN(如关联子查询改写为INNER JOIN,减少嵌套循环);
  • 避免左模糊匹配(LIKE '%abc'改为LIKE 'abc%',或用全文索引);
  • 大分页改范围查询(LIMIT 100000,10改为WHERE id > 100000 LIMIT 10)。

3. 控制数据量

  • WHERE提前过滤无效数据(如WHERE create_time > '2024-01-01'),减少扫描和排序行数。

4. 8.0 版本的优化

  • 慢查询日志支持表存储:除文件外,可将日志写入mysql.slow_log表(log_output = TABLE),支持 SQL 查询分析(如按时间范围统计)。
  • 日志内容更丰富:新增query_id(唯一标识)、rows_affected等字段,方便关联性能模式(Performance Schema)数据。
  • 动态调整更灵活:long_query_time支持小数(如 0.5 秒),且修改后立即生效(无需重启连接)。

5. 总结

慢查询定位与分析的核心流程是:开启日志捕获 → 工具统计筛选 → EXPLAIN分析执行计划 → 针对性优化(索引 / 改写 SQL)

实践中,需结合业务场景(如订单表慢查询多与user_idcreate_time索引相关),优先解决 “高频 + 高耗时” 的慢查询。

06. 复杂SQL的拆分与改写

大表分页优化:延迟关联、书签分页。

MySQL 大表分页是业务中高频痛点(如订单列表、日志查询),普通LIMIT offset, sizeoffset较大时(如LIMIT 100000,10)性能极差:因 MySQL 需扫描前 10 万行再丢弃,仅取最后 10 行,IO 开销剧增。

复杂 SQL 拆分与改写的核心思路是减少无效扫描行数,常用优化手段包括延迟关联书签分页

1. 普通分页的性能瓶颈

普通分页 SQL 示例(订单表order,按create_time逆序):

SELECT * FROM `order` ORDER BY create_time DESC LIMIT 100000, 10;

问题:当offset=100000时,MySQL 需先遍历索引找到第 10 万行的位置(即使有create_time索引),再回表读取 10 行数据,前 10 万行的扫描属于无效开销,数据量越大越慢。

2. 关键优化方法

1. 延迟关联(延迟回表)

概念:先通过覆盖索引获取目标数据的主键(或唯一键),再关联原表获取完整字段,避免 “早期回表” 带来的大量 IO。

原理:利用索引的 “覆盖查询” 特性(只查主键,无需回表)快速定位目标行,再通过主键关联原表取数,减少扫描和回表的数据量。

优化后 SQL 示例:

-- 子查询用索引获取主键(覆盖索引,无需回表)
SELECT o.* 
FROM `order` o 
JOIN (SELECT id FROM `order` ORDER BY create_time DESC LIMIT 100000, 10) tmp 
ON o.id = tmp.id;
  • 优势:子查询仅扫描索引获取id(轻量),关联原表时仅取 10 行完整数据,性能比普通分页提升 5-10 倍(10 万 + offset 场景)。
  • 适用场景:需展示完整字段、支持任意跳页(如第 100 页、第 200 页)的列表。

2. 书签分页(游标分页 / 键集分页)

概念:用 “上一页最后一条记录的唯一标识”(书签,如idcreate_time+id)作为条件,替代offset直接定位下一页起点,完全避免无效扫描。

原理:利用主键 / 唯一索引的有序性,通过WHERE条件跳过前面数据,直接从书签位置取数。

优化后 SQL 示例:假设上一页最后一条记录的create_time='2024-01-01 10:00:00'id=100000,下一页查询:

SELECT * 
FROM `order` 
WHERE create_time < '2024-01-01 10:00:00' 
   OR (create_time = '2024-01-01 10:00:00' AND id < 100000) 
ORDER BY create_time DESC, id DESC 
LIMIT 10;
  • 关键:用create_time+id作为书签(create_time可能重复,需加id保证唯一性),通过索引直接定位,无offset开销。
  • 优势:性能极致(无论翻多少页,扫描行数固定为LIMIT size);
  • 局限:不支持 “跳页”(如直接到第 100 页),仅适合 “上一页 / 下一页”“加载更多” 场景(APP / 移动端常用)。

3. 覆盖索引分页

覆盖索引分页:若查询字段(如id, order_no, create_time)全部包含在索引中,直接用索引查询,无需回表:

-- 索引`idx_create_time_order_no (create_time DESC, order_no, id)`覆盖所有查询字段
SELECT id, order_no, create_time 
FROM `order` 
ORDER BY create_time DESC 
LIMIT 100000, 10;

4. 限制最大 Offset

业务上禁止offset超过阈值(如 10000),引导用户通过筛选条件(如 “按时间范围筛选”)缩小数据范围,避免大 offset 查询。

5. 分表分页

超大规模表(亿级)需水平分表(如按create_time分表),分页时先定位分表,再在分表内分页,避免跨表扫描。

3. 总结

大表分页优化的核心是 “用索引定位替代无效扫描”

  • 需支持跳页选延迟关联,通过覆盖索引减少回表;
  • 移动端 / 加载更多选书签分页,用唯一键直接定位;
  • 极致性能选覆盖索引分页,避免回表开销。

07. JOIN优化

小表驱动大表、 避免 cross join

1. JOIN 优化的核心逻辑

MySQL 中 JOIN 的底层实现以Nested Loop Join(嵌套循环连接)为主:外层循环遍历 “驱动表” 的数据,内层循环用驱动表的字段去 “被驱动表” 中匹配数据。。就像Java里的双层for循环。

优化的核心是减少外层循环次数 + 降低内层匹配成本,这也是 “小表驱动大表” 的原理基础。

2. 优化1:小表驱动大表

1. 原理

“小表” 指参与 JOIN 的数据集更小的表(而非物理表大小)。
用小表做驱动表(外层循环),大表做被驱动表(内层循环),可显著减少外层循环次数,从而降低整体 IO 和匹配开销。

  • 举例:小表有 100 行,大表有 100 万行
    • 小表驱动大表:外层循环 100 次,内层每次匹配大表 → 总匹配 100 次;
    • 大表驱动小表:外层循环 100 万次,内层每次匹配小表 → 总匹配 100 万次;
    • 性能差异一目了然。

2. 实践判断与示例

  • 如何判断 “小表”:
    • 通过EXPLAINrows字段(预估扫描行数),行数少的就是 “小表”;
    • 或业务逻辑中明确数据量更小的表(如字典表、配置表)。
  • SQL 示例(用户表user是小表,订单表order是大表):
-- 推荐:小表user驱动大表order(INNER JOIN中MySQL会自动优化,但LEFT JOIN需显式控制)
SELECT u.username, o.order_no 
FROM user u 
INNER JOIN `order` o ON u.id = o.user_id;

-- LEFT JOIN需注意:左表是驱动表,因此左表必须是小表
SELECT u.username, o.order_no 
FROM user u  -- 小表放左表(驱动表)
LEFT JOIN `order` o ON u.id = o.user_id;

3. 注意点

  • INNER JOIN:MySQL 优化器会自动选择小表作为驱动表,无需手动调整;
  • LEFT JOIN:驱动表固定为左表,因此必须将小表放在左表位置(否则会用大表驱动小表,性能暴跌);
  • RIGHT JOIN:驱动表固定为右表,需将小表放在右表位置(实际开发中建议用LEFT JOIN替代,更符合阅读习惯)。

3. 优化2:避免 CROSS JOIN(笛卡尔积)

1. CROSS JOIN 的危害

CROSS JOIN会返回两张表的笛卡尔积(行数 = 表 A 行数 × 表 B 行数),若表 A 有 1 万行、表 B 有 10 万行,结果会有 10 亿行,直接导致数据库 CPU/IO 打满、查询超时。

2. 常见触发场景

  • 显式使用CROSS JOIN关键字且无ON条件;
  • JOIN时遗漏ON关联条件(如SELECT * FROM A JOIN B);
  • WHERE条件无法过滤笛卡尔积(如关联字段值全为 NULL)。

3. 如何避免

  • 任何JOIN必须加ON关联条件(即使是INNER JOIN);
  • 禁止显式使用无关联条件的CROSS JOIN
  • 若需关联但无直接字段,通过业务逻辑补充关联条件(如时间范围、状态过滤)。

4. 优化补充

1. 被驱动表的关联字段必须加索引
被驱动表的ON字段(如order.user_id)需建索引,否则内层循环会全表扫描,即使小表驱动大表也会很慢。

2. 避免SELECT *
只查询需要的字段,减少数据传输量(尤其大表有大字段如TEXT时)。

3. 提前过滤数据
WHERE先过滤驱动表和被驱动表的无效数据(如WHERE o.status=1),减少参与 JOIN 的行数。

5. 总结

MySQL JOIN 优化的核心是:

  • 小表驱动大表:利用嵌套循环的特性减少外层循环次数,LEFT JOIN需手动控制驱动表;
  • 杜绝笛卡尔积:JOIN必须加ON条件,避免CROSS JOIN
  • 索引兜底:被驱动表的关联字段加索引,是 JOIN 性能的基础保障。

08. count的性能差异

count(1)、count(*)、count(字段)

1. count 的本质

count 的本质:统计 “非 NULL 行数”。

count()是聚合函数,核心作用是统计查询结果集中符合条件的 “非 NULL 行数”,不同参数的差异在于 “统计范围” 和 “是否需要判断 NULL”,这直接决定性能。

2. 三种 count 的区别与性能对比

1. count(*)

统计所有行数(含 NULL)。

逻辑:专门用于统计表 / 结果集的总行数。
MySQL 对其有特殊优化:会优先选择最小的非聚集索引(覆盖索引)扫描,若没有则用聚集索引,避免全表扫描。

特点:包含 NULL 值(因为不判断字段,只统计行数),性能最优。

2. count(1)

统计所有行数(占位符)。

逻辑:用常量1作为占位符,统计所有行数(无论字段是否为 NULL)。
MySQL 优化器会将其与count(*)视为等价,底层执行计划完全相同。

特点:性能与count(*)几乎无差异,属于 “语法糖”。

3. count(字段)

统计字段非 NULL 的行数。

逻辑:需逐行检查该字段是否为 NULL,仅统计非 NULL 的行数。性能分两种情况:

  • 字段是索引字段:用索引扫描(比全表快),但需判断 NULL
  • 字段是非索引字段:全表扫描 + 判断 NULL,性能最差。

特点:性能低于count(*)/count(1),且结果可能不等于总行数(因为字段可能为 NULL)。

3. 性能排序

从快到慢:count(*)count(1) > ount(索引字段) > count(非索引字段)

关键原因:

  • count(*)/count(1)无需判断字段 NULL,且 MySQL 会选最优索引;
  • count(字段)需额外判断 NULL,非索引字段还需全表扫描。

4. 实践建议

  • 统计总行数:优先用count(*)(语义最清晰,MySQL 优化最好),而非count(1)count(主键)
  • 统计字段非 NULL 行数:若需此逻辑,确保字段加索引(如count(user_name)user_name有索引);
  • 避免误区:
    • 认为count(主键)更快:实际count(*)会选更小的索引,比主键索引(聚集索引)扫描更快;
    • 认为count(1)count(*)快:MySQL 优化后两者无区别,count(*)更符合语义。

5. 总结

  • 核心差异:count(*)/count(1)统计总行数(含 NULL),性能最优;count(字段)统计非 NULL 行数,性能较差;
  • 实践选择:统计总数用count(*),统计非 NULL 字段用count(索引字段)
  • 优化关键:利用 MySQL 对count(*)的索引优化,避免count(非索引字段)的全表扫描。

6. 扩展

统计useremailnull的总行数,email是普通索引,是SELECT COUNT(email) FROM user快,还是SELECT COUNT(*) FROM user WHERE email IS NOT NULL快?

答:基本相同,但是不推荐使用后者,推荐使用COUNT(email)。因为这个全程是索引,虽然需要索引扫描,但是后者使用了WHERE条件,同样也是需要进行索引扫描,同时还多出了一步条件过滤。同时COUNT(email)语义更明确。

09. INJOIN哪个快

先明确两者的底层逻辑

  • IN:属于 “子查询 / 值列表匹配”,本质是将子查询结果加载到内存后做匹配(或直接匹配值列表),适合 “单字段匹配” 场景;
  • JOIN:属于 “多表关联”,底层以Nested Loop Join为主(小表驱动大表),通过关联字段索引直接匹配,适合 “多字段关联取值” 场景。

  • IN的优势:语法简单,适合 “单字段匹配” 的简单场景;
  • JOIN的优势:支持多字段关联,大结果集下利用索引更高效,是复杂关联的首选。

10. MySQL的函数

MySQL 函数按功能可分为内置函数(系统提供,重点)和自定义函数(用户编写)。

1. 核心内置函数

1. 字符串函数

处理文本数据。

用于字符串拼接、截取、替换等,是业务中最常用的函数类别。

  • CONCAT(str1, str2,...):字符串拼接(如:拼接用户姓名和手机号:CONCAT(username, '-', mobile));
  • SUBSTRING(str, pos, len):截取子串(如:取手机号后 4 位:SUBSTRING(mobile, 8, 4));
  • REPLACE(str, old, new):替换字符串(如:清理内容中的特殊字符:REPLACE(content, '<', ''));
  • LENGTH(str):获取字符串长度(如:校验用户名长度:WHERE LENGTH(username) > 6)。

实践:用户信息格式化、文本内容清洗场景高频使用。

2. 数值函数

处理数字运算。

用于数值计算、取整、进制转换等。

  • ROUND(num, n):四舍五入(如:金额保留 2 位小数:ROUND(amount, 2));
  • ABS(num):取绝对值(如:计算差值的绝对值:ABS(score1 - score2));
  • CEIL(num)/FLOOR(num):向上 / 向下取整(如:订单数量向上取整:CEIL(total/10));
  • SUM(num)/AVG(num):求和 / 平均值(报表统计核心函数)。

实践:金额计算、数据统计场景必备。

3. 日期时间函数

处理时间数据。

用于时间获取、格式化、差值计算,是业务中仅次于字符串函数的高频函数。

  • NOW()/CURDATE():获取当前时间(含时分秒)/ 当前日期(仅年月日);
  • DATE_FORMAT(date, fmt):时间格式化(如:订单时间转字符串:DATE_FORMAT(create_time, '%Y-%m-%d %H:%i:%s'));
  • DATEDIFF(date1, date2):计算日期差值(如:用户注册天数:DATEDIFF(NOW(), register_time));
  • DATE_ADD(date, INTERVAL expr unit):时间增减(如:计算 7 天后的日期:DATE_ADD(NOW(), INTERVAL 7 DAY))。

实践:订单时间筛选、会员有效期计算、报表时间维度统计。

4. 聚合函数

数据统计分析。

用于对多行数据进行聚合计算,需结合GROUP BY使用。

  • COUNT(*):统计总行数(用户数、订单数统计);
  • SUM(field):求和(销售额、销量统计);
  • MAX(field)/MIN(field):最大值 / 最小值(最高订单金额、最早注册时间);
  • GROUP_CONCAT(field):分组拼接字符串(如:查询用户的所有订单号:GROUP_CONCAT(order_no))。

实践:报表系统、数据看板的核心函数。

5. 条件函数

逻辑判断。

用于实现 SQL 中的条件分支,替代复杂的WHERE判断。

  • IF(condition, val1, val2):简单条件判断(如:判断用户状态:IF(status=1, '正常', '禁用'));
  • CASE WHEN:复杂条件分支(如:订单状态映射:CASE status WHEN 0 THEN '待支付' WHEN 1 THEN '已支付' ELSE '已取消' END);
  • IFNULL(val, default)NULL 值替换(如:用户昵称为空时显示默认值:IFNULL(nickname, '游客'))。

实践:查询结果格式化、动态状态展示。

6. 其他常用函数

  • UUID():生成唯一标识符(分布式场景临时 ID 生成);
  • INSTR(str, substr):查找子串位置(如:判断内容是否含关键词:INSTR(content, 'MySQL') > 0);
  • CAST(val AS type):类型转换(如:字符串转数字:CAST(score AS UNSIGNED))。

2. 面试高频考点

函数使用的性能陷阱。

1. 函数操作索引字段会导致索引失效

这是面试必问点!若对索引字段使用函数,MySQL 无法使用索引,会触发全表扫描。

  • 反例:WHERE DATE(create_time) = '2024-01-01'(对索引字段create_timeDATE()函数,索引失效);
  • 正例:WHERE create_time >= '2024-01-01' AND create_time < '2024-01-02'(直接匹配字段范围,利用索引)。这个之所以能利用索引,是因为没有使用函数破坏索引的有序性,同时MySQL将给出的查询条件值'2024-01-01''2024-01-02'隐式的转换为了DATETIME类型,从而避免了将整个表的create_time字段转成其他类型,保证了原索引的可用性。

2. 聚合函数的NULL处理

  • COUNT(field)会忽略 NULL 值,COUNT(*)统计所有行(含 NULL);
  • SUM(field)NULL 值会被视为 0,但建议用IFNULL(field, 0)显式处理(避免语义歧义)。

3. 自定义函数的慎用场景

  • 自定义函数(CREATE FUNCTION)可实现复杂逻辑,但执行效率低(无法并行执行),且可能导致锁表,高并发场景建议用存储过程或应用层代码替代。

3. 总结

MySQL 函数是提升 SQL 灵活性的核心工具。

  • 分类记忆:重点掌握字符串、日期、聚合、条件函数的常用用法;
  • 性能陷阱:索引字段避免使用函数,否则索引失效;
  • 实践优先:简单逻辑用内置函数,复杂逻辑优先应用层处理(避免 SQL 臃肿)。

回目录: 《面试笔记:MySQL 相关目录》
上一篇: 《面试笔记:MySQL 相关02 – 索引》

喜欢面试笔记:MySQL 相关03 – SQL语法与查询优化这篇文章吗?您可以点击浏览我的博客主页 发现更多技术分享与生活趣事。

  •  

面试笔记:MySQL 相关02 – 索引

感谢订阅陶其的个人博客!

索引

回目录: 《面试笔记:MySQL 相关目录》
上一篇: 《面试笔记:MySQL 相关01 – 基础核心》
下一篇: 《面试笔记:MySQL 相关03 – SQL语法与查询优化》


01. 各种树的原理和特性

学习树,是为了便于学习索引。索引的核心作用是“加速查询”,而高效的树结构正是实现这一点的关键。

答:

1. “树”形象的理解

可以从“现实中的树”类比数据结构的“树”,它和路边的树长得很像,只是“倒过来”了。

想象一棵简化的苹果树:

  • 最底下的 “树根” 是起点,对应数据结构中树的根节点(只有一个);
  • 从树根往上长的 “树干” 会分杈出 “树枝”,这些树枝就是父节点;
  • 树枝再分杈出更细的枝桠,这些细枝桠就是子节点(一个父节点可以有多个子节点);
  • 最顶端的 “苹果”(没有再分杈的部分),就是叶子节点。
    数据结构里的 “树”,就是这样一种 “分层、有分支” 的结构:
  • 所有节点(根、父、子、叶子)都像 “果实” 一样,存储着数据(或索引);
  • 节点之间的 “连接”(比如根到父、父到子)像 “树枝”,表示数据之间的关系;
  • 整个结构是 “单向” 的:只能从根往下找子节点,不能从子节点反推回根(就像苹果不会自己长回树枝)。

举个更具体的例子:

  • 如果用树存 “班级学生名单”,根节点可以是 “三年级”;
  • 根节点的子节点(父节点)可以是 “三班”“五班”;
  • “三班” 的子节点可以是 “男生组”“女生组”;
  • “男生组” 的子节点(叶子节点)就是具体的学生:“小明”“小李”…

这种结构的核心好处是:查找效率高。
比如想找 “三年级三班男生组的小明”,你不用遍历所有学生,只需从根(三年级)→ 三班 → 男生组 → 小明,一步一步按 “分支” 找,比在一堆乱序的名单里翻快得多。

2. 二叉查找树

“不合适”的树(Binary Search Tree)

特性:每个节点最多 2 个子节点(左小右大),查询时从根节点开始,比当前节点小就走左子树,大就走右子树。

问题:容易 “失衡”。比如插入一串递增数据,会退化成链表(左子树为空,只有右子树),查询效率从 O (logn) 暴跌到 O (n)(和遍历链表一样慢)。

为什么 MySQL 不用:数据库索引需要稳定高效的查询,二叉树的失衡问题无法满足。

3. 平衡二叉树

又叫:AVL树/红黑树
解决 “失衡”,但仍有局限

特性:在二叉查找树基础上,通过旋转保持 “平衡”(左右子树高度差不超过 1),确保查询效率稳定在 O (logn)。

问题:还是 “二叉”(每个节点最多 2 个子节点),导致树的 “高度过高”。
比如存 100 万条数据,平衡二叉树的高度大概是 20 层(2^20≈100 万)。

为什么 MySQL 不用:索引数据存在磁盘上,每次查询需要从磁盘读数据(一次 IO 读一个 “页”,比如 4KB)。树高 20 就意味着最多要读 20 次磁盘,IO 成本太高。

4. B 树

多路平衡查找树,降低高度

特性:

  • “多路”:每个节点可以有多个子节点(比如 100 个),不再是二叉。
  • “平衡”:所有叶子节点在同一层,避免某条路径过长。
  • 节点存 “数据”:每个节点不仅存索引键,还直接存对应的数据(或数据地址)。

优势:高度大大降低。比如每个节点有 100 个子节点,存 100 万数据,树高只需 3 层(100^3=100 万),最多 3 次 IO 就能查到数据,比平衡二叉树高效得多。

为什么 MySQL 不完全用它:范围查询不方便。比如查 “id 从 100 到 1000”,B 树需要回溯父节点找下一个范围,效率低。

5. B + 树

MySQL 索引的 “标准答案”
B + 树是 B 树的变种,专门为数据库索引优化设计,核心特性完美适配索引需求:

非叶子节点只存索引,不存数据:

  • 非叶子节点仅保留索引键(比如 id),不存实际数据,这样一个节点能存更多索引键,子节点数量更多(比如 200 个),树高更低(100 万数据只需 2-3 层),IO 次数更少。

叶子节点存完整数据,且首尾相连:

  • 所有实际数据只存在叶子节点,且叶子节点之间用链表连接(形成有序链表)。

对索引的核心价值:

  • 单值查询快:和 B 树一样,通过索引键快速定位到叶子节点,IO 少。
  • 范围查询无敌:比如查 “id>100”,找到第一个 id=100 的叶子节点后,直接顺着链表往后扫,不用回溯父节点,效率极高(MySQL 中范围查询很频繁,这是关键)。

6. MySQL 为什么选 B + 树做索引?

  1. 树高低,IO 次数少(磁盘读写效率高);
  2. 叶子节点有序相连,范围查询(如 between、in)效率远超其他树;
  3. 非叶子节点只存索引,节点存储密度高,进一步降低树高。

02. 索引的作用

答:

  1. 加速查询,降低IO:索引通过有序结构(如B+树)将分散的数据按索引键排序,查询时无需全表扫描,而是通过索引快速定位目标数据所在的磁盘位置,大幅减少磁盘IO次数(比如从扫描全表100万行降到扫描几十行);

  2. 保证数据唯一性,强化业务约束:通过唯一索引(含主键索引),数据库会强制约束索引键的值不重复,直接实现 “业务唯一标识” 需求(如用户 ID、订单号不可重复),避免手动校验的繁琐和风险。

  3. 优化排序与分组操作:索引本身是有序的,当查询包含order by(排序)、group by(分组)时,可直接利用索引的有序性避免全表数据的额外排序(减少内存 / 磁盘临时表开销),提升这类操作的效率。

03. 常见索引类型及其特性

答:

1. 主键索引

Primary Key

特性:

  • 强制唯一性(表中唯一标识一行数据),且不允许NULL值;
  • 一张表只能有一个主键索引;
  • InnoDB 中主键索引是聚簇索引(叶子节点直接存储整行数据),无需回表,查询效率最高。

适用场景:作为表的唯一标识(如用户 ID、订单 ID),确保数据唯一性并加速行查询。

2. 唯一索引

Unique Index

特性:

  • 确保索引列的值唯一,但允许NULL值(且NULL可出现多次);
  • 一张表可创建多个唯一索引;
  • 基于 B + 树结构,非聚簇索引(叶子节点存主键值,需回表查数据);
  • 查询时找到一个匹配值就停止扫描(无需确认是否有重复),比普通索引少了 “继续校验” 的步骤,回表前的索引定位效率更高。

适用场景:需唯一约束但非主键的字段(如手机号、邮箱,允许未填写即NULL)。

3. 普通索引

Normal Index

特性:

  • 最基础的索引类型,无唯一性约束,允许重复值和NULL;
  • 基于 B + 树结构,非聚簇索引,仅用于加速查询,不影响数据本身的约束;
  • 查询时需扫描所有匹配的索引节点(可能有多个重复值),再批量回表取数据,比唯一索引多了 “扫描重复索引” 的开销。

适用场景:高频查询的非唯一字段(如商品分类、用户昵称),单纯提升查询效率。

4. 联合索引

Composite Index

特性:

  • 由多个字段组合创建(如(a, b, c)),遵循最左前缀原则(查询需包含最左字段才能命中索引);
  • 索引按第一个字段排序,第一个字段相同则按第二个,以此类推;
  • 可覆盖多字段查询,减少回表(如查询a, b时,若联合索引包含a, b,则无需回表);
  • 效率依赖查询条件:仅命中最左前缀且为覆盖索引时(无需回表),效率接近普通索引;若未命中全部前缀或需回表,效率会下降。
  • 整体比普通索引慢的核心:索引结构是多字段排序,定位时需匹配多个字段的有序性,逻辑比单字段索引复杂。

适用场景:多字段组合查询(如 “按用户 ID + 订单状态查询”),需合理设计字段顺序(区分度高的字段放左侧)。

5. 前缀索引

Prefix Index

特性:

  • 仅对字符串字段的前 N 个字符创建索引(如index(name(10))),大幅节省存储空间,但索引选择性低(重复率高);
  • 适用于长字符串(如 URL、长文本),但可能降低索引选择性(重复率升高),且无法用于order by/group by或覆盖索引。
  • 查询时需扫描更多索引节点才能定位目标,且无法使用覆盖索引(必须回表),额外增加 IO 开销。

适用场景:长字符串字段的模糊查询(如like ‘abc%’),平衡空间与查询效率。

6. 全文索引

Full-Text Index

特性:

  • 针对大文本内容(如文章、评论)的关键词搜索,针对大文本分词匹配(而非like的字符匹配,非 B + 树精确查找,需先分词、匹配倒排索引,再关联原表数据);
  • 效率远高于like ‘%关键词%’,但仅支持MATCH() AGAINST()语法,且有最小 / 最大词长限制。
  • 逻辑复杂度远高于其他索引,仅适合关键词检索,单值精确查询效率远低于 B + 树结构的索引。

适用场景:全文检索需求(如博客系统的文章关键词搜索)。

7. 索引效率排序

这些索引类型,按照查询速度和效率,从高到低排序

基于InnoDB引擎、但是精确查询场景:
查询效率从高到低排序:主键索引 > 唯一索引 > 普通索引 > 联合索引(命中最左前缀 + 覆盖索引)> 前缀索引 > 全文索引
但是并非绝对,依旧需要结合使用场景进行判断。
排序前提:默认是「单值精确查询」,若为「范围查询」,主键索引和唯一索引的差距会缩小(范围查询需扫描多个节点)。

04. 聚簇索引与非聚簇索引

答:
聚簇索引与非聚簇索引的核心区别是:数据存储位置
聚簇索引,又叫做聚集索引,它的叶子节点存储整行数据;
非聚簇索引的叶子节点仅存储 “索引键 + 主键值”,需通过主键回表查询完整数据(覆盖索引除外)。

05. 最左前缀原则

答:
联合索引的最左前缀原则:
查询条件必须从联合索引的最左字段开始匹配,且连续匹配,才能用到索引的对应部分。

举个例子:联合索引(name, age, score)

  • 能命中索引的情况(从左到右连续匹配):

    • where name = '张三'(只用name,命中索引的name部分);
    • where name = '张三' and age = 20(用name+age,命中索引的name+age部分);
    • where name = '张三' and age = 20 and score > 90(用name+age+score,命中索引的全部三部分)。
  • 不能命中索引的情况(跳过左字段或不连续):

    • where age = 20(跳过最左的name,完全无法命中);
    • where name = '张三' and score > 90(跳过中间的age,只能命中name部分,score无法利用索引)。

联合索引含范围查询或模糊查询能否命中索引?
对于范围查询(大于、小于等)会导致“当前字段右侧的索引字段失效”,但左侧已匹配的字段仍能命中,(当前字段也能命中);
对于模糊查询,如果是前缀模糊(例如:"快乐大本营",like '快乐%'),能命中当前字段及左侧的索引,但当前字段右侧的索引失效;如果是后缀/全模糊查询(例如:%大本营%乐大%),会导致当前字段及右侧字段的索引都失效,仅当前字段左侧已匹配索引有效。

06. 索引的维护成本

答:
MySQL索引的维护成本本质是“空间换时间”的代价,主要集中在“写入性能损耗”、“存储空间占用”、“索引碎片维护”。索引数量越多,结构越复杂,则维护成本越高。

1. 写入操作(插入/更新/删除)的性能损耗

  • 写入数据时,不仅要修改表数据,还需同步维护索引的 B + 树结构。
  • 插入:可能触发 B + 树节点分裂(如节点满时),需重新组织索引排序;若为唯一索引,还需额外校验唯一性(扫描索引确认无重复)。
  • 更新:若更新的是索引列,需先删除旧索引条目,再插入新索引条目,相当于两次索引操作;非索引列更新不影响索引,但仍需维护聚簇索引的物理顺序(InnoDB)。
  • 删除:不会立即释放索引空间,仅标记为 “删除”,后续需通过碎片整理回收,且可能触发 B + 树节点合并(如节点数据过少时)。

2. 存储空间的额外占用

  • 索引需独立存储 B + 树结构,一张表的索引越多、字段越长,占用的磁盘空间越大。
  • 示例:大表的联合索引、长字符串的前缀索引,可能占用与数据本身相当的存储空间;聚簇索引虽无需额外存数据,但非聚簇索引(如唯一、普通索引)需存储主键值,叠加后空间开销显著。

3. 索引碎片与定期维护开销

  • 频繁插入 / 删除后,B + 树会产生 “空洞”(已删除但未释放的空间),导致索引碎片增多。
  • 碎片会降低查询效率(磁盘 IO 增多),需定期执行维护操作(如OPTIMIZE TABLE、重建索引),这些操作会锁表或占用大量 IO 资源,影响业务高峰期性能。

4. 优化器的决策负担

  • 表中索引过多时,MySQL 优化器需遍历所有可能的索引组合,评估最优查询计划,导致查询解析时间变长(尤其复杂查询场景)。

5. 如何优化维护策略

  • 不同索引的维护成本差异:联合索引 > 普通索引 > 唯一索引 > 主键索引(结构越复杂,维护时 B + 树调整逻辑越繁琐);前缀索引、全文索引的维护成本高于单字段索引(前者需处理部分字符,后者需维护分词倒排索引)。
  • 平衡维护成本的核心原则:避免 “过度索引”,仅为高频查询字段创建索引;优先选择窄索引(短字段、少字段组合),减少存储空间和维护开销;定期监控索引使用率,删除无效索引(如长期未被使用的索引)。

07. 回表查询与索引覆盖

答:
回表查询是 “非聚簇索引查询后需二次查聚簇索引拿完整数据” 的过程;
索引覆盖是 “查询字段(即所需的返回字段)全在索引中,无需二次查询” 的优化场景。
二者是 “需回表” 与 “免回表” 的对立关系,直接影响查询效率。

1. 回表查询(Bookmark Lookup)

1. 定义
当使用非聚簇索引(如唯一索引、普通索引、联合索引)查询时,若查询字段未完全包含在该索引中,需先通过非聚簇索引找到 “索引键 + 主键值”,再用主键值查询聚簇索引(主键索引) ,才能获取整行完整数据,这个 “二次查询” 的过程就是回表。

2. 核心逻辑(结合 InnoDB 引擎)

  • 非聚簇索引的叶子节点仅存储 “索引键 + 主键值”,不存完整行数据。
  • 若查询需要非索引字段(如用唯一索引email查user表的主键id和非索引字段name),必须通过主键值回聚簇索引 “兜底”,才能拿到name。

3. 示例

-- 表结构:id(主键)、email(唯一索引)、name(非索引字段)
select id, name from user where email = 'a@test.com';

第一步:通过唯一索引email(非聚簇)找到主键id=100
第二步:用id=100查询聚簇索引,拿到name字段,完成查询;
这两步共同构成回表查询,额外增加了一次聚簇索引查询的 IO 开销。

2. 索引覆盖(Covering Index)

1. 定义
当查询的所有字段(包括筛选条件、返回字段)都包含在某一个索引中时,则无需回表,仅通过该索引就能获取所有需要的数据,这个索引就是 “覆盖索引”,也叫紧凑索引,对应的查询就是覆盖索引查询

2. 核心优势
避免回表,减少一次磁盘 IO(聚簇索引查询),大幅提升效率;
索引数据量远小于全表数据,查询时扫描的数据量更少。

3. 示例

-- 联合索引:idx_email_name(email, name)(包含email和name字段)
select email, name from user where email = 'a@test.com';

查询的筛选字段email、返回字段name均在联合索引中;
直接通过该联合索引就能拿到所有需要的数据(返回字段),无需回聚簇索引,实现 “一次查询完成”。

3. 补充项

触发条件对比:

  • 回表:非聚簇索引 + 查询字段超出索引范围;
  • 索引覆盖:查询字段(筛选 + 返回)完全匹配某一索引(单字段索引或联合索引)。

实战价值:
索引覆盖是优化回表开销的核心手段,设计索引时可将高频查询字段加入联合索引(如idx_userid_status(user_id, status)),避免回表;
聚簇索引查询天然支持 “索引覆盖”(叶子节点存完整数据),无需回表,这也是其查询效率最高的原因之一。

易混点:
索引覆盖的关键是 “字段全包含”,与索引类型无关(单字段索引、联合索引均可作为覆盖索引);
前缀索引无法实现索引覆盖(仅存字段前 N 个字符,无法返回完整字段值)。

4. 不能触发索引覆盖示例

SELECT id, name, email FROM user WHERE email = 'xxx@qq.com';
-- 在user表中,id是主键,email是唯一索引,name是普通索引

结论:不能触发索引覆盖。
索引覆盖的核心要求是查询的所有字段(筛选条件 + 返回字段)必须完全包含在同一个索引中,跨索引无法实现覆盖。

如何修改才能触发索引覆盖?

  • 创建联合唯一索引 unique index idx_email_name(email, name)
  • 筛选字段email、返回字段emailnameid(主键会自动包含在非聚簇索引中),全部包含在这个联合索引里;
  • 查询时无需回表,直接通过该联合索引就能获取所有需要的数据,触发索引覆盖。

08. 如何合理使用索引?

以业务查询为导向,平衡查询效率与维护成本,避免 “过度索引” 或 “无效索引”。

  1. 优先为 “高频查询字段” 建索引,低频查询不建;
  2. 联合索引需遵循 “最左前缀 + 区分度优先(区分度高,重复值少)” 原则;
  3. 避免索引失效场景,确保索引被正确使用;
  4. 利用 “索引覆盖” 减少回表,提升效率;
  5. 根据字段特性选择合适的索引类型;
  6. 定期维护索引,清理无效索引。

合理使用索引的核心是 “按需设计、避免失效、注重维护”。
“索引不是越多越好,而是越合适越好”。

09. 索引失效场景

MySQL 索引失效的本质是:查询条件破坏了索引的有序性或匹配规则;
常见场景集中在:“字段操作”、“类型不匹配”、“查询语法不当”三类;
避免核心是:让查询条件贴合索引设计规则。

以下是常见索引失效场景 + 避免方法:

1. 对索引字段做函数 / 运算操作

失效场景:查询时对索引字段用函数(如substrdate_format)或数学运算,会导致 MySQL 无法利用索引的有序性,只能全表扫描。

  • 示例:where substr(name, 1, 3) = '张三'(name是普通索引)、where age + 1 = 20(age是普通索引)。

避免方法:将函数 / 运算移到等号右侧,或提前计算结果。

  • 优化后:where name like '张三%'where age = 19

2. 隐式类型转换

失效场景:查询条件中字段类型与传入值类型不匹配,MySQL 会自动做类型转换,导致索引失效。

  • 示例:where phone = '13800138000'(phone是int类型,字符串转数字)、where id = '100'(id是int,字符串转数字)。
    避免方法:确保查询值类型与字段类型完全一致。
  • 优化后:where phone = 13800138000where id = 100

3. 模糊查询(非前缀匹配)

失效场景:like的模糊查询若以%开头(后缀模糊 / 全模糊),会破坏索引有序性,导致索引失效;前缀模糊(%在末尾)可正常使用索引。

  • 示例:where name like '%张三'(后缀模糊,失效)、where name like '%张三%'(全模糊,失效)。
    避免方法:优先用前缀模糊查询;若需全模糊,改用全文索引(如fulltext index)或应用层分词。
  • 优化后:where name like '张三%'(前缀模糊)、MATCH(name) AGAINST('张三')(全文索引)。

4. 联合索引不满足最左前缀原则

失效场景:联合索引(如(a, b, c))需从左到右连续匹配,跳过左侧字段或不连续匹配,会导致索引失效或部分失效。

  • 示例:where b = 2 and c = 3(跳过最左a,全失效)、where a = 1 and c = 3(跳过中间b,仅a部分生效,c失效)。
    避免方法:查询条件需包含联合索引的最左字段,且按索引字段顺序匹配;若高频查询b + c,可单独建联合索引(b, c)
  • 优化后:where a = 1 and b = 2 and c = 3(全生效)、where b = 2 and c = 3(改用(b, c)联合索引)。

5. 范围查询后字段失效

失效场景:联合索引中,某字段用><>=<=between做范围查询后,其右侧的索引字段会失效。

  • 示例:where a = 1 and b > 2 and c = 3(联合索引(a, b, c)c失效)。
    避免方法:将范围查询字段放在联合索引的最右侧;若需多字段范围查询,改用覆盖索引或拆分查询。
  • 优化后:where a = 1 and b > 2(仅用a + b索引)、建联合索引(a, b, c)并确保查询字段覆盖(select a, b, c,避免回表)。

6. or连接非索引字段

失效场景:or连接的查询条件中,若有一个字段无索引,会导致整个查询无法使用索引(MySQL 会选择全表扫描)。

  • 示例:where name = '张三' or address = '北京'name有索引,address无索引,全失效)。
    避免方法:确保or连接的所有字段都有索引;或改用union all拆分查询(需字段一致)。
  • 优化后:where name = '张三' union all where address = '北京'address补建索引)。

7. is not null/not in/not exists

失效场景:对索引字段用is not null(部分场景失效)、not innot exists,会破坏索引的匹配逻辑,导致失效(is null通常可使用索引)。

  • 示例:where name is not nullname是普通索引,可能失效)、where id not in (1,2,3)id是主键索引,大数据量下失效)。
    避免方法:is not null改用union拼接非空结果;not in改用left join ... on ... is null;小数据量not in可接受,大数据量必优化。
  • 优化后:where id in (select id from user) union ...is not null替代)、select * from user u left join tmp t on u.id = t.id where t.id is nullnot in替代)。

8. 索引选择性差(失效等价场景)

失效场景:索引字段重复率极高(如 “性别” 字段,仅男 / 女),MySQL 优化器会判断 “全表扫描比索引查询更快”,主动放弃使用索引。

  • 示例:where gender = '男'gender建了索引,但全表 80% 是男性,索引失效)。
    避免方法:不针对低选择性字段建单字段索引;若需查询,将其作为联合索引的右侧字段(如(age, gender)),通过高选择性字段先过滤。

9. 补充

  1. 验证索引是否失效的核心方法:explain分析执行计划,若key字段为NULL或非目标索引,说明索引失效。
  2. 特殊情况:Innodb的聚簇索引(主键)即使有上述场景(如is not null),通常也不会完全失效,因聚簇索引的物理存储特性,优化器更倾向使用。
  3. 避免失效的核心原则:不破坏索引的有序性、不改变字段的原始形态、让查询条件贴合索引设计。

10. explain执行计划分析

explain 是 MySQL 分析 SQL 执行计划的核心工具。
通过输出 12 个字段(面试重点关注 7 个核心字段),可判断索引是否生效、查询是否全表扫描、是否存在文件排序 / 临时表等性能问题。

1. explain 核心作用

  1. 判断 SQL 是否使用了目标索引(避免索引失效);
  2. 识别全表扫描、文件排序、临时表等低效操作;
  3. 分析表的连接顺序、查询类型(简单 / 复杂查询);
  4. 预估查询扫描的行数,评估查询效率。

2. 7 个核心字段解析

1. type:访问类型

核心,判断查询效率的关键

含义:表示 MySQL 如何访问表中的数据(即查询方式),取值决定查询效率,从优到差排序:
system > const > eq_ref > ref > range > index > ALL

关键取值解读:

  • system:表中只有 1 行数据(如系统表),效率最高(罕见);
  • const:通过主键 / 唯一索引查询,匹配 1 行数据(如where id=100),高效;
  • eq_ref:多表连接时,被连接表通过主键 / 唯一索引匹配,每行只返回 1 行(如join on 主键);
  • ref:通过普通索引 / 联合索引前缀匹配,返回多行匹配数据(如where name=’张三’,name 是普通索引);
  • range:范围查询(><betweenin),只扫描索引的某一范围(比ref差,但比全表扫描好);
  • index:扫描整个索引树(索引全扫描),比ALL好(索引数据量小于全表);
  • ALL:全表扫描(最差),需避免(通常是索引失效或未建索引)。

判断标准:type至少要达到range级别,最优是refconst,出现ALL说明存在性能问题。

2. key:实际使用的索引

含义:表示 MySQL 实际选择的索引(若为NULL,说明未使用任何索引,索引失效或无合适索引);
判断标准:若key不是你设计的目标索引(如预期用idx_email,但keyNULL),说明索引失效,需排查原因(结合索引失效场景)。

3. rows:预估扫描行数

含义:MySQL 优化器预估的、查询需要扫描的行数(非精确值,但可反映效率);
判断标准:行数越少越好,若rows远大于表中实际数据量,可能是统计信息过时(需执行analyze table更新),或索引设计不合理。

4. extra:额外执行信息

核心!暴露性能隐患

含义:记录 SQL 执行的额外操作,重点关注 “好的标识” 和 “坏的标识”:

  • 优质标识:
    • Using index:触发覆盖索引,无需回表(高效,面试加分点);
    • Using index condition:索引下推(ICP),减少回表次数(高效)。
  • 性能隐患标识(必须避免):
    • Using filesort:需在内存 / 磁盘中排序(未利用索引有序性,如order by字段无索引);
    • Using temporary:创建临时表存储中间结果(如group by无索引、多表连接无合适索引,性能极差);
    • Using where:全表扫描后过滤数据(type=ALL时出现,说明无索引可用);
    • Using join buffer:多表连接时未用索引,使用连接缓冲区(低效)。

5. id:查询执行顺序

含义:表示查询中每个select子句的执行顺序(数字越大越先执行,相同数字按从上到下顺序);
应用场景:复杂查询(子查询、join)中,判断表的连接顺序是否合理(如小表驱动大表)。

6. select_type:查询类型

含义:区分简单查询和复杂查询,面试高频取值:

  • SIMPLE:简单查询(无子查询、无 union);
  • SUBQUERY:子查询(select中嵌套select);
  • DERIVED:派生表(from中嵌套select);
  • UNIONunion连接的第二个及以后的查询。

面试价值:说明查询的复杂程度,复杂查询(如多层子查询)可能导致优化器选择低效执行计划,需考虑拆分 SQL。

7. table:当前查询的表

含义:显示 SQL 查询的表名(或别名),多表连接时按id顺序显示表的执行顺序。

加项

  1. 如何判断索引是否生效:
    看key字段是否为目标索引(非NULL),同时type不是ALL/index

  2. 如何判断查询是否高效:
    typerange + keyNULL + rows值小 + extrafilesort/temporary

  3. 常见问题排查:

    • type=ALL + key=NULL:索引失效或未建索引,排查索引失效场景;
    • extrafilesortorder by/group by字段未建索引,需添加索引;
    • extratemporarygroup by无索引或多表连接无合适索引,优化索引设计。
  4. explain extended:在explain基础上增加filtered字段(过滤行数占比),filtered越高说明过滤效果越好;

  5. explain format=json:输出 JSON 格式的详细执行计划,适合复杂查询分析;

  6. 执行计划是 “预估” 而非 “实际”:优化器可能因统计信息过时、索引选择性差等误判,需结合实际执行耗时验证。

11. 升序索引与降序索引

在 MySQL 中,索引的升序(ASC)和降序(DESC)是通过创建索引时指定排序方向来设置的,核心目的是匹配查询中ORDER BY的排序需求,避免额外的文件排序(Using filesort)或反向扫描带来的额外开销。

ORDER BY排序时,如果排序字段存在索引,但是索引的顺序与排序的顺序不一致,则排序时导致优化器选择“反向扫描索引”,性能虽然比文件排序快非常多,但是依旧不如排序和索引同向的效果好。

比如:create_time字段存在索引idx_create_time,索引是升序(ASC)的,但是查询排序时,需要逆序排序(比如ORDER BY create_time DESC),那么优化器选择“反向扫描索引”。

1. 升序 / 降序索引的创建语法

MySQL 中创建索引时,默认是升序(ASC),可显式指定ASC(升序)或DESC(降序)。语法如下:

-- 升序索引(默认,可省略ASC)
CREATE INDEX idx_create_time_asc ON 表名(create_time ASC);

-- 降序索引(显式指定DESC)
CREATE INDEX idx_create_time_desc ON 表名(create_time DESC);

升序索引(ASC:索引中字段值按从小到大排序,适用于ORDER BY 字段 ASC的查询。
降序索引(DESC:索引中字段值按从大到小排序,适用于ORDER BY 字段 DESC的查询(如 “按创建时间逆序排序的列表”)。

这里面还存在着版本差异:

2. 5.7版本

5.7版本的限制与实践。

5.7 版本对降序索引的支持是 “语法层面” 的,实际存储仍为升序,查询时若用ORDER BY 字段 DESC,优化器会 “反向扫描升序索引”(而非直接使用降序索引),虽然比全表扫描 + 文件排序高效,但仍有一定开销。

场景:查询列表经常按create_time逆序排序(ORDER BY create_time DESC
在 5.7 中,即使创建DESC索引,实际效果与ASC索引差异不大,但仍建议显式创建DESC索引(为 8.0 升级兼容,且优化器可能优先选择):

-- 5.7中创建“伪降序索引”(语法支持,实际升序存储)
CREATE INDEX idx_create_time_desc ON order(create_time DESC);

-- 查询时,优化器会反向扫描该索引,避免文件排序
SELECT id, order_no FROM order 
WHERE status = 1 
ORDER BY create_time DESC 
LIMIT 10;

5.7 的替代方案(核心优化)

ORDER BY create_time DESC还需配合WHERE条件(如status=1),建议创建联合索引,将过滤字段放前面,排序字段放后面(显式指定DESC):

-- 联合索引:先过滤(status),再排序(create_time DESC)
CREATE INDEX idx_status_create_time_desc ON order(status, create_time DESC);

-- 该索引可同时优化WHERE和ORDER BY,避免全表扫描和文件排序
SELECT id, order_no FROM order 
WHERE status = 1 
ORDER BY create_time DESC 
LIMIT 10;

3. 8.0 版本

8.0 版本的关键优化:真正支持降序索引。

8.0 版本开始,DESC索引会真正按降序存储,查询时若ORDER BY 字段 DESC,可直接匹配降序索引,无需反向扫描,性能比 5.7 提升明显(尤其大数据量排序场景)。

场景:同样按create_time逆序排序

在 8.0 中,创建DESC索引后,ORDER BY create_time DESC会直接使用该索引,EXPLAINExtra列无Using filesort

-- 8.0中创建“真降序索引”(实际按create_time从大到小存储)
CREATE INDEX idx_create_time_desc ON order(create_time DESC);

-- 查询时直接匹配索引,无需排序
SELECT id, order_no FROM order 
ORDER BY create_time DESC 
LIMIT 10;

联合索引的降序支持(8.0 增强)
8.0 允许联合索引中部分字段指定DESC,更精准匹配复杂排序场景。

例如 “按status升序、create_time降序排序”:

-- 联合索引:status升序,create_time降序
CREATE INDEX idx_status_asc_create_time_desc ON order(status ASC, create_time DESC);

-- 完全匹配ORDER BY,无文件排序
SELECT id, order_no FROM order 
ORDER BY status ASC, create_time DESC 
LIMIT 10;

4. 实践建议

1. 优先匹配查询的排序方向:

  • 若查询常用ORDER BY create_time DESC(如订单列表默认 “最新在前”),直接创建create_time DESC索引(8.0 最佳,5.7 次之)。
  • 若排序字段同时有ASCDESC需求(极少),可分别创建升序和降序索引(但需平衡索引维护成本)。

2. 联合索引的排序字段放最后:

  • 索引遵循 “最左前缀原则”,过滤字段(WHERE条件)放前面,排序字段(ORDER BY)放后面,例如WHERE status=1 ORDER BY create_time DESC,联合索引为(status, create_time DESC)。

3. 避免过度创建降序索引:

  • 降序索引与升序索引的维护成本相同(都会占用磁盘空间,影响写入性能),仅对 “高频逆序排序” 的字段创建降序索引,低频场景无需单独创建(依赖优化器反向扫描即可)。

5. 总结

  • 创建方式:通过ASC(默认)或DESC关键字指定,语法简单。
  • 版本差异:5.7 仅语法支持降序索引(实际升序存储),8.0 真正支持(按降序存储,性能更优)。
  • 核心价值:匹配ORDER BY的排序方向,避免文件排序,尤其在大数据量的列表查询(如订单、日志)中,能显著提升性能。

回目录: 《面试笔记:MySQL 相关目录》
上一篇: 《面试笔记:MySQL 相关01 – 基础核心》
下一篇: 《面试笔记:MySQL 相关03 – SQL语法与查询优化》

喜欢面试笔记:MySQL 相关02 – 索引这篇文章吗?您可以点击浏览我的博客主页 发现更多技术分享与生活趣事。

  •  

面试笔记:MySQL 相关01 – 基础核心

感谢订阅陶其的个人博客!

基础核心

回目录: 《面试笔记:MySQL 相关目录》
下一篇: 《面试笔记:MySQL 相关02 – 索引》


01. 整体架构

以5.7版本为基础,8.0的更改会特别标注。

MySQL 采用分层架构,整体可分为 4 层。
连接层、服务层、存储引擎层、文件系统层。

1. 连接层

客户端连接层

  • 负责接收客户端(如Java程序、Navicat等)的TCP/IP连接请求,处理身份认证(用户名、密码、主机权限校验)。
  • 提供连接池机制,复用已建立的连接(避免频繁 TCP 握手 / 挥手的开销),同时管理连接状态(如空闲超时断开)。
  • 核心组件:连接器(验证用户名密码、权限)、连接池(复用已建立的连接,减少握手开销)。
  • 8.0版本变化:
    • 认证插件升级:默认使用caching_sha2_password替代 5.7 的mysql_native_password,加密强度更高,需客户端(如 JDBC 驱动)适配支持。
    • 连接池优化:增强连接复用效率,减少空闲连接占用的资源,同时支持 “连接属性动态修改”(无需重启连接即可调整部分参数)。

2. 服务层

核心处理层

  • 所有存储引擎共享的核心层,负责 SQL 的 “解析 – 优化 – 执行” 全流程,不直接操作数据,仅通过接口调用存储引擎。
  • 包含查询缓存(默认关闭)、解析器、优化器、执行器等核心逻辑。
  • 统一处理日志(如 binlog)、权限二次校验(执行 SQL 前再次确认权限)等通用逻辑。
  • 核心流程:接收 SQL 后,先查查询缓存(若命中直接返回)→ 解析器生成语法树(检查 SQL 语法)→ 优化器生成最优执行计划(如选择索引、连接方式)→ 执行器调用存储引擎接口执行计划。
  • 8.0版本变化:
    • 移除查询缓存组件:因 5.7 中查询缓存命中率极低(数据更新会清空对应表缓存),8.0 直接删除该模块,简化服务层逻辑,避免无效开销。
    • 优化器增强:架构层面支持更多优化规则(如复杂 JOIN 的执行计划调整)。
    • binlog 默认开启:5.7 中 binlog 默认关闭,8.0 默认启用,且架构上支持 binlog 的 “即时回放”(加速主从同步),无需额外配置。

3. 存储引擎层

数据存储层

  • 采用 “插件式架构”,可动态加载不同存储引擎(如 InnoDB、MyISAM),负责数据的实际存储、读取和事务管理。
  • 与服务层通过统一的handler接口交互,服务层无需关心底层数据存储格式(如 InnoDB 的表空间、MyISAM 的文件存储)。
  • 默认存储引擎为 InnoDB,架构上支持事务、行锁等特性的底层实现。
  • 8.0版本变化:
    • 强化 InnoDB 的架构适配:移除其他低效存储引擎(如 Federated),仅保留 InnoDB、MyISAM、Memory 等常用引擎,聚焦 InnoDB 的性能优化。
    • 架构层面支持 InnoDB 的新特性:如自增 ID 持久化(通过 redo log 架构实现)、隐藏索引(架构上支持索引的 “逻辑禁用”,不影响物理存储)。
    • 锁机制架构优化:行锁的范围判断逻辑在架构层面更精准,减少锁冲突(依赖与服务层的接口交互优化)。

4. 文件系统层

  • 负责将数据、日志等持久化到磁盘,依赖操作系统的文件系统(如 ext4、NTFS)。
  • 存储文件类型包括:表空间文件(InnoDB 的.ibd)、日志文件(redo log(重做日志)、binlog(二进制日志)、undo log(回滚日志))、配置文件(my.cnf)等。
  • 8.0版本变化:
    • 日志文件架构调整:redo log 默认存储路径优化,支持更大的日志文件大小(默认单个文件 1GB,5.7 默认 48MB),提升崩溃恢复效率。
    • 表空间文件优化:默认使用独立表空间(5.7 需手动配置),且架构上支持 “表空间加密”(透明数据加密 TDE),文件存储更安全。
    • 自增 ID 存储架构变更:5.7 中自增 ID 存于内存,8.0 架构上改为写入 redo log,重启后可通过日志恢复自增 ID 序列,避免重复。

02. 核心组件

1. 连接器

Connection Manager
核心功能:负责客户端连接的建立、管理与身份认证,是客户端与 MySQL 交互的 “入口”。

  • 接收 TCP 连接请求后,校验用户名、密码及客户端主机权限(基于mysql.user表),通过后分配连接线程。
  • 维护连接状态(如空闲、活跃),默认空闲超时时间为 8 小时(wait_timeout参数控制),超时后自动断开。
  • 认证依赖mysql_native_password插件(默认),加密强度一般,兼容性好。
  • 8.0 版本变化:
    • 默认认证插件升级为caching_sha2_password,采用 SHA-256 加密,安全性更高(需客户端驱动支持,如 JDBC 需 8.0 + 版本)。
    • 优化连接复用机制,减少空闲连接的资源占用,支持 “连接属性动态修改”(如无需重连即可调整部分会话参数)。

2. 查询缓存

Query Cache
核心功能:缓存 SQL 语句与结果集,相同 SQL(字节级一致)可直接返回缓存结果,减少重复计算。

  • 存在但默认关闭(query_cache_type=OFF),需手动开启;缓存以表为单位,表数据更新(增删改)时会清空该表所有缓存。
  • 局限性明显:仅适用于静态数据(如配置表),高并发写场景下命中率极低,反而因缓存维护消耗资源。
  • 8.0 版本变化:
    • 彻底移除查询缓存组件(相关参数如query_cache_size失效),原因是其实际应用价值低,移除后简化了服务层逻辑,减少无效开销。

3. 解析器

Parser
核心功能:对 SQL 语句进行语法分析,生成 “语法树”,确保 SQL 符合语法规则。

  • 解析过程包括:词法分析(识别关键字、表名、字段名等)→ 语法分析(检查 SQL 结构是否合法,如SELECT后是否有字段、WHERE是否搭配条件)。
  • 若语法错误(如关键字拼写错误),直接返回报错(如 “you have an error in your SQL syntax”)。
  • 8.0 版本变化:
    • 核心功能不变,但扩展了对新语法的支持(如WITH RECURSIVE递归查询、降序索引语法),解析效率略有优化(减少语法树生成的内存占用)。

4. 优化器

Optimizer
核心功能:基于语法树生成 “最优执行计划”,目标是最小化执行成本(CPU、IO 开销)。

  • 优化逻辑包括:选择合适的索引(如判断全表扫描 vs 索引扫描更快)、调整多表连接顺序(小表驱动大表减少中间结果集)、简化表达式(如a=1 and a=2直接判定为无效)。
  • 依赖表统计信息(如行数、数据分布),但统计信息更新不及时可能导致执行计划偏差。
  • 8.0 版本变化:
    • 增强统计信息:引入 “直方图”(Histogram),更精准记录数据分布(如字段值的频率),优化器对非均匀分布数据的索引选择更合理。
    • 优化规则扩展:支持复杂 JOIN(如多表嵌套连接)的执行计划调整,子查询优化更彻底(减少 “派生表” 的临时表开销)。

5. 执行器

Executor
核心功能:根据优化器生成的执行计划,调用存储引擎接口执行操作,并返回结果。

  • 执行前再次校验权限(避免连接建立后权限被修改导致的安全问题)。
    • 例如:检查用户是否有目标表的SELECT权限。
  • 通过统一接口(如handler::read_row)调用存储引擎,获取数据后进行过滤、聚合等处理(如WHERE条件过滤、GROUP BY分组)。
  • 8.0 版本变化:
    • 优化接口调用效率,减少与存储引擎的交互次数(如批量读取数据)。
    • 支持 “即时执行”(Instant Execution),对简单查询可跳过部分优化步骤,直接执行,提升响应速度。

6. 日志组件

核心日志模块
负责记录 MySQL 的操作和状态,支撑数据恢复、主从同步等功能。

1. binlog(二进制日志)

  • 5.7 版本:默认关闭,需手动开启(log_bin=ON)。记录所有数据修改操作(增删改、DDL),格式支持STATEMENT(语句)、ROW(行)、MIXED(混合),用于主从同步和时间点恢复。
  • 8.0 版本:默认开启。格式默认ROW(更安全),支持 “即时回放”(Binlog Instant),主从同步时可跳过部分无效日志,提升同步效率。

2. redo log(重做日志,InnoDB 依赖)

  • 5.7 版本:InnoDB 专属,记录数据页的物理修改,采用 “循环写” 机制(固定大小文件),保证崩溃后数据可恢复(先写日志再写磁盘,即 WAL 机制)。
  • 8.0 版本:默认单个日志文件大小从 48MB 增至 1GB,减少日志切换频率;支持 “并行写入”,提升高并发下的日志写入效率。

3. undo log(回滚日志,InnoDB 依赖)

  • 5.7 版本:记录数据修改前的状态,用于事务回滚和 MVCC(多版本并发控制),默认随表空间存储,可能因长期积累导致空间膨胀。
  • 8.0 版本:支持 “undo log 自动回收”(通过innodb_undo_log_truncate参数),无需手动清理,减少维护成本。

7. 权限组件

核心功能:管理用户权限,控制对数据库、表、字段的操作权限。

  • 权限存储在mysql库的系统表中(如userdbtables_priv),权限修改后需通过FLUSH PRIVILEGES刷新或重启生效(静态权限)。
  • 支持库级、表级、列级权限,但缺乏细粒度的动态权限(如管理特定日志的权限)。
  • 8.0 版本变化:
    • 引入 “动态权限”(如BINLOG_ADMINBACKUP_ADMIN),权限修改后即时生效,无需刷新或重启。
    • 权限检查逻辑优化,结合角色(Role)管理(5.7 后期引入但不完善,8.0 强化),可批量分配权限,简化权限管理。

8. 总结

MySQL 核心组件的核心逻辑(连接、解析、优化、执行、日志、权限)在 5.7 和 8.0 中保持一致。
8.0 的改进集中在:移除低效组件(查询缓存)、增强安全性(认证插件)、优化性能(优化器、日志)、简化维护(动态权限、undo 回收)等。

03. 存储引擎

MySQL 的存储引擎是负责数据存储、读取及底层特性实现的核心模块,采用 “插件式” 设计,不同引擎支持的功能(如事务、锁机制)差异显著。

1. InnoDB(默认)

InnoDB 是 MySQL 最常用(默认)的存储引擎,以事务支持、高并发为核心优势,5.7 和 8.0 均将其作为默认引擎,版本间优化集中在性能、可靠性和功能扩展。

核心通用特性(5.7 和 8.0 共通)

  • 支持事务(ACID 特性):通过 redo log(保证持久性)、undo log(保证原子性和隔离性)实现。
  • 行级锁:仅锁定修改的行(而非全表),适合高并发写场景(如电商订单更新)。
  • 聚簇索引:数据与主键索引物理存储在一起,查询主键时效率极高。
  • 外键约束:支持表间外键关联(如orders表关联users表的user_id)。

5.7 版本 InnoDB 特性

  • 自增 ID(AUTO_INCREMENT):存储在内存中,重启 MySQL 后可能因未持久化导致重复(需依赖 binlog 恢复,但存在风险)。
  • 索引限制:仅语法支持降序索引(DESC),实际仍按升序存储,查询降序数据时需额外排序。
  • 锁机制:间隙锁(Gap Lock)范围较宽泛,可能导致高并发下锁冲突增加(如批量插入相邻 ID 时)。
  • undo log:默认随表空间存储,长期运行后可能因未自动回收导致磁盘空间膨胀(需手动清理)。

8.0 版本 InnoDB 关键改进

  • 自增 ID 持久化:将自增 ID 写入 redo log,重启后可通过日志恢复,彻底解决 5.7 的重复问题。
  • 索引增强:
    • 真正支持降序索引(DESC),查询降序排序数据时无需额外排序,直接使用索引。
    • 新增 “隐藏索引”(INVISIBLE):可标记索引为隐藏(不影响物理存储),用于临时禁用索引测试性能(无需删除重建)。
  • 锁优化:间隙锁范围更精准,减少非必要锁定(如批量插入时仅锁定实际需要的区间),降低锁冲突。
  • undo log 自动回收:支持innodb_undo_log_truncate参数(默认开启),自动收缩过大的 undo log,减少人工维护成本。
  • 表空间加密:支持透明数据加密(TDE),表空间文件(.ibd)加密存储,提升数据安全性。

2. MyISAM

MyISAM 是早期 MySQL 的默认引擎,因不支持事务和行锁,逐渐被 InnoDB 替代,5.7 和 8.0 中仍保留但应用场景有限。

核心通用特性(5.7 和 8.0 共通)

  • 不支持事务和外键:仅适合无需事务保证的场景。
  • 表级锁:写操作(增删改)会锁定全表,读操作需等待写锁释放,并发写性能差。
  • 独立文件存储:数据存于.MYD文件,索引存于.MYI文件,可直接复制文件迁移表。

5.7 版本:仍有部分场景使用(如只读日志表),但已明确不推荐用于核心业务。
8.0 版本:进一步弱化 MyISAM,默认配置下性能优化倾向 InnoDB,且移除了部分对 MyISAM 的冗余支持(如全文索引的部分优化仅针对 InnoDB)。

3. Memory

内存引擎
数据存储在内存中,适合临时数据处理,性能极快但数据易失(重启丢失)。

5.7 版本特性

  • 支持哈希索引(默认)和 B + 树索引,哈希索引适合等值查询(=),不支持范围查询(>、<)。
  • 表大小受max_heap_table_size限制(默认 16MB),超出后会报错。

8.0 版本改进

  • 优化内存分配机制,减少小表的内存浪费。
  • 支持动态调整max_heap_table_size(无需重建表),更灵活适配临时数据大小。

4. CSV

逗号分割值引擎
数据以 CSV 格式文件存储(.csv),适合数据交换(如与 Excel、文本文件交互)。

5.7 版本特性

  • 表结构存于.frm文件,数据存于.csv文件,可直接用文本编辑器查看 / 修改。
  • 不支持索引和事务,仅适合简单的导入导出场景。

8.0 版本改进

  • 增强兼容性:支持 CSV 文件中包含换行符(需特殊处理),减少导入导出时的格式错误。

5. 总结

  • InnoDB:5.7 已具备事务、行锁核心能力,8.0 通过自增 ID 持久化、索引优化、锁细化等提升可靠性和性能,是所有业务的首选。
  • MyISAM:仅适合只读、低并发场景,8.0 中进一步被边缘化。
  • Memory/CSV:作为辅助引擎,8.0 主要优化了易用性(如动态调整内存表大小),核心功能无本质变化。

04. InnoDB存储引擎

InnoDB作为MySQL的默认且常用的搜索引擎,有如下的核心特性:

1. 事务支持(ACID)

  • 完全支持事务的原子性(A)、一致性(C)、隔离性(I)、持久性(D)。

2. 锁机制

  • 支持行级锁 + 间隙锁(Next-Key Lock),行级锁仅锁定修改行(高并发友好),间隙锁防止幻读。
  • 8.0 变化:间隙锁范围更精准,减少非必要锁定(如批量插入相邻 ID 时仅锁实际区间),降低锁冲突概率。

3. 聚簇索引

  • 数据与主键索引物理存储在一起,形成 B + 树结构,主键查询可直接获取数据。

4. 自增 ID(AUTO_INCREMENT)

  • 自增 ID 存储在内存中,MySQL 重启后可能因未持久化导致序列重复(需依赖 binlog 部分恢复)。
  • 8.0 变化:自增 ID 写入 redo log 持久化,重启后可通过日志恢复序列,彻底解决重复问题。

5. 索引特性

  • 支持 B + 树索引(默认)、全文索引、前缀索引;仅语法支持降序索引(DESC),实际仍按升序存储。
  • 8.0 变化:
    • 真正支持降序索引,查询降序排序数据时无需额外排序,直接复用索引。
    • 新增隐藏索引(INVISIBLE),可临时禁用索引(不删除),方便测试索引性能影响。

6. 日志依赖(redo log/undo log)

  • redo log:循环写的物理日志,记录数据页修改,保证崩溃恢复。
  • undo log:记录数据修改前的状态,用于事务回滚和 MVCC(多版本并发控制),默认随表空间存储,易膨胀需手动清理。
  • 8.0 变化:
    • redo log 默认单个文件大小从 48MB 增至 1GB,减少日志切换开销。
    • undo log 支持自动回收(innodb_undo_log_truncate默认开启),无需手动清理。

7. 外键约束

  • 支持表间外键关联(如:orders.user_id关联users.id),保证数据引用完整性。

8. 安全性增强

  • 无表空间加密功能,数据文件(.ibd)以明文存储。
  • 8.0 变化:新增透明数据加密(TDE),支持表空间文件加密存储,提升敏感数据安全性(如用户密码、支付信息表)。

05. 三大范式和反范式

MySQL 的三大范式(1NF、2NF、3NF)是关系型数据库设计的基础原则,目的是减少数据冗余、保证数据一致性、避免插入 / 更新 / 删除异常
反范式是有意打破范式规则,有策略地保留适量冗余,目的是减少多表 JOIN提升查询效率(尤其读多写少场景)。

1. 第一范式(1NF)

第一范式(1NF):字段原子化,不可再分。

  • 核心要求:表中所有字段的值必须是 “原子性” 的(不可再拆分为更小的数据单元)。
  • 目的:避免同一字段存储多维度信息,导致查询和修改混乱。
  • 例子:
    • 反例:用户表的address字段存储 “中国 – 北京 – 朝阳区”(可拆分为countrycitydistrict);
    • 正例:拆分为countrycitydistrict三个字段,每个字段不可再分。
  • 实践:1NF 是基础,几乎所有业务表都需满足(如:订单表的phone字段不存储 “固话 + 手机”,而是单独字段)。

2. 第二范式(2NF)

第二范式(2NF):消除 “部分依赖”,非主属性完全依赖主键。

  • 核心要求:在 1NF 基础上,表的主键必须是 “联合主键”(多字段组成),且所有非主属性必须完全依赖于整个主键,不能仅依赖主键的一部分(即消除 “部分依赖”)。
  • 目的:避免因主键部分字段变化导致的数据异常(如修改部分主键后,非主属性需联动更新)。
  • 例子:
    • 反例:订单项表(联合主键order_id+product_id)中,product_name仅依赖product_id(主键的一部分),属于部分依赖;
    • 正例:product_name应存储在产品表中,订单项表只存product_id,通过关联产品表获取名称(非主属性quantityprice完全依赖order_id+product_id)。
  • 实践:多对多关系的中间表(如 “用户 – 角色” 关联表)需满足 2NF,避免冗余存储角色名称等信息。

3. 第三范式(3NF)

第三范式(3NF):消除 “传递依赖”,非主属性不依赖其他非主属性。

  • 核心要求:在 2NF 基础上,所有非主属性必须直接依赖于主键,不能依赖于其他非主属性(即消除 “传递依赖”)。
  • 目的:避免因某个非主属性变化,导致其他非主属性需联动更新(如 A 依赖主键,B 依赖 A,则 B 传递依赖主键)。
  • 例子:
    • 反例:用户表(主键user_id)中,area_name依赖area_idarea_id依赖user_id,则area_name传递依赖user_id
    • 正例:area_name应存储在区域表中,用户表只存area_id,通过关联区域表获取名称(非主属性仅直接依赖user_id)。
  • 实践:用户表、商品表等核心表需满足 3NF,避免存储 “部门名称”“分类名称” 等可通过关联获取的字段。

4. 反范式

反范式是有意打破范式规则,有策略地保留适量冗余,目的是减少多表 JOIN提升查询效率(尤其读多写少场景)。

  • 核心思路
    通过在表中冗余存储其他表的字段,避免查询时关联多个表(JOIN 操作耗时,尤其大数据量时)。
  • 例子:
    • 电商商品列表页需展示 “商品名称 + 分类名称”,若严格遵循 3NF,需关联product表和category表;
    • 反范式优化:在product表中冗余category_name字段,查询时直接从product表获取,无需 JOIN。
  • 适用场景
    1. 高频查询、低频更新:如商品详情页(查询频繁,分类名称很少修改),冗余后查询性能提升 10 倍以上;
    2. 多表关联复杂:如订单列表需关联用户表、商品表、物流表,冗余 “用户名”“商品名” 后,查询从多表 JOIN 简化为单表查询;
    3. 统计分析场景:报表系统需聚合多维度数据,冗余存储聚合结果(如 “每月销售额”),避免实时计算。
  • 注意事项
    • 冗余字段需同步更新:如category_name修改后,需同步更新product表中的冗余字段(可通过触发器、Java 代码事务保证);
    • 控制冗余范围:只冗余高频查询的核心字段(如名称、状态),避免表过大(如冗余大文本字段会增加存储和 IO 成本)。

5. 总结

  • 范式:适合写多读少、数据一致性要求高的场景(如订单系统、用户中心),通过减少冗余降低更新异常风险;
  • 反范式:适合读多写少、查询性能敏感的场景(如电商列表、报表),通过可控冗余提升查询效率。
  • 实际开发中,很少严格遵循某一范式,而是混合使用(如核心交易表用 3NF 保证一致性,查询表用反范式提升性能)。

06. DDL、DML、DCL、DQL

MySQL 中 DDL、DML、DCL、DQL 是按操作类型划分的四大类 SQL 语言,分别对应:

  • DDL:数据库结构定义;
  • DML:数据操纵;
  • DCL:权限控制;
  • DQL:数据查询。

1. DQL:数据查询语言

Data Query Language。

  • 定义:用于从数据库中查询数据,不修改数据或结构,是业务系统中最频繁的操作。
  • 核心命令:SELECT(含WHEREJOINGROUP BYORDER BYLIMIT等子句)。
  • 作用:从表中提取所需数据,支撑业务展示(如列表页、详情页)、统计分析(如报表)等场景。
  • 实践要点:
    • 是 Java 开发中最常用的 SQL(如查询用户信息、订单列表),需结合索引优化(如WHERE条件加索引、避免SELECT *);
    • 复杂查询(多表关联、聚合)需用EXPLAIN分析执行计划,避免全表扫描或文件排序。

2. DML:数据操纵语言

Data Manipulation Language。

  • 定义:用于修改表中的数据(增、删、改),会改变数据内容,但不改变表结构。
  • 核心命令:
    • INSERT(新增);
    • UPDATE(修改);
    • DELETE(删除)。
  • 作用:处理业务数据的生命周期(如创建订单、更新状态、删除无效记录)。
  • 实践要点:
    • 操作会触发事务(默认自动提交,可通过BEGIN手动控制),需保证原子性(如订单创建时同时扣减库存,失败则回滚);
    • 批量操作优化:INSERTVALUES (),(),()批量插入(比单条循环高效),DELETE/UPDATE避免全表操作(加WHERE条件,如DELETE FROM log WHERE create_time < '2024-01-01');
    • 高频写入场景(如日志)需控制频率,避免锁表影响查询。

3. DDL:数据定义语言

Data Definition Language。

  • 定义:用于定义或修改数据库、表、索引等结构,会改变数据库的元数据(结构信息)。
  • 核心命令:
    • CREATE(创建,如CREATE TABLECREATE INDEX);
    • ALTER(修改,如ALTER TABLE ADD COLUMN);
    • DROP(删除,如DROP TABLE);
    • TRUNCATE(清空表)。
  • 作用:初始化数据库结构(如建表、加字段)、调整表结构(如新增索引、扩展字段)。
  • 实践要点:
    • 执行时可能锁表(尤其ALTER TABLE在 InnoDB 中,大表修改会阻塞读写),生产环境需在低峰期执行,大表建议用在线 DDL 工具(如 pt-online-schema-change);
    • 谨慎使用DROPTRUNCATE(不可逆,TRUNCATE会清空数据且不触发事务回滚);
    • 索引相关 DDL(CREATE INDEX)需评估必要性(索引提升查询但降低写入性能)。

4. DCL:数据控制语言

Data Control Language。

  • 定义:用于管理数据库用户权限和事务控制,控制谁能操作数据、操作范围。
  • 核心命令:`
    • GRANT`(授予权限);
    • REVOKE(撤销权限);
    • COMMIT(提交事务);
    • ROLLBACK(回滚事务)。
  • 作用:保障数据安全(如限制应用账号只能操作指定表)、控制事务一致性。
  • 实践要点:
    • 权限遵循 “最小原则”:应用程序账号只授予SELECT/INSERT/UPDATE等必要权限,禁止DROP/GRANT等高风险权限,避免用 root 账号直接连接业务系统;
    • 事务控制(COMMIT/ROLLBACK)需在 Java 代码中配合业务逻辑(如分布式事务场景,确保多库操作一致性)。

5. 总结

四类语言分工明确:

  • DQL:查数据(业务展示核心);
  • DML:改数据(业务操作核心);
  • DDL:改结构(架构维护核心);
  • DCL:控权限(安全保障核心)。

07. 数据库表字段类型

1. 核心字段类型及适用场景

1. 数值型:适合存储数字(整数、小数)

类型 特点(长度 / 范围) JDK8对应类型 适用场景
TINYINT 1 字节,范围 – 128 ~ 127
(无符号 0 ~ 255)
Byte 状态标识(如status:0 – 禁用、1 – 正常)、
性别(0 – 女、1 – 男)
INT 4 字节,范围 – 21 亿 ~ 21 亿 Integer 普通 ID(如user_idorder_id,中小规模业务足够)、数量(如count
BIGINT 8 字节,范围 ±9e18,
大约±90亿亿
Long 大整数 ID(如分布式 ID、雪花 ID)、
高频增长数据(如万亿级订单量)
DECIMAL(M,D) 高精度小数
(M 总长度,D 小数位)
java.math.BigDecimal 金额(如amountDECIMAL(10,2)表示
最多 10 位,2 位小数)、利率
FLOAT/DOUBLE 单 / 双精度浮点
(有精度损失)
Float/Double 非精确计算(如温度、重量,允许微小误差)

2. 字符串型:适合存储文本

类型 特点(长度 / 范围) JDK8对应类型 适用场景
CHAR(N) 固定长度(N 字节,0 ~ 255),
空格填充
String 长度固定的字符串(如手机号CHAR(11)、身份证号CHAR(18)
VARCHAR(N) 可变长度(0 ~ 65535),
存储实际长度
String 长度不固定的文本(如用户名VARCHAR(50)、地址VARCHAR(200)
TEXT 大文本(最大 64KB) String 较长文本(如商品描述、用户备注,不适合建索引)
MEDIUMTEXT/LONGTEXT 更大文本(16MB/4GB) String 超大文本(如文章内容、日志详情,慎用,会拖慢查询)

3. 日期时间型:适合存储时间

类型 特点(长度 / 范围) JDK8对应类型 适用场景
DATETIME 8 字节,范围 1000 ~ 9999 年,
无时区
java.time.LocalDateTime
(不推荐Date
业务时间(如订单创建时间create_time,不受服务器时区影响)
TIMESTAMP 4 字节,范围 1970 ~ 2038 年,
受时区影响
java.time.LocalDateTime
(不推荐Date
记录系统时间(如最后更新时间update_time,自动随时区转换)
DATE 3 字节,仅日期(年月日) java.time.LocalDate
(不推荐Timestamp
生日、到期日(如birthdayexpire_date
TIME 3 字节,仅时间(时分秒) java.time.LocalTime 时段记录(如会议时长、打卡时间)

4. 特殊类型:针对性场景

类型 特点(长度 / 范围) JDK8对应类型 适用场景
ENUM 枚举(存储整数,显示字符串) String 固定可选值(如pay_typeENUM('WECHAT','ALIPAY','CARD')
SET 集合(多选,最多 64 个值) String 多选项(如tagsSET('hot','new','discount')
BLOB 二进制数据(如图片、文件) byte[](字节数组) 小型二进制(如头像缩略图,大型文件建议存 OSS,库中只存 URL)

2. 经典面试点

1. 数值型:精度与范围陷阱

  • 金额为什么用DECIMAL而非FLOAT
    FLOAT是浮点型,存在精度损失(如0.1 + 0.2 = 0.300000004),而DECIMAL是精确小数,适合金额等强精度场景。

  • INTBIGINT怎么选?
    中小业务(千万级数据)用INT足够;分布式系统(如订单 ID 用雪花 ID,长度 18 位)必须用BIGINT,避免溢出。

2. 字符串型:长度与性能

  • CHARVARCHAR的核心区别?
    CHAR固定长度,查询快但浪费空间(如手机号用CHAR(11)VARCHAR(11)高效,无需计算长度);
    VARCHAR节省空间,但查询需额外解析长度,适合长度波动大的场景(如地址)。

  • 为什么不建议VARCHAR(255)滥用?
    MySQL 中VARCHAR(255)在某些引擎(如 InnoDB)中会按 255 字节分配临时内存,即使实际数据很短,也会浪费内存(尤其排序、JOIN 时),建议按实际需求设长度(如用户名VARCHAR(50))。

  • TEXT类型的坑?
    TEXT字段不适合建索引(即使建索引也只能取前 N 个字符),且查询时会额外 IO,大文本建议拆分表(如商品表存desc_id,关联单独的product_desc表存TEXT内容)。

3. 日期时间型:时区与范围

  • DATETIMETIMESTAMP怎么选?
    业务时间(如订单创建时间)用DATETIME(固定值,不受服务器时区影响);系统时间(如最后修改时间)用TIMESTAMP(自动更新,适配多时区部署)。

  • TIMESTAMP的 2038 年问题?
    因范围是 1970 ~ 2038 年,长期系统需注意(可改用DATETIME规避)。

4. 枚举类型ENUM的利弊

  • 优点:存储高效(用整数存字符串),约束数据合法性(只能选定义的值);
  • 缺点:修改枚举值需ALTER TABLE(DDL 操作,大表阻塞),不适合值频繁变化的场景(如活动状态,建议用TINYINT+ 字典表替代)。

3. 总结

字段类型选择的核心原则:“够用即可,避免冗余

  • 数字优先选小类型(如状态用TINYINT而非INT);
  • 字符串按长度固定与否选CHAR/VARCHAR,避免大文本字段拖累主表;
  • 日期根据时区需求选DATETIME/TIMESTAMP
  • 金额、ID 等关键字段必须保证精度和范围,避免溢出或精度丢失。

回目录: 《面试笔记:MySQL 相关目录》
下一篇: 《面试笔记:MySQL 相关02 – 索引》

喜欢面试笔记:MySQL 相关01 – 基础核心这篇文章吗?您可以点击浏览我的博客主页 发现更多技术分享与生活趣事。

  •  

面试笔记:MySQL 相关目录

感谢订阅陶其的个人博客!

MySQL高频问点

1. 基础核心

  • 整体架构
  • 核心组件
  • 存储引擎
  • InnoDB存储引擎
  • 三大范式和反范式
  • DDL/DML/DCL/DQL
  • 数据库表字段类型

2. 索引

  • 各种树
  • 索引的作用
  • 常见索引类型
  • 聚簇索引与非聚簇索引
  • 联合索引的最左前缀原则
  • 索引的维护成本
  • 回表查询与索引覆盖
  • 如何合理使用索引
  • 索引失效场景
  • explain执行计划分析
  • 升序索引与降序索引

3. SQL语法与查询优化

  • 子查询与连接查询
  • 子查询与join性能对比及适用场景
  • 复杂查询(group by/having、limit分页)的执行逻辑
  • order by 的排序原理
  • 慢查询的定位与分析
  • 复杂SQL的拆分与改写(大表分页优化:延迟关联、书签分页)
  • join优化(小表驱动大表、 避免 cross join)
  • count的性能差异(count(1)、count(*)、count(字段))
  • IN和JOIN哪个快
  • MySQL的函数

4. 事务和锁机制

  • 事务
    • ACID
    • 四大隔离级别的定义及实现
    • 默认隔离级别为何是可重复读?
    • 隔离级别与并发问题(脏读、不可重复读、幻读)的对应关系
  • 锁机制
    • 锁的分类
    • 行锁的触发条件
    • MVCC的原理
    • 死锁的产生原因与定位
    • 避免死锁的策略
    • 高并发下如何减少锁竞争

5. 性能调优

  • MySQL核心配置参数的含义与调优依据
  • 连接池配置
  • InnoDB 缓冲池
  • redo log 与 binlog 的协作
  • 刷盘策略对性能与安全性的影响
  • 磁盘IO优化
  • CPU与内存分配
  • 网络优化

6. 高可用与集群架构

  • 主从复制:
    • 复制原理
    • 复制模式
    • 复制延迟的原因与解决
  • 读写分离:
    • 读写分离的实现
    • 一致性问题
    • 分库分表的必要性与拆分策略
  • 高可用架构
    • 主从切换工具
    • 集群方案的适用场景
    • 云原生

7. 数据安全与运维

  • 备份与恢复:
    • 备份类型的选择
    • 基于binlog的时间点恢复流程
    • 大表备份的性能优化
  • 日志系统:
    • binlog
    • redo log
    • 慢查询日志与错误日志的分析
  • 故障付出:
    • 数据库宕机恢复流程
    • 数据一致性校验
    • 索引损坏的修复

8. 进阶特性与版本差异

  • MySQL 8.0新特性:
    • 窗口函数、
    • CTE、
    • 角色管理与权限细化、
    • innoDB的自增锁优化
  • 特殊场景处理:
    • 大表DDL
    • JSON类型的存储与查询优化
    • 地理信息(GIS)功能的实战应用

喜欢面试笔记:MySQL 相关目录这篇文章吗?您可以点击浏览我的博客主页 发现更多技术分享与生活趣事。

  •  

安装MySQL8

1.下载

打开官网下载页面: https://downloads.mysql.com/archives/community/,在这里我选择8.0.25版本,Operating System一栏通常没有CentOS,选择RedHat即可,OS Version选择7,X86_64,然后下载第一个mysql-8.0.25-1.el7.x86_64.rpm-bundle.tar

下载完成后,将文件移动到/opt下,并解压,会解压出多个rpm软件包

[root@localhost opt]# tar -xvf mysql-8.0.25-1.el7.x86_64.rpm-bundle.tar mysql-community-client-8.0.25-1.el7.x86_64.rpmmysql-community-client-plugins-8.0.25-1.el7.x86_64.rpmmysql-community-common-8.0.25-1.el7.x86_64.rpmmysql-community-devel-8.0.25-1.el7.x86_64.rpmmysql-community-embedded-compat-8.0.25-1.el7.x86_64.rpmmysql-community-libs-8.0.25-1.el7.x86_64.rpmmysql-community-libs-compat-8.0.25-1.el7.x86_64.rpmmysql-community-server-8.0.25-1.el7.x86_64.rpmmysql-community-test-8.0.25-1.el7.x86_64.rpm

2.安装前准备

2.1 卸载自带的mariadb

安装基于CentOS 7.9.2009

[root@localhost ~]# cat /etc/redhat-release CentOS Linux release 7.9.2009 (Core)

CentOS 7.9系统预装了一个mariadb数据库,这个数据库是MySQL的”姐妹”,都是同一个作者编写的,mariadb会在/etc目录生成一个my.cnf作为配置文件,MySQL安装后也会在/etc目录生成一个my.cnf,因此为了排除干扰,首先要把预装的mariadb卸载掉。

查看是否预装:

[root@localhost ~]# rpm -qa | grep -i mariamariadb-libs-5.5.68-1.el7.x86_64
[root@localhost ~]# ls /etc/ | grep mymy.cnfmy.cnf.d

卸载mariadb

rpm -e --nodeps mariadb-libs

2.2 安装依赖

截至目前,CentOS7的yum源已经无法访问,因此需要先更换为阿里巴巴的yum源

cp /etc/yum.repos.d/CentOS-Base.repo /etc/yum.repos.d/CentOS-Base.repo.backup
curl -o /etc/yum.repos.d/CentOS-Base.repo http://mirrors.aliyun.com/repo/Centos-7.repo
yum clean all
yum makecache

然后安装依赖项,我采用的系统版本需要额外安装这两个依赖,缺失这两个将导致后续安装mysql-community-server时出错

yum install -y net-toolsyum install -y perl

3.安装和初始化

cd进入/opt目录安装刚刚解压出的软件包,不需要每个都安装,仅安装MySQL的Server和Client以及依赖的包,安装需要按照顺序,因为rpm包有相互的依赖关系,需要安装的包以及安装顺序如下:

  1. mysql-community-common-8.0.25-1.el7.x86_64.rpm
  2. mysql-community-client-plugins-8.0.25-1.el7.x86_64.rpm
  3. mysql-community-libs-8.0.25-1.el7.x86_64.rpm
  4. mysql-community-client-8.0.25-1.el7.x86_64.rpm
  5. mysql-community-server-8.0.25-1.el7.x86_64.rpm

使用rpm -ivh命令安装这些包

[root@localhost opt]# rpm -ivh mysql-community-common-8.0.25-1.el7.x86_64.rpm警告:mysql-community-common-8.0.25-1.el7.x86_64.rpm: 头V3 DSA/SHA1 Signature, 密钥 ID 5072e1f5: NOKEY准备中...                          ################################# [100%]正在升级/安装...   1:mysql-community-common-8.0.25-1.e################################# [100%][root@localhost opt]# rpm -ivh mysql-community-client-plugins-8.0.25-1.el7.x86_64.rpm'警告:mysql-community-client-plugins-8.0.25-1.el7.x86_64.rpm: 头V3 DSA/SHA1 Signature, 密钥 ID 5072e1f5: NOKEY准备中...                          ################################# [100%]正在升级/安装...   1:mysql-community-client-plugins-8.################################# [100%][root@localhost opt]# rpm -ivh mysql-community-libs-8.0.25-1.el7.x86_64.rpm警告:mysql-community-libs-8.0.25-1.el7.x86_64.rpm: 头V3 DSA/SHA1 Signature, 密钥 ID 5072e1f5: NOKEY准备中...                          ################################# [100%]正在升级/安装...   1:mysql-community-libs-8.0.25-1.el7################################# [100%][root@localhost opt]# rpm -ivh mysql-community-client-8.0.25-1.el7.x86_64.rpm警告:mysql-community-client-8.0.25-1.el7.x86_64.rpm: 头V3 DSA/SHA1 Signature, 密钥 ID 5072e1f5: NOKEY准备中...                          ################################# [100%]正在升级/安装...   1:mysql-community-client-8.0.25-1.e################################# [100%][root@localhost opt]# rpm -ivh mysql-community-server-8.0.25-1.el7.x86_64.rpm警告:mysql-community-server-8.0.25-1.el7.x86_64.rpm: 头V3 DSA/SHA1 Signature, 密钥 ID 5072e1f5: NOKEY准备中...                          ################################# [100%]正在升级/安装...   1:mysql-community-server-8.0.25-1.e################################# [100%]

安装完成,查看一下安装的版本

[root@localhost opt]# mysql --versionmysql  Ver 8.0.25 for Linux on x86_64 (MySQL Community Server - GPL)[root@localhost opt]# mysqladmin --versionmysqladmin  Ver 8.0.25 for Linux on x86_64 (MySQL Community Server - GPL)

为了保证数据库目录与文件的所有者为mysql登录用户,如果是以root身份运行mysql服务,需要执行下面的命令初始化:

mysqld --initialize --user=mysql

说明: --initialize选项默认以安全模式来初始化,则会为root用户生成一个密码并将该密码标记为过期,登录后你需要设置一个新的密码。生成的临时密码会往日志中记录一份,查看密码:

cat /var/log/mysqld.log
[root@localhost opt]# mysqld --initialize --user=mysql[root@localhost opt]# cat /var/log/mysqld.log2025-04-06T10:34:02.749835Z 0 [System] [MY-013169] [Server] /usr/sbin/mysqld (mysqld 8.0.25) initializing of server in progress as process 94792025-04-06T10:34:02.921735Z 1 [System] [MY-013576] [InnoDB] InnoDB initialization has started.2025-04-06T10:34:06.304242Z 1 [System] [MY-013577] [InnoDB] InnoDB initialization has ended.2025-04-06T10:34:10.865584Z 6 [Note] [MY-010454] [Server] A temporary password is generated for root@localhost: /*E.4-*jf_#)[root@localhost opt]# 

接下来启动服务

systemctl start mysqld
[root@localhost opt]# systemctl status mysqld● mysqld.service - MySQL Server   Loaded: loaded (/usr/lib/systemd/system/mysqld.service; enabled; vendor preset: disabled)   Active: active (running) since 日 2025-04-06 18:39:55 CST; 7s ago     Docs: man:mysqld(8)           http://dev.mysql.com/doc/refman/en/using-systemd.html  Process: 9531 ExecStartPre=/usr/bin/mysqld_pre_systemd (code=exited, status=0/SUCCESS) Main PID: 9556 (mysqld)   Status: "Server is operational"   CGroup: /system.slice/mysqld.service           └─9556 /usr/sbin/mysqld4月 06 18:39:46 localhost.localdomain systemd[1]: Starting MySQL Server...4月 06 18:39:55 localhost.localdomain systemd[1]: Started MySQL Server.

4.登录和基础配置

4.1 登录并重置密码

用之前生成的初始密码登录MySQL的root用户,命令是:mysql -uroot -p,Enter password:后输入密码,登录成功并进入MySQL交互命令行,但是不能操作,提示需要重置root密码

[root@localhost opt]# mysql -uroot -pEnter password: Welcome to the MySQL monitor.  Commands end with ; or \g.Your MySQL connection id is 8Server version: 8.0.25Copyright (c) 2000, 2021, Oracle and/or its affiliates.Oracle is a registered trademark of Oracle Corporation and/or itsaffiliates. Other names may be trademarks of their respectiveowners.Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.mysql> show databases;ERROR 1820 (HY000): You must reset your password using ALTER USER statement before executing this statement.mysql> 

执行这个语句重置密码

alter user 'root'@'localhost' identified by 'lzj';

4.2 打开远程连接

安装好的MySQL会自带4个数据库,连接配置的表位于mysql库的user表上,use mysql;将当前库切换为mysql,通过select host,user from user;查看得知系统中的4个用户都仅允许本机访问

mysql> show databases;+--------------------+| Database           |+--------------------+| information_schema || mysql              || performance_schema || sys                |+--------------------+4 rows in set (0.65 sec)mysql> use mysql;Database changedmysql> select host,user from user;+-----------+------------------+| host      | user             |+-----------+------------------+| localhost | mysql.infoschema || localhost | mysql.session    || localhost | mysql.sys        || localhost | root             |+-----------+------------------+4 rows in set (0.01 sec)

通过执行以下语句修改root用户可访问的地址,可以采用通配符,按自己需要修改即可,我选择直接修改为%

允许root用户从192.168.1.x主机访问

update mysql.user set host ='192.168.1.%' where user = 'root';

允许root用户从一切远程主机访问

update mysql.user set host ='%' where user = 'root';

修改完成,刷新权限,即可远程连接

flush privileges;

远程连接前一定要关闭CentOS7自带的防火墙firewalld

4.3 修改字符集

在MySQL8.0版本之前,默认字符集为latin1,utf8字符集指向的是utf8mb3。网站开发人员在数据库设计的时候往往会将编码修改为utf8字符集,如果遗忘修改默认的编码,就会出现乱码的问题,从MySQL8.0开始,数据库的默认编码将改为utf8mb4,从而避免上述乱码的问题。

mysql> show variables like '%char%';+--------------------------+--------------------------------+| Variable_name            | Value                          |+--------------------------+--------------------------------+| character_set_client     | utf8mb4                        || character_set_connection | utf8mb4                        || character_set_database   | utf8mb4                        || character_set_filesystem | binary                         || character_set_results    | utf8mb4                        || character_set_server     | utf8mb4                        || character_set_system     | utf8mb3                        || character_sets_dir       | /usr/share/mysql-8.0/charsets/ |+--------------------------+--------------------------------+8 rows in set (0.12 sec)
  •  

安装MySQL5.7

1.下载

打开官网下载页面: https://downloads.mysql.com/archives/community/,在这里我选择5.7.28版本,Operating System一栏通常没有CentOS,选择RedHat即可,OS Version选择7,X86_64,然后下载第一个mysql-5.7.28-1.el7.x86_64.rpm-bundle.tar

下载完成后,将文件移动到/opt下,并解压,会解压出多个rpm软件包

[root@localhost opt]# tar -xvf mysql-5.7.28-1.el7.x86_64.rpm-bundle.tar mysql-community-embedded-5.7.28-1.el7.x86_64.rpmmysql-community-libs-compat-5.7.28-1.el7.x86_64.rpmmysql-community-devel-5.7.28-1.el7.x86_64.rpmmysql-community-embedded-compat-5.7.28-1.el7.x86_64.rpmmysql-community-libs-5.7.28-1.el7.x86_64.rpmmysql-community-test-5.7.28-1.el7.x86_64.rpmmysql-community-common-5.7.28-1.el7.x86_64.rpmmysql-community-embedded-devel-5.7.28-1.el7.x86_64.rpmmysql-community-client-5.7.28-1.el7.x86_64.rpmmysql-community-server-5.7.28-1.el7.x86_64.rpm

2.安装前准备

2.1 卸载自带的mariadb

安装基于CentOS 7.9.2009

[root@localhost ~]# cat /etc/redhat-release CentOS Linux release 7.9.2009 (Core)

CentOS 7.9系统预装了一个mariadb数据库,这个数据库是MySQL的”姐妹”,都是同一个作者编写的,mariadb会在/etc目录生成一个my.cnf作为配置文件,MySQL安装后也会在/etc目录生成一个my.cnf,因此为了排除干扰,首先要把预装的mariadb卸载掉。

查看是否预装:

[root@localhost ~]# rpm -qa | grep -i mariamariadb-libs-5.5.68-1.el7.x86_64
[root@localhost ~]# ls /etc/ | grep mymy.cnfmy.cnf.d

卸载mariadb

rpm -e --nodeps mariadb-libs

2.2 安装依赖

截至目前,CentOS7的yum源已经无法访问,因此需要先更换为阿里巴巴的yum源

cp /etc/yum.repos.d/CentOS-Base.repo /etc/yum.repos.d/CentOS-Base.repo.backup
curl -o /etc/yum.repos.d/CentOS-Base.repo http://mirrors.aliyun.com/repo/Centos-7.repo
yum clean all
yum makecache

然后安装依赖项,我采用的系统版本需要额外安装这两个依赖,缺失这两个将导致后续安装mysql-community-server时出错

yum install -y net-toolsyum install -y perl

3.安装和初始化

cd进入/opt目录安装刚刚解压出的软件包,不需要每个都安装,仅安装MySQL的Server和Client以及依赖的包,安装需要按照顺序,因为rpm包有相互的依赖关系,需要安装的包以及安装顺序如下:

  1. mysql-community-common-5.7.28-1.el7.x86_64.rpm
  2. mysql-community-libs-5.7.28-1.el7.x86_64.rpm
  3. mysql-community-client-5.7.28-1.el7.x86_64.rpm
  4. mysql-community-server-5.7.28-1.el7.x86_64.rpm

使用rpm -ivh命令安装这些包

[root@localhost opt]# rpm -ivh mysql-community-common-5.7.28-1.el7.x86_64.rpm警告:mysql-community-common-5.7.28-1.el7.x86_64.rpm: 头V3 DSA/SHA1 Signature, 密钥 ID 5072e1f5: NOKEY准备中...                          ################################# [100%]正在升级/安装...   1:mysql-community-common-5.7.28-1.e################################# [100%][root@localhost opt]# rpm -ivh mysql-community-libs-mysql-community-libs-5.7.28-1.el7.x86_64.rpm         mysql-community-libs-compat-5.7.28-1.el7.x86_64.rpm[root@localhost opt]# rpm -ivh  mysql-community-libs-5.7.28-1.el7.x86_64.rpm警告:mysql-community-libs-5.7.28-1.el7.x86_64.rpm: 头V3 DSA/SHA1 Signature, 密钥 ID 5072e1f5: NOKEY准备中...                          ################################# [100%]正在升级/安装...   1:mysql-community-libs-5.7.28-1.el7################################# [100%][root@localhost opt]# rpm -ivh  mysql-community-client-5.7.28-1.el7.x86_64.rpm警告:mysql-community-client-5.7.28-1.el7.x86_64.rpm: 头V3 DSA/SHA1 Signature, 密钥 ID 5072e1f5: NOKEY准备中...                          ################################# [100%]正在升级/安装...   1:mysql-community-client-5.7.28-1.e################################# [100%][root@localhost opt]# rpm -ivh  mysql-community-server-5.7.28-1.el7.x86_64.rpm警告:mysql-community-server-5.7.28-1.el7.x86_64.rpm: 头V3 DSA/SHA1 Signature, 密钥 ID 5072e1f5: NOKEY准备中...                          ################################# [100%]正在升级/安装...   1:mysql-community-server-5.7.28-1.e################################# [100%]

通过rpm -qa已经能看到装上去的包了

[root@localhost opt]# rpm -qa | grep mysqlmysql-community-libs-5.7.28-1.el7.x86_64mysql-community-common-5.7.28-1.el7.x86_64mysql-community-client-5.7.28-1.el7.x86_64mysql-community-server-5.7.28-1.el7.x86_64

查看一下安装的版本

[root@localhost opt]# mysql --versionmysql  Ver 14.14 Distrib 5.7.28, for Linux (x86_64) using  EditLine wrapper

为了保证数据库目录与文件的所有者为mysql登录用户,如果是以root身份运行mysql服务,需要执行下面的命令初始化:

mysqld --initialize --user=mysql

说明: --initialize选项默认以安全模式来初始化,则会为root用户生成一个密码并将该密码标记为过期,登录后你需要设置一个新的密码。生成的临时密码会往日志中记录一份,查看密码:

cat /var/log/mysqld.log
[root@localhost opt]# mysqld --initialize --user=mysql[root@localhost opt]# cat /var/log/mysqld.log2025-04-06T12:26:08.004754Z 0 [Warning] TIMESTAMP with implicit DEFAULT value is deprecated. Please use --explicit_defaults_for_timestamp server option (see documentation for more details).2025-04-06T12:26:08.339163Z 0 [Warning] InnoDB: New log files created, LSN=457902025-04-06T12:26:08.403698Z 0 [Warning] InnoDB: Creating foreign key constraint system tables.2025-04-06T12:26:08.470936Z 0 [Warning] No existing UUID has been found, so we assume that this is the first time that this server has been started. Generating a new UUID: 5151ca04-12e2-11f0-8409-000c29920d74.2025-04-06T12:26:08.472919Z 0 [Warning] Gtid table is not ready to be used. Table 'mysql.gtid_executed' cannot be opened.2025-04-06T12:26:10.249602Z 0 [Warning] CA certificate ca.pem is self signed.2025-04-06T12:26:10.762512Z 1 [Note] A temporary password is generated for root@localhost: G*kRrqwYn7KX

接下来启动服务

systemctl start mysqld
[root@localhost opt]# systemctl status mysqld● mysqld.service - MySQL Server   Loaded: loaded (/usr/lib/systemd/system/mysqld.service; enabled; vendor preset: disabled)   Active: active (running) since 日 2025-04-06 20:27:54 CST; 8s ago     Docs: man:mysqld(8)           http://dev.mysql.com/doc/refman/en/using-systemd.html  Process: 1468 ExecStart=/usr/sbin/mysqld --daemonize --pid-file=/var/run/mysqld/mysqld.pid $MYSQLD_OPTS (code=exited, status=0/SUCCESS)  Process: 1451 ExecStartPre=/usr/bin/mysqld_pre_systemd (code=exited, status=0/SUCCESS) Main PID: 1471 (mysqld)   CGroup: /system.slice/mysqld.service           └─1471 /usr/sbin/mysqld --daemonize --pid-file=/var/run/mysqld/mysqld.pid4月 06 20:27:53 localhost.localdomain systemd[1]: Starting MySQL Server...4月 06 20:27:54 localhost.localdomain systemd[1]: Started MySQL Server.

4.登录和基础配置

4.1登录并重置密码

用之前生成的初始密码登录MySQL的root用户,命令是:mysql -uroot -pEnter password:后输入密码回车,登录成功并进入MySQL交互命令行,但是不能操作,提示需要重置root密码

[root@localhost opt]# mysql -uroot -pEnter password: Welcome to the MySQL monitor.  Commands end with ; or \g.Your MySQL connection id is 2Server version: 5.7.28Copyright (c) 2000, 2019, Oracle and/or its affiliates. All rights reserved.Oracle is a registered trademark of Oracle Corporation and/or itsaffiliates. Other names may be trademarks of their respectiveowners.Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.mysql> show databases;ERROR 1820 (HY000): You must reset your password using ALTER USER statement before executing this statement.

执行这个语句重置密码

alter user 'root'@'localhost' identified by 'lzj';

4.2 打开远程连接

安装好的MySQL会自带4个数据库,连接配置的表位于mysql库的user表上,use mysql;将当前库切换为mysql,通过select host,user from user;查看得知系统中的4个用户都仅允许本机访问

mysql> show databases;+--------------------+| Database           |+--------------------+| information_schema || mysql              || performance_schema || sys                |+--------------------+4 rows in set (0.00 sec)mysql> use mysql;Database changedmysql> select host,user from user;+-----------+---------------+| host      | user          |+-----------+---------------+| localhost | mysql.session || localhost | mysql.sys     || localhost | root          |+-----------+---------------+3 rows in set (0.00 sec)

通过执行以下语句修改root用户可访问的地址,可以采用通配符,按自己需要修改即可,我选择直接修改为%

允许root用户从192.168.1.x主机访问

update mysql.user set host ='192.168.1.%' where user = 'root';

允许root用户从一切远程主机访问

update mysql.user set host ='%' where user = 'root';

修改完成,刷新权限,即可远程连接

flush privileges;

远程连接前一定要关闭CentOS7自带的防火墙firewalld

4.3 修改字符集

在MySQL8.0版本之前,默认字符集为latin1,utf8字符集指向的是utf8mb3。网站开发人员在数据库设计的时候往往会将编码修改为utf8字符集,如果遗忘修改默认的编码,就会出现乱码的问题,从MySQL8.0开始,数据库的默认编码将改为utf8mb4,从而避免上述乱码的问题。

查看当前字符集

show variables like '%char%';
mysql> show variables like '%char%';+--------------------------+----------------------------+| Variable_name            | Value                      |+--------------------------+----------------------------+| character_set_client     | utf8                       || character_set_connection | utf8                       || character_set_database   | latin1                     || character_set_filesystem | binary                     || character_set_results    | utf8                       || character_set_server     | latin1                     || character_set_system     | utf8                       || character_sets_dir       | /usr/share/mysql/charsets/ |+--------------------------+----------------------------+8 rows in set (0.00 sec)

字符集不支持中文,因此MySQL5.7需要修改服务端字符集,修改字符集的配置文件位于/etc/my.cnf,将配置添加在[mysqld]节点上

[mysqld]character-set-server = utf8

修改后新建的库和表才会使用utf8,之前建的库表的字符集不会被自动修改,需要手动修改:

⚠️ utf8兼容latin1,因此之前的数据支持修改字符集为utf8,如果不兼容就可能出现乱码问题,不兼容的情况可以将数据导出在重新插入

修改库

alter database db_xxx character set 'utf8';

修改表

alter table tb_xxx convert to character set 'utf8';
  •  

RockyLinux9环境下编译MySQL8

Linux版本: Rocky Linux release 9.5 (Blue Onyx)

1.下载

打开MySQL-Community-Server官方下载页面:https://downloads.mysql.com/archives/community/

筛选出要下载的版本,Product Version选择8.0.39,Operating System选择Source Code,OS Version选择All Operating Systems,然后选择最后一个自带boost的MySQL源码包All Operating Systems (Generic) (Architecture Independent), Compressed TAR Archive
Includes Boost Headers (mysql-boost-8.0.39.tar.gz),进行下载,并将下载到的源码包先放到服务器的/opt目录下。

下载链接:

解压tar包,得到文件夹mysql-8.0.39,里面就是MySQL源码和一些构建信息

[root@localhost opt]# cd mysql-8.0.39/[root@localhost mysql-8.0.39]# lltotal 684-rw-r--r--.  1 7161 31415  94660 Jul 13 03:15 CMakeLists.txtdrwxr-xr-x.  2 7161 31415     65 Jul 13 03:43 Docs-rw-r--r--.  1 7161 31415   5425 Jul 13 03:15 Doxyfile-ignored-rw-r--r--.  1 7161 31415 123326 Jul 13 03:15 Doxyfile.in-rw-r--r--.  1 7161 31415    333 Jul 13 03:15 INSTALL-rw-r--r--.  1 7161 31415 282186 Jul 13 03:15 LICENSE-rw-r--r--.  1 7161 31415    118 Jul 13 03:15 MYSQL_VERSION-rw-r--r--.  1 7161 31415    666 Jul 13 03:15 READMEdrwxr-xr-x.  3 7161 31415     26 Jul 13 03:45 boostdrwxr-xr-x. 10 7161 31415   4096 Jul 13 03:43 clientdrwxr-xr-x.  4 7161 31415   4096 Jul 13 03:43 cmakedrwxr-xr-x. 16 7161 31415   4096 Jul 13 03:43 components-rw-r--r--.  1 7161 31415  10843 Jul 13 03:15 config.h.cmake-rw-r--r--.  1 7161 31415  23642 Jul 13 03:15 configure.cmakedrwxr-xr-x.  3 7161 31415    124 Jul 13 03:43 doxygen_resourcesdrwxr-xr-x. 19 7161 31415   4096 Jul 13 03:43 extradrwxr-xr-x.  4 7161 31415   8192 Jul 13 03:43 includedrwxr-xr-x.  5 7161 31415    112 Jul 13 03:43 libbinlogeventsdrwxr-xr-x.  3 7161 31415     39 Jul 13 03:43 libbinlogstandalonedrwxr-xr-x.  4 7161 31415     54 Jul 13 03:43 libchangestreamsdrwxr-xr-x.  7 7161 31415   4096 Jul 13 03:43 libmysqldrwxr-xr-x.  2 7161 31415   4096 Jul 13 03:43 libservicesdrwxr-xr-x.  2 7161 31415   4096 Jul 13 03:45 mandrwxr-xr-x. 10 7161 31415   4096 Jul 13 03:43 mysql-testdrwxr-xr-x.  2 7161 31415   4096 Jul 13 03:43 mysysdrwxr-xr-x. 10 7161 31415    131 Jul 13 03:43 packagingdrwxr-xr-x. 25 7161 31415   4096 Jul 13 03:43 plugindrwxr-xr-x.  6 7161 31415    155 Jul 13 03:43 router-rw-r--r--.  1 7161 31415   6268 Jul 13 03:15 run_doxygen.cmakedrwxr-xr-x.  4 7161 31415   4096 Jul 13 03:43 scriptsdrwxr-xr-x.  3 7161 31415    155 Jul 13 03:43 sharedrwxr-xr-x. 24 7161 31415  24576 Jul 13 03:44 sqldrwxr-xr-x.  3 7161 31415   4096 Jul 13 03:43 sql-commondrwxr-xr-x. 14 7161 31415   4096 Jul 13 03:45 storagedrwxr-xr-x.  3 7161 31415   4096 Jul 13 03:43 stringsdrwxr-xr-x.  2 7161 31415   4096 Jul 13 03:43 support-filesdrwxr-xr-x.  2 7161 31415    101 Jul 13 03:43 testclientsdrwxr-xr-x.  5 7161 31415     70 Jul 13 03:43 unittestdrwxr-xr-x.  2 7161 31415   4096 Jul 13 03:43 utilitiesdrwxr-xr-x.  2 7161 31415   4096 Jul 13 03:43 vio

和编译MySQl5.7一样,进入boost/文件夹下,查看boost是哪个版本,一会编译时会用到。

[root@localhost mysql-8.0.39]# cd boost/[root@localhost boost]# lltotal 0drwxr-xr-x. 3 7161 31415 19 Jul 13 03:45 boost_1_77_0

2.编译安装前准备

1.新添加一个用户: mysql

useradd mysql

2.安装编译所需依赖

dnf install -y make cmake gcc gcc-c++ ncurses-devel  openssl-devel libtirpc

还有些不能通过yum安装的软件包要手动下载安装

yum install -y wgetwget https://dl.rockylinux.org/pub/rocky/9/devel/x86_64/os/Packages/l/libtirpc-devel-1.3.3-9.el9.x86_64.rpmwget https://dl.rockylinux.org/pub/rocky/9/AppStream/x86_64/os/Packages/r/rpcgen-1.4-9.el9.x86_64.rpmrpm -ivh libtirpc-devel-1.3.3-9.el9.x86_64.rpmrpm -ivh rpcgen-1.4-9.el9.x86_64.rpm

然后再安装MySQL8.0.39需要的gcc工具集,安装前,需要升级软件包libsemanage为高版本,同样需要下载软件包到本地,然后rpm -Uvh更新

wget https://dl.rockylinux.org/pub/rocky/9/BaseOS/x86_64/os/Packages/l/libsemanage-3.6-2.1.el9_5.x86_64.rpmrpm -Uvh libsemanage-3.6-2.1.el9_5.x86_64.rpm

然后执行安装gcc工具集命令

yum install gcc-toolset-12-gcc gcc-toolset-12-gcc-c++ gcc-toolset-12-binutils gcc-toolset-12-annobin-annocheck gcc-toolset-12-annobin-plugin-gcc

3./usr/local/下必须新建文件夹mysql

mkdir /usr/local/mysql

3.编译安装

1.在源码目录/opt/mysql-8.0.39下执行cmake命令,进行编译前的检查和配置,使用CMake配置和生成MySQL源码编译所需的构建系统文件,安装MySQL到/usr/local/mysql目录下,-DWITH_BOOST=的路径根据源码包内自带boost的版本而定,默认端口3306,数据路径/usr/local/mysql/data,字符集utf8mb4

cmake \-DCMAKE_INSTALL_PREFIX=/usr/local/mysql \-DINSTALL_DATADIR=/usr/local/mysql/data \-DMYSQL_USER=mysql \-DMYSQL_UNIX_ADDR=/usr/local/mysql/mysqld.sock \-DDEFAULT_CHARSET=utf8mb4 \-DDEFAULT_COLLATION=utf8mb4_general_ci \-DWITH_INNOBASE_STORAGE_ENGINE=1 \-DWITH_EMBEDDED_SERVER=1 \-DFORCE_INSOURCE_BUILD=1 \-DWITH_MYISAM_STORAGE_ENGINE=1 \-DENABLED_LOCAL_INFILE=1 \-DEXTRA_CHARSETS=all \-DWITH_BOOST=boost/boost_1_77_0/

2.cmake命令执行成功后,执行以下make命令开始编译安装,-j 2是同时开启两个线程编译,加速编译

make -j 2 && make install

执行后,开始编译,不断输出日志,编译的过程很耗时间,需要等待很久,编译完成后,可以在/usr/local/mysql目录下看到编译好的MySQL

[root@localhost mysql-8.0.39]# cd /usr/local/mysql/[root@localhost mysql]# lltotal 704-rw-r--r--.  1 root root 282186 Jul 13 03:15 LICENSE-rw-r--r--.  1 root root 282186 Jul 13 03:15 LICENSE-test-rw-r--r--.  1 root root 115948 Jul 13 03:15 LICENSE.router-rw-r--r--.  1 root root    666 Jul 13 03:15 README-rw-r--r--.  1 root root    666 Jul 13 03:15 README-test-rw-r--r--.  1 root root    679 Jul 13 03:15 README.routerdrwxr-xr-x.  2 root root   4096 Dec 21 18:08 bindrwxr-xr-x.  2 root root     69 Dec 21 18:08 docsdrwxr-xr-x.  3 root root   4096 Dec 21 18:08 includedrwxr-xr-x.  6 root root   4096 Dec 21 18:08 libdrwxr-xr-x.  4 root root     30 Dec 21 18:08 mandrwxr-xr-x. 10 root root   4096 Dec 21 18:08 mysql-test-rw-r--r--.  1 root root   1672 Dec 21 13:33 mysqlrouter-log-rotatedrwxrwxr-x.  2 root root      6 Dec 21 18:08 rundrwxr-xr-x. 28 root root   4096 Dec 21 18:08 sharedrwxr-xr-x.  2 root root     77 Dec 21 18:08 support-filesdrwxr-xr-x.  3 root root     17 Dec 21 18:08 var

4.安装后配置

1.在/usr/local/mysql创建一个mysql配置文件,并写入字符集相关配置进去。

vi /usr/local/mysql/my.cnf
[mysqld]port = 3306character-set-server=utf8mb4default_authentication_plugin=mysql_native_password[mysql]default-character-set=utf8mb4

2.在/usr/local/mysql下执行命令初始化MySQL数据库

./bin/mysqld  \--defaults-file=/usr/local/mysql/my.cnf  \--user=mysql \--basedir=/usr/local/mysql  \--datadir=/usr/local/mysql/data  \--initialize-insecure

3.在/usr/local/mysql目录执行以下命令,将文件所有者设置为之前新建的mysql用户

chown -R mysql:mysql ./

5.启动和测试MySQL Server

/usr/local/mysql下执行启动脚本文件启动数据库

./support-files/mysql.server start

关闭

./support-files/mysql.server stop

重启

./support-files/mysql.server restart

登录数据库测试,切换到目录/usr/local/mysql,执行./bin/mysql -uroot -p命令登录数据库,root密码默认是空

[root@localhost mysql]# ./bin/mysql -uroot -pEnter password: Welcome to the MySQL monitor.  Commands end with ; or \g.Your MySQL connection id is 8Server version: 8.0.39 Source distributionCopyright (c) 2000, 2024, Oracle and/or its affiliates.Oracle is a registered trademark of Oracle Corporation and/or itsaffiliates. Other names may be trademarks of their respectiveowners.Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.mysql> show databases;+--------------------+| Database           |+--------------------+| information_schema || mysql              || performance_schema || sys                |+--------------------+4 rows in set (0.01 sec)

修改默认密码

mysql> ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'lzj';

修改后要刷新权限

mysql> FLUSH PRIVILEGES;

刷新权限后退出使用新密码再次登录,此时已经可以正常操作数据库了。

[root@localhost mysql]# ./bin/mysql -uroot -pEnter password: Welcome to the MySQL monitor.  Commands end with ; or \g.Your MySQL connection id is 9Server version: 8.0.39 Source distributionCopyright (c) 2000, 2024, Oracle and/or its affiliates.Oracle is a registered trademark of Oracle Corporation and/or itsaffiliates. Other names may be trademarks of their respectiveowners.Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.mysql> show databases;+--------------------+| Database           |+--------------------+| information_schema || mysql              || performance_schema || sys                |+--------------------+4 rows in set (0.01 sec)mysql> select version();+-----------+| version() |+-----------+| 8.0.39    |+-----------+1 row in set (0.00 sec)mysql> SHOW VARIABLES LIKE 'character_set%';+--------------------------+----------------------------------+| Variable_name            | Value                            |+--------------------------+----------------------------------+| character_set_client     | utf8mb4                          || character_set_connection | utf8mb4                          || character_set_database   | utf8mb4                          || character_set_filesystem | binary                           || character_set_results    | utf8mb4                          || character_set_server     | utf8mb4                          || character_set_system     | utf8mb3                          || character_sets_dir       | /usr/local/mysql/share/charsets/ |+--------------------------+----------------------------------+8 rows in set (0.02 sec)

至此,就完成了在RockyLinux9上面安装和运行MySQL8

  •  

MySQL字符集及底层原理

1.字符集优先级

查看MySQL中的字符集:

mysql> show variables like '%char%';+--------------------------+----------------------------+| Variable_name            | Value                      |+--------------------------+----------------------------+| character_set_client     | utf8                       || character_set_connection | utf8                       || character_set_database   | latin1                     || character_set_filesystem | binary                     || character_set_results    | utf8                       || character_set_server     | latin1                     || character_set_system     | utf8                       || character_sets_dir       | /usr/share/mysql/charsets/ |+--------------------------+----------------------------+8 rows in set (0.00 sec)
  • character_set_server: 服务器级别的字符集

  • character_set_database: 当前数据库的字符集

  • character_set_client: 服务器解码请求时使用的字符集

  • character_set_connection: 服务器处理请求时会把请求字符串从character_set_client转为character_set_connection

  • character_set_results: 服务器向客户端返回数据时使用的字符集

MySQL有4个级别的字符集和比较规则,分别是

  1. 服务器级别: character_set_server,配置在my.cnf配置文件character-set-server的字符集
  2. 数据库级别: character_set_database,character_set_server决定character_set_database
  3. 表级别: 在某个库下建表,如果没有指定表的字符集,默认与当前库的字符集一致
  4. 列级别: 表中的字段不指定字符集的时候,默认采用所在表的字符集。

2.utf8mb3和utf8mb4

查看MySQL支持的字符集(Charset)和对应的比较规则(Default collation)以及占用大小(Maxlen):

mysql> show charset;+----------+---------------------------------+---------------------+--------+| Charset  | Description                     | Default collation   | Maxlen |+----------+---------------------------------+---------------------+--------+| big5     | Big5 Traditional Chinese        | big5_chinese_ci     |      2 || dec8     | DEC West European               | dec8_swedish_ci     |      1 || cp850    | DOS West European               | cp850_general_ci    |      1 || hp8      | HP West European                | hp8_english_ci      |      1 || koi8r    | KOI8-R Relcom Russian           | koi8r_general_ci    |      1 || latin1   | cp1252 West European            | latin1_swedish_ci   |      1 || latin2   | ISO 8859-2 Central European     | latin2_general_ci   |      1 || swe7     | 7bit Swedish                    | swe7_swedish_ci     |      1 || ascii    | US ASCII                        | ascii_general_ci    |      1 || ujis     | EUC-JP Japanese                 | ujis_japanese_ci    |      3 || sjis     | Shift-JIS Japanese              | sjis_japanese_ci    |      2 || hebrew   | ISO 8859-8 Hebrew               | hebrew_general_ci   |      1 || tis620   | TIS620 Thai                     | tis620_thai_ci      |      1 || euckr    | EUC-KR Korean                   | euckr_korean_ci     |      2 || koi8u    | KOI8-U Ukrainian                | koi8u_general_ci    |      1 || gb2312   | GB2312 Simplified Chinese       | gb2312_chinese_ci   |      2 || greek    | ISO 8859-7 Greek                | greek_general_ci    |      1 || cp1250   | Windows Central European        | cp1250_general_ci   |      1 || gbk      | GBK Simplified Chinese          | gbk_chinese_ci      |      2 || latin5   | ISO 8859-9 Turkish              | latin5_turkish_ci   |      1 || armscii8 | ARMSCII-8 Armenian              | armscii8_general_ci |      1 || utf8     | UTF-8 Unicode                   | utf8_general_ci     |      3 || ucs2     | UCS-2 Unicode                   | ucs2_general_ci     |      2 || cp866    | DOS Russian                     | cp866_general_ci    |      1 || keybcs2  | DOS Kamenicky Czech-Slovak      | keybcs2_general_ci  |      1 || macce    | Mac Central European            | macce_general_ci    |      1 || macroman | Mac West European               | macroman_general_ci |      1 || cp852    | DOS Central European            | cp852_general_ci    |      1 || latin7   | ISO 8859-13 Baltic              | latin7_general_ci   |      1 || utf8mb4  | UTF-8 Unicode                   | utf8mb4_general_ci  |      4 || cp1251   | Windows Cyrillic                | cp1251_general_ci   |      1 || utf16    | UTF-16 Unicode                  | utf16_general_ci    |      4 || utf16le  | UTF-16LE Unicode                | utf16le_general_ci  |      4 || cp1256   | Windows Arabic                  | cp1256_general_ci   |      1 || cp1257   | Windows Baltic                  | cp1257_general_ci   |      1 || utf32    | UTF-32 Unicode                  | utf32_general_ci    |      4 || binary   | Binary pseudo charset           | binary              |      1 || geostd8  | GEOSTD8 Georgian                | geostd8_general_ci  |      1 || cp932    | SJIS for Windows Japanese       | cp932_japanese_ci   |      2 || eucjpms  | UJIS for Windows Japanese       | eucjpms_japanese_ci |      3 || gb18030  | China National Standard GB18030 | gb18030_chinese_ci  |      4 |+----------+---------------------------------+---------------------+--------+41 rows in set (0.00 sec)

utf8字符集表示一个字符需要使用1-4个字节,但是我们常用的一些字符使用1-3个字节就可以表示了。而字符集表示一个字符所用的最大字节长度,在某些方面会影响系统的存储和性能,所以设计MySOL的设计者自定义了两个概念

  • utf8mb3 阉割过的utf8字符集,只使用1-3个字节表示字符。
  • utf8mb4 正宗的utf8字符集,使用1-4个字节表示字符。

在MySQL中utf8是utf8mb3的别名,所以之后在MySQL中提到utf8就意味着使用1-3个字节来表示一个字符。如果有使用4字节编码一个字符的情况,比如存储一些emoii表情,那请使用utf8mb4。

3.比较规则

show charset;表中,一共支持41种字符集,其中的Default collation列表示这种字符集默认的比较规则,里面包含着该比较规则主要作用于哪种语言,比如utf8_polish_ci表示以波兰语的规则比较,utf8_spanish_ci是以西班牙语的规则比较,utf8_general_ci是一种通用的比较规则。

后缀表示该比较规则是否区分语言中的重音、大小写,具体如下:

后缀英文释义描述
_aiaccent insensitive不区分重音
_asaccent sensitive区分重音
_cicase insensitive不区分大小写
_cscase sensitive区分大小写
_binbinary以二进制方式比较

查看某种字符集的比较规则

mysql> SHOW COLLATION LIKE'utf8%';+--------------------------+---------+-----+---------+----------+---------+| Collation                | Charset | Id  | Default | Compiled | Sortlen |+--------------------------+---------+-----+---------+----------+---------+| utf8_general_ci          | utf8    |  33 | Yes     | Yes      |       1 || utf8_bin                 | utf8    |  83 |         | Yes      |       1 || utf8_unicode_ci          | utf8    | 192 |         | Yes      |       8 || utf8_icelandic_ci        | utf8    | 193 |         | Yes      |       8 || utf8_latvian_ci          | utf8    | 194 |         | Yes      |       8 || utf8_romanian_ci         | utf8    | 195 |         | Yes      |       8 || utf8_slovenian_ci        | utf8    | 196 |         | Yes      |       8 || utf8_polish_ci           | utf8    | 197 |         | Yes      |       8 || utf8_estonian_ci         | utf8    | 198 |         | Yes      |       8 || utf8_spanish_ci          | utf8    | 199 |         | Yes      |       8 || utf8_swedish_ci          | utf8    | 200 |         | Yes      |       8 || utf8_turkish_ci          | utf8    | 201 |         | Yes      |       8 || utf8_czech_ci            | utf8    | 202 |         | Yes      |       8 || utf8_danish_ci           | utf8    | 203 |         | Yes      |       8 || utf8_lithuanian_ci       | utf8    | 204 |         | Yes      |       8 || utf8_slovak_ci           | utf8    | 205 |         | Yes      |       8 || utf8_spanish2_ci         | utf8    | 206 |         | Yes      |       8 || utf8_roman_ci            | utf8    | 207 |         | Yes      |       8 || utf8_persian_ci          | utf8    | 208 |         | Yes      |       8 || utf8_esperanto_ci        | utf8    | 209 |         | Yes      |       8 || utf8_hungarian_ci        | utf8    | 210 |         | Yes      |       8 || utf8_sinhala_ci          | utf8    | 211 |         | Yes      |       8 || utf8_german2_ci          | utf8    | 212 |         | Yes      |       8 || utf8_croatian_ci         | utf8    | 213 |         | Yes      |       8 || utf8_unicode_520_ci      | utf8    | 214 |         | Yes      |       8 || utf8_vietnamese_ci       | utf8    | 215 |         | Yes      |       8 || utf8_general_mysql500_ci | utf8    | 223 |         | Yes      |       1 || utf8mb4_general_ci       | utf8mb4 |  45 | Yes     | Yes      |       1 || utf8mb4_bin              | utf8mb4 |  46 |         | Yes      |       1 || utf8mb4_unicode_ci       | utf8mb4 | 224 |         | Yes      |       8 || utf8mb4_icelandic_ci     | utf8mb4 | 225 |         | Yes      |       8 || utf8mb4_latvian_ci       | utf8mb4 | 226 |         | Yes      |       8 || utf8mb4_romanian_ci      | utf8mb4 | 227 |         | Yes      |       8 || utf8mb4_slovenian_ci     | utf8mb4 | 228 |         | Yes      |       8 || utf8mb4_polish_ci        | utf8mb4 | 229 |         | Yes      |       8 || utf8mb4_estonian_ci      | utf8mb4 | 230 |         | Yes      |       8 || utf8mb4_spanish_ci       | utf8mb4 | 231 |         | Yes      |       8 || utf8mb4_swedish_ci       | utf8mb4 | 232 |         | Yes      |       8 || utf8mb4_turkish_ci       | utf8mb4 | 233 |         | Yes      |       8 || utf8mb4_czech_ci         | utf8mb4 | 234 |         | Yes      |       8 || utf8mb4_danish_ci        | utf8mb4 | 235 |         | Yes      |       8 || utf8mb4_lithuanian_ci    | utf8mb4 | 236 |         | Yes      |       8 || utf8mb4_slovak_ci        | utf8mb4 | 237 |         | Yes      |       8 || utf8mb4_spanish2_ci      | utf8mb4 | 238 |         | Yes      |       8 || utf8mb4_roman_ci         | utf8mb4 | 239 |         | Yes      |       8 || utf8mb4_persian_ci       | utf8mb4 | 240 |         | Yes      |       8 || utf8mb4_esperanto_ci     | utf8mb4 | 241 |         | Yes      |       8 || utf8mb4_hungarian_ci     | utf8mb4 | 242 |         | Yes      |       8 || utf8mb4_sinhala_ci       | utf8mb4 | 243 |         | Yes      |       8 || utf8mb4_german2_ci       | utf8mb4 | 244 |         | Yes      |       8 || utf8mb4_croatian_ci      | utf8mb4 | 245 |         | Yes      |       8 || utf8mb4_unicode_520_ci   | utf8mb4 | 246 |         | Yes      |       8 || utf8mb4_vietnamese_ci    | utf8mb4 | 247 |         | Yes      |       8 |+--------------------------+---------+-----+---------+----------+---------+53 rows in set (0.00 sec)

查看服务器的字符集和比较规则

mysql> SHOW VARIABLES LIKE '%_server';+----------------------+-------------------+| Variable_name        | Value             |+----------------------+-------------------+| character_set_server | latin1            || collation_server     | latin1_swedish_ci |+----------------------+-------------------+2 rows in set (0.02 sec)

查看当前数据库的字符集和比较规则

mysql> SHOW VARIABLES LIKE '%_database';+------------------------+-------------------+| Variable_name          | Value             |+------------------------+-------------------+| character_set_database | latin1            || collation_database     | latin1_swedish_ci || skip_show_database     | OFF               |+------------------------+-------------------+3 rows in set (0.00 sec)

修改具体数据库的字符集

ALTER DATABASE dbtest DEFAULT CHARACTER SET 'utf8' COLLATE 'utf8_general_ci';

utf8_unicode_ciutf8_general_ci对中、英文来说没有实质的差别。utf8_general_ci校对速度快,但准确度稍差。utf8_unicode_ci准确度高,但校对速度稍慢。

⚠️ 一般情况,用utf8_general_ci就够了,但如果你的应用有德语、法语或者俄语,请一定使用utf8_unicode_ci
⚠️ 修改了数据库的默认字符集和比较规则后,原来已经创建的表格的字符集和比较规则并不会改变,如果需要,那么需单独修改。

  •  

Redis的公共操作命令

1.Key操作命令

Redis是Key-Value数据库,Key都是字符串且区分大小写,关于Redis的key操作,主要有常见的以下几个

Redis的命令是不区分大小写的

1.1 keys *

查看当前库所有的Key,类似于数据库的select * from tb_xxx

127.0.0.1:6379> keys *1) "k1"2) "k2"

1.2 exists <key]>

Key是否存在,返回bool,1代表true,0代表false

127.0.0.1:6379> exists k1(integer) 1127.0.0.1:6379> exists k2(integer) 1127.0.0.1:6379> exists k3(integer) 0

Redis的底层使用C语言实现,很多命令返回bool时,多用0和1表示

1.3 type <key>

key对应的value是什么类型

127.0.0.1:6379> type k1string

1.4 del <key>

删除数据,返回bool

127.0.0.1:6379> del k2(integer) 1

1.5 unlink <key>

非阻塞删除,仅仅将key从keyspace元数据中删除,真正的数据删除将在后续异步进行,返回bool

127.0.0.1:6379> unlink k1(integer) 1

1.6 ttl <key>

查看key还有多少秒过期,-1代表永不过期,-2代表已过期,通常和expire命令搭配使用

127.0.0.1:6379> ttl k1(integer) -1

1.7 expire <key> <秒数>

为指定的key设置过期时间

127.0.0.1:6379> expire k1 100(integer) 1127.0.0.1:6379> ttl k1(integer) 90127.0.0.1:6379> ttl k1(integer) 86

1.8 move <key> <index>

将当前key移动到指定的数据库中,返回bool

127.0.0.1:6379> move k1 2(integer) 1

2.库操作命令

2.1 select <index>

选中几号仓库。redis.conf配置文件默认Redis共16个数据库(0-15),默认选中0号库

127.0.0.1:6379> select 2OK127.0.0.1:6379[2]> select 3OK127.0.0.1:6379[3]> 

2.2 dbsize

查看当前库有多少key

127.0.0.1:6379[3]> dbsize(integer) 0127.0.0.1:6379[3]> set k1 v1OK127.0.0.1:6379[3]> dbsize(integer) 1127.0.0.1:6379[3]> 

2.3 flushdb

清空当前库中的所有key

127.0.0.1:6379> flushdbOK

2.4 flushall

清空整个Redis中的所有key

127.0.0.1:6379> flushallOK

3.其他命令

3.1 help @<type>

命令行下输入help @<type>命令,redis服务器会返回该数据类型的所有用法

127.0.0.1:6379> help @stringAPPEND key valuesummary: Appends a string to the value of a key. Creates the key if it doesn't exist.since: 2.0.0DECR keysummary: Decrements the integer value of a key by one. Uses 0 as initial value if the key doesn't exist.since: 1.0.0DECRBY key decrementsummary: Decrements a number from the integer value of a key. Uses 0 as initial value if the key doesn't exist.since: 1.0.0GET keysummary: Returns the string value of a key.since: 1.0.0GETDEL keysummary: Returns the string value of a key after deleting the key.since: 6.2.0GETEX key [EX seconds|PX milliseconds|EXAT unix-time-seconds|PXAT unix-time-milliseconds|PERSIST]summary: Returns the string value of a key after setting its expiration time.since: 6.2.0GETRANGE key start endsummary: Returns a substring of the string stored at a key.since: 2.4.0GETSET key valuesummary: Returns the previous string value of a key after setting it to a new value.since: 1.0.0INCR keysummary: Increments the integer value of a key by one. Uses 0 as initial value if the key doesn't exist.since: 1.0.0INCRBY key incrementsummary: Increments the integer value of a key by a number. Uses 0 as initial value if the key doesn't exist.since: 1.0.0INCRBYFLOAT key incrementsummary: Increment the floating point value of a key by a number. Uses 0 as initial value if the key doesn't exist.since: 2.6.0LCS key1 key2 [LEN] [IDX] [MINMATCHLEN min-match-len] [WITHMATCHLEN]summary: Finds the longest common substring.since: 7.0.0MGET key [key ...]summary: Atomically returns the string values of one or more keys.since: 1.0.0MSET key value [key value ...]summary: Atomically creates or modifies the string values of one or more keys.since: 1.0.1MSETNX key value [key value ...]summary: Atomically modifies the string values of one or more keys only when all keys don't exist.since: 1.0.1PSETEX key milliseconds valuesummary: Sets both string value and expiration time in milliseconds of a key. The key is created if it doesn't exist.since: 2.6.0SET key value [NX|XX] [GET] [EX seconds|PX milliseconds|EXAT unix-time-seconds|PXAT unix-time-milliseconds|KEEPTTL]summary: Sets the string value of a key, ignoring its type. The key is created if it doesn't exist.since: 1.0.0SETEX key seconds valuesummary: Sets the string value and expiration time of a key. Creates the key if it doesn't exist.since: 2.0.0SETNX key valuesummary: Set the string value of a key only when the key doesn't exist.since: 1.0.0SETRANGE key offset valuesummary: Overwrites a part of a string value with another by an offset. Creates the key if it doesn't exist.since: 2.2.0STRLEN keysummary: Returns the length of a string value.since: 2.2.0SUBSTR key start endsummary: Returns a substring from a string value.since: 1.0.0127.0.0.1:6379> 
  •  

Redis数据结构之Bitfleid

1.概述

bitfield将一个字符串看作一个由二进制位构成的数组,并对这个数组中任何偏移进行访问,通过bitfield命令可以一次性操作多个比特位域(指的是连续的多个比特位),它会执行一系列操作并返回一个响应数组,这个数组中的元素对应参数列表中的相应操作的执行结果。

在实际应用中,bitfield并不是一种常用的数据结构。

  •  

Redis数据结构之Bitmap

1.概述

  • 由0和1状态表现的二进制位的bit数组(数组里每个元素只能是0或1)该数组由多个二进制位组成,每个二进制位都对应一个偏移量(我们称之为一个索引)。
  • 基于String数据类型的按位的操作。
  • Bitmap支持的最大位数是2³²位,它可以极大的节约存储空间,使用512M内存就可以存储多达42.9亿的字节信息(2³²= 4294967296)

2.常见操作

2.1 SETBIT

设置0或1到Bitmap

SETBIT key offset value
  • key
  • offset 偏移位,从0开始计算
  • value 只能是0或1

例:

127.0.0.1:6379> setbit k1 1 1(integer) 1127.0.0.1:6379> setbit k1 2 1(integer) 1127.0.0.1:6379> setbit k1 3 1(integer) 1

验证:Bitmap基于String数据类型的

127.0.0.1:6379> type k1string

2.2 GETBIT

获取0或1,不是1就默认0

127.0.0.1:6379> getbit k1 1(integer) 1127.0.0.1:6379> getbit k1 2(integer) 1127.0.0.1:6379> getbit k1 3(integer) 1127.0.0.1:6379> getbit k1 4(integer) 0

2.3 STRLEN

不是字符串长度而是Bitmap占据几个字节,按照8位一组扩容,统计有多少组

127.0.0.1:6379> setbit k3 0 1(integer) 0127.0.0.1:6379> setbit k3 7 1(integer) 0127.0.0.1:6379> strlen k3(integer) 1127.0.0.1:6379> setbit k3 8 1(integer) 0127.0.0.1:6379> strlen k3(integer) 2

2.4 BITCOUNT

一个bitmap中从START到END有多少个1,不加START|END统计全部

BITCOUNT KEY [START|END]

例:

127.0.0.1:6379> bitcount k3(integer) 3

2.5 BITOP

对bitmap进行二进制运算(与AND、或OR、非NOT、异或XOR)保存结果到DESTKEY KEY

BITOP AND|OR|NOT|XOR DESTKEY KEY [KEY ...]

例:

准备数据

127.0.0.1:6379> setbit bin1 0 1(integer) 0127.0.0.1:6379> setbit bin1 1 1(integer) 0127.0.0.1:6379> setbit bin1 2 0(integer) 0127.0.0.1:6379> setbit bin1 3 1(integer) 0127.0.0.1:6379> setbit bin2 0 0(integer) 0127.0.0.1:6379> setbit bin2 1 1(integer) 0127.0.0.1:6379> setbit bin2 2 1(integer) 0127.0.0.1:6379> setbit bin2 3 0

测试:BITOP AND

127.0.0.1:6379> BITOP AND bintemp bin1 bin2(integer) 1

查看写入到bintemp中的结果

127.0.0.1:6379> getbit bintemp 0(integer) 0127.0.0.1:6379> getbit bintemp 1(integer) 1127.0.0.1:6379> getbit bintemp 2(integer) 0127.0.0.1:6379> getbit bintemp 3(integer) 0

3.总结

命令作用时间复杂度
setbit给指定key的值的第offset赋值valO(1)
getbit获取指定key的第offset位O(1)
bitcount返回指定key中[start,end]中为1的数量O(n)
bitop对不同的二进制存储数据进行位运算(AND、OR、NOT、XOR)O(n)

使用场景:

1.统计签到、登录、打卡等,例如:ID为10001的用户2025年5月的第12个工作日未打卡

setbit sign:uid_10001:202505 12 0
  •  

Redis数据结构之GEO

1.概述

移动互联网时代LBS应用越来越多,交友软件中附近的人、外卖软件中附近的美食店铺等,那这种附近各种形形色色的地址位置选择是如何实现的?

地球上的地理位置是使用二维的经纬度表示,经度范围(-180,180],纬度范围(-90,90],只要我们确定一个点的经纬度就可以取得它在地球上的位置。

例如滴滴打车,最直观的操作就是实时记录更新各个车的位置,然后当我们要找车时,在数据库中查找距离我们(x0, y0)附近r公里范围内部的车辆,使用如下SQL即可:

select taxi from position where x0 - r < x < x0 + r and y0 - r < y < y0 + r

但是这样会有查询性能问题,如果并发高数据量大,这种查询会影响数据库性能,而且这个查询到的是一个矩形范围,而不是以点为中心r公里为半径的圆形范围。

为了解决这一问题,Redis在3.2版本之后支持了GEO这一数据结构,GEO主要用于存储地理位置信息,并对存储的信息进行操作,包括:

  • 添加地理位置的坐标
  • 获取地理位置的坐标
  • 计算两个位置之间的距离
  • 根据用户给定的经纬度坐标来获取指定范围内的地理位置集合

2.常用命令

2.1 GEOADD

多个经度(longitude)、纬度(latitude)、位置名称(member)添加到指定的key中

GEOADD key longitude latitude member [longitude latitude member ...]

经纬度可以通过百度地图坐标拾取获得:https://api.map.baidu.com/lbsapi/getpoint/index.html

例如:

127.0.0.1:6379> GEOADD city 116.515802 39.951893 "北京朝阳站" 116.385671 39.871141 "北京南站"(integer) 1

GEO的本质是Set的子类型:

127.0.0.1:6379> type cityzset127.0.0.1:6379> zrange city 0 -11) "\xe5\x8c\x97\xe4\xba\xac\xe5\x8d\x97\xe7\xab\x99"2) "\xe5\x8c\x97\xe4\xba\xac\xe7\xab\x99"3) "\xe5\x8c\x97\xe4\xba\xac\xe6\x9c\x9d\xe9\x98\xb3\xe7\xab\x99"127.0.0.1:6379> 

中文乱码的解决:修改客户端参数

127.0.0.1:6379> quit[root@localhost bin]# ./redis-cli --raw127.0.0.1:6379> auth lzjOK127.0.0.1:6379> zrange city 0 -1北京南站北京站北京朝阳站

2.2 GEOPOS

用于从给定的key里返回所有指定名称(member)的位置(经度和纬度),不存在的返回nil

GEOPOS key member [member ...]

127.0.0.1:6379> GEOPOS city 北京站116.4341434836387634339.90890963654599233127.0.0.1:6379> GEOPOS city  北京朝阳站116.5158006548881530839.95189343796607062

2.3 GEODIST

返回两个给定位置之间的距离

  • m
  • km千米
  • ft英尺
  • mi英里
GEODIST key memberl member2 [m|km|ft|mi]

例:高铁站之间的距离

127.0.0.1:6379> GEODIST city 北京站 北京朝阳站 km8.4477127.0.0.1:6379> GEODIST city 北京南站 北京朝阳站 km14.2804127.0.0.1:6379> GEOADD city 117.978057 40.893571 "承德南站"1127.0.0.1:6379> GEODIST city 承德南站 北京朝阳站 km162.1702

2.4 GEORADIUS

以给定的经纬度为中心,返回与中心的距离不超过给定最大距离的所有位置元素,说白了就是根据半径检索附近的POI(兴趣点)

GEORADIUS key longitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC] [STORE key] [STOREDIST key]
  • key: 存储地理位置的有序集合键名
  • longitude latitude: 中心点的经度和纬度
  • radius: 半径距离
  • m|km|ft|mi: 距离单位(米、千米、英尺、英里)
  • WITHCOORD: 将位置元素的经度和维度也一并返回。
  • WITHDIST: 在返回位置元素的同时,将位置元素与中心之间的距离也一并返回, 距离的单位和用户给定的范围单位保持一致
  • WITHHASH: 以52位有符号整数的形式,返回位置元素经过原始geohash编码的有序集合分值,这个选项主要用于底层应用或者调试,实际中的作用并不大
  • COUNT count: 限制返回结果数量
  • ASC|DESC: 按距离排序(升序/降序)
  • STORE key: 将结果存储为有序集合(存储 geohash)
  • STOREDIST key: 将结果存储为有序集合(存储距离)

例:以朝阳公园(116.486817,39.953201)为圆心,10千米为半径,搜索city中满足条件的地点

127.0.0.1:6379> GEORADIUS city 116.486817 39.953201 10 km WITHCOORD WITHDIST北京站6.6672116.4341434836387634339.90890963654599233北京朝阳站2.4755116.5158006548881530839.95189343796607062

2.5 GEORADIUSBYMEMBER

跟GEORADIUS类似

2.6 GEOHASH

返回一个或多个位置元素的GEOHASH表示,比位数众多的小数更好处理

GEOHASH算法生成的base32编码值,主要分为三步:
1.将三维的地球变为二维的坐标
2.将二维的坐标转换为一维的点块
3.最后将一维的点块转换为二进制再通过base32编码

GEOHASH key member [member ...]

127.0.0.1:6379> GEOHASH city 北京站 北京南站 北京朝阳站wx4g190y0p0wx4fb6m6nx0wx4g73h0wu0
  •  

Redis数据结构之Hash

1.概述

  • Hash是一个String类型的field(字段)和value(值)的映射表,而且value是一个键值对集合,类似Map<String, Map<Object, Object>>,Hash特别适合用于存储对象。
  • 每个Hash可以存储2³²-1个键值对 (40多亿)。

2.常见操作

2.1 H(M)SET/H(M)GET

HSET/HGET,设置和获取hash的键值对

语法:

hset key field value [field value ......] 

例:

127.0.0.1:6379> hset user:01 name liming(integer) 1hset user:01 id 1 name liming age 30

语法:

hget key field [field ......] 

例:

127.0.0.1:6379> hget user:01 name"liming"127.0.0.1:6379> hget user:01 age"30"127.0.0.1:6379> hget user:01 id"1"

HMSET,一次批量设置hash的多个值

127.0.0.1:6379> HMSET user:02 id 02 name lisiOK

从 Redis 4.0.0 开始,HSET 也支持批量设置,HMSET 被视为已弃用,但仍可使用。

HMGET,一次获取某个hash的多个值

127.0.0.1:6379> hmget user:02 id name 1) "02"2) "lisi"

2.2 HGETALL

获取一个hash中的所有键值

127.0.0.1:6379> hgetall user:011) "name"2) "liming"3) "id"4) "1"5) "age"6) "30"

2.3 HDEL

删除hash中的某个键值对

127.0.0.1:6379> hdel user:01 age(integer) 1127.0.0.1:6379> hgetall user:011) "name"2) "liming"3) "id"4) "1"

2.4 HLEN

hash中键值对数量

127.0.0.1:6379> hlen user:01(integer) 2

2.5 HEXISTS

hash中某个键是否存在

127.0.0.1:6379> HEXISTS user:01 age(integer) 0127.0.0.1:6379> HEXISTS user:01 name(integer) 1

2.6 HKEYS/HVALS

获取一个hash中所有的键/值

127.0.0.1:6379> HKEYS user:011) "name"2) "id"
127.0.0.1:6379> HVALS user:011) "liming"2) "1"

2.7 HINCRBY

对hash中某个键的值进行自增

127.0.0.1:6379> hmset user:03 age 13OK127.0.0.1:6379> hincrby user:03 age 1(integer) 14127.0.0.1:6379> hincrby user:03 age 2(integer) 16

2.8 HSETNX

不存在就赋值,如已存在则无效

127.0.0.1:6379> hgetall user:031) "age"2) "16"127.0.0.1:6379> HSETNX user:03 name liming(integer) 1127.0.0.1:6379> HSETNX user:03 age 3(integer) 0

3.总结

KV键值对的结构,适合早期的购物车等场景

  •  

Redis数据结构之HyperLogLog

1.概述

基数统计是一种去重复统计功能的基数估计算法,HyperLogLog是用来做基数统计的数据结构,HyperLogLog的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定且是很小的。

在Redis里面,每个HyperLogLog键只需要花费12KB内存,就可以计算接近2⁶⁴个不同元素的基数,这和计算基数时,元素越多耗费内存就越多的集合形成鲜明对比。

但是,因为HyperLogLog只会根据输入元素来计算基数,而不会储存输入元素本身,所以HyperLogLog不能像集合那样,返回输入的各个元素。

去重类似Java的HashSet,只是不能存储数据

2.常用命令

2.1 添加元素

PFADD key element [element ..]

127.0.0.1:6379> pfadd pf1 1 2 3 4 5 6 5 4 5 6 7 34 2 5 6 4(integer) 1127.0.0.1:6379> pfadd pf2 1 2 3 4 5 6 5 4 43 4 21 65 32 54 (integer) 1

hyperloglog本质上是string

127.0.0.1:6379> type pf1string

2.2 返回基数估算值

PFCOUNT key [key....]

例:去重后只有8个元素

127.0.0.1:6379> PFCOUNT pf1(integer) 8

2.3 合并hyperloglog

合并到新的destkey中

PFMERGE destkey sourcekey [sourcekey...]

例:合并pf1和pf2为pfm

127.0.0.1:6379> PFMERGE pfm pf1 pf2OK127.0.0.1:6379> pfcount pfm(integer) 13

3.总结

使用场景:

1.网站UV统计,文章阅读数UV统计

  •  

Redis数据结构之List

1.概述

  • List是简单的字符串列表,单key多个value,按照插入顺序排序。

  • 支持添加一个元素到列表的头部(左边)或者尾部(右边)

  • 它的底层实际是个双端链表,主要功能有push/pop等,用在栈,队列,消息队列等场景,left/right都可以插入添加,如果键不存在创建新的链表,键已存在,则新增内容,如果值全被移除了,对应的键也就消失了。

    双端链表两端操作的效率很高,通过索引下标的操作性能略有下降

  • 最多可以包含2³²-1个元素 (4294967295, 每个列表超过40亿个元素)。

2.常见操作

2.1 LPUSH/RPUSH/LRANGE

LPUSH将一个或多个值插入头部(左边),RPUSH是将一个或多个值插入尾部(右边)

案例:

127.0.0.1:6379> lpush list1 1 2 3 4 5(integer) 5127.0.0.1:6379> rpush list2 1 2 3 4 5(integer) 5

LRANGE从头部开始遍历,获取元素,下标从0开始,0到-1是全部遍历

案例:

127.0.0.1:6379> LRANGE list1 0 31) "5"2) "4"3) "3"4) "2"127.0.0.1:6379> LRANGE list2 0 -11) "1"2) "2"3) "3"4) "4"5) "5"127.0.0.1:6379> LRANGE list1 0 -11) "5"2) "4"3) "3"4) "2"5) "1"

不存在RRANGE命令

2.2 LPOP/RPOP

LPOP,从左侧弹出(移除)元素,RPOP从右侧弹出元素,被弹出的元素会被返回。

127.0.0.1:6379> lrange list1 0 -11) "5"2) "4"3) "3"4) "2"5) "1"127.0.0.1:6379> lpop list1 "5"127.0.0.1:6379> lpop list1 "4"127.0.0.1:6379> lpop list1 "3"127.0.0.1:6379> lpop list1 "2"127.0.0.1:6379> lpop list1 "1"
127.0.0.1:6379> lrange list2 0 -11) "1"2) "2"3) "3"4) "4"5) "5"127.0.0.1:6379> rpop list2"5"127.0.0.1:6379> rpop list2"4"127.0.0.1:6379> rpop list2"3"127.0.0.1:6379> rpop list2"2"127.0.0.1:6379> rpop list2"1"

2.3 LINDEX

根据下标获取元素

127.0.0.1:6379> rpush list1 1 2 3 4 5(integer) 5127.0.0.1:6379> lindex list1 0"1"127.0.0.1:6379> lindex list1 2"3"

2.4 LLEN

元素个数,list.size();

127.0.0.1:6379> lrange list1 0 -11) "1"2) "2"3) "3"4) "4"5) "5"127.0.0.1:6379> llen list1(integer) 5

2.5 LREM

删除num个值是value的元素

lrem key num value

例:

127.0.0.1:6379> lpush list3 1 2 3 4 5 5 5 5 5 6 7 8(integer) 12127.0.0.1:6379> lrange list3 0 -1 1) "8" 2) "7" 3) "6" 4) "5" 5) "5" 6) "5" 7) "5" 8) "5" 9) "4"10) "3"11) "2"12) "1"127.0.0.1:6379> lrem list3 2 5(integer) 2127.0.0.1:6379> lrange list3 0 -1 1) "8" 2) "7" 3) "6" 4) "5" 5) "5" 6) "5" 7) "4" 8) "3" 9) "2"10) "1"

2.6 LTRIM

截取指定范围的值后再赋给key

LTRIM key start end

例:

127.0.0.1:6379> rpush list1 1 2 3 4 5 6 (integer) 6127.0.0.1:6379> lrange list1 0 -11) "1"2) "2"3) "3"4) "4"5) "5"6) "6"127.0.0.1:6379> LTRIM list1 3 5OK127.0.0.1:6379> lrange list1 0 -11) "4"2) "5"3) "6"

2.7 RPOPLPUSH

移除列表的最后一个元素,并将该元素添加到另一个列表并返回

RPOPLPUSH key source target

例:

127.0.0.1:6379> lrange list1 0 -11) "1"2) "2"3) "3"4) "4"127.0.0.1:6379> lrange list2 0 -11) "5"2) "6"3) "7"4) "8"127.0.0.1:6379> rpoplpush list1 list2"4"127.0.0.1:6379> lrange list1 0 -11) "1"2) "2"3) "3"127.0.0.1:6379> lrange list2 0 -11) "4"2) "5"3) "6"4) "7"5) "8"

2.8 LSET

设置某个下标的值

LSET key index value

例:

127.0.0.1:6379> lrange list2 0 -11) "4"2) "5"3) "6"4) "7"5) "8"127.0.0.1:6379> lset list2  3 abcOK127.0.0.1:6379> lrange list2 0 -11) "4"2) "5"3) "6"4) "abc"5) "8"

2.9 LINSERT

在某个已有值existValue前或后加个新的值newValue

LINSERT key before|after existValue newValue

例:

127.0.0.1:6379> lrange list2 0 -11) "4"2) "5"3) "6"4) "abc"5) "8"127.0.0.1:6379> LINSERT list2 before abc def(integer) 6127.0.0.1:6379> lrange list2 0 -11) "4"2) "5"3) "6"4) "def"5) "abc"6) "8"
  •  

Redis数据结构之Set

1.概述

  • Set是String类型的无序集合,集合成员是唯一的,这就意味着集合中不能出现重复的数据,集合对象的编码可以是intset或者hashtable。
  • Set是通过哈希表实现的,所以添加,删除,查找的复杂度都是O(1)。
  • Set中最大的成员数为2³²-1 (4294967295,每个集合可存储40多亿个成员)。

2.常见操作

2.1 SADD

添加元素到set中,不能重复添加

127.0.0.1:6379> sadd set1 a b c d e f g(integer) 7127.0.0.1:6379> sadd set2 a a a b b b c c c(integer) 3

2.2 SMEMBERS

查看set中所有元素

127.0.0.1:6379> SMEMBERS set11) "a"2) "g"3) "c"4) "f"5) "d"6) "e"7) "b"127.0.0.1:6379> SMEMBERS set21) "a"2) "c"3) "b"

2.3 SISMEMBER

判断set中是否有某个元素

127.0.0.1:6379> SISMEMBER set2 a(integer) 1127.0.0.1:6379> SISMEMBER set2 d(integer) 0

2.4 SREM

从set 中移除元素

127.0.0.1:6379> SMEMBERS set11) "a"2) "g"3) "c"4) "f"5) "d"6) "e"7) "b"127.0.0.1:6379> srem set1 g(integer) 1127.0.0.1:6379> SMEMBERS set11) "a"2) "c"3) "f"4) "d"5) "e"6) "b"

2.5 SCARD

set中元素个数

127.0.0.1:6379> SCARD set1(integer) 6

2.6 SRANDMEMBER

从set中随机选择n个元素(不会删除)

SRANDMEMBER key n

例:

127.0.0.1:6379> SMEMBERS set11) "a"2) "c"3) "f"4) "d"5) "e"6) "b"127.0.0.1:6379> SRANDMEMBER set1 21) "f"2) "e"127.0.0.1:6379> SRANDMEMBER set1 21) "a"2) "e"

2.7 SPOP

从set中随机弹出(删除)n个元素

SPOP key n

例:

127.0.0.1:6379> SMEMBERS set11) "a"2) "c"3) "f"4) "d"5) "e"6) "b"127.0.0.1:6379> SPOP set1 21) "c"2) "a"127.0.0.1:6379> SPOP set1 21) "b"2) "f"127.0.0.1:6379> SPOP set1 21) "d"2) "e"127.0.0.1:6379> SPOP set1 2(empty list or set)

2.8 SMOVE

将set1中的v移动到set2中去

smove key key value

例:

127.0.0.1:6379> sadd set1 1 2 3(integer) 3127.0.0.1:6379> sadd set2 a b c(integer) 3127.0.0.1:6379> smove set1 set2 2(integer) 1127.0.0.1:6379> SMEMBERS  set11) "1"2) "3"127.0.0.1:6379> SMEMBERS  set21) "a"2) "2"3) "c"4) "b"

2.9 集合运算

127.0.0.1:6379> smembers set31) "1"2) "c"3) "b"4) "a"5) "2"6) "3"127.0.0.1:6379> smembers set41) "c"2) "b"3) "2"4) "5"5) "4"6) "d"7) "3"

1.差集运算

set3-set4:set3有set4没有

127.0.0.1:6379> sdiff  1) "a"2) "1"

2.并集运算

语法

sunion key key [key ......]  

例:

127.0.0.1:6379> sunion set3 set41) "c"2) "4"3) "5"4) "1"5) "3"6) "b"7) "a"8) "2"9) "d"

3.交集运算

sinter,同时属于几个集合的公共部分

sinter key key [key ...]  

例:

127.0.0.1:6379> sinter set3 set41) "c"2) "b"3) "2"4) "3"

SINTERCARD,redis7新命令,不返回结果集,只返回结果的基数,返回由所有给定集合的交集产生的集合的基数

SINTERCARD numkeys key [key ...] [LIMIT limit]

例:2个集合,set3和set4,交集中共有4个元素,可以限制返回的个数,但是不能超过元素的总个数

127.0.0.1:6379> SINTERCARD  2 set3 set4(integer) 4127.0.0.1:6379> SINTERCARD  2 set3 set4 limit 1(integer) 1127.0.0.1:6379> SINTERCARD  2 set3 set4 limit 2(integer) 2127.0.0.1:6379> SINTERCARD  2 set3 set4 limit 3(integer) 3127.0.0.1:6379> SINTERCARD  2 set3 set4 limit 4(integer) 4127.0.0.1:6379> SINTERCARD  2 set3 set4 limit 5(integer) 4

3.总结

Set集合的使用场景很多,例如:

  1. 可能认识的人或共同感兴趣的话题,商品
    sdiff a b
    sdiff b a

  2. 年会抽奖活动
    sadd 活动key 用户ID:参与抽奖
    scard 活动key:统计参加总人数
    SRANDMEMBER 活动key n:抽取n个幸运的人,这几个人还能继续抽奖
    spop 活动key n:抽取n个幸运的人,这几个人不能继续抽奖

  •  

Redis数据结构之Stream

1.概述

Redis Stream是Redis 5.0版本新增加的数据结构。

Redis Stream主要用于消息队列(MQ,Message Queue),Redis本身是有一个Redis发布订阅(pub/sub)来实现消息队列的功能,但它有个缺点就是消息无法持久化,如果出现网络断开、Redis宕机等,消息就会被丢弃,而且没有ACK机制来保证数据的可靠性,假设一个消费者都没有,那消息就直接被丢弃了,简单来说发布订阅(pub/sub)可以分发消息,但无法记录历史消息。

而Redis Stream提供了消息的持久化和主备复制功能,支持自动生成全局唯一ID、支持ack确认消息的模式、支持消费组模式等,让消息队列更加的稳定和可靠,可以让任何客户端访问任何时刻的数据,并且能记住每一个客户端的访问位置,还能保证消息不丢失。

Redis Stream是一个链表,会将所有加入的消息都串起来,每个消息都有一个唯一的ID和对应的内容

Redis Stream组成部分说明:

  • Message Content

    消息

  • Consumer group

    消费组,通过XGROUP CREATE命令创建,同一个消费组可以有多个消费者

  • Last_delivered_id

    游标,每个消费组会有个游标last_delivered_id,任意一个消费者读取了消息都会使游标last_delivered_id往前移动

  • Consumer

    消费者,包含在消费组当中

  • Pending_ids

    消费者会布一个状态变量,用于记录被当前消费者已读取但未ack的消息id,如果客户端没有ack,这个变量里面的消息ID会越来越多,一旦某个消息被ack,它就开始减少。这个pending_ids变量在Redis官方被称之为PEL(Pending Entries List),记录了当前已经被客户端读取的消息,但是还没有ack(Acknowledge character:确认字符),它用来确保客户端至少消费了消息一次,而不会在网络传输的中途丢失了没处理

2.队列(生产)相关命令

2.1 XADD

添加消息到队列末尾,如果队列不存在,则会先创建队列

  • key
  • *|id 消息的ID,格式必须是时间戳-序列号这样的方式,下一条的ID必须比上一条的要大,写成*号,系统将自动生成。ID比较大小时,先比较时间戳大小,若相同再比较序列号
  • field value 键值对
XADD <key> <*|id> <field value> [<field value ...>]

127.0.0.1:6379> XADD mystream * name lzj age 28"1758432825026-0"127.0.0.1:6379> XADD mystream * name xxn age 24"1758432832463-0"

返回的1748835683065-01748835967616-0就是消息的ID,-前的数字为生成消息时的毫秒级时间戳,-后面的数字代表同一毫秒内产生的第几个序列号

在相同的毫秒下序列号从0开始递增,序列号是64位长度,理论上在同一毫秒内生成的数据量无法到达这个级别,因此不用担心序列号会不够用。毫秒数取的是Redis节点服务器的本地时间,如果存在当前的毫秒时间戳比以前已经存在的数据的时间戳小的话(本地时间钟后跳),那么系统将会采用以前相同的毫秒创建新的ID,也即redis在增加信息条目时会检査当前id与上一条目的id,自动纠正错误的情况,一定要保证后面的id比前面大,一个流中信息条目的ID必须是单调增的,这是stream的基础。

Stream的数据类型就是Stream

127.0.0.1:6379> type mystreamstream

2.2 XRANGE

获取消息列表(可以指定范围),忽略删除的消息

  • key 对应的stream
  • start 开始值,用-表示最小
  • end 结束值,用+表示最小
  • count 限制条数
XRANGE <key> <start> <end> [Count <count>]

127.0.0.1:6379> xrange mystream - + 1) 1) "1758432825026-0"   2) 1) "name"      2) "lzj"      3) "age"      4) "28"2) 1) "1758432832463-0"   2) 1) "name"      2) "xxn"      3) "age"      4) "24"

2.3 XREVRANGE

和XRANGE相比,区别在于反向获取,ID从大到小

127.0.0.1:6379> xrevrange mystream + - 1) 1) "1758432832463-0"   2) 1) "name"      2) "xxn"      3) "age"      4) "24"2) 1) "1758432825026-0"   2) 1) "name"      2) "lzj"      3) "age"      4) "28"

2.4 XDEL

删除消息,按照ID删除

XDEL <key> <id> [<id ...>]

XDEL mystream 1748835967616-0

2.5 XLEN

获取Stream中的消息长度

127.0.0.1:6379> XLEN mystream2

2.6 XTRIM

限制Stream的长度,如果已经超长会进行截取

  • MAXLEN 允许的最大长度,对流进行修剪限制长度
  • MINID 允许的最小id,从某个id值开始比该id值小的将会被抛弃
XTRIM <stream> MAXLEN|MINID <n>

127.0.0.1:6379> XTRIM mystream MAXLEN 24
127.0.0.1:6379> XTRIM mystream MINID 1748841640027-01

2.7 XREAD

获取消息(阻塞/非阻塞),返回大于指定ID的消息

  • COUNT 最多读取多少条
  • BLOCK 是否以阻塞的方式读取消息,默认不阻塞,如果miliseconds设置为0,表示永远阻塞
  • STREAMS 队列
  • ID 指定的ID,用$代表队列内现存最大的ID的后一个ID,用0-0(或0; 00)代表队列中最小的ID
XREAD [COUNT <count>] [BLOCK <milliseconds>] STREAMS <key> [<key ...>] ID [<id ...>]

例:用$代表队列内现存最大的ID的后一个ID,因为该ID并不存在所以会返回空

127.0.0.1:6379> Xrange mystream - +1) 1) "1758432825026-0"   2) 1) "name"      2) "lzj"      3) "age"      4) "28"2) 1) "1758432832463-0"   2) 1) "name"      2) "xxn"      3) "age"      4) "24"127.0.0.1:6379> XREAD  STREAMS mystream  $(nil)

例:用0-0代表队列中最小的ID,当指定为0-0的同时不指定count时,会返回队列中所有的元素

127.0.0.1:6379> Xrange mystream - +1) 1) "1758432825026-0"   2) 1) "name"      2) "lzj"      3) "age"      4) "28"2) 1) "1758432832463-0"   2) 1) "name"      2) "xxn"      3) "age"      4) "24"127.0.0.1:6379> XREAD  STREAMS mystream  0-01) 1) "mystream"   2) 1) 1) "1758432825026-0"         2) 1) "name"            2) "lzj"            3) "age"            4) "28"      2) 1) "1758432832463-0"         2) 1) "name"            2) "xxn"            3) "age"            4) "24"

例:阻塞:监听mystream中比最新的一条还靠后的一条,读取不到就会阻塞,一直监听

127.0.0.1:6379> XREAD COUNT 1 BLOCK 0  STREAMS mystream $

打开另一个命令窗口,生产消息

127.0.0.1:6379> xadd mystream * k1 v1"1758434052255-0"

XREAD停止阻塞,打印出新生产的一条消息和等待时间

127.0.0.1:6379> XREAD COUNT 1 BLOCK 0  STREAMS mystream $1) 1) "mystream"   2) 1) 1) "1758434052255-0"         2) 1) "k1"            2) "v1"(183.38s)

3.消费(组)相关命令

3.1 XGROUP CREATE

创建消费组

  • stresm 队列
  • group 消费组
  • id 创建消费组时必须指定ID,0代表从头消费,$代表从队尾消费(只消费最新消息)
XGROUP CREATE <stresm> <group> <id>

例:

127.0.0.1:6379> xgroup create mystream group1 $OK127.0.0.1:6379> xgroup create mystream group2 0OK

3.2 XREADGROUP GROUP

允许多个消费者作为一个组来合作消费同一个stream中的消息,同一个stream中的消息一旦被消费组里面的一个消费者读取了,同组的其他消费者就无法再次读取

  • group 消费组
  • consumer 消费者
  • stream 队列
  • id 指定从哪个ID开始读取,特殊写法:>代表获取组内未分发给其他消费者的新消息(未分发必然未ACK),0代表获取已分发但未被消费者ACK的消息
  • count 读取的数量,可以用于每个消费者读取一部分消息,实现消费的负载均衡
XREADGROUP GROUP <group> <consumer> [COUNT <count>] STREAMS <stream, ...> <id>

例:同组消费者不能重复消费消息

先建两个消费组

127.0.0.1:6379> xgroup create mystream groupA 0OK127.0.0.1:6379> xgroup create mystream groupB 0OK

groupA里面的新建消费者consumer1进行消费

127.0.0.1:6379> XREADGROUP GROUP groupA consumer1 STREAMS mystream >1) 1) "mystream"   2) 1) 1) "1758432825026-0"         2) 1) "name"            2) "lzj"            3) "age"            4) "28"      2) 1) "1758432832463-0"         2) 1) "name"            2) "xxn"            3) "age"            4) "24"      3) 1) "1758434052255-0"         2) 1) "k1"            2) "v1"

groupA中再去新建消费者consumer2进行消费,无法再消费消息

127.0.0.1:6379> XREADGROUP GROUP groupA consumer2 STREAMS mystream >(nil)

但是,groupB中新建一个消费者取消费,可以读取到消息

127.0.0.1:6379> XREADGROUP GROUP groupB consumer1 STREAMS mystream >1) 1) "mystream"   2) 1) 1) "1758432825026-0"         2) 1) "name"            2) "lzj"            3) "age"            4) "28"      2) 1) "1758432832463-0"         2) 1) "name"            2) "xxn"            3) "age"            4) "24"      3) 1) "1758434052255-0"         2) 1) "k1"            2) "v1"

例:负载均衡的消费,新建消费组groupC,三个消费者每个消费一条消息

127.0.0.1:6379> xgroup create mystream groupC 0OK127.0.0.1:6379> XREADGROUP GROUP groupC consumer1 COUNT 1 STREAMS mystream >1) 1) "mystream"   2) 1) 1) "1758432825026-0"         2) 1) "name"            2) "lzj"            3) "age"            4) "28"127.0.0.1:6379> XREADGROUP GROUP groupC consumer2 COUNT 1 STREAMS mystream >1) 1) "mystream"   2) 1) 1) "1758432832463-0"         2) 1) "name"            2) "xxn"            3) "age"            4) "24"127.0.0.1:6379> XREADGROUP GROUP groupC consumer3 COUNT 1 STREAMS mystream >1) 1) "mystream"   2) 1) 1) "1758434052255-0"         2) 1) "k1"            2) "v1"

3.3 消息的ACK机制

基于Stream的消息,怎样保证消费者发生故障或宕机以后,仍然能读取未处理完的消息?Stream采用的是一个内部队列pending_list,记录消费组中消费者的读取记录,直到消费者使用xack命令来通知stream消息已经处理完成,这种消费确认机制增强了消息的可靠性。

刚刚的消费操作仅仅是对消息的读取,实际上并没有ACK“签收”

例:通过命令XPENDING查询groupC中已读取,但未确认的消息

127.0.0.1:6379> XPENDING mystream groupC1) (integer) 3 #总数2) "1758432825026-0" #起始ID3) "1758434052255-0" #结束ID4) 1) 1) "consumer1" #每个消费组消费的消息数量      2) "1"   2) 1) "consumer2"      2) "1"   3) 1) "consumer3"      2) "1"

例:通过命令XPENDING查询groupB中consumer1已读取,但未确认的消息,从小到大查询10个

127.0.0.1:6379> XPENDING mystream groupB - + 10  consumer11) 1) "1758432825026-0"   2) "consumer1"   3) (integer) 2848434   4) (integer) 12) 1) "1758432832463-0"   2) "consumer1"   3) (integer) 2848434   4) (integer) 13) 1) "1758434052255-0"   2) "consumer1"   3) (integer) 2848434   4) (integer) 1

例:使用XACK向消息队列确认groupB组的消息1758432825026-0已经处理完成

127.0.0.1:6379> XACK mystream groupB 1758432825026-0(integer) 1
127.0.0.1:6379> XPENDING mystream groupB - + 10  consumer11) 1) "1758432832463-0"   2) "consumer1"   3) (integer) 3301753   4) (integer) 12) 1) "1758434052255-0"   2) "consumer1"   3) (integer) 3301753   4) (integer) 1

4.XINFO

XINFO命令用于打印出一些stream结构相关的信息

例:XINFO stream打印出mystream队列的详细信息

127.0.0.1:6379> XINFO stream  mystream 1) "length" 2) (integer) 3 3) "radix-tree-keys" 4) (integer) 1 5) "radix-tree-nodes" 6) (integer) 2 7) "last-generated-id" 8) "1758434052255-0" 9) "max-deleted-entry-id"10) "0-0"11) "entries-added"12) (integer) 313) "recorded-first-entry-id"14) "1758432825026-0"15) "groups"16) (integer) 517) "first-entry"18) 1) "1758432825026-0"    2) 1) "name"       2) "lzj"       3) "age"       4) "28"19) "last-entry"20) 1) "1758434052255-0"    2) 1) "k1"       2) "v1"

例:XINFO groups打印出mystream队列队列上存在的消费组信息

127.0.0.1:6379> XINFO  groups mystream1)  1) "name"    2) "group1"    3) "consumers"    4) (integer) 0    5) "pending"    6) (integer) 0    7) "last-delivered-id"    8) "1758434052255-0"    9) "entries-read"   10) (nil)   11) "lag"   12) (integer) 02)  1) "name"    2) "group2"    3) "consumers"    4) (integer) 0    5) "pending"    6) (integer) 0    7) "last-delivered-id"    8) "0-0"    9) "entries-read"   10) (nil)   11) "lag"   12) (integer) 33)  1) "name"    2) "groupA"    3) "consumers"    4) (integer) 2    5) "pending"    6) (integer) 3    7) "last-delivered-id"    8) "1758434052255-0"    9) "entries-read"   10) (integer) 3   11) "lag"   12) (integer) 04)  1) "name"    2) "groupB"    3) "consumers"    4) (integer) 1    5) "pending"    6) (integer) 2    7) "last-delivered-id"    8) "1758434052255-0"    9) "entries-read"   10) (integer) 3   11) "lag"   12) (integer) 05)  1) "name"    2) "groupC"    3) "consumers"    4) (integer) 3    5) "pending"    6) (integer) 3    7) "last-delivered-id"    8) "1758434052255-0"    9) "entries-read"   10) (integer) 3   11) "lag"   12) (integer) 0
  •  

Redis数据结构之String

1.概述

  • String是最常用的数据类型,一个key对应一个value。
  • String是二进制安全的,可以包含任何数据(例如图片和序列化对象),支持序列化。
  • 单个Value最大512MB。

2.常见操作

2.1 SET/GET

语法:

[ ]是可选的参数

SET key value [NX | XX] [GET] [EX seconds | PX milliseconds | EXAT unix-time-seconds | PXAT unix-time-milliseconds | KEEPTTL]

SET命令有EXPXNXXX以及KEEPTTL五个可选参数,其中KEEPTTL为6.0版本添加的可选参数,其它为2.6.12版本添加的可选参数。

  • EX seconds 以秒为单位设置过期时间
  • PX milliseconds 以毫秒为单位设置过期时间
  • EXAT timestamp 设置以秒为单位的UNIX时间戳所对应的时间为过期时间
  • PXAT milliseconds-timestamp 设置以毫秒为单位的UNIX时间戳所对应的时间为过期时间
  • NX 键不存在的时候设置键值
  • XX 键存在的时候设置键值
  • KEEPTTL 保留设置前指定键的生存时间
  • GET 返回指定键原本的值,若键不存在时返回nil

SET命令使用EXPXNX参数,其效果等同于SETEXPSETEXSETNX命令。根据官方文档的描述,未来版本中SETEXPSETEXSETNX命令可能会被淘汰。

EXNX可用于分布式锁。

案例:最常用的set/get

127.0.0.1:6379> set k1 v1OK127.0.0.1:6379> get k1"v1"

案例:NX,键不存在才能创建,否则不能创建

127.0.0.1:6379> set k1 v1 nxOK127.0.0.1:6379> set k1 v1 nx(nil)

案例:XX,已存在的才创建,否则不能创建

127.0.0.1:6379> set k1 v1 OK127.0.0.1:6379> set k1 v1 xxOK127.0.0.1:6379> get k2(nil)127.0.0.1:6379> set k2 v2 xx(nil)

案例:GET,设置新的值前先把旧的值返回

127.0.0.1:6379> set k1 v1OK127.0.0.1:6379> set k1 v2 get"v1"

案例:EX,10秒过期

127.0.0.1:6379> set k1 v1 ex 10OK127.0.0.1:6379> ttl k1(integer) 8127.0.0.1:6379> ttl k1(integer) 6127.0.0.1:6379> ttl k1(integer) 4

set ex是原子操作,和先set key value然后expire key是不同的,后者不是原子的

案例:PX,9000毫秒过期

127.0.0.1:6379> set k1 v1 px 9000OK127.0.0.1:6379> ttl k1(integer) 7127.0.0.1:6379> ttl k1(integer) 5127.0.0.1:6379> ttl k1(integer) 4

案例:KEEPTTL

同一个key如果设置了新的值,又没有追加过期时间,redis会令其立即过期

127.0.0.1:6379> set k1 v1 ex 40OK127.0.0.1:6379> ttl k1(integer) 37127.0.0.1:6379> set k1 v2OK127.0.0.1:6379> ttl k1(integer) -1

如果需要续接过期时间,就需要用到参数KEEPTTL,设置新值后,过期时间会被续接下来

127.0.0.1:6379> set k1 v1 ex 50  OK127.0.0.1:6379> ttl k1(integer) 46127.0.0.1:6379> set k1 v1 keepttlOK127.0.0.1:6379> ttl k1(integer) 33

2.2 MSET/MGET/MSETNX

案例:MSET同时设置和获取多个值

127.0.0.1:6379> mset k1 v1 k2 v2 k3 v3OK127.0.0.1:6379> mget k1 k2 k31) "v1"2) "v2"3) "v3"

案例 MSETNX,同时设置多个key的值,且key不存在才设置,只会同时成功或同时失败

失败,因为k1已经存在,k1没有成功修改,k2也根本存不进去

127.0.0.1:6379> flushdbOK127.0.0.1:6379> set k1 v1OK127.0.0.1:6379> msetnx k1 a1 k2 v2(integer) 0127.0.0.1:6379> get k1"v1"127.0.0.1:6379> get k2(nil)

成功,k1,k2都不存在,全部添加成功

127.0.0.1:6379> flushdbOK127.0.0.1:6379> msetnx k1 v1 k2 v2(integer) 1127.0.0.1:6379> mget k1 k21) "v1"2) "v2"

2.3 GETRANGE/SETRANGE

GETRANGE,类似Java中的substring(),字符串截取, 0到-1代表不截取

案例:

127.0.0.1:6379> set k1 abcdefgOK127.0.0.1:6379> getrange k1 0 -1"abcdefg"127.0.0.1:6379> getrange k1 1 4"bcde"

SETRANGE,从第几个字符开始设置新的内容

案例:

127.0.0.1:6379> set k1 abcdefgOK127.0.0.1:6379> setrange k1 1 xxyy(integer) 7127.0.0.1:6379> get k1"axxyyfg"

2.4 INCR(BY)/DECR(BY)

数值的加减,值一定要是数字才能进行这个操作

案例:INCR,每次执行加1

127.0.0.1:6379> set k1 100OK127.0.0.1:6379> get k1"100"127.0.0.1:6379> incr k1(integer) 101127.0.0.1:6379> incr k1(integer) 102127.0.0.1:6379> incr k1(integer) 103127.0.0.1:6379> incr k1(integer) 104

案例:INCRBY,修改步长为5

127.0.0.1:6379> set k1 0OK127.0.0.1:6379> incrby k1 5(integer) 5127.0.0.1:6379> incrby k1 5(integer) 10127.0.0.1:6379> incrby k1 5

案例:DECR,递减1,DECRBY同理

127.0.0.1:6379> set k1 100OK127.0.0.1:6379> decr k1(integer) 99127.0.0.1:6379> decr k1(integer) 98127.0.0.1:6379> decr k1(integer) 97
127.0.0.1:6379> set k1 100OK127.0.0.1:6379> decrby k1 5(integer) 95127.0.0.1:6379> decrby k1 5(integer) 90127.0.0.1:6379> decrby k1 5(integer) 85

2.5 STRLEN

字符串长度

语法

strlen key

案例

127.0.0.1:6379> set k1 aaaOK127.0.0.1:6379> strlen k1(integer) 3

2.6 APPEND

字符串追加

语法

APPEND key value

案例

127.0.0.1:6379> set k1 aaaOK127.0.0.1:6379> append k1 bbb(integer) 6127.0.0.1:6379> get k1"aaabbb"

2.7 GETSET

getset,顾名思义,先取值在设置新的值进去,和set key value get命令相同

127.0.0.1:6379> set k1 v1OK127.0.0.1:6379> getset k1 v2"v1"127.0.0.1:6379> get k1"v2"

3.小结

字符串是一个最基本的数据结构,可用于分布式锁,点赞数量统计等场景。

  •  

Redis数据结构之ZSet

1.概述

  • ZSet和Set一样也是String类型元素的集合,且不允许重复的成员,不同的是ZSet每个元素都会关联一个double类型的分数,Redis正是通过分数来为集合中的成员进行从小到大的排序。
  • ZSet的成员是唯一的,但分数(score)却可以重复。
  • ZSet集合是通过哈希表实现的,所以添加,删除,査找的复杂度都是O(1)。
  • ZSet集合中最大的成员数为2³²-1。

2.常见操作

2.1 ZADD

向有序集合中添加元素和元素的分数

ZADD key score member [score member ...]

例:

127.0.0.1:6379> zadd zset1 10 v1 20 v2 30 v3 40 v4(integer) 4

2.2 ZRANGE

遍历,0到-1代表遍历所有,WITHSCORES结果带着分数

ZRANGE key start stop [WITHSCORES]

例:遍历

127.0.0.1:6379> zrange zset1 0 -11) "v1"2) "v2"3) "v3"4) "v4"

例:遍历,结果带着分数

127.0.0.1:6379> zrange zset1 0 -1 withscores1) "v1"2) "10"3) "v2"4) "20"5) "v3"6) "30"7) "v4"8) "40"

2.3 ZREVRANGE

根据分数反转

127.0.0.1:6379> ZREVRANGE zset1 0 -11) "v4"2) "v3"3) "v2"4) "v1"127.0.0.1:6379> ZREVRANGE zset1 0 -1 withscores1) "v4"2) "40"3) "v3"4) "30"5) "v2"6) "20"7) "v1"8) "10"

2.4 ZRANGEBYSCORE

获取指定分数范围的元素

ZRANGEBYSCORE key [(]min max [WITHSCORES] [LIMIT offset count]

(:不包含
min:分数from
max:分数to
offset:开始下标
count:数量

例:获取分数区间[20, 30]的元素

127.0.0.1:6379> ZRANGE zset1 0 -1 withscores1) "v1"2) "10"3) "v2"4) "20"5) "v3"6) "30"7) "v4"8) "40"127.0.0.1:6379> ZRANGEBYSCORE zset1 20 301) "v2"2) "v3"127.0.0.1:6379> ZRANGEBYSCORE zset1 20 30 withscores1) "v2"2) "20"3) "v3"4) "30"

例:获取分数区间(20, 40]的元素

127.0.0.1:6379> ZRANGE zset1 0 -1 withscores1) "v1"2) "10"3) "v2"4) "20"5) "v3"6) "30"7) "v4"8) "40"127.0.0.1:6379> ZRANGEBYSCORE zset1 (20 401) "v3"2) "v4"

例:limit限制返回的数量

127.0.0.1:6379> ZRANGE zset1 0 -1 withscores1) "v1"2) "10"3) "v2"4) "20"5) "v3"6) "30"7) "v4"8) "40"127.0.0.1:6379> ZRANGEBYSCORE zset1 10 40 limit 1 21) "v2"2) "v3"

2.5 ZSCORE

获取元素分数

127.0.0.1:6379> zscore zset1 v3"30"

2.6 ZCARD

元素个数

127.0.0.1:6379> zcard zset1 (integer) 4

2.6 ZREM

删除某个元素

127.0.0.1:6379> zrem zset1 v3 (integer) 1

2.7 ZINCRBY

为元素member增加分数increment

ZINCRBY key increment member

例:对元素v1加3分

127.0.0.1:6379> zrange zset1 0 -1 withscores1) "v1"2) "10"3) "v2"4) "20"5) "v4"6) "40"127.0.0.1:6379> zincrby zset1 3 v1"13"127.0.0.1:6379> zrange zset1 0 -1 withscores1) "v1"2) "13"3) "v2"4) "20"5) "v4"6) "40"

2.8 ZCOUNT

获得指定分数范围内的元素个数

min: 最小分数
max: 最大分数

ZCOUNT key min max

例:

127.0.0.1:6379> zrange zset1 0 -1 WITHSCORES 1) "v1" 2) "10" 3) "v2" 4) "20" 5) "v3" 6) "30" 7) "v4" 8) "40" 9) "v5"10) "50"11) "v6"12) "60"13) "v7"14) "70"127.0.0.1:6379> zcount zset1  30 50(integer) 3

2.9 ZMPOP

7.0新特性,在指定的numkeys个集合中,弹出分数最大(MAX)或最小(MIN)的count个元素(分数和值成对),可以实现在一个或多个集合中,取出最小或最大的几个元素

ZMPOP numkeys key [key ...] <MIN | MAX> [COUNT count]

例:在1个zset1集合中,弹出最小的1个元素

127.0.0.1:6379> zrange zset1 0 -1 WITHSCORES 1) "v1" 2) "10" 3) "v2" 4) "20" 5) "v3" 6) "30" 7) "v4" 8) "40" 9) "v5"10) "50"11) "v6"12) "60"13) "v7"14) "70"127.0.0.1:6379> ZMPOP 1 zset1 min count 11) "zset1"2) 1) 1) "v1"      2) "10"127.0.0.1:6379> 

2.10 ZRANK

正序下标,集合的某个元素,正序处于集合第几个

127.0.0.1:6379> zrange zset1 0 -11) "v2"2) "v3"3) "v4"4) "v5"5) "v6"6) "v7"127.0.0.1:6379> zrank zset1 v2(integer) 0127.0.0.1:6379> zrank zset1 v3(integer) 1127.0.0.1:6379> zrank zset1 v4(integer) 2127.0.0.1:6379> zrank zset1 v5(integer) 3

2.11 ZREVRANK

倒序下标,集合的某个元素,倒序处于集合第几个

127.0.0.1:6379> zrange zset1 0 -11) "v2"2) "v3"3) "v4"4) "v5"5) "v6"6) "v7"127.0.0.1:6379> zrevrank zset1 v2(integer) 5127.0.0.1:6379> zrevrank zset1 v3(integer) 4127.0.0.1:6379> zrevrank zset1 v4(integer) 3127.0.0.1:6379> zrevrank zset1 v5(integer) 2

3.总结

排序集合大量应用于项目,例如实时展示热销商品统计,打赏点赞数量排行榜统计。将销量和点赞打赏数作为分数绑定在值上面即可。

  •  

Redis安装

此处使用64位的Rocky Linux release 9.5环境编译安装Redis-7.2.6,Redis要发挥出最佳性能需要安装运行在Linux系统

1.下载

从官方GitHub地址 https://github.com/redis 下载7.2.6版本源码到服务器

cd /optwget  https://github.com/redis/redis/archive/refs/tags/7.2.6.tar.gz

2.编译安装

解压下载的tar包后,切换到解压后的目录redis-7.2.6里面,src就是redis的源代码,redis.conf是redis配置文件,sentinel.conf和哨兵模式配置有关,Makefile用于编译安装redis

[root@localhost opt]# cd redis-7.2.6/[root@localhost redis-7.2.6]# lltotal 252-rw-rw-r--.  1 root root  22388 Oct  3 03:13 00-RELEASENOTES-rw-rw-r--.  1 root root     51 Oct  3 03:13 BUGS-rw-rw-r--.  1 root root   5027 Oct  3 03:13 CODE_OF_CONDUCT.md-rw-rw-r--.  1 root root   2634 Oct  3 03:13 CONTRIBUTING.md-rw-rw-r--.  1 root root   1487 Oct  3 03:13 COPYING-rw-rw-r--.  1 root root     11 Oct  3 03:13 INSTALL-rw-rw-r--.  1 root root   6888 Oct  3 03:13 MANIFESTO-rw-rw-r--.  1 root root    151 Oct  3 03:13 Makefile-rw-rw-r--.  1 root root  22607 Oct  3 03:13 README.md-rw-rw-r--.  1 root root   1695 Oct  3 03:13 SECURITY.md-rw-rw-r--.  1 root root   3628 Oct  3 03:13 TLS.mddrwxrwxr-x.  8 root root    133 Oct  3 03:13 deps-rw-rw-r--.  1 root root 107512 Oct  3 03:13 redis.conf-rwxrwxr-x.  1 root root    279 Oct  3 03:13 runtest-rwxrwxr-x.  1 root root    283 Oct  3 03:13 runtest-cluster-rwxrwxr-x.  1 root root   1772 Oct  3 03:13 runtest-moduleapi-rwxrwxr-x.  1 root root    285 Oct  3 03:13 runtest-sentinel-rw-rw-r--.  1 root root  14700 Oct  3 03:13 sentinel.confdrwxrwxr-x.  4 root root   8192 Oct  3 03:13 srcdrwxrwxr-x. 11 root root   4096 Oct  3 03:13 testsdrwxrwxr-x.  9 root root   4096 Oct  3 03:13 utils

编译安装前,要先安装必要的软件包

yum install -y make gcc

要保证gcc版本 >= 4.8.5

[root@localhost local]# gcc -vUsing built-in specs.COLLECT_GCC=gccCOLLECT_LTO_WRAPPER=/usr/libexec/gcc/x86_64-redhat-linux/11/lto-wrapperOFFLOAD_TARGET_NAMES=nvptx-noneOFFLOAD_TARGET_DEFAULT=1Target: x86_64-redhat-linuxConfigured with: ../configure --enable-bootstrap --enable-host-pie --enable-host-bind-now --enable-languages=c,c++,fortran,lto --prefix=/usr --mandir=/usr/share/man --infodir=/usr/share/info --with-bugurl=https://bugs.rockylinux.org/ --enable-shared --enable-threads=posix --enable-checking=release --with-system-zlib --enable-__cxa_atexit --disable-libunwind-exceptions --enable-gnu-unique-object --enable-linker-build-id --with-gcc-major-version-only --enable-plugin --enable-initfini-array --without-isl --enable-multilib --with-linker-hash-style=gnu --enable-offload-targets=nvptx-none --without-cuda-driver --enable-gnu-indirect-function --enable-cet --with-tune=generic --with-arch_64=x86-64-v2 --with-arch_32=x86-64 --build=x86_64-redhat-linux --with-build-config=bootstrap-lto --enable-link-serialization=1Thread model: posixSupported LTO compression algorithms: zlib zstdgcc version 11.5.0 20240719 (Red Hat 11.5.0-2) (GCC) 

在解压后的redis-7.2.6目录下执行以下命令,编译安装,将redis安装到/usr/local/redis/bin这个目录下

make PREFIX=/usr/local/redis install

编译完成,切换到/usr/local/redis/bin目录,就会看到编译好的redis二进制文件了,里面包含几个命令

  • redis-benchmark 性能测试工具
  • redis-check-aof 修复有问题的AOF文件
  • redis-check-dump 修复有问题的dump.rdb文件
  • redis-cli 客户端,操作入口
  • redis-sentinel redis集群使用
  • redis-server redis服务器启动命今
[root@localhost redis-7.2.6]# cd /usr/local/redis/bin[root@localhost bin]# lltotal 28496-rwxr-xr-x. 1 root root  6446056 Feb  4 13:14 redis-benchmarklrwxrwxrwx. 1 root root       12 Feb  4 13:14 redis-check-aof -> redis-serverlrwxrwxrwx. 1 root root       12 Feb  4 13:14 redis-check-rdb -> redis-server-rwxr-xr-x. 1 root root  7030624 Feb  4 13:14 redis-clilrwxrwxrwx. 1 root root       12 Feb  4 13:14 redis-sentinel -> redis-server-rwxr-xr-x. 1 root root 15699832 Feb  4 13:14 redis-server

返回redis-7.2.6目录,将目录下的redis.conf拷贝到安装目标位置/usr/local/redis/bin,这个是redis的配置文件

cp redis.conf  /usr/local/redis/bin

3.运行测试

使用redis.conf配置启动redis

./redis-server ./redis.conf 
[root@localhost bin]# ./redis-server ./redis.conf5837:C 04 Feb 2025 13:15:52.211 # WARNING Memory overcommit must be enabled! Without it, a background save or replication may fail under low memory condition. Being disabled, it can also cause failures without low memory condition, see https://github.com/jemalloc/jemalloc/issues/1328. To fix this issue add 'vm.overcommit_memory = 1' to /etc/sysctl.conf and then reboot or run the command 'sysctl vm.overcommit_memory=1' for this to take effect.5837:C 04 Feb 2025 13:15:52.212 * oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo5837:C 04 Feb 2025 13:15:52.213 * Redis version=7.2.6, bits=64, commit=00000000, modified=0, pid=5837, just started5837:C 04 Feb 2025 13:15:52.213 * Configuration loaded5837:M 04 Feb 2025 13:15:52.214 * Increased maximum number of open files to 10032 (it was originally set to 1024).5837:M 04 Feb 2025 13:15:52.215 * monotonic clock: POSIX clock_gettime                _._                                                             _.-``__ ''-._                                                   _.-``    `.  `_.  ''-._           Redis 7.2.6 (00000000/0) 64 bit  .-`` .-```.  ```\/    _.,_ ''-._                                   (    '      ,       .-`  | `,    )     Running in standalone mode |`-._`-...-` __...-.``-._|'` _.-'|     Port: 6379 |    `-._   `._    /     _.-'    |     PID: 5837  `-._    `-._  `-./  _.-'    _.-'                                    |`-._`-._    `-.__.-'    _.-'_.-'|                                   |    `-._`-._        _.-'_.-'    |           https://redis.io         `-._    `-._`-.__.-'_.-'    _.-'                                    |`-._`-._    `-.__.-'    _.-'_.-'|                                   |    `-._`-._        _.-'_.-'    |                                    `-._    `-._`-.__.-'_.-'    _.-'                                         `-._    `-.__.-'    _.-'                                                 `-._        _.-'                                                         `-.__.-'                                               5837:M 04 Feb 2025 13:15:52.224 * Server initialized5837:M 04 Feb 2025 13:15:52.224 * Ready to accept connections tcp

出现这个界面就说明启动成功了。

4.简单设置

vim redis.conf配置文件,进行以下修改,完成后重启redis-server

1.默认daemonize no 改为 daemonize yes,让redis从后台启动。

# By default Redis does not run as a daemon. Use 'yes' if you need it.# Note that Redis will write a pid file in /var/run/redis.pid when daemonized.# When Redis is supervised by upstart or systemd, this parameter has no impact.daemonize yes

2.默认protected-mode yes 改为 protected-mode no,关闭保护模式,让其他服务(例如spring-boot后端服务)可以连接到这个redis。

# Protected mode is a layer of security protection, in order to avoid that# Redis instances left open on the internet are accessed and exploited.## When protected mode is on and the default user has no password, the server# only accepts local connections from the IPv4 address (127.0.0.1), IPv6 address# (::1) or Unix domain sockets.## By default protected mode is enabled. You should disable it only if# you are sure you want clients from other hosts to connect to Redis# even if no authentication is configured.protected-mode no

3.默认bind 127.0.0.1改为直接注释掉(默认bind 127.0.0.1只能本机访问)或改成0.0.0.0,否则影响远程IP连接

# By default, if no "bind" configuration directive is specified, Redis listens# for connections from all available network interfaces on the host machine.# It is possible to listen to just one or multiple selected interfaces using# the "bind" configuration directive, followed by one or more IP addresses.# Each address can be prefixed by "-", which means that redis will not fail to# start if the address is not available. Being not available only refers to# addresses that does not correspond to any network interface. Addresses that# are already in use will always fail, and unsupported protocols will always BE# silently skipped.## Examples:## bind 192.168.1.100 10.0.0.1     # listens on two specific IPv4 addresses# bind 127.0.0.1 ::1              # listens on loopback IPv4 and IPv6# bind * -::*                     # like the default, all available interfaces## ~~~ WARNING ~~~ If the computer running Redis is directly exposed to the# internet, binding to all the interfaces is dangerous and will expose the# instance to everybody on the internet. So by default we uncomment the# following bind directive, that will force Redis to listen only on the# IPv4 and IPv6 (if available) loopback interface addresses (this means Redis# will only be able to accept client connections from the same host that it is# running on).## IF YOU ARE SURE YOU WANT YOUR INSTANCE TO LISTEN TO ALL THE INTERFACES# COMMENT OUT THE FOLLOWING LINE.## You will also need to set a password unless you explicitly disable protected# mode.# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~bind 0.0.0.0

4.添加redis密码,requirepass一行的注释打开,并将默认密码改为requirepass 自己设置的密码,设置密码更加安全防止被黑客利用攻击。

# IMPORTANT NOTE: starting with Redis 6 "requirepass" is just a compatibility# layer on top of the new ACL system. The option effect will be just setting# the password for the default user. Clients will still authenticate using# AUTH <password> as usually, or more explicitly with AUTH default <password># if they follow the new protocol: both will work.## The requirepass is not compatible with aclfile option and the ACL LOAD# command, these will cause requirepass to be ignored.#requirepass lzj

5.客户端redis-cli

1.客户端连接
使用redis-cli命令进入交互模式。
-a设置密码
-p设置端口,不写默认6379
-h设置主机(没有用到)
进入交互模式后,客户端发送ping指令,服务端返回pong即为连接成功。
使用quit命令断开连接,退出交互模式。

[root@localhost bin]# ./redis-cli -a lzj  -p 6379 Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.127.0.0.1:6379> pingPONG127.0.0.1:6379> quit[root@localhost bin]#

如果忘记输入密码,交互模式下无法执行命令,使用auth命令补上密码,即可连接成功。

[root@localhost bin]# ./redis-cli   -p 6379 127.0.0.1:6379> ping(error) NOAUTH Authentication required.127.0.0.1:6379> auth lzjOK127.0.0.1:6379> pingPONG127.0.0.1:6379> 

服务端不设置密码的话,-a, auth都是不需要的。

2.测试存储数据到Redis

127.0.0.1:6379> set k1 v1OK127.0.0.1:6379> get k1"v1"

3.使用客户端命令关闭redis服务器,该命令关闭的是redis-server。

./redis-cli -a lzj  -p 6379 shutdown

6.用Docker安装Redis

docker run -d \--privileged=true \-v /data/redis/conf:/usr/local/etc/redis \-v /data/redis/data:/data \-p 16379:6379 \--name redis7 \redis:7.0.11  \redis-server /usr/local/etc/redis/redis.conf
  •