文章851
标签121
分类10

[转载] MySQL为什么”错误”选择代价更大的索引

原文地址 : MySQL为什么”错误”选择代价更大的索引
2024-09-25T07:06:49.png

1. 问题描述

群友提出问题,表里有两个列c1、c2,分别为INT、VARCHAR类型,且分别创建了unique key。

SQL查询的条件是 WHERE c1 = ? AND c2 = ?,用EXPLAIN查看执行计划,发现优化器优先选择了VARCHAR类型的c2列索引。

他表示很不理解,难道不应该选择看起来代价更小的INT类型的c1列吗?

2. 问题复现

创建测试表t1:

[root@yejr.run]> CREATE TABLE `t1` (
  `c1` int NOT NULL AUTO_INCREMENT,
  `c2` int unsigned NOT NULL,
  `c3` varchar(20) NOT NULL,
  `c4` varchar(20) NOT NULL,
  PRIMARY KEY (`c1`),
  UNIQUE KEY `k3` (`c3`),
  UNIQUE KEY `k2` (`c2`)
) ENGINE=InnoDB;

利用 mysql_random_data_load 写入一万行数据:

mysql_random_data_load -h127.0.0.1 -uX -pX yejr t1 10000

查看执行计划:

[root@yejr.run]> EXPLAIN SELECT * FROM t1 WHERE
 c2 = 1755950419 AND c3 = 'MichaelaAnderson'\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: t1
   partitions: NULL
         type: const
possible_keys: k3,k2
          key: k3
      key_len: 82
          ref: const
         rows: 1
     filtered: 100.00
        Extra: NULL

可以看到优化器的确选择了 k3 索引,而非”预期”的 k2 索引,这是为什么呢?

3. 问题分析

其实原因很简单粗暴:优化器认为这两个索引选择的代价都是一样的,只是优先选中排在前面的那个索引而已。

再建一个相同的表 t2,只不过把 k2、k3 的索引创建顺序对调下:

[root@yejr.run]> CREATE TABLE `t2` (
  `c1` int NOT NULL AUTO_INCREMENT,
  `c2` int unsigned NOT NULL,
  `c3` varchar(20) NOT NULL,
  `c4` varchar(20) NOT NULL,
  PRIMARY KEY (`c1`),
  UNIQUE KEY `k2` (`c2`),
  UNIQUE KEY `k3` (`c3`)
) ENGINE=InnoDB;

再查看执行计划:

[root@yejr.run]> EXPLAIN SELECT * FROM t2 WHERE
 c2 = 1755950419 AND c3 = 'MichaelaAnderson'\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: t1
   partitions: NULL
         type: const
possible_keys: k2,k3
          key: k2
      key_len: 4
          ref: const
         rows: 1
     filtered: 100.00
        Extra: NULL

我们利用 EXPLAIN ANALYZE 来查看下两次执行计划的代价对比:

-- 查看t1表执行计划代价
[root@yejr.run]> EXPLAIN ANALYZE SELECT * FROM t1 WHERE
  c2 = 1755950419 AND c3 = 'MichaelaAnderson'\G
*************************** 1. row ***************************
EXPLAIN: -> Rows fetched before execution  (cost=0.00..0.00 rows=1) (actual time=0.000..0.000 rows=1 loops=1)

-- 查看t2表执行计划代价
[root@yejr.run]> EXPLAIN ANALYZE SELECT * FROM t2 WHERE  c2 = 1755950419 AND c3 = 'MichaelaAnderson'\G
*************************** 1. row ***************************
EXPLAIN: -> Rows fetched before execution  (cost=0.00..0.00 rows=1) (actual time=0.000..0.000 rows=1 loops=1)

可以看到,很明显代价都是一样的。

再利用 OPTIMIZE_TRACE 查看执行计划,也能看到两个SQL的代价是一样的:

...
          {
            "rows_estimation": [
              {
                "table": "`t1`",
                "rows": 1,
                "cost": 1,
                "table_type": "const",
                "empty": false
              }
            ]
          },
...

所以,优化器认为选择哪个索引都是一样的,就看哪个索引排序更靠前。

从执行SELECT时的debug trace结果也能佐证:

-- 1、 T1表,k3索引在前面
  PRIMARY KEY (`c1`),
  UNIQUE KEY `k3` (`c3`),
  UNIQUE KEY `k2` (`c2`)

