理解Oracle的优化器

什么是数据库的优化器

 

最近有些朋友希望我能够解释一下Oracle数据库的优化器在CBO里的一些特点,我想就写一篇文章吧,这样其他有同样问题的朋友们也能够参考一下。

那么,数据库的优化器是什么呢,优化器主要是用于决定SQL语句的执行计划,执行计划就是如何通过一系列的步骤从而得出SQL语句的执行结果的一个“计划”,顾名思义,“执行计划”就是一个计划而已,并不是真正执行了SQL语句,需要一个执行计划的根本原因是要取得我们的SQL语句所需要的结果通常有多种不同的方式,最简单最常见的比如:当数据库需要执行一个SQL语句

 

Select * from emp where empno=1234

 

的时候,既可以去找到组成emp表的所有数据块扫描一遍,从中找出所有empno的值为1234的记录,也可以从扫描empno的索引块里(假如存在这么一个索引的话)找到所有empno入口为1234的值相对应的rowid,再通过这些rowid找到相对应的记录从而得出同样的结果,这个最简单的例子就显示了优化器的一些基本特征:如何在数据库服务层面的概念(都是些数据块,索引块的概念在起作用)里执行一个SQL语句,从而得出SQL语句所需要的结果,其实两种方式都可以得出我们需要的结果,但是数据库的优化器会选择它认为代价最小的一种执行“计划”,这个就是现在所说的CBO(cost-based optimization)的含义。

 

优化器在数据库里已经存在了很长的时间了,在Oracle版本8或者之前,并不存在我们上面所介绍的优化器选择最小代价执行计划的问题,那个时候优化器决定执行路径的方式是通过一系列的规则去决定执行计划,比如对于上面的同样的例子,优化器的工作方式则基于规则的优化方式,如下图:

 

基于规则的优化方式就是根据一些预先定义好的规则库,逐步去决定执行计划,比如上述的优化就是根据一个索引规则:如果where col=?的col字段上建立了一个索引,就优先使用该索引来访问数据,如果没有索引,则通过全表扫描方式来访问数据,基于规则优化的优化器有一个规则库,每一个SQL语句的执行计划都会对照这个规则库的规则来生成,所以每一个SQL在一个数据库版本里都具有非常确定的执行计划,不会因为表或者索引的数据量的大小改变。这个既是规则优化的优势,也是规则优化的致命缺点。说是优势是因为规则优化的确定性,一个SQL语句只要在测试环境了测试好了,在生产环境就一定会有相同的执行计划从而不会出现系统忽慢忽快的表现;说这是规则优化的致命确定是因为执行计划不会因为一些显然的条件进行执行计划的变更,不会利用某些从我们自己来看显然能够加快执行速度的一些外部条件。举个例子来说如果有一张表person里总共有10万条的记录,person里有一个性别字段gender并建有索引,这个字段的取值只可能是F(代表女性),M(代表男性)又或者是BI(代表。。。:))如果我们这个表的数据出于某种原因绝大部分数据都是女性,比如说女性数据占了99990条,男性的数据就只有10条记录,现在我们来考察SQL语句

 

Select * from person where gender=’M’

 

的执行计划,在规则优化模式下,这个SQL语句永远会选择走索引的执行计划,这个执行计划的确是上面的语句的最优的执行计划? 但是且慢,我们再来考察SQL语句

 

Select * from person where gender=’F’

 

的执行计划,这个时候我们会发现,使用索引的执行计划远要比不使用索引的执行计划慢的多,但是如果我们使用成本优化方式,我们会发现优化器会自动在第一个SQL语句里选择索引,而在第二个SQL语句里选择全表扫描,在两种条件下基于成本的优化方式都选择了正确的执行计划,这个就是成本优化的最大优点。

 

数据库的优化器通常工作在后台,但是数据库本身提供了命令让开发人员也能够看见优化器决定后的执行计划,如explain语句或者sqlplus的autotrace,又或者通过plsql developer或者Oracle的EM管理器,这样就为我们熟悉优化器的行为提供了很好的手段,我们可以通过各种方式去影响优化器,而可以清楚地看到每个影响是如何影响优化器的。

 

