码迷,mamicode.com
首页 > 数据库 > 详细

Informix 11.5 SQL 语句性能监控方法及实现

时间:2016-06-16 09:15:49      阅读:180      评论:0      收藏:0      [点我收藏+]

标签:

我们知道,在数据库应用系统中,SQL 语句的性能好坏至关重要。如果 SQL 语句性能很差,可能会导致整个数据库应用系统的性能也非常差。那么,如何监控数据库系统中 SQL 语句的性能,导致 SQL 语句性能差的原因是什么? SQL 语句运行过程中对系统资源的使用情况如何?系统资源存在哪些瓶颈?在 Informix 11.5 中,主要提供了两个工具来解决上述问题。一个是 set explain 命令,我们可以通过查看数据库的查询计划来分析导致 SQL 语句性能差的原因并给予相应的调整,另一个是 SQL 下钻查询特性,通过它,我们可以分析系统中哪些 SQL 语句执行比较慢、SQL 语句执行的时间是多少、SQL 语句运行时对资源的占用情况及系统存在的瓶颈是什么并及时进行相应的调整。下面,我们具体来看一下这两种监控工具的具体使用方法,希望对大家能有所帮助。

SET EXPLAIN 命令

当我们发现系统中某一个或一组 SQL 语句性能比较差时,我们往往会使用 set explain 命令来查看一下 SQL 语句的查询计划,看看 SQL 语句性能差的原因是什么并进行相应的调整。在 Informix 中,查询计划主要包括访问计划(access plan)及表连接计划(join plan)。访问计划是指 Informix 数据库是通过什么方法来读取磁盘上的数据。一般来讲,Informix 主要提供以下几种访问计划:

  • 顺序扫描(Sequential scan):数据库服务器按照物理顺序读取表中的所有记录。
  • 索引扫描(Index scan):数据库服务器读取索引页,并通过相应的 ROWID 来读取相关的记录。
  • 键值扫描(Key-only index scan):如果读取的相关数据包含在索引节点中,数据库服务器就只需读取索引,不需要再去读取相应的数据页。
  • 键优先扫描(Key-first index scan):键优先扫描是一种索引扫描,它首先使用索引键过滤器来减少查询读取的数据量。
  • 自动索引扫描(Auto-index scan):自动索引扫描特性允许数据库服务器在一个或多个字段上自动创建临时索引,数据库服务器通过这个临时索引读取相应的数据。这个临时索引只在查询过程中生效。该特性在一些 OLTP 批处理操作中特别有意义,它一方面利用到索引,另一方面又不需要索引维护的开销。

Informix 主要提供两种表连接计划:

  • 嵌套循环连接(nested-loop join):在嵌套循环连接中,将扫描第一个(或外部)表,以查找满足查询规则的行。对于在外部表中找到的每一行,数据库服务器将在第二个(或内部)表中搜索其相应的行。通过索引扫描还是表扫描来访问外部表则取决于该表。如果有过滤器,数据库服务器首先会应用它们。如果内部表没有索引,那么数据库服务器就会将在表上构建索引的成本与连续扫描的成本进行比较,然后选择成本最低的那一种方法。总成本取决于连接列上是否有索引。如果连接列上有一个索引,那么其成本会相当低;否则,数据库服务器就必须对所有表(外部和内部表)执行表扫描。
  • 哈希连接(hash join):当一个或多个连接表上没有索引时,或者当数据库服务器必须从所有连接表中读取大量行时,就使用这种方法。在该方法中,需要扫描其中的一个表,通常扫描较小的那个表,用它在内存中创建一个哈希表。通过哈希函数,将具有相同哈希值的行放在一个 bucket 中。在扫描完第一个表并将它放在哈希表中之后,就扫描第二个表,并在哈希表中查找该表中的每一行,看是否可以进行连接。如果连接中有更多表,那么数据库服务器将对每个连接表执行相同的操作。

哈希连接包含两个动作:构建哈希(或者是我们所称的构建阶段),以及探测哈希表(或探测阶段)。在构建阶段,数据库服务器读取一个表,并且在应用所有现有过滤器之后,在内存中创建一个哈希表。可以在概念上将哈希表认为是一系列的 bucket,每个 bucket 所拥有的地址是通过应用哈希函数从键值导出的。在探测阶段,数据库服务器将读取连接中的其他表,如果存在过滤器,就应用它们。在满足过滤器条件的每个行中,数据库服务器将对键应用哈希函数,并探测哈希表以查找匹配的键值。如果读取的相关数据包含在索引节点中,数据库服务器就只需读取索引,不需要再去读取相应的数据页。

如何获取查询计划

在 Informix 中,我们可以使用 SET EXPLAIN 语句或 EXPLAIN 伪指令来显示优化程序所选择的查询计划。

使用 SET EXPLAIN  

SET EXPLAIN 语句的输出显示了查询优化程序作出的决策。它显示了是否已使用并行扫描、响应查询所需的最大线程数以及用于查询的连接类型。您可以使用 SET EXPLAIN 来查看应用程序的查询计划。

SET EXPLAIN 语句的基本语法:

>>-SET EXPLAIN--------------------------------------------------> 
 
 >--+-+-OFF-------------------+-------------------------+------->< 
   | ‘-ON--+---------------+-‘                         |  
   |       ‘-AVOID_EXECUTE-‘                           |  
   ‘-FILE TO--+-‘filename ‘--+--+--------------------+-‘

其中:

  • ON :为每个后续查询生成评估并将结果写入当前目录中的输出文件。如果文件已经存在,那么新输出会附加到现有文件。
  • AVOID_EXECUTE :防止 SELECT、INSERT、UPDATE 或 DELETE 语句在数据库服务器将查询计划打印到输出文件中时执行。
  • OFF :终止 SET EXPLAIN 语句的活动,以便不再为后续查询生成评估或不再将评估写入输出文件 。
  • FILE TO :为每个后续查询生成评估并使您能够指定说明输出文件的位置。

在发出 SET EXPLAIN OFF 语句或程序结束之前,来自 SET EXPLAIN ON 语句的输出将定向到适当的文件。如果没有输入 SET EXPLAIN 语句,那么缺省行为是 OFF,并且数据库服务器不会为查询生成评估。

SET EXPLAIN 语句在数据库服务器优化阶段期间执行,该优化阶段在您启动查询时开始。对于与游标相关的查询,如果查询已准备好且没有主变量,那么优化在您准备期间发生。否则,优化在您打开游标时发生。

