惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

F
Fortinet All Blogs
宝玉的分享
宝玉的分享
酷 壳 – CoolShell
酷 壳 – CoolShell
T
The Exploit Database - CXSecurity.com
Help Net Security
Help Net Security
腾讯CDC
Project Zero
Project Zero
C
CXSECURITY Database RSS Feed - CXSecurity.com
IT之家
IT之家
C
Cyber Attacks, Cyber Crime and Cyber Security
T
Tailwind CSS Blog
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
D
Darknet – Hacking Tools, Hacker News & Cyber Security
L
LINUX DO - 最新话题
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
T
Threatpost
N
News | PayPal Newsroom
C
Cybersecurity and Infrastructure Security Agency CISA
Hacker News - Newest:
Hacker News - Newest: "LLM"
S
SegmentFault 最新的问题
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
P
Proofpoint News Feed
A
Arctic Wolf
B
Blog RSS Feed
Forbes - Security
Forbes - Security
P
Privacy & Cybersecurity Law Blog
Attack and Defense Labs
Attack and Defense Labs
V2EX - 技术
V2EX - 技术
P
Proofpoint News Feed
I
Intezer
Application and Cybersecurity Blog
Application and Cybersecurity Blog
阮一峰的网络日志
阮一峰的网络日志
aimingoo的专栏
aimingoo的专栏
T
Tenable Blog
MyScale Blog
MyScale Blog
U
Unit 42
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
WordPress大学
WordPress大学
W
WeLiveSecurity
D
DataBreaches.Net
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
G
GRAHAM CLULEY
有赞技术团队
有赞技术团队
Martin Fowler
Martin Fowler
罗磊的独立博客
The Last Watchdog
The Last Watchdog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
V
Vulnerabilities – Threatpost
美团技术团队
Microsoft Security Blog
Microsoft Security Blog

卡瓦邦噶!

服务器高性能网络调优 | 卡瓦邦噶! 为何写作 | 卡瓦邦噶! 读《金阁寺》 | 卡瓦邦噶! 雨季又来 | 卡瓦邦噶! MTU Probe 引起的初始延迟 | 卡瓦邦噶! 3.5 秒的固定延迟问题 | 卡瓦邦噶! 学习网络的一点经验 | 卡瓦邦噶! ARP 问题诊断 | 卡瓦邦噶! 网络断断续续…… | 卡瓦邦噶! Piccolo P2P 镜像分发 | 卡瓦邦噶! 一起看电影 | 卡瓦邦噶! 《征服C指针》 | 卡瓦邦噶! 我的姥姥 | 卡瓦邦噶! Python的哲学 Python 3.5的新特性 学校不教的计算机课 垃圾回收(GC)的三种基本方式 在编程中体验纯粹的快乐 从《美丽新世界》谈自由 在快钱实习 迷人的嗓音和迷人的故事——《Sleepyhead》 Python 的十个自然语言处理工具 记一个愚蠢的bug 一年炉石传说的游戏体验 《以撒的结合:重生》网页版图鉴 分清 C++的指针、引用和数组 笑话三则 自由比皇帝更伟大——《悲惨世界》笔记 Git 10 周年访谈:Linus 讲述背后故事 用 0x3f3f3f3f 设定最大int值的优点 Joel给计算机系学生的建议 一个词法分析器的简单实现 怎样才算健康的生活方式 MacVim 配置攻略 学习培训课程的视频效果好吗? 语言的控制 可悲的大多数 CSS样式思维导图 2014年终总结 奥巴马成为首位写程序的美国总统 青岛老城区的下水道好在哪里? 做优秀 UI 的七个建议(第二部分) 选择爱情的骑士 Git简明教程 Java集合总览 读 《1984》 java问答:终极父类(六)——等待/唤醒和接口 Java 问答:终极父类(五)——toString() Hyperlapse快速视频背后的技术细节 平庸之恶 Java程序员须知的七个日志管理工具 苏州 逛书摊随想 关于考试作弊 你的工作不仅仅是编程 推荐在线学习Java的英文资源 关注女性命运——《千禧年三部曲》 用好你的幻灯片——《演说之禅》 使用ReentrantLock和Lambda表达式让同步更纯净 新手学编程,从哪里开始? 程序员都是工程师吗? 不要学习代码,要学会思考 Java 问答:终极父类(四)——hashCode() Java 问答:终极父类(三)——finalize()和 getClass() 程序员职业之路的选择 我是一名摄影家 写给何小树的城市指南 为什么一些语言会比别的快? Java的常见误区与细节 跟朋友在一起玩游戏 死神永生——读《三体》 五种类型的程序员 创业圣经——读《黑客与画家》 纪念加西亚·马尔克斯 Junit中处理异常的另一种方式:catch-exception Java8采用Martin Fowler的方法创建内部DSL Linux HotSopt虚拟机GC线程的CPU占用率 J2EE概念介绍 如何成为一名黑客 Java 问答:终极父类(二)——equals()方法 为什么我喜欢Java Java 问答:终极父类(一)——clone()方法 七个改变世界的Java项目 java中默认类型转换的小问题 传统与创新 欢迎来到互联网 莫言和马尔克斯——读《生死疲劳》 位运算的妙用 读《人为什么活着》 写博客教会我的事情 中国特色操作系统 2013年总结 《永不妥协》影评 恨不相逢未嫁时——《廊桥遗梦》影评 如何优雅地使用PPT 给明年依然年轻的我们 天才与柱子 黑客守则和黑客精神 Looking for Freedom——《被解救的姜戈》 简洁之道
Use the Index, Luke! 笔记3:避免回表
laixintao · 2020-03-22 · via 卡瓦邦噶!