T@2: | | | | | | | | opt: (null): starting struct
T@2: | | | | | | | | opt: table: "`t1`"
T@2: | | | | | | | | opt: field: "c3"   (C3在前面,因此最后使用k3)
T@2: | | | | | | | | >convert_string
T@2: | | | | | | | | | >alloc_root
T@2: | | | | | | | | | | enter: root: 0x40a8068
T@2: | | | | | | | | | | exit: ptr: 0x4b41ab0
T@2: | | | | | | | | | <alloc_root 304
T@2: | | | | | | | | <convert_string 2610
T@2: | | | | | | | | opt: equals: "'Louise Garrett'" 
T@2: | | | | | | | | opt: null_rejecting: 0
T@2: | | | | | | | | opt: (null): ending struct
T@2: | | | | | | | | opt: Key_use: optimize= 0 used_tables=0x0 ref_table_rows= 18446744073709551615 keypart_map= 1
T@2: | | | | | | | | opt: (null): starting struct
T@2: | | | | | | | | opt: table: "`t1`"
T@2: | | | | | | | | opt: field: "c2"
T@2: | | | | | | | | opt: equals: "22896242"
T@2: | | | | | | | | opt: null_rejecting: 0
T@2: | | | | | | | | opt: null_rejecting: 0
T@2: | | | | | | | | opt: (null): ending struct
T@2: | | | | | | | | opt: Key_use: optimize= 0 used_tables=0x0 ref_table_rows= 18446744073709551615 keypart_map= 1
T@2: | | | | | | | | opt: (null): starting struct
T@2: | | | | | | | | opt: table: "`t1`"
T@2: | | | | | | | | opt: field: "c2"
T@2: | | | | | | | | opt: equals: "22896242"
T@2: | | | | | | | | opt: null_rejecting: 0
T@2: | | | | | | | | opt: (null): ending struct
T@2: | | | | | | | | opt: ref_optimizer_key_uses: ending struct
T@2: | | | | | | | | opt: (null): ending struct

-- 2、 T2表,k2索引在前面
  PRIMARY KEY (`c1`),
  UNIQUE KEY `k2` (`c2`),
  UNIQUE KEY `k3` (`c3`)

T@2: | | | | | | | | opt: (null): starting struct
T@2: | | | | | | | | opt: table: "`t2`"
T@2: | | | | | | | | opt: field: "c2" (C2在前面因此使用k2索引)
T@2: | | | | | | | | opt: equals: "22896242"
T@2: | | | | | | | | opt: null_rejecting: 0
T@2: | | | | | | | | opt: (null): ending struct
T@2: | | | | | | | | opt: Key_use: optimize= 0 used_tables=0x0 ref_table_rows= 18446744073709551615 keypart_map= 1
T@2: | | | | | | | | opt: (null): starting struct
T@2: | | | | | | | | opt: table: "`t2`"
T@2: | | | | | | | | opt: field: "c3"
T@2: | | | | | | | | >convert_string
T@2: | | | | | | | | | >alloc_root
T@2: | | | | | | | | | | enter: root: 0x40a8068
T@2: | | | | | | | | | | exit: ptr: 0x4b41ab0
T@2: | | | | | | | | | <alloc_root 304
T@2: | | | | | | | | <convert_string 2610
T@2: | | | | | | | | opt: equals: "'Louise Garrett'"
T@2: | | | | | | | | opt: null_rejecting: 0
T@2: | | | | | | | | opt: (null): ending struct
T@2: | | | | | | | | opt: ref_optimizer_key_uses: ending struct
T@2: | | | | | | | | opt: (null): ending struct

4. 问题延伸

到这里,我们不禁有疑问,这两个索引的代价真的是一样吗?

就让我们用 mysqlslap 来做个简单对比测试吧:

-- 测试1:对c2列随机point select
mysqlslap -hlocalhost -uroot -Smysql.sock --no-drop --create-schema X -i 3 --number-of-queries 1000000 -q "set @xid = cast(round(rand()*2147265929) as unsigned); select * from t1 where c2 = @xid" -c 8
...
    Average number of seconds to run all queries: 9.483 seconds
...


-- 测试2:对c3列随机point select
mysqlslap -hlocalhost -uroot -Smysql.sock --no-drop --create-schema X -i 3 --number-of-queries 1000000 -q "set @xid = concat('u',cast(round(rand()*2147265929) as unsigned)); select * from t1 where c3 = @xid" -c 8
...
    Average number of seconds to run all queries: 10.360 seconds
...

可以看到,如果是走 c3 列索引,耗时会比走 c2 列索引多出来约 7% ~ 9%(在我的环境下测试的结果,不同环境、不同数据量可能也不同)。

看来,MySQL优化器还是有必要进一步提高的哟 :)

测试使用版本:GreatSQL 8.0.25(MySQL 5.6.39结果亦是如此)。

MySQL 8.4 Reference Manual - mysqlslap

2024-09-25T06:57:59.png
mysqlslap - A Load Emulation Client

mysqlslap是一个诊断程序,旨在模拟MySQL服务器的客户端负载并报告每个阶段的时间。它的工作方式类似于多个客户端正在访问服务器。

使用方法