对于成本优化的一个通常疑惑是:对于执行计划所显示的执行成本,有些时候明明是成本数字显示出来低的执行计划,为什么执行的时候时间反而比成本数字显示高的执行计划的执行时间还要更长,我想根本原因还是在于大家需要记住,执行计划只是一个“计划”而已,在大部分的时候它选择的执行计划是正确的,但是又并非总是如此,这就如我们早上听交通台,说从国贸到天安门是通畅的,可是当我们走这条路的时候,却发现极其拥堵是一样的。当各种优化器的输入项有所误差(比如数据对象的统计信息没有更新),又或者哪怕根本没有误差,优化器有时候选择出来的执行计划对于一个具体的执行并非总是最好的,当你对于执行计划理解得越多,你就越能够理解为什么有些时候执行计划不能总是选择一个最佳的执行计划。

 


我的应用为什么不使用索引?

我想对于许多人的一个最大的疑惑就是当明明感觉使用索引要更快的时候,Oracle的优化器为什么选择了全表扫描,特别是许多使用习惯了规则优化的人来说,简直觉得Oracle数据库的新版本好像比以前退步了似的,可是真是如此吗?我们上一篇文章已经初步介绍了基于成本优化的Oracle优化器的概念,今天就来仔细研究这个问题。

 

优化器执行过程简介

 

上图是一个优化器如何生成执行计划的过程,优化器第一步首先是看看已经被编译(parse)的SQL语句是否能够定向到相关物化视图(或者11g的olap立方体),这个过程是Query Transformer的过程,这一个部分对于数据仓库的项目而言特别重要,我们以后会专门去介绍这部分的内容;然后就到了估计成本的过程,在估计SQL语句执行成本的时候,统计信息(存在在数据字典里)是至关重要的信息,因为在10g的优化器里已经不再允许规则优化方式,所以如果缺失了统计信息,则数据库将自动进行采用式的统计信息收集(请注意,这个10g的行为和9i的已经不一样了,9i的版本是没有统计信息的时候则专用规则优化,而10g已经取消了规则优化,所以在缺失统计信息的时候只好由优化器自动地去马上做一个统计信息收集),显而易见这个过程会影响SQL的执行效率,所以在10g的时候必须保证统计信息得到了有效的收集(创建完数据库之后一般而言会创建自动收集统计信息的任务,但是我们仍然需要确认该任务顺利完成并且该任务的收集是符合我们的要求的,我曾经碰到过一个客户,他的执行计划总是有问题,但是他声称他的数据库里已经有自动统计信息收集的任务存在,后来发现那个自动任务出于某种原因从来没有自动完成过)。

 

Estimator的目标是估计并计算一个执行计划的具体执行代价,Estimator通常是使用3个指标对执行计划进行评价,他们分别是Selectivity, Cardinality, 和Cost。

Cardinality:代表了一个结果集的记录的条数,如果你曾经使用过plsql developer来查看执行计划的话,你可以发现执行计划里就cardinality这一项。如下图:

这个记录数可能是一个join或者select或者group by产生的结果集,它可以被作为执行计划里下一步的输入,因而在一个更加复杂的执行计划里,可以看见Cardinality和Cost都会在每个步骤改变。

Cost:这个是成本优化里提到的最多的概念,也就是执行一个SQL语句的成本,它通常是由一个数值来代表,通常的概念就是,当Cost越高,则执行成本越高,则就意味着SQL语句所需要花费的资源和时间越长。但是Cost的具体的值本身并没有非常明确的含义,通常它的数值代表的是一个SQL可能花费的资源的综合情况(CPU,IO,内存等),但是并没有和特别明确的单位对应上,一般可以认为,如果经过我们的优化,可以发现Cost减少了,则该SQL的真正耗费的资源会减少。

Estimator的最终用途就是要计算不同执行计划的成本,然后选择一个成本最小的执行方式,这个就是成本优化的核心。

 

现在回过头来看我们开始提到的问题:为什么我的SQL不使用索引?建议大家对比使用索引方式和优化器自动选择的不使用索引方式的成本进行对比(可以考虑通过使用hint的方式强制优化器选择索引),对比之后可能你就能够明白,原因是如果你选择了索引方式,你会发现,Cost增加了,而成本优化就是要选择最小成本的执行计划,所以优化器没有选择索引的执行方式。可是,我们似乎又回到了那个问题:明明使用索引SQL执行更快呀!!怎么会成本更高呢?回答这个问题就需要考察:什么因素会影响成本?

 