你一定听说过一种加速查询的技术,使得只使用索引就可以得到查询的内容,而“避免回表”,至少应该在别人的面试经验中看过,或者自己面试中被问到过吧。这篇文章是 use the index luke! 中 clusting data 这一章节的笔记,解释了这样做的原理,实践。

Data Cluster

Data Cluster 的意思是将连续的,相关的数据尽量存储在一起,这样查找的时候 IO 的成本更低,因为顺序读比随机读速度快,磁头移动少。我们的索引也有这样的功能,帮我们安排数据的顺序,让读的速度更快。

我们通过索引可以达到这一目的。之前,我们介绍过通过执行计划的 filter 可以推测出哪些索引创建的不合理,因为 filter 是一个遍历操作,速度很慢,access 的速度很快。其实,filter 的信息信息很有用,还能帮助我们将数据聚集,来加速查询。

索引的第一大能力是 B-Tree 比那里,第二大能力就是数据聚集。

比如下面这个查询:

MySQL

1

2

3

4

SELECT first_name, last_name, subsidiary_id, phone_number

  FROM employees

WHERE subsidiary_id = ?

   AND UPPER(last_name) LIKE '%INA%'

因为 LIKE 条件是以 % 开头的,所以无论是对 last_name 建立索引还是对 UPPER(last_name) 建立索引,查询都不会用到。

所以,这个查询的瓶颈在于,按照 subsidiary_id 过滤之后,找到符合 LIKE 条件的 row。这一步的流程是这样:取出一个 row,判断它的属性是否满足条件,然后再去取出下一个 row,再判断。对于这样的操作,如果 row 分散在不同的 block 上的话,一个一个取出来读的话,性能是很差的。如果这些 row 能尽量在同一个 block 上,或者相邻的 block 上,读起来就会快很多了。

有一种方式是将 row 按照索引查找的顺序排序,这样就可以将 filter 的这个操作从随机读,变成顺序读了。但是这样会影响整个表的顺序,只能根据一个索引优化,如果出现另一个查询,就无法根据另一个查询重新设定 row 的顺序了。并且数据库提供的这种 “row sequencing” 的技术和工具都颇为古老,很少使用。

更实用的一个解决方案是将 last_name 也放到索引中。这样就相当于将要查找的 column 放到了一起,我们遍历索引的时候,直接可以得到 last_name 这一利,不需要根据索引读出来 row 再进行判断。这时候索引中 last_name 这个数据不再是提供遍历树的索引功能,而是提供查询功能。

MySQL

1

2

CREATE INDEX empsubupnam ON employees

       (subsidiary_id, UPPER(last_name))

在执行计划中,可以看到 filter 这一步的成本显著降低。因为我们不需要去读表中的数据了,在索引的数据中就可以完成 access,判断是否符合 LIKE 这两步操作。

Covering Index

如果一个索引,能使一次查询避免 table access,那么这个索引就叫做 covering index.

比如我们建立下面这个索引:

Oracle PL/SQL

1

2

3

CREATE INDEX sales_sub_eur

    ON sales

     ( subsidiary_id, eur_value )

那么这个查询就不需要 table access:

Oracle PL/SQL

1

2

3

SELECT SUM(eur_value)

  FROM sales

WHERE subsidiary_id = ?

因为索引中已经有一份 eur_value 的 copy 了。Oracle 中的执行计划如下:

Oracle PL/SQL

1

2

3

4

5

6

7

8

9

10

11

----------------------------------------------------------

| Id  | Operation         | Name          |  Rows | Cost |

----------------------------------------------------------

|   0 | SELECT STATEMENT  |               |     1 |  104 |

|   1 |  SORT AGGREGATE   |               |     1 |      |

|*  2 |   INDEX RANGE SCAN| SALES_SUB_EUR | 40388 |  104 |

----------------------------------------------------------

Predicate Information (identified by operation id):

---------------------------------------------------

   2 - access("SUBSIDIARY_ID"=TO_NUMBER(:A))

执行计划中的 INDEX ONLY SCAN 极大地提高了性能,执行计划说一共要聚合 40388 行,假如不是 INDEX ONLY SCAN 的话,这意味着数据库要读 40388(最多,如果这些 row 都没有在同一个 block 的话)次。

INDEX ONLY SCAN 是一种非常激进的优化策略,潜在的增加了数据的维护成本。比如会降低 update 操作的速度。使用 INDEX ONLY SCAN 需要三思。

另外,INDEX ONLY SCAN 也会带来潜在的性能问题。比如下面这个查询:

Oracle PL/SQL

1

