当前位置: 首页 > news >正文

第三章 OB SQL 引擎高级技术

3.1 SQL 请求执行流程

OceanBase 数据库的 SQL 引擎是整个数据库的数据计算中枢,和传统数据库类似,整个引擎分为解析器、优化器、执行器三部分。当 SQL 引擎接受到了 SQL 请求后,经过语法解析、语义分析、查询重写、查询优化等一系列过程后,再由执行器来负责执行。所不同的是,在分布式数据库里,查询优化器会依据数据的分布信息生成分布式的执行计划。如果查询涉及的数据在多台服务器,需要走分布式计划,这是分布式数据库 SQL 引擎的一个重要特点,也是十分考验查询优化器能力的场景。OceanBase 数据库查询优化器做了很多优化,诸如算子下推、智能连接、分区裁剪等。如果 SQL 语句涉及的数据量很大,OceanBase 数据库的查询执行引擎也做了并行处理、任务拆分、动态分区、流水调度、任务裁剪、子任务结果合并、并发限制等优化技术。

下图描述了一条 SQL 语句的执行过程,并列出了 SQL 引擎中各个模块之间的关系。

3.1.1 Parser(词法/语法解析)

Parser 是整个 SQL 执行引擎的词法或语法解析器,在收到用户发送的 SQL 请求串后,Parser 会将字符串分成一个个的单词,并根据预先设定好的语法规则解析整个请求,将 SQL 请求字符串转换成带有语法结构信息的内存数据结构,称为语法树(Syntax Tree)。为了加速 SQL 请求的处理速度,OceanBase 数据库对 SQL 请求采用了特有的快速参数化,以加速查找执行计划的速度。OceanBase使用lex进行词法分析,使用yacc进行语法分析,将SQL语句生成语法分析树。

OceanBase既然可以同时兼容MySQL和Oracle,那语法解析阶段该如何实现?实际上OceanBase是以租户的方式提供MySQL和Oracle这两种兼容模式的(一个租户等同一个MySQL或Oracle实例,OceanBase可以混布这两种兼容模式的租户),在创建租户时指定兼容模式(不能修改),之后不同的租户走各自对应兼容模式的语法解析流程即可。

3.1.2 Resolver(语义解析)

当生成语法树之后,Resolver 会进一步将该语法树转换为带有数据库语义信息的内部数据结构。在这一过程中,Resolver 将根据数据库元信息将 SQL 请求中的 token 翻译成对应的对象(例如库、表、列、索引等),生成语句树。

语义分析器(Resolver)相比上一阶段要复杂得多。针对不同类型的SQL语句(DML、DDL、DCL)或命令,会有不同的解析。这一步主要用于生成SQL语句的数据结构,其中主要包含各子句(如SELECT/FROM/ WHERE)及表达式信息。

3.1.3 Transfomer(逻辑改写)

在查询优化中,经常利用等价改写的方式,将用户 SQL 转换为与之等价的另一条 SQL,以便于优化器生成最佳的执行计划,这一过程称为查询改写。Transformer 在 Resolver 之后,分析用户 SQL 的语义,并根据内部的规则或代价模型,将用户 SQL改写为与之等价的其他形式,并将其提供给后续的优化器做进一步的优化。Transformer 的工作方式是在原 Statement Tree 上做等价变换,变换的结果仍然是一棵语句树。

查询重写的核心思想在于保证执行结果不变的情况下将SQL语句做等价转换,以获得更优的执行效率。因为用户认为的“好”SQL,不一定是内核认为的“好”SQL,我们不能希望所有程序开发人员都在成为SQL优化的专家之后再来写SQL、生成好的执行计划让数据库高效执行,这就是查询重写模块存在的意义。

查询重写本质上是一个模式匹配的过程,先基于规则对SQL语句进行重写(这些规则如:恒真恒假条件简化、视图合并、内连接消除、子查询提升、外连接消除等等),之后进入基于代价的重写判定。相比总能带来性能提升的基于规则的重写,基于代价的重写多了代价评估这一步(需要查询优化器参与):基于访问对象的统计信息以及是否有索引,在进行了重写之后会对重写前后的执行计划进行比较,如果代价降低则接受,代价不减反增则拒绝。在迭代了基于代价的重写之后,如果接受了重写的SQL,内部会再迭代一次基于规则的重写,生成终态的内核认为的“好”SQL并给到查询优化器模块。

3.1.4 Optimizer(优化器)

