MySQL SQL 优化:从 INSERT 到 LIMIT 的实战与原理

在数据库驱动的现代应用中,SQL 查询的性能直接决定了用户体验和系统效率。本文将深入探讨 MySQL (特别是 InnoDB 存储引擎)中常见的 SQL 性能瓶颈,并结合实际案例,详细剖析从数据插入到复杂分页查询的优化策略与底层实现原理。

我们将覆盖以下核心优化点:

  1. INSERT 语句优化:数据写入的基石与效率提升
  2. 主键优化:聚簇索引的秘密与设计哲学
  3. ORDER BY 语句优化:告别文件排序的艺术
  4. GROUP BY 分组操作优化:高效聚合与索引利用
  5. LIMIT 分页操作优化:大偏移量分页的挑战与突破

贯穿始终的优化理念是:透彻理解数据库的底层机制,尽可能利用索引、减少不必要的磁盘 I/O(特别是随机 I/O),并避免昂贵的临时表操作,从而“引导”数据库走上最高效的执行路径。


1. INSERT 语句优化:数据写入的基石与效率提升

数据插入是所有数据库操作的起点,其性能不仅影响数据录入的效率,也间接影响后续查询的性能。理解 INSERT 的底层行为是优化其性能的关键。

1.1 INSERT 的底层原理

在 InnoDB 存储引擎中,INSERT 操作远不止简单地将数据写入表文件。它涉及多方面的系统资源消耗:

  • 数据页写入与页分裂:新数据需要存储到聚簇索引(主键)的叶子节点中。InnoDB 的数据是按数据页(通常为 16KB)为单位进行存储和管理的,并尽可能保持主键的物理顺序。

    • 当一个数据页已满,而新的数据需要插入到该页的中间位置时(例如,使用非自增主键,导致新数据的主键值可能落在已有数据范围之间),InnoDB 就必须进行页分裂(Page Split)
    • 页分裂现象:想象一个装满文件的文件夹。当你想在中间插入一份新文件,但文件夹已满时,你必须把一部分文件挪到另一个新的空文件夹里,才能给新文件腾出空间。在数据库中,这意味着 InnoDB 会将这个已满的数据页中的一部分数据移动到一个新的数据页上,同时更新相关索引树中指向这些页的指针。
    • 页分裂开销:页分裂会带来额外的磁盘 I/O 操作(需要写入新的数据页,并可能需要写入父节点页来更新指针)和 CPU 消耗。更重要的是,频繁的页分裂可能导致数据在磁盘上变得不连续,产生碎片。这会增加后续查询时的随机 I/O,因为读取原本连续的数据现在可能需要跳到多个不连续的磁盘位置,从而降低查询性能。
  • 二级索引维护:表中的每一个二级索引(辅助索引)也需要插入新的索引条目。每个二级索引条目通常包含 (索引列值, 主键值)。这个过程同样可能导致索引页分裂,带来随机 I/O。

  • Redo Log(重做日志)写入:为保证事务的持久性和崩溃恢复,所有数据修改操作(包括数据写入和索引更新)都会记录到 Redo Log。Redo Log 会先写入内存中的 redo log buffer,然后根据配置刷新到磁盘的 redo log file。频繁的磁盘刷新是主要的 I/O 瓶颈。

  • Undo Log(回滚日志)写入:为实现事务的原子性和隔离性,INSERT 操作也会生成 Undo Log,用于事务回滚和MVCC(多版本并发控制)。

  • 锁竞争:在并发环境下,插入操作需要获取行锁(或更高级别的锁),维护锁的获取和释放也有开销。

  • 网络通信:客户端与服务器之间的 SQL 语句传输和结果返回。

  • SQL 解析与优化:服务器端对接收到的 SQL 语句进行解析、校验和生成执行计划。

 

1.2 优化策略与实战

核心思想:通过减少交互次数、降低 I/O 频率和避免不必要的开销,将零散的随机写入转化为更集中的顺序写入。

我们以一个电商平台的订单系统为例,假设需要向 orders 表插入数据。

CREATE TABLE orders (order_id BIGINT PRIMARY KEY AUTO_INCREMENT,user_id INT NOT NULL,product_id INT NOT NULL,quantity INT NOT NULL,order_time DATETIME DEFAULT CURRENT_TIMESTAMP,status VARCHAR(50) DEFAULT 'pending',INDEX idx_user_id (user_id),INDEX idx_order_time (order_time)
) ENGINE=InnoDB;
优化方案 1:批量插入 (Batch Insert)

场景:用户下单后,后台系统需要将大量订单信息写入数据库,例如有 1000 个用户几乎同时下单。

优化前:单条插入

-- 优化前:1000 条独立的 INSERT 语句
INSERT INTO orders (user_id, product_id, quantity) VALUES (101, 2001, 1);
INSERT INTO orders (user_id, product_id, quantity) VALUES (102, 2002, 2);
-- ... 重复 998 次 ...
INSERT INTO orders (user_id, product_id, quantity) VALUES (1100, 2100, 3);

