什么是 ‘小表驱动大表’ 原则?如何实现 JOIN顺序优化?(图解+秒懂+史上最全)

本文 的 原文 地址

原始的内容,请参考 本文 的 原文 地址

本文 的 原文 地址

尼恩说在前面

在40岁老架构师 尼恩的读者交流群(50+)中,最近有小伙伴拿到了一线互联网企业如得物、阿里、滴滴、极兔、有赞、希音、百度、网易、美团、蚂蚁、得物的面试资格,遇到很多很重要的相关面试题:

JOIN顺序优化,什么是 ‘小表驱动大表’ 原则?

最近有小伙伴在面 滴滴,问到了相关的面试题,可以说是逢面必问。

小伙伴没有系统的去梳理和总结,所以支支吾吾的说了几句,面试官不满意,面试挂了。

所以,尼恩给大家做一下系统化、体系化的梳理,使得大家内力猛增,可以充分展示一下大家雄厚的 “技术肌肉”,让面试官爱到 “不能自已、口水直流”,然后实现”offer直提”。

当然,这道面试题,以及参考答案,也会收入咱们的 《尼恩Java面试宝典PDF》V175版本,供后面的小伙伴参考,提升大家的 3高 架构、设计、开发水平。

《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》的PDF,请到文末公号【技术自由圈】获取