如果用户希望了解下述 SQL 语句的查询计划并执行下述 SQL 语句,我们可以执行:

SET EXPLAIN ON; 
 SELECT * FROM customer, orders 
   WHERE customer.customer_num = orders.customer_num 
   AND customer.lname = "Higgins";

如果用户希望了解下述 SQL 语句的查询计划但不希望执行下述 SQL 语句,我们可以执行:

SET EXPLAIN ON AVOID_EXECUTE; 
 SELECT * FROM customer, orders 
   WHERE customer.customer_num = orders.customer_num 
   AND customer.lname = "Higgins";

如果用户希望了解下述 SQL 语句的查询计划,并将结果输出到自己指定的位置,我们可以执行:

SET EXPLAIN ON AVOID_EXECUTE; 
 SET EXPLAIN FILE TO ‘/tmp/explain.out‘ 
 SELECT * FROM customer, orders 
   WHERE customer.customer_num = orders.customer_num 
   AND customer.lname = "Higgins";

如果用户不再希望了解下述 SQL 语句的查询计划,我们可以执行:

SET EXPLAIN OFF;

当执行 SPL 例程时,它已经优化。要显示包含在 SPL 例程中的每个 SQL 语句的查询计划,请在执行以下任何 SQL 语句(它们总是试图优化 SPL 例程)之前执行 SET EXPLAIN ON 语句:

CREATE PROCEDURE 
 UPDATE STATISTICS FOR PROCEDURE

例如:使用以下语句显示 SPL 例程的查询计划:

SET EXPLAIN ON; 
 UPDATE STATISTICS FOR PROCEDURE procname;

使用 AVOID_EXECUTE 选项

SET EXPLAIN ON AVOID_EXECUTE 语句为会话激活 Avoid Execute 选项,或直到执行下一个不带 AVOID_EXECUTE 的 SET EXPLAIN OFF(或 ON)。 AVOID_EXECUTE 关键字将使得 DML 语句无法执行;数据库服务器会将查询计划打印到输出文件中。如果为包含远程表的查询激活 AVOID_EXECUTE,那么查询不会在本地或远程站点执行。

如果设置了 AVOID_EXECUTE,数据库服务器会发送警告消息。如果您正在使用 DB-Access,它会对所有选择、删除、更新或插入查询操作显示文本消息:

Warning! avoid_execute has been set

根据 ESQL,sqlwarn.sqlwarn7 字符设置为“ W ”。

使用 SET EXPLAIN ON 或 SET EXPLAIN OFF 语句可关闭 AVOID_EXECUTE 选项。 SET EXPLAIN ON 语句将关闭 AVOID_EXECUTE 选项,但会继续生成查询计划并将结果写入输出文件。

如果在 SPL 例程中发出 SET EXPLAIN ON AVOID_EXECUTE 语句,那么 SPL 例程和所有 DDL 语句仍然执行,但该 SPL 例程内的 DML 语句将不执行。数据库服务器会将 SPL 例程的查询计划打印到输出文件。要关闭此选项,必须在 SPL 例程外执行 SET EXPLAIN ON 或 SET EXPLAIN OFF 语句。如果在执行 SPL 例程之前执行 SET EXPLAIN ON AVOID_EXECUTE 语句,那么 SPL 例程内的 DML 语句将不执行,并且数据库服务器不会将 SPL 例程的查询计划打印到输出文件中。

当 AVOID_EXECUTE 生效时仍然计算查询中的恒定函数,因为数据库服务器会在进行优化之前计算这些函数。

例如,尽管没有执行以下 SELECT 语句,仍计算 func( ) 函数:

SELECT * FROM orders WHERE func(10) > 5;

如果在打开 ESQL/C 程序中的游标之前执行 SET EXPLAIN ON AVOID_EXECUTE 语句,那么每个 FETCH 操作将返回找不到行的消息。但是,如果在 ESQL/C 程序打开游标后执行 SET EXPLAIN ON AVOID_EXECUTE,那么此语句不会影响游标,并将继续返回行。

sqexplain.out 文件

在 UNIX 系统中,数据库服务器将 SET EXPLAIN ON 语句或 EXPLAIN 伪指令的输出写入到 sqexplain.out 文件。

如果客户机应用程序和数据库服务器在同一计算机上,那么 sqexplain.out 文件存储在当前目录中。如果您正使用版本 5.x 或更早版本的客户机应用程序并且 sqexplain.out 文件没有出现在当前目录中,那么请检查您的主目录查找该文件。

当当前数据库在另一台计算机上时,sqexplain.out 文件将存储在远程主机上的主目录中。

在 Windows 系统中,数据库服务器将 SET EXPLAIN ON 语句或 EXPLAIN 伪指令的输出写入到文件%InformixDIR%\sqexpln\username.out

sqexplain.out 文件内容。

sqexplain.out 文件主要分为 3 个部分:

部分 1:

部分 1 包含以下内容,它们的顺序与以下列出的顺序相同:

  • 用于查询的 SQL 语句。
  • 以优化器用来比较计划的单位表示的查询成本估计值。这些单位代表查询执行的相对时间,每个单位大约相当于一次典型的磁盘存取时间。优化器选择某个查询计划是因为执行这个计划的估计成本在所有评估的计划中是最低的。
  • 期望查询产生的行数估计。
  • 执行查询所派生的最大数量的线程(如果设置了 PDQPRIORITY 的话)。
  • 用于执行 ORDER BY 和/或 GROUP BY (如果需要的话)的临时文件可选项。

部分 2:

表在这里是按访问它们的顺序列出的。对于每个表,列出了所应用的过滤器。

  • 已访问表的名称
  • 数据库服务器读取表所采取的访问计划 — 顺序扫描、索引路径和自动索引。另外,如果表是分段的,则在这里列出对于这一特定查询所要访问的活动分段。
  • 列出了每对表的连接计划:嵌套循环连接或动态哈希连接连接。对于动态哈希连接连接也列出了执行哈希连接连接所用到的过滤器。

部分 3:

根据所选存取计划的不同,这一部分的内容会有所不同。对于部分 2 中的每一个表,这一部分会出现一次。

对于顺序扫描:本部分包含要应用的过滤器(如果有的话)。如果子查询是过滤器的一部分,会在这里扩展它,它会象主查询一样包括所有的部分。

