将MySQL查询优化从190秒缩短至1秒:数千万条记录的处理

varchar是什么字段类型_varchar(50)中50代表的含义_varchar(50)

首先,必须明确一点,处理数千万条数据通常并不是MySQL的最佳用例。

优化MySQL以处理数千万条记录有几种策略:

在本讨论中,我们考虑的是一个单张MySQL表包含数千万条记录的场景。

该表的设计不佳,业务规则不允许将SQL查询拆分为多个子查询。

在这种情况下,开发者可以通过优化SQL来尝试实现他们的查询目标。

当一个MySQL表包含数千万条记录时,就需要特别注意了。

本讨论主要关注极端情况下的SQL优化策略。

varchar(50)中50代表的含义_varchar是什么字段类型_varchar(50)

生成数千万条记录

我们将使用存储过程来生成1000万条记录。

以下是表结构:

CREATE TABLE `orders` (
  `order_id` int NOT NULL AUTO_INCREMENT,
  `user_id` int DEFAULT NULL,
  `order_date` date NOT NULL,
  `total_amount` decimal(10,2NOT NULL,
  PRIMARY KEY (`order_id`),
  KEY `idx_user_id` (`user_id`) USING BTREE,
  KEY `idx_user_amount` (`user_id`,`total_amount`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

CREATE TABLE `users` (
  `user_id` int NOT NULL AUTO_INCREMENT,
  `username` varchar(50COLLATE utf8mb4_general_ci NOT NULL,
  `email` varchar(100COLLATE utf8mb4_general_ci NOT NULL,
  `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`user_id`),
  KEY `idx_user_id` (`user_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

生成数据的存储过程如下。

用户数据:

-- 存储过程生成1000个用户
CREATE DEFINER=`root`@`localhost` PROCEDURE `create_users`()
BEGIN
    DECLARE i INT DEFAULT 0;
    DECLARE total_users INT DEFAULT 1000-- 调整用户数量
    DECLARE rnd_username VARCHAR(50);
    DECLARE rnd_email VARCHAR(100);

    WHILE i < total_users DO
        -- 生成随机的用户名和电子邮件
        SET rnd_username = CONCAT('User'FLOOR(1 + RAND() * 10000000)); -- 假设用户名唯一
        SET rnd_email = CONCAT(rnd_username, '@example.com'); -- 假设电子邮件唯一
        -- 将数据插入到users表中
        INSERT INTO users (username, email) VALUES (rnd_username, rnd_email);

        SET i = i + 1;
    END WHILE;
END

订单数据生成存储过程:

CREATE DEFINER=`root`@`localhost` PROCEDURE `generate_orders`()
BEGIN
    DECLARE i INT DEFAULT 0;
    DECLARE total_users INT DEFAULT 1000-- 用户数量
    DECLARE total_orders_per_user INT DEFAULT 1000-- 每个用户的订单数量
    DECLARE rnd_user_id INT;
    DECLARE rnd_order_date DATE;
    DECLARE rnd_total_amount DECIMAL(102);
    DECLARE j INT DEFAULT 0;

    WHILE i < total_users DO
        -- 获取用户ID
        SELECT user_id INTO rnd_user_id FROM users LIMIT i, 1;

        WHILE j < total_orders_per_user DO
            -- 生成订单日期和总金额
            SET rnd_order_date = DATE_ADD('2020-01-01'INTERVAL FLOOR(RAND() * 1096DAY); -- 随机日期,介于2020-01-01和2022-12-31之间
            SET rnd_total_amount = ROUND(RAND() * 10002); -- 随机总金额,介于0和1000之间
            -- 将数据插入到orders表中
            INSERT INTO orders (user_id, order_date, total_amount) VALUES (rnd_user_id, rnd_order_date, rnd_total_amount);

            SET j = j + 1;
        END WHILE;
        SET j = 0;

        SET i = i + 1;
    END WHILE;
END

分离users和数据的生成过程,以便多次调用存储过程,利用多线程。

首先调用call (),然后打开15个窗口调用订单生成存储过程call ()。

整个过程将生成1000个用户和15*1000*1000,即1500万条订单记录。

varchar(50)_varchar是什么字段类型_varchar(50)中50代表的含义

原始SQL

这是一个非常简单的SQL查询varchar(50)中50代表的含义,用于计算每个用户的总订单金额。

在没有默认创建索引的情况下,这个查询需要超过190秒才能执行。

-- 第一版
SELECT a.*sum(b.total_amount) as total 
FROM users a 
LEFT JOIN orders b ON a.user_id = b.user_id
GROUP BY a.user_id;

查询分析如下:

正如我们所见,没有使用任何索引,查询类型为 "ALL",意味着全表扫描。

执行时间:191秒。

第一次优化:常规索引

在SQL条件中使用的所有列上创建索引,包括WHERE、JOIN和SUM。

CREATE INDEX idx_orders_user_id ON orders (user_id);
CREATE INDEX idx_orders_total_amount ON orders (total_amount);
CREATE INDEX idx_users_user_id ON users (user_id);

SQL查询保持不变:

-- 第一版
SELECT a.*sum(b.total_amount) as total 
FROM users a 
LEFT JOIN orders b ON a.user_id = b.user_id
GROUP BY a.user_id;

让我们检查一下执行计划:

varchar(50)_varchar(50)中50代表的含义_varchar是什么字段类型

类型变为 "index" 或 "ref",所有查询都使用了索引。

然而,结果令人失望。执行时间增加到了超过460秒。

这表明查询变慢是因为MySQL使用主键定位相关字段的一种机制。

第二次优化:覆盖索引

覆盖索引是一个包含满足查询所需的所有列的索引,允许查询直接从索引中执行,而不需要访问实际的数据行。

-- 不要删除常规索引
-- drop INDEX idx_orders_user_id ON orders;
-- drop INDEX idx_orders_total_amount ON orders;
CREATE INDEX idx_orders_total_amount_user_id ON orders (total_amount, user_id);
CREATE INDEX idx_orders_user_id_total_amount ON orders (user_id, total_amount);

在1500万条记录上创建索引需要超过300秒,因此索引应该谨慎创建。

SQL查询保持不变:

-- 第一版
SELECT a.*sum(b.total_amount) as total 
FROM users a 
LEFT JOIN orders b ON a.user_id = b.user_id
GROUP BY a.user_id;

让我们检查一下执行计划:

varchar(50)_varchar是什么字段类型_varchar(50)中50代表的含义

我们可以看到,表的类型已从 "index" 更改为 "ref"。

查询时间现在已从460多秒减少到10秒。

这个结果证明了覆盖索引可以提高查询性能。

关键观察点是,只有索引减少了查询时间,而没有做到。

这部分的解释与MySQL关键字的执行顺序(假设的varchar(50)中50代表的含义,因为没有找到确切的来源)有关。

MySQL执行顺序:

shell from
on
join
where
group by
having
select
distinct
union (all)
order by
limit

覆盖索引在的SUM函数之前使用了WHERE子句,这与索引的创建顺序一致。

drop INDEX idx_orders_user_id ON orders;
drop INDEX idx_orders_total_amount ON orders;
drop INDEX idx_orders_total_amount_user_id ON orders;

删除冗余索引显示执行时间保持不变,仍然是10秒。

因此,这里的优化策略是使用覆盖索引来有效地击中索引。

varchar(50)_varchar是什么字段类型_varchar(50)中50代表的含义

第三次优化:减少数据量

减少数据量涉及删除不必要的数据或进行架构调整。一种实现方式是分割表。

通过这种方式,数据量可以从数千万减少到数百万甚至数十万,从而显著提高查询速度。

-- 第三次优化:减少数据量
SELECT a.*sum(b.total_amount) as total 
FROM users a 
LEFT JOIN orders b ON a.user_id = b.user_id
WHERE a.user_id > 1033
GROUP BY a.user_id;

执行计划如下:

varchar(50)中50代表的含义_varchar(50)_varchar是什么字段类型

正如我们所见,users表的类型是 "range",过滤了数据的一部分。

查询时间从10秒减少到7秒,证明了减少数据量是有效的。

varchar(50)_varchar(50)中50代表的含义_varchar是什么字段类型

第四次优化:小表驱动大表

在MySQL中,优化器通常会根据查询条件和表的大小选择最合适的驱动表。

“小表驱动大表”策略涉及在连接查询中选择较小的表作为驱动表,以减少内存使用和处理时间。

基于第三次优化的结果,让我们尝试这种策略。

-- 第三版,小表驱动大表,没有显著效果
SELECT a.*sum(b.total_amount) as total 
FROM users a 
LEFT JOIN (SELECT user_id, total_amount FROM orders c WHERE c.user_id > 1033) b ON a.user_id = b.user_id
WHERE a.user_id > 1033
GROUP BY a.user_id;

这个更改将LEFT JOIN表修改为子查询,预先过滤了一些数据。

执行计划如下:

varchar(50)_varchar(50)中50代表的含义_varchar是什么字段类型

正如我们所见,执行计划没有太大变化,实际的执行性能保持不变。

虽然“小表驱动大表”策略在这里没有效果,但它仍然可能是一个有效的优化策略,具体取决于特定的业务逻辑。

第五次优化:强制使用索引

当在涉及数千万条记录的查询中使用IN子句时,设计不佳的索引可能会导致索引效率低下,影响查询性能。

通常,MySQL优化器会选择最佳的执行计划,包括适当的索引。然而,对于大量数据的IN子句查询,MySQL可能无法有效地使用索引,导致全表扫描或索引效率低下。

以下SQL演示了这一点,由于IN数据不是稀疏的,强制索引和不强制索引产生了相似的结果:

-- 第五版,强制索引
SELECT a.*sum(b.total_amount) as total 
FROM users a 
LEFT JOIN orders b FORCE INDEX (idx_orders_user_id_total_amount) ON a.user_id = b.user_id
WHERE b.user_id IN (103310341035103610371038)
GROUP BY a.user_id;
-- 第五版,不强制索引
SELECT a.*sum(b.total_amount) as total 
FROM users a 
LEFT JOIN orders b ON a.user_id = b.user_id
WHERE b.user_id IN (103310341035103610371038)
GROUP BY a.user_id;

在这两种情况下,查询时间都不到一秒。

在实际的商业场景中,可能会出现查询虽然根据执行计划命中了索引,但仍然很慢的情况。在这种情况下,可以尝试强制使用索引。

优化策略优化SQL Tips

命中索引的核心是覆盖索引。对于数千万条数据量的情况,可能需要使用强制索引。

小贴士结果中type的含义

在MySQL的查询结果中,type字段表示查询使用的访问类型,代表查询执行过程中的访问方法。

根据访问类型,MySQL的查询优化器将选择不同的执行计划。以下是type字段的可能值及其含义:

通常,type字段的值按从好到坏的顺序排列为、const、、ref、range、index、all。然而,实际情况取决于特定的查询、表结构和索引使用。更好的查询性能通常对应于更好的type值。

varchar是什么字段类型_varchar(50)_varchar(50)中50代表的含义

MySQL的表查找机制

在MySQL中,表查找(也称为“ref”或“ ”)指的是MySQL首先使用索引找到满足条件的行位置,然后查找主表中的实际数据行。

这个过程通常发生在覆盖索引无法满足所有查询要求的情况下。

当查询无法完全由索引满足时,MySQL需要查找主表以获取额外信息。这通常发生在以下情况:

当MySQL执行表查找时,由于需要从主表读取数据,因此会发生额外的磁盘访问,这可能会降低性能,尤其是在大型表或高并发环境中。

为了最小化表查找,请考虑以下建议:

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注