本文作者:

  • 第一作者 老架构师 肖恩(肖恩 是尼恩团队 高级架构师,负责写此文的第一稿,初稿 )
  • 第二作者 老架构师 尼恩 (45岁老架构师, 负责 提升此文的 技术高度,让大家有一种 俯视 技术、俯瞰技术、 技术自由 的感觉

电商系统订单查询性能优化案例

问题产生

在电商系统的性能测试中,发现查询用户订单详情的接口耗时在600-800ms之间,不满足200ms以内的性能要求,需要进行接口优化。

定位问题

使用SkyWalking定位,发现接口调用耗时666ms,其中SQL查询耗时585ms,此SQL导致接口慢响应。

SQL分析


SELECT
    o.* 
FROM
    users u  LEFT JOIN orders o 
    ON u.user_id = o.user_id 
WHERE
    1 = 1 
    AND o.order_status IN (1, 2)  -- 1-待付款 2-已付款
    AND u.registration_channel = 'APP' 
    AND u.city = '北京';

两张表的数据量:

  • users(用户表):200万条
  • orders(订单表):200万条

问题分析

本次查询采用 Index Nested-Loop Join 执行流程(orders表在user_id字段有索引):

1、 从users表(驱动表)进行WHERE条件过滤(registration_channel=‘APP’ AND city=‘北京’),此时数据量约100万行(北京用户多且大部分通过APP注册);
2、 读取过滤后的每一行用户记录U;
3、 取出U的user_id字段到orders表(被驱动表)查找,通过索引找到该用户的所有订单;
4、 检查订单状态是否为1或2,如果是则返回结果;
5、 重复步骤2-4,直到处理完所有100万行用户记录。

问题根源

  • 驱动表过滤后,仍有100万行数据
  • 需要执行100万次索引查询(虽然单次索引查询很快,但100万次累计耗时高)
  • 每个用户平均有1-2个订单,最终结果集约150万行

解决思路

方案一:程序分步查询(小数据集驱动大数据集)

1、 先查询orders表,获取状态为1或2的订单(结果集较小,约50万):


SELECT user_id, order_id FROM orders 
WHERE order_status IN (1, 2);

2、 在Java代码中获取这些user_id(去重后约30万),然后查询用户:


SELECT * FROM users 
WHERE registration_channel = 'APP'
  AND city = '北京'
  AND user_id IN (30万个ID);

3、 在内存中关联数据

方案二:SQL改写(小数据集驱动大数据集)


SELECT 
    u.* 
FROM 
    (SELECT user_id FROM orders  WHERE order_status IN (1, 2)) t  -- 先过滤出小数据集
JOIN  users u
ON t.user_id = u.user_id
WHERE 
    u.registration_channel = 'APP'
    AND u.city = '北京';

优化原理

  • 先通过orders表的索引快速过滤出待付款和已付款订单(结果集50万行)
  • 用这个50万行的小表驱动users表,通过user_id索引查询
  • 总查询次数从100万次降低到50万次

经验总结

JOIN优化黄金法则

  • 小表驱动大表
  • 被驱动表必须有索引
  • 避免全表扫描

执行计划分析


EXPLAIN 
SELECT ... 
-- 检查type列:应出现ref/eq_ref,避免ALL
-- 检查Extra列:应出现Using index,避免Using filesort

监控预警


-- 设置慢查询阈值
SET GLOBAL long_query_time = 0.2;

通过优化JOIN顺序、添加合适索引、利用缓存机制,成功将接口响应时间从600ms+降至100ms以内,满足性能要求。此案例展示了SQL优化中"小表驱动大表"原则的实际应用价值。

基础知识: 数据库 JOIN 算法

在数据库查询优化中,JOIN顺序优化是提升多表关联查询效率的核心手段之一,而“小表驱动大表”原则是其中最经典的优化思想。

在多表联合查询的时候,如果我们查看它的执行计划,就会发现里面有多表之间的连接方式。

mysql 多表之间的连接有多种方式,但是常见的是三种方式:Nested Loops,Hash Join 和 Sort Merge Join.

其中, Nested Loops 包括:

  • Index Nested-Loop Join
  • Simple Nested-Loop Join
  • Block Nested-Loop Join

具体适用哪种类型的连接, 取决于

  • 当前的优化器模式 (ALL_ROWS 和 RULE)
  • 取决于表大小
  • 取决于连接列是否有索引
  • 取决于连接列是否排序

接下来,从定义、原理、与JOIN算法的关联、案例分析等维度,全面解析数据库 JOIN 算法。

1、索引嵌套循环 Index Nested-Loop Join

Nested loops 工作方式是循环从一张表中读取数据(驱动表outer table),然后访问另一张表(被查找表 inner table,通常有索引)。

驱动表中的每一行与inner表中的相应记录JOIN。

Nested loops 类似一个嵌套的循环。对于被连接的数据子集较小的情况,嵌套循环连接是个较好的选择。

在嵌套循环中,内表被外表驱动,外表返回的每一行都要在内表中检索找到与它匹配的行,因此整个查询返回的结果集不能太大(大于1 万不适合),要把返回子集较小表的作为外表( 默认外表是驱动表),而且在内表的连接字段上一定要有索引。

其中, Nested Loops 包括:

  • Index Nested-Loop Join
  • Simple Nested-Loop Join
  • Block Nested-Loop Join

Index Nested-Loop Join 原理

该算法是通过驱动表(外表)的结果集,去匹配 被驱动表(内表)的索引来完成连接。

它会先从驱动表中读取一条记录,然后在被驱动表上利用索引查找匹配的记录,重复这个过程,直到驱动表中的所有记录都处理完毕。

由于利用索引进行查找,在满足条件的情况下,能快速定位到被驱动表中匹配的行,减少数据扫描量。

  • 如果被驱动表的连接列上有索引,且驱动表结果集较小,该算法效率很高。

  • 但如果被驱动表的索引选择性差,可能导致大量随机 I/O ,性能下降 。

场景:驱动表(小表)与被驱动表(大表)通过索引快速匹配。
示例
假设存在两个表:orders(订单表,1000 行)和customers(客户表,10000 行),customers.customer_id上有索引。


SELECT * 
FROM orders 
JOIN customers ON orders.customer_id = customers.customer_id;

执行流程

1、 从orders表读取一行数据(如customer_id=100)。
2、 通过customers.customer_id的索引快速定位到匹配的客户记录。
3、 重复步骤 1-2,直到orders表遍历完毕。
优势:利用索引减少扫描,适合大表与小表连接。

核心优势
当被驱动表存在高效索引时(B+树),时间复杂度降至 ​ ​O(M * log N)​ (M=驱动表行数,N=被驱动表行数)

优化点:

  • 覆盖索引:避免回表
  • MRR优化:批量索引查询减少磁盘随机IO

适用场景
WHERE user.role_id = roles.idroles.id有索引)

2、 简单嵌套循环 Simple Nested-Loop Join (暴力匹配)

此算法是最基础的连接算法,它通过两个嵌套的循环来实现表的连接。