在命令行中使用mysqlslap时,您可以使用不同的选项来指定包含SQL语句的字符串或包含语句的文件。如果指定文件,默认情况下每行必须包含一个语句。您可以使用--delimiter选项指定不同的分隔符,这样可以指定跨越多行的语句或将多个语句放在一行上。在文件中不能包含注释,因为mysqlslap无法理解它们。

mysqlslap分为三个阶段:

  1. 创建模式、表,以及可选的用于测试的任何存储程序或数据。此阶段使用单个客户端连接。
  2. 运行负载测试。此阶段可以使用许多客户端连接。
  3. 清理(断开连接,如果指定则删除表)。此阶段使用单个客户端连接。

示例

  1. 提供您自己的创建和查询SQL语句,使用50个客户端进行查询,每个客户端查询200次:

    mysqlslap --delimiter=";"
      --create="CREATE TABLE a (b int);INSERT INTO a VALUES (23)"
      --query="SELECT * FROM a" --concurrency=50 --iterations=200
  2. mysqlslap构建查询SQL语句,使用具有两个INT列和三个VARCHAR列的表,使用五个客户端查询每个20次。不创建表或插入数据(即使用上一个测试的模式和数据):

    mysqlslap --concurrency=5 --iterations=20
      --number-int-cols=2 --number-char-cols=3
      --auto-generate-sql
  3. 告诉程序从指定文件中加载创建、插入和查询SQL语句,其中create.sql文件包含多个以;分隔的表创建语句和多个以;分隔的插入语句。--query文件应包含多个以;分隔的查询。先运行所有加载语句,然后使用五个客户端(每个执行五次)运行查询文件中的所有查询:

    mysqlslap --concurrency=5
      --iterations=5 --query=query.sql --create=create.sql
      --delimiter=";"

支持的选项

mysqlslap支持多种选项,可以在命令行中或在选项文件的[mysqlslap][client]组中指定。有关MySQL程序使用的选项文件的信息,请参阅使用选项文件。以下是部分支持的选项:

选项名称描述
--auto-generate-sql在没有文件或命令选项提供SQL语句时自动生成SQL语句
--auto-generate-sql-add-autoincrement自动为生成的表添加AUTO_INCREMENT列
--concurrency发出SELECT语句时模拟的客户端数
--create包含用于创建表的语句的文件或字符串
--iterations运行测试的次数
--hostMySQL服务器所在的主机
--user连接MySQL服务器的用户名
--password连接MySQL服务器的密码
......

以上仅为部分选项。有关更多选项,请参阅官方文档

Redis持久化问题 MISCONF Redis is configured to save RDB snapshots

2024-09-12T06:48:48.png

上次遇到的问题 : Redis 抛错:MISCONF Redis is configured to save RDB snapshots, but it is currently not able to...

在使用 Redis 时,您可能会遇到以下错误信息:

Fatal error: Uncaught RedisException: MISCONF Redis is configured to save RDB snapshots, but is currently not able to persist on disk. Commands that may modify the data set are disabled. Please check Redis logs for details about the error.

同时,在 Redis 错误日志中,您可能看到这样的信息:

20464:M 12 Sep 14:34:32.021 * 1 changes in 900 seconds. Saving...
20464:M 12 Sep 14:34:32.021 # Can't save in background: fork: Cannot allocate memory

这表明 Redis 无法在后台保存 RDB 快照,因为系统无法分配足够的内存。本文将探讨导致此问题的原因以及解决方案。

问题分析

原因

  1. 内存不足: Redis 尝试创建一个进程以保存数据快照,但系统内存不足,无法完成 fork 操作。
  2. 配置问题: Redis 配置可能导致内存使用不合理,或者其他进程占用了过多内存。

解决方案

1. 检查系统内存

使用以下命令检查系统内存使用情况:

free -h

如果可用内存很少,您需要释放一些内存或增加系统的物理内存。

2. 优化 Redis 配置

可以考虑以下配置调整:

  • 减少内存使用: 在 redis.conf 文件中,调整 maxmemory 设置,限制 Redis 使用的最大内存。
maxmemory 256mb
  • 设置内存淘汰策略: 根据需求设置内存淘汰策略,例如:
maxmemory-policy allkeys-lru

3. 释放系统内存

如果其他进程占用了过多的内存,可以考虑:

  • 停止不必要的服务: 关闭一些不必要的服务或应用程序,释放内存。
  • 重启系统: 在某些情况下,重启系统可以释放被占用的内存。

4. 增加系统内存

如果可能,可以考虑增加物理内存或使用交换空间(swap),以便在内存不足时使用。

5. 使用持久化设置

如果您不需要频繁保存 RDB 快照,可以调整持久化设置,减少保存频率:

save 900 1  # 每900秒至少有1个键被修改时保存