在本文结束前需要澄清的一个概念就是,并非所有的全表扫描就一定比索引扫描慢,如果大家对于数据库的存储机制比较清楚的话,就不难理解这句话的意思:无论是表数据还是索引,都是由一个一个的数据块组成,通过索引选取一条记录的最少IO是两个(一个IO读取索引数据块找到表的数据的rowid,一个IO读取通过rowid读取表的数据块);而如果使用全表扫描,最少的时候却只需要一个IO就可以完成任务(当然前提是该IO能够包含我们所需要的数据的数据块)

 

影响优化器的参数

一个升级了数据库的挑战是:在新的数据库里,因为优化器的改变而会导致对于以前SQL语句的优化改变,为了减少升级所带来的影响,一个比较可行的方式是让升级后的数据库使用原来版本数据库的优化器,这个可以通过设置

OPTIMIZER_FEATURES_ENABLE=升级前版本号

的方式来让升级后的数据库具有原来数据库优化器的特征从而减小升级对于应用系统带来的影响。

 

CURSOR_SHARING

要了解这个参数对于成本优化的影响就需要了解数据库里histogram的概念,histogram是一个表的一个列的值对于具有类似的值的分布的信息。举个例子来说,假设一个1万条记录的客户表的某一列是客户的级别,普通客户是1,银卡客户是2,金卡客户是3,则所有的记录的级别的值都是1,2,3这三个值中的一个,关于级别的histogram信息就是级别是1的客户记录占有9000条,级别是2的客户记录占有800条,而金卡客户记录有200条,这个信息就是这个表上的级别字段上的histogram信息,这些信息对于产生高效的执行计划是非常重要的,我们在本系列的第一篇文章里的例子已经交代过,这种关于值的分布的不均匀的现象被称为skewed distribution of data,凡是对于这种值的非均匀分布,优化器就会根据具体的值进行执行计划的选择,而不是固定地使用索引还是全表扫描,但是这种优化仅当传入的值是个常数有效,而cursor_sharing参数的设置有可能是把常数转换成绑定变量,这样就让优化器无法使用histogram信息从而不能产生最佳的执行计划。

 

DB_FILE_MULTIBLOCK_READ_COUNT

这个参数的意思是在一次全表扫描或者索引全扫描中连续多几个数据块,这个参数会影响成本优化是因为它会降低全表扫描的代价,从而使优化器趋向于选择全表扫描的执行计划。写到这里我想说的一句话是Oracle数据库优化器的成本评估是受许多因素影响的,而且可以被有目的的影响,记得一些朋友老是抱怨说升级数据库之后本应该使用索引执行计划的SQL不用了,可是却不知道其实优化器是可以向我们所希望它的方式进行的。

 

OPTIMIZER_INDEX_COST_ADJ

这个参数应该是解决朋友们一直抱怨优化器不使用索引的最直接的参数了,它的取值是1到10000,缺省值是100,这个参数的缺省值代表了执行计划在索引选择和全表扫描选择之间的某种平衡,如果设置一个小于100的值,则优化器朝着利于索引选择的方向走,否则反之。举个例子来说,如果把这个值设置为10,则优化器则认为原来成本是100的索引扫描现在成本是原来的十分之一,也就是10,这样所有使用索引的执行计划就会成本下降,从而被更加倾向选择,所以如果发现有许多SQL语句应该使用索引效率会更高,但是却没有使用,就可以把这个值的参数调小。

 

OPTIMIZER_MODE