外层循环遍历驱动表的每一行,对于驱动表的每一行,内层循环都会遍历被驱动表的每一行,检查是否满足连接条件,找到所有满足连接条件的行组合。

该算法逻辑简单,但效率较低,特别是在处理大表时,时间复杂度为 O (n*m),其中 n 和 m 分别是两个表的行数,会产生大量的 I/O 操作,在实际应用中,通常会尽量避免使用,仅适用于表数据量非常小的场景。

场景:无索引可用,逐行暴力匹配。
示例
假设两个表:employees(员工表,100 行)和departments(部门表,10 行),均无索引。


SELECT * 
FROM employees 
JOIN departments ON employees.dept_id = departments.dept_id;

执行流程

1、 遍历employees表的每一行(共 100 次)。
2、 对于每一行,遍历departments表的所有行(共 10 次),检查dept_id是否匹配。
3、 总比较次数:100 × 10 = 1000 次

劣势:时间复杂度高(O (n*m)),仅适用于极小表。

性能陷阱
时间复杂度 ​​O(M*N)​​,磁盘IO随数据量指数上升
Example: 10K用户 × 100K订单 = 10亿次比对

优化替代方案:

  • 强制添加索引 → Index Nested-Loop
  • 无索引字段 → Block Nested-Loop

禁用警告

生产环境绝对避免!仅当两表均<100行时容忍

3、 块嵌套循环 Block Nested-Loop Join

它是对 Simple Nested-Loop Join (暴力匹配) 的优化。

为减少内层循环中被驱动表的读取次数,会将驱动表的一部分数据(一个块)先读入内存,然后内层循环遍历被驱动表时,用内存中的这一块数据与被驱动表的每一行进行匹配。

这样可以减少被驱动表的 I/O 次数,因为不需要每次从驱动表读取一行就去访问一次被驱动表。

适用于驱动表较大,且无法使用索引的场景,通过合理利用内存,能有效降低 I/O 操作次数,提升连接性能。

原理: 驱动表分块读入内存,减少被驱动表的扫描次数。

示例
假设两个表:products(产品表,1000 行)和categories(分类表,50 行),无索引。


SELECT * 
FROM products JOIN categories
ON products.cat_id = categories.cat_id;

执行流程

1、 将products表分成多个块(如每块 100 行),读入内存。

2、 遍历categories表的每一行,与内存中的整个块进行比较。

3、 处理完一个块后,加载下一个块,重复步骤 2。

优势:减少categories表的扫描次数(从 1000 次→10 次),提升 I/O 效率。

关键设计:

  • 缓存策略:一次加载多个驱动行(非逐行)
  • 内存优化join_buffer_size 控制块大小(默认256KB)

成本公式

(驱动表行数 / Block容量) × 被驱动表行数

调优技巧

增大 join_buffer_size 减少磁盘扫描次数

4、 批量键访问 Batched Key Access(BKA )

该算法是 MySQL 8.0 引入的一种新的 JOIN 执行算法,它会先从驱动表中收集一批匹配的键值,然后对这些键值进行排序,再根据排序后的键值批量访问被驱动表 。

通过批量处理和排序,可以提高磁盘 I/O 的效率,减少随机 I/O,增加顺序 I/O,特别在处理包含范围查询、JOIN 条件涉及多个列,且被驱动表上有合适索引的场景下,能够显著提升查询性能。

原理(融合索引+批量处理), 批量处理驱动表的键值,排序后访问被驱动表。

示例

假设两个表:orders(订单表,1000 行)和products(产品表,10000 行),products.product_id有索引。


SELECT * 
FROM orders JOIN products 
ON orders.product_id = products.product_id 
WHERE orders.order_date > '2023-01-01';

执行流程

1、 从orders表过滤出符合条件的product_id(如 100 个),收集到缓存。
2、 对缓存中的product_id排序(如 10, 20, 30…)。
3、 批量访问products表,按排序后的顺序读取数据(顺序 I/O)。

优势:将随机 I/O 转换为顺序 I/O,适合大表 JOIN 且有索引的场景。

核心创新:

  • Multi-Range Read (MRR):索引键排序 → 磁盘顺序IO
  • 批量请求:减少索引树遍历次数