对于索引扫描和自动索引扫描:对于索引和自动索引扫描,这一部分包含以下信息:

  • 一些索引键,将对它们应用过滤器,跟着是以下项中的一个或全部:
  • 仅键项,如果它是只用到键的索引扫描。
  • 聚合项,如果查询聚合了索引键。
  • 键优先项,如果对索引键应用键优先过滤器。
  • 下限索引过滤器(如果有的话)。如果子查询是过滤器的一部分,会在这里扩展它,它会象主查询一样包括所有的部分。
  • 上限索引过滤器(如果有的话)。如果子查询是过滤器的一部分,会在这里扩展它,它会象主查询一样包括所有的部分。
  • 要应用的键优先过滤器(如果有的话)。如果子查询是过滤器的一部分,会在这里扩展它,它会象主查询一样包括所有的部分。

EXPLAIN 伪指令

如果希望仅显示一个 SQL 语句的查询计划时,可以使用 EXPLAIN 伪指令代替 SET EXPLAIN ON 或 SET EXPLAIN ON AVOID_Execute 语句。

使用 EXPLAIN 伪指令按以下方式显示查询计划:

  • EXPLAIN :显示优化程序所选择的查询计划。
  • EXPLAIN AVOID_EXECUTE :显示优化程序所选择的查询计划,但是不执行查询。

在 Informix 中,伪指令可以采用如下方法表示:

--+directive text 
 {+directive text} 
 /*+directive text */

下边例子显示了使用 EXPLAIN 伪指令的查询语句:

select --+ explain 
 l.customer_num, l.lname, l.company,l.phone, r.call_dtime, r.call_descr 
 from customer l, cust_calls r where l.customer_num = r.customer_num

下边例子显示了使用 EXPLAIN AVOID_Execute 伪指令的查询语句:

select --+ explain,avoid_execute 
 l.customer_num, l.lname, l.company,l.phone, r.call_dtime, r.call_descr 
 from customer l, cust_calls r where l.customer_num = r.customer_num

下边显示了使用 EXPLAIN AVOID_EXECUTE 伪指令的查询的 sqexplain.out 文件输出样本:

QUERY: 
 ------ 
 select --+ explain ,avoid_execute 
 l.customer_num, l.lname, l.company,l.phone, r.call_dtime, r.call_descr 
 from customer l, cust_calls r where l.customer_num = r.customer_num 
 DIRECTIVES FOLLOWED: 
 EXPLAIN 
 AVOID_EXECUTE 
 DIRECTIVES NOT FOLLOWED: 

 Estimated Cost: 7 
 Estimated # of Rows Returned: 7 

  1) Informix.r: SEQUENTIAL SCAN 

  2) Informix.l: INDEX PATH 

    (1) Index Keys: customer_num   (Serial, fragments: ALL) 
        Lower Index Filter: Informix.l.customer_num = Informix.r.customer_num 
 NESTED LOOP JOIN

下表对上述 sqexplain.out 文件中描述所选的查询计划的相关输出行进行了描述。

输出行所选的查询计划描述
DIRECTIVES FOLLOWED: EXPLAIN AVOID_EXECUTE 使用伪指令 EXPLAIN 和 AVOID_EXECUTE 来显示查询计划,而不执行查询。
Estimated # of Rows Returned: 7 估计该查询返回 7 行。
Estimated Cost: 7 该估计成本值为 7,优化程序使用该值来比较不同查询计划并选择成本最低的查询计划。
1) Informix.r: SEQUENTIAL SCAN 将 cust_calls r 表用作外表并对它进行扫描以获取每一行。
2) Informix.l: INDEX PATH 对于外表中的每一行,请使用索引获取内表 customer l 中的匹配行。
(1) Index Keys: customer_num (Serial, fragments: ALL) 使用 customer_num 列的索引,对其进行连续扫描,并扫描所有的分段(customer l 表只有一个分段组成)。
Lower Index Filter: Informix.l.customer_num = Informix.r.customer_num 从外表的 customer_num 值开始进行索引扫描。
NESTED LOOP JOIN 采用嵌套循环连接方式

onmode -Y:动态更改 SET EXPLAIN

如果用户没有访问 SQL 源代码的权限,那么数据库管理员可以通过使用运行 SQL 代码的 onmode -Y 命令动态地设置 SET EXPLAIN 。对于单独的会话,我们也可以使用 onmode -Y 命令动态更改 SET EXPLAIN 语句的值。

onmode -Y 命令基本语法:

调用解释
onmode -Y sessionid 2 打开对 sessionid 的 SET EXPLAIN,并且仅显示查询计划
onmode -Y sessionid 1 打开对 sessionid 的 SET EXPLAIN
onmode -Y sessionid 0 关闭对 sessionid 的 SET EXPLAIN

当使用 onmode -Y 命令打开 SET EXPLAIN 时,输出显示在sqexplain.out.sessionid 文件中。

如果希望动态对 session 30 打开 SET EXPLAIN ON AVOID_Execute 特性,我们可以运行:

onmode – Y 30 2

如果希望动态对 session 30 打开 SET EXPLAIN ON 特性,我们可以运行:

onmode – Y 30 1

如果希望动态对 session 30 打开 SET EXPLAIN OFF 特性,我们可以运行:

onmode – Y 30 0

显示查询统计信息

在 Informix ONCONFIG 配置文件中的 EXPLAIN_STAT 配置参数用来控制是否在 sqexplain.out 文件中包含“查询统计信息”。 如果启用 EXPLAIN_STAT 配置参数,那么查询统计信息部分将出现在 sqexplain.out 文件中。 sqexplain.out 文件中的“查询统计信息”部分显示了查询计划预期返回的估计行数、返回的实际行数和查询的其他信息。可以使用这些信息(显示查询计划综合流量以及查询的每个阶段通过的行流量数)来调试性能问题。

EXPLAIN_STAT 配置参数可以设置为下列值:

描述
0 禁用查询统计信息的显示
1 启用查询统计信息的显示

我们也可以通过修改 onconfig 文件来修改此值,也可以通过 onmode – wf 及 onmode – wm 命令动态设置该值。

onmode – wf EXPLAIN_STAT= 1 
 onmode – wm EXPLAIN_STAT= 1

查询统计信息只有在 SQL 语句执行之后才可以生成。当执行 set explain on 及 onmode – Y session_id 1 时,sqexplain.out 文件中会包含“查询统计信息”。

