0
点赞
收藏
分享

微信扫一扫

MySQL 条件下推与排序优化实例--MySQL8.035

MySQL数据库衍生出很多兼容他的数据库产品,Mariadb、OceanBase (开源mysql兼容版本)、PolarDB for MySQL 等这些数据库产品都兼容MySQL.国产的项目不允许有MySQL的存在,导致大部分乙方的产品都在研究信创数据库,dump 完Oracle,继续dump MySQL.

所以这是一个系列,这里将使用MySQL,以及国产数据库产生同样的数据,进行他们查询规则的分析,看看是否有差异,为后续接受国产数据库,和新的项目做准备。本期是MySQL 8.035,针对这个版本在查询分析中的一些问题和特色进行分析和总结。

在进行总结前我们要产生测试数据。

CREATE DATABASE IF NOT EXISTS test;
USE test;

CREATE TABLE users (
    user_id INT PRIMARY KEY AUTO_INCREMENT,
    username VARCHAR(50),
    email VARCHAR(100),
    signup_date DATE
);

CREATE TABLE products (
    product_id INT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(100),
    price FLOAT,
    created_at DATE
);

CREATE TABLE orders (
    order_id INT PRIMARY KEY AUTO_INCREMENT,
    user_id INT,
    product_id INT,
    order_date DATE,
    amount FLOAT,
    note TEXT,
    FOREIGN KEY (user_id) REFERENCES users(user_id),
    FOREIGN KEY (product_id) REFERENCES products(product_id)
);

DELIMITER $$

CREATE FUNCTION rand_string(n INT) RETURNS VARCHAR(255)
DETERMINISTIC
BEGIN
    DECLARE chars VARCHAR(62) DEFAULT 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
    DECLARE result VARCHAR(255) DEFAULT '';
    DECLARE i INT DEFAULT 0;
    WHILE i < n DO
        SET result = CONCAT(result, SUBSTRING(chars, FLOOR(1 + RAND() * 62), 1));
        SET i = i + 1;
    END WHILE;
    RETURN result;
END$$

DELIMITER ;

DELIMITER $$

CREATE PROCEDURE insert_users(IN total INT)
BEGIN
    DECLARE i INT DEFAULT 0;
    WHILE i < total DO
        INSERT INTO users (username, email, signup_date)
        VALUES (
            rand_string(8),
            CONCAT(rand_string(5), '@example.com'),
            DATE_SUB(CURDATE(), INTERVAL FLOOR(RAND() * 365 * 5) DAY)
        );
        SET i = i + 1;
    END WHILE;
END$$

DELIMITER ;

DELIMITER $$

CREATE PROCEDURE insert_products(IN total INT)
BEGIN
    DECLARE i INT DEFAULT 0;
    WHILE i < total DO
        INSERT INTO products (name, price, created_at)
        VALUES (
            rand_string(10),
            ROUND(RAND() * 5000, 2),
            DATE_SUB(CURDATE(), INTERVAL FLOOR(RAND() * 365 * 5) DAY)
        );
        SET i = i + 1;
    END WHILE;
END$$

DELIMITER ;

DELIMITER $$

CREATE PROCEDURE insert_orders(IN total INT)
BEGIN
    DECLARE i INT DEFAULT 0;
    DECLARE uid INT;
    DECLARE pid INT;
    DECLARE user_count INT;
    DECLARE product_count INT;

    SELECT COUNT(*) INTO user_count FROM users;
    SELECT COUNT(*) INTO product_count FROM products;

    WHILE i < total DO
        SET uid = FLOOR(1 + RAND() * user_count);
        SET pid = FLOOR(1 + RAND() * product_count);
        INSERT INTO orders (user_id, product_id, order_date, amount, note)
        VALUES (
            uid,
            pid,
            DATE_SUB(CURDATE(), INTERVAL FLOOR(RAND() * 365 * 5) DAY),
            ROUND(RAND() * 100, 2),
            rand_string(20)
        );
        SET i = i + 1;
    END WHILE;
END$$

DELIMITER ;

-- 插入 10000 用户
CALL insert_users(10000);

-- 插入 1000 产品
CALL insert_products(1000);

-- 插入 100000 订单
CALL insert_orders(100000);

上面测试数据,产生3张表

mysql: [Warning] Using a password on the command line interface can be insecure.
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 14
Server version: 8.0.35 MySQL Community Server - GPL

Copyright (c) 2000, 2023, Oracle and/or its affiliates.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h'forhelp. Type '\c' to clear the current input statement.

mysql> use test;
Database changed
mysql> show tables;
+----------------+
| Tables_in_test |
+----------------+
| orders         |
| products       |
| users          |
+----------------+
3 rows inset (0.03 sec)

mysql>

这里这三张表之间的关系为

  1. users 表 (用户表) 主键:user_id

表示一个注册用户

一个用户可以创建多个订单(1 对多)

  1. products 表 (产品表) 主键:product_id

表示一个商品

一个产品可以被多个订单引用(1 对多)

  1. orders 表 (订单表) 主键:order_id

外键:user_id -> users(user_id)

外键:product_id -> products(product_id)

表示某个用户下了某个产品的订单

表名

外键字段

关联主表

说明

orders

user_id

users

一个订单属于一个用户

orders

product_id

products

一个订单对应一个产品

表名

与其他表的关系

说明

users

1 对 多 → orders.user_id

一个用户可以有多个订单

products

1 对 多 → orders.product_id

一个商品可以出现在多个订单中

orders

多 对 1 ← users / products

每条订单记录属于一个用户、对应一个商品

问题1:过滤条件到底写在表链接中,还是写到where 条件中,我们看下面的图



MySQL 条件下推与排序优化实例--MySQL8.035_PostgreSQL