这个值大家应该不会陌生,在这里提这个参数是需要那些习惯于使用图形界面来测试SQL效率的人提个醒。这个参数的取值可以是ALL_ROWS,FIRST_ROW_n等,习惯于使用图形界面的开发人员往往习惯于把从下命令开始直到记录出现在屏幕上的时候作为SQL执行的时间,这个习惯的缺点在于,一般图形工具可能在取得前几十条记录的时候就返回了,所以这个时间只能说明前几十条记录返回的时间,却不能说明整个SQL语句执行完的时间。事实上这个参数被设置成FIRST_ROW_n的时候优化的目标就是前几十条记录返回的成本最小的执行计划,这种优化针对于在网上查询需要比较快展现前几十条记录的应用是合适的,但是如果是对于报表应用,我们关心的就应该是整个报表数据完全运行完整的时间,这个恰恰就是把这个参数设置成ALL_ROWS的时候选择的目标。当n值越小,优化器在表连接的时候更加倾向于使用nested loop和索引查找,当n值大的时候,优化器更倾向于使用hash连接和全表扫描。n值的选择完全取决于应用,比如一个internet查找应用,大部分的用户一般只查询整个结果的前几十条,而很少会去看后面几百条的数据,这个就是优化如何和应用结合的方法。

 

PGA_AGGREGATE_TARGET

可能大家会觉得奇怪的是怎么这个参数也能够影响优化器吗?事实上,原因是因为大的PGA能够分配更多的内存用于sort join和hash join,可以有效降低这些操作的成本,所以可以影响优化器的选择。

 

小结

通过前面的介绍,我们总结一下,实际上优化器在安装配置好数据库之后有自己默认的优化行为,但是这个行为并非是固定不变的,我们完全可以针对自己的应用,以自己的理解去影响优化器,让优化器能够朝着我们理解的方式去产生执行计划。以前在给一些客户进行培训的时候,我经常说的一句话就是,数据库软件就是一个软件,它毕竟不是一个会思考的智能的大脑,它比较不知道我们应用的特点,当有时候我们确信我们非常了解我们应用的时候,我们可以通过某种方式把这些我们知道的信息告诉它从而影响它的选择,这个才是数据库优化的一个重要目的。

 

各种访问路径

前面的文章已经出现了一些关于访问路径的名词(Access Path),如全表扫描,索引扫描,但是有时候我们并没有对这些词做详细的解释,我们甚至没有区分索引扫描(index Scan)和行ID选取(rowid scan)两种方式,本篇文章的目的就是要解释Oracle数据库里的各种访问路径。它们主要有全表扫描(Full-table scan),行ID扫描(rowid scan),索引扫描(index scan)。一般而言,如果是存取一个表的少部分记录,则使用索引的方式是合适的,如果是要访问一个表的大部分记录,则全表扫描更为高效,这样就表现为对于联机交易而言(OLTP),因为大部分交易都是针对某一个账户或几个账户进行的交易,所以只会涉及到表的极少数的记录,所以对于OLTP系统一般使用索引访问的方式,而对于决策支持系统,因为通常需要从很多记录甚至全部记录去产生一个结论,因此很多时候需要至少需要全表访问所有交易的记录一次才可以,因而对于这种交易类型全表扫描是普遍的,下面就对这些最基本的访问路径做一解释。

 

FULL-table Scan

一个表在Oracle数据库里的存储机制通常是由形成某种指针链接的数据块组成,所谓全表扫描的意思就是从组成表的起始数据块开始,一直跟着指针链接把所有的数据块读到内存里,然后根据Where语句里的条件把所有数据块里符合条件的记录选取出来(如果存在Where语句的话)。当表上面没有针对where条件里的字段建立索引的话,全表扫描是不可避免的,初一听可能觉得这种方式实在是很没有效率的一种方式,这个从某种程度上来说是正确的,从我的经验来看,在很多有性能问题的客户那里通常能够发现低效率的全表扫描,如果应用系统能够解决全表扫描问题,则系统至少解决了70%的效率问题,当然随着大家数据库使用水平的提高,想通过这个就解决系统大部分系统效率的可能性也就低了,但是无论如何,要看一个系统的问题,通常第一步还是去看看是否有低效率的全表扫描存在。

其实还是有一些参数能够提高全表扫描的效率的,比如我们上一篇文章里说的DB_FILE_MULTIBLOCK_READ_COUNT参数等,这个参数对于IO能力强劲的硬盘阵列能够极大加快全表扫描的效率。

当然,也存在另外的场景全表扫描是被认为比索引更加高效的,比如当一个表很小的时候,或者当需要访问组成一个表的绝大部分记录的时候。

 

Row ID Scan

我想大家应该清楚在Oracle数据库的表里,每一行记录都存在一个ID我们称之为Row ID,从很早版本的Oracle开始,RowID的值就是:

Row ID=数据文件编号+数据文件里数据块的编号+数据块的记录编号

所以一但知道了一条记录的Row ID,Oracle软件就可以马上把它翻译成硬件的IO指令去读到特定的记录,所以Row ID从某种程度上来说是Oracle数据库里最快的记录访问方式,如果大家想从图形工具里看看这个访问路径的话,可以试着把以下的SQL语句输入到执行计划解析器里:

 

select * from dual where rowid=:a

 

这个SQL语句出来的执行计划就是我们所说的Row ID Scan的含义,当然一般而言我们的应用可能很少有机会这么来写,那么除了上述的SQL语句,Row ID Scan一般还会出现在什么场景呢?

大家应该还记得Oracle索引的真正组成吧,索引一般是这样的:

 

索引=被索引的列+指向被索引的记录的Row ID

组成

 

所以大家在使用索引的过程中,很多时候就不自觉地使用了Row ID Scan,比如可以查看一下下面的SQL语句的执行计划

 

select * from emp where empno=:a

 

在执行计划里,首先最里层出现的是index unique Scan的访问路径,然后接着就出现了一个Row ID 方式,所以在很多时候,Row ID Scan的方式是经常能够见到的,只不过大家用到了却不知道而已。

 

Index Scans

要了解Index Scan首先是需要了解索引的存储结构,大家都知道一般索引的存储结构是有序的B*Tree存储,这就意味着索引的查找的基本方式是比较传统的类似于二分法的方式,比如举个例子来说,假如从1到100的键值分布在10个索引块里,每个索引块存储10条记录,则在根索引块里存储着键值1~10对应于索引块1,11到20对应索引块2的信息,则可以想像成在查找键值为49的记录的时候首先会定位到索引块4,然后找到所有键值为49的记录(这些记录一定是相邻的因为索引存储的有序存储特性),然后根据相对应的rowid去找到所有的数据块来完成index Range Scan的过程。

Index Scan又分为Unique Index Scan和Index Range Scan,他们的区别在于Unique Index Scan只会在主键上或唯一索引上发生,因为已经定义了该索引键值是唯一的,所以一旦找到值,索引的查找就停止了,而range Scan则要找到所有的值才停止查找。

当where语句里有c1=:a或者c1>:a或者c1<:a的语句或者他们的组合的时候,就有可能使用Index Scan的执行计划。另外当where语句有order by的时候,可能会使用一个index Scan来代替排序操作。

 

还有一些派生出来的Index Full Scan等执行路径我们就不详细讨论了,顺便说一下,如果SQL语句所需要的字段通过索引就能获得,则这个时候不需要使用索引去访问表,只需要Index Full Scan就可以完成SQL语句的执行,这个在有些时候是个不错的调优方法。

 

表连接操作

表连接操作应该是算比较难优化的一种操作类型,

对于表连接操作需要记住的一件事情是,任何一个时候只可能有两个表参加表的连接操作,如果from后面有多于两个表的时候,一般会先选择两个表做连接,然后再把连接的结果继续和另一个表做连接,直到所有的表被连接完为止

优化器对于表连接的决定主要是在于:

表连接的顺序

表连接的方法

中间连接结果的访问路径

 

优化器连接操作的优化

规则一:任何一个能够通过where条件里的一部分来确定一个结果集的表将被优先连接,比如一个SQL语句:

Select * from a,b,c where a.col1=b.col1 and b.col2=c.col2 and a.col3=:var

则一般而言在以上的例子中a表将被优先连接,因为连接的成本和结果集的大小性能关系很大,所以一旦能够缩小结果集,则该缩小结果集的条件会被优先考虑。

 

规则二:被外连接的表在连接顺序上优先,比如contries表和customers表,如果你希望能够列出所有的国家和该国家的客户,这个就是一个对于contries的外连接,因为有些国家可能并没有客户,如果以customers来做驱动表,结果将只能列出所有有客户的国家,所以只能用contries来做驱动表,这个应该说甚至不是一种优化的方法,因为不这么执行将不能够得出正确结果,因而只能这么执行。

 