优化器是整个 SQL 优化的核心,其作用是为 SQL 请求生成最佳的执行计划。在优化过程中,优化器需要综合考虑 SQL 请求的语义、对象数据特征、对象物理分布等多方面因素,解决访问路径选择、联接顺序选择、联接算法选择、分布式计划生成等多个核心问题,最终选择一个对应该 SQL 的最佳执行计划。SQL 的执行计划是一棵由多个操作符构成的执行树。

为了充分利用OceanBase的分布式架构和多核计算资源的优势,OceanBase的查询优化器会对执行计划做并行优化:根据计划树上各个节点的数据分布,对串行执行计划进行自底向上的分析,把串行的逻辑执行计划改造成一个可以并行执行的逻辑计划。

查询优化器是数据库管理系统的“大脑”,它会枚举传入语句的执行计划,基于代价模型和统计信息对每一个执行计划算出代价,并最终选取一条代价最低的执行计划。OceanBase的查询优化器基于System-R框架,是一个bottom-up的过程,通过选择基表访问路径、连接算法和连接顺序、最后综合一些其他算子来计算代价,从而生成最终的执行计划。

直到这里我们会发现,似乎与普通的单机 RDBMS 区别不大。诚然,作为一个已存在 40 多年的行业,很多理论的工程实践大同小异,但 OceanBase 作为 NewSQL 有独特的魅力。上面的查询优化部分生成的是串行执行计划,为了充分利用 OceanBase 的分布式架构和多核计算资源的优势,OceanBase 的查询优化器随即会进入并行优化阶段:根据计划树上各个节点的数据分布,对串行执行计划进行自底向上的分析,把串行的逻辑执行计划改造成一个可以并行执行的逻辑计划。并行优化最重要的参考信息是数据的分布信息(Location),即查询所需访问的每个分区的各个副本在集群中的存储位置,这一信息由总控服务(主)(Master Root Service)维护,为了提升访问效率该分布信息还有缓存机制。

3.1.5 Code Generator(代码生成器)

优化器负责生成最佳的执行计划,但其输出的结果并不能立即执行,还需要通过代码生成器将其转换为可执行的代码,这个过程由 Code Generator 负责。Code Generator执行的过程只是忠实地将优化器的生成结果翻译成可执行代码,并不做任何优化选择。

代码生成器是SQL编译器的最后一个步骤,其作用是把逻辑执行计划翻译成物理执行计划。

查询优化器生成逻辑执行计划是对执行路径的逻辑表示,理论上已经具备可执行能力,但是这种逻辑表达带有过多的语义信息和优化器所需的冗余数据结构,对于执行引擎来说还是相对“偏重”。为了提高计划的执行效率,OceanBase通过代码生成器把逻辑计划树翻译成更适合查询执行引擎运行的树形结构,最终得到一个可重入的物理执行计划。

3.1.6 Executor(执行器)

当 SQL 的执行计划生成后,Executor 会启动该 SQL 的执行过程。对于不同类型的执行计划,Executor 的逻辑有很大的不同:

  • 对于本地执行计划,Executor 会简单的从执行计划的顶端的算子开始调用,由算子自身的逻辑完成整个执行的过程,并返回执行结果
  • 对于远程或分布式计划,Executor 需要根据预选的划分,将执行树分成多个可以调度的线程,并通过 RPC 将其发送给相关的节点执行。

物理执行计划的生成意味着SQL编译器的工作完成,之后交给执行引擎进行处理。需要注意的是,由于OceanBase是一个分布式数据库,分区副本遍布每一台OBServer上,我们执行的SQL语句要根据是访问本机数据、还是其他机器上的数据抑或是多台服务器上的数据来区别对待,这里就引入了调度器的概念。调度器把执行计划分为本地、远程、分布式三种作业类型,在对外提供统一接口且不侵入SQL执行引擎的同时,根据三种作业类型的特点,充分利用存储层和事务层的特性,实现了各自情况下最合适的调度策略。

本地作业:所有要访问的数据都位于本机的查询,就是一个本地作业。调度器对于这样的执行计划,无需多余的调度动作,直接在当前线程运行执行计划。事务也在本地开启。如果是单语句事务,则事务的开启和提交都在本地执行,

也不会发生分布式事务。这样的执行路径和传统单机数据库类似。

远程作业:如果查询只涉及到一个分区组,但是这个分区组的数据位于其他服务器上,这样的执行计划就是远程作业(多半是由于切主等原因导致缓存还未来得及更新数据的分布信息)。调度器把整个执行计划发送到数据所在机器上

