精华内容
下载资源
问答
  • MySQL优化技巧

    万次阅读 多人点赞 2017-09-10 14:14:00
    MySQL优化三大方向① 优化MySQL所在服务器内核(此优化一般由运维人员完成)。② 对MySQL配置参数进行优化(my.cnf)此优化需要进行压力测试来进行参数调整。③ 对SQL语句以及表优化。MySQL参数优化1:MySQL 默认的最大...

    MySQL优化三大方向

    ① 优化MySQL所在服务器内核(此优化一般由运维人员完成)。
    ② 对MySQL配置参数进行优化(my.cnf)此优化需要进行压力测试来进行参数调整。
    ③ 对SQL语句以及表优化。

    MySQL参数优化

    1:MySQL 默认的最大连接数为 100,可以在 mysql 客户端使用以下命令查看
    mysql> show variables like 'max_connections';
    2:查看当前访问Mysql的线程
    mysql> show processlist;
    3:设置最大连接数
    mysql>set globle max_connections = 5000;
    最大可设置16384,超过没用
    4:查看当前被使用的connections
    mysql>show globle status like 'max_user_connections'

    对MySQL语句性能优化的16条经验

    ① 为查询缓存优化查询
    ② EXPLAIN 我们的SELECT查询(可以查看执行的行数)
    ③ 当只要一行数据时使用LIMIT 1
    ④ 为搜索字段建立索引
    ⑤ 在Join表的时候使用相当类型的列,并将其索引
    ⑥ 千万不要 ORDER BY RAND  ()
    ⑦ 避免SELECT *
    ⑧ 永远为每张表设置一个ID
    ⑨ 可以使用ENUM 而不要VARCHAR
    ⑩ 尽可能的使用NOT NULL
    ⑪ 固定长度的表会更快
    ⑫ 垂直分割
    ⑬ 拆分打的DELETE或INSERT语句
    ⑭ 越小的列会越快
    ⑮ 选择正确的存储引擎
    ⑯ 小心 "永久链接"
    具体描述如下:
    (一) 使用查询缓存优化查询
    大多数的MySQL服务器都开启了查询缓存。这是提高性能最有效的方法之一,而且这是被MySQL引擎处理的。当有很多相同的查询被执行了多次的时候,这些查询结果会被放入一个缓存中,这样后续的相同查询就不用操作而直接访问缓存结果了。
    这里最主要的问题是,对于我们程序员来说,这个事情是很容易被忽略的。因为我们某些查询语句会让MySQL不使用缓存,示例如下:
    1:SELECT username FROM user WHERE    signup_date >= CURDATE()
    2:SELECT username FROM user WHERE    signup_date >= '2014-06-24‘
    上面两条SQL语句的差别就是 CURDATE() ,MySQL的查询缓存对这个函数不起作用。所以,像 NOW() 和 RAND() 或是其它的诸如此类的SQL函数都不会开启查询缓存,因为这些函数的返回是会不定的易变的。所以,你所需要的就是用一个变量来代替MySQL的函数,从而开启缓存。
    (二) 使用EXPLAIN关键字检测查询
    使用EXPLAIN关键字可以使我们知道MySQL是如何处理SQL语句的,这样可以帮助我们分析我们的查询语句或是表结构的性能瓶颈;EXPLAIN的查询结果还会告诉我们索引主键是如何被利用的,数据表是如何被被搜索或排序的....等等。语法格式是:EXPLAIN +SELECT语句;


    我们可以看到,前一个结果显示搜索了 7883 行,而后一个只是搜索了两个表的 9 和 16 行。查看rows列可以让我们找到潜在的性能问题。 
    (三)当只要一行数据时使用LIMIT 1
    加上LIMIT 1可以增加性能。MySQL数据库引擎会在查找到一条数据后停止搜索,而不是继续往后查询下一条符合条件的数据记录。
    (四)为搜索字段建立索引
    索引不一定就是给主键或者是唯一的字段,如果在表中,有某个字段经常用来做搜索,需要将其建立索引。
    索引的有关操作如下:
    1.创建索引
    在执行CREATE TABLE语句时可以创建索引,也可以单独用CREATE INDEX或ALTER TABLE来为表增加索引。
    1.1> ALTER TABLE
    ALTER TABLE 用来创建普通索引、唯一索引、主键索引和全文索引
    ALTER TABLE table_name ADD INDEX index_name (column_list);
    ALTER TABLE table_name ADD UNIQUE (column_list);
    ALTER TABLE table_name ADD PRIMARY KEY (column_list);
    ALTER TABLE table_name ADD FULLTEXT (column_list);
    其中table_name是要增加索引名的表名,column_list指出对哪些列列进行索引,多列时各列之间使用半角逗号隔开。索引名index_name是可选的,如果不指定索引名称,MySQL将根据第一个索引列自动指定索引名称,另外,ALTER TABLE允许在单个语句中更改多个表,因此可以在同时创建多个索引。
    1.2> CREATE INDEX
    CREATE INDEX可对表增加普通索引或UNIQUE索引以及全文索引,但是不可以对表增加主键索引
    CREATE INDEX index_name ON table_name (column_list);
    CREATE UNIQUE index_name ON table_name (column_list);
    CREATE FULLTEXT index_name ON table_name (column_list);
    table_name、index_name和column_list具有与ALTER TABLE语句中相同的含义,索引名必须指定。另外,不能用CREATE INDEX语句创建PRIMARY KEY索引。
    2.索引类型
    普通索引INDEX:适用于name、email等一般属性
    唯一索引UNIQUE:与普通索引类似,不同的是唯一索引要求索引字段值在表中是唯一的,这一点和主键索引类似,但是不同的是,唯一索引允许有空值。唯一索引一般适用于身份证号码、用户账号等不允许有重复的属性字段上。
    主键索引:其实就是主键,一般在建表时就指定了,不需要额外添加。
    全文检索:只适用于VARCHAR和Text类型的字段。
    注意:全文索引和普通索引是有很大区别的,如果建立的是普通索引,一般会使用like进行模糊查询,只会对查询内容前一部分有效,即只对前面不使用通配符的查询有效,如果前后都有通配符,普通索引将不会起作用。对于全文索引而言在查询时有自己独特的匹配方式, 例如我们在对一篇文章的标题和内容进行全文索引时:
    ALTER TABLE article ADD FULLTEXT ('title', 'content'); 在进行检索时就需要使用如下的语法进行检索:
    SELECT * FROM article WHERE MATCH('title', 'content') AGAINST ('查询字符串');
    在使用全文检索时的注意事项:
    MySql自带的 全文索引只能用于数据库引擎为MYISAM的数据表,如果是其他数据引擎,则全文索引不会生效。此外, MySql自带的全文索引只能对英文进行全文检索,目前无法对中文进行全文检索。如果需要对包含中文在内的文本数据进行全文检索,我们需要采用Sphinx(斯芬克斯)/Coreseek技术来处理中文。另外使用MySql自带的全文索引时, 如果查询字符串的长度过短将无法得到期望的搜索结果。MySql全文索引所能找到的词默认最小长度为4个字符。另外,如果查询的字符串包含停止词,那么该停止词将会被忽略。
    3.组合索引
    组合索引又称多列索引,就是建立索引时指定多个字段属性。有点类似于字典目录,比如查询 'guo' 这个拼音的字时,首先查找g字母,然后在g的检索范围内查询第二个字母为u的列表,最后在u的范围内查找最后一个字母为o的字。比如组合索引(a,b,c),abc都是排好序的,在任意一段a的下面b都是排好序的,任何一段b下面c都是排好序的
    组合索引的生效原则是  从前往后依次使用生效,如果中间某个索引没有使用,那么断点前面的索引部分起作用,断点后面的索引没有起作用;
    造成断点的原因:
    前边的任意一个索引没有参与查询,后边的全部不生效。
    前边的任意一个索引字段参与的是范围查询,后面的不会生效。
    断点跟索引字字段在SQL语句中的位置前后无关,只与是否存在有关。在网上找到了很好的示例:
    比如:
    where a=3 and b=45 and c=5 .... #这种三个索引顺序使用中间没有断点,全部发挥作用;
    where a=3 and c=5... #这种情况下b就是断点,a发挥了效果,c没有效果
    where b=3 and c=4... #这种情况下a就是断点,在a后面的索引都没有发挥作用,这种写法联合索引没有发挥任何效果;
    where b=45 and a=3 and c=5 .... #这个跟第一个一样,全部发挥作用,abc只要用上了就行,跟写的顺序无关
    (a,b,c) 三个列上加了联合索引(是联合索引 不是在每个列上单独加索引)而是建立了a,(a,b),(a,b,c)三个索引,另外(a,b,c)多列索引和 (a,c,b)是不一样的。
    具体实例可以说明:
    (0) select * from mytable where a=3 and b=5 and c=4;
    #abc三个索引都在where条件里面用到了,而且都发挥了作用
    (1) select * from mytable where  c=4 and b=6 and a=3;
    #这条语句为了说明 组合索引与在SQL中的位置先后无关,where里面的条件顺序在查询之前会被mysql自动优化,效果跟上一句一样
    (2) select * from mytable where a=3 and c=7;
    #a用到索引,b没有用,所以c是没有用到索引效果的
    (3) select * from mytable where a=3 and b>7 and c=3;
    #a用到了,b也用到了,c没有用到,这个地方b是范围值,也算断点,只不过自身用到了索引
    (4) select * from mytable where b=3 and c=4;
    #因为a索引没有使用,所以这里 bc都没有用上索引效果
    (5) select * from mytable where a>4 and b=7 and c=9;
    #a用到了  b没有使用,c没有使用
    (6) select * from mytable where a=3 order by b;
    #a用到了索引,b在结果排序中也用到了索引的效果,前面说了,a下面任意一段的b是排好序的
    (7) select * from mytable where a=3 order by c;
    #a用到了索引,但是这个地方c没有发挥排序效果,因为中间断点了,使用 explain 可以看到 filesort
    (8) select * from mytable where b=3 order by a;
    #b没有用到索引,排序中a也没有发挥索引效果
    注意:在查询时,MYSQL只能使用一个索引,如果建立的是多个单列的普通索引,在查询时会根据查询的索引字段,从中选择一个限制最严格的单例索引进行查询。别的索引都不会生效。
    4. 查看索引
    mysql> show index from tblname;
    mysql> show keys from tblname;
    5.删除索引
    删除索引的mysql格式 :DORP INDEX IndexName ON tab_name;
    注意:不能使用索引的情况 
    对于普通索引而言 在使用like进行通配符模糊查询时,如果首尾之间都使用了通配符,索引时无效的。
    假设查询内容的关键词为'abc'
    SELECT * FROM tab_name WHERE index_column LIKE  'abc%';  #索引是有效的
    SELECT * FROM tab_name WHERE index_column LIKE  '%abc';  #索引是无效的
    SELECT * FROM tab_name WHERE index_column LIKE  '%cba';  #索引是有效的
    SELECT * FROM tab_name WHERE index_column LIKE  '%abc%';  #索引是无效的
    当检索的字段内容比较大而且检索内容前后部分都不确定的情况下,可以改为全文索引,并使用特定的检索方式。
    (五)在join表的时候使用相当类型的列,并将其索引
    如果在程序中有很多JOIN查询,应该保证两个表中join的字段时被建立过索引的。这样MySQL颞部会启动优化JOIN的SQL语句的机制。 注意:这些被用来JOIN的字段,应该是相同类型的。 例如:如果要把 DECIMAL 字段和一个 INT 字段Join在一起,MySQL就无法使用它们的索引。对于那些STRING类型,还需要有相同的字符集才行。(两个表的字符集有可能不一样)  
    例如:
    SELECT company_name FROM users LEFT JOIN companies ON (users.state = companies.state) WHERE users.id = “user_id”
    两个 state 字段应该是被建过索引的,而且应该是相当的类型,相同的字符集。
    (六)切记不要使用ORDER BY RAND()
    如果你真的想把返回的数据行打乱了,你有N种方法可以达到这个目的。这样使用只让你的数据库的性能呈指数级的下降。这里的问题是:MySQL会不得不去执行RAND()函数(很耗CPU时间),而且这是为了每一行记录去记行,然后再对其排序。就算是你用了Limit 1也无济于事(因为要排序) 
    (七)避免使用SELECT *
    从数据库里读出越多的数据,那么查询就会变得越慢。并且,如果我们的数据库服务器和WEB服务器是两台独立的服务器的话,这还会增加网络传输的负载。 所以,我们应该养成一个需要什么就取什么的好的习惯。
    Hibernate性能方面就会差,它不用*,但它将整个表的所有字段全查出来 
    优点:开发速度快
    (八)永远为每张表设置一个ID主键
    我们应该为数据库里的每张表都设置一个ID做为其主键,而且最好的是一个INT型的(推荐使用UNSIGNED),并设置上自动增加的 AUTO_INCREMENT标志。 就算是我们 users 表有一个主键叫 “email”的字段,我们也别让它成为主键。使用 VARCHAR 类型来当主键会使用得性能下降。另外,在我们的程序中,我们应该使用表的ID来构造我们的数据结构。 而且,在MySQL数据引擎下,还有一些操作需要使用主键,在这些情况下,主键的性能和设置变得非常重要,比如,集群,分区…… 在这里,只有一个情况是例外,那就是“关联表”的“外键”,也就是说,这个表的主键,通过若干个别的表的主键构成。我们把这个情况叫做“外键”。比如:有一个“学生表”有学生的ID,有一个“课程表”有课程ID,那么,“成绩表”就是“关联表”了,其关联了学生表和课程表,在成绩表中,学生ID和课程ID叫“外键”其共同组成主键。 
    (九)使用ENUM而不是VARCHAR
    ENUM 类型是非常快和紧凑的。在实际上,其保存的是 TINYINT,但其外表上显示为字符串。这样一来,用这个字段来做一些选项列表变得相当的完美。 如果我们有一个字段,比如“性别”,“国家”,“民族”,“状态”或“部门”,我们知道这些字段的取值是有限而且固定的,那么,我们应该使用 ENUM 而不是 VARCHAR。
    (十)尽可能的不要赋值为NULL
    如果不是特殊情况,尽可能的不要使用NULL。在MYSQL中对于INT类型而言,EMPTY是0,而NULL是空值。而在Oracle中 NULL和EMPTY的字符串是一样的。NULL也需要占用存储空间,并且会使我们的程序判断时更加复杂。现实情况是很复杂的,依然会有些情况下,我们需要使用NULL值。 下面摘自MySQL自己的文档: “NULL columns require additional space in the row to record whether their values are NULL. For MyISAM tables, each NULL column takes one bit extra, rounded up to the nearest byte.” 
    (十一) 固定长度的表会更快
    如果表中的所有字段都是“固定长度”的,整个表会被认为是 “static” 或 “fixed-length”。 例如,表中没有如下类型的字段: VARCHAR,TEXT,BLOB。只要我们包括了其中一个这些字段,那么这个表就不是“固定长度静态表”了,这样,MySQL 引擎会用另一种方法来处理。 固定长度的表会提高性能,因为MySQL搜寻得会更快一些,因为这些固定的长度是很容易计算下一个数据的偏移量的,所以读取的自然也会很快。而如果字段不是定长的,那么,每一次要找下一条的话,需要程序找到主键。 并且,固定长度的表也更容易被缓存和重建。不过,唯一的副作用是,固定长度的字段会浪费一些空间,因为定长的字段无论我们用不用,他都是要分配那么多的空间。另外在取出值的时候要使用trim去除空格 
    (十二)垂直分割
    “垂直分割”是一种把数据库中的表按列变成几张表的方法,这样可以降低表的复杂度和字段的数目,从而达到优化的目的。
    (十三)拆分大的DELETE或INSERT
    如果我们需要在一个在线的网站上去执行一个大的 DELETE 或 INSERT 查询,我们需要非常小心,要避免我们的操作让我们的整个网站停止相应。因为这两个操作是会锁表的,表一锁住了,别的操作都进不来了。Apache 会有很多的子进程或线程。所以,其工作起来相当有效率,而我们的服务器也不希望有太多的子进程,线程和数据库链接,这是极大的占服务器资源的事情,尤其是内存。如果我们把我们的表锁上一段时间,比如30秒钟,那么对于一个有很高访问量的站点来说,这30秒所积累的访问进程/线程,数据库链接,打开的文件数,可能不仅仅会让我们的WEB服务Crash,还可能会让我们的整台服务器马上掛了。所以在使用时使用LIMIT 控制数量操作记录的数量。
    (十四)越小的列会越快  
    对于大多数的数据库引擎来说,硬盘操作可能是最重大的瓶颈。所以,把我们的数据变得紧凑会对这种情况非常有帮助,因为这减少了对硬盘的访问。 参看 MySQL 的文档 Storage Requirements 查看所有的数据类型。 如果一个表只会有几列罢了(比如说字典表,配置表),那么,我们就没有理由使用 INT 来做主键,使用 MEDIUMINT, SMALLINT 或是更小的 TINYINT 会更经济一些。如果我们不需要记录时间,使用 DATE 要比 DATETIME 好得多。 
    (十五)选择正确的存储引擎
    在MYSQL中有两个存储引擎MyISAM和InnoDB,每个引擎都有利有弊。
    MyISAM适合于一些需要大量查询的应用,但是对于大量写操作的支持不是很好。甚至一个update语句就会进行锁表操作,这时读取这张表的所有进程都无法进行操作直至写操作完成。另外MyISAM对于SELECT  COUNT(*)这类的计算是超快无比的。InnoDB 的趋势会是一个非常复杂的存储引擎,对于一些小的应用,它会比 MyISAM 还慢。它支持“行锁” ,于是在写操作比较多的时候,会更优秀。并且,他还支持更多的高级应用,比如:事务。
    MyISAM是MYSQL5.5版本以前默认的存储引擎,基于传统的ISAM类型,支持B-Tree,全文检索,但是不是事务安全的,而且不支持外键。不具有原子性。支持锁表。
    InnoDB是事务型引擎,支持ACID事务(实现4种事务隔离机制)、回滚、崩溃恢复能力、行锁。以及提供与Oracle一致的不加锁的读取方式。InnoDB存储它的表和索引在一个表空间中,表空间可以包含多个文件。
    MyISAM和InnoDB比较,如下图所示:

    对于Linux版本的MYSQL  配置文件在 /etc/my.cnf中

    在5.5之后默认的存储引擎是INNODB
    可以单独进行修改也可以在创建表时修改:
    ALTER TABLE tab_name ENGINE INNODB;
    (十六)小心永久链接
    “永久链接”的目的是用来减少重新创建MySQL链接的次数。当一个链接被创建了,它会永远处在连接的状态,就算是数据库操作已经结束了。而且,自从我们的Apache开始重用它的子进程后——也就是说,下一次的HTTP请求会重用Apache的子进程,并重用相同的 MySQL 链接。 
    而且,Apache 运行在极端并行的环境中,会创建很多很多的了进程。这就是为什么这种“永久链接”的机制工作地不好的原因。在我们决定要使用“永久链接”之前,我们需要好好地考虑一下我们的整个系统的架构。


    展开全文
  • 黑帽SEO大牛告诉你:想学网站SEO优化的百种法则你必须了解,这些SEO规则就像是交通指示灯一样,按照这些SEO规则走的,排名肯定会好,不按套路走的,除了快排只有死路一条。本文章句句精炼,不用深入剖析,如果想...

    黑帽SEO大牛告诉你:想学网站SEO优化的百种法则你必须了解,这些SEO规则就像是交通指示灯一样,按照这些SEO规则走的,排名肯定会好,不按套路走的,除了快排只有死路一条。本文章句句精炼,不用深入剖析,如果想进一步了解更细节的东西,可以联系汉文博客,接下来就是主题了!
    SEO黑帽大牛
    SEO白帽大牛免费分享网站SEO优化秘籍,百度首页排名的干货:

    5、网站改版尽可能保留原有数据和链接结构

    6、不要轻易改变一个网站的标题、关键词、描述

    7、新站上线三个月之内请勿修改网站标题

    20、伪静态和静态生成一样利于SEO,伪静态稍微消耗资源一点,但利于交互功能的实现,纯静态耗资源少,但交互功能不好做。建议是买个好点服务器(现在服务器都很便宜)。

    21、有的服务器会封禁搜索引擎蜘蛛抓取网站,可以用网页蜘蛛模拟抓取测试工具看看是否成功。

    22、CDN加速目的是提升网站速度,但大多免费的CDN都不太稳定,而且速度可能还没有你网站本身速度快,这样就没有必要做CDN了。不稳定的CDN加速反而影响SEO效果。

    23、独立服务器、VPS、虚拟主机一般只是价格的问题,不直接影响到排名,如果非要说有影响也是间接地,比如一般独立服务器、VPS都是独立域名,而虚拟主机大多是共享IP的。
    43、banner图片轮播效果要用js切换图片或者背景图片的方式,不要用flash的,记着一句话:flash以后是要被淘汰的,起码目前看来是这样。

    44、页面主体部分最好有一个h1标签,并且h1标签里要含有网站的关键词。首页可能不太好加h1,但内容页的标题是完全可以用h1环绕的。

    45、图片上可以用alt标签加上一些跟图片有关的关键词,比超级蜘蛛池的banner图片就写上 alt=“超级蜘蛛池,超百万蜘蛛与您共享”,带SEO的关键词。汉文博客

    46、ajax的使用一般都是用在表单上,不要用ajax的方式切换页面,看上去似乎很炫酷,实际上是给搜索引擎设置抓取障碍。

    47、有的网站有图片的友情链接(比如合作伙伴,然后点击指向对方网站),但也要有文字的友情链接功能,因为很多人更愿意换带文字关键词的链接,否则人家不给你换。

    48、JS文件尽可能的不要用外置的,因为有时候外置的js加载缓慢,会影响网站的打开速度。

    49、图片文件一般也不要外置,我们经常见到用外置图片文章,对方网站删除或者设置防止外连导致页面其丑无比,而且你也找不到匹配的图片了。这就要求做网站的时候要找负责任的建站公司。

    50、压缩包、文档、视频等文件一般不要放在自己服务器上,因为会占用你的带宽,如果下载量大,就会影响到网站的速度。一般100k以上的就要放在其他网盘或者专门的服务器上。

    51、网站尽可能用静态化的页面结构,更利于搜索引擎抓取收录。
    24、服务器被黑也是很头疼的,如果导致你网站出现问题,会影响到SEO,需要谨慎对待。有时候是同服务器其他网站有漏洞造成,大部分是你自己网站被黑,遭篡改造成。

    25、你的网页突然被插入很多黄赌毒的链接,那就是网站被黑,请抓紧处理。白帽seo大牛

    26、你的网页总是莫名其妙的跳转到其他网站,那就是网站被黑了,最可能的地方是JS,请抓紧处理。

    27、页面右下角有广告,有可能是网站被黑,但大多数是因为电信运营商被劫持,仔细观察研判再做处理。

    28、网站速度慢的原因有很多种:你自己网线慢、电脑慢、服务器配置低、机房带宽不够,请找专业人士研判处理,处理不好会影响网站优化效果。

    29、网站经常打不开有很多原因:被攻击、服务器不稳定、程序有问题,请找专业人士解决,这会影响SEO。

    30、网站做得好,会对SEO起到极大地帮助;相反,网站做不好,SEO效果就很难做好。一个好网站是SEO的前提。

    31、网站文字要稍微多一些,起码不要太少,否则搜索引擎进入网站就会抓瞎了一样,不确定网站做的是什么内容。

    32、减少网页中大面积JS的使用,这并不是不要人使用JS,一些非重要文字和数据完全可以用ajax或者js方式进行操作。但需要搜索引擎抓取的文字还是要直接写到界面上。

    33、减少使用flash。flash里边的内容搜索引擎是看不到的。这些年css3和js的动画都发展很好,可以用这些替代,效果也不差。

    34、手机网站一定不要用flash,因为大多数手机默认状态下是不支持flash的,搜索引擎会低看你的网站。

    35、网页标题中一定要包含你想做的关键词,重点关键词可以重复出现2~3次。

    36、网页标题中重点关键词要及早的出现,越往后的关键词越不受到重视。

    37、网页关键词里设置什么样的关键词对网站形象不是特别大,但这起码是网页中的一些文字,还是要写你想做的关键词。

    38、网页描述里要包含关键词,重点关键词也要重复2~3次。

    39、网页第一屏要包含关键词,重点关键词在不影响界面设计的情况下可以用h标签、b标签强调一下。

    40、LOGO的尺寸尽可能的接近121*75的比例,这样更容易被搜索引擎收录为搜索结果展示图片,提高点击率。

    41、导航要用文字a标签环绕的,不要用flash、图片、JS输出、iframe的形式来做。

    42、导航制作过程中不是不让用js,正常的js效果可以用,一些鼠标放上去隐藏出现的效果是可以的,只是需要在源码里看到指向的链接和文字。

    52、网站要用目录化的结构,一个栏目一个目录,这样更利于搜索引擎理解你网站的结构,分析出栏目侧重。

    53、搜索功能根据情况开通,一般文章内容多就可以有搜索功能,但开通了搜索功能就要注意安全,防止提交恶意代码。

    54、网站留言功能可以根据情况开通,也是同样要考虑安全。

    55、页面如果有tag功能,是最好的,这样等于页面增加了内链,让页面与页面之间的联系更紧密。

    56、页面中可以增加最近更新、热门文章、推荐文章、置顶等功能,这可以提高重点内容的排名,也更利于搜索引擎发现每篇文章。

    57、内容页可以增加上“上一页”、“下一页”的功能,增加内链,方便搜索引擎抓取。

    58、列表页的翻页功能一定要有(起码PC站要有,手机站无所谓),而且这个翻页不能是JS翻页,而应该做成正常的翻一页打开另一个列表页面。

    59、面包屑导航是网页中不可少的组件,要清晰地告诉用户和搜索引擎这个页面属于哪个栏目,现在处于什么位置,也加强了页面之间的指向关系,增加内链。

    60、页面底部的备案或者是网站认证之类的需要指向外部链接,最好是在这些a标签上增加rel=‘nofllow’,禁止权重导出。
    79、网站开通的栏目都尽可能的精彩,经常发低质量内容的栏目时间长了,搜索引擎会把这个栏目封杀掉。所以开通栏目像做网站一样,有能力就多开几个,越多越好;没有能力就尽最大努力运营好目前的栏目。

    80、内容中可以做一些链接,通过关键词的形式推荐你的其他文章,这样就能做好内容页的排名和权重导入导出工作。

    81、网站的外链相比以前几乎决定网站排名的地位到现在一定程度上影响网站排名,显得没有那么重要。这就像钱一样,有钱并不是万能的,但没有钱是万万不能的。没有外链也是万万不能的,垃圾外链也是万万不能的。

    82、网站外链的惩罚机制越来越成熟,也越来越严厉。这就要求我们外链方面要慎之又慎,手工发布是最保险的。

    83、在不好的网站上发布外链可能对网站排名起到负面影响,在同一个平台发布大量外链是无用的,如果特别多可能也会带来负面影响。

    84、网上关于外链制作方法很多,但很多都是过时的东西,所以在看的时候要用批评的眼光看,要边思考边看。不能以为他们是某位大咖写的就一定正确。

    85、关于外链的算法调整从未停歇,但方针政策只有一个:让垃圾外链少一些,自然产生的外链才会对网站起到更好的作用。也许以后外链对排名带来的影响会比现在更少,但起码目前以及可以看到的未来,搜索引擎还得靠外链来区别网站的好坏。

    86、自己网站的内容转载到其他平台上,最好直接带那篇文章的链接,这样属于转载,时间长了搜索引擎会认为你网站的内容更受欢迎。

    87、锚文本外链相对来说会比帖子外链对排名提升的帮助作用更大,因为锚文本外链可以让搜索引擎知道你的这个网页跟哪个词最相关。

    88、好的外链平台一般有以下特点:

    (1)页面干净整洁、广告少

    (2)帖子排名高

    (3)审核严格

    (4)页面打开速度快。

    在好的外链平台上发一条外链,有时候顶其他平台几百条外链。
    61、友情链接中如果有打不开的网址、降权的网址,尽可能第一时间改成好的网站,这样既维持友情链接数量不变,又去掉不好的链接,稳定且有效果。实际操作中可能酌情选择替换成自己网址、删除等操作。

    62、网站底部最好能设计一个导航,这样更利于页面之间的链接投递,也可以用来放重要链接。

    63、网站上一定记得放联系方式,见过不少客户网站上放的联系方式无法使用或者根本就不留联系方式、留成别人的联系方式,这就很尴尬了。

    64、网站要有网站地图,这样更方便搜索引擎爬向每一个链接并收录网站。xml和html的地图都可以加,现在html的地图已经意义不大了,但xml的还是有点意义的。

    65、资讯类网站可以加上rss订阅功能,可以让别人订阅你的网站内容,尤其是博客,最好有这个功能。

    66、百度ping功能已经升级被百度自动推送功能取代,有技术开发能力的可以做一下这个功能。

    67、网站上可以加上分享组件,让更多的人分享你网站好的内容。

    68、网站上可以加上统计代码,目前建议用百度的统计,因为站长统计是看不到百度搜索进来的关键词。加了统计可以检测自己的SEO效果。
    89、选择好的外链平台没有错,但同时还要注意外链广度,就是要在多个外链平台上发布,这样即使某几个外链平台出现问题,还有其他外链支撑,你的网站不至于一下子排名一落千丈。

    90、我们希望发布的外链都是很快被收录的,但见的没有被收录的外链帖子就不能对网站带来好处。有的外链实际上被搜索引擎加入到搜索库,只是因为平台的原因还不能够搜索到而已,这只是时间的问题。

    100、用户体验的提升是全方位的,比如你为了用户更容易用你的网站,小到每一个按钮点击的舒适度、页面设计的美观度,大到从APP、微信公众平台、提成分润模式的设计、架构、优化,都可以提升网站的用户体验。很多网站SEO人员是网站建好才进驻进来的,所以用户体验的提升大部分时间还是要靠网站经营者自己完善,SEO人员顶多给一些意见建议。用户体验做好了,网站的SEO工作就更有效果。

    8、网站速度可以的话,不一定非要做cdn加速,有时候免费cdn加速可能会拖慢你网站速度

    9、百度的排名算法最苛刻,作弊惩罚机制比较完善,所以尽可能以百度的规则来玩SEO。

    10、拿一个老域名来做新站,不知道老域名以前做什么行业的情况下尽量不要冒这个险,万一以前作弊过呢

    11、域名注册用com后缀的比较多,但其他后缀域名也可以,排名上都是一样的

    12、不建议用中文域名,因为你看不到一个中文域名被搜索引擎收录,现在还不支持

    13、服务器备案与否不影响网站排名,但不备案只能放国外,速度慢就影响排名了

    14、网站做好,尽量让建站公司托管,自己管理风险很大,如果处理不当对排名是致命的

    15、独立IP服务器肯定对排名好,但现在影响越来越小

    16、IP历史会一定程度上影响排名,但我们很难查到它的历史,所以能做的就是尽量别频繁换IP

    17、换IP会一段时间内影响排名,长期来看几乎是没有影响的

    18、http协议和https协议的网站是同一个网站,搜索引擎会自动识别,所以不必担心

    19、gzip压缩会让网站打开快一些,但耗费资源,所以建议是买个好点的服务器,开启GZIP压缩

    1、域名年龄老的网站排名更好

    2、服务器一定要稳定、不能经常打不开

    3、程序不能经常报错

    4、网站一般不要频繁改版
    69、目前最好的手机站做法是做二级域名的、与PC网站数据互通的手机网站。

    70、网站源代码手写原创是最好的,用模板来做的就会差一点。模板网站主要差在:源码被反复使用多次、有些程序不安全、售后差出问题不容易快速修复。
    72、市面上利于SEO的程序很多,基本满足:目录化结构、静态化网页、安全高效这三个条件,就可以用来做利于SEO的网站。

    73、选择网站程序要考虑网站的拓展性,尽可能在下次改版的时候不换后台,直接换皮肤或增加新功能。

    74、网站尽可能的经常做内容更新,一个经常更新的网站才是活网站。更新内容越多,网站信息量越大,搜索引擎越喜欢。

    75、网站内容更新要宁缺毋滥,不能为了增加内容而用垃圾的信息来填充,起码的要求是要语句通顺,中心明确。汉文seo

    76、网站经常更新高质量的原创文章肯定对网站有帮助,但第一条件是:高质量,第二条件才是原创。不要为了原创而去写三四个字充当原创,或者弄一些语句不通顺的东西去充当原创。

    77、高质量内容的判断标准很简单:语句通顺、中心明确、人们爱看。如果你的这个文章被人搜到了,他们愿意仔细的看,恨不得把每一个字都琢磨一下,这就是高质量的内容。

    78、文章内容繁杂的时候不妨多做一些专题页或者宣传单页,把优质内容汇集起来,这样你的这个专题页就是高质量的内容,而相应的你所推荐的这些高质量内容也会更加受到人们的重视。
    71、某梦的网站程序比较利于SEO,但是很多人在使用的过程中做不好安全防护,另外这个程序在网上被黑客研究并发布攻击攻略,所以排名好的某梦网站被黑几率接近100%,如果你不是技术特别高,尽可能选择其他程序来做。

    91、网站的外链一定不是越多越好,如果短时间能增加很多外链,也许你网站的灾难也就来了。适可而止、贪婪是魔鬼,揠苗助长终究是愚蠢的事情。

    92、无为则是有为,外链不发都要比那种乱发的强(当然一点不发也不行)。外链要找专业的SEO团队来做,不是他们的效率多高,是他们知道你网站目前是什么状态,目前应该怎么做。

    93、友情链接是一种特殊的外链,尽可能你所换的链接要有排名,有一个技巧就是看他在站长之家的预计流量,如果有预计流量则换,没有就不要换。

    94、有人在换友情链接之前,会给自己网站伪造一些数据,比如说给自己的品牌词流量刷上去,这样自己的预计流量就很大,这时候要剔除这些无效的伪造的数据,看他真实的数据情况,这一切靠的都是经验。

    95、面对目前搜索引擎动荡的局面,有些时候你的友情链接排名下降,不要急着给去掉,可以等待一阵子观察观察,如果实在没救了再去掉。保持你友情链接的稳定也比较重要。

    96、入侵别人网站,做黑链的方式可能会让你排名短时间能上升,但长远来看,对网站的伤害很大,因为黑链不会稳定,而且这是搜索引擎严厉打击的,一旦查到格杀勿论。

    97、网站越老越能承受一些外链作弊手段,这是搜索引擎的容忍,但不代表搜索引擎看不到,所以看到别人网站作弊排名上去了,不要羡慕。因为你哪一个新站或者表现一般的网站,你用作弊方式也做不上去。

    98、外链发布有两种思路:一个是给你网站做广告,一个是给你网站的内容做转发。两种方式做出来的网站都相对自然,同时用两种方式做外链是比较好的。

    99、不管是外链还是内容更新或者网站建设,都要考虑用户体验,用户体验有时候很难用语言来描述,但总体上就是你要让网站是给人看的,是给人用的,人们看了还想看,用了还想用,进来一次就忘不掉,这样你的网站用户体验就是好的,排名自然也不会差。

    展开全文
  • iOS之性能优化·优化App的启动速度

    万次阅读 2020-11-11 04:16:35
    苹果是一家特别注重用户体验的公司,过去几年一直在优化 App 的启动时间,特别是去年的 WWDC 2019 keynote [1] 上提到,在过去一年苹果开发团队对启动时间提升了 200%; 虽然说是提升了 200%,但是有些问题还是没有...

    抛砖引玉

    • 启动是 App 给用户的第一印象,启动越慢用户流失的概率就越高,良好的启动速度是用户体验不可缺少的一环。
    • 苹果是一家特别注重用户体验的公司,过去几年一直在优化 App 的启动时间,特别是去年的 WWDC 2019 keynote [1] 上提到,在过去一年苹果开发团队对启动时间提升了 200%;
    • 虽然说是提升了 200%,但是有些问题还是没有说清楚,比如:
      • 为什么优化了这么多时间?
      • 作为开发者的我们,我们还可以做哪些针对启动速度的优化?
      • 所以我们今天结合 WWDC2019 - 423 - Optimizing App Launch [2] 聊一下和启动相关的东西。

    概念引入

    一、Mach-O
    • Mach-O 是 iOS 系统不同运行时期可执行文件的文件类型统称。主要分以下三类:
      • Executable :可执行文件,是 App 中的主要二进制文件;
      • Dylib :动态库,在其他平台也叫 DSO 或者 DLL;
      • Bundle :苹果平台特有的类型,是无法被连接的 Dylib。只能在运行时通过 dlopen() 加载。
    • Mach-O 的基本结构如下图所示,分为三个部分:

    在这里插入图片描述

    • 结构分析:
      • Header: 包含了 Mach-O 文件的基本信息,如 CPU 架构,文件类型,加载指令数量等;
      • Load Commands: 是跟在 Header 后面的加载命令区,包含文件的组织架构和在虚拟内存中的布局方式,在调用的时候知道如何设置和加载二进制数据;
      • Data:包含 Load Commands 中需要的各个 Segment 的数据;
    • 绝大多数 Mach-O 文件包括以下三种 Segment:
      • __TEXT :代码段,包括头文件、代码和常量,只读不可修改。
      • __DATA :数据段,包括全局变量, 静态变量等,可读可写。
      • __LINKEDIT :如何加载程序, 包含了方法和变量的元数据(位置,偏移量),以及代码签名等信息,只读不可修改。
    二、Image
    • 指的是 Executable,Dylib 或者 Bundle 的一种。
    三、Framework
    • 有很多东西都叫做 Framework,但在本文中,Framework 指的是一个 dylib,它周围有一个特殊的目录结构来保存该 dylib 所需的文件。
    • 一般会用 Root Controller 的 viewDidApper 作为渲染的终点,但其实这时候首帧已经渲染完成一小段时间了,Apple 在 MetricsKit 里对启动终点定义是第一个CA::Transaction::commit()。
    • 什么是 CATransaction 呢?我们先来看一下渲染的大致流程:

    在这里插入图片描述

    • iOS 的渲染是在一个单独的进程 RenderServer 做的,App 会把 Render Tree 编码打包给 RenderServer,RenderServer 再调用渲染框架(Metal/OpenGL ES)来生成 bitmap,放到帧缓冲区里,硬件根据时钟信号读取帧缓冲区内容,完成屏幕刷新。CATransaction 就是把一组 UI 上的修改,合并成一个事务,通过 commit 提交。
    • 渲染可以分为四个步骤:
      • Layout(布局),源头是 Root Layer 调用[CALayer layoutSubLayers],这时候 UIViewController 的 viewDidLoad 和 LayoutSubViews 会调用,autolayout 也是在这一步生效;
      • Display(绘制),源头是 Root Layer 调用[CALayer display],如果 View 实现了 drawRect 方法,会在这个阶段调用;
      • Prepare(准备),这个过程中会完成图片的解码;
      • Commit(提交),打包 Render Tree 通过 XPC 的方式发给 Render Server。

    在这里插入图片描述

    四、虚拟内存(Virtual Memory)
    • 虚拟内存是建立在物理内存和进程之间的中间层。是一个连续的逻辑地址空间,而且逻辑地址可以没有对应的实际物理内存地址,也可以让多个逻辑地址对应到一个物理内存地址上。
    • 内存可以分为虚拟内存和物理内存,其中物理内存是实际占用的内存,虚拟内存是在物理内存之上建立的一层逻辑地址,保证内存访问安全的同时为应用提供了连续的地址空间。
    • 物理内存和虚拟内存以页为单位映射,但这个映射关系不是一一对应的:一页物理内存可能对应多页虚拟内存;一页虚拟内存也可能不占用物理内存。

    在这里插入图片描述

    • iPhone 6s 开始,物理内存的 Page 大小是 16K,6 和之前的设备都是 4K,这是 iPhone 6 相比 6s 启动速度断崖式下降的原因之一。
    五、Page Fault
    • 当进程访问一个没有对应物理地址的逻辑地址时,会发生 Page Fault。
    六、Lazy Reading
    • 某个想要读取的页没有在内存中就会触发 Page Fault,系统通过调用 mmap() 函数读取指定页,这个过程叫做 Lazy Reading。
    七、COW(Copy-On-Write)
    • 当进程需要对某一页内容进行修改时,内核会把需要修改的部分先复制一份,然后再修改,并把逻辑地址重新映射到新的物理内存去,这个过程叫做 Copy-On-Write。
    八、Dirty Page & Clean Page
    • Image 加载后,被修改过内容的 Page 叫做 Dirty Page,会包含着进程特定的信息。
    • 与之相对的叫 Clean Page,可以从磁盘重新生成。
    九、共享内存(Share RAM)
    • 当多个 Mach-O 都依赖同一个 Dylib(eg. UIKit)时,系统会让这几个 Mach-O 的调用 Dylib 的逻辑地址都指向同一块物理内存区域,从而实现内存共享。
    • Dirty Page 为进程独有,不能被共享。
    十、地址空间布局随机化(ASLR)
    • 当 Image 加载到逻辑地址空间的时候,系统会利用 ASLR 技术,使得 Image 的起始地址总是随机的,以避免黑客通过起始地址+偏移量找到函数的地址。
    • 当系统利用 ASLR 分配了随机地址后,从 0 到该地址的整个区间会被标记为不可访问,意味着不可读,不可写,不可被执行。这个区域就是 __PAGEZERO 段,它的大小在 32 位系统是 4KB+,而在 64 位系统是 4GB+
    十一、代码签名(Code Sign)
    • 代码签名可以让 iOS 系统确保要被加载的 Image 的安全性,用 Code Sign 设置签名时,每页内容都会生成一个单独的加密散列值,并存储到 __LINKEDIT 中去,系统在加载时会校验每页内容确保没有被篡改。
    十二、dyld(dynamic loader)
    • dyld 是 iOS 上的二进制加载器,用于加载 Image。有不少人认为 dyld 只负责加载应用依赖的所有动态链接库,这个理解是错误的。dyld 工作的具体流程如下:

    在这里插入图片描述

    十三、Load dylibs
    • dyld 在加载 Mach-O 之前会先解析 Header 和 Load Commands, 然后就知道了这个 Mach-O 所依赖的 dylibs,以此类推,通过递归的方式把全部需要的 dylib 都加载进来。
    • 一般来说,一个 App 所依赖的 dylib 在 100 - 400 左右,其中大多数都是系统的 dylib,因为有缓存和共享的缘故,读取速度比较高。
    十四、Fix-ups
    • 因为 ASLR 和 Code Sign 的原因,刚被加载进来的 dylib 都处于相对独立的状态,为了把它们绑定起来,需要经过一个 Fix-ups 过程。
    • Fix-ups 主要有两种类型:Rebase 和 Bind。
    十五、PIC(Position Independent Code)
    • 因为代码签名的原因,dyld 无法直接修改指令,但是为了实现在运行时可以 Fix-ups,在 code gen 时,通过动态 PIC(Position Independent Code)技术,使本来因为代码签名限制不能再修改的代码,可以被加载到间接地址上。
    • 当要调用一个方法时,会先在 __DATA 段中建立一个指针指向这个方法,再通过这个指针实现间接调用。
    十六、Rebase
    • Rebase:修复内部指针。这是因为 Mach-O 在 mmap 到虚拟内存的时候,起始地址会有一个随机的偏移量 slide,需要把内部的指针指向加上这个 slide。
    • Rebase 是针对“因为 ASLR 导致 Mach-O 在加载到内存中是一个随机的首地址”这一个问题做一个数据修正的过程。会将内部指针地址都加上一个偏移量,偏移量的计算方法如下:
       Slide = actual_address - preferred_address
    
    • 所有需要 Rebase 的指针信息已经被编码到 __LINKEDIT 里。然后就是不断重复地对 __DATA 中需要 Rebase 的指针加上这个偏移量。这个过程中可能会不断发生 Page Fault 和 COW,从而导致 I/0 的性能损耗问题,不过因为 Rebase 处理的是连续地址,所以内核会预先读取数据,减少 I/O 的消耗。
    十七、Binding
    • Binding:修复外部指针。这个比较好理解,因为像 printf 等外部函数,只有运行时才知道它的地址是什么,Binding 就是把指针指向这个地址。
    • Binding 就是对调用的外部符号进行绑定的过程。比如我们要使用到 UITableView,即符号 OBJC_CLASS$_UITableView,但这个符号又不在 Mach-O 中,需要从 UIKit.framework 中获取,因此需要通过 Binding 把这个对应关系绑定到一起。
    • 在运行时,dyld 需要找到符号名对应的实现。而这需要很多计算,包括去符号表里找。找到后就会将对应的值记录到 __DATA 的那个指针里。Binding 的计算量虽然比 Rebasing 更多,但实际需要的 I/O 操作很少,因为之前 Rebasing 已经做过了。
    • 举个例子:一个 Objective C 字符串@“1234”,编译到最后的二进制的时候是会存储在两个 section 里的:
      • __TEXT,__cstring,存储实际的字符串"1234"
      • __DATA,__cfstring,存储 Objective C 字符串的元数据,每个元数据占用 32Byte,里面有两个指针:内部指针,指向__TEXT,__cstring中字符串的位置;外部指针 isa,指向类对象的,这就是为什么可以对 Objective C 的字符串字面量发消息的原因。
    • 如下图,编译的时候,字符串 1234 在__cstring的 0x10 处,所以 DATA 段的指针指向 0x10。但是 mmap 之后有一个偏移量 slide=0x1000,这时候字符串在运行时的地址就是 0x1010,那么 DATA 段的指针指向就不对了。Rebase 的过程就是把指针从 0x10,加上 slide 变成 0x1010。运行时类对象的地址已经知道了,bind 就是把 isa 指向实际的内存地址。
      在这里插入图片描述
    十八、dyld2 & dyld3
    • 在 iOS 13 之前,所有的第三方 App 都是通过 dyld 2 来启动 App 的,主要过程如下:
      • 解析 Mach-O 的 Header 和 Load Commands,找到其依赖的库,并递归找到所有依赖的库
      • 加载 Mach-O 文件
      • 进行符号查找
      • 绑定和变基
      • 运行初始化程序
    • 上面的所有过程都发生在 App 启动时,包含了大量的计算和I/O,所以苹果开发团队为了加快启动速度,在 WWDC2017 - 413 - App Startup Time: Past, Present, and Future [4] 上正式提出了 dyld3。
    • dyld2 & dyld3 比较如下:

    在这里插入图片描述

    • dyld3 被分为了三个组件:
      • 一个进程外的 MachO 解析器

        • 预先处理了所有可能影响启动速度的 search path、@rpaths 和环境变量
        • 然后分析 Mach-O 的 Header 和依赖,并完成了所有符号查找的工作
        • 最后将这些结果创建成了一个启动闭包
        • 这是一个普通的 daemon 进程,可以使用通常的测试架构
      • 一个进程内的引擎,用来运行启动闭包

        • 这部分在进程中处理
        • 验证启动闭包的安全性,然后映射到 dylib 之中,再跳转到 main 函数
        • 不需要解析 Mach-O 的 Header 和依赖,也不需要符号查找。
      • 一个启动闭包缓存服务

        • 系统 App 的启动闭包被构建在一个 Shared Cache 中, 我们甚至不需要打开一个单独的文件
        • 对于第三方的 App,我们会在 App 安装或者升级的时候构建这个启动闭包。
        • 在 iOS、tvOS、watchOS中,这这一切都是 App 启动之前完成的。在 macOS 上,由于有 Side Load App,进程内引擎会在首次启动的时候启动一个 daemon 进程,之后就可以使用启动闭包启动了。
    • dyld 3 把很多耗时的查找、计算和 I/O 的事前都预先处理好了,这使得启动速度有了很大的提升。
    十九、mmap
    • mmap 的全称是 memory map,是一种内存映射技术,可以把文件映射到虚拟内存的地址空间里,这样就可以像直接操作内存那样来读写文件。当读取虚拟内存,其对应的文件内容在物理内存中不存在的时候,会触发一个事件:File Backed Page In,把对应的文件内容读入物理内存。
    • 启动的时候,Mach-O 就是通过 mmap 映射到虚拟内存里的(如下图)。下图中部分页被标记为 zero fill,是因为全局变量的初始值往往都是 0,那么这些 0 就没必要存储在二进制里,增加文件大小。操作系统会识别出这些页,在 Page In 之后对其置为 0,这个行为叫做 zero fill。
      在这里插入图片描述
    二十、Page In
    • 启动的路径上会触发很多次 Page In,其实也比较容易理解,因为启动的会读写二进制中的很多内容。Page In 会占去启动耗时的很大一部分,我们来看看单个 Page In 的过程:
      在这里插入图片描述
    • 分析如下:
      • MMU 找到空闲的物理内存页面;
      • 触发磁盘 IO,把数据读入物理内存;
      • 如果是 TEXT 段的页,要进行解密;
      • 对解密后的页,进行签名验证;
    • 其中解密是大头,IO 其次。为什么要解密呢?因为 iTunes Connect 会对上传 Mach-O 的 TEXT 段进行加密,防止 IPA 下载下来就直接可以看到代码。这也就是为什么逆向里会有个概念叫做“砸壳”,砸的就是这一层 TEXT 段加密。iOS 13 对这个过程进行了优化,Page In 的时候不需要解密了。
    二十一、二进制重排
    • 既然 Page In 耗时,有没有什么办法优化呢?
    • 启动具有局部性特征,即只有少部分函数在启动的时候用到,这些函数在二进制中的分布是零散的,所以 Page In 读入的数据利用率并不高。如果我们可以把启动用到的函数排列到二进制的连续区间,那么就可以减少 Page In 的次数,从而优化启动时间:
    • 以下图为例,方法 1 和方法 3 是启动的时候用到的,为了执行对应的代码,就需要两次 Page In。假如我们把方法 1 和 3 排列到一起,那么只需要一次 Page In,从而提升启动速度。
      在这里插入图片描述
    • 链接器 ld 有个参数-order_file 支持按照符号的方式排列二进制。获取启动时候用到的符号的有很多种方式,这里不做说明。

    IPA 构建

    • 既然要构建,那么必然会有一些地方去定义如何构建,对应 Xcode 中的两个配置项:
      • Build Phase:以 Target 为维度定义了构建的流程。可以在 Build Phase 中插入脚本,来做一些定制化的构建,比如 CocoaPod 的拷贝资源就是通过脚本的方式完成的。
      • Build Settings:配置编译和链接相关的参数。特别要提到的是 other link flags 和 other c flags,因为编译和链接的参数非常多,有些需要手动在这里配置。很多项目用的 CocoaPod 做的组件化,这时候编译选项在对应的.xcconfig 文件里。
    • 以单 Target 为例,来看下构建流程:
      在这里插入图片描述
    • 流程说明:
      • 源文件(.m/.c/.swift 等)是单独编译的,输出对应的目标文件(.o)
      • 目标文件和静态库/动态库一起,链接出最后的 Mach-O
      • Mach-O 会被裁剪,去掉一些不必要的信息
      • 资源文件如 storyboard,asset 也会编译,编译后加载速度会变快
      • Mach-O 和资源文件一起,打包出最后的.app
      • 对.app 签名,防篡改

    编译

    • 编译器可以分为两大部分:前端和后端,二者以 IR(中间代码)作为媒介。这样前后端分离,使得前后端可以独立的变化,互不影响。C 语言家族的前端是 clang,swift 的前端是 swiftc,二者的后端都是 llvm。
      • 前端负责预处理,词法语法分析,生成 IR;
      • 后端基于 IR 做优化,生成机器码;

    在这里插入图片描述

    • 那么如何利用编译优化启动速度呢?
      代码数量会影响启动速度,为了提升启动速度,我们可以把一些无用代码下掉。那怎么统计哪些代码没有用到呢?可以利用 LLVM 插桩来实现。LLVM 的代码优化流程是一个一个 Pass,由于 LLVM 是开源的,我们可以添加一个自定义的 Pass,在函数的头部插入一些代码,这些代码会记录这个函数被调用了,然后把统计到的数据上传分析,就可以知道哪些代码是用不到的了 。
    • Facebook 给 LLVM 提的 order_file[2]的 feature 就是实现了类似的插桩。

    链接

    • 经过编译后,我们有很多个目标文件,接着这些目标文件会和静态库,动态库一起,链接出一个 Mach-O。链接的过程并不产生新的代码,只会做一些移动和补丁。

    在这里插入图片描述

    • tbd 的全称是 text-based stub library,是因为链接的过程中只需要符号就可以了,所以 Xcode 6 开始,像 UIKit 等系统库就不提供完整的 Mach-O,而是提供一个只包含符号等信息的 tbd 文件。
    • 最开始讲解 Page In 的时候,我们提到 TEXT 段的页解密很耗时,有没有办法优化呢?可以通过 ld 的-rename_section,把 TEXT 段中的内容,比如字符串移动到其他的段(启动路径上难免会读很多字符串),从而规避这个解密的耗时。
      在这里插入图片描述

    App 启动

    一、启动定义
    • 启动有两种定义:
      • 广义:点击图标到首页数据加载完毕;
      • 狭义:点击图标到 Launch Image 完全消失第一帧;
    • 不同产品的业务形态不一样,对于抖音来说,首页的数据加载完成就是视频的第一帧播放;对其他首页是静态的 App 来说,Launch Image 消失就是首页数据加载完成。由于标准很难对齐,所以我们一般使用狭义的启动定义:即启动终点为启动图完全消失的第一帧
    • 启动最佳时间是 400ms 以内,因为启动动画时长是 400ms。
    • 这是从用户感知维度定义启动,那么代码上如何定义启动呢?Apple 在 MetricKit 中给出了官方计算方式:
      • 起点:进程创建的时间;
      • 终点:第一个CA::Transaction::commit();
    • CATransaction 是 Core Animation 提供的一种事务机制,把一组 UI 上的修改打包,一起发给 Render Server 渲染。
    二、App 启动为什么这么重要?
    • App 启动是和用户的第一个交互过程,所以要尽量缩短这个过程的时间,给用户一个良好的第一印象;
    • 启动代表了你的代码的整体性能,如果启动的性能不好,其他部分的性能可能也不会太好
      启动会占用 CPU 和内存,从而影响系统性能和电池;
    • 所以我们要好好优化启动时间。
    三、启动类型

    App 的启动类型分为三类

    • Cold Launch 也就是冷启动,即为系统里没有任何进程的缓存信息,典型的是重启手机后直接启动 App。冷启动需要满足以下几个条件:
      • 重启之后
      • App 不在内存中
      • 没有相关的进程存在
    • Warm Launch 也就是热启动,即为如果把 App 进程杀了,然后立刻重新启动,这次启动就是热启动,因为进程缓存还在。热启动需要满足以下几个条件:
      • App 刚被终止
      • App 还没完全从内存中移除
      • 没有相关的进程存在
    • Resume Launch 指的是被挂起的 App 继续的过程,大多数时候不会被定义为启动,因为此时 App 仍然活着,只不过处于 suspended 状态。需要满足以下几个条件:
      • App 被挂起
      • App 还全部都在内存中
      • 还存在相关的进程
    四、App 启动阶段
    • App 启动分为三个阶段:
      • 初始化 App 的准备工作;
      • 绘制第一帧 App 的准备工作及绘制(这里的第一帧并不是获取到数据之后的第一帧,可以是一张占位视图),这时候用户与App已经可以交互了,比如 tabbar 切换;
      • 获取到页面的所有数据之后的完整的绘制第一帧页面。
    • 在这个地方,苹果再次强调了一下,建议「用户从点击 App 图标到可以再次交互,也就是第二阶段结束」的时间最好在 400ms 以内。目前来看,大部分 App 都没有达到这个目标。
    • 下面,我们把上面三个阶段分成下面这 6 个部分,讲一下这几个阶段做了什么以及有什么可以优化的地方。

    在这里插入图片描述

    五、启动优化

    System Interface

    • 初始化 App 的准备工作,系统主要做了两个事情:Load dylibs 和 libSystem init;
    • 在 2017 年苹果介绍过 dyld3 给系统 App 带来了多少优化,今年 dyld3 正式开发给开发者使用,这意味着 iOS 系统会将热启动的运行时依赖给缓存起来,以达到减少启动时间的目的,这也就是提升 200% 的原因之一。
    • 除此之外,在 Load dylibs 阶段,开发者还可以做以下优化:
      • 避免链接无用的 frameworks,在 Xcode 中检查一下项目中的「Linked Frameworks and Librares」部分是否有无用的链接。
      • 避免在启动时加载动态库,将项目的 Pods 以静态编译的方式打包,尤其是 Swift 项目,这地方的时间损耗是很大的。
      • 硬链接你的依赖项,这里做了缓存优化。
    • 也许有人会困惑是不是使用了 dyld3 了,我们就不需要做 Static Link 了,其实还是需要的,感兴趣的可以看一下 Static linking vs dyld3 [5] 这篇文章,里面有一个详细的数据对比。
    • libSystem init 部分,主要是加载一些优先级比较低的系统组件,这部分时间是一个固定的成本,所以我们开发人员不需要关心。

    Static Runtime Initializaiton

    • 这个阶段主要是 Objective-C 和 Swift Runtime 的初始化,会调用所有的 +load 方法,将类的信息注册到 runtime 中。
    • 在这个阶段,原则上不建议开发者做任何事情,所以为了避免一些启动时间的损耗,你可以做以下几个事情:
      • 在 framework 开发时,公开专有的初始化 API;
      • 减少在 +load 中做的事情;
      • 使用 initialize 进行懒加载初始化工作;

    UIKit Initializaiton

    • 这个阶段主要做了两个事情:
      • 实例化 UIApplication 和 UIApplicationDelegate;
      • 开始事件处理和系统集成。
    • 所以这个阶段的优化也比较简单,需要做两个事情:
      • 最大限度的减少 UIApplication 子类初始化时候的工作,更甚至与不子类化 UIApplication;
      • 减少 UIApplicationDelegate 的初始化工作。

    Application Initializaiton

    • 这个阶段主要是生命周期方法的回调,也正是我们开发者最熟悉的部分。
    • 调用 UIApplicationDelegate 的 App 生命周期方法:
    	application:willFinishLaunchingWithOptions: 
        application:didFinishLaunchingWithOptions:
    
    • UIApplicationDelegate 的 UI 生命周期方法:
    	applicationDidBecomeActive:
    
    • 同时,iOS 13 针对 UISceneDelegate 增加了新的回调:
    	scene:willConnectToSession:options:
        sceneWillEnterForeground:
        sceneDidBecomeActive:
    
    • 也会在这个阶段调用。感兴趣的可以关注一下 Getting the Most out of Multitasking 这个 Session,暂时没有视频资源,怀疑是现场演示翻车了,所以没有把视频资源放出来。
    • 在这个阶段,我们可以做的优化:
      • 推迟和启动时无关的工作
      • Senens 之间共享资源

    Fisrt Frame Render

    • 这个阶段主要做了创建、布局和绘制视图的工作,并把准备好的第一帧提交给渲染层渲染。会频繁调用以下几个函数:
    	loadView
        viewDidLoad 
        layoutSubviews
    
    • 在这个阶段,开发者可以做的优化:
      • 减少视图层级,懒加载一些不需要的视图;
      • 优化布局,减少约束。
    • 更多细节可以从 WWDC2018 - 220 - High Performance Auto Layout [6] 中了解。

    Extend

    • 大部分 App 都会通过异步的方式获取数据,并最终呈现给用户。我们把这一部分称为 Extend。
    • 因为这一部分每个 App 的表现都不一样,所以苹果建议开发者使用 os_signpost 进行测量然后慢慢分析慢慢优化。

    load 举例

    • 如果+load 方法里的内容很简单,会影响启动时间么?比如这样的一个+load 方法?
    	+ (void)load { 
    		printf("1234"); 
    	}
    
    • 编译完了之后,这个函数会在二进制中的 TEXT 两个段存在:__text存函数二进制,cstring存储字符串 1234。为了执行函数,首先要访问__text触发一次 Page In 读入物理内存,为了打印字符串,要访问__cstring,还会触发一次 Page In。
    • 为了执行这个简单的函数,系统要额外付出两次 Page In 的代价,所以 load 函数多了,page in 会成为启动性能的瓶颈。
      在这里插入图片描述
    • static initializer 产生的条件:静态初始化是从哪来的呢?以下几种代码会导致静态初始化
      • attribute((constructor))
      • static class object
      • static object in global namespace
    • 注意,并不是所有的 static 变量都会产生静态初始化,编译器很智能,对于在编译期间就能确定的变量是会直接 inline。
    	// 会产生静态初始化
    	class Demo{ 
    	static const std::string var_1; 
    	};
    	const std::string var_2 = "1234"; 
    	static Logger logger;
    	// 不会产生静态初始化
    	static const int var_3 = 4; 
    	static const char * var_4 = "1234";
    
    • std::string 会合成 static initializer 是因为初始化的时候必须执行构造函数,这时候编译器就不知道怎么做了,只能延迟到运行时。
    • +load 和 static initializer 执行完毕之后,dyld 会把启动流程交给 App,开始执行 main 函数。main 函数里要做的最重要的事情就是初始化 UIKit。UIKit 主要会做两个大的初始化:
      • 初始化 UIApplication;
      • 启动主线程的 Runloop;
    • 由于主线程的 dispatch_async 是基于 runloop 的,所以在+load 里如果调用了 dispatch_async 会在这个阶段执行。
    • 线程在执行完代码就会退出,很明显主线程是不能退出的,那么就需要一种机制:事件来的时候执行任务,否则让线程休眠,Runloop 就是实现这个功能的。
    • Runloop 本质上是一个While 循环,在图中橙色部分的 mach_msg_trap 就是触发一个系统调用,让线程休眠,等待事件到来,唤醒 Runloop,继续执行这个 while循环。
    • Runloop 主要处理几种任务:Source0,Source1,Timer,GCD MainQueue,Block。在循环的合适时机,会以 Observer 的方式通知外部执行到了哪里。
      在这里插入图片描述
    • 那么,Runloop 与启动又有什么关系呢?
      • App 的 LifeCycle 方法是基于 Runloop 的 Source0 的;
      • 首帧渲染是基于 Runloop Block 的。
        在这里插入图片描述
    • Runloop 在启动上主要有几点应用:
      • 精准统计启动时间;
      • 找到一个时机,在启动结束去执行一些预热任务;
      • 利用 Runloop 打散耗时的启动预热任务。

    测量 App 启动时间

    • 要找到启动过程中的问题,就要进行多次测量并前后比较。但是如果变量没有控制好,就会导致误差。
    • 所以为了保证测量的数据能够真实的反应问题,我们要减少不稳定性因素,保证在可控的相近的环境下进行测量,最后使用一致的结果来分析。
    • ① 条件一致性
      • 为了保证环境一致,我们可以做下面这几个事情:
        • 重启手机,并等待 2-3 分钟
        • 启用飞行模式或者使用模拟网络
        • 不使用或者不变更 iCloud 的账户
        • 使用 release 模式进行 build
        • 测量热启动时间
        • iColud 账户切换会影响性能,所以不要切换账号或者不开启 iCloud。
    • ② 测量注意点
      • 尽可能的使用具有代表性的数据进行测试;
      • 如果不使用具有代表性的数据进行测试,就会出现偏差;
      • 使用不同的新旧设备进行测试;
      • 最后你还可以使用 XCTest 来测试,多运行几次,取平均结果。
    • ③ 关于使用 XCTest 测试启动时间的信息,可以看一下 WWDC2019 - 417 - Improving Battery Life and Performance [7],但是我测试了一下,目前好像还有一部分 API 还没有开放出来,暂时还不能使用。

    使用 Instruments 分析和优化 App 启动过程

    一、Minimize Work
    • 推迟与第一帧无关的工作
    • 从主线程移开阻塞工作
    • 减少内存使用量
    二、Prioritize Work
    • 定义好任务的优先级
    • 利用好 GCD 来优化你的启动速度
    • 让重要的事情保持优先
    三、Optimize Work
    • 简化现有工作,比如只请求必要的数据
    • 优化算法和数据结构
    • 缓存资源和计算
    四、使用 Instruments 分析 App 启动过程
    • 当知道如何优化之后,我们需要针对我们的启动过程进行分析。Xcode 11 的 Instruments 为此新增了一个 App launch 模板,让开发者可以更好的分析自己 App 的启动速度。

    在这里插入图片描述

    • 运行后可以看到各个阶段的具体时间,根据数据进行优化,还能看到耗时的函数调用。

    系统优化

    • 苹果做了很多优化,下面这几个高亮的是和启动速度有关的优化:

    在这里插入图片描述

    • 但是不知道是不是时间原因,在 session 中对于这部分的解释特别少,很难理解 200% 到底做了什么。
    • 但是 Craig Federighi 在 The Talk Show Live From WWDC 2019, With Craig Federighi and Greg Joswiak[9] 中针对为什么优化了 200% 说了这样一段话:

    Isn’t that crazy that was quite a discovery for us. No it turns out that over times as in terms of the way the apps were encrypted and the way fair play worked and so forth. The encryption became part of the critical path actually of launching the apps. I mean the processors are capable or up and through the thing that actually it was a problem. And then there are other optimizations that based on what was visible to system at certain things. And so it actually cut out optimization opportunities and so when we really identified that opportunity we said okay. We can actually come up with better format that’s gonna eliminate that being on the critical path, It’s going to enable all these pre-binding things. And then we did a whole bunch of other work to optimize the objective-c runtime to optimize the linker the dynamic linker a bunch of other things and you put it all together. And yeah that I mean a cold launch this is we’ve never had a win like this to launch time in a single release.

    • 从这段话中,除了 dyld3 的功劳之外,减少对代码签名加密也是优化之一。

    监控线上用户 App 的启动

    • Xcode 11 在 Xcode Organizer 新增了一个监控面板,在这个面板里面可以查看多个维度的用户数据,其中还包括平均启动时间。
    • 当你通过 Instruments 分析完你的启动过程,并做了大量优化之后,你就可以通过 Xcode Organizer 来分析你这次优化效果到底怎么样。
    • 当然你可以通过去年新出的 MetricKit [10] 获取一些自定义的数据,具体参照 WWDC2019 - 417 -Improving Battery Life and Performance [11]

    参考资料

    展开全文
  • PostgreSQL 优化器入门

    千次阅读 2018-11-06 11:49:11
    优化器经典实现/数据库从业人员核心技能 专家推荐 “这门课从 SQL 示例入手,深入浅出地介绍了 PostgreSQL 优化器技术内幕,让读者能够快速熟悉 PostgreSQL 优化器,对 SQL 优化有非常好的指导作用,值得推荐!” ...

    课程亮点

    • 数据库内核专家通俗解读 PgSQL 优化器
    • 无需阅读分析 PgSQL 源码即可快速掌握
    • 优化器经典实现/数据库从业人员核心技能

    专家推荐

    “这门课从 SQL 示例入手,深入浅出地介绍了 PostgreSQL 优化器技术内幕,让读者能够快速熟悉 PostgreSQL 优化器,对 SQL 优化有非常好的指导作用,值得推荐!”

    ——谭峰,网名 francs,《PostgreSQL 实战》作者之一,《PostgreSQL 9 Administration Cookbook(第2版)》译者之一

    “我相信这一课程能够为对数据库优化器有兴趣却又对其复杂性望而却步的读者指点迷津。”

    ——林文,Pivotal 资深开发工程师

    “国内研究数据库优化器这一领域的人很少,希望本课程能带领广大读者进入这个广阔而精彩的世界。”

    ——李茂增,华为高斯数据库 SQL 优化专家

    “本课程内容用诙谐幽默的语言,将晦涩难懂的数据库查询优化器娓娓道来,是 PostgreSQL 及其他数据库从业者值得一读的好书!”

    ——张文升,PostgreSQL 社区核心成员,《PostgreSQL 实战》作者

    课程背景

    作为数据库的从业者,如果对优化器不够了解,便如同猛虎没有了利爪,苍鹰没有了翅膀,在使用数据库的过程中往往心有余而力不足。PostgreSQL 是世界上最先进的开源关系数据库,而 PgSQL 优化器被广泛认为教科书级实现。

    本课程从数据库一线开发人员的角度出发,通过实打实的案例,结合外在的系统表信息、参数信息、执行计划信息反向把 PgSQL 查询优化器的原理深入浅出、透彻地讲解明白。

    作者介绍

    张树杰,Pivotal 资深开发工程师、数据库内核专家。目前从事 Greenplum 数据库的内核开发工作。拥有超过 13 年的 IT 从业经验,多年从事国产数据库内核开发工作,对数据库内核各个方面均有涉猎,近些年尤其专注于研究对分布式数据库的查询优化、查询执行的改进工作。著有《PostgreSQL 技术内幕:查询优化深度探索》一书。

    课程大纲

    enter image description here

    适宜人群

    • 数据库开发从业人员与学生
    • 数据库优化器研究爱好者
    • 数据库内核研发人员

    课程内容

    开篇词:翻过数据库优化器这座山峰

    大家好,我是张树杰,是一名数据库内核开发者。我在 2018 年 6 月出版了《PostgreSQL 技术内幕:查询优化深度探索》一书,这本书对 PostgreSQL 优化器的源代码进行了详尽的分析,但也有一些朋友向我抱怨:“你只顾自己源码分析得 High,考虑过我们的感受吗?”是的,除了 PostgreSQL 的内核开发者,广阔天地间还有更多 PostgreSQL 的使用者以及其他数据库使用者。如果我们切换一个角度,从使用者的角度出发,是否能够把 PostgreSQL 的优化器解释清楚呢?于是我写了这个课程,相信跟随这个课程,大家可以翻过数据库优化器这座山峰。

    课程背景

    PostgreSQL 号称世界上最先进的开源关系数据库,它的优化器虽然比不上商业数据库的优化器那样复杂,但对于大部分用户来说,已经比较晦涩难懂。如果搞一个投票来评选数据库中最难以理解的模块,那么非优化器莫属。在使用 PostgreSQL 数据库的过程中,你可能会遇到下面这些问题:

    • 在你遇到一个比较糟糕的执行计划时,你是否有能力对其进行改造?
    • 当你遭遇一个莫名的慢查询时,你是否能够通过优化实现方法提升性能?
    • 当你创建的索引不为优化器所用时,你是否清楚地知道优化器的选择习性?
    • 你是否想通过等价改写一个 SQL 语句来改变执行计划,那等价改写 SQL 语句是否隐藏着某些规则?

    优化器是数据库的大脑。作为数据库的从业者,你是否想知道数据库的大脑在思考些什么?反之,如果对优化器不够了解,便如同猛虎没有了利爪、苍鹰没有了翅膀,在使用数据库的过程中往往心有余而力不足。因此今天我们明知山有虎,偏向虎山行,拿出愚公移山的精神,把优化器的知识消化掉。

    针对数据库从业人员的不同,我想对优化器的理解大致可以分成以下 3 个层次。

    • 层次一:粗浅了解,比如知道优化器分为逻辑优化和物理优化,了解一些逻辑优化的方法,知道执行计划的来源,能看懂优化器产生的执行计划。
    • 层次二:详细了解,在粗浅了解的基础上,能够根据自己对优化器的了解,调整出优化器“喜爱”的 SQL 语句,并且对于产生的执行计划的优劣一目了然,知其然更知其所以然。
    • 层次三:深度了解,需要对优化器的每个细节有清楚的认知,在我们写出一个 SQL 语句之后,可以庖丁解牛式地在脑海中浮现出语句在优化器中的优化过程,清楚地知道每个细节的实现过程。

    要想达到第一个层次只需要阅读一些基础理论即可,这种了解对于实际应用的意义不大;而要想达到第三个层次则需要细致地解读 PostgreSQL 优化器的源代码,这个过程又过于“艰辛”。因此,本课程的目标是使大家达到第二个层次:不分析数据库内核的源代码,从数据库使用者的角度出发,结合外在的系统表信息、参数信息、执行计划信息反向把 PostgreSQL 查询优化器的原理讲清楚。

    课程框架

    本课程内容划分为 5 大部分,共计 25 篇,覆盖了 PostgreSQL 优化器的所有重要知识点。我们通过介绍各种系统表信息、参数信息、执行计划信息,从而引出这些信息背后的优化器理论。

    导读部分

    万丈高楼平地起,这部分内容主要介绍了查询优化的一些基本概念。通过小明、大明和牛二哥对话的方式,将查询优化器的基础理论、基本流程、优化规则融入其中。对优化器不甚了解的同学能够快速进入第一个层次,从而为后面的学习打好基础。

    第一部分(第 01 ~ 03 课):准备工作

    工欲善其事、必先利其器。要想知道优化器怎么优化,就需要知道在优化之前,我们给优化器提供了什么。于是,第 01 课通过一个 SQL 示例来分析这个 SQL 语句的执行流程,从而能让读者清楚地知道 SQL 语句的执行过程。另外,查看 SQL 语句的执行计划是数据库从业人员必备的技能之一,我们不但对执行计划的查看进行了说明,还对执行计划背后隐藏的理论进行了说明。这些在第 02 课和第 03 课进行了说明,有了这些知识,就可以很方便地对优化器进行解读了。

    第二部分(第 04 ~ 11 课):逻辑优化部分

    逻辑优化也叫基于规则的优化,它主要优化的方式是检查查询树。如果查询树满足既定的优化规则,那么就按照规则对查询树进行改造。PostgreSQL 的优化规则虽然比较多,但是比较重要的有以下规则:子查询提升、表达式预处理、外连接消除、谓词下推、连接顺序交换和等价类推理等。我们在这一部分对这些规则进行了统一的说明。

    第三部分(第 12 ~ 18 课):物理优化部分

    物理优化中最重要的就是代价计算的部分。为了更好地加深理解代价,我们先是解读了统计信息的内容,根据统计信息可以计算查询数据的选择率,而统计信息和选择率是代价计算的基石。有了这些信息之后,我们尝试对扫描路径、连接路径、Non-SPJ 路径进行代价计算,这样就能让读者了解代价计算的具体过程了。在代价计算之后,我们对路径的搜索算法——动态规划方法和遗传算法进行了说明。

    第四部分(第 19 ~ 22 课):查询执行的部分

    执行计划在生成之后到底是如何执行的?这部分我们列举了一部分执行算子的执行过程中的关键点,这些关键点或者是优化措施,或者是实现的细节。理解这些执行算子的执行过程,有助于去理解执行算子的代价计算的流程。对于 Greenplum 数据库中的分布式执行计划,我们也尝试作出了说明。

    课程特色

    优化器是数据库从业人员必须熟练掌握的内容,而目前单独针对优化器的课程少之又少。在通常情况下,它可能在一本书中只能占到一个很小的章节,这些只能让读者对优化器有一个粗浅的了解。另外,如《PostgreSQL 技术内幕:查询优化深度探索》这样专业剖析 PostgreSQL 优化器源代码的书,对于数据库的“使用者”而言又过于繁琐了。因此本课程致力于不分析 PostgreSQL 的源代码,从一个 SQL 语句的执行开始,逐步分析优化器中涉及的各种优化原则。从参数、系统表、执行计划开始说明,逐步由表及里、由外及内,把 PostgreSQL 优化器背后隐藏的优化思想一一列举出来,最终做到深入浅出解读 PostgreSQL 优化器。

    课程寄语

    本课程的写作目的是让数据库从业人员对数据库的优化器有一个比较详尽的了解。我希望大家在学习的过程中不但能掌握优化器中的各种优化规则,更能举一反三,在工作中,结合自己学到的优化器知识轻松应对各种优化问题。最后祝大家学习愉快,轻松翻过优化器这座山峰!

    点击了解更多《PostgreSQL 优化器入门》

    导读:逻辑优化篇

    这部分课程致力于让读者达到对数据库优化器理解的第二个层次:详细了解

    愿上层楼骋远目,勿在浮沙筑高台,在开始学习第二个层次的内容之前,让我们先来复习一下第一个层次的内容。为了使对优化器分析的过程更为形象生动,接下来我们跟着小明、大明和牛二哥一起来探讨一下 PostgreSQL 查询优化器的一些基础知识。对这块内容已经了如指掌的朋友可以跳过导读,直接开始后面内容的学习。

    查询优化器的基本原理

    小明与大明

    小明考上了北清大学的计算机研究生,今年学校开了数据库原理课。小明对查询优化的内容不是很理解,虽然已经使出了洪荒之力,仍觉得部分原理有些晦涩难懂,于是打算问一下自己的哥哥大明。

    大明是一位资深的数据库内核开发老码农,对 Greenplum/HAWQ 数据库有多年的内核开发经验,眼镜片上的圈圈像年轮一样见证着大明十多年的从业经历。知道小明要来问问题,大明有点紧张,虽然自己做数据库内核好多年了,但是对优化器研究不甚深入,如果被小明这样的小菜鸟问倒就尴尬了。于是大明只好临时抱佛脚,拿出了好多年不看的《数据库系统实现》啃了起来。

    小明的问题

    小明的第一个问题:“为什么数据库要进行查询优化?”

    大明推了推鼻梁上的眼镜,慢条斯理地说:“不止是数据库要进行优化,基本上所有的编程语言在编译的时候都要优化。比如,你在编译 C 语言的时候,可以通过编译选项 -o 来指定进行哪个级别的优化,只是查询数据库的查询优化和 C 语言的优化还有些区别。”

    “有哪些区别呢?” 大明停顿了一下,凝视着小明,仿佛期望小明能给出答案,或是给小明腾挪出足够思考的空间。三、五秒之后,大明自答道:“C 语言是过程化语言,已经指定好了需要执行的每一个步骤;但 SQL 是描述性语言,只指定了 WHAT,而没有指定 HOW。这样它的优化空间就大了,你说是不是?”

    小明点了点头说:“对,也就是说条条大路通罗马,它比过程语言的选择更多,是不是这样?” 大明笑道:“孺子可教也。虽然我们知道它的优化空间大,但具体如何优化呢?”

    说着大明将身子向沙发一靠,翘上二郎腿继续说:“通常来说分成两个层面,一个是基于规则的优化,另一个是基于代价的优化。基于规则的优化也可以叫逻辑优化(或者规则优化),基于代价的优化也可以叫物理优化(或者代价优化)。”

    小明的第二个问题:“为什么要进行这样的区分呢?优化就优化嘛,何必还分什么规则和代价呢?”

    “分层不分层不是重点,有些优化器层次分得清楚些,有些优化器层次分得就不那么清楚,都只是优化手段而已。”大明感到有点心虚,再这么问下去恐怕要被问住,于是试图引开话题:“我们继续说说 SQL 语言吧,我们说它是一种介于关系演算和关系代数之间的语言,关系演算和关系代数你看过吧?”

    小明想了想,好像上课时老师说过关系代数,但没有说关系演算,于是说:“接触过一点,但不是特别明白。”大明得意地说:“关系演算是纯描述性的语言,而关系代数呢,则包含了一些基本的关系操作,SQL 主要借鉴的是关系演算,也包含了关系代数的一部分特点。”

    大明看小明有点懵,顿了一下继续说道:“上课的时候老师有没有说过关系代数的基本操作?”小明想了一下说:“好像说了,有投影、选择、连接、并集、差集这几个。”大明点点头说:“对,还有一个叫重命名的,一共 6 个基本操作。另外,结合实际应用在这些基本操作之上又扩展出了外连接、半连接、聚集操作、分组操作等。”

    大明继续说道:“SQL 语句虽然是描述性的,但是我们可以把它转化成一个关系代数表达式。而关系代数中呢,又有一些等价的规则,这样我们就能结合这些等价规则对关系代数表达式进行等价的转换。”

    小明的第三个问题:“进行等价转换的目的是找到性能更好的代数表达式吧?”

    “对,就是这样。”大明投去赞许的目光。

    那么如何确定等价变换之后的表达式就能变得比之前性能更好呢?或者说为什么要进行这样的等价变换,而不是使用原来的表达式呢?”

    大明愣了一下,仿佛没有想到小明会提出这样的问题,但是基于自己多年的忽悠经验,他定了定神,回答道:“这既有经验的成分,也有量化的考虑。例如,将选择操作下推,就能优先过滤数据,那么表达式的上层计算结点就能降低计算量,因此很容易可以知道是能降低代价的。再例如,我们通常会对相关的子查询进行提升,这是因为如果不提升这种子查询,那么它执行的时候就会产生一个嵌套循环。这种嵌套循环的执行代价是 O(N^2),这种复杂度已经是最坏的情况了,提升上来至少不会比它差,因此提升上来是有价值的。”大明心里对自己的临危不乱暗暗点了个赞。

    大明看小明没有提问,继续说道:“这些基于关系代数等价规则做等价变换的优化,就是基于规则的优化。当然数据库本身也会结合实际的经验,产生一些优化规则,比如外连接消除,因为外连接优化起来不太方便,如果能把它消除掉,我们就有了更大的优化空间,这些统统都是基于规则的优化。同时这些都是建立在逻辑操作符上的优化,这也是为什么基于规则的优化也叫做逻辑优化。”

    小明想了想,自己好像对逻辑操作符不太理解,连忙问第四个问题:“逻辑操作符是啥?既然有物理优化,难道还有物理操作符吗?”

    大明伸了个懒腰继续说:“比如说吧,你在 SQL 语句里写上了两个表要做一个左外连接,那么数据库怎么来做这个左外连接呢?”

    小明一头雾水地摇摇头,向大明投出了期待的眼神。

    大明继续说道:“数据库说‘我也不知道啊,你说的左外连接意思我懂,但我也不知道怎么实现啊?你需要告诉我实现方法啊’。因此优化器还承担了一个任务,就是告诉执行器,怎么来实现一个左外连接。”

    数据库有哪些方法来实现一个左外连接呢?它可以用嵌套循环连接、哈希连接、归并连接等。注意了,重要的事情说三遍,你看内连接、外连接是连接操作,嵌套循环连接、归并连接等也叫连接,但内连接、外连接这些就是逻辑操作符,而嵌套循环连接、归并连接这些就是物理操作符。因此,你说对了,物理优化就是建立在物理操作符上的优化。”

    大明:“从北京去上海,你说你怎么去?”

    小明:“坐高铁啊,又快又方便。”

    大明:“坐高铁先去广州再倒车到上海行不?”

    小明:“有点扎心了,这不是吃饱了撑的吗?”

    大明:“为什么?”

    小明:“很明显,我有直达的高铁,既省时间又省钱,先去广州再倒车?我脑子瓦特了?!”

    大明笑了笑说:“不知不觉之间,你的大脑就建立了一个代价模型,那就是性价比。优化器作为数据库的大脑,也需要建立代价模型,对物理操作符计算代价,然后筛选出最优的物理操作符来。因此,基于代价的优化是建立在物理操作符上的优化,所以也叫物理优化。”

    小明似乎懂了:“公司派我去上海出差就是一个逻辑操作符,它和我们写一个 SQL 语句要求数据库对两个表做左外连接类似;而去上海的实际路径有很多种,这些就像是物理操作符,我们对这些实际的物理路径计算代价之后,就可以选出来最好的路径了。”

    大明掏出手机,分别打开了两个不同的地图 App,输入了北京到上海的信息,然后拿给小明看。小明发现两个 App 给出的最优路径是不一样的。小明若有所思地说:“看来代价模型很重要,代价模型是不是准确决定了最优路径选择得是否准确?”

    大明一拍大腿,笑着说:“太对了,所以我作为一个数据库内核的资深开发人员,需要不断地调整优化器的代价模型,以期望获得一个相对稳定的代价模型,不过仍然是任重道远啊。”

    关于语法树

    听了大明对查询优化器基本原理的讲解,小明在学校的数据库原理课堂上顺风顺水,每天吃饭睡觉打豆豆,日子过得非常悠哉。不过眼看就到了数据库原理实践课,老师给出的题目是分析一个数据库的某一模块的实现。小明千挑万选,终于选定了要分析 PostgreSQL 数据库的查询优化器的实现,据说 PostgreSQL 数据库的查询优化器层(相)次(当)清(复)晰(杂),具有教科书级的示范作用。

    可是当小明下载了 PostgreSQL 数据库的源代码,顿时就懵圈了,虽然平时理论说得天花乱坠,但到了实践的时候却发现,理论和实际对应不上。小明深深陷入代码细节中不可自拔,查阅了好多资料,结果是读破书万卷,下笔如有锤,一点进展都没有。于是小明又想到了与 PostgreSQL 有着不解之缘的大明,想必他一定能站得更高,看得更远,于是小明蹬着自己的宝马向大明驶去。

    大明看着大汗淋漓找上门的小明,意味深长地说:“PostgreSQL 的查询优化器功能比较多,恐怕一次说不完,我们分成几次来说清楚吧。”

    小明说:“的确是,我在看查询优化器代码的时候觉得无从下手。虽然一些理论学过了,但不知道代码和理论如何对应,而且还有一些优化规则好像我们讲数据库原理的时候没有涉及,毕竟理论和实践之间还是有一些差距。”

    PostgreSQL 查询执行的基本流程

    大明打开电脑,调出 PostgreSQL 的代码说:“我们先来看一下 PostgreSQL 一个查询执行的基本流程。”然后调出了一张图。

    7740d080-cdf8-11e8-9819-c5f6b437e972

    “这张图是我自己画的,这种图已经成了优化器培训开篇的必备图了,我们有必要借助这张图来看一下 PostgreSQL 源码的大体结构,了解查询优化器所处的位置。”

    大明一边指点着电脑屏幕,一边继续说:“我们要执行一条 SQL 语句,首先会进行词法分析,也就是说把 SQL 语句做一个分割,分成很多小段段……”小明连忙说:“我们在学编译原理的时候老师说了,分成的小段段可以是关键字、标识符、常量、运算符和边界符,是不是分词之后就会给这些小段段赋予这些语义?”

    “对的!看来你对《编译原理》的第 1 章很熟悉嘛。”大明笑着说。

    “当然,我最擅长写 Hello World。”

    “好吧,Let’s 继续,PostgreSQL 的分词是在 scan.l 文件中完成的。它可能分得更细致一些,比如常量它就分成了 SCONST、FCONST、ICONST 等,不过基本的原理是一样的。进行分词并且给每个单词以语义之后,就可以去匹配 gram.y 里的语法规则了。gram.y 文件里定义了所有的 SQL 语言的语法规则,我们的查询经过匹配之后,最终形成了一颗语法树。”

    “语法树?我还听过什么查询树、计划树,这些树要怎么区分呢?”

    “一个查询语句在不同的阶段,生成的树是不同的,这些树的顺序应该是先生成语法树,然后得到查询树,最终得到计划树,计划树就是我们说的执行计划。”

    “那为什么要做这些转换呢?”小明不解地问。

    “我们通过词法分析、语法分析获得了语法树,但这时的语法树还和 SQL 语句有很紧密的关系。比如我们在语法树中保存的还是一个表的名字,一个列的名字,但实际上在 PostgreSQL 数据库中,有很多系统表,比如 PG_CLASS 用来将表保存成数据库的内部结构。当我们创建一个表的时候,会在 PG_CLASS、PG_ATTRIBUTE 等系统表里增加新的元数据,我们要用这些元数据的信息取代语法树中表的名字、列的名字等。”

    小明想了想,说:“这个取代的过程就是语义分析?这样就把语法树转换成了查询树,而查询树是使用元数据来描述的,所以我们在数据库内核中使用它就更方便了?”

    看着小明迷离的眼神,大明继续说:“我们可以把查询树认为是一个关系代数表达式。”

    小明定了定神,问道:“关系代数表达式?上次我问你查询优化原理的时候你是不是说基于规则的优化就是使用关系代数的等价规则对关系代数表达式进行等价的变换,所以查询优化器的工作就是用这个查询树做等价变换?”

    “恭喜你,答对了。”大明暗暗赞许小明的理解能力和记忆力,继续说:“查询树就是查询优化器的输入,经过逻辑优化和物理优化,最终产生一颗最优的计划树,而我们要做的就是看看查询优化器是如何产生这棵最优的计划树的。”

    逻辑优化示例

    午饭过后,大明惬意地抽起了中华烟,小明看着他好奇地问:“咱爷爷抽的是在农村种的烟叶,自给自足还省钱,你也干脆回农村种烟叶吧。你这中华烟和农村的自己卷的烟叶,能有什么区别?”

    大明看电视剧正看得起劲,心不在焉地说:“自己种的烟叶直接用报纸卷了抽,没有过滤嘴,会吸入有害颗粒物,而且烟叶的味道也不如现在改进的香烟。”说到这里大明好像想到了什么,继续说:“这就像是查询优化器的逻辑优化,查询树输入之后,需要进行持续的改进。无论是自己用报纸卷的烟,还是在超市买的成品烟,都是香烟,但是通过改进之后,香烟的毒害作用更低、香型更丰富了。逻辑优化也是这个道理,通过改进查询树,能够得到一个更‘好’的查询树。”

    “那逻辑优化是如何在已有的查询树上增加香型的呢?”

    大明继续说:“我总结,PostgreSQL 在逻辑优化阶段有这么几个重要的优化——子查询 & 子连接提升、表达式预处理、外连接消除、谓词下推、连接顺序交换、等价类推理。”大明又抽了一口烟,接着说:“从代码逻辑上来看,我们还可以把子查询 & 子连接提升、表达式预处理、外连接消除叫做逻辑重写优化,因为他们主要是对查询树进行改造。而后面的谓词下推、连接顺序交换、等价类推理则可以称为逻辑分解优化,他们已经把查询树蹂躏得不成样子了,已经到了看香烟不是香烟的地步了。”

    “可是我们的数据库原理课上并没有说有逻辑重写优化和逻辑分解优化啊。”

    “嗯,是的,这是我根据 PostgreSQL 源代码的特征自己总结的,不过它能比较形象地将现有的逻辑优化区分开来,这样就能更好地对逻辑优化进行提炼、总结、分析。”大明想了一下觉得如果把所有的逻辑优化规则都说完有点多,于是对小明说:“我们就从中挑选一两个详细说明吧,我们就借用关系代数表达式来说一下谓词下推和等价类推理吧。”

    小明想了想说:“选择下推和等价类是逻辑分解优化中的内容了,可是逻辑重写优化里还有子查询提升、表达式预处理、外连接消除这些大块头你还没有给我讲解过呀。”

    大明说:“这些先留给你自己去理解,如果理解不了再来找我吧。逻辑优化的规则实际上还是比较多的,但可以逐个击破,也就是他们之间通常而言并没有多大的关联,我们不打算在这上面纠缠太多时间,我相信以你自己的能力把他们搞定是没有问题的。” (注意:课程中会对这些内容做介绍。)

    “选择下推是为了尽早地过滤数据,这样就能在上层结点降低计算量,是吧?”

    “是的。”大明点了点头,“还是通过一个关系代数的示例来说明一下吧,顺便我们把等价类推理也说一说。比如说我们想要获得编号为 5 的老师承担的所有课程名字,我们可以给出它的关系代数表达式。”说着在电脑上敲了一个关系代数表达式:

    Πcname,tname (σTEACHER.tno=5∧TEACHER.tno=COURSE.tno (TEACHER×COURSE))

    “小明,你看这个关系代数表达式怎么下推选择操作?”

    小明看着关系代数表达式思考了一会,说:“我看这个 TEACHER.tno = 5 比较可疑。你看这个关系代数表达式,先做了 TEACHER×COURSE,也就是先做了卡氏积,我要是把 TEACHER.tno = 5 放到 TEACHER 上先把一些数据过滤掉,岂不是……完美!”说着小明也在电脑上敲出了把 TEACHER.tno = 5 下推之后的关系代数表达式。

    Πcname,tname (TEACHER.tno=COURSE.tno (σTEACHER.tno=5(TEACHER)×COURSE))

    大明说:“对,你这样下推下来的确能降低计算量,这应用的是关系代数表达式中的分配率 σF(A × B) == σF1(A) × σF2(B),那你看看,既然下推这么好,是不是投影也能下推?”小明看了一下,关系代数表达式中值需要对 cname 进行投影,顿时想到了,COURSE 表虽然有很多个列,但是我们只需要使用 cname 就够了嘛,于是小明在电脑上敲了投影下推的关系代数表达式。

    Πcname,tname (σTEACHER.tno=COURSE.tno (Πcname(σTEACHER.tno=5(TEACHER))×Πcname(COURSE))

    大明拍了小明的头一下说:“笨蛋,你这样下推投影,TEACHER.tno=COURSE.tno 还有办法做吗?”小明顿时领悟了,如果只在 COURSE 上对 cname 做投影是不行的,上层结点所有的表达式都需要考虑到,于是修改了表达式:

    Πcname,tname (σTEACHER.tno=COURSE.tno (Πtname,tno(σTEACHER.tno=5(TEACHER))×Πcname,tno(COURSE)))

    “这还差不多。”大明笑着说:“这是使用的投影的串接率,也是一个非常重要的关系代数等价规则,目前我们对这个表达式的优化主要是使用了选择下推和投影下推,如果用 SQL 语句来表示,就像这样。”大明在电脑的记事本上快速打出了两个 SQL 语句:

    SELECT sname FROM TEACHER t, COURSE c WHERE t.tno = 5 AND t.tno = c.tno;SELECT sname FROM (SELECT * FROM TEACHER WHERE tno = 5) tt, (SELECT cname, tno FROM COURSE) cc WHERE tt.tno = cc.tno;

    “你看这两个语句,就是谓词下推和投影下推前后的对照语句。在做卡氏积之前,先做了过滤,这样笛卡尔积的计算量会变小。”

    小明仔细观察代数表达式和这两个 SQL 语句,发现了一个问题,就是关系代数表达式中有 TEACHER.tno = 5 和 TEACHER.tno = COURSE.tno 这样两个约束条件,这是不是意味着 COURSE.tno 也应该等于 5 呢?小明试着在电脑上写了一个 SQL 语句:

    SELECT sname FROM (SELECT * FROM TEACHER WHERE tno = 5) tt, (SELECT cname, tno FROM COURSE WHERE tno=5) cc WHERE tt.tno = cc.tno;

    然后小明说:“你看,由于有 TEACHER.tno = 5 和 TEACHER.tno = COURSE.tno 两个约束条件,我们是不是可以推理出一个 COURSE.tno = 5 的新约束条件来呢?这样还可以把这个条件下推到 COURSE 表上,也能降低笛卡尔积的计算量。”

    大明说:“是的,这就是等价推理。PostgreSQL 在查询优化的过程中,会将约束条件中等价的部分都记录到等价类中,这样就能根据等价类生成新的约束条件。比如示例的语句中就会产生一个等价类 {TEACHER.tno, COURSE.tno, 5},这是一个含有常量的等价类,是查询优化器比较喜欢的等价类,这种等价类可以得到列属性和常量组合成的约束条件,通常都是能下推的。”

    小明心里很高兴,自己通过仔细观察,得到了等价类的优化,感觉有了学习的动力,心里美滋滋的,然后问大明:“那上面的 SQL 语句还有什么可优化的吗?”

    大明观察了一下这个语句说:“我们已经在 TEACHER 表上进行了 TEACHER.tno = 5 的过滤,在 COURSE 表上也做了 COURSE.tno = 5 的过滤,这就说明在做笛卡尔积时,实际上已确定了 TEACHER.tno = COURSE.tno = 5。也就是说 TEACHER.tno = COURSE.tno 这个约束条件已经隐含成立了,也就没什么用了,我们可以把它去掉,最终形成一个这样的 SQL 语句。”大明敲下了最终的语句:

    SELECT sname FROM (SELECT * FROM TEACHER WHERE tno = 5) tt, (SELECT cname, tno FROM COURSE WHERE tno=5) cc;

    同时也敲出了这个语句对应的关系代数表达式:

    Πcname,tname (Πtname, tno(σTEACHER.tno=5(TEACHER))× Πcname, tno(σCOURSE.tno=5(COURSE)))

    大明说:“经过选择下推、投影下推和等价类推理,我们对这个 SQL 语句或者说关系代数表达式进行了优化,最终降低了计算量。”

    小明感觉对谓词下推已经理解了:“看上去也不复杂嘛,我发现了可以下推的选择我就下推,完全没有问题啊。”大明笑着说:“甚矣,我从未见过如此厚颜无耻之人。我们现在看的这个例子,只不过是最简单的一种情况啊,你就这样大言不惭,你的人生字典里还有羞耻二字吗?”

    小明愤愤地说:“我的人生没有字典……”

    大明问道:“我们这个例子有一个问题,它是内连接,因此我们可以肆意妄为地将选择下推下来,可以没羞没臊地做等价类推理。但如果是外连接,还能这么做吗?”

    小明顿时陷入了苦苦的沉思。

    点击了解更多《PostgreSQL 优化器入门》

    导读:物理优化篇

    通过大明和小明的对话,相信读者朋友对逻辑优化的部分已经有了一个简单的了解,逻辑优化也叫基于规则的优化,这种优化方式比较呆板、不够灵活,就是按照定义好的规则硬性地进行等价变换,于是就催生了新的优化方法——物理优化,物理优化又叫基于代价的优化,今天我们再次跟着小明、大明和牛二哥一起来探讨一下 PostgreSQL 优化器是怎么计算路径代价的,又是怎么筛选路径的。对这些内容已经了如指掌的朋友可以跳过这个导读,直接开始后面的内容学习。

    统计信息和选择率

    “咚咚咚……”门外传来了敲门声,大明打开门一看,原来是同事牛二哥,牛二哥是专门从事数据库查询优化开发的码农,也有十几年从业经验了。大明感到非常 Happy,因为这两天给小明讲查询优化器讲得有些吃力,今天牛二哥来了正好可以帮上忙:“牛二同志,我弟弟小明最近学校要做数据库原理实践,总来问我优化器的问题,可我对优化器也是一知半解,这下你来了可以帮帮忙不?”

    牛二哥痛快地说:“这难不倒我,随时都可以讲。”

    小明对牛二哥早有耳闻,接到大明电话后速速赶到,见面不久便吐起了苦水:“我最近正在查看基于代价的优化,感觉付出了很多代价,但收获甚微,期望今天能得到牛二哥的指导。”

    牛二哥说:“说到代价,我觉得有个东西是绕不过去的,就是统计信息和选择率。PostgreSQL 的物理优化需要计算各种物理路径的代价,而代价估算的过程严重依赖于数据库的统计信息。统计信息是否能准确地描述表中的数据分布情况是决定代价准确性的重要条件之一。”

    小明说:“大明和我说过,数据库有很多物理路径,这些物理路径也叫物理算子。和逻辑算子不同,物理算子是查询执行器的执行方法,我们只需要计算物理算子每个步骤的代价,汇总起来就是路径的代价了,那要统计信息有什么用呢?”

    牛二哥说:“是的,我们就是要计算一个物理算子的代价,但是物理算子的计算量并不是一成不变的。”说着他从旁边的书桌上拿来纸和笔,写了两个 SQL 语句。

    SELECT A+B FROM TEST_A WHERE A > 1;SELECT A+B FROM TEST_A WHERE A > 100000000;

    然后说:“你看,这两个语句可以用同样的物理算子来完成,但是它们的计算量一样吗?”

    小明心想:A > 1 和 A > 1000000000 都是过滤条件,经过过滤之后,它们产生的数据量就不同了,这样投影中的 A + B 的计算次数就不同了,所以它们的代价应该是不同的,那它和统计信息有什么关系呢?小明灵光一闪,马上说:“我知道了,我在计算物理算子的代价的时候,要知道 A > 1 之后还剩下多少数据或者 A > 1000000000 之后还剩下多少数据,如果我们提前对表上的数据内容做了统计,剩下多少数据就不难计算了,所以必须要有统计信息。”

    牛二哥点了点头说:“嗯,通过统计信息,代价估算系统就可以了解一个表有多少行数据、用了多少个数据页面、某个值出现的频率等,然后就能根据这些信息计算出一个约束条件能过滤掉多少数据,这种约束条件过滤出的数据占总数据量的比例称为选择率。”

    统计信息是什么形式

    小明追问道:“那么统计信息是什么形式的呢?”

    牛二哥挠挠头说:“这个还真是有点麻烦,我们说常用的统计信息的形式就是 distinct 率、NULL 值率、高频值、直方图、相关系数这些,它们分别有不同的作用。比如说 distinct 率,你可以获知某一列有多少个独立值,这种信息对于像性别这种列就显得特别有用。NULL 值率呢,在统计的过程中,NULL 值是不好处理的,因此把它独立出来,形成 NULL 值率,这样在高频值、直方图这些形式中就不用考虑 NULL 值的情况了。高频值属于奇异值,顾名思义,就是出现得比较多的一些列值。去掉了 NULL 值,再去掉高频值,剩下的值可以用来做一个等频的直方图。”

    大明看小明有点跟不上,过来说:“统计信息嘛,主要的还是高频值、直方图和相关系数,实际上我建议还是不要纠结于统计信息有哪些形式,只要知道它是用来算代价的就可以了。”

    牛二哥对大明说:“这怎么可以,我还没有说统计信息是如何生成的呢!比如它通过了两阶段采样,然后对样本进行统计时使用的统计方法,哪些值可以作为高频值,直方图有几个桶,相关系数是怎么计算的,相关系数在计算索引扫描路径代价的时候怎么用的……而且我和你说,PostgreSQL 还出了基于多列的扩展统计信息,多列统计信息分成了哪些类型,分别是什么含义,各自是怎么计算的,还有选择率是怎么结合统计信息计算的,这些我还没说呢……”

    大明忍不住说:“像你这样讲优化器,岂不是要出一本书了?”

    牛二哥做痛苦状:“那好吧,统计信息我们就说到这里,但是它确实是代价计算的基石。小明同学,你理解了它的作用就可以了。”

    大明继续神秘地说:“实际上统计信息往往也不准,你想想本来就是采样的结果嘛,样本是否显著压根就不好说,而且随着应用程序对表的更新,统计信息可能更新不及时,那就更会出现偏差。更严重的是,如果我们遇到 a > b 这样的约束条件,使用统计信息计算选择率也很不好计算,即使算出来,也可能不准。”

    牛二哥说:“是的,统计信息确实也有不准确的问题。我听说有个数据库用户,他家后院出了一口泉水,他爸爸觉得是吉兆,去找风水大师看。风水大师掐指一算说:你儿子每次遇到数据库性能慢就知道更新统计信息,可是统计信息太水了,都从你家后院冒出来了。”

    三个人顿时笑做一团。

    关于物理路径

    玩笑过后,小明说:“不如给我说说物理路径吧,代价算来算去,最终还是为了物理路径计算代价嘛。大明和我说过它大体分成扫描路径和连接路径,我查过一些说明,知道扫描路径有顺序扫描路径、索引扫描路径、位图扫描路径等;而连接路径通常有嵌套循环连接路径、哈希连接路径、归并连接路径,另外还有一些其他的路径,比如排序路径、物化路径等。”

    牛二哥说:“是的,我们就来说说这些路径的含义吧。如果要获得一个表中的数据,最基础的方法就是将表中的所有的数据都遍历一遍,从中挑选出符合条件的数据,这种方式就是顺序扫描路径。顺序扫描路径的优点是具有广泛的适用性,各种表都可以用这种方法,缺点自然是代价通常比较高,因为要把所有的数据都遍历一遍。”大明同时在纸上画了个图,说:“这个图大概就是顺序扫描路径。”

    c34383b0-cdd5-11e8-8458-03f9794b87bd

    牛二哥则继续说:“将数据做一些预处理,比如建立一个索引,如果要想获得一个表的数据,可以通过扫描索引获得所需数据的‘地址’,然后通过地址将需要的数据获取出来。尤其是在选择操作带有约束条件的情况下,在索引和约束条件共同的作用下,表中有些数据就不用再遍历了,因为通过索引就很容易知道这些数据是不符合约束条件的。更有甚者,因为索引上也保存了数据,它的数据和关系中的数据是一致的,因此如果索引上的数据就能满足要求,只需要扫描索引就可以获得所需数据了。也就是说在扫描路径中还可以有索引扫描路径和快速索引扫描路径两种方式。”

    大明则继续为牛二哥“捧哏”,在纸上画出了索引扫描和快速索引扫描的图。

    ea0367e0-cdd5-11e8-8458-03f9794b87bd

    索引扫描随机读的问题

    小明看到图上写了“随机读”三个字,问道:“我看这个索引扫描有随机读的问题,这个问题能否解决掉呢?也就是说既利用了索引,还避免了随机读的问题,有这样的办法吗?”

    牛二哥说:“索引扫描路径确实带来随机读的问题,原因是索引中记录的是数据元组的地址,索引扫描是通过扫描索引获得元组地址,然后通过元组地址访问数据,索引中保存的“有序”的地址,到数据中就可能是随机的了。位图扫描就能解决这个问题,它通过位图将地址保存起来,把地址收集起来之后,然后让地址变得有序,这样就通过中间的位图把随机读消解掉了。”大明则继续在纸上画出了位图扫描的示意图。

    164d72f0-cdd6-11e8-8458-03f9794b87bd

    大明补充说道:“扫描过程中还会结合一些特殊的情况,有一些非常高效的扫描路径,比如 TID 扫描路径。TID 实际上是元组在磁盘上的存储地址,我们能够根据 TID 直接就获得元组,这样查询效率就非常高了。”

    牛二哥点了点头继续说:“扫描路径通常是执行计划中的叶子结点,也就是在最底层对表进行扫描的结点。扫描路径就是为连接路径做准备的,扫描出来的数据就可以给连接路径来实现连接操作了。”

    大明一边在纸上画一边说:“要对两个关系做连接,受笛卡尔积的启发,可以用一个算法复杂度是 O(mn) 的方法来实现,我们叫它嵌套循环连接(Nested Loop Join) 方法。这种方法虽然复杂度比较高,但是和顺序扫描一样,胜在具有普适性。”

    牛二哥说:“嵌套循环连接这种方法的复杂度比较高,看上去没什么意义,但是如果嵌套循环连接的内表的路径是一个索引扫描路径,那么算法的复杂度就会降下来。索引扫描的算法复杂度是 O(logn),因此如果嵌套循环连接的内表是一个索引扫描,它整体的算法复杂度就变成了 O(mlogn),看上去这样也是可以接受的。”

    350c3cd0-cdd6-11e8-8458-03f9794b87bd

    哈希连接

    小明点了点头说:“嗯,索引实际上是对数据做了一些预处理,我想如果哈希连接(Hash Join)方法就是将内表做一个哈希表,这样也等于将内表的数据做了预处理,也能方便外表的元组在里面探测吧?”

    牛二哥点了点头说:“假设哈希表有 N 个桶,内表数据均匀地分布在各个桶中,那么哈希连接的时间复杂度就是 O(m * n /N),当然,这里我们没有考虑上建立哈希表的代价。”

    大明则在纸上画出了哈希连接的示意图,并补充道:“哈希连接通常只能用来做等值判断。”

    584cbe40-cdd6-11e8-8458-03f9794b87bd

    归并连接

    牛二哥继续说:“如果将两个表先排序,那么就可以引入第三种连接方式:归并连接(Merge Join)。这种连接方式的代价主要浪费在排序上。如果两个关系的数据量都比较小,那么排序的代价是可控的,归并连接就是适用的。另外如果关系上有有序的索引,那就可以不用单独排序了,这样也比较适用归并连接。你看我画的这个归并连接的示意图,外表是需要排序的,而内表则借用了原有的索引的顺序,消除了排序的时间,降低了物理路径的代价。”

    ed080d70-d027-11e8-a802-f373f137079f

    “这些路径属于 SPJ 路径,在 PostgreSQL 的优化器中,通常会先生成 SPJ 的路径,然后在这基础上再叠加 Non-SPJ 的路径,比如说聚集操作、排序操作、limit 操作、分组操作……”牛二哥继续补充道。

    关于代价的计算

    小明说:“可是算来算去,物理路径的代价还是有选不准的时候啊。”

    牛二哥说:“最优路径选得不准是谁的原因?那就是代价模型不行啊。代价模型不行赖谁?那就是程序员没建好啊,所以要怪就怪到程序员自己头上。”

    小明问道:“可是我看 PostgreSQL 的代价计算已经很复杂了啊。”

    “但数据库的周边环境更复杂啊。你想想,在实际应用中,数据库用户的配置硬件环境千差万别,CPU 的频率、主存的大小和磁盘介质的性质都会影响执行计划在实际执行时的效率。”牛二哥说。

    大明接过来继续说道:“虽然在代价估算的过程中,我们无法获得‘绝对真实’的代价,但是‘绝对真实’的代价也是不必要的。因为我们只是想从多个路径(Path)中找到一个代价最小的路径,只要这些路径的代价是可以‘相互比较’的就可以了。因此可以设定一个‘相对’的代价的单位 1,同一个查询中所有的物理路径都基于这个‘相对’的单位 1 来计算的代价,这样计算出来的代价就是可以比较的,也就能用来对路径进行挑选了。”

    牛二哥接着说:“PostgreSQL 采用顺序读写一个页面的 IO 代价作为单位 1,而把随机 IO 定为了顺序 IO 的 4 倍。”

    小明说:“我知道,这个我查过相关的书。首先,目前的存储介质很大部分仍然是机械硬盘,机械硬盘的磁头在获得数据库的时候需要付出寻道时间。如果要读写的是一串在磁盘上连续的数据,就可以节省寻道时间,提高 IO 性能。而如果随机读写磁盘上任意扇区的数据,那么会有大量的时间浪费在寻道上。其次,大部分磁盘本身带有缓存,这就形成了主存→磁盘缓存→磁盘的三级结构。在将磁盘的内容加载到内存的时候,考虑到磁盘的 IO 性能,磁盘会进行数据的预读,把预读到的数据保存在磁盘的缓存中。也就是说如果用户只打算从磁盘读取 100 个字节的数据,那么磁盘可能会连续地读取磁盘中的 512 字节(不同的磁盘预读的数量可能不同)并将其保存到磁盘缓存。如果下一次是顺序读取 100 个字节之后的内容,那么预读的 512 字节的数据就会发挥作用,性能会大大增加。而如果读取的内容超出了 512 字节的范围,那么预读的数据就没有发挥作用,磁盘的 IO 性能就会下降。”说完小明得意地说:“怎么样,我说得对吧?”

    牛二哥说:“你说得对,目前 PostgreSQL 的查询优化大量考虑了随机 IO 和顺序 IO 所带来的性能差别,在这方面做了不少优化。但是现在的磁盘技术越来越发达了,以后随机 IO 和顺序 IO 是不是还差这么多,就值得商榷了。”

    代价基准单位

    “那到底还有哪些代价基准单位呢?”小明继续问道。

    大明回答:“基于磁盘 IO 的代价单位当然就是和 Page 有关的了,也就是说我们刚才说的顺序 IO 和随机 IO 都属于 IO 方面的基准代价。让牛二哥给你介绍一下 CPU 方面的代价基准单位吧。”

    牛二哥说:“CPU 方面的基准单位有哪些呢?比如说我们通过 IO 把磁盘页面读到了缓存,但我们要处理的是元组啊,所以还需要把元组从页面里解出来,还要处理元组,这部分主要消耗的是 CPU,所以会有一个元组处理的代价基准单位。另外,我们在投影、约束条件里有大量的表达式,这些表达式求解也主要消耗 CPU 资源,所以还有一个表达式代价的基准单位。”

    牛二哥继续说道:“现在 PostgreSQL 增加了很多并行路径,因此它也产生了通信代价,这个也需要计算的。”

    小明听后说:“那我们就能得到一个这样的公式。”说着在纸上写了一个公式:

    总代价 = CPU 代价 + IO 代价 + 通信代价

    牛二哥笑道:“总结得不错,这样就可以计算每种物理路径的代价,就可以对路径进行筛选了,最后挑选出来的路径就是最优路径。”

    关于最优路径

    小明、大明和牛二哥在外卖 App 里搜索附近的饭店,大明突然感叹道:“看,这就是蓝海,我们可以创业搞一个 AI 点评,只推荐最优的饭店,我准确地找到了吃货们的痛点,这里面隐含着很大的商机啊!”

    牛二哥瞥了他一眼说:“AI 推荐当然好,可是要推荐得准才行啊。一个人一个口味,你这个需求太‘智能’了,我估计不好弄。”

    小明突然说:“我最近在算法课上学过一些最优解问题的解决方法,应该能用得上。”

    牛二哥叹口气说:“可是这些方法用到优化器里都不一定够用,何况用到一个更加智能的项目上呢?”

    “嗯?优化器里也用到最优解问题的方法了吗?我们学过动态规划、贪心算法……”小明如数家珍地说起来。

    大明说:“用到了啊, 虽然物理路径看上去也不多,但实际上枚举起来,它的搜索空间也不小。例如,在扫描路径中,我们就可以有顺序扫描、索引扫描和位图扫描。假如一个表上有多个索引,就可能产生多个不同的索引扫描,那么哪个索引扫描路径好呢?还有,索引扫描和顺序扫描、位图扫描相比,哪个好呢?”

    数据库路径的搜索方法

    大明看着小明迷离的眼神后继续说:“数据库路径的搜索方法通常有 3 种类型:自底向上方法、自顶向下方法、随机方法,而 PostgreSQL 采用了其中的两种方法。”

    “采用了哪两种方法?”牛二哥明知故问。

    “采用了自底向上和随机方法,其中自底向上的方法是采用动态规划方法,而随机方法采用的是遗传算法。”

    “那有谁使用了自顶向下的方法呢?”牛二哥继续“捧哏”道。

    “嗯……这个嘛,Pivotal 公司的开源优化器 ORCA 用的就是自顶向下的方法。可以让牛二哥先给你说说怎样用动态规划方法搜索最优物理路径。”

    最优物理路径

    牛二哥拿出纸来画了几个圈,然后说:“这代表四个表,自底向上嘛,所以是从底下向上堆积,这是最底层,我们叫它第一层。”

    bc717e10-cdd6-11e8-8458-03f9794b87bd

    “动态规划方法首先考虑两个表的连接,其中优先考虑有连接关系的表进行连接。两个表的连接可以建立一个新的表,我们把这些新表叫做第二层。”牛二哥通过连线,产生了一些新的“表”。

    f77426c0-cdd6-11e8-8458-03f9794b87bd

    “第二层的表和第一层的表再连接,可以生成基于三个表连接的新的‘表’,这样就又向前推进了一层,产生了第三层。”

    1e911f10-cdd7-11e8-8458-03f9794b87bd

    “然后再用第三层的表和第一层的表进行连接,最终生成整个问题的最优路径。”

    385000b0-cdd7-11e8-8458-03f9794b87bd

    “可是,这不就是穷举吗?”小明问道。

    牛二哥解释说:“动态规划有两个特点,一个是要重复地利用子问题的解,这样能减少计算量,降低复杂度;另外一点就是通过子问题的最优解能够构造出最终的最优解,也就是说需要具有最优子结构的性质,所以动态规划的复杂度和穷举是不一样的。”

    大明继续解释说:“还有,虽然你看图里的连线比较多,但在实际情况里,并不是所有的圈圈之间都能产生连线,连接关系也有个合法性的问题嘛,所以复杂度是可以控制住的。”

    小明感觉好像明白了一点,然后赶紧追问:“那遗传算法呢?”

    大明突然意识到了什么,说:“哎哎哎,我们不是在搜索饭店吗,怎么就说起最优路径了?先点餐吧,再晚饭都没得吃了。”

    于是三个人又热火朝天地搜起饭店来……

    小结

    随着小明、大明和牛二哥的对话结束,我们也要开始进入本次课程的主要内容了......欢迎各位走进 PostgreSQL 优化器的大门,这次我们就没完了。

    点击了解更多《PostgreSQL 优化器入门》

    第01课:SQL 语句的历程
    第02课:解读执行计划
    第03课:调整执行计划
    第04课:谓词下推
    第05课:连接顺序交换规则
    第06课:子连接提升
    第07课:子查询提升
    第08课:消除外连接
    第09课:等价推理
    第10课:表达式的规范化
    第11课:逻辑优化汇总
    第12课:统计信息
    第13课:表达式提取
    第14课:选择率
    第15课:参数化路径
    第16课:选择最优执行计划
    第17课:扫描代价计算
    第18课:连接代价和 Non-SPJ 代价
    第19课:扫描计划路径的执行
    第20课:连接路径的执行说明
    第21课:聚集与分组的执行说明
    第22课:Greenplum 的执行计划

    阅读全文: http://gitbook.cn/gitchat/column/5bbee9ed2409541174645a2d

    展开全文
  • SpringBoot性能优化方案

    2019-07-05 17:44:20
    该框架使用了特定的方式来进行配置,从而使开发人员不再需要定义样板化的配置。通过这种方式,Spring Boot致力于在蓬勃发展的快速应用开发领域(rapid application development)成为领导者。 第一节-SpringBoot...
  • 服务名称 ...对使用下一代技术编写的程序的运行提供优化。该服务的默认运行方式是手动,不建议更改。 \\WINDOWS\Microsoft.NET\Framework\v2.0.50215\mscorsvw.exe 估计使用该技术之后,运行.Net技术编写
  • 优化】--Squid优化汇总

    千次阅读 2013-06-08 14:15:46
    优化针对实际业务,并不针对硬件环境或者简单的配置文件! 优化的目标是增强性能、增强安全性、增强鲁棒性、充分利用硬件资源、降低成本。 优化目的是在提高业务处理能力的基础上充分发挥硬件的性能,两者...
  • 网站优化 14条--雅虎十四条优化原则

    千次阅读 2016-02-23 11:23:51
    雅虎十四条优化原则
  • Nginx反向代理+Nginx性能优化配置详解

    万次阅读 2018-01-29 17:13:05
    配置nginx作为反向代理和负载均衡,同时利用其缓存功能,将静态页面放在nginx上缓存,以达到降低后端服务器连接数的目的并检查后端web服务器的健康状况。 1、安装nginx 环境: OS:centos7.2 nginx...
  • 【性能优化】Hive优化

    千次阅读 2016-06-06 16:56:50
     继续《那些年使用Hive踩过的坑》一文中的剩余部分,本篇博客赘述了在工作中总结Hive的常用优化手段和在工作中使用Hive出现的问题。下面开始本篇文章的优化介绍。 2.介绍  首先,我们来看看Hadoop的计算框架特性...
  • seo简介及优化

    千次阅读 2012-08-03 17:49:05
    seoSEO(Search Engine Optimization),汉译为搜索引擎优化,是较为流行的网络营销方式,主要目的是增加特定关键字的曝光率以增加网站的能见度,进而增加销售的机会。分为站外SEO和站内SEO两种。SEO的主要工作是...
  • squid 优化指南

    万次阅读 2011-07-01 10:38:00
    很多squid 优化只限于在 squid参数和系统参数上面的调整。但是这个实在只是细枝末节的事情,只要不是太弱智的配置导致无法缓存,squid的性能不会有太大差距,也就提高10%左右,只有实际的业务针对squid 进行一些调整...
  • 随着Android应用增多,功能越来越复杂,布局也越来越丰富了,而这些也成为了阻碍一个应用流畅运行,因此,对复杂的功能进行性能优化是创造高质量应用的基础,本章节将为大家展示几种性能优化的方法,帮助开发者快速...
  • MySQL 性能优化实战

    千次阅读 2018-07-03 09:05:17
    MySQL 知识点及性能优化的知识,其实性能优化的目标是针对后端研发人员及一些资深的前端人员,可能会从如下大的知识点讲解。一、安装说明首先学习数据库,当然是安装软件。千里之行,始于足下,如果...
  • Apache网页优化

    千次阅读 2021-01-07 14:40:24
    目录一、Apache网页优化1、概述2、优化内容二、gizp介绍1、概述2、作用三、Apache的压缩模块1、 一、Apache网页优化 1、概述 在企业中,部署Apache后只采用默认的配置参数,会引发网站很多问题,换言之默认配置是...
  • 携程移动端优化

    千次阅读 2017-01-03 11:35:22
    携程移动端优化
  • 前端性能优化方法总结

    万次阅读 多人点赞 2017-03-20 10:25:37
    那么,前端优化目的是什么 ?  1. 从用户角度而言,优化能够让页面加载得更快、对用户的操作响应得更及时,能够给用户提供更为友好的体验。  2. 从服务商角度而言,优化能够减少页面请求数、或者减小
  • 由于不能依靠重新装修和部署解决问题,只能通过补救和优化配置的方式进行解决。设备多,只是布局的不合理,所以只能尽量的将增补的AP设置为功率最大,让其尽可能多的分担负载重的...
  • Redis Cluster架构优化

    万次阅读 热门讨论 2015-09-25 15:01:09
    Redis Cluster架构优化在《全面剖析Redis Cluster原理和应用》中,我们已经详细剖析了现阶段Redis Cluster的缺点: 无中心化架构 Gossip消息的开销 不停机升级困难 无法根据统计区分冷热数据 客户端的挑战 Cluster...
  • Spark 优化

    千次阅读 2016-11-13 15:45:12
    这种设置方式该序列化器,不仅在Shuffle阶段,在RDD保存到磁盘时也会被使用,使得Kryo不能成为默认配置的唯一原因就是编程人员需要对序列化类进行注册。当开发网络需求密集的程序时,本文推荐尝试使用Kryo序列化方式...
  • 从 View 绘制谈性能优化

    千次阅读 2017-02-26 16:02:49
    转载请注明出处:http://blog.csdn.net/wl9739/article/details/57416433在开发过程中,往往会听到 “性能优化” 这个概念,这个概念很大,比如网络性能优化、耗电量优化等等,对我们开发者而言,最容易做的,或者是...
  • app性能优化

    千次阅读 2017-08-14 22:06:34
    一般app的优化我们可以从启动、布局、内存、存储、耗电等进行优化。 1、启动优化: *应用的启动分为冷启动和热启动。 冷启动:应用第一次启动的时候,系统会为应用创建一个新的进程,所以首先会创建和初
  • Android性能优化

    千次阅读 2016-08-10 09:40:26
    讲到Android开发,就不得不谈一下Android的优化,不管是平时开发中我们需要注意的一些Android对Java的一些类的优化,还是实际开发中对性能的优化,其实早在15年的google全球大会上google就Android的性能优化就给我们...
  • 16、nginx 错误页面配置 nginx错误页面包括404 403 500 502 503 504等页面,只需要在server中增加以下配置即可: error_page 404 403 500 502 503 504 /404.html; location = /404.html { root /usr/local/nginx/...
  • SQL优化笔记—CPU优化

    千次阅读 2011-12-26 13:56:17
    补充:常规服务器动态管理对象包括,下面有些资料可能会应用到 dm_db_*:数据库和...优化性能的常用方法是检索速度最慢的查询构成您 SQL Server 实例上的正常、 每日工作负载的一部分,然后调整它们,一个接一个的
  • 电机优化程序使用说明

    千次阅读 2017-02-08 09:32:43
    电机优化程序使用说明电机优化程序使用说明 引言 1编写目的 2项目背景 3 定义 软件概述 1目标 2功能 3 性能 运行环境 1硬件 硬件最小配置 硬件推荐配置 2支持软件 使用说明 1安装和初始化 第一步解压缩软件包到一个...
  • MySQL数据库访问性能优化

    万次阅读 多人点赞 2018-03-01 09:07:50
    MYSQL应该是最流行的WEB后端数据库。... MYSQL如此方便和稳定,以至于我们在开发 WEB 程序...即使想到优化也是程序级别的,比方不要写过于消耗资源的SQL语句。可是除此之外,在整个系统上仍然有非常多能够优化的地方。...
  • 优化新常态> 前半生

    千次阅读 2017-08-24 15:21:39
    ORACLE 优化 新常态
  • Tomcat 启动速度优化

    千次阅读 2017-04-27 20:36:19
    目的是为了精简web应用结构, 并降低插件框架的复杂度。 杯具的是, 这些特性会扫描所有的JAR包和类文件,这会耗费较长的时间。按照规范, Servlet容器在启动时, 默认会执行扫描; 但用户也可以通过配置文件禁用此特性...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 86,650
精华内容 34,660
关键字:

优化人员配置的目的