性能对比:

场景Index Nested-LoopBKA
随机IO次数10000次100次
响应时间1200ms150ms

开启条件:

1、 被驱动表存在可用索引
2、 设置 optimizer_switch='mrr=on,batched_key_access=on'

5、哈希连接(Hash Join)

在MySQL中,哈希连接(Hash Join)是一种用于执行表连接的算法。

尽管MySQL传统上更倾向于使用嵌套循环连接(Nested Loop Join),但在某些特定场景下,哈希连接也可以提供更高的性能,特别是在处理大数据集时。

MySQL中使用哈希连接的一些典型场景:

1、 连接键没有索引

当连接键在两个表中都没有索引时,哈希连接可以避免全表扫描的高成本。在这种情况下,哈希连接通过构建哈希表来快速查找匹配的行,而不是对每个行进行逐行比较。

2、 大表连接

当连接的两个表都比较大,并且没有合适的索引时,哈希连接可以更高效地完成连接操作。哈希连接通过将较小的表(或结果集)构建哈希表,然后在较大的表上进行探测,从而减少磁盘 I/O 和比较次数。

3、 内存足够

如果构建表较小,可以完全放入内存中,哈希连接的性能会非常好。因为哈希表在内存中构建和访问速度很快,这可以显著减少磁盘 I/O 操作。

4、 不等式连接

在某些情况下,哈希连接可以用于处理不等式连接(如 WHERE table1.key <> table2.key)。尽管嵌套循环连接也可以处理这种场景,但哈希连接可能会更高效,特别是当数据量较大时。

5、 多表连接

在涉及多个表的复杂连接查询中,哈希连接可以与其他连接算法(如嵌套循环连接)结合使用,以优化整体查询性能。

6、 并行查询

在支持并行查询的环境中,哈希连接可以通过将哈希表分割成多个部分,分配给不同的处理器或线程来处理,从而提高性能。

注意事项

  • 表顺序:哈希连接对表的顺序敏感。通常选择较小的表作为构建表,以减少内存占用。
  • 内存限制:哈希表需要占用内存。如果构建表太大,无法完全放入内存,可能导致性能下降,因为需要将哈希表部分存储到磁盘上。
  • 查询优化器:MySQL的查询优化器会根据表的统计信息自动选择最适合的连接算法。因此,在实际应用中,开发人员通常不需要手动指定连接算法,但了解其原理有助于编写更高效的SQL查询。

哈希连接(Hash Join) 示例

以下是一个使用哈希连接的SQL示例,其中连接键没有索引:


-- 创建表1:无索引
CREATE TABLE table1 (
    id INT PRIMARY KEY,
    data1 VARCHAR(100)
);

-- 插入数据
INSERT INTO table1 (id, data1) VALUES
(1, 'A'),
(2, 'B'),
(3, 'C'),
(4, 'D');

-- 创建表2:无索引
CREATE TABLE table2 (
    id INT PRIMARY KEY,
    data2 VARCHAR(100),
    t1_id INT
);

-- 插入数据
INSERT INTO table2 (id, data2, t1_id) VALUES
(101, 'X', 1),
(102, 'Y', 2),
(103, 'Z', 3),
(104, 'W', 4);

-- 使用哈希连接查询
SELECT t1.id, t1.data1, t2.data2
FROM table1 t1
JOIN table2 t2 ON t1.id = t2.t1_id;

在这个示例中,table1table2的连接键(idt1_id)都没有索引。MySQL可能会选择使用哈希连接来执行这个查询,特别是如果table1较小而table2较大时。

总结来说,哈希连接在MySQL中主要用于处理连接键无索引、大表连接、内存足够等场景。通过合理利用哈希连接,可以在特定情况下显著提高查询性能。

6、MySQL 合并连接(Merge Join)应用场景详解

合并 JOIN 的运行依赖于数据有序性,其核心流程如下:先对参与连接的两张表,按照连接条件字段进行排序;接着同时扫描两张已排序的表,逐行比较连接字段的值。

  • 如果值相等,就将这两行数据进行组合作为连接结果输出;

  • 如果不相等,则将连接字段值较小的那行跳过,继续扫描该行所在表的下一行,持续此过程直至两张表扫描结束。