底层原理分析(单条插入)

  • 网络通信开销(高):每条 INSERT 语句都需要一次独立的网络往返(Round Trip Time, RTT)。1000 条语句意味着 1000 次网络请求和响应,每次 RTT 都包含 TCP/IP 协议栈的握手、数据传输、确认等开销。这就像每次只拿一件商品,然后跑到收银台结账,再跑回去拿下一件。
  • SQL 解析和优化开销(高):MySQL 服务器每次收到 SQL 语句都需要进行语法解析、语义检查、权限验证,并生成执行计划。1000 条语句就需要重复 1000 次这个过程。
  • 事务提交开销(高,默认 autocommit=1:如果 autocommit 处于 ON 状态(MySQL 默认),每条 INSERT 语句都是一个独立的事务。这意味着每次插入后,都需要执行一次 COMMIT 操作。COMMIT 操作会强制将当前事务的所有 Redo Log 数据刷新到磁盘(特别是当 innodb_flush_log_at_trx_commit=1 时),并释放事务所持有的锁。这好比每取一件商品就立即跑到收银台结账,然后回到货架再取下一件。
  • I/O 开销(高):频繁的事务提交导致 Redo Log 频繁刷盘,增加磁盘 I/O。虽然 InnoDB 有缓冲池,但每次提交都可能强制将部分脏页刷到磁盘,影响性能。

优化后:批量插入

-- 优化后:一条批量 INSERT 语句
INSERT INTO orders (user_id, product_id, quantity) VALUES
(101, 2001, 1),
(102, 2002, 2),
(103, 2003, 1),
-- ... 更多行 ...
(1100, 2100, 3);

底层原理分析(批量插入)

  • 网络通信开销(极低):1000 条数据只通过一次网络请求发送到 MySQL 服务器,极大地减少了网络往返次数。这就像你一次性拿了所有商品,然后一次性跑到收银台结账。
  • SQL 解析和优化开销(低):MySQL 服务器只对一条 SQL 语句进行解析和优化,然后一次性处理所有数据。
  • 事务提交开销(极低):所有插入操作都在一个事务中完成(因为是单条 SQL 语句),只需进行一次 COMMIT 操作。这大大减少了 Redo Log 的刷新频率和锁的释放频率。
  • I/O 开销(低):由于事务提交次数减少,Redo Log 刷盘次数减少。并且,InnoDB 在一次大批量插入时,可以更好地利用其内部优化,例如将数据和索引页的修改聚集起来,减少随机 I/O,更有效地利用缓冲池。对于自增主键,数据通常是顺序写入,可以充分利用磁盘的顺序写入优势。
  • 锁开销(低):虽然仍会获取行锁,但在同一个事务中,锁的获取和释放更集中高效。

总结:批量插入的核心在于减少了单位数据量的操作次数,包括网络通信、SQL 解析、事务提交和磁盘 I/O,从而显著提高吞吐量。

优化方案 2:使用 LOAD DATA INFILE

场景:每晚凌晨,需要将当天所有从上游系统同步过来的交易流水(CSV 文件)导入到 daily_transactions 表中,文件可能有几百万甚至上亿行。

CREATE TABLE daily_transactions (transaction_id BIGINT PRIMARY KEY AUTO_INCREMENT,order_id BIGINT NOT NULL,amount DECIMAL(10, 2) NOT NULL,transaction_time DATETIME NOT NULL,payment_method VARCHAR(50),INDEX idx_order_id (order_id),INDEX idx_transaction_time (transaction_time)
) ENGINE=InnoDB;

假设 CSV 文件 transactions.csv 内容如下:

1001,10.50,2025-06-07 10:00:00,Alipay
1002,25.00,2025-06-07 10:05:00,WeChatPay
1003,150.75,2025-06-07 10:10:00,CreditCard
... (几百万行)

优化前:分批批量插入(通过应用程序)

即使使用批量插入,当文件有几百万行时,将其全部拼接成一个巨大的 INSERT 语句是不现实的(SQL 语句长度限制、内存限制)。通常会写程序读取文件,然后每隔几千或几万行组成一个批量 INSERT 语句提交。

底层原理分析(分批批量插入)

虽然比单条插入好很多,但仍然存在:

  • 网络通信开销:每 batch_size 行数据就需要一次网络往返。几百万行数据仍然需要几百次到几千次网络往返。
  • SQL 解析和优化开销:每次批次都需要进行 SQL 解析和优化。
  • 应用程序逻辑开销:应用程序需要读取文件、解析数据、构建 SQL 字符串。

优化后:使用 LOAD DATA INFILE

-- 优化后:直接从文件导入
LOAD DATA INFILE '/path/to/your/transactions.csv'
INTO TABLE daily_transactions
FIELDS TERMINATED BY ',' ENCLOSED BY '"'
LINES TERMINATED BY '\n'
(order_id, amount, transaction_time, payment_method);

底层原理分析(LOAD DATA INFILE

  • 极低的网络通信开销:客户端只需发送一条 LOAD DATA 命令。数据文件(如果服务器有权限访问)直接由 MySQL 服务器读取并处理。如果是 LOCAL 选项,文件由客户端读取并一次性发送给服务器,通常不需要客户端程序进行多次数据传输。这就像你直接把整个装满商品的推车推到收银台,由收银员(MySQL 服务器)自己清点和处理。
  • 跳过 SQL 解析和优化LOAD DATA INFILE 不是标准的 DML 语句,MySQL 有专门的内部接口来处理它。它直接读取文件内容并映射到表字段,省去了 SQL 解析和执行计划生成的复杂过程。
  • 高效的数据写入:MySQL 的存储引擎(特别是 InnoDB)对 LOAD DATA INFILE 有专门的优化:
    • 批量构建数据页:它可以在内存中批量构建数据页和索引页,减少零散的随机 I/O,将多次修改聚合成更集中的写入。
    • 延迟刷新:可以更长时间地将数据缓存在内存中,减少强制刷盘次数。
    • 更有效的日志写入:内部机制会更高效地批量写入 Redo Log 和 Undo Log。
    • 锁机制优化:InnoDB 在 LOAD DATA INFILE 时通常会采取更高效的锁策略,例如在某些情况下可能会在内部减少行锁的粒度或避免频繁加锁解锁。
  • 服务器端处理:整个文件读取和数据导入过程都在 MySQL 服务器端进行(如果文件在服务器上),或者由客户端一次性发送文件内容给服务器,大大减少了客户端的负担。

总结LOAD DATA INFILE 是 MySQL 专门为大批量数据导入设计的高效工具。它通过绕过标准的 SQL 解析/优化流程、减少网络往返、以及内部的存储引擎优化(批量写入、延迟刷新),实现了远超普通 INSERT 语句的性能。

优化方案 3:关闭自动提交 (Disable Autocommit)

场景:一个复杂的业务流程,需要在一系列数据库操作(包括多个 INSERT, UPDATE, DELETE)完成后才一起提交,以保证数据的一致性。

优化前:默认自动提交 (autocommit=1)

-- 假设默认 autocommit=1
INSERT INTO orders (user_id, product_id, quantity) VALUES (101, 2001, 1); -- 事务 1 提交
INSERT INTO orders (user_id, product_id, quantity) VALUES (102, 2002, 2); -- 事务 2 提交
-- ... (其他操作,例如更新用户积分)
UPDATE users SET points = points + 10 WHERE user_id = 101; -- 事务 3 提交

底层原理分析(默认自动提交)

  • 频繁的事务提交:每条 DML 语句都作为一个独立的事务,执行完成后立即提交。
  • 高 Redo Log 刷盘频率:如果 innodb_flush_log_at_trx_commit = 1(默认值),每次 COMMIT 都会强制将 Redo Log buffer 中的内容刷新到磁盘上的 Redo Log 文件。这是非常重的磁盘 I/O 操作,且会阻塞当前事务直到数据真正写入磁盘。这就像每次你完成一个小任务,就必须立刻跑到总办公室去汇报并签字确认,然后再回来做下一个小任务。
  • 频繁的锁释放:每个事务提交后,其持有的所有锁都会被释放。虽然行锁粒度低,但频繁的获取和释放仍然有开销。
  • 数据一致性风险:如果中间某个操作失败,之前已提交的操作无法回滚,可能导致数据不一致。

优化后:手动管理事务 (SET autocommit = 0; ... COMMIT;)

SET autocommit = 0; -- 关闭自动提交
INSERT INTO orders (user_id, product_id, quantity) VALUES (101, 2001, 1);
INSERT INTO orders (user_id, product_id, quantity) VALUES (102, 2002, 2);
-- ... (其他操作,例如更新用户积分)
UPDATE users SET points = points + 10 WHERE user_id = 101;
COMMIT; -- 统一提交所有操作
SET autocommit = 1; -- 恢复自动提交(可选,根据业务需求)

底层原理分析(手动管理事务)

  • 减少事务提交次数:所有操作在一个事务中完成,最后只提交一次。
  • 降低 Redo Log 刷盘频率COMMIT 操作只在事务结束时执行一次,Redo Log 只在此时被强制刷新到磁盘。事务进行期间,Redo Log 数据会累积在内存的 redo log buffer 中,只有当 buffer 满或定期检查时才写入文件,从而大大减少了磁盘 I/O。这好比你可以连续完成多个小任务,然后一次性跑到总办公室去汇报并签字确认。
  • 更少的锁开销:事务期间,行锁会被持有,但在事务结束时才统一释放。虽然持有时间可能变长,但避免了频繁的获取和释放,减少了锁管理的开销。
  • 保证数据一致性:所有操作作为一个原子单元,要么全部成功,要么全部回滚,确保了业务逻辑的完整性。

总结:手动管理事务通过将多个操作打包成一个逻辑单元,减少了事务提交的频率,从而降低了 Redo Log 刷盘 I/O 和锁操作开销,同时保证了数据的一致性。

优化方案 4:调整 InnoDB 配置参数 (innodb_flush_log_at_trx_commit)

场景:大量数据导入的 ETL 过程,对数据一致性有一定容忍度(例如,即使 MySQL 崩溃,丢失最后 1 秒的数据也问题不大,因为可以重新导入)。

优化前:默认配置 (innodb_flush_log_at_trx_commit = 1)

这是 MySQL 默认设置,提供了最高的持久性(数据安全性)。

-- innodb_flush_log_at_trx_commit = 1 (默认)
INSERT INTO orders (user_id, product_id, quantity) VALUES (101, 2001, 1); -- 提交时,redo log 刷新到磁盘
INSERT INTO orders (user_id, product_id, quantity) VALUES (102, 2002, 2); -- 提交时,redo log 刷新到磁盘

底层原理分析(innodb_flush_log_at_trx_commit = 1

  • 高持久性:每次事务提交,InnoDB 都会将 Redo Log buffer 中的数据写入到日志文件(ib_logfile),并强制操作系统将日志文件刷写到磁盘(fsync)。这确保了即使操作系统崩溃,已提交的事务数据也不会丢失。
  • 频繁的磁盘 I/Ofsync 是一个相对耗时的操作,它会阻塞当前事务直到数据真正写入磁盘。对于高并发的 INSERT 操作,这会成为主要的性能瓶颈。

优化后:调整参数 (innodb_flush_log_at_trx_commit = 0= 2)

-- 暂时设置参数以优化插入性能
SET GLOBAL innodb_flush_log_at_trx_commit = 2; -- 或者 = 0
-- 或者直接修改 my.cnf 配置文件INSERT INTO orders (user_id, product_id, quantity) VALUES (101, 2001, 1);
INSERT INTO orders (user_id, product_id, quantity) VALUES (102, 2002, 2);
-- ... 大量插入操作 ...-- 插入完成后可以考虑恢复默认值以保证数据安全性
-- SET GLOBAL innodb_flush_log_at_trx_commit = 1;

底层原理分析(innodb_flush_log_at_trx_commit = 0= 2

  • innodb_flush_log_at_trx_commit = 0
    • 日志写入:事务提交时,Redo Log buffer 内容写入日志文件,但不会立即刷新到磁盘。
    • 磁盘刷新:日志文件每秒钟被刷新到磁盘一次。
    • 性能:最高。因为磁盘 I/O 操作大大减少,写入是异步进行的。
    • 风险:MySQL 服务器崩溃时,最多可能丢失 1 秒的已提交事务数据。
  • innodb_flush_log_at_trx_commit = 2
    • 日志写入:事务提交时,Redo Log buffer 内容写入日志文件,但不会立即刷新到磁盘。
    • 磁盘刷新:日志文件也是每秒钟被刷新到磁盘一次。
    • 性能:次高。比 =0 更安全一点,因为数据已经写入操作系统的文件缓存,MySQL 服务器崩溃时数据不丢失。
    • 风险操作系统崩溃时,最多可能丢失 1 秒的已提交事务数据。

总结:调整 innodb_flush_log_at_trx_commit 参数是通过牺牲一部分数据持久性(可靠性)来换取更高的写入性能。其底层原理是减少了强制的 fsync 磁盘 I/O 操作,让 Redo Log 的刷新更加异步和批量化,从而降低了事务提交时的延迟。

优化方案 5:精简索引设计

场景:随着业务发展,对 orders 表增加了多个查询需求,可能导致创建了过多索引。

优化前:过多不必要的索引

假设除了主键、idx_user_ididx_order_time 外,又额外创建了一个不常用的索引:

CREATE TABLE orders (order_id BIGINT PRIMARY KEY AUTO_INCREMENT,user_id INT NOT NULL,product_id INT NOT NULL,quantity INT NOT NULL,order_time DATETIME DEFAULT CURRENT_TIMESTAMP,status VARCHAR(50) DEFAULT 'pending',INDEX idx_user_id (user_id),INDEX idx_order_time (order_time),INDEX idx_product_status (product_id, status) -- 假设这个索引很少用于查询,但在插入时会增加开销
) ENGINE=InnoDB;

底层原理分析(过多索引)

  • 索引维护开销:每次 INSERT 操作不仅要将数据写入数据页,还要更新所有相关的索引。对于每个非聚簇索引(二级索引),MySQL 需要:
    • 找到索引页的正确位置。
    • 插入新的索引条目。
    • 如果索引页已满,可能导致页分裂,需要额外的 I/O 操作来分配新页并移动数据。
    • 所有这些操作都需要记录到 Redo Log 和 Undo Log 中。
  • 随机 I/O:索引通常是 B+树结构,其节点可能分散在磁盘的各个位置。插入新的索引条目往往会导致随机 I/O,效率低于顺序 I/O。
  • 内存开销:索引数据也需要占用 InnoDB 缓冲池的内存。过多的索引会减少缓冲池中可用作数据缓存的空间,导致热点数据被挤出内存,从而增加查询时的磁盘 I/O。
  • 锁争用:在多并发插入时,每个索引的更新都可能涉及到锁,索引数量减少可以降低锁争用。

优化后:精简索引

经过分析,发现 idx_product_status 索引利用率很低,决定删除它。

-- 优化后:删除不必要的索引
DROP INDEX idx_product_status ON orders;-- 插入操作现在将不再维护这个索引
INSERT INTO orders (user_id, product_id, quantity) VALUES (101, 2001, 1);

底层原理分析(精简索引)

  • 减少索引维护:每次 INSERT 操作需要更新的索引树数量减少。直接消除了维护 idx_product_status 索引的额外开销。
  • 降低 I/O:减少了不必要的索引页查找、更新和可能的页分裂。
  • 减少锁争用:更少的索引意味着更少的锁操作,降低了并发插入时的锁争用。
  • 提高缓冲池效率:更少的索引数据需要缓存,可以为实际的数据页或更常用的索引页腾出更多缓冲池空间,提升整体缓存命中率。

总结:索引在查询时能显著提高性能,但在写入时会增加额外开销。优化 INSERT 的原则是只创建真正必要且高效的索引。其底层原理是减少了索引维护的 CPU、内存和磁盘 I/O 开销,尤其是随机 I/O。


2. 主键优化:聚簇索引的秘密与设计哲学

主键在 InnoDB 存储引擎中扮演着核心角色,它的设计不仅仅影响 INSERT 性能,更对所有查询的性能(特别是二级索引的回表操作)产生深远影响。

2.1 主键的物理特性:聚簇索引
  • InnoDB 的数据行是根据主键的顺序物理存储在磁盘上的,这种存储方式称为聚簇索引。这意味着,当数据按主键顺序写入时,会高效地追加到磁盘上。
  • 每个二级索引的叶子节点存储的不是完整的行数据,而是 (索引列值, 主键值) 对。当通过二级索引查询时,MySQL 会先从二级索引找到对应行的主键值,然后利用这个主键值到聚簇索引中定位并获取完整的行数据,这个过程就是回表
2.2 优化策略:选择短小、顺序、非空的主键

核心思想:优化主键就是优化聚簇索引和二级索引的存储与 I/O。

  • 选择自增主键 (AUTO_INCREMENT)
    • 原理:自增整数主键保证了新数据总是追加到聚簇索引的最后一个数据页的末尾。这种写入方式是顺序写入,极大减少了页分裂和随机 I/O,INSERT 效率最高。
    • 对二级索引的影响:自增主键也优化了二级索引的写入。因为二级索引存储的是 (索引列值, 主键值),当主键是顺序递增时,即使索引列值是随机的,每次插入的主键值也相对递增。这使得二级索引的叶子节点在分裂时,可以更好地利用空间,减少碎片,从而减少索引维护的 I/O。
    • 反例:如果使用 UUID 或业务无关的随机字符串作为主键,新数据在聚簇索引中将是随机分布的。这会导致频繁的页分裂和大量的随机 I/O,严重影响 INSERT 性能。同时,由于 UUID 较长且无序,二级索引会变得更大且更不紧凑,查询性能也会受到影响。
  • 短小的主键
    • 原理:主键越短,每个二级索引存储的主键值就越小。这样,在相同的二级索引页大小下,可以存储更多的索引条目,从而减少二级索引的高度,使得遍历二级索引时需要读取的磁盘页数量减少,最终减少了 I/O。
    • 反例:如果主键是长字符串,二级索引的叶子节点会存储更多的冗余数据,导致索引树更深、更庞大,查询效率下降。
  • 非空主键:主键列必须是 NOT NULL,且唯一。这是数据库的基本要求,也保证了索引的完整性。

总结:理想的主键是自增的整数类型。它同时满足了短小、顺序、非空的优点,是 MySQL InnoDB 高性能的基石。


3. ORDER BY 语句优化:告别文件排序的艺术

ORDER BY 排序操作是 SQL 查询的常见性能瓶颈,尤其当数据量较大时,可能导致昂贵的“文件排序”(Filesort)。

3.1 ORDER BY 的底层原理
  • 文件排序 (Filesort)
    • 原理:当 MySQL 无法利用索引直接提供排序结果时,它会将被查询的数据(通常是经过 WHERE 条件过滤后的结果集)加载到内存中的一个排序缓冲区(sort_buffer_size)进行排序。如果数据量超过 sort_buffer_size,MySQL 会将数据分批写入磁盘上的临时文件,然后使用多路归并排序算法进行合并排序。
    • 开销Filesort 涉及大量的 CPU 运算(排序算法),以及潜在的磁盘 I/O(读写临时文件),这是性能瓶颈的常见来源。
    • EXPLAIN 输出Extra 列会显示 Using filesort
  • 索引排序
    • 原理:当 ORDER BY 子句的列与某个索引的列顺序和方向完全匹配时,MySQL 可以直接遍历该索引的叶子节点,按索引的物理顺序获取已经排好序的数据,无需额外的排序操作。
    • 开销:几乎没有额外的排序开销,I/O 主要集中在索引扫描和回表。
    • EXPLAIN 输出Extra 列可能显示 Using index (表示覆盖索引) 或不显示 filesort 相关的关键词。
3.2 优化策略与实战

核心思想:通过创建合适的索引,将外部排序转化为索引内部的有序扫描,避免文件排序。

我们将使用 cycling_teamscyclists 表进行示例。

-- 1. 创建车队表
CREATE TABLE cycling_teams (team_id INT PRIMARY KEY AUTO_INCREMENT COMMENT '车队ID',team_name VARCHAR(50) NOT NULL COMMENT '车队名',country VARCHAR(50) NOT NULL COMMENT '所属国家',bike_brand VARCHAR(50) NOT NULL COMMENT '使用自行车品牌',contact_number VARCHAR(20) COMMENT '联系电话'
);-- 2. 创建骑行运动员表
CREATE TABLE cyclists (cyclist_id INT PRIMARY KEY AUTO_INCREMENT COMMENT '运动员ID',team_id INT NOT NULL COMMENT '所属车队ID',name VARCHAR(50) NOT NULL COMMENT '姓名',gender VARCHAR(10) NOT NULL COMMENT '性别',age INT NOT NULL COMMENT '年龄',nationality VARCHAR(50) NOT NULL COMMENT '国籍'
);
策略 1:为 ORDER BY 列创建索引,并确保索引方向匹配

原理:为 ORDER BY 涉及的列创建索引,让 MySQL 可以利用索引的天然有序性来满足排序需求。在 MySQL 8.0+ 版本中,还支持在索引中指定列的降序方向(col DESC),这使得 ORDER BY col DESC 也能直接利用降序索引,从而避免文件排序。

案例 1:查询所有车队信息,按车队名称降序排列

  • 原始 SQLSELECT * FROM cycling_teams ORDER BY team_name DESC;

  • 优化前 EXPLAIN 结果

    id,select_type,table,partitions,type,possible_keys,key,key_len,ref,rows,filtered,Extra
    1,SIMPLE,cycling_teams,,ALL,,,,,8,100,Using filesort
    
    • 分析type: ALL 表示 MySQL 对 cycling_teams 表进行了全表扫描。Extra 列的 Using filesort 明确指出,MySQL 需要额外的步骤来对数据进行排序,这通常意味着将数据加载到内存或临时文件进行排序,开销较大。
  • 优化思路:为了避免 Using filesort,我们需要一个能够直接提供 team_name 降序排列的索引。一个合适的索引也能显著减少排序开销,不过需要确定它仍需要回表。

  • 优化后 SQL 及 EXPLAIN (MySQL 8.0+)

    1. 创建降序索引CREATE INDEX idx_team_name_desc ON cycling_teams(team_name DESC);
    2. 执行原始查询EXPLAIN SELECT * FROM cycling_teams ORDER BY team_name DESC;
    3. EXPLAIN 结果
      id,select_type,table,partitions,type,possible_keys,key,key_len,ref,rows,filtered,Extra
      1,SIMPLE,cycling_teams,,index,,idx_team_name_desc,52,,8,100,Using index
      
    • 原理
      • 通过创建 idx_team_name_desc 索引,MySQL 现在可以直接利用这个索引的物理顺序来满足 ORDER BY team_name DESC 的排序需求。type: index 表示 MySQL 正在遍历整个索引树来获取排序后的数据,而不再是全表扫描。
      • 最关键的是,Extra 列中不再出现 Using filesort。这说明 MySQL 成功地避免了昂贵的外部文件排序操作。
      • 虽然 SELECT * 仍然意味着需要进行回表操作(因为 team_name 索引的叶子节点只存储 team_name 和主键 team_id,不包含其他列),但相比于全表扫描并进行文件排序,利用索引进行顺序扫描和回表通常是更优的选择,因为它减少了大量的 CPU 排序开销,并可能减少磁盘 I/O。
策略 2:WHEREORDER BY 同时利用复合索引

原理:将 WHERE 条件中用于过滤的列作为复合索引的前缀,ORDER BY 的列紧随其后,且顺序和方向匹配。这样,MySQL 可以先通过索引过滤数据,再利用索引的有序性进行排序。

案例 2:查询年龄大于25岁的男性运动员的id,按国籍升序、年龄降序排列

  • SQLSELECT cyclist_id FROM cyclists WHERE age > 25 AND gender = '男' ORDER BY nationality ASC, age DESC;

  • 优化前 EXPLAIN 结果

    id,select_type,table,partitions,type,possible_keys,key,key_len,ref,rows,filtered,Extra
    1,SIMPLE,cyclists,,ALL,,,,,80,3.33,Using where; Using filesort
    
    • 分析type: ALLUsing filesort 再次表明全表扫描和文件排序的瓶颈。多条件过滤和多列排序,且排序方向不同,使得优化器无法利用现有索引。
  • 优化思路:我们需要一个复合索引,能够同时服务 WHERE 条件和 ORDER BY 子句。

    • gender 是等值条件,适合放在索引最前面进行快速过滤。
    • nationalityORDER BY 的第一列,应紧随 gender
    • age DESCORDER BY 的第二列,应紧随 nationality,并指定降序。
  • 优化后索引CREATE INDEX idx_gender_nationality_age ON cyclists(gender, nationality, age DESC);

  • 优化后 EXPLAIN 结果

    id,select_type,table,partitions,type,possible_keys,key,key_len,ref,rows,filtered,Extra
    1,SIMPLE,cyclists,,range,idx_gender_nationality_age,idx_gender_nationality_age,xxx,,54,10,Using where; Using index
    
    • 原理
      1. gender = '男' 利用索引前缀进行 refrange 查找,快速缩小范围。
      2. gender 固定的条件下,索引的 nationalityage DESC 部分与 ORDER BY 的顺序和方向完全匹配,因此可以在索引内部直接完成排序,避免了 Using filesort
      3. cyclist_id(主键)包含在索引叶子节点中,避免了回表操作,进一步提升性能。
策略 3:覆盖索引的重要性

原理:如果 SELECT 列表中的所有列、WHERE 条件中的所有列以及 ORDER BY 子句中的所有列都能在一个索引中找到,那么这个索引就是覆盖索引。MySQL 无需访问主表数据行,所有数据直接从索引中获取,这大大减少了磁盘 I/O(特别是随机 I/O)。

案例 3:查询所有车队中,使用“Specialized”品牌自行车的车队名称和联系电话,按车队名称升序排列

  • SQLSELECT team_name, contact_number FROM cycling_teams WHERE bike_brand='Specialized' ORDER BY team_name;

  • 优化前 EXPLAIN 结果

    id,select_type,table,partitions,type,possible_keys,key,key_len,ref,rows,filtered,Extra
    1,SIMPLE,cycling_teams,,ALL,,,,,8,12.5,Using where; Using filesort
    
    • 分析bike_brand 上没有索引,导致全表扫描,并且需要对 team_name 进行文件排序。
  • 优化后索引CREATE INDEX idx_bike_brand_team_name_contact_number ON cycling_teams(bike_brand, team_name, contact_number);

  • 优化后 EXPLAIN 结果

    id,select_type,table,partitions,type,possible_keys,key,key_len,ref,rows,filtered,Extra
    1,SIMPLE,cycling_teams,,ref,idx_bike_brand_team_name_contact_number,idx_bike_brand_team_name_contact_number,202,const,2,100,Using index
    
    • 原理
      1. bike_brand='Specialized' 利用索引前缀进行 ref 查找,高效过滤。
      2. bike_brand 确定的前提下,索引的 team_name 部分天然有序,直接满足 ORDER BY team_name避免了 Using filesort
      3. SELECT 列表中的 team_namecontact_number 都包含在这个联合索引中,因此实现了覆盖索引(Using index,完全避免了回表操作,性能得到极致提升。

其他 ORDER BY 优化建议

  • 避免在 ORDER BY 中使用函数或表达式:这会使得索引失效,强制进行文件排序。
  • 尽量保持 ORDER BY 中所有列的排序方向一致:如 col1 ASC, col2 ASCcol1 DESC, col2 DESC
  • 适当调整 sort_buffer_sizemax_length_for_sort_data:如果无法避免 filesort,增大这些参数有助于将排序在内存中完成,减少磁盘 I/O。但这不能解决扫描大量数据的问题。

4. GROUP BY 分组操作优化:高效聚合与索引利用

GROUP BY 操作用于将具有相同值的行分组,并对每个组执行聚合函数。其优化目标是避免创建昂贵的临时表(Using temporary)和文件排序(Using filesort)。

4.1 GROUP BY 的底层原理

MySQL 执行 GROUP BY 主要有以下几种内部策略:

  • 索引分组(松散索引扫描 / 紧凑索引扫描):这是 GROUP BY 最理想的优化方式,通过利用索引的有序性直接完成分组。
    • 松散索引扫描 (Loose Index Scan):当 GROUP BY 的列是某个复合索引的最左前缀,并且查询中没有 WHERE 条件或 WHERE 条件只涉及索引的这个最左前缀时,MySQL 可以跳过不必要的索引条目,直接在索引中找到每个分组的第一个值,然后跳到下一个分组的第一个值。它不会扫描所有符合条件的索引条目。EXPLAIN 通常显示 Using index for group-by
    • 紧凑索引扫描 (Tight Index Scan):当 GROUP BY 的列是某个复合索引的完整前缀,或者 WHERE 条件使用了索引的最左前缀,并且 GROUP BY 剩余的索引前缀,MySQL 会连续扫描索引中的所有符合条件的条目,并利用索引的有序性进行分组。它扫描的范围比松散索引扫描大,但仍然是高效的索引扫描。EXPLAIN 通常不显示特定的 group-by 关键词,但 typerangeindex,且没有 Using filesortUsing temporary
  • 使用临时表(Using Temporary Table:当无法利用索引直接完成分组时,MySQL 会创建内部临时表来辅助 GROUP BY 操作。
    • 排序分组(Sort-based Grouping):MySQL 将符合 WHERE 条件的数据读取出来,然后将这些数据排序,使得相同分组的行相邻。排序完成后,再遍历有序的数据集,进行分组和聚合计算。这个排序过程可能在内存中完成,也可能在磁盘上进行文件排序。EXPLAIN 通常显示 Using temporaryUsing filesort
    • 哈希分组(Hash-based Grouping):MySQL 构建一个哈希表,将每一行的 GROUP BY 列作为哈希键,将聚合结果存储在哈希表中。这种方式避免了排序,但消耗内存。在较新的 MySQL 版本中,哈希聚合可能不显示 Using filesortUsing temporary
4.2 优化策略与实战

核心思想:通过合理的索引设计,让 GROUP BY 操作利用索引的有序性,避免昂贵的临时表和文件排序。

策略 1:创建复合索引,并确保 GROUP BY 列是索引的最左前缀

原理:这是实现松散索引扫描的关键。如果 GROUP BY col1, col2,那么创建一个 (col1, col2) 的复合索引是最优的。MySQL 可以直接在索引上进行分组,不需要额外的排序或哈希操作。

案例 1:统计每个车队的运动员数量

  • SQLEXPLAIN SELECT team_id, COUNT(cyclist_id) FROM cyclists GROUP BY team_id;
  • 优化前 EXPLAIN 结果:可能显示 Using temporary; Using filesort (如果 team_id 没有索引)。
  • 优化后索引CREATE INDEX idx_team_id ON cyclists(team_id);
  • 优化后 EXPLAIN 结果:显示 Using index 。
  • 原理:有了 idx_team_id 索引,MySQL 可以直接扫描这个索引。由于索引本身就是按 team_id 排序的,它可以高效地按 team_id 进行分组,无需创建临时表或进行排序。
策略 2:WHERE 条件和 GROUP BY 条件同时利用索引

原理:如果 WHERE 条件能够使用索引过滤,并且 GROUP BY 也能利用同一个索引的后续列,那么组合索引的效率会更高。将 GROUP BY 的列放在索引前部,WHERE 条件中用于过滤的列放在其后(特别是对于范围查询)。

案例 2:统计年龄大于 20 岁的男性运动员,按国籍和性别分组

  • SQLEXPLAIN SELECT nationality, gender, COUNT(cyclist_id) FROM cyclists WHERE age > 20 AND gender = '男' GROUP BY nationality, gender;
  • 优化前 EXPLAIN 结果:可能显示 Using temporary; Using filesort
  • 优化思路:为了避免临时表,GROUP BY 的列 (nationality, gender) 应该作为索引的最左前缀。WHERE 条件中的 agegender(等值条件)则放在其后。
  • 优化后索引CREATE INDEX idx_nationality_gender_age ON cyclists(nationality, gender, age);
  • 优化后 EXPLAIN 结果Using where; Using index (或 Using index condition),不再有 Using temporaryUsing filesort
  • 原理idx_nationality_gender_age 索引首先按 nationality 分组,然后按 gender 分组。WHERE age > 20gender = '男' 可以在扫描索引时进行过滤(索引条件下推)。这样,整个分组过程都可以在索引上完成,无需临时表。
策略 3:GROUP BYORDER BY 列的顺序一致

原理:如果 GROUP BYORDER BY 使用相同的列且顺序一致,MySQL 可以在一次索引扫描中同时完成两者要求。

案例 3:统计每个车队的运动员数量,并按车队 ID 升序排列

  • SQLEXPLAIN SELECT team_id, COUNT(cyclist_id) FROM cyclists GROUP BY team_id ORDER BY team_id;
  • 优化前 EXPLAIN 结果:可能显示 Using temporary; Using filesort
  • 优化后索引CREATE INDEX idx_team_id ON cyclists(team_id);
  • 优化后 EXPLAIN 结果Using index for group-by (或 Using index),没有 Using temporaryUsing filesort
  • 原理:由于 team_id 索引本身就是有序的,MySQL 可以直接扫描索引,按 team_id 分组,同时这个顺序也满足了 ORDER BY team_id 的要求,从而同时避免了临时表和文件排序。

其他 GROUP BY 优化建议

  • 避免在 GROUP BY 中使用函数或表达式:这会使得索引失效,强制 MySQL 使用临时表进行分组。
  • 合理使用 HAVING 子句HAVINGGROUP BY 之后对分组结果进行过滤。如果能将过滤条件前置到 WHERE 子句中,会更早地减少需要分组的数据量,效率更高。
  • 调整 MySQL 配置参数:适当增大 tmp_table_size / max_heap_table_size,可以让更多临时表在内存中完成,减少磁盘 I/O。

5. LIMIT 分页操作优化:大偏移量分页的挑战与突破

LIMIT 子句常用于分页。当 offset 较小(前几页)时性能良好,但当 offset 变得非常大时,LIMIT 操作会变得异常缓慢,成为查询的瓶颈。

5.1 LIMIT 分页操作的底层原理

当执行 LIMIT offset, row_count 时,MySQL 的大致执行过程是:

  1. 扫描并排序:MySQL 首先会扫描(或通过索引)所有满足 WHERE 条件的行,并进行 ORDER BY 排序。
  2. 跳过偏移量:然后,它会从排序后的结果集中,逐条跳过offset 条记录。
  3. 返回指定数量:最后,它会收集从 offset 之后开始的 row_count 条记录并返回。

问题出在哪里?

offset 很大时,即使 ORDER BY 列有索引,SELECT * 仍然是瓶颈。MySQL 为了确定最终要返回的 row_count 条记录,它不得不扫描并处理前面 offset + row_count 条记录。尤其当 SELECT * 时,MySQL 需要为每一条被跳过的记录也执行回表操作,以获取其完整的行数据。这意味着,随着 offset 增大,MySQL 需要处理的数据量线性增加,导致大量的随机 I/O 和 CPU 浪费。这就像你翻阅一本非常厚的书,要看第 10000 页开始的 10 页,你仍然需要从头开始翻过前面的 9999 页才能开始阅读你真正想看的内容。

5.2 优化策略及原理

核心思想:减少 MySQL 扫描和跳过的数据量,特别是避免为被跳过的记录执行昂贵的回表操作。

策略 1:使用覆盖索引和子查询优化大偏移量分页(“延迟回表”)

原理:这是处理大偏移量分页最常用且最有效的策略。它将“扫描大量数据并回表”转化为“轻量级扫描索引定位主键 + 精准回表获取少量完整数据”。

  • 第一步(子查询):快速定位目标主键
    • 利用一个覆盖索引(只包含排序字段和主键)来快速定位到 offset 后的第一条记录的主键值。这一步只扫描索引,无需回表,效率极高。
  • 第二步(主查询):精准回表获取完整数据
    • 利用第一步中获取到的少量主键值,使用 WHERE primary_key IN (...)WHERE primary_key > some_id LIMIT row_count 的方式,直接从主键索引中精确获取所需的 row_count 条完整数据,避免了扫描和跳过大量不相关的数据。

案例:查询 cyclists 表中按 age 降序排列的第 10001 到 10010 条记录(即 LIMIT 10000, 10

  • 优化前 SQLSELECT * FROM cyclists ORDER BY age DESC LIMIT 10000, 10;

  • 优化前分析:假设 age 列有 idx_age (age DESC) 索引。MySQL 会扫描 idx_age 索引 10010 次。每次扫描到一个 (age, cyclist_id) 条目,为了满足 SELECT *,它需要立即根据 cyclist_id 回表,获取该行的所有其他列。这意味着,为了跳过前 10000 条记录,MySQL 不得不执行 10000 次昂贵的随机回表 I/O,即使这些数据最终都会被丢弃。

  • 优化后 SQL

    -- 确保 age 列上有索引,例如:CREATE INDEX idx_age ON cyclists(age DESC);
    -- 假设 cyclist_id 是主键SELECT c.*
    FROM cyclists c
    INNER JOIN (SELECT cyclist_idFROM cyclistsORDER BY age DESCLIMIT 10000, 10 -- 子查询只获取主键
    ) AS sub
    ON c.cyclist_id = sub.cyclist_id
    ORDER BY c.age DESC; -- 外部再次排序以确保最终结果正确顺序
    
  • 原理

    1. 子查询SELECT cyclist_id FROM cyclists ORDER BY age DESC LIMIT 10000, 10
      • MySQL 利用 idx_age 索引的有序性,从索引叶子节点开始扫描。
      • 由于子查询只请求 cyclist_id(主键),而 cyclist_id 包含在二级索引的叶子节点中,因此这是一个覆盖索引查询无需回表
      • MySQL 只扫描索引 10010 次,然后直接从索引中获取 10 个目标 cyclist_id。这一步 I/O 开销极小,因为是顺序扫描索引。
    2. 外层查询SELECT c.* FROM cyclists c INNER JOIN ...
      • 外层查询得到 10 个精确的 cyclist_id 列表。
      • MySQL 使用这 10 个 cyclist_id 去主键索引中进行查找,精确地获取这 10 条完整的记录
      • 主键查找是最快的查找方式,仅需要 10 次高效的随机 I/O。
  • 优点:显著减少了扫描的数据量和随机回表 I/O,特别是对于大偏移量查询,性能提升巨大。

  • 缺点:SQL 语句相对复杂。

策略 2:基于上次查询结果进行优化(“书签式”分页或“游标式”分页)

原理:不使用 OFFSET。而是记录上次查询结果的最后一条记录的某个有序字段的值(通常是排序字段和主键),在下一次查询时,利用这个值作为新的查询起点。

  • 适用场景:实时滚动加载(如社交媒体的“加载更多”),或者用户总是从上一页或下一页跳转,而不是直接跳到任意页码。这种方法无法直接跳转到任意页

  • 示例:加载下一页按 registration_time 倒序排列的用户列表。

    首次加载(第一页):

    SELECT cyclist_id, name, registration_time FROM cyclists ORDER BY registration_time DESC LIMIT 10;

    假设第一页的最后一条记录的 registration_time 是 '2025-01-10 10:00:00',其 cyclist_id 是 500。

    加载下一页(第二页)

    SELECT cyclist_id, name, registration_time
    FROM cyclists
    WHERE registration_time < '2025-01-10 10:00:00'OR (registration_time = '2025-01-10 10:00:00' AND cyclist_id < 500) -- 处理相同时间戳的情况
    ORDER BY registration_time DESC
    LIMIT 10;
    
  • 原理WHERE 子句能够直接利用 registration_time 上的索引(或复合索引 (registration_time, cyclist_id))进行范围查找。MySQL 只需要从上次查询的结束点开始扫描,而不是从头开始扫描并跳过大量记录。LIMIT 10 限制了扫描的数据量。没有 OFFSET,大大减少了处理的数据量。

  • 优点:性能极高,尤其适合“加载更多”的场景,I/O 效率几乎与查询第一页相同。

  • 缺点:不能直接跳转到任意页码;需要前端或应用记住上次查询的“书签”值。

策略 3:优化 COUNT(*) 查询以获取总页数

原理:在分页时,经常需要知道总记录数来计算总页数。SELECT COUNT(*) FROM table_name WHERE ... 在没有索引的情况下,可能导致全表扫描。

  • 优化

    • 利用覆盖索引:如果 COUNT(*) 可以在某个非空索引上完成,MySQL 可以只扫描索引,而不是扫描整个表。COUNT(*) 会选择最小的索引进行扫描。
    • 估算代替精确计数:对于非常大的表,精确计数可能非常耗时。在某些业务场景下,可以接受一个大致的估算值(例如使用 EXPLAIN 结果中的 rows 字段),或者定期将 COUNT(*) 结果缓存到 Redis 等地方。
  • 示例SELECT COUNT(*) FROM cyclists WHERE gender = '男';

  • 优化:如果 gender 列有索引(CREATE INDEX idx_gender ON cyclists(gender);),MySQL 可以通过扫描该索引的叶子节点进行计数,实现覆盖索引,比全表扫描快得多。


总结

SQL 优化是一个持续的、需要深入理解数据库原理的过程。从 INSERT 的物理写入顺序,到 ORDER BYGROUP BY 如何利用索引避免昂贵的排序/临时表,再到 LIMIT 分页如何巧妙地规避大偏移量的性能陷阱,无不体现着对索引机制I/O 成本的深刻洞察。

核心优化策略始终围绕着:

  • 选择合适的主键:短小、顺序、非空,降低所有二级索引的成本。
  • 创建合理的索引:特别是复合索引,使其能够同时服务 WHEREORDER BYGROUP BY
  • 利用覆盖索引:避免不必要的回表操作,将 I/O 降至最低。
  • 规避全表扫描和临时表操作:理解 EXPLAIN 输出,识别 Using filesortUsing temporary 等瓶颈,并针对性优化。
  • 掌握特殊场景优化:如大批量 INSERT 使用 LOAD DATA INFILE,大偏移量分页的子查询策略和游标式分页。

通过持续的实践、对 EXPLAIN 结果的分析以及对数据库底层原理的理解,您将能够编写出更高效、更健壮的 SQL 查询,为您的应用带来卓越的性能。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.pswp.cn/web/82901.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

SQL 基础入门

SQL 基础入门 SQL&#xff08;全称 Structured Query Language&#xff0c;结构化查询语言&#xff09;是用于操作关系型数据库的标准语言&#xff0c;主要用于数据的查询、新增、修改和删除。本文面向初学者&#xff0c;介绍 SQL 的基础概念和核心操作。 1. 常见的 SQL 数据…

HTTP 请求协议简单介绍

目录 常见的 HTTP 响应头字段 Java 示例代码&#xff1a;发送 HTTP 请求并处理响应 代码解释&#xff1a; 运行结果&#xff1a; 文件名&#xff1a; 总结&#xff1a; HTTP&#xff08;HyperText Transfer Protocol&#xff09;是用于客户端与服务器之间通信的协议。它定…

《100天精通Python——基础篇 2025 第5天:巩固核心知识,选择题实战演练基础语法》

目录 一、踏上Python之旅二、Python输入与输出三、变量与基本数据类型四、运算符五、流程控制 一、踏上Python之旅 1.想要输出 I Love Python,应该使用()函数。 A.printf() B.print() C.println() D.Print() 在Python中想要在屏幕中输出内容&#xff0c;应该使用print()函数…

求解一次最佳平方逼近多项式

例 设 f ( x ) 1 x 2 f(x)\sqrt{1x^2} f(x)1x2 ​&#xff0c;求 [ 0 , 1 ] [0,1] [0,1]上的一个一次最佳平方逼近多项式。 解 &#xff1a; d 0 ∫ 0 1 1 x 2 d x 1 2 ln ⁡ ( 1 2 ) 2 2 ≈ 1.147 d_0\int_{0}^{1}\sqrt{1x^2}dx\frac{1}{2}\ln(1\sqrt{2})\frac{\sqrt…

在Ubuntu上使用 dd 工具制作U盘启动盘

在Ubuntu上使用 dd 工具制作U盘启动盘 在Linux系统中&#xff0c;dd 是一个功能强大且原生支持的命令行工具&#xff0c;常用于复制文件和转换数据。它也可以用来将ISO镜像写入U盘&#xff0c;从而创建一个可启动的操作系统安装盘。虽然图形化工具&#xff08;如 Startup Disk…

如何理解OSI七层模型和TCP/IP四层模型?HTTP作为如何保存用户状态?多服务器节点下 Session方案怎么做

本篇概览&#xff1a; OSI 七层模型是什么&#xff1f;每一层的作用是什么&#xff1f;TCP/IP四层模型和OSI七层模型的区别是什么&#xff1f; HTTP 本身是无状态协议&#xff0c;HTTP如何保存用户状态? 能不能具体说一下Cookie的工作原理、生命周期、作用域&#xff1f;使用…

深入剖析 RocketMQ 中的 DefaultMQPushConsumerImpl:消息推送消费的核心实现

前言 在 Apache RocketMQ 的消息消费体系中&#xff0c;RocketMQ 提供了DefaultMQPushConsumer&#xff08;推送消费&#xff09;和DefaultMQPullConsumer&#xff08;拉取消费&#xff09;两种主要消费方式。DefaultMQPushConsumer与DefaultMQPullConsumer在消息获取方式&…

Linux编程:2、进程基础知识

一、进程基本概念 1、进程与程序的区别 程序&#xff1a;静态的可执行文件&#xff08;如电脑中的vs2022安装程序&#xff09;。进程&#xff1a;程序的动态执行过程&#xff08;如启动后的vs2022实例&#xff09;&#xff0c;是操作系统分配资源的单位&#xff08;如 CPU 时…

React Router 中 navigate 后浏览器返回按钮不起作用的问题记录

React Router 中 navigate 后浏览器返回按钮不起作用的问题记录 在使用 React Router&#xff08;v6&#xff09;开发项目时&#xff0c;我遇到了一个让人困惑的问题&#xff1a; 当我从 /article 页面使用 navigate("/article/next") 进行跳转后&#xff0c;点击浏…

[面试精选] 0094. 二叉树的中序遍历

文章目录 1. 题目链接2. 题目描述3. 题目示例4. 解题思路5. 题解代码6. 复杂度分析 1. 题目链接 94. 二叉树的中序遍历 - 力扣&#xff08;LeetCode&#xff09; 2. 题目描述 给定一个二叉树的根节点 root &#xff0c;返回 它的 中序 遍历 。 3. 题目示例 示例 1 : 输入&…

Addressable-配置相关

1、Profile 概述窗口配置 主要用于配置Addressable打包&#xff08;构建&#xff09;加载AB包时使用的一些变量,这些变量定义了 在哪里保存打包&#xff08;构建&#xff09;的AB包运行时在哪里加载AB包 可以添加自定义变量&#xff0c;以便在打包加载时使用,之后在设置 组中…

aws(学习笔记第四十三课) s3_sns_sqs_lambda_chain

文章目录 aws(学习笔记第四十三课) s3_sns_sqs_lambda_chain学习内容&#xff1a;1. 整体架构1.1 代码链接1.2 整体架构1.3 测试代码需要的修改1.3.1 unit test代码中引入stack的修改1.3.2 test_outputs_created代码中把错误的去掉 2. 代码解析2.1 生成dead_letter_queue死信队…

Python训练营打卡Day43

kaggle找到一个图像数据集&#xff0c;用cnn网络进行训练并且用grad-cam做可视化 进阶&#xff1a;并拆分成多个文件 config.py import os# 基础配置类 class Config:def __init__(self):# Kaggle配置self.kaggle_username "" # Kaggle用户名self.kaggle_key &quo…

hive 3集成Iceberg 1.7中的Java版本问题

hive 3.1.3 集成iceberg 1.7.2创建Iceberg表报错如下&#xff1a; Exception in thread "main" java.lang.UnsupportedClassVersionError: org/apache/iceberg/mr/hive/HiveIcebergStorageHandler has been compiled by a more recent version of the Java Runtime …

文本切块技术(Splitter)

为什么要分块&#xff1f; 将长文本分解成适当大小的片段&#xff0c;以便于嵌入、索引和存储&#xff0c;并提高检索的精确度。 用ChunkViz工具可视化分块 在线使用 ChunkViz github https://github.com/gkamradt/ChunkViz 如何确定大模型所能接受的最长上下文 可以从…

C++:用 libcurl 发送一封带有附件的邮件

编写mingw C 程序&#xff0c;用 libcurl 发送一封带有附件的邮件 下面是一个使用 MinGW 编译的 C 程序&#xff0c;使用 libcurl 发送带附件的邮件。这个程序完全通过代码实现 SMTP 邮件发送&#xff0c;不依赖外部邮件客户端&#xff1a; // send_email.cpp #include <i…

tensorflow image_dataset_from_directory 训练数据集构建

以数据集 https://www.kaggle.com/datasets/vipoooool/new-plant-diseases-dataset 为例 目录结构 训练图像数据集要求&#xff1a; 主目录下包含多个子目录&#xff0c;每个子目录代表一个类别。每个子目录中存储属于该类别的图像文件。 例如 main_directory/ ...cat/ ...…

遨游Spring AI:第一盘菜Hello World

Spring AI的正式版已经发布了&#xff0c;很显然&#xff0c;接下来我们要做的事情就是写一个Hello World。 总体思路就是在本地搭建一个简单的大模型&#xff0c;然后编写Spring AI代码与模型进行交互。 分五步&#xff1a; 1. 安装Ollama&#xff1b; 2. 安装DeepSeek&…

华为云Flexus+DeepSeek征文|基于华为云Flexus X和DeepSeek-R1打造个人知识库问答系统

目录 前言 1 快速部署&#xff1a;一键搭建Dify平台 1.1 部署流程详解 1.2 初始配置与登录 2 构建专属知识库 2.1 进入知识库模块并创建新库 2.2 选择数据源导入内容 2.3 上传并识别多种文档格式 2.4 文本处理与索引构建 2.5 保存并完成知识库创建 3接入ModelArts S…

Java优化:双重for循环

在工作中&#xff0c;经常性的会出现在两张表中查找相同ID的数据&#xff0c;许多开发者会使用两层for循环嵌套&#xff0c;虽然实现功能没有问题&#xff0c;但是效率极低&#xff0c;一下是一个简单的优化过程&#xff0c;代码耗时凑从26856ms优化到了748ms。 功能场景 有两…