以下示例显示了 SET EXPLAIN 输出中的查询统计信息。如果已扫描或已连接的估计行数和实际行数相差很大,那么关于这些表的统计信息可能是旧的,应该更新它们。

select * from tab1, tab2 where tab1.c1 = tab2.c1 and tab1.c3 between 0 and 15 

 Estimated Cost: 104 
 Estimated # of Rows Returned: 69 

  1) zelaine.tab2: SEQUENTIAL SCAN 

  2) zelaine.tab1: INDEX PATH 

    (1) Index Keys: c1 c3   (Serial, fragments: ALL) 
        Lower Index Filter: (zelaine.tab1.c1 = zelaine.tab2.c1 
                             AND zelaine.tab1.c3 >= 0 ) 
        Upper Index Filter: zelaine.tab1.c3 <= 15 
 NESTED LOOP JOIN 

 Query statistics: 
 ----------------- 

  Table map : 
  ---------------------------- 
  Internal name     Table name 
  ---------------------------- 
  t1                tab2 
  t2                tab1 

  type     table  rows_prod  est_rows  rows_scan  time       est_cost 
  ------------------------------------------------------------------- 
  scan     t1     50         50        50         00:00:00   4       

  type     table  rows_prod  est_rows  rows_scan  time       est_cost 
  ------------------------------------------------------------------- 
  scan     t2     67         69        4          00:00:00   2       

  type     rows_prod  est_rows  time       est_cost 
  ------------------------------------------------- 
  nljoin   67         70        00:00:00   104

查询计划样例分析

下边我们例举一些查询计划的样例,希望能够对查询计划能够有一个比较全面的了解。

单表查询

下边显示了 customer 表上复杂查询的 SET EXPLAIN 输出:

QUERY: 
 ------ 
 SELECT fname, lname, company FROM customer 
 WHERE company MATCHES ‘Sport*‘ AND 
    customer_num BETWEEN 110 AND 115 
 ORDER BY lname 

    Estimated Cost: 1 
    Estimated # of Rows Returned: 1 
 Temporary Files Required For: Order By 

  1) virginia.customer: INDEX PATH 

        Filters: virginia.customer.company MATCHES ‘Sport*‘ 

    (1) Index Keys: customer_num   (Serial, fragments: ALL) 
 Lower Index Filter: virginia.customer.customer_num >= 
 110 
 Upper Index Filter: virginia.customer.customer_num <= 
 115

以下输出行显示了该查询的索引扫描的范围:

  • 以索引键值 110 开始索引扫描 :

    Lower Index Filter: virginia.customer.customer_num >= 110

  • 以索引键值 115 停止索引扫描 :

    Upper Index Filter: virginia.customer.customer_num <= 115

多表查询

下边显示了多表查询的 SET EXPLAIN 输出:

QUERY: 
 ------ 
 SELECT C.customer_num, O.order_num, SUM (I.total_price) 
 FROM customer C, orders O, items I 
 WHERE C.customer_num = O.customer_num 
 AND O.order_num = I.order_num 
 GROUP BY C.customer_num, O.order_num 

 Estimated Cost: 78 
    Estimated # of Rows Returned: 1 
 Temporary Files Required For: Group By 

  1) virginia.o: SEQUENTIAL SCAN 

  2) virginia.c: INDEX PATH 

    (1) Index Keys: customer_num   (Key-Only)  (Serial, fragments: ALL) 
      Lower Index Filter: virginia.c.customer_num = virginia.o.customer_num 
 NESTED LOOP JOIN 

  3) virginia.i: INDEX PATH 

    (1) Index Keys: order_num   (Serial, fragments: ALL) 
        Lower Index Filter: virginia.o.order_num = virginia.i.order_num 
 NESTED LOOP JOIN

SET EXPLAIN 输出列出数据库服务器访问表的顺序和读取每个表的存取计划。上边输出的计划指示数据库服务器将执行以下操作:

  • 数据库服务器将首先读取orders表。

    因为orders表上没有过滤器,所以数据库服务器必须读取所有的行。按物理顺序读取表是成本最低的方法。

  • 对于orders的每一行,数据库服务器将在 customer 表中搜索匹配的行。

    搜索使用customer_num的索引。标志仅键意味着对于 customer 表只需要读取索引,因为只有c.customer_num列用在连接和输出中且该列是索引键。

  • 对于orders中具有一个匹配customer_num的每一行,数据库服务器将使用order_num的索引在items表中搜索匹配行。

键优先扫描

键优先扫描是一种索引扫描,该扫描使用未列为低索引过滤器和高索引过滤器的键。下边显示了使用键优先扫描的样本查询:

select * from tab1 where (c1 > 0) and ( (c2 = 1) or (c2 = 2)) 
 Estimated Cost: 4 
    Estimated # of Rows Returned: 1 

 1) pubs.tab1: INDEX PATH 

    (1) Index Keys: c1 c2   (Key-First)  (Serial, fragments: ALL) 
		 Lower Index Filter: pubs.tab1.c1 > 0 
		 Index Key Filters:  (pubs.tab1.c2 = 1 OR pubs.tab1.c2 = 2)

在此示例中数据库服务器将首先通过应用附加的键过滤器尝试减少可能的行数。数据库服务器使用索引来应用附加的过滤器 c2 = 1 OR c2 = 2 之后才读取行数据。

子查询的查询计划

如果连接的成本较低,那么优化程序可自动将子查询更改成连接。下边例子中的 SET EXPLAIN ON 语句的样本输出显示了优化程序将子查询中的表更改成连接中的内表:

QUERY: 
 ------ 
 SELECT company, fname, lname, phone 
 FROM customer c 
 WHERE EXISTS( 
   SELECT customer_num FROM cust_calls u 
      WHERE c.customer_num = u.customer_num) 

 Estimated Cost: 6 
 Estimated # of Rows Returned: 7 

 1) virginia.c: SEQUENTIAL SCAN 

  2) virginia.u: INDEX PATH  (First Row) 

    (1) Index Keys: customer_num call_dtime   (Key-Only)  (Serial, fragments: ALL) 
          Lower Index Filter: virginia.c.customer_num = virginia.u.customer_num 
 NESTED LOOP JOIN  (Semi Join)

