news 2026/10/19 2:51:13

Oracle SQL调优实战:从执行计划看懂慢查询根因

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Oracle SQL调优实战:从执行计划看懂慢查询根因

做 Oracle 性能优化这些年,我几乎每天都会重复一个动作:拿到一条慢 SQL,先不急着加索引、改业务,先看执行计划。执行计划就是优化器给这条 SQL 设计好的“执行路线图”——先访问哪张表、怎么连接、走全表扫描还是索引、预估扫多少行,全都写在那几行缩进里。很多人觉得执行计划难读,其实只是没掌握读的顺序和几个关键栏位,再加上生产环境里统计信息过期、隐式转换、连接方式误判这些坑,才会觉得玄乎。这篇文章就围绕执行计划分析,结合几个我在生产环境里实际处理过的案例,把怎么读、怎么定位、怎么解决一次讲清楚。适合刚接触 SQL 调优的开发同学、专职 DBA,以及所有被生产慢查询折磨过的朋友。

1. 执行计划到底在算什么?先看懂优化器的“算盘”

1.1 一条 SQL 变成执行计划,优化器经历了什么

你给 Oracle 丢一条 SQL,它会经历解析、优化、生成行源代码几个阶段,其中优化这一步决定了最终的执行计划。基于代价的优化器(CBO)会枚举多条可能的执行路径:全表扫还是走索引,嵌套循环连接还是哈希连接,先连哪张表再连哪张表。然后根据表的统计信息,比如行数、块数、列的基数、直方图分布等,给每条路径算一个代价,最后选代价最低的作为执行计划。

这里一定要理解一个关键点:优化器不是在真正执行 SQL,它是在“猜”执行这条 SQL 的代价。猜得准不准,取决于统计信息全不全、新不新,也取决于查询条件本身能不能被准确估算。生产环境里绝大多数执行计划问题,都出在这个“猜”的环节。所以我排查问题时有个习惯,先看统计信息的 last_analyzed,再看谓词条件,然后才看连接方式。顺序反了,容易被表象带偏。

1.2 访问路径和连接方式,这两张老底子必须背下来

执行计划里最核心的两类操作,一个是访问路径(怎么取数据),一个是连接方式(多张表怎么拼)。新手最容易在这里犯迷糊:是不是走索引就一定快?答案是不一定。访问路径的选择是全局博弈,跟过滤条件的选择性、表的大小、统计信息都有关系。

访问路径典型场景备注
TABLE ACCESS FULL返回数据比例大、小表、无可选索引不等于“差”,大表扫 80% 数据时比走索引更合理
INDEX RANGE SCAN列上有等值或范围条件最常用,按索引定位后再回表
INDEX FULL SCAN查询列都在索引中且需要排序单块读,通常比全表扫慢,但能避免排序
INDEX FAST FULL SCAN查询列都在索引中,不需要排序多块读,适合只取索引列的统计类查询
INDEX SKIP SCAN复合索引前导列没有过滤条件依赖前导列基数低,否则代价很高

连接方式同样适合用表格对照理解:

连接方式适合场景劣势
NESTED LOOP外层结果集很小,内层能走索引外层结果集大时,循环次数暴增
HASH JOIN两张大表等值连接,或没有可用索引建哈希表耗内存
SORT MERGE JOIN数据已排序、非等值连接(>、<)排序开销大

优化器选择连接方式时,核心看两个东西:驱动表(外层表)的返回行数,以及被驱动表能不能高效定位数据。这两个数字一旦估算错误,连接方式就会翻车,后面案例里我会详细展开。

1.3 代价不等于真实性能,别被 Cost 数字带偏

执行计划里的 Cost 是优化器内部计算的相对代价单位,不是秒,也不是逻辑读数量。生产上经常出现这样的情况:计划里 cost 是 3,实际跑了 30 秒;另一个计划 cost 是 300,实际反而 1 秒完成。所以拿到执行计划,别盯着 Cost 列纠结,重点看 E-Rows(估算行数)、A-Rows(实际行数)、Buffers(逻辑读)、A-Time(实际耗时)这几个可以被验证的指标。Cost 的作用是让优化器自己排序比较,不是给 DBA 看性能的。

2. 拿到执行计划后怎么读?顺序和方法是命门

2.1 三种最常用的取计划方式

第一种,EXPLAIN PLAN FOR 配合 DBMS_XPLAN.DISPLAY,适合事前预测,它不会真正执行 SQL,只是让优化器算一条计划出来:

EXPLAIN PLAN FOR SELECT * FROM T_ORDER WHERE ORDER_ID = :id; SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY);

第二种,DBMS_XPLAN.DISPLAY_CURSOR,这是调优时我最常用的一招。它能拿到一条 SQL 真实执行时的计划外加实际统计信息,比如 A-Rows、A-Time、物理读:

SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR('sql_id', NULL, 'ALLSTATS LAST'));

第三种,DBMS_XPLAN.DISPLAY_AWR。当 SQL 早就跑完、shared pool 里已经找不到时,可以从 AWR 快照里挖历史计划,特别适合排查那种“昨晚慢了一下,现在又正常”的问题:

SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_AWR('sql_id'));

提示:EXPLAIN PLAN 给的是估算值,只有 ALLSTATS 里带出来的 A-Rows 才是实际值。排查性能问题时,能用 DISPLAY_CURSOR 就别只用 EXPLAIN PLAN。

2.2 读执行计划的顺序:从缩进最深处开始,由内向外

很多新手拿到计划直接从上往下念,这恰好是错的。Oracle 执行计划的执行顺序是:缩进最深、层级最靠右的操作最先执行;同一缩进级别的操作,按从上到下顺序执行;父操作要等子操作的结果返回后才开始。看下面这个简单的嵌套循环计划:

-------------------------------------------------------------------------------- | Id | Operation | Name | E-Rows | A-Rows | -------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | | 100 | | 1 | NESTED LOOP | | 100 | 100 | | 2 | TABLE ACCESS BY INDEX ROWID| T_A | 10 | 10 | |* 3 | INDEX RANGE SCAN | IDX_A | 10 | 10 | | 4 | TABLE ACCESS BY INDEX ROWID| T_B | 1 | 10 | |* 5 | INDEX RANGE SCAN | IDX_B | 10 | 10 | --------------------------------------------------------------------------------

执行顺序是:先执行 Id 3(缩进最深),扫描 T_A 表上的 IDX_A 索引取得 rowid,再回表 Id 2 取出 T_A 的行数据,这样得到第一批驱动行;然后针对每一行驱动数据,执行 Id 5 扫描 T_B 的索引 IDX_B,再回表 Id 4 取 T_B 的行。Id 1 的 NESTED LOOP 只是承接这些结果。理解这个“先内后外、先右后左”的读取顺序,你才能把计划读成真正的时间线,而不是一堆操作名词的堆叠。

2.3 重点盯住 E-Rows 和 A-Rows 的差距

执行计划分析中,最重要的诊断信号就是 E-Rows 与 A-Rows 的差异。E-Rows 是优化器根据统计信息估算的返回行数,A-Rows 是实际执行返回的行数。理论上两者应该接近,一旦差了一个数量级以上,就说明优化器对条件或连接基数的判断出了严重偏差,这种偏差会在连接方式、访问路径上放大,最终发展为慢查询。

我拿到计划后,会先看 Predicate Information 这一段。它分两种:access 表示通过索引或哈希直接定位数据的谓词,filter 表示拿到数据后再过滤的谓词。如果某个过滤条件出现在 filter 而不是 access 里,往往意味着数据量被低估,或者索引设计有问题。这一栏是整个执行计划里信息密度最高的地方,值得每个做调优的人反复研究。

3. 生产案例一:统计信息过期,让好索引被晾在一边

3.1 凌晨跑批突然多了 40 秒

某公司财务系统的对账批处理,有一张大表 T_FLOW(账户流水表),每天凌晨按日期范围汇总记账。有一天批处理时间从 8 分钟膨胀到 20 分钟,直接拖慢了后续一系列任务。抓出来造成问题的 SQL 是这样的:

SELECT 账号, SUM(金额) FROM T_FLOW WHERE 流水日期 BETWEEN :start_date AND :end_date GROUP BY 账号;

T_FLOW 表里积累了一年的流水,总量约 1.2 亿行,流水日期上建了普通索引 IDX_FLOW_DATE。这条 SQL 以前跑得一直很快,为什么偏偏今天变慢了?查库时我发现一个细节:表数据这半年几乎翻了一倍,而问题恰恰出在“数据量变了,统计数据却没跟上”。

3.2 执行计划显示:索引就在那,优化器就是不选

用 DBMS_XPLAN.DISPLAY_CURSOR 拿到的真实计划如下:

----------------------------------------------------------------------------------------------- | Id | Operation | Name | E-Rows | A-Rows | Buffers | A-Time | ----------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | | 100K | 480235 | 00:00:42.12 | | 1 | HASH GROUP BY | | 100K | 100K | 480235 | 00:00:42.12 | | 2 | TABLE ACCESS FULL | T_FLOW | 12M | 12M | 478889 | 00:00:35.60 | -----------------------------------------------------------------------------------------------

A-Rows 显示实际扫了 1200 万行,占全表的 10%,这个量级确实适合全表扫描。但问题是,这条 SQL 是跑某一天的流水,真正应该命中的数据只有 90 万行左右。也就是说,优化器把查询范围的选择率估算得过高,误以为 BETWEEN 条件会命中 12% 的数据,于是放弃了索引。

3.3 一查统计信息,果然半年没收集

排查步骤很直接。先看表统计信息:

SELECT table_name, num_rows, last_analyzed FROM user_tables WHERE table_name = 'T_FLOW';

结果 last_analyzed 还是半年前的日期。那时候这张表只有 6000 万行,日期列的最小值和最大值也都跟现在不一样了。统计信息过期,等于优化器拿着半年前的“地图”在给现在的数据指路,走错路一点都不意外。我手工收集了统计信息:

BEGIN DBMS_STATS.GATHER_TABLE_STATS(ownname => 'DW', tabname => 'T_FLOW', method_opt => 'FOR ALL COLUMNS SIZE AUTO', estimate_percent => DBMS_STATS.AUTO_SAMPLE_SIZE); END; /

收完之后重新执行,执行计划切回索引范围扫描,批处理恢复到 8 分钟左右。从发现问题到恢复,整个过程不到半小时,但前提是你会先怀疑统计信息,而不是一上来就改 SQL。

3.4 复盘:统计信息维护是纪律问题

这个案例给我最大的教训是:大表的统计信息维护不能靠运气。生产环境里增长有明显规律的表,应该做成定时任务,每天或每周根据数据变化频率收集。就算怕收集开销大,也至少要保证大表的 num_rows 和关键列的高价值直方图是新的。另外要记住一个排查原则:看到执行计划里出现全表扫描,先反问一句“统计信息新鲜吗?”再下结论。很多时候问题不在 SQL 写得烂,而在优化器掌握的情报已经过时了。

4. 生产案例二:隐式类型转换,索引被“绑住手脚”

4.1 一条本来没问题的 SQL,上线一个月后开始拖库

某业务系统的用户表 T_USER,有个查询语句:

SELECT * FROM T_USER WHERE ACCOUNT_NO = :acc;

ACCOUNT_NO 字段是 VARCHAR2(30),上面建了普通索引 IDX_USER_ACCOUNT。上线初期执行一直是毫秒级,一个月之后突然变成秒级全表扫描。用户量只增长了一点点,为什么性能会掉这么多?抓 SQL 的时候我注意到,接口层的调用参数是数值类型,而表里的账号列是字符串,这个细节很可能就是根源。

4.2 计划里的谓词信息泄露了天机

使用 DISPLAY_CURSOR 抓计划,重点看 Predicate Information:

---------------------------------------------------------------------------------------------- | Id | Operation | Name | E-Rows | A-Rows | Predicate Information | ---------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | | 10 | | |* 1 | TABLE ACCESS FULL | T_USER | 10 | 10 | filter(TO_NUMBER("ACCOUNT_NO")=:acc) | ----------------------------------------------------------------------------------------------

看到 TO_NUMBER("ACCOUNT_NO") = :acc 这行,问题立刻清楚了。绑定变量 :acc 传进来的是 NUMBER 类型,而 ACCOUNT_NO 列是 VARCHAR2。Oracle 在解析时选择把列类型转换为数字去比较,等于在列上套了个函数。只要列被函数包裹,普通的 B+ 树索引就没法用范围扫描,只能全表扫一遍,再对每行做转换和过滤。

这类隐式转换是生产环境里最常见的“隐形杀手”。它不报错、不告警,就是让你发现 SQL 越来越慢。比较典型的原因有三个:应用代码没有按表结构定义参数类型,接口层把账号当数字处理,或者 SQL 里直接拿数字字面量与字符串列比较。

4.3 修复手段:优先改应用,兜底建函数索引

最干净的修法是让应用层保证绑定变量是字符串类型,SQL 写成 WHERE ACCOUNT_NO = '123456',从句法上消灭转换。如果应用改起来成本高,还有一招是建函数索引来配合 Oracle 的隐式转换:

CREATE INDEX IDX_USER_ACCOUNT_NUM ON T_USER(TO_NUMBER(ACCOUNT_NO));

这样 Oracle 虽然还是会做 TO_NUMBER,但索引本身也是 TO_NUMBER 后的值,两边能对上,索引就能用。不过函数索引属于兜底方案,等于承认了“列类型设计不合理”这个事实,后续维护会多一些成本。我个人的习惯是:能在应用层改就改应用,不能改再考虑函数索引。

4.4 排查时专门盯谓词区

以后大家拿到执行计划,可以养成一个职业习惯:先扫一眼 Predicate Information,只要看到 TO_NUMBER、TO_DATE、TO_CHAR 这些函数包着列名,基本就可以锁定隐式转换。这个检查只需要十秒钟,却能在所有索引优化手段之前,帮你排除一个非常隐蔽的根因。

5. 生产案例三:连接方式被基数估算带偏

5.1 多表关联查询,晚上高峰直接“卡死”

某订单系统的报表查询,在每天晚上高峰时段出现积压。SQL 大致是这样:

SELECT o.order_id, m.merchant_name FROM T_ORDER o JOIN T_MERCHANT m ON o.merchant_id = m.merchant_id WHERE o.order_time >= :start_dt AND o.order_time < :end_dt AND m.status = 1;

T_ORDER 订单表大概 5000 万行,T_MERCHANT 商户表只有 5 万行,这个量级下通常会让小表驱动大表,配合哈希连接。但当时这条 SQL 单次执行就要 45 秒,并发一高,共享池和 I/O 全部告急。

5.2 优化器选择了一个离谱的嵌套循环

抓到执行计划:

--------------------------------------------------------------------------------------------- | Id | Operation | Name | E-Rows | A-Rows | Buffers | --------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | | 200K | 800301 | | 1 | NESTED LOOP | | 200K | 200K | 800301 | | 2 | TABLE ACCESS FULL | T_MERCHANT | 1 | 5 | 20 | |* 3 | TABLE ACCESS FULL | T_ORDER | 1 | 200K | 800200 | ---------------------------------------------------------------------------------------------

注意 E-Rows 这一列:T_MERCHANT 被估算成只返回 1 行,因为统计信息里 status=1 的选择率被算得极低。优化器一看,驱动表只有 1 行,拿这 1 行去嵌套循环连 T_ORDER,成本不高,于是选了这个计划。可实际执行时,T_MERCHANT 里满足 status=1 的有 5 万行,这意味着外层循环跑了 5 万次,每次都要对 5000 万行的 T_ORDER 做全表扫描。A-Rows 显示的 200K 只是最终返回结果,真正恐怖的逻辑读是 80 万个块。

5.3 修复思路:先修统计,再考虑用提示干预

这种场景的处理路径有三条:

  1. 重新收集 T_MERCHANT 表上 status 列的统计信息,并确保有直方图,让优化器正确认识 status=1 的选择率;
  2. 用提示强制走哈希连接:
SELECT /*+ USE_HASH(m o) LEADING(m o) */ o.order_id, m.merchant_name FROM T_ORDER o JOIN T_MERCHANT m ON o.merchant_id = m.merchant_id WHERE o.order_time >= :start_dt AND o.order_time < :end_dt AND m.status = 1;
  1. 改写 SQL,先过滤出小商户集合,再和订单表关联。

我当时先收集了这两个表过期的统计信息,优化器重新计算后自己切回了哈希连接。但考虑到这是核心报表面,改天统计信息一变化,计划可能又飘回去,所以最终还是用 SQL 计划基线把“哈希连接 + 商户表先过滤”这个形态固定了下来。

5.4 这个案例给的核心规律

E-Rows 和 A-Rows 的差距是执行计划最大的警报信号。一旦看到差一个数量级以上,第一反应应该是“优化器在瞎猜”,然后顺着统计信息去查。修复时两条腿走路:先用统计信息让基数回归正确,再用计划基线或提示把成果固化。这样既能解决眼前问题,又能避免明天因为一次统计信息的变化重新掉坑。

6. 生产案例四:OR 条件引发的“被迫全表扫描”

6.1 一个 OR,让建好的索引全白搭

某短信平台有张消息表 T_MSG,按 create_time 和 send_status 分别建了索引。有一天运营同学要拉一批“昨天创建且发送中或发送成功”的消息做核对:

SELECT * FROM T_MSG WHERE create_time >= :start_dt AND create_time < :end_dt AND (send_status = 1 OR send_status = 2);

执行计划是清一色的 TABLE ACCESS FULL。表一共 8000 万行,其实按 create_time 范围过滤后只剩几万行,理论上走索引非常划算。但 OR 条件让优化器在两个可选的索引路径之间难以取舍——它既不想先按时间过滤再回表判断 send_status,也不想先按状态索引拿一半数据再去和时间比对,最后干脆选择了全表扫描。这就是 OR 条件最常见的代价。

6.2 改写成 UNION ALL 分支,各走各的索引

把 OR 拆开成两个互斥分支:

SELECT * FROM T_MSG WHERE create_time >= :start_dt AND create_time < :end_dt AND send_status = 1 UNION ALL SELECT * FROM T_MSG WHERE create_time >= :start_dt AND create_time < :end_dt AND send_status = 2;

改写后,两个分支都能稳稳地用到 create_time 上的索引,先按时间范围收窄数据,再过滤状态。实际执行时间从 20 秒降到了 0.5 秒。注意这里必须用 UNION ALL 而不是 UNION,因为 send_status 分别为 1 和 2 的两个查询结果在逻辑上不可能重复,UNION 会额外做一次去重和排序,纯属浪费。

6.3 什么时候不用改写

如果 OR 里的条件都是等值,而且取值很少,比如性别、类型这种枚举字段,总数只有两三个值,并且总选择性本来就低,那么直接用 IN 列表有时候也能走索引。关键是让每条分支路径都可索引。写 SQL 时把条件改写成“每个分支都能独立走索引”的形态,比依赖优化器智能更可靠。尤其在数据量超过千万级的场景里,这种改写带来的收益非常明显。

6.4 留个自查标准

以后遇到带 OR 的查询,先从计划里看两件事:是不是全表扫描?谓词能不能拆成可索引的分支?能拆就拆,不能拆再考虑加复合索引或者换执行方式。记住,OR 不是原罪,让 OR 变成全表扫描的,是优化器面对多路选择时无法精准评估合并代价的短板。

7. 生产环境里那些坑:执行计划变了、固定计划失败怎么办

7.1 为什么同一个 SQL 的计划会变来变去

很多开发同学有这样的困惑:这条 SQL 昨天还走索引,今天突然全表扫描,代码一行没动。其实执行计划是优化器基于统计信息、绑定变量、数据库参数、环境变量综合算出来的,任何一个输入变了,输出都可能变。常见的“计划突变”诱因包括:

  • 统计信息更新,选择性重新估算;
  • 绑定变量窥探(bind peeking)机制下,不同取值会生成不同计划;
  • 数据库参数调整,比如 optimizer_index_cost_adj、optimizer_mode 变化;
  • 索引被删除、失效或者重建;
  • 12c 之后的自适应计划特性会根据运行时反馈切换连接方式。

理解这一点后,就不会把执行计划当成固定不变的东西,而是会主动给关键 SQL 做“计划保护”。

7.2 固定计划的三板斧

第一板斧是 SQL Plan Baseline,从 11g 开始支持,能够捕获 SQL 的执行计划快照,并在后续执行中锁定。数据库升级、统计信息变化导致计划突变时,基线能帮你安全度过。第二板斧是 SQL Profile,它不直接锁定计划,而是通过调整优化器的估算系数来修正基数误差,适合不能改代码的场景。第三板斧是 Hints 改写,适合能改 SQL 的场合,前面用到的 LEADING、USE_HASH、INDEX 都是常用武器。

选择顺序上,我一般先看 SQL 能不能改,能改优先 Hints,配合代码评审;不能改再看能不能建基线。SQL Profile 属于干扰优化器估算的手段,能用但要有度,用多了会让执行计划越来越依赖人工干预。

7.3 排查执行计划问题实用清单

症状排查点
不该全表扫描却全表扫统计信息、隐式转换、OR 条件、索引是否真的存在且有效
连接方式异常(本该哈希却嵌套)驱动表基数估算、连接列数据分布、能否走索引
E-Rows 与 A-Rows 差异巨大直方图缺失、采样率低、绑定变量取值异常
计划白天正常晚上变慢并发变化、参数变化、统计信息收集窗口、分区裁剪
数据库升级后计划突变版本行为差异、未建立计划基线