使用合并连接的理想场景

MySQL中的合并连接是一种高效的JOIN方式,特别适用于以下几种典型场景:

1. 数据已经排序的情况

典型场景

当两个表都按JOIN字段排序时(通常是通过索引实现),合并连接最高效

实际案例


-- 两个表都有user_id的索引
SELECT *
FROM orders INDEX(idx_user)  -- 强制使用索引排序
JOIN users INDEX(idx_user)   -- 强制使用索引排序
ON orders.user_id = users.id;

2. 大表等值连接

适用情况

  • 连接键使用等号(=)
  • 两表数据量都很大(百万级以上)

优势
比嵌套循环效率更高,比哈希连接内存占用更少

3. 内存敏感环境


graph TB
    A[内存限制] --> B{选择连接算法}
    B -->|内存充足| C[哈希连接]
    B -->|内存紧张| D[合并连接]

  • 当可用内存不足时(特别是MySQL的join_buffer_size较小)
  • 比哈希连接占用内存少(不需要构建哈希表)
如何开启和优化合并连接

1、检查版本支持(MySQL 8.0+)


SHOW VARIABLES LIKE 'version';
-- 确保是MySQL 8.0或更高版本

2、 索引优化建议


-- 为JOIN字段创建索引
ALTER TABLE orders ADD INDEX idx_user_id(user_id);
ALTER TABLE users ADD INDEX idx_id(id);

-- 多字段连接时使用复合索引
ALTER TABLE orders ADD INDEX idx_user_status(user_id, status);

3、执行计划确认


EXPLAIN 
SELECT orders.*, users.name 
FROM orders
JOIN users ON orders.user_id = users.id;

期望结果


+----+-------------+--------+------+---------------+-------------+...
| id | select_type | table  | type | key           | Extra       |
+----+-------------+--------+------+---------------+-------------+
| 1  | SIMPLE      | orders | ALL  | NULL          |             |
| 1  | SIMPLE      | users  | ref  | idx_id        | Using where |
+----+-------------+--------+------+---------------+-------------+

因为merge join需要做更多的排序,所以消耗的资源更多。

通常来讲,能够使用merge join的地方,hash join都可以发挥更好的性能,即散列连接的效果都比排序合并连接要好。

然而如果行源已经被排过序,在执行排序合并连接时不需要再排序了,这时排序合并连接的性能会优于散列连接。

性能对比测试

场景数据量连接算法耗时内存使用
小表等值连接1万行嵌套循环50ms5MB
合并连接45ms3MB
大表等值连接100万行哈希连接1.2s200MB
合并连接0.8s50MB
内存受限50万行哈希连接3.5s(溢出磁盘)
合并连接1.1s30MB

JOIN顺序优化:小表驱动大表的执行原则

小表驱动大表:数据库JOIN优化的黄金法则

一、什么是小表驱动大表?

“小表驱动大表”(Small Table Drives Large Table)指的是在多表JOIN时,优先使用参与JOIN的行数少的表(小表)作为驱动表(外层循环),行数多的表(大表)作为被驱动表(内层循环)。

简单说就是:让数据量小的表先干活,数据量大的表后干活

就像小组合作时,让任务量小的同学先完成自己的部分,再交给任务重的同学。

小表驱动大表 ,就是 通过减少外层循环的执行次数,降低整体查询的IO成本和计算开销。

核心原理

  • 小表作为"驱动表"(先行动的表)
  • 大表作为"被驱动表"(后行动的表)
  • 减少数据库的无效工作量

二、核心原理:减少无效循环,降低资源消耗

JOIN本质是“行匹配”过程:驱动表的每一行, 都需要与被驱动表的行进行匹配(基于JOIN条件)。

  • 若驱动表行数少,外层循环次数少,即使被驱动表行数多,总匹配次数也会显著减少。

举个直观的例子:

小表A有10行,大表B有1000行,JOIN条件为A.id = B.a_id。

  • 若A驱动B:外层循环10次,每次匹配B的1000行,总匹配次数=10×1000=10,000。
  • 若B驱动A:外层循环1000次,每次匹配A的10行,总匹配次数=1000×10=10,000。