规则三:from语句后面的表越多,则连接的可能执行计划越多,因此SQL语句的解析时间越长,如两个表,连接可能性就是2!=2,而三个表的连接可能性则是3!=6,随着表的数量的增加,可能的执行计划的数量也大大增加,从而无论是从parse时间或者针对每一个执行计划去计算成本的时间也大大增加。

 

表连接的方法

把表进行连接的方法主要有三种

Nested Loop:NL连接的方法是,首先需要有一个驱动表(Driving Table或者Outer Table),然后在扫描驱动表的一条记录的时候,再去查找另一个表(又称内部表或inner Table)的相匹配的记录,这样来产生结果集,如下图:

 

一般而言,驱动表一般使用全表扫描来访问,而内部表则使用连接键上的索引来访问。当连接是非等连接的时候,有可能在内部表上也使用全表扫描从而极大降低性能。 当被连接的数据集比较少而且连接条件可以被用于非常高效地获取第二个表的数据的时候,适合使用NL连接,从NL连接的解释可以看出,从驱动表的一条记录高效地获取内部表的记录的效率是保证NL连接效率的关键,所以表连接的顺序对于NL连接是至关重要的,当你觉得优化器的选择顺序不好的时候,可以使用hint USE_NL(T1,T2)来让执行计划按照你设定的方式执行。

 

Hash Join:Hash Join的原理是Oracle优化器使用连接键在较小的表上在内存里建立一个哈希表(就像是较小表上建立在连接键上的哈希索引,只不过索引里没有rowid,而是包含了所有的数据);然后在扫描大表的过程中去根据每一个连接值去内存的哈希表里找到相应的记录进行匹配(大家应该还记得对于“等于”操作而言哈希索引是相当快的操作),从哈希连接的上面的原理上看哈希连接只适用于“等于”连接,当被连接的数据集很大的时候,使用哈希连接相对比于NL连接而言能够得到比较好的性能,可以使用hint USE_HASH来强制优化器选择哈希连接。

 

Sort Merge Join:Sort Merge连接是另一种比较有意思的连接类型,在某些时候可以很快地完成连接的任务。它首先是需要把被连接的两个表进行排序,然后在排序表上进行连接操作(所以Sort Merge连接最影响性能的部分是排序部分,因为很多时候排序是非常昂贵的操作,比如有可能使用磁盘去缓冲排序的中间结果,以Oracle的专业术语来讲叫做不能进行one-pass的排序性能会比较慢,所以如果可以使用某种机制去掉排序部分,比如使用某个索引,Sort Merge连接的性能就会大大增加),为了清楚说明Sort Merge连接的原理,我们举个例子:假设有两种表T1,T2,连接键是col1,首先Sort Merge连接需要做的是对于T1对于col1形成一个排序后的结果集,然后针对T2形成一个排序后的结果集,假如在我们的结果集里,T1表里col1的值是1~10,T2表里col2的值是20~100,则排序后,按照col1形成的两个结果集之一是以1~10开始,而另一个结果集是以20~100开始,这样无论是寻找两个结果集之间的相等关系还是大于小于关系都是非常容易的,在这个例子里根本不可能有等于关系,因为(1~10)显然不可能等于(20~100),从而就可以很快完成连接的操作,这个是我想出来的说明Sort Merge连接的一个例子,希望能够把Sort Merge说得清楚明白些了。

从上面的例子可以看出,Sort Merge连接在有些时候是非常高效的,特别是对于哈希连接所不能满足的“非等于”类型连接!

 

最后需要说明的是,对于一个特点的SQL连接语句,很多时候你不能够说使用某种连接手段就一定性能最快,因为连接的性能本身还受非常多参数的影响(比如我们以前说过的PGA_AGG*参数将会影响哈希连接和sort-merge连接的性能等),所以知道这些连接的工作原理,将有助于你对于应该使用什么连接最有效进行判断,但是这个判断不是绝对的,否则优化器就能够自己做出判断了,当你自己对于所用到的表的数据量,数据值的分布,每一个中间结果的大小等知道得越清楚,你就越能够把这些信息以某种方式告诉优化器,从而帮助它选择一个正确的执行计划,这个就是数据库优化的本质!

 

写到这里,我不知道是否还会继续写这个主题的下一篇文章了,所以我也不知道这是不是文章的结尾?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值