执行,查询结果流式地返回给调度器,同时流式地返回给客户端。这样的流式转发能够提供较优的响应时间。不仅如此,远程作业对于事务层的意义更大。对于一个远程作业,如果是单语句事务,事务的开启提交等也都在数据所在服务器上执行,这样可以避免事务层的RPC 也不会发生分布式事务。

分布式作业:当查询涉及的数据位于多台不同的服务器时,需要作为分布式作业来处理,这种调度模式同时具有并行计算的能力。对于分布式计划,执行时间相对较长,消耗资源也较多。对于这样的查询,我们希望能够在任务这个小

粒度上提供容灾能力。每个任务的执行结果并不会立即发送给下游,而是缓存到本机,由调度器驱动下游的任务去拉取自己的输入。这样,当任务需要重试时,上游的数据是可以直接获取到的。同时,对于分布式的计划,需要在调度

器所在服务器上开启事务,事务层需要协调多个分区,必要时会产生分布式事务。

3.1.7 Plan Cache(执行计划缓存)

执行计划的生成是一个比较复杂的过程,耗时比较长,尤其是在 OLTP 场景中,这个耗时往往不可忽略。

为了加速 SQL 请求的处理过程,SQL 执行引擎会将该 SQL 第一次生成的执行计划缓存在内存中,后续的执行可以反复执行这个计划,避免了重复查询优化的过程。

3.2 DML 语句处理

数据操纵语言(Data Manipulation Language, DML)是SQL 语言中,负责对数据库对象运行数据访问工作的指令集。它的三种核心指令:INSERT(插入)、UPDATE(更新)、DELETE(删除),是在以数据为中心的应用程序开发中必定会使用到的指令。

除此之外,OceanBase 还支持REPLACE 和INSERT INTO...ON DUPLICATED KEY UPDATE 两种DML 语句,DML 的主要功能是访问数据,因此其语法都是以读取与写入数据库为主,除了INSERT 以外,其他指令都可能需搭配WHERE 指令来过滤数据范围,或是不加WHERE 指令来访问全部的数据。

3.2.1 DML 执行计划

  • 对于 INSERT/REPLACE 语句而言,由于其不用读取表中的已有数据,因此,INSERT语句的执行计划相对简单,其执行计划为简单的EXPR VALUES+INSERT OP算子构成
  • 对于 UPDATE 或 DELETE 语句而言,优化器会通过代价模型对WHERE条件进行访问路径的选择,或者ORDER BY数据顺序的选择

3.2.2 DML 一致性校验

DML操作的表对象每一列都有相关的约束性定义,例如列的 NOT NULL 约束,UNIQUEKEY约束等。

校验过程为

  • 写入数据前进行:约束检查 和 类型转换
  • 约束性检查失败,需要回滚该DML语句写入的脏数据。

NOT NULL检查和类型转换通过SQL层生成的COLUMN_CONVERT表达式来完成,执行计划会为DML语句写入表中的每一列都添加该表达式,在执行算子中,数据以行的形式被流式的迭代,在迭代过程中,COLUMN_CONVERT表达式被计算,即可完成相应的类型转换和约束性检查,而UNIQUE KEY约束的检查是在存储层的data buffer中完成。

3.2.2 DML 锁管理

OceanBase锁的类型

− 只有行锁,没有表锁;在线DDL,不中断DML。

− 只有写锁(X锁),没有读锁。

与MVCC的结合

− 读取“已提交”数据的最新版本,不需要读锁,不支持“脏读”。

− 避免读写之间的锁互斥,实现更好的并发性。

3.3 DDL 语句处理

OceanBase DDL 语句处理的特点

  • 支持传统数据库的DDL语句,自动完成全局统一的schema变更,无需用户在多节点间做schema一致性检查。
  • DDL任务由OceanBase的RootServer统一调度执行,保证全局范围内的schema一致性。
  • DDL不会产生表锁;DML根据schema信息的变更自动记录格式,对业务零影响。

3.4 查询改写

数据库中的查询改写(Query Rewrite)把一个 SQL 改写成另外一个更加容易优化的 SQL。OceanBase 数据库的查询改写规则分为基于规则的查询改写和基于代价的查询改写。

  • 基于规则的查询改写总是会把 SQL 往“好”的方向进行改写,从而增加该 SQL 的优化空间。
  • 基于代价的查询改写并不能总是把 SQL 往“好”的方向进行改写,所以需要代价模型来判断。
  • 基于代价的改写之后可能又会重新触发基于规则的改写,所以整体上采用迭代式的方式进行改写。