2

3

4

SELECT SUM(eur_value)

  FROM sales

WHERE subsidiary_id = ?

   AND sale_date > ?

假如一个同事写了这样的查询,认为这个查询查到的 row 更少,理应更快才是。

但实际上却不是的,这个查询增加了一个不在索引中的 row,原来不需要 table access 的,现在需要了。所以查询的速度会慢很多。

Oracle 的查询计划:

Oracle PL/SQL

1

2

3

4

5

6

7

8

9

10

11

12

13

--------------------------------------------------------------

|Id | Operation                    | Name      | Rows  |Cost |

--------------------------------------------------------------

| 0 | SELECT STATEMENT             |           |     1 | 371 |

| 1 |  SORT AGGREGATE              |           |     1 |     |

|*2 |   TABLE ACCESS BY INDEX ROWID| SALES     |  2019 | 371 |

|*3 |    INDEX RANGE SCAN          | SALES_DATE| 10541 |  30 |

--------------------------------------------------------------

Predicate Information (identified by operation id):

---------------------------------------------------

   2 - filter("SUBSIDIARY_ID"=TO_NUMBER(:A))

   3 - access("SALE_DATE">:B)

在无法使用 INDEX ONLY SCAN 的时候,优化器会选择第二优的执行计划。在这个例子中,选择了 SALE_DATE 这个索引。这里的选择原因有2:1)通过 sale_date 过滤出来的 row 更少,执行计划显示是 ~10000 行,而上面的 subsidiary_id 有 ~40000 行。2)sale_date 这个索引,数据更加聚集。因为订单创建的顺序和时间是相关的,都是 append 在最后面。这可以可以看出,有些索引“自然”具有 cluster 的性质。

这个例子中使用 sale_date 这个索引正好是一个巧合,虽然它让 INDEX ONLY SCAN 失效了,但是使用它的这个索引,聚集效果也是很好的。但是通常,给 select 添加查询条件会导致聚集很差的查询路径,所以记得用 comment 或者其他的形式,来提醒自己这里的查询是一个 INDEX ONLY SCAN。避免后来添加查询条件导致性能急剧下降。

另外要格外注意,很多数据库多索引大小是有限制的。比如 Postgres 数据库,从 9.2 开始支持 index-only scans,B-Tree Entry 最大为 2137 bytes(硬编码)。如果插入或更新的时候超过了这个大小,会得到错误 “index row size … exceeds btree maximum, 2713” ,B-Tree 索引最多有 32 列。

Clustered Index

由上面一小节自然而然就会想到:那我们可以将整个表都保存在索引中吗?这样都不用回表查询了。答案是可以的。Oracle 将这种所有的数据都在索引中的形式,叫做 index-organized tables (IOT),名字很好的反应了它表达的意思,即表是完全按照索引组织的。另一些数据库将这个术语叫做 clustered index. 其实他们都是一回事。

简单说,clustered index 就是 B-Tree 索引中保存了所有数据,没有 heap table。

这样有两个好处:

  1. 省掉了表的空间,即 heap 的空间,因为所有的数据都在索引中;
  2. 每次查询都不需要回表操作,所有的查询都是 index-only scan;

坏处也显而易见:

因为 row 都是按照索引来组织的,所以 row 的顺序(物理位置)无法固定。正常情况下,我们在 heap table 中保存 row 的时候,我们随便找到一个空的地方保存下来就好了,然后在索引中更新 ROWID 的位置。这个 row 一旦保存好了,它的物理位置永远不会变了,我们只会更新索引中这个 ROWID 的位置。

然而这在 clusted index 中是无法做到的,因为 row 的顺序是按照索引来组织的,一个 row 的位置无法固定。一个 row 即使没有更新它,添加新的 row 的时候,它的位置也可能受到影响,跟着索引移动。

这是根本问题,导致的表面问题是什么呢?

假如我们现在有第二个查询语句,不是根据主索引来的(secondary index),按照有 heap table 的情况下,我们是这么查询的:先在 secondary index 中找到 ROWID 的位置,然后去 heap table 中找到这个 ROW。

但是在 clustered index 中,我们没有 ROWID,只有主索引,所以必须在 secondary index 中找到主索引,然后再去主索引中查找这个 row 的位置。

clustered index 中的这个查询(文中第二个)比 heap table 查询要慢很多,因为第一个查询是 INDEX RANGE SCAN + TABLE ACCESS 的操作,第二个操作需要经过两个索引,INDEX RANGE SCAN + INDEX UNIQUE SCAN 的操作。

当然,对于这个查询,其实可以针对 secondary index 再建立一个 index-only scan,这样针对这个查询又避免了回表操作。比起上面两个查询都是最优的。

总的来讲,clustering index 并不实用,clustering index 的 key 也比 ROWID 要长很多,所以占用更多的空间,适用场景非常有限。

如果 table 能确定,查询总是按照一个顺序的,绝对不会需要 secondary index 的话,可以考虑使用这种索引。

另外,不同的数据库对 index-organized tables 的支持差异很多。PG 不支持没有 heap table,但是可以用 CLUSTER 来将整个 table 放到 index 中。