这张表是我每次处理“慢 SQL 溯源”时的工作底稿,基本上照着过一遍,方向不会跑偏。

7.4 一个小技巧:用真实执行统计代替估算

最后分享一个实用技巧。如果你想亲眼看到一条 SQL 的真实执行路径和实际行数,可以在单独会话里打开统计级别,再执行 SQL,然后取计划:

ALTER SESSION SET statistics_level = ALL; -- 在这里执行你的业务SQL SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR(NULL, NULL, 'ALLSTATS LAST'));

生产库上不建议全局开启,但在具体会话里开启影响很小。ALLSTATS LAST 会给你最关键的 A-Rows 和 A-Time,这比干啃 EXPLAIN PLAN 的估算值靠谱得多。我处理大多数疑难杂症时,都是靠这一招拿到真实数据的。

做执行计划分析这几年,我最大的体会是:别把它当成玄学,它是可以系统拆解的。拿到一条慢 SQL,固定动作就是一看统计信息、二看谓词、三看连接方式、四看实际行数,四个动作做完,90% 的问题都能定位。案例里那些奇奇怪怪的计划突变,归根结底都是基数估算出了问题。另外想提醒一句,调优解决的是当前问题,如果不想明天再踩同一个坑,务必把统计信息维护和计划基线这些长效机制一并做掉。这行干久了会明白,最值钱的不是把一个计划调多快,而是让系统明天、下个月还能保持这个速度。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/19 2:51:12

LLM驱动移动端自动化测试:Mobile-Agent-v3与GUI-Owl模型路线对比

1. 移动端自动化测试的现状与核心痛点移动端自动化测试这件事&#xff0c;做过的人都懂&#xff0c;它跟Web端完全不是一个量级的复杂度。Web端你至少还能靠DOM树、XPath、CSS选择器去精确定位元素&#xff0c;哪怕页面改版&#xff0c;大部分时候改改选择器就能跑。但移动端呢…

作者头像 李华
网站建设 2026/10/19 2:50:25

图像去噪算法集合:PyQt5实现高斯、中值、FFT等滤波与工程实践

简介&#xff1a;这是一套基于Python与PyQt5实现的图像去噪算法合集&#xff0c;面向图像处理初学者、算法研究人员及需要快速对比去噪效果的技术人员&#xff0c;可一站式体验多种主流去噪算法。资源围绕高斯滤波、中值滤波、FFT频域去噪、双边滤波、非局部均值与深度学习去噪…

作者头像 李华
网站建设 2026/10/19 2:50:08

编译原理第一章拆解:从词法分析到代码生成,一张作战地图

学《编译原理》第一章的时候&#xff0c;大部分人都有一种相同的感觉&#xff1a;全是新名词&#xff0c;词法、语法、语义、中间代码、符号表&#xff0c;一个接一个&#xff0c;像在背没有故事的词汇表。我当时也是这样&#xff0c;考前能把“六阶段”背得滚瓜烂熟&#xff0…

作者头像 李华
网站建设 2026/10/19 2:49:49

Linux Raw Socket 二层包收发实战:从原理到工业网关应用

1. 二层包收发的核心价值与适用场景第一次接触 Raw Socket 收发二层包&#xff0c;是在做一个工业现场的数据采集网关。当时的需求很明确&#xff1a;设备之间跑的是私有二层协议&#xff0c;上层没有 IP、没有 TCP/UDP&#xff0c;就是最原始的以太网帧。用常规的 socket 编程…

作者头像 李华
网站建设 2026/10/19 2:49:49

LangChain检索器实践:从向量检索到多路召回与重排

最近在整理文档助手系列的时候&#xff0c;正好做到第5篇——检索器。前几篇把文档加载、切分、向量化、存储都铺好了路&#xff0c;到这步就是真正"查资料"的环节了。很多朋友做RAG应用时会发现一个矛盾&#xff1a;向量库建好了&#xff0c;数据也塞进去了&#xf…

作者头像 李华
网站建设 2026/10/19 2:49:35

Shell脚本安全清理Elasticsearch索引与过期数据实战

1. 场景与核心痛点先说个真实场景&#xff0c;我估摸着不少同行的运维群里都出现过类似求助&#xff1a;ES集群健康状态变黄了&#xff0c;磁盘使用率飙到85%以上&#xff0c;kibana上一查才发现是某个业务索引疯狂增长&#xff0c;或者历史日志索引堆积太多。有人下意识就想登…

作者头像 李华