在数据库中,一个改写规则通常需要满足特定的条件才能够进行改写,而且很多规则的改写可以互相作用(一个规则的改写会触发另外一个规则的改写),所以在 OceanBase 数据库中,把能够互相作用的改写规则组织成一个规则集合。因为一个规则集合中的规则可以互相作用,所以 OceanBase 数据库采用迭代式的方式进行改写一直到 SQL 不能被改写为止或者迭代次数达到预先设定的阈值。

OceanBase 数据库把所有基于规则的查询改写分成若干个规则集合。对于每个规则集合,OceanBase 数据库采用迭代式的方式进行改写一直到 SQL 不能被改写为止或者迭代次数达到预先设定的阈值。类似地,对于基于代价的改写规则也是采用这种方式处理。这里需要注意的是,基于代价的改写之后可能又会重新触发基于规则的改写,所以整体上的基于代价的改写和基于规则的改写也会采用这种迭代式的方式进行改写。

3.4.1 基于规则的查询改写

基于规则的查询改写总是会把 SQL 往“好”的方向进行改写,从而增加该 SQL 的优化空间。一个典型的基于规则的改写是把子查询改写成连接,如果不改写,子查询的执行方式只能是 NESTED LOOP JOIN。 但是改写之后,优化器就也可以考虑 HASH JOIN 和 MERGE JOIN 的执行方式。

详细内容参考官网文档: https://www.oceanbase.com/docs/oceanbase-database/oceanbase-database/V2.2.76/rule-based-query-rewriting-1

3.4.2 基于代价的查询改写

基于代价的查询改写并不能总是把 SQL 往“好”的方向进行改写,所以需要代价模型来判断。一个典型的基于规则的改写就是 OR-EXPANSION。

详细内容参考官网文档:https://www.oceanbase.com/docs/oceanbase-database/oceanbase-database/V2.2.76/cost-based-query-rewriting-1

3.5 执行计划

3.5.1 执行计划概念

SQL 是一种“描述型”语言。与“过程型”语言不同,用户在使用 SQL 时,只描述了“要做什么”,而不是“怎么做”。

数据库在接收到 SQL 查询时,必须为其生成一个“执行计划”。OceanBase 数据库的执行计划与其他关系数据库类似,本质上是由物理操作符构成的一棵执行树。物理操作符一般对应一个关系操作,如表扫描、联接、聚合、排序等。执行计划通过将不同的物理操作符按照一定的先后顺序组织在一棵执行树中,最终完成该 SQL 查询。

执行计划在 OceanBase 数据库中被实现为一棵由操作符组成的“二叉树”(OceanBase 数据库目前暂不支持操作自分支超过两个的“多元操作符”)。当查询涉及到多个关系(Relation)时,优化器会通过依次执行多个 JOIN 操作,将多个关系连接在一起,最终完成执行。执行树从形状上可以分为“左深树”、“右深树”和“多枝树”三种(参见下图)。目前 OceanBase 数据库的优化器只支持左深树的计划生成。

3.5.2 执行计划展示

EXPLAIN 命令

  • 通过Explain命令查看优化器针对给定SQL生成的逻辑执行计划。
  • Explain不会真正执行给定的SQL,可以放心使用该功能而不用担心在性能调试中可能给系统性能带来影响。
  • Explain 命令格式如下例所示,展示的格式包括BASIC、EXTENDED、PARTITIONS 等等,内容的详细程度有所区别
EXPLAIN [BASIC | EXTENDED | PARTITIONS | FORMAT = format_name] explainable_stmtformat_name: { TRADITIONAL | JSON }explainable_stmt: { SELECT statement| DELETE statement| INSERT statement| REPLACE statement| UPDATE statement }

实时执行计划展示

  • 通过查询 v plan_cache_plan_explain 视图实现

更多内容参考文档《读懂 OceanBase Explain》

OceanBase 数据库 读懂 OceanBase Explain 20201029.pdf

3.6 快速参数化

为减少执行计划生成次数,OceanBase 使用了执行计划缓存(Plan Cache)。对于同一 SQL 不同请求对应的参数经常是不同的,而这些请求通常能够共用相同的执行计划,为了能够将这些 SQL 请求在 Plan Cache 中命中相同的计划,我们将 SQL 进行参数化(即将 SQL 中的常量转换为参数),然后使用参数化的 SQL 文本作为键值在 Plan Cache 中获取执行计划,从而达到仅参数不同的 SQL 能够共用相同的计划目的,由于传统数据库在进行参数化时一般是对语法树进行参数化,然后使用参数化后的语法树作为键值在 Plan Cache 中获取计划,而我们是使用的词法分析对文本串直接参数化后作为 Plan Cache 的键值,因此叫做快速参数化。基于快速参数化的获取执行计划过程如下图所示:

参数化过程是指把 SQL 查询中的常量变成变量的过程。如下例所示:

select * from t1 where c1 = 5 and c2 = "oceanbase";

参数化后结果如下例所示:

select * from t1 where c1 = @1 and c2 = @2;

但在计划匹配中,不是所有常量都可以被参数化,比如 ORDER BY 后面的常量,表示按照 SELECT 投影列中第几列进行排序。

如下场景中的常量均不能参数化(约束条件):

  • 所有 ORDER BY 后常量(例如"ORDER BY 1,2;")
  • 所有 GROUP BY 后常量(例如"GROUP BY 1,2;")
  • LIMIT 后常量(例如"LIMIT 5;")
  • 作为格式串的字符串常量 (例如"SELECT DATE_FORMAT('2006-06-00', '%d'); "里面的"%d")
  • 被物化的参数精度数字,带有隐含信息并最终影响执行计划的常量
  • 例如 "SELECTUNIX_TIMESTAMP('2015-11-13 10:20:19.012');" 里面的"2015-11-13 10:20:19.012",指定输入时间戳的同时,隐含指定了函数处理的精度值为毫秒
  • 例如 "CAST(999.88 asNUMBER(2,1))"中的"NUMBER(2,1)",或者"SUBSTR('abcd', 1, 2)" 中的 "1, 2"
  • 在优化过程中被用来做语义等价改写的恒真恒假条件
  • SELECT 投影列中常量

基于快速参数化的执行计划缓存优点如下:

  • 省去语法分析过程;
  • 查找 hash map 时,对参数化后语法树的 hash 和比较操作,可替换为对文本串进行 hash 和 memcmp 操作,效率更高。

3.7 执行计划缓存

参考的官网文档: https://www.oceanbase.com/docs/oceanbase-database/oceanbase-database/V2.2.76/execution-plan-cache-2

SQL 语句的优化是一个比较耗时的过程,随着 SQL 语句的复杂度增高,优化的时间也会越来越长。为了避免反复执行优化过程,生成的执行计划会加入到计划缓存中,以便再次执行该 SQL 时使用。每一个租户在每一台服务器上都有一个独立的计划缓存,用以缓存在此服务器上处理过的 SQL 计划。

在应用系统中,同一条 SQL 每次执行可能会使用不同的参数,为了减少计划缓存的大小,系统会首先将用户 SQL 的参数做参数化处理,得到与具体参数无关的 SQL 字符串,并使用该字符串作为计划缓存的键值。

计划缓存是一个典型的 Key-Value 结构,Key 就是参数化后的 SQL 字符串,Value 就是该条 SQL 所对应的执行计划。在 OceanBase 数据库的计划缓存中,SQL 的执行计划可以分为本地计划、远程计划和分布式计划三种类型。在计划缓存中,同一条 SQL 根据其需要访问的数据不同,可能同时具有三种执行计划。

对于某一条 SQL 的某一种执行计划,默认情况下 OceanBase 数据库只会保留一个计划,该计划由第一次执行 SQL 时生成;但在某些情况下,同一条 SQL 的参数值可能会影响到计划的选择,计划缓存可能会根据需要,为不同的参数值保留不同的执行计划,从而保证每次执行时可以使用最合适的计划。

3.7.1 执行计划原理

OceanBase 数据库内置有计划缓存,会将首次优化后生成的计划缓存在内存中,随后的执行会首先访问计划缓存,查找是否有可用计划,如果有,则直接使用该计划;否则将执行查询优化的过程。

执行计划的生成过程非常复杂,优化器需要综合考虑多种因素,为 SQL 生成“最佳”的执行计划。因此,查询优化的过程本身也是一个比较耗时的过程,当 SQL 本身执行耗时较短时,查询优化所带来的开销也变得不可忽略。一般来说,数据库在这种场景会缓存之前生成的执行计划,以便在下次执行该SQL 时直接使用,这种策略被称为“optimize once”,即“一次优化”。

实际场景中,“一次优化”的策略虽然可以大大降低每次执行时优化的开销,但也会引入一些问题。例如,当同一条 SQL 不同的参数值需要使用不同计划时,“一次优化”的策略就无法处理。针对这种场景,OceanBase 数据库 V2.0 版本引入了自适应计划共享(Adaptive Cursor Sharing)功能,能够较好地处理不同参数条件下的计划选择。