当优化程序将子查询更改成连接时,它可以使用存取计划和连接计划的几种变形形式:

  • 首行扫描

    首行扫描是表扫描的一种变形形式。当数据库服务器找到一个匹配时,表扫描将停止。

  • 忽略副本索引扫描

    忽略副本索引扫描是索引扫描的一种变形形式。数据库服务器不扫描副本。

  • 半连接

    半连接是嵌套循环连接的一种变形形式。当第一个匹配找到时,数据库服务器将停止内表扫描。

SQL 语句优化

通过对 SQL 语句查询计划的分析,我们可以知道 SQL 语句在执行过程中是采用什么样的访问方法,是顺序扫描还是索引扫描;表之间连接采用什么样的方法,是嵌套循环连接还是哈希连接;表之间访问的顺序是什么;是否产生了临时表;该查询的成本是多少。依此,我们就可以考虑,为了提高 SQL 语句性能,我们是不是要创建合适的索引, 是不是要调整一下表之间连接的顺序,是不是要修改一下 SQL 语句的写法等。通常,我们在调整时,可以比较一下改变之前及改变之后的查询成本,保证查询成本有一个明显的减少。另外,我们还可以通过设置 OPTCOMPIND 参数来指定数据访问方法 ;通过访问计划指示、连接次序指示、连接计划指示、目标指示来指定数据访问方法、表连接顺序、表连接方法及数据返回结果集;通过执行 update statistics 语句提高 SQL 语句性能。关于 OPTCOMPIND 参数及查询指示的具体使用方法,请参考 Informix 信息中心相关内容。

 

SQL 下钻查询特性

在 SQL 语句性能监控时,我们经常要了解 SQL 语句执行了多长时间; SQL 语句运行时占用了多少系统资源,如 CPU 占用情况、内存占用情况、磁盘 I/O 读写情况; SQL 语句等待系统资源如磁盘 I/O 及锁的时间及次数等。通过 SQL 语句对系统的资源使用及等待情况,我们可以了解到 SQL 语句运行的瓶颈,并及时调整系统资源配置,或者调整用户的应用程序。我们上面介绍的 set explain 方法,可以帮助我们了解一些 SQL 语句性能问题,但是当我们启用 SET EXPLAIN 功能时,SQL 语句性能可能已经出现了问题,为了能够让 DBA 更及时、更详细地了解 SQL 语句的资源使用情况并做出相应的调整,在 Informix 中,提供了 SQL 下钻查询特性来满足上述功能。

SQL 下钻查询特性可以收集关于系统上执行的每个 SQL 语句的统计信息,并分析语句历史。它可以帮助您回答如下问题:

  • SQL 语句需要多长时间
  • 各个语句使用多少资源?
  • 等待每个资源需要多长时间?
  • 查询计划是什么?

统计信息存储在循环缓冲区(内存中名为 syssqltrace 的伪表)中,即存储在 sysmaster 数据库中。您可以动态地调整循环缓冲区的大小。

缺省情况下,该功能处于关闭状态,但是您可以对所有用户或一组特定用户将其打开。在启用带有缺省配置的该功能时,数据库服务器跟踪运行的上 1000 条 SQL 语句以及这些语句的概要统计信息,每个 SQL 语句占用 1K 大小的空间。

如果您想要保存大量历史信息,那么该功能需要的内存较大。 SQL 历史跟踪所需的缺省空间量为 1 兆字节。您可以根据需求增加或减少存储量。如果不想要对此使用内存,那么可以禁用 SQL 历史跟踪。

  使用 SQLTRACE 配置参数指定启动 SQL 跟踪信息

我们可以通过修改 $InformixDIR/etc/$ONCONFIG 文件中的 SQLTRACE 配置参数来控制数据库服务器启动时的缺省跟踪行为。 所设置的信息包括要跟踪的 SQL 语句数目和跟踪方式。

SQLTRACE 配置参数语法:

SQLTRACE [Level=off|low|med|high], 
 [Ntraces=number of traces], 
 [Size=size of each trace buffer],[Mode=global|user]

其中:

level字段,可以指定以下某个值:

  • Low:它用于捕获语句统计信息、语句文本和语句迭代器。当启用 SQL 跟踪时,它是缺省的跟踪级别。
  • Medium:此跟踪级别捕获低级跟踪中包含的所有信息,再加上表名、数据库名称和存储过程堆栈。
  • High:此跟踪级别捕获中级跟踪中包含的所有信息,再加上主变量。
  • Off:这不指定 SQL 跟踪。系统缺省为 OFF

ntraces字段,指定要跟踪的 SQL 语句的数目,其范围是 : 500 -2147483647 。

size字段, 指定跟踪缓冲区大小的千字节数。每个 SQL 语句的跟踪信息使用一个跟踪缓冲区,如果超过了此缓冲区大小,那么数据库服务器丢弃已保存的数据。其范围是 : 1K-100K 。

mode字段, 指定以下任意一项:

  • Global:跟踪系统上的所有用户,它是缺省值。
  • User:跟踪指定用户(如果想要获取一小组用户正在运行的 SQL 样本,那么指定此项)。

在设置 SQLTRACE 参数时,我们需要考虑以下一些内容:

  • 关于 SQLTRACE 缓冲区的问题:

    我们收集到的统计信息都保存在内存的缓冲区中,它的大小 =Ntraces*Size,如果收集的语句数量越多、收集信息越详细,需要的缓冲区就越大,占用的内存资源也就越多。因此,在配置时,要考虑自己的实际情况来选择。另外,这个缓冲区是一个循环缓冲区,当缓冲区大小不够时,它会将旧的信息丢掉,因此,如果需要保存 SQL 语句跟踪历史,要考虑它的大小问题。当我们将 SQL 跟踪特性关闭时,保存在 SQLTRACE 缓冲区中的信息也会丢掉,如果需要保存,在关闭 SQL 跟踪特性关闭前,请将这些信息保存到表或文件中,防止丢失。

  • 关于 size 参数

    size 参数的大小主要由 SQL 语句的大小及 Level 来决定。不同的 level,所收集的信息量不同,Medium 会比 low 收集的信息量大,同样,High 会比 Medium 收集的信息量大。因此,在选择时,要充分考虑好我们收集统计信息的用途。通常,Low Level 比较适合于错误诊断及性能调优,High Level 常用来做系统负载重现功能。 Medium Level 也主要用于一些错误诊断场合。

  • 关于 Mode 参数

    global 模式用于跟踪系统上所有用户的统计信息,因此,当在一个比较繁忙的系统上,可能很快就会收集到大量的信息,同时,所有用户的信息都包含在一起,分析时也比较繁琐,通常,选择 global 模式,主要用于比较系统上所有会话的资源使用情况,或者是我们不清楚要分析哪一个具体用户的资源使用情况。一般情况下,我们会使用 user 模式,这样,我们分析起来会比较清晰,同时,占用的系统资源也不会太多。