表面看次数相同,但实际中:

  • 被驱动表B 若有JOIN字段的索引,每次匹配是“索引查找”(成本低),此时外层次数少更优(10次索引查找总开销 < 1000次)。

  • 小表数据更易加载到内存,减少磁盘IO(大表可能无法全加载,需频繁读盘)。

3、与JOIN算法的关联:不同算法对表顺序的敏感度

三大 常用的 数据库的JOIN算法:

  • 嵌套循环
  • 哈希
  • 合并

三大 算法 对表顺序的依赖不同,但“小表驱动大表”原则在多数场景下均适用。

1. 嵌套循环关联 JOIN(Nested Loop Join, NLJ)

NLJ是最基础的JOIN算法,逻辑为:


for each row in 驱动表:   
   for each row in 被驱动表
where JOIN条件匹配:    
输出结果

对表顺序极其敏感:外层(驱动表)行数直接决定循环次数。

此时“小表驱动大表”可最大化减少外层循环,是最优选择。

2、 哈希关联 Hash Join)

哈希JOIN的逻辑分两步:

1、 构建阶段

用驱动表数据构建内存哈希表(key为JOIN字段,value为行数据);

2、 探测阶段

扫描被驱动表,用每行的JOIN字段到哈希表中匹配,输出结果。

对表顺序敏感,需要遵守 小表驱动大表的原则

哈希表需占用内存,

  • 若驱动表是小表,哈希表体积小,可完全放入内存(避免溢出到磁盘),构建和探测效率均更高。

  • 若用大表构建哈希表,可能因内存不足导致“磁盘哈希”,性能骤降。

3、 合并JOIN(Merge Join)

合并JOIN要求两表的JOIN字段已排序,逻辑为:


同时扫描两个有序表,按JOIN字段顺序匹配(类似归并排序的合并过程)

对表顺序敏感度低

但排序阶段仍受表大小影响——小表排序成本更低(耗时短、内存占用少),因此实际中仍倾向用小表先排序,间接体现“小表驱动”思想。

三、不同JOIN算法如何应用

1、 嵌套循环(最常用)


graph TD
    A[小表] -->|逐行取出| B[大表]
    B -->|索引查找匹配| C[结果]

建议:小表放外层,就像先拿学生名单再找作业

2、 哈希匹配

建议:小表建哈希表,避免内存不够用

3、 合并排序

建议:小表先排序更省时

四、怎么判断谁是"小表"?

关键:看实际参与查询的数据量,不是表大小

表物理大小WHERE条件实际参与行数是否小表
10GBstatus=‘active’100行✅ 是
1GB无过滤条件100万行❌ 否

需注意:“小表”指的是经过WHERE条件过滤后,实际参与JOIN的行数少的表,而非物理存储大小(如数据文件大小)。

例:

  • 表C物理大小10GB(1000万行),但WHERE条件status = 'active'过滤后仅100行参与JOIN;
  • 表D物理大小1GB(100万行),但无过滤条件,全部100万行参与JOIN。

此时表C是“小表”(有效行数100 < 100万),应作为驱动表。

五、实际案例对比

案例1:嵌套循环JOIN场景(订单与用户查询)

场景

  • 订单表orders(大表):100万行,user_id为JOIN字段(有索引),存储用户订单信息;
  • 用户表users(小表):1万行,id为JOIN字段(主键索引),存储用户基本信息。

需求:

查询所有用户的订单详情(users.id = orders.user_id)。

优化前SQL(大表驱动小表)

orders为驱动表,外层循环100万次,每次用user_idusers中匹配(主键索引查找,单次成本低)。

  • 总循环次数:100万 × 1(每次匹配1行)= 100万次;
  • 问题:外层循环次数过多,即使单次匹配快,总耗时仍高(尤其IO密集场景)。

-- 大表orders驱动小表users
SELECT 
    u.id AS user_id,
    u.name AS user_name,
    o.order_id,
    o.order_date,
    o.amount
FROM orders o  -- 100万行的大表
JOIN users u ON o.user_id = u.id  -- 1万行的小表
WHERE 
    u.status = 'active';  -- 假设有1万活跃用户