3.7.2 执行计划淘汰

包括

  • 自动淘汰
  • 手动淘汰 alter system flush plan cache;

当计划缓存占用的内存达到了需要淘汰计划的内存上限(即淘汰计划的高水位线)时,对计划缓存中的执行计划自动进行淘汰。优先淘汰最久没被使用的执行计划,影响淘汰策略的参数和变量如下:

  • plan_cache_evict_interval(parameter):检查执行计划是否需要淘汰的间隔时间。
  • ob_plan_cache_percentage(variable):计划缓存可使用内存占租户内存的百分比(最多可使用内存为:租户内存上限* ob_plan_cache_percentage/100)。
  • ob_plan_cache_evict_high_percentage (variable) :计划缓存使用率(百分比)达到多少时,触发计划缓存的淘汰。
  • ob_plan_cache_evict_low_percentage (variable) :计划缓存使用率(百分比)达到多少时,停止计划缓存的淘汰。

手动淘汰支持按租户、Server 或删除计划缓存,或者全部删除缓存:

alter system flush plan cache [tenant_list] [global]

  • tenant_list 格式:tenant = 'tenant1, tenant2, tenant3….'
  • tenant_list 和 global为可选字段:
  • 如果tenant_list没有指定,则清空所有租户的计划缓存,否则只清空特定租户的。
  • 如果global没有指定,则清空本机的计划缓存,否则清空该租户所在的所有Server 上的计划缓存。

3.7.3 执行计划缓存刷新

计划缓存中的执行计划因各种原因失效时,会将计划缓存中失效的计划进行刷新(可能会导致新的计划):

  • SQL中涉及的表的SCHEMA进行变更时(比如添加索引,删除或增加列等),该SQL在计划缓存中对应的执行计划将被刷新;
  • SQL中涉及的表的统计信息被更新时,该SQL对应的执行计划会被刷新,由于OceanBase在合并时统一进行统计信息的收集,因此每次合并之后,计划缓存中所有的计划将被刷新;
  • SQL进行outline计划绑定变更时,该SQL对应的执行计划会被刷新,更新为按绑定的outline生成的执行计划。

3.7.4 执行计划缓存使用控制

系统变量控制

  • ob_enable_plan_cache 设置为 ture 时表示 SQL 请求可以使用计划缓存,设置为 false 时表示 SQL 请求不使用

计划缓存。可进行 session 级和 global 级设置,默认设置为true。

Hint控制

  • /*+use_plan_cache(none)*/ 该hint表示请求不使用计划缓存
  • /*+use_plan_cache(default)*/ 该hint表示使用计划缓存
http://www.lryc.cn/news/588999.html

相关文章:

  • HR数字化转型:3大痛点解决方案与效率突破指南
  • B/S架构系统角色与对应协议详解
  • AAAI-2025 | 同济大学面向嘈杂环境的音频视觉导航!BeDAViN:大规模音频-视觉数据集与多声源架构研究
  • HCIA第三次综合实验:VLAN
  • iOS高级开发工程师面试——常见第三方框架架构设计
  • ESP32S3+VSCode+PlatformIO+Arduino+Freertos开发入门指南:基于Arduino框架的应用开发全流程
  • 基于LAMP环境的校园论坛项目
  • 新手向:Python数据处理Excel报表自动化生成与分析
  • 剑指offer62_骰子的点数
  • 为什么市场上电池供电的LoRa DTU比较少?
  • [Pytest][Part 5]单条测试和用例集测试
  • MMYSQL刷题
  • CAU数据挖掘 第五章 聚类问题
  • 【canal+mysql+example+数据验证测试】
  • Python 内置函数random
  • 行为模式-状态模式
  • 小智完整MCP交互流程(以调节音量为例)
  • 网络安全职业指南:探索网络安全领域的各种角色
  • 使用llama-factory进行qwen3模型微调
  • elasticsearch 下载/安装
  • MaxKB使用笔记【持续ing】
  • python+selenium UI自动化初探
  • JAVA高级第一章 集合框架和泛型(一)
  • Ubuntu18.04 系统重装记录
  • 写作词汇积累(A):自洽、自恰、恰如其分、恰当
  • MQ2烟雾传感器模块(第九天)
  • C++学习笔记五
  • 《时间简史》:窥探宇宙的奥秘
  • IOS 18下openURL 失效问题
  • 032_API参考文档