where 带条件

MySQL 条件下推与排序优化实例--MySQL8.035_数据库_02


条件写join上

两种查询的写法,一个是将order_date的时间过滤写到 inner join中的一行,二另一个写到了where条件中,在实际的应用中我们该怎么写。

1  先分清inner join 和 left join,在inner join 中是匹配两个表之间重合的数据,相当于交集,则可以把where的条件上推,写成图2的方式,如果是写成left join 而我们的where 条件中的过滤是左表,则还是可以把where条件写到上面,但如果过滤条件的表是右面的表,且使用的是left join,则不能把时间的条件写到 join后,而是需要写到where 条件中。我们可以用下面的表格来表达,简述中的意思。

情况

建议写法

想保留左表所有记录

过滤条件写在 ON 子句

明确只取有右表匹配的

用 INNER JOIN 或 LEFT JOIN + WHERE

在MySQL中,条件下推 是一种优化策略:将 WHERE 或 JOIN 的过滤条件尽可能早地应用在执行计划中,通常是在访问表数据前或刚开始访问时,从而减少不必要的数据访问和中间结果的行数。

在我们上图的执行中,可以多次运行两种语句观察具体的执行时间,在观察中我发现一些规律和问题。

一、在where条件中撰写条件,比在join后跟上过滤条件,运行的时间总是 where条件使用更多的时间。

二、如果将where条件上移,则需要建立更适合的索引,需要建立 user_id,order_date的联合索引在orders表中。

比较方式差异

ON 写过滤条件

WHERE 写过滤条件

语义上

在 JOIN 之前进行过滤

在 JOIN 之后进行过滤

优化器是否可合并

✅ 是(对 INNER JOIN)

✅ 是(等价)

实际执行计划

⚠️ 通常一样(当索引存在)

⚠️ 通常一样

典型差别出现在哪

子查询 + 多表复杂 JOIN 时,或含 LEFT JOIN 时

更可能导致逻辑或性能差异

问题2 在多表查询中的排序问题和优化

在早期的MySQL数据库中,对于几个词老手都是敏感的

1  倒排 2  filesort 3  临时表

问题 1,什么情况可以避免使用filesort 在有排序的情况下

答:在查询的字段都在索引中,且排序的字段为这个索引的第一个字段的情况下,是无需进行filesort的,这里的原理为数据给付的时候已经是按照排序进行给付的,所以无需再获取数据后,在进行二次排序。

我们可以看下面的案例,第一个案例是有排序覆盖索引的情况下,查询没有进行usering filesort,第二个案例,我们删除了索引,则查询走了using filesort



MySQL 条件下推与排序优化实例--MySQL8.035_数据库_03


索引覆盖排序的情况,无需进行filesort

MySQL 条件下推与排序优化实例--MySQL8.035_mysql_04


当我们没有对应的索引的情况下

这部分在MySQL有一些特殊的情况和优化的手段,一个字解释就是拆。

我们以上面的例子中,如果要进行大量返回值的排序后的查询,我们可以用这样的方法,返回的数据太多的情况下采用这样的方案有助于解耦和降低filesoft时的性能消耗。

SELECT  user_id 
FROM users 
WHERE user_id in (1,...,1000) ORDER BY signup_date DESC LIMIT 900;

SELECT user_id,user_name FROM orders WHERE id IN (user_id);

在排序的写法中还有一种要不得就是随即函数,这样的写法中最大的问题为结果的每行都要进行一次rand()函数的计算。

SELECT  user_id 
FROM users 
WHERE user_id in (1,.....,1000) ORDER BY rand() DESC LIMIT 1;



MySQL 条件下推与排序优化实例--MySQL8.035_mysql_05


排序不要使用rand()函数

其实如果是想抽取一个随机的数据并给付结果,是可以通过下面的方案来进行的。也就是给一个确定的值。不要在ORDER BY 中进行计算。具体的方案是

explain SELECT  user_id 
FROM users 
WHERE user_id in (1,999,34,23,56,564,1000) ORDER BY rand() DESC LIMIT 1;

可以撰写成下方的样子,数字7是可以改变的,在有多少值的情况下,可以改成多少。

explain SELECT user_id, username
FROM users
WHERE user_id = (
  SELECT CAST(SUBSTRING_INDEX(SUBSTRING_INDEX('1,999,34,23,56,564,1000', ',', n), ',', -1) AS UNSIGNED)
  FROM (
    SELECT FLOOR(1 + RAND() * 7) AS n
  ) AS rand_pos
);



MySQL 条件下推与排序优化实例--MySQL8.035_MySQL_06


原有的语句与执行计划

MySQL 条件下推与排序优化实例--MySQL8.035_mysql_07


修改后的语句和执行计划

最后进行一个总结,MySQL在查询中由于本身数据处理引擎比较弱,很多复杂的语句需要进行拆分,同时在mysql中的条件下推 predicate Pushdown 要会使用,分清楚什么时候可以把条件写到 JOIN中,什么时候不可以会导致业务逻辑错误。

下推类型

说明

版本支持

✅ 条件下推(Predicate Pushdown)

将 WHERE 条件下推到 JOIN、子查询、视图中提前执行

全版本支持(优化器决定)

✅ 计算下推(Expression Pushdown)

将表达式计算提前到扫描阶段(如 JSON、函数计算)

8.0+ 有更智能优化

✅ 投影下推(Projection Pushdown)

只读取需要的字段,避免多读无用字段

常用于 SELECT col1 场景

✅ 外部表下推(Storage Pushdown)

在远程存储/引擎中执行过滤计算(如 FEDERATED, 外部引擎)

视插件能力

MySQL 条件下推与排序优化实例--MySQL8.035_数据库_08

举报

相关推荐

0 条评论