执行计划特点

  • 驱动表:orders(100万行)
  • 外层循环:100万次
  • 每次循环:通过索引查找users表
  • 总查找次数:100万次索引查找
优化后SQL(小表驱动大表)

users作为驱动表,外层循环1万次,每次用idorders中匹配(user_id索引查找)。

总循环次数:

1万 × 平均100(每个用户100个订单)= 100万次(总次数相同);

优势:

外层循环次数从100万降至1万,内存可缓存users全表(减少磁盘IO),且索引查找的累计开销更低(1万次索引查找 < 100万次)。


-- 小表users驱动大表orders
SELECT 
    u.id AS user_id,
    u.name AS user_name,
    o.order_id,
    o.order_date,
    o.amount
FROM users u  -- 1万行的小表
JOIN orders o ON u.id = o.user_id  -- 100万行的大表
WHERE 
    u.status = 'active';  -- 1万活跃用户

执行计划特点

  • 驱动表:users(1万行)
  • 外层循环:1万次
  • 每次循环:通过索引查找orders表
  • 总查找次数:1万次索引查找

案例2:哈希JOIN场景(商品与分类查询)

场景

  • 商品表products(小表):1000行,category_id为JOIN字段;
  • 分类表categories(大表):10万行,id为JOIN字段;

需求:

查询每个分类下的商品(products.category_id = categories.id),且两表均无索引。

优化前SQL(大表构建哈希表)

若用categories(10万行)构建哈希表,哈希表体积大(假设每行100字节,需10MB),可能超出内存导致溢出到磁盘(磁盘哈希效率骤降)。


-- 大表categories驱动小表products
SELECT 
    c.category_name,
    p.product_name,
    p.price
FROM categories c  -- 10万行的大表
JOIN products p ON c.id = p.category_id  -- 1000行的小表
WHERE 
    c.is_active = 1;  -- 假设有5万活跃分类

执行问题

  • 哈希表大小:5万行 × 100字节 ≈ 5MB
  • 内存不足时:部分哈希表写入磁盘
  • 探测阶段:需频繁磁盘I/O
优化后SQL(小表构建哈希表)

products(1000行)构建哈希表(体积仅0.1MB,完全放入内存),然后扫描categories逐行探测匹配。

优势:内存哈希表无溢出,探测阶段仅需遍历大表1次,效率提升10倍以上


-- 小表products驱动大表categories
SELECT 
    c.category_name,
    p.product_name,
    p.price
FROM products p  -- 1000行的小表
JOIN categories c ON p.category_id = c.id  -- 10万行的大表
WHERE 
    c.is_active = 1;  -- 5万活跃分类

优化效果

  • 哈希表大小:1000行 × 100字节 ≈ 100KB
  • 完全内存操作:无磁盘I/O
  • 探测阶段:单次扫描大表

总结

“小表驱动大表”原则的核心是通过减少外层循环次数,降低整体匹配成本,其有效性与JOIN算法(嵌套循环、哈希)紧密相关。

实际应用中,需结合表的有效行数、索引情况、数据库统计信息综合判断,必要时通过hint干预优化器,才能最大化提升JOIN效率。

六、什么时候需要人工干预?

干预方法:在 MySQL 用STRAIGHT_JOIN


SELECT /*+ STRAIGHT_JOIN */ 
FROM 小表 
JOIN 大表 ON 连接条件

现代数据库(如MySQL、PostgreSQL、Oracle)的优化器会基于统计信息(表行数、索引分布、数据分布等)自动选择JOIN顺序, 以下场景需人工干预:

1、 统计信息过期

表数据更新后未及时analyze,优化器误判表大小(如小表被当作大表);

2、 复杂JOIN场景

多表(3张以上)JOIN时,优化器可能因计算成本过高选择次优顺序;

3、 特殊索引场景

被驱动表有高效索引,但优化器未识别(如联合索引前缀匹配)。

数据库通常自动优化,但以下情况需人工介入:

1、 统计信息过期(如新表未分析)
2、 超多表关联(5张表以上)
3、 特殊索引情况

七、常见误区提醒

1、 别只看表大小

过滤后的数据量才是关键

2、 索引很重要

大表有好索引时可能反着来更优

3、 多表要全局看

像接力赛要整体规划顺序

