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>
这里这三张表之间的关系为
- users 表 (用户表) 主键:user_id
表示一个注册用户
一个用户可以创建多个订单(1 对多)
- products 表 (产品表) 主键:product_id
表示一个商品
一个产品可以被多个订单引用(1 对多)
- orders 表 (订单表) 主键:order_id
外键:user_id -> users(user_id)
外键:product_id -> products(product_id)
表示某个用户下了某个产品的订单
表名 | 外键字段 | 关联主表 | 说明 |
orders |
|
| 一个订单属于一个用户 |
orders |
|
| 一个订单对应一个产品 |
表名 | 与其他表的关系 | 说明 |
| 1 对 多 → | 一个用户可以有多个订单 |
| 1 对 多 → | 一个商品可以出现在多个订单中 |
| 多 对 1 ← | 每条订单记录属于一个用户、对应一个商品 |
问题1:过滤条件到底写在表链接中,还是写到where 条件中,我们看下面的图
where 带条件
条件写join上
两种查询的写法,一个是将order_date的时间过滤写到 inner join中的一行,二另一个写到了where条件中,在实际的应用中我们该怎么写。
1 先分清inner join 和 left join,在inner join 中是匹配两个表之间重合的数据,相当于交集,则可以把where的条件上推,写成图2的方式,如果是写成left join 而我们的where 条件中的过滤是左表,则还是可以把where条件写到上面,但如果过滤条件的表是右面的表,且使用的是left join,则不能把时间的条件写到 join后,而是需要写到where 条件中。我们可以用下面的表格来表达,简述中的意思。
情况 | 建议写法 |
想保留左表所有记录 | 过滤条件写在 |
明确只取有右表匹配的 | 用 |
在MySQL中,条件下推 是一种优化策略:将 WHERE 或 JOIN 的过滤条件尽可能早地应用在执行计划中,通常是在访问表数据前或刚开始访问时,从而减少不必要的数据访问和中间结果的行数。
在我们上图的执行中,可以多次运行两种语句观察具体的执行时间,在观察中我发现一些规律和问题。
一、在where条件中撰写条件,比在join后跟上过滤条件,运行的时间总是 where条件使用更多的时间。
二、如果将where条件上移,则需要建立更适合的索引,需要建立 user_id,order_date的联合索引在orders表中。
比较方式差异 |
|
|
语义上 | 在 JOIN 之前进行过滤 | 在 JOIN 之后进行过滤 |
优化器是否可合并 | ✅ 是(对 INNER JOIN) | ✅ 是(等价) |
实际执行计划 | ⚠️ 通常一样(当索引存在) | ⚠️ 通常一样 |
典型差别出现在哪 | 子查询 + 多表复杂 JOIN 时,或含 LEFT JOIN 时 | 更可能导致逻辑或性能差异 |
问题2 在多表查询中的排序问题和优化
在早期的MySQL数据库中,对于几个词老手都是敏感的
1 倒排 2 filesort 3 临时表
问题 1,什么情况可以避免使用filesort 在有排序的情况下
答:在查询的字段都在索引中,且排序的字段为这个索引的第一个字段的情况下,是无需进行filesort的,这里的原理为数据给付的时候已经是按照排序进行给付的,所以无需再获取数据后,在进行二次排序。
我们可以看下面的案例,第一个案例是有排序覆盖索引的情况下,查询没有进行usering filesort,第二个案例,我们删除了索引,则查询走了using filesort
索引覆盖排序的情况,无需进行filesort
当我们没有对应的索引的情况下
这部分在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;
排序不要使用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在查询中由于本身数据处理引擎比较弱,很多复杂的语句需要进行拆分,同时在mysql中的条件下推 predicate Pushdown 要会使用,分清楚什么时候可以把条件写到 JOIN中,什么时候不可以会导致业务逻辑错误。
下推类型 | 说明 | 版本支持 |
✅ 条件下推(Predicate Pushdown) | 将 | 全版本支持(优化器决定) |
✅ 计算下推(Expression Pushdown) | 将表达式计算提前到扫描阶段(如 JSON、函数计算) | 8.0+ 有更智能优化 |
✅ 投影下推(Projection Pushdown) | 只读取需要的字段,避免多读无用字段 | 常用于 |
✅ 外部表下推(Storage Pushdown) | 在远程存储/引擎中执行过滤计算(如 FEDERATED, 外部引擎) | 视插件能力 |