以下语句指定了数据库服务器将收集关于系统上所有用户执行的最多 2000 条 SQL 语句的低级别信息,并将分配大约 4 兆字节的内存(2000 * 2 千字节)。

SQLTRACE level=LOW,ntraces=2000,size=2,mode=global

以下语句指定了数据库服务器将收集关于系统上所有用户执行的最多 2000 条 SQL 语句的高级别信息,并将分配大约 4 兆字节的内存(2000 * 2 千字节)。

SQLTRACE level=high,ntraces=2000,size=2,mode=global

使用 SQLTRACE 配置参数方式比较适合于设置一些缺省的配置,如果需要经常变化,使用 ADMIN API 命令方式则比较方便。

使用 ADMIN API 命令指定启动 SQL 跟踪信息

如果不想设置 SQLTRACE 配置参数以重新启动服务器,那么可执行以下 ADMIN API 命令,该命令可提供与设置 SQLTRACE 相同的功能。使用 ADMIN API 的 task() and admin() 函数可以动态改变 SQLTRACE 的设置,不需要重新启动服务器,使用更加灵活。只有 Informix 用户有权力执行 ADMIN API 命令。通过 ADMIN API 命令修改的 SQLTRACE 的设置不会保存到 SQLTRACE 配置参数中,因此,当服务器重新启动后,SQLTRACE 配置参数值将会生效。

当 SQLTRACE 配置参数为 OFF,我们通过下面的 ADMIN API 命令启动 SQL 跟踪信息时,系统会使用缺省的设置值。 即:

SQLTRACE level=low,ntraces=1000,size=1,mode=global

execute function task("set sql tracing on");

我们也可以指定自己设置的值。以下语句指定了数据库服务器将收集关于系统上所有用户执行的最多 2000 条 SQL 语句的低级别信息,并将分配大约 4 兆字节的内存(2000 * 2 千字节)。

execute function task("set sql tracing on", 2000,"2k","low","global");

停止收集 SQL 语句信息,我们可以执行:

execute function task("SET SQL TRACING OFF");

当执行上述命令后,会关闭 SQL 跟踪功能,同时,跟踪缓冲区中的内容会丢失。

启用特定用户的 SQL 历史跟踪

启用特定用户的 SQL 历史跟踪,不仅可以节省系统内存资源,而且分析起来也更加清晰,一般我们会建议采用这种方式。在以用户方式启用 SQL 跟踪系统后,就可以启用对特定用户的跟踪。在指定 user 作为 SQLTRACE 配置参数中的方式后,必须执行管理 API task() 或 admin() 函数来打开对特定用户的 SQL 历史跟踪。

如果全局 SQL 跟踪被禁用,那么可执行管理 API task() 或 admin() 函数来启用对特定用户的 SQL 跟踪。

要启用特定用户的 SQL 历史跟踪,您可以执行 task 或 admin() 函数,并指定 set sql tracing on 和定义用户的信息。

如果需要对 session 30 启用 SQL 语句跟踪,我们可以执行:

execute function task("set sql tracing on", 1000, 1,"low","user"); 
 execute function task("set sql user tracing on",30)

如果需要对当前连接到系统的用户(只要它们未作为用户 root 或 Informix 登录)启用 SQL 语句跟踪,我们可以执行:

dbaccess sysadmin -<<END 
 execute function task("set sql tracing on", 1000, 1,"low","user"); 
 select task("set sql user tracing on", sid) 
 FROM sysmaster:syssessions WHERE username not in ("root","Informix"); 
 END

如果需要停止对 session 30 进行跟踪,我们可以执行:

execute function task( “ set sql user tracing off",30);

SQL 跟踪信息显示及分析

我们可以通过执行 onstat -g his 命令或查询sysmaster数据库中的 syssqltrace 伪表来获取 SQL 下钻查询信息。

onstat – g his 命令

我们可以通过执行 onstat -g his 命令来显示 SQL 下钻查询信息。

语法:

>>-onstat-- -g--his--------------------------------------------><

onstat -g his 选项显示 SQLTRACE 收集到的信息并以格式化输出。缺省情况下,只有 DBSA 可以查看 onstat -g his syssqltrace 信息。然而,当 UNSECURE_ONSTAT = 1 时,所有的用户可以查看此信息。 onstat -g his 选项会将所收集到的所有信息全部显示出来,目前还不能够只显示某一特定的用户会话或 SQL 语句的信息。因此,它比较适合小数据量的显示,它的好处是比较方便。

onstat -g his 选项的输出主要包含三部分内容:trace profile, statement text and statement statistics.

Trace profile:这是 onstat -g his 命令输出的前几行信息,用于描述跟踪的级别、跟踪模式、跟踪的 SQL 语句条数、跟踪缓冲区的大小及跟踪缓冲区保持的时间。如下所示:

onstat -g his 的输出示例(Trace profile 部分):

Statement history: 

 Trace Level                  High 
 Trace Mode                   User 
 Number of traces            50000 
 Current Stmt ID                 3 
 Trace Buffer size           12264 
 Duration of buffer             37 Seconds 
 Trace Flags            0x00007F21 
 Control Block          0x4b8cd018 

 ... ...

Statement text and iterators:onstat – g his 命令输出的接下几行用来描述被跟踪的 SQL 语句以及查询中用到的迭代器及查询计划信息。 SQL 语句部分的内容根据跟踪级别的不同而有所不同。如果跟踪级别是 LOW,只是显示被跟踪的 SQL 语句及正在使用的数据库的十六进制描述。如果踪级别是 medium,将显示数据库的名称,SQL 语句,SQL 语句中用到的表名称及存储过程的调用堆栈信息。如果跟踪级别是 high,除了显示 medium level 的信息外,还将显示 SQL 语句中用到的宿主变量信息。如下所示:

onstat -g his 的输出示例(Statement text and iterators: 部分):

... ... 
 Statement # 3:     @ 0x4b907018 
 Database:        sysmaster 
 Statement text: 
  select first 2 substr(tabname,1,20) as table, isreads as reads from 
    sysptprof where isreads > 10 order by isreads desc 

  SELECT using tables [ systabnames sysptntab ] 

 Iterator/Explain 
 ================ 
    ID   Left  Right   Est Cost   Est Rows   Num Rows    Partnum Type 
     3      0      0          9         33         40         20 Seq Scan 
     4      0      0          1        100          1         15 Index Scan 
     2      3      4         28         33         40          0 Nested Join 
     1      2      0          1         33          2          0 Sort 
     4      0      0         18         92         92     Disk Scan 
     2      3      4        287       1380       5060     Nested Join 
     1      2      0          1          1       5060     Insert 

 ... ...

Statement information and statistics:接下来的部分主要包含 SQL 语句及性能统计信息,也是我们进行监控的最主要的部分。通过它,我们可以发现 SQL 语句相关的内存使用情况、磁盘 I/O 情况、锁使用及争用情况,CPU 使用情况,排序及索引使用情况等信息。据此,我们可以进行相应的调整。 我们又可以将其细化为下面三部分内容:

Statement information:描述下边一些信息:

  • 运行命令的用户的用户标识
  • 数据库会话标识
  • 数据库的名称
  • SQL 语句的类型
  • SQL 语句执行的持续时间
  • 该语句完成的时间
  • 带有语句类型的 SQL 语句文本或函数调用列表(也称为堆栈跟踪),例如: procedure1() calls procedure2() calls procedure3()

RSAM statistics:描述下边一些信息:

  • 缓冲区读取和写入的数目
  • 页面读取和写入的数目
  • 排序和磁盘排序的数目
  • 锁请求和等待的数目
  • 逻辑日志记录的数目

SQL statistics:描述下边一些信息:

  • 估计的行数
  • 优化器估计成本
  • 返回的行数
  • SQL/ISAM 错误
  • 数据库隔离级别
  • SQL 语句内存使用量。

onstat -g his 的输出示例(Statement information and statistics 部分):

... ... 

 Statement information: 
  Sess_id  User_id  Stmt Type          Finish Time    Run Time 
  26       501      SELECT             23:31:01       0.0054 

 Statement Statistics: 
  Page       Buffer     Read       Buffer     Page       Buffer     Write 
  Read       Read       % Cache    IDX Read   Write      Write      % Cache 
  0          410        100.00     0          0          0          0.00 

  Lock       Lock       LK Wait    Log        Num        Disk       Memory 
  Requests   Waits      Time (S)   Space      Sorts      Sorts      Sorts 
  0          0          0.0000     0.000 B    1          0          1 

  Total      Total      Avg        Max        Avg        I/O Wait   Avg Rows 
  Executions Time (S)   Time (S)   Time (S)   IO Wait    Time (S)   Per Sec 
  1          0.0108     0.0108     0.0054     0.000000   0.000000   370.1291 

  Estimated  Estimated  Actual     SQL        ISAM       Isolation  SQL 
  Cost       Rows       Rows       Error      Error      Level      Memory 
  28         33         2          0          0          CR         34176

输出描述 :

  • Page Read:已从磁盘读取的页数
  • Buffer Reads: 从缓冲池读取而不是从磁盘读取页面的次数
  • Read % Cache: 应从缓冲池读取页面的次数的百分比
  • Buffer IDX Read: 索引页的缓冲区读取数
  • Page Write: 写入磁盘的页数
  • Buffer Write:修改并发送回缓冲池的页数
  • Write % Cache:页面写入缓冲池而不是写入磁盘的次数的百分比
  • Lock Requests:该语句所需的锁的总数
  • Lock Waits:该 SQL 语句等待锁的次数
  • LK Wait Time:在该 SQL 语句执行期间,用于等待锁的时间(以秒为单位)
  • Log Space
  • Num Sorts:用于执行语句的排序总数
  • Disk Sorts:对于该 SQL 语句,对磁盘执行的排序的次数
  • Memory Sorts
  • Total Executions:该语句已执行的总次数,或者该游标重用的次数
  • Total Time:执行该语句的总时间(以秒为单位)
  • Avg Time:执行该语句的平均时间(以秒为单位)
  • Max Time:运行 SQL 语句的总时间(以秒为单位),不包括应用程序使用的任何时间
  • LK Wait Time:语句等待应用程序锁的时间量
  • Avg IO Wait:语句等待 I/O 的时间量,不包括任何异步 I/O 。
  • Avg Rows Per Sec:该语句每秒产生的平均行数
  • Estimated Cost:与 SQL 语句关联的代价
  • Estimated Rows:返回的估计行数,由语句的优化程序估计
  • Actual Rows:对于该语句返回的行数
  • SQL Error:SQL 错误号
  • ISAM Error:RSAM/ISAM 错误号
  • Isolation Level:该语句运行时使用的隔离级别
  • SQL Memory:该 SQL 语句需要的字节数

syssqltrace 伪表

在 Informix 中,提供了 3 张内存伪表用来保存 SQL 跟踪信息。其中,syssqltrace 表用来提供被跟踪的每一个 SQL 语句的详细的跟踪信息。它的大小由 ntraces*size 来决定。我们可以动态调整其大小。我们可以通过 sysmaster 数据库来访问这 3 张内存伪表。

syssqltrace 表的输出信息同执行 onstat -g his 命令输出的 Statement information and statistics 部分的内容相似。

由于我们可以使用 SQL 语句访问 SQL 跟踪信息,所以,采用该种方法比较适合查询有关单个 SQL 语句或一组 SQL 语句的详细跟踪信息。

syssqltrace 表的基本结构如下:

类型描述
sql_id int8 唯一 SQL 执行标识
sql_address int8 代码块中的语句的地址
sql_sid int 运行 SQL 语句的用户的数据库会话标识
sql_uid int 运行 SQL 的语句的用户标识
sql_stmttype int 语句类型
sql_stmtname char(40) 显示为单词的语句类型
sql_finishtime int 此语句的完成时间(UNIX)
sql_begintxtime int 此事务的启动时间
sql_runtime float 语句执行时间
sql_pgreads int 此 SQL 语句的磁盘读取数
sql_bfreads int 此 SQL 语句的缓冲区读取数
sql_rdcache float 从缓冲池读取页的时间百分比
sql_bfidxreads int 索引页缓冲区读取数
sql_pgwrites int 写入磁盘的页数
sql_bfwrites int 已修改并返回到缓冲池的页数
sql_wrcache float 页已写入缓冲池,但未写入磁盘的时间百分比
sql_lockreq int 此 SQL 语句所需锁总数
sql_lockwaits int SQL 语句等待锁的次数
sql_lockwttime float SQL 语句期间系统等待锁定的时间
sql_logspace int 逻辑日志中 SQL 语句所用空间量
sql_sorttotal int 为语句运行的排序数
sql_sortdisk int 磁盘上运行的排序数
sql_sortmem int 内存中运行的排序数
sql_executions int SQL 语句运行的次数
sql_totaltime float 运行语句所用时间总量
sql_avgtime float 运行语句所用平均时间量
sql_maxtime float 执行 SQL 语句所用最大时间量
sql_numiowaits int I/O 操作必须等待的次数
sql_avgiowaits float SQL 语句必须等待的平均时间量
sql_totaliowaits float SQL 语句必须等待 I/O 的时间总量。这不包含任何异步 I/O 。
sql_rowspersec float 产生的平均行数(每秒)
sql_estcost int 与 SQL 语句关联的成本
sql_estrows int 按照优化器的预测为 SQL 语句返回的预估行数
sql_actualrows int 为 SQL 语句返回的行数
sql_sqlerror int SQL 错误号
sql_isamerror int RSAM/ISAM 错误号
sql_isollevel int SQL 语句的隔离级别。
sql_sqlmemory int 执行 SQL 语句所需字节数
sql_numiterators int 语句所用迭代器数
sql_database char(128) 数据库名
sql_numtables int 执行 SQL 语句中所用表数
sql_tablelist char(4096) SQL 语句中直接引用的表名列表。如果 SQL 语句击发对其他表执行语句的触发器,将不列出其他这些表。
sql_statement char(1600) 已运行的 SQL 语句

如果我们要查看会话 30 的 SQL 跟踪信息,可以执行:

select * from syssqltrace where sql_id =30;

syssqltrace_info 伪表

syssqltrace_info 伪表也是一张内存表,用来保存 tracing profile 信息。我们可以通过 sysmaster 数据库来访问这张内存伪表。 tracing profile 信息主要用于描述跟踪的级别、跟踪模式、跟踪的 SQL 语句条数、跟踪缓冲区的大小及跟踪缓冲区保持的时间。

syssqltrace_info 伪表的输出内容同执行 onstat -g his 命令输出的 tracing profile 部分的内容相似。

syssqltrace_info 表的基本结构如下:

类型描述
flags integer SQL 跟踪标志
ntraces integer 要跟踪的项数
tracesize integer 为各 SQL 跟踪项存储的文本的大小
duration integer 跟踪缓冲区(以秒为单位)
sqlseen int8 启动或调整大小以来跟踪的 SQL 项数
starttime integer 跟踪的启用时间
memoryused int8 SQL 跟踪所用内存的字节数

syssqltrace_iter 伪表

syssqltrace_iter 伪表也是一张内存表,用来保存 Statement text and iterators 信息。我们可以通过 sysmaster 数据库来访问这张内存伪表。 Statement text and iterators 信息主要用来描述被跟踪的 SQL 语句以及查询中用到的迭代器及查询计划信息。这个表常用来查询特定 SQL 语句的查询计划信息。

syssqltrace_iter 伪表的输出内容同执行 onstat -g his 命令输出的 Statement text and iterators 部分的内容相似。

syssqltrace_iter 表的基本结构如下:

类型描述
sql_id int8 SQL 执行标识
sql_address int8 SQL 语句块的地址
sql_itr_address int8 迭代器的地址
sql_itr_id int 迭代器标识
sql_itr_left int 向左的迭代器标识
sql_itr_right int 向右的迭代器标识
sql_itr_cost int 迭代器成本
sql_itr_estrows int 迭代器预估行数
sql_itr_numrows int 迭代器实际处理的行数
sql_itr_type int 迭代器类型
sql_itr_misc int 迭代器杂项标志
sql_it_info char(256) 显示为文本的迭代器杂项标志

如果我们要查看 sql_id=15 的查询所使用的迭代器及类型,我们可以执行:

select  sql_itr_type, 
        substr(sql_itr_info,1,20) as iterator_info, 
        sql_itr_numrows 
 from    syssqltrace_iter 
 where   sql_id = 14;

通过执行 onstat -g his 命令或查询sysmaster数据库中的 syssqltrace 伪表,我们可以查看系统中运行的 SQL 语句、执行 SQL 所用的资源、运行 SQL 花费的时间、磁盘 / 页面 / 缓冲区读和写的数量、使用的锁数量、排序数量和使用的内存量。另外,还可以查看 Informix 优化器估计的运行 SQL 所要花费的时间。这样,我们可以了解到 SQL 语句占用资源情况及存在的资源瓶颈,并进行相应的调整。另外,我们还可以对比 Informix 优化器估计的返回行数和实际的返回行数(sql_estrows 和 sql_actualrows)。如果这两个数值差异很大,就说明 Informix 优化器并不掌握关于表中行和索引数量的正确的统计数据。这意味着需要运行 update statistics,从而向优化器提供正确的数据。

另外,通过查询 syssqltrace 表,我们还可以发现系统中运行时间最长的 SQL 查询、带有太多表连接操作的查询、不希望的连接类型、返回记录太多的查询、存在锁等待的查询等信息,以便于我们进行及时的调整及改进来提高 SQL 语句的性能。

如果我们要发现运行时间最长的 SQL 查询,我们可以执行:

select  first 5 
        substr(sql_statement,1,50) as statement 
        , sql_avgtime as Average_Time 
        , sql_executions as Number_of_times 
 from syssqltrace 
 order by sql_avgtime desc ;

如果我们要发现返回记录太多的查询,我们可以执行:

select  first 5 
        sql_estrows as est_rows 
        , sql_actualrows as actual_rows 
        , substr(sql_statement,1,30) as statement 
 from syssqltrace 
 order by sql_actualrows desc ;
 

结论

本文主要给大家介绍了 Informix 数据库对 SQL 语句性能监控的两种基本的方法及使用,关于 SQL 语句调优的相应方法及建议,大家可以参考 Informix 信息中心中性能调整部分。

Informix 11.5 SQL 语句性能监控方法及实现

标签:

原文地址:http://www.cnblogs.com/equation/p/5589756.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!