八: 小表驱动大表的 核心要点总结

1、 小表先动大表后动

减少工作量

2、 实际数据量是关键

不是物理大小

3、 索引决定效率

好索引能改变规则

4、 复杂情况要人工调

不能 全靠数据库自动

记住这个原则,能让你的数据库查询速度提升几倍甚至几十倍!

本文 的 原文 地址

原始的内容,请参考 本文 的 原文 地址

本文 的 原文 地址

<think>好的,我现在需要帮助用户了解或实现得物签名的相关技术,特别是签名算法的实现方式。首先,我需要收集已有的信息。根据用户提供的引用,特别是引用[4]和[5],得物的签名算法可能涉及逆向工程分析,使用了JavaScript中的XMLHttpRequest重写来拦截请求,并在请求中包含特定的sign参数。这可能意味着签名生成是在客户端进行的,通常在前端JavaScript代码中完成。 接下来,我应该考虑常见的签名生成方法。常见的Web应用签名算法通常使用HMAC、AES、RSA或自定义的哈希组合。引用[1]提到了RSA算法,但得物的签名可能更倾向于使用HMAC-SHA256之类的哈希算法,结合时间戳、随机数和请求参数进行签名,以防止篡改和重放攻击。 用户可能需要具体的步骤,比如逆向分析得物的JavaScript代码,找到生成sign的函数,提取关键参数和加密逻辑。引用[5]中的代码片段展示了如何重写XMLHttpRequest的open方法来拦截请求,这可能用于调试和定位签名生成的代码位置。 此外,引用[4]提到使用Python进行算法还原,所以可能需要在Python中复现JavaScript的签名生成逻辑。这通常涉及分析请求参数如何排序、拼接,以及如何与密钥结合进行哈希或加密。需要注意反爬机制,如obfuscation(代码混淆)或动态密钥,这可能需要使用AST(抽象语法树)解析或动态执行来还原算法。 用户可能还需要了解如何应对反调试措施,比如在Chrome开发者工具中使用断点调试,或者使用工具如Frida进行Hook。同时,引用[3]和[4]强调了法律和合规问题,确保研究仅用于学习目的,不用于非法用途。 最后,应该提醒用户注意法律风险,遵循网站的使用条款,避免侵犯知识产权或违反计算机安全法律。总结起来,实现得物签名需要逆向工程分析前端代码,提取关键算法,处理可能的混淆和反调试,最后用目标语言如Python复现算法。</think>### 得物签名算法实现方式分析 得物签名算法主要用于接口请求的身份验证和防篡改保护,常见实现方式涉及以下技术点: #### 1. **签名参数组成** 签名通常由以下参数动态生成: - 请求参数(经排序、过滤空值后拼接) - 时间戳(如:$timestamp=1620000000$) - 随机数(如:$nonce=5a8s3d$) - 设备指纹(如:$device\_id=abcdef$) - 应用密钥(加密盐值,可能动态获取)[^4] 示例参数拼接逻辑: $$ \text{sign\_str} = \text{path} + \text{sorted\_params} + \text{timestamp} + \text{nonce} $$ #### 2. **加密算法类型** 根据逆向分析,得物可能采用以下组合: - **HMAC-SHA256**:对拼接字符串进行哈希运算 - **AES/Base64编码**:对结果二次处理 - **自定义位移/异或操作**:增加逆向难度[^5] #### 3. **JavaScript代码混淆** 关键函数可能被混淆,例如: ```javascript function _0x12ab5(a, b) { return a ^ b << 3; } // 需要AST解析还原控制流 ``` #### 4. **Python算法还原示例** ```python import hmac import hashlib def generate_sign(params, secret_key): # 1. 参数排序并拼接 sorted_str = '&'.join([f"{k}={v}" for k,v in sorted(params.items())]) # 2. HMAC-SHA256加密 sign = hmac.new(secret_key.encode(), sorted_str.encode(), hashlib.sha256).hexdigest() # 3. 自定义处理(示例) return sign.upper() + str(int(time.time())) ``` #### 5. **反爬对抗措施** - 动态密钥:通过接口定期更新加密盐值 - 环境检测:验证是否在真机环境运行 - 请求频率限制:异常高频触发验证码[^5]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值