总结

Redis "Cannot Allocate Memory" 错误通常与系统内存不足有关。通过检查系统内存、优化 Redis 配置、释放内存或增加物理内存,您可以有效解决此问题。定期监控 Redis 的内存使用情况和系统资源,可以帮助您预防此类问题的发生。

记一次 Vue2 中使用 Pako 解压缩 Blob 失败的经历

2024-09-28T08:55:06.png
在前端开发中,处理压缩数据是常见的需求。最近在一个项目中,我尝试使用 Pako 库解压缩从 WebSocket 接收到的 Blob 数据,结果遇到了一些问题。本文将分享我的经验和解决方案。

背景

在这个项目中,我使用 Vue2 和 Pako 库来处理 WebSocket 数据。目标是从服务器接收 Gzip 压缩的 Blob 数据,并将其解压缩为可用的 JSON 对象。以下是我最初的代码片段:

ws.onmessage = function (res) {
    console.log("数据接收中...");
    // 直接尝试解压缩 Blob 数据
    let msg = JSON.parse(pako.inflate(res.data, { to: 'string' }));
    console.log("解压缩结果", msg);
};

遇到的问题

在运行上述代码时,我收到以下错误信息:

Uncaught Error: unknown compression method

经过排查,我发现问题的根源在于 Blob 数据的处理方式不正确。直接传递 Blob 数据给 pako.inflate 并不能成功解压。

原因分析

Blob 对象需要先转换为 ArrayBuffer,才能被 Pako 正确解压缩。对此我缺乏足够的重视,导致了错误的发生。

解决方案

为了解决这个问题,我需要使用 FileReader 将 Blob 转换为 ArrayBuffer。以下是修改后的代码:

ws.onmessage = function (res) {
    console.log("数据接收中...");
    let reader = new FileReader();
    reader.readAsArrayBuffer(res.data); // 将 Blob 转换为 ArrayBuffer

    reader.onload = function() {
        // 使用 Pako 解压缩 ArrayBuffer 数据
        let msg = JSON.parse(pako.inflate(reader.result, { to: 'string' }));
        console.log("解压缩结果", msg);
    };
};

关键步骤

  1. 使用 FileReader: 通过 readAsArrayBuffer 方法将 Blob 数据转换为 ArrayBuffer。
  2. onload 事件中解压: 一旦读取完成,在 onload 事件中调用 pako.inflate 来解压数据。

总结

通过这次经历,我认识到在处理 Blob 数据时,正确的转换步骤是至关重要的。直接将 Blob 数据传递给解压缩函数是不可行的。使用 FileReader 将其转换为 ArrayBuffer 是解决此问题的有效方法。

如果您在使用 Pako 解压缩 Blob 数据时遇到类似问题,可以参考我的解决方案。希望这篇博客能对您有所帮助!

参考文献

Linux 临时目录 /tmp 与 /var/tmp

什么是 /tmp 目录?

顾名思义,/tmp 目录用于存放系统和用户应用程序在短时间内需要的数据。大多数 Linux 发行版在每次重新启动后会自动清空该目录。
2024-09-05T12:23:14.png

用途

  • 临时存储:安装软件时,安装程序可能会在此目录存储所需的临时文件。
  • 项目处理:在处理项目时,系统可能将文件的自动保存版本存储在此目录中。

简单来说,/tmp 目录用于存放那些不再需要时可以删除的临时文件。

/tmp/var/tmp 的区别

虽然 /tmp/var/tmp 都是临时目录,但它们之间存在显著差异:

特性/tmp/var/tmp
生命周期文件在重启时会被删除文件在重启后依然保留
用户访问所有用户可以访问文件通常特定于某个用户
用途存储短期临时文件,如安装包存储长期临时文件,如备份或日志

自动清理 /tmp 目录

大多数发行版在重启 Linux 系统时会清理 /tmp 目录。然而,如果服务器长时间运行,可能需要手动清理。

清理策略

建议删除最近三天未使用且不属于 root 用户的文件。可以使用以下命令查找并删除这些文件:

sudo find /tmp -type f \( ! -user root \) -atime +3 -delete

自动化清理任务

为了自动清理 /tmp 目录,可以创建一个 cron 作业。首先,打开系统级别的 crontab:

sudo crontab -e

如果您是第一次使用 crontab,系统会要求您选择文本编辑器。推荐使用 vimnano。选择后,在文件末尾添加以下行:

0 0 * * * sudo find /tmp -type f ! -user root -atime +3 -delete

保存文件并退出编辑器后,系统将在每天的午夜自动清理 /tmp 目录。

结论

通过本文,您了解了 /tmp 目录和 /var/tmp 目录的作用与区别,以及如何在 Linux 服务器上清理 /tmp 目录的文件。

">