P3:Lecture 3 SQL II (cont) + Disks, Buffers, Files I - main - BV1cL411t7Fz
我做到了,嗯,如果你点击,你没有,即使如此,也有更多选项,然后允许记录,好的,我该走了吗?好的,所有的权限,嗯,给我一秒钟,嗨,伙计们,这是三班,我猜是18:16,嗯,今天可能会有些停顿,因为我有了一个新桌面。大家忍耐一下,我们会尽力解决任何问题,至于我们正在解决的其他问题,聊天已经恢复了,所以你可以在那里自由提问,嗯,阿尔文会监控的,也许还有其他TA,我们看看他们中是否有人,呃,今天正在伐木。我们还鼓励你在广场上提问,这也是另一个论坛,让你提更长的问题。所以,我觉得从某种意义上说,聊天很好,能够立即回答问题,更多像是即时的东西,而广场更适合长篇讨论。因此,我们觉得两者结合会很好。好了,这是我们目前的想法,我们今天会看看它是如何工作的,如果如果不行,我们下次再换方案。好的,呃,所以提醒大家,呃,关掉视频,如果可以的话,你们大多数人已经做过了,太好了,呃,关掉音频。
除非你大声说出来,不然就不要做任何事。如果你没有正常做,亲自,嗯,类的,嗯,维生素的最后期限被推迟了,所以这给了你更多时间,我想是到星期天,而项目一,呃,最后期限应该还在正常进度上,假设我今天要涵盖的内容都已经讲过了。所以你会看到,如果我没有讲到,阿尔文会很生气的,开玩笑的,艾尔文不会生气,但助教们可能会生气的,嗯,好吧,所有的权限,嗯,我们现在讨论的是,呃,联合方差,好的,接下来是延伸连接,我们讨论了如何从选择或者从查询的地方选择,然后我们讨论了基本的连接,呃,我们讨论了如何在from子句和选择子句中使用别名,还有表表达式,我们讨论了字符串比较和连接器等等。好的,现在让我们来谈谈连接的延伸。所以这意味着超越简单的,我们来做一个笛卡尔积,来为更多的事情产生一个结果集,嗯,所以图表差异,我们要讲解,图表里面的自然连接和外连接,外连接有不同的类型,我们稍后再谈。
所以内连接是,呃,是默认的,好的,这和我们到目前为止所做的一样,并没有提供额外的便利,呃,标准的只是列出一堆表,所以你做内连接的方式。我将在下一张幻灯片中讨论如何在连接中做到这一点,但实际上我认为,它没有提供真正的便利,这是一个非常相似的语法糖,类似我们已经看到的功能,它只是作为自然连接和外连接对比的一部分,绝对是。呃,提供更多的东西,相比内连接,所以我看到很多问题,呃,我看到很多问题出现在我的屏幕分辨率上,大家有问题吗?所以我在这里做的是,我在分享我的屏幕的一部分,正好对应着这个内容,嗯,如果不是,我可以做一个扩展的视图。所以让我试试,呃,只是为了,如果你在缩放应用程序上,嗯,有一个叫做风景的选项,你可以在屏幕顶部点击,然后选择不同的视图模式,像是适合。
窗口原件,百分之百,百分之三百等等,如果你从浏览器访问,我觉得这个选项是不存在的,虽然,所以,嗯,看看你能不能玩一下,但除此之外,我觉得它在,呃。
你的幻灯片,好的,让我快速做一下,只是为了试试看,我希望这能奏效,它可能不会给我看到一切的能力,但那也没关系,可能是,好的,这应该没问题,希望如此,到了这一点,它占据了我的整个屏幕,好的,所有的权限。太酷了,太酷了,让我们谈谈内连接和自然连接的区别,然后我们讨论外连接。所以我们上次看到的是这个连接,所以我们上次看到这个语法,对吧?这基本上是一个常规的连接,在工作流中表达一系列条件,你把两张表连接起来。水手和预备役可以发出同样的查询,呃,使用内连接,你说,水手,呃,在某个谓词下有一个保留的连接,我在这里说,我希望这个联合发生在这个特定的谓词上,其中s点h大于20。所以这个查询基本上是说,在某些特定谓词下做水手和预备役之间的内连接,并且附加一些额外的过滤条件。就像我说的那样,一个内连接不提供额外的好处,像我们之前讨论过的版本一样,所以这不是我会做的事,我会更关注自然连接,它更有趣。
所以自然连接基本上允许我省略条件,它允许我省略s点ID等于r点ID,基本上这里发生的是,当我做自然连接时,它隐式地假设两个表中的所有元组在具有相同属性的情况下应该具有相同的值。所以在这张表中,如果我做一个自然连接,我会将ID设置为相等,这是这两个表之间唯一的公共属性,就这样隐式地进行。因此,所有这些替代方案的任何一个都是等价的。所以基本上这里,嗯,你是在添加一个隐式谓词,即这个共同属性被假定为相等的,这个公共属性被设置为相等,表示自然连接。这也是I连接的一种形式,其中两个关系的属性值被认为是相等的。所以关于自然连接要注意的一点是,嗯,它有时会产生意想不到的结果,尤其是当两个关系中有许多相同的属性时,嗯,是啊,是啊,你不打算让它们在连接中平等,所以我想说。
尽管自然连接提供了句法上的便利,关于其他形式的连接,它仍然存在一定的风险,有时它会给你一些你不想要的结果,因此有时属性相等的条件不应自动视为平等,尽管它们的名字相同。到目前为止,所有问题似乎都有意义,嗯,是的,你能听到我吗?是的,是的,好吧,所以,嗯,我只是,我不太确定,我不是很清楚,但是自然的连接,嗯,当主键相等时,或者你必须指定哪些列时,不,它不是,呃,指定的,它并不是基于主键,它只是基于属性名。所以在这两张表中,水手和预备役,它将应用一个隐式的谓词,比如说,SEALERS SID 等于结果或SI,这是因为ID在两者之间是共同的,它不依赖于主键或外键,没有那些东西。好的。所以如果我在这里有另一个属性,不知为什么,它叫S的名字,它也会在上面应用一个基于平等的谓词,但它有水手表。因此,它只是查找两个表中存在的相同属性,然后基于此添加一个等式。好的。
我明白了,还有一个问题,对不起,真的很快,嗯,假设每个人身上的ID都是一样的,但是如果它在储备表中被称为ID,我们该怎么处理呢?我们怎么做到这一点,如果一个是ID,另一个是ID?你必须使用其他类型的连接形式来表示它,自然连接不会自动处理这种情况,因为自然连接只是基于属性名相同的假设,并且在特定情况下会将其视为相等。如果这是ID,那么它就是ID。我需要加一个额外的where子句来正确处理这个问题,所以我必须说水手或者id等于id,明白了,谢谢。好的,那么让我们来谈谈左外连接。左外连接是一种有趣的变体,它超出了我们到目前为止所看到的功能。因此,它可以捕获自然连接和内连接,使用我们过去看到的功能。省略一个连接返回所有匹配的行,就像我们以前那样,但它还保留表中所有不匹配的行,位于连接的左边。好的,在这种情况下,正如你所看到的,呃。
大力水手元组匹配这两个元组,这个橄榄油元组和这个元组,而加菲猫和鲍勃不匹配任何元组从储备,所以外连接的左边是,它为这些元组填充空值,然后生成额外的元组,这样,左手边的每个元组都以某种方式在输出中捕获。好的,所以如果我这么做了,比如说,这里,水手们漏掉了一个接头,s点id上的储量等于r点id,嗯,我会归还所有的水手,如果没有匹配的S点ID,就像加菲猫或鲍勃的案子一样,r点ID将设置为空,因此。另一个第三个属性将被设置为现在,所以除了嗯,对应于这两个被连接的元组的一个元组,对应于这两个被连接的元组的一个元组,和一个元组对应于这两个被连接,我还会再输出两个元组,对应三个加菲猫和空四个鲍勃和空。因为那是我的名字,r b r或b填空,因为我不知道该搭配什么,对呀,所以如果我不知道该匹配什么,我现在就说,右边的外部连接基本上是相反的,因此,如果我保留表中所有不匹配的行,在关节爪的右边。
所以在这个储备的情况下,如果这里有一个元组,让我们说,我不认识五个,这个元组,因为它和左边的任何东西都不匹配,它将被填充适当的no,如果我在做一个正确的关节,同样地,我可以将此扩展到完整的外部连接。这在某种意义上给了我两边相同的功能,所以我首先返回两个关系中所有匹配的元组,如果我左手边没有火柴,然后我会让其他值填充为NULL,如果我右手边没有火柴,左手边的值按现在的方式填充,好的。所以我在这里做了一个预备队和船只之间的连接,我在做一个完整的外部连接,嗯,所以基本上对于这个R中的所有元组,与B中的任何东西都不匹配,我会的,我将填写b b和b b名称为空。对于B中所有与R中任何东西都不匹配的船,我会填补R SID到现在,这是完整的外部连接,它只是把它加进去,确保覆盖两个关系中所有不匹配的元组,关于这一点的续集,因为你在作业中使用它。
不支持右和完整的外连接,所以只支持左边的外连接,你可以,呃,努力克服这个限制,呃,通过重新安排关系的顺序,以及应用面向集的运算符,你稍后会谈论的,好的,所以你看到了空值的概念,对呀,所以它会填充空值。空值是什么意思,让我们详细讨论一下,因此,空值用于指示任何属性的占位符值,在任意元组中,嗯,这表明了许多可能的意思中的一个,可能是值存在但未知,或者价值不适用,所以一个刚进大学的学生。也许他们的平均绩点是,他们没有学分,所以不适用,但也许他们的家乡是未知的,但现在是对的,所以这可能是家乡的空值,GPA的空值将不适用,家乡的空值可能未知,对呀,因此NULL可以用来编码许多不同的解释。null,当然啦,也自然来自外部连接,都是左外接,右外侧关节以及全外侧关节,空值的存在使我们的事情变得更加困难,所以让我们专门讨论选择谓词,所以在where子句中以及,所以让我们从网络损失中的选择开始。
所以让我们假设这个元组的等级等于空,假设我有这个查询,它是从等级大于8的水手中挑选的明星,好的,所以基本上我是说,把所有的数据给我,或者所有等级大于8的水手都应该大力水手,是否在输出中,不是真的。在输出中包含大力水手是没有意义的,因为我们不知道他们的评分是多少,因此它们不应该是输出的一部分,所以我们特别要求评分大于8,我们不知道读数是多少,可能不到八个,所以这个。因此Poppy不应该是此查询输出输出的一部分,它们也不是此查询输出的一部分,对呀,它要求所有的收视率,呃,水手读数小于或等于8,所有的权利,所以现在进一步扩展,让我们说我说说,从水手中挑选明星。在网上评级很好或评级不平等的地方,现在这里,技术上来说,呃,大力水手仍然不会输出,即使我们在课堂上有同义反复,如果你选了编程语言类或编译器类,你会说,好的,这只是一个同义反复,让我们把它处理掉。
每时每刻都是真的,但在这种情况下大力水手仍然不会输出正确的,好的,所以可以肯定,呃,产出,呃,我们可以使用一个显式条件,它是空的或不是空的,所以在这种情况下,你可以纠正这个并强制执行那个。popeye在输出中显示为添加一个子句,它的额定值为空,所以你的评分等级超过8,评级小于或等于8或评级为现在,这将确保大力水手显示在输出中,因此,这将正确地输出设置中的所有元组。我们做这个黑客攻击是为了处理这个上下文,但是我们需要一个更有原则的方法来处理空值,这就是你们接下来要讨论的,以便推断空值,我们需要一种方法来计算空谓词,涉及空值的sorry单元谓词,一种组合它们的方法。呃,当你有布尔连接词时,然后确定它们是否应该成为输出的一部分的方法,因此,特定的元组应该是输出的一部分,或者不是,当元组包含空值时,那么让我们来谈谈第一个正在评估的成分,单位谓词,所以这个,嗯。
所以这和我们到目前为止看到的很相似,如果有一定的固定值,一些比较算子,然后为NULL,计算结果为NULL,好的,所以我们要说这将求值为空,也就是,我不知道对吧,认为空,因为我不知道。如果你把x和null进行比较,你不知道结果是什么,所以你会说,我不知道,嗯一百等于零,呃,这是呃,基本上一个条件是空的,等于一百,我不知道,所以它将是空的,精选百课,再次为空将为空。因为你不知道结果是什么,嗯,另一种类型的谓词是我们在上一张幻灯片中看到的is空谓词,所以这计算为true,如果为空和假,否则没问题,所以这是一个特例,它的计算结果为true,如果为空和假,否则嗯。所以很抱歉,一个问题,是啊,是啊,呃,你能听到我吗,好的,呃,返回到上一张幻灯片,这就像上一张幻灯片一样,有一堆条件,我喜欢知道,但是的,如果就像我有,我有一种情况,呃,有些东西不等于做新的东西。
包括在那里,例如,比如评分不等于十一的地方,就像这是AL中的新编码,好的,是啊,是啊,让我们谈谈那个吧,所以评分不等于十一,那应该是,你的回答是"是"还是"不是"。我们需要一个有原则的方式来谈论这个问题,我们稍后会讨论那个具体的例子,某物不等于某物,看看null是否有效,好还是不好,在那个场景中,在几张幻灯片中,好吧,如果我不在几张幻灯片中回答你的问题,让我们呃。再举起你的手,所有的权利,好的,所以嗯,所以我们需要一种方法来评估,单位谓词,所以一些东西操作一些东西呃,我们会认为这是无效的,嗯,让我们谈谈第三种成分,所以这是第一种成分,这个食材二,它是一个配料树。所以让我们来谈谈,呃,我们已经讨论过单位谓词,我们需要想出一个方法来决定是否输出,以便决定是否输出,如果对于给定的元组,well子句的计算结果为null,只是不输出元组,好的,足够简单,所以说。
如果你从水手中挑选明星,如果给定元组的某些属性值为NULL,它仍然可以是输出的一部分,因为其中子句默认为true,另一方面,例如,如果额定值为空,您正在计算那个元组的单词丢失,子句求值为空的位置。因为它的计算结果为NULL,您将不输出元组,好的,所以规则是,如果某物的计算结果为NULL,不输出元组,否则输出元组,所以现在有了这两个方面,这是呃,评估单元谓词并决定输出,第三个方面是组合谓词,好吧。那么我如何看待谓词的组合,涉及到,呃,各种操作,就像它们之间的布尔连接词,因此,我们需要一种新的逻辑形式,它是布尔逻辑的扩展,这就是所谓的三值逻辑,所以让我们谈谈这个,呃现在。所以这里有一个三值逻辑的真值表,好的,所以我要扩展传统的牙桌,或者没有新的值,它是空的,好吧,那么现在,我需要,自从,我有第三个值,它不是,呃,两个值,第三个值,呃,我现在在我的两个表中有九个条目。
还有我在R里的两张桌子,而不是四个,当它只是真的或假的时候就在那里,好的,好的,所以让我们通过几个例子,然后你会看到这些真值表是如何有意义的,假设我有一个元组,呃,其中额定值为空,好的,就像我的你。因此,如果额定值为空,让我们看看这两个谓词会发生什么,所以额定值为空,所以它是空的大于八或空,呃,小于或等于八,我们知道如何正确地计算单位谓词,与某物的空比较为空,因此这两个都是空的,所以这将是,好的。所以现在我们有null或null,现在我们可以查一下我们的真相表,我们的真值表说得好吗,如果你有一个零,一个零是奇数,你得到一个空,好的,因为如果额定值为空,则第一个谓词为空,第二个谓词为空。你在命令他们,你回到零,所以这个有等级的元组等于空,不会因此而被退回,呃查询,让我们考虑这个例子,这是一个学生早些时候提出的例子,我忘了名字,嗯,所以假设它的评级不大于八,好的,所以再一次。
假设评级为零,空大于八,然后我试着做一个结,好的,大于八的零,是单位谓词,所以我将把它视为空,使用我们的单位谓词规则,我没有空的,好的,现在我可以查我的非真相表,查找NULL的结。NULL的NOT再次为NULL,所以我要回去了,空,返回此元组作为此结果的一部分,对呀,这是有道理的,对呀,你不知道他们在吃什么,所以你不应该把它作为输出的一部分来生产,所以总的来说。如果你看看这个真值表,我想让你花一些时间盯着这个真相表,你会意识到空值基本上被视为我不知道,基本上它可以是真的也可以是假的,如此如此,涉及它们的谓词可以被评估为true或false。当它们作为输出的一部分生产时,你最终会丢弃它们,当你以一个我不知道的场景结束时,好的,所以你最终会丢弃它们,你进入了一个我不知道的场景,你必须考虑这两个值是真还是假,所以我将停止这些空值的东西。
我鼓励你多看几个例子,去嗯,说服自己这些真值表是有意义的,那么空值如何使用聚合,对呀,所以这里的一般规则是,空值将被聚合函数忽略,因为我不知道价值是多少,所以我不想把它作为我的聚合的一部分。那么这三个或四个查询会发生什么,嗯,如果我做一个计数,星星星星没事,因为星号指的是所有值的组合,然后嗯,所以在这种情况下,我会正确地返回所有水手的计数,即使评级有一些空值,好的,所以这不会影响这个计数。另一方面,如果我说水手的评分,那么我最终会数出非零评级的水手,对于总和也是如此,和平均数,我要计算非空评级的总和,或者这两种情况下非零评级的平均值,对这些东西有什么问题吗,我有个问题。那么有没有可能有一个角色,只需使用所有空值,是否可以有一行具有所有空值,这是个有趣的问题,我不知道答案,也许我不知道数据库是否能阻止,阿尔文你知道吗,嗯,是啊,是啊,我想你应该能做到,我是说。
除非列或属性本身被声明为不允许可知值,否则肯定,是啊,是啊,所以我想我能想到两个案例来扩展阿尔文说的话,如果属性声明为非空,您当然可以设置该约束,另一种情况是,如果某些属性又是主键,您不能让该属性为空。但是如果您的表没有主键,并且没有额外的约束,我认为有可能,我不知道数据库,它会阻止它,我想你有可能应付得了,呃,是啊,是啊,如果列中的所有值或现在,那么该列的和将返回空,对呀。所以如果你不知道价值是多少,所以你最终会返回空,所以这就是答案,任何其他问题,嗯,只是为了澄清平均计算,就像它除以,就像那些,呃,嗯哼,那个数字是,它没有考虑到空行,对呀,正确。所以分母基本上是非空评级,不全是收视率,如果你想捕捉所有的收视率,有办法做到这一点,比如说,你可以做评级之和除以总计数什么的,所以你可以近似地,嗯,所以说,但那是,我是说,所以说,是啊,是啊,呃。
你基本上是中等评分,分母是非空额定值的数目,所有的权利,所以这是呃,的结论,呃,所以基本上是空的嗯,当您将空与任何东西进行比较时,它最终会是空的,如果您有一个包含null的where子句。那么您就不将该元组发送到输出,对呀,所以如果它是空的,不要将那个元组发送到输出,布尔连接词来处理这个问题,当您有空值时,你用三值逻辑,我给你看了一些真相表,我鼓励你去看看那两张桌子,用眼睛思考这些空。因为我不知道,因此我不能做出正确的承诺,所以我不知道,因此,我不能提交是读取这两个表的有用方法,然后聚合忽略空值输入,好吧那你怎么处理空值呢,你基本上在做聚合之前就把它们扔掉了。那么让我们来谈谈查询组合,因此,这将SQL的能力扩展到连接之外的其他更复杂的形式,呃,将查询分层,所以在我们进入之前,我想谈谈布景和包,所以这是我们一直在掩盖的事情,嗯但是呃,让我们试着再正式一点。
好的,所以一套是呃,你以前可能看过的东西,集合是一组条目,在没有重复的地方,好的,所以你有一个苹果,红苹果,橙色等等,一个包或一套多,这些是一样的允许重复,所以你可以有两个红苹果的副本。一个绿色和三个橙色中的一个,现在想象这些苹果和橘子等等是元组,好的,所以这才是我们真正要讨论的,我们说的不是水果我们说的是元组,所以SQL使用包语义,对呀,因此,在给定的关系中。您可以有多个主题和每个元组的多个副本,所以你可以有多个副本,所以现在的一个问题是,我们如何跨关系组合元组,当然加入,允许跨关系连接元组,但你觉得怎么样,把这些元组当作一个袋子或一套,然后加减,呃。以那种方式,并这样做,我们需要更多的面向集或面向袋的操作员,所以我们讨论了强制set或bag语义的运算符,所以让我们先谈谈设置语义,因为想一套更容易,当然啦,就像我说的,是不同元素的集合,用关系的说法。
嗯,每个元组或行被视为唯一的,而这些操作员,即使输入实际上不是,实际上并不遵守这个限制,实际上最终会,在操作员完成执行后遵守限制,好的,我马上就会告诉你我的意思,所以首先让我们谈谈运营商。所以a联合b a截取b a除b,这些都很熟悉,一种面向设置的运算符,允许您,标识A或B中的不同元组,这是两个关系,a和b中相交的不同元组,除了哪一个是不同的,也叫差异,嗯,中的不同元组,但不是在B中。好的,所以基本上我们把关系中的元组看作集合的元素,好的,所以让我们来举一个例子,假设你有五个元组,a、b、c、d、e,所以我只是给他们打电话,带着他们的信,你有两个关系,R和S,我们将在,呃,在一点点。所以让我们关注这两个,你有RSS关系,这些不是集合,因为它们可能在SQL和关系数据库中有重复的,你确实允许关系有重复,假设R有四个元组A的副本,两个元组B,呃。
元组C中的一个元组D和S中的一个元组有两个A,b的三个,以此类推,所以联合基本上是将这两个关系中的元组联合起来,并删除重复的,所以你保留了一个,b中的一个,c中的一个,D中的一个。B交叉点中的一个将保持公共,呃,跨这两个关系的元组并删除重复项,所以它将保留一个B和C,因为这是这两个关系之间仅有的三个共同的元组,或者区别本质上是在r中找到元组,但不是在S中。所以r中唯一不是s的元组是,只有一份,所以我不需要消除重复,这是我很快的输出,嗯,如果我有这张预订桌,我从预备队中挑选了明星,工会选择从储备开始,我们认为输出会是多少,对不起,我没有监视聊天。所以也许你在聊天时大声说出了你的答案,如果你们中的一个能举手或大声回答,那就太好了,着色器,我想你只是把所有重复的都去掉了,所以你只要把所有重复的,本质上就是这样,所以你会扔掉其中一个副本,好的听着。
让我们来谈谈多集语义,那么当我们有每个元组的多个副本时会发生什么,我们正在应用依赖于这些的运营商,每个元组的多个副本,所以推理和表示多个集合的一种方法,是通过用副本注释每个元组,呃,所以你有四个副本。b的两个,c中的一个,B中的一个,以此类推,好的,所以这允许我们以一种更方便的方式来表示这些关系,这真的不是它在数据库中的表示方式,这只是我们的方式,呃,讨论这些运算符的语义。所以让我们来谈谈第一个运算符,它是联合,全部,呃,这是在多个集合上执行联合样式操作,它所要做的就是,每种元组的基数之和,所以你有四个第一个关系的A副本,从第二个开始两个,所以你有四加二。也就是结果中A的六个副本,呃,B在第二个中的两个副本,呃,在第一个关系中,第二个关系中的三个,所以结果中有五个B的副本,呃,c中的一个,每个c中的一个,所以你有两个C的副本,第一个关系中的D之一。
不从第二个开始,第二个关系没有,所以你有一个,呃在结果等等,好的,所以这是,呃,基本上把两个关系中的副本结合起来,呃,所以你把那些副本放在身边,你只需加上副本的数量,这样你就有了。你做一个袋子联盟或联盟,或者回到我们的例子,我们怎么看,工会,所有的查询都会给我们,嗯,我会给你答案,它会给我们,这个元组的四个副本和这个元组的两个副本,对呀,所以你最终会,嗯,基本上结合了呃的多重性。加起来,两者的多重性,呃,这个联盟双方的关系,所有的权利,所以你有两个第一个的副本要从这里拉出来,从这里拉出第一个的两个副本,第一份的一份,从这里开始的第二个或第三个元组中的两个。从这里开始的第三个元组的一个副本,所以你有第一个元组的四个副本,第三个元组的两个副本,好的,对呀,所以要把所有的,我希望我不必在这件事上纠缠太多,它基本上是工会的延伸,所有的想法交叉,嗯。
所以它将取最小值而不是和,所以您将查看a for r的拷贝数,A for S的拷贝数,你拿最小的,那是两个,所以这就是你回报的,嗯,再举一个例子,d,你在我们的,在s中没有副本,所以d不是输出的一部分。所以这是相交的,除了会有所不同,好的,所以第一关系中的拷贝数,减去第二个关系中的拷贝数,所以在第一关系中有四个A的副本,第二关系中a的两个副本,所以你要做四减二,你只剩下两个A的副本。你在第一个关系中有两个副本,在第二个关系中有三个副本,所以是b 2减3,所以你只剩下-一个b的副本,所以B不是输出的一部分,所以如果你有负数的副本,它不是输出的一部分,也是如此,E不是输出的一部分。b是产出的一部分,因为它是一个副本在一个,一个副本在我们的无副本在S,c也不是输出的一部分,因为两个都有一个副本,好的,所以这是在袋子操作中设置的,所以工会交叉并接受,执行面向集合的操作。
处理元组和a、b集并集都相交,所有和接受所有执行面向袋的操作,将两个A和B视为袋,因此,正确应用这些操作的一个重要注意事项是,您试图结合的两个关系的模式,相交,接受必须是一样的,如果架构不相同。这个操作没有意义,所有的权利,所以嗯,在复杂布尔逻辑的使用之间有一些微妙的作用,在where子句中并设置操作,所以让我们看一些例子,这里有一个例子,我想找预订红船或绿船的水手,一种方法是简单地。呃一个工会,对呀,所以我可以做一个选择订单,来自船只和储备的ID,在哪里,呃,r点id等于b点b,b点颜色等于红色,所以船的颜色是红色的,预留了一块绿板,所以这些人预订了红板,这些人保留了绿板。我在做工会,这意味着我正在处理重复的,所以我只剩下水手的身份证了,他们预订了一艘红色或绿色的船,我可以通过将这些谓词移动到一个查询中来做一些不同的事情,用一个更复杂的,在哪里上课。
所以我可以从两个B储备中选择不同的R SID,其中我应用相同的谓词,rb等于b,这意味着我说的是两个关系中的同一个元组,我说要么船色是红色的,或者板色是绿色的,好的。所以这是在这里说同样事情的另一种方式,所以我可以用,在这个更复杂的,呃网络课,所以这些应该会得到完全相同的结果,事情变得更棘手了,如果你通过这些例子不管有没有工会,或者更确切地说,用工会全部取代工会。如果我们省略了,呃明显,所以我会让你考虑一下,嗯,在那些情况下会发生什么,这些是做什么的,uh查询求值并执行这些查询,在那个上下文中最终意味着同样的事情,所以这是给你的家庭作业,乡亲们,好的。让我们看另一个例子,所以我在这里,我已经用互联系统取代了我的工会,我想找到预订了红船和绿船的水手,所以这个查询突然成功了,好的,对呀,所以我找到所有预订红板的水手,和预订绿色木板的水手。
我在做他们之间的交集,所以这就解决了,好的,另一方面,这不是很好,对呀,为什么我们认为这没有给我们想要的,呃阴嗯,因为where类应用于每一行,每一行不能有两种完全正确的颜色。所以where子句一次应用于每一行,一个给定的两者不可能同时是红色和绿色,对吗,所以你真的得考虑把磨损布,把红色和绿色分开组合在一起是行不通的,所以这种方法不起作用,即使在上一张幻灯片中。我们看到我们可以用一个更复杂的where条款取代工会,在这种情况下,你真的不能用更复杂的减肥来取代互联系统,这就是从中得到的洞察力,所以说集合定向操作,让我们继续筑巢,这也是查询组合和集合的精神。嵌套允许您将查询嵌套在其他查询中,所以您可以使用查询表达式作为子表达式,在另一个查询中,我们将讨论嵌套和子查询,我们将讨论视图和常见的表表达式,这是三种机制,允许您使用查询结果,在另一个查询中。
所以让我们举一个嵌套查询的例子,这是使用一个特殊的关键字,所以我想找到那些在12号港口的水手,所以我能做到这一点的方法是说,选择水手或水手的名字,所以水手的名字在哪里,那么水手ID在这个查询中,好的。这个查询被称为子查询,这个查询说了什么?这个查询的意思是,我想让你找到我,所有的,呃,备用表中的水手身份证,所以我说的船ID是一零二,所以这个子查询基本上是在找我,所有的,呃,同时预订了一零二的水手身份证。然后这个子句在检查,如果我们谈论的这个特定的水手有水手身份证,等于集合的一部分,那么它到底在不在这一套里,所以这被称为子查询,好的。所以这个,呃,这个子查询的结果基本上被视为一个集合或多个集合。在集合中选中s点id的成员资格,使用IN运算符,因此,您正在检查s点id是否在集合的结果内,就像在,你也可以不在,所以你可以找到没有预订给定船只的水手的名字,所以你简单地说s点id,不在子查询结果中。
好的,所以和之前的非常相似,所以现在让我们讨论具有相关性的查询,所以这是一个,这些是没有相关性的查询示例,我们将讨论具有相关性的查询,那么相关性的差值是,呃,变得更加明显,所以嗯。所以我们到目前为止讨论的机制基本上是这种机制,你说,值不,uh是某些嵌套查询中的可选关键字,我们还可以检查嵌套查询结果是否为空,不是简单地检查集合中的成员资格,还要检查嵌套查询结果是否为空,这才有意义。仅在相关性的上下文中,好的,所以让我们谈谈这个,所以让我们说我想找到,呃,相同的查询结果,我想找到既预订了一零二的水手,我想使用这个现有的类,那么我该怎么表达呢,这就是下面的。我可以从水手的名字中选择一个水手的名字,在保留表中存在元组的地方,呃对应于两个ID 1,零二,对应于这个水手,好的,那么子查询的独特之处或有趣之处在于什么,你有这个变量,s也是一个范围变量。
基本上是一个指标,它允许您循环水手关系的元组,这个变量在这个子查询中计算,所以它实际上是子查询的一部分,这与以前嵌套查询的使用非常不同。所以在这种情况下,您有一个相关的子查询,因此计算此查询。从概念上讲每个水手都要这样做一次,对于每一个水手元组,您将检查这个子查询中是否存在结果,所以即使这在概念上是你在现实中的想法,不是这样执行的,我们想出如何在随后的讲座中更有效地执行它。但这是你在概念上应该如何思考的,这就是为什么它是相关的,因为沙拉表中的每一个元组,您将继续执行这个新的子查询,并检查该子查询,呃,已经,呃,空结果与否,好的,所以你在检查,如果这个结果中是否存在元组。所以这是一个相关性的例子,嗯,通过这个存在,呃,关键字,对此有什么问题吗,呃,在,然后你举起你的手,但我听不见,哦对不起,我让自己静音,所以只是为了澄清,嗯,所以我们,我们正在为每个卖方条目执行子查询。
对呀,谢谢。是啊,是啊,在概念上是正确的,其实不是,我们会想出如何更有效地执行这件事,但从概念上讲,对于每个水手元组来说,这是正确的想法,你要评估井条款,猜猜每个封口师要拉什么。where子句需要从头开始计算,因为子句中提到了细胞元组,或者至少在where子句的子查询中提到,所以您必须重新执行子查询,对于每一个元组,所以我们已经讨论过了,我们已经讨论过了存在。所以也有不在和不存在,所以我们已经涵盖了所有这些类型的操作,我们也有,嗯,一些操作员,因此比较运算符与以下任何或所有运算符,所以你可以说,如果我想找到水手,其等级大于某个特定水手的等级,我们说大力水手。所以我可以表达这个查询,我选择的地方,从无状态开始,其中s点等级大于此子查询的任何等级,子查询将返回一系列与Popper相对应的评级,好的,所以嗯,我想要我给出的评级。
也就是S点的额定值比这些环中的任何一个都大,所以这让我有能力找到,嗯对那些呃,以一种相当呃方便的方式满足这个条件,所以再一次关于光的注意,不支持任何和所有。您可以了解如何表达其中的一些基于任何和所有的查询,用另一个,呃,到目前为止我们描述的关键字,所以这是一个艰难的问题,嗯,这叫做关系除法,所以假设我想找到预订所有船只的水手,好的。所以我想找到预订所有船只的水手,这很难直接接近,解决这个问题的方法实际上是把,在某种意义上的谓语,所以我想在某种意义上找到没有失踪船只的水手,以至于没有他们没有预订的船,所以不是去找保留所有船只的水手。我想找到水手,没有他们不保留的船,为了表达这一点,这里是这个查询,它比,到目前为止我们看到的典型查询,所以呃,只是带我们经历这一切,嗯,那么让我们来谈谈第一部分,所以我想找到所有不存在的水手。
一艘他们还没有解决的船,所以这个子查询正在检查,它发现它正在检查所有的,它正在寻找他们没有预订的所有船只,就是这个子查询,这个大箱子在做下一个箱子,所以下一个子查询嵌套在,它正在执行以下操作,例如:
给了一个特定的水手和一艘特定的船,我正在找这艘船的预订,用另一种方式说,我想找到所有的水手,在没有他们没有预订的船的地方,并找到他们没有预订的一套船,我想找到不存在的船,嗯马马虎虎,在某种意义上。内部的查询正在检查给定的端口,他们有没有预订,这就是这个更复杂的查询的两个部分,这就是所谓的关系划分,这基本上是在为所有谓词做一个,对呀,如此优秀的水手预订了所有的船,它有两个嵌套查询。一个嵌套在另一个里面,对此有什么问题吗,好的,所有的权利,所以继续前进,嗯,为了节省时间,我要去,我将跳过关于argmax的讨论,我会把它盖住,嗯,我们要么在一节中讨论这一点,否则我们就发条通知。并继续查看,所以这些是命名查询,是进行查询组合的另一种方式,以便声明视图,我只说创建视图视图名称,呃,作为某种查询,好的,所以这是一个简单的选择语句,可以是简单的选择语句,也可以是复杂的选择语句,因此。
视图有几个目的,一是让开发更便捷,所以它封装了一个查询,结果也经常用于安全性,所以如果我想让人们接触到我关系的片段,我可以根据那个片段创建一个视图,只给他们访问那个片段的权限,um视图通常不具体化。也有具体化的使用版本,但通常至少我们谈论它的方式,我们没有物化,所以这里有一个视图的例子,所以这是一个叫做红色的观点,所以它们没有实现这一事实的含义,思考这一点的方式只是重写,对呀。所以如果我创建一个视图,我基本上可以替换该视图的任何实例,在这个扩展表单的查询中使用,所以让我们举一个例子,假设我想创建一个名为Red Count的视图,我有这个从哪里选择查询,嗯。所以我只是在计算所有红色船只的预订数量,所以嗯,我在做b b b,我在数,嗯,呃的数量,该港口的预订,在船只和储备之间进行联合,然后我有一个关节条件艺术将是,我正在检查,如果木板的颜色是红色。
我在船旁边分组,在我执行此聚合之前,哪个是当前的明星,好的,所以这是我的观点,我就是这样表达的,既然我已经表达了,我可以在以后的查询中使用红色计数作为关系,所以我基本上可以发出一个查询。上面写着从红色开始选择,s数小于十的地方,这将归还我所有的船,预订次数较少,然后是柜台预订不到十个的所有红板,所以有一种方便的方法来引用一个相当复杂的表达式,所以这有点像,呃,呃,呃,所以如果我要用。嗯,宁愿让我回溯,所以如果我是这样,一种表示查询并在以后的查询中重用该查询的机制,是作为一个露骨的人,我也可以在飞行中声明一个视图,在从哪里选择语句期间,所以在这种情况下,我是说。下面是相同的红色计数查询,我宣布它是,动态中的新关系是红色计数,b id逗号计数,好的,所以我宣布一个新的,在从条款中飞行的关系,所以在前面的情况下。
我在select from where语句中显式使用视图,在这里声明之后,我在执行不同的查询时在原位声明它,所以我基本上是在飞机上宣布的,作为查询的一部分,所以我可以声明这样的视图作为前爪的一部分。我也可以在查询发出前声明,所以我可以使用width语句来表示查询,所以在这种情况下,我再说一遍红色计数,算作两个属性,嗯对应于这个,呃查询权利,所以这个红色计数再次对应于这个查询,这就像一个风景。只是将视图声明为查询一部分的显式方式,所以这个红色计数引用了这个查询的结果,现在,从红色计数中选择恒星,好的,所以这应该是计数,我觉得呃,她写道,有一个问题,去拿椅子,哦耶,我只是在想,有什么区别。比如创建一个视图而不是为它创建另一个表,因为就像是的,所以我现在的理解是视图几乎就像一个子表,我会说是的,但就像,在这种情况下,我为什么不创建一个表,是啊,是啊,好的,好问题,所以用标准的说法。
我们一直在谈论观点的方式,未实现视图,所以它们只是一种方便的手段,安全手段,你所说的不是以这种方式创建一个视图,为什么我现在不创建一个新表呢,用查询结果创建新表是一个问题。因为我需要确保那张桌子是最新的,当影响此表的其他表发生更改时,好的,所以这个红色计数,比如说,如果我根据查询结果创建了一个新表,那么每次更改或结果都更改,我得去更新这张红牌,因为否则它就不新鲜了。这有道理吗,就像这些与桌子同步一样,这样视图就会自动同步,因为我实际上不储存任何东西,我只是存储查询,对,我刚看到查询,然后我重写了引用这个的其他查询,使用视图表达式,所以我只是撕掉了视图表达式。然后把它放进去,每当我看到红色计数出现,我只是把它放在有意义的展开形式中,所以你不能写进账单里,你只能从账单上读到,又对了,写采访是有办法的,但从我们的角度来看,我们不要担心写采访。
实际上有很多研究文献在更新使用,但从我们在实践中的角度来看,这不是,呃不是我首先会推荐的东西,所以视图更新是一个复杂的野兽,从我们的角度来看,让我们把使用看作是重写的一种机制,我想这就足够了。那是有道理的,非常感谢,是的,任何其他问题,呃,卡森,所以说,这是一个关于,嗯,阿尔文,所以在聊天的时候他说,嗯,子查询可能有点棘手,所以尽量避开他们,所以说,我在想。如果这意味着我们应该更喜欢评论和TS而不是子查询,然后你这么说是什么意思,我不太确定,所以基本上是说,就像你知道的,它们很难写,他们很难讲道理,他们也很难,呃,在效率方面执行,因此。如果您可以重写为单个查询,没有任何意见,没有任何TS,那是最好的,但如果你不能做到这一点,那你就知道这取决于哪一个在执行方面更困难,效率,仅此而已,我就是这个意思,是啊,是啊,所以我想其中一件事是。
我是说,在这一点上有不同的制度,嗯,比如说,Postgres在公共表表达式方面做得不太好,所以我们在这里谈论的那些,嗯,我经常建议,如果有方法可以表达查询,用最少的花哨的东西,那将是以最简单的方式。数据库系统来解释,我建议不要使用更花哨的功能,只是因为嗯,数据库系统,即使经过几十年的发展,也不能有效地执行其中的一些,呃,呃,某种结构,所以我同意艾伯特在这方面的观点,好的,所以width语句。所以再一次,我们有三种方式来讨论观点,一个是独立于其他查询声明的显式视图,以便在后续查询中使用,嗯,正如我们在这里看到的,在一个from子句中动态声明的视图。或者在查询之前声明为with语句一部分的视图,所以你说,用,你可以做多个序列,所以你可以有一系列这些东西,所以你可以用红色计数来说,然后是别的名字,另一个视图作为从红色计数中选择恒星,s数小于十的地方。
所以我可以在我的第二个,呃,在这个公共表表达式中排序视图语句,然后我最终可以有Select语句,它是从不受欢迎的明星中挑选出来的,所以我现在在这个选择中声明了两个视图查询,其中。这两个视图查询都是在选择开始之前声明的,从哪里,所以在这种情况下,它只是一个选择,而不是选择,好的,所以我将跳过这个Argmax组,我因为我没有描述Argmax,嗯如此广泛,呃。我觉得在某种意义上我们今天讨论了很多,总的来说,这是一个,嗯,写SQL查询是非常困难的,这需要大量的练习,这是一种不同的思考数据的方式,编程与传统编程有很大不同,它需要不同的心态,所以这是不可能做到的。除非你经常练习,所以我会鼓励你多多练习以获得这方面的经验,测试是一种重要的方式,可以加强你的理解,嗯,所以不是每个数据库实例都会揭示bug,例如,呃,没有任何角色的数据库实例。
它可能根本不会发现任何bug,所以它可能不会揭示任何概念或句法,语法错误经常被捕获,但至少概念上的错误可能不会在空的数据库实例中显露出来,所以在某种意义上我们建议不要仅仅依靠测试数据。而且还要考虑查询在所有实例中的行为,所以考虑空值的存在,考虑给定元组的多个副本的存在,嗯想想,呃,是啊,是啊,所以如果你在做连接,你是否应该做左连接,右连接,等等,构造测试数据,我想说这是有价值的。但在有限的程度上,所以有各种各样的方法来构建测试数据来检查你的,您的um查询是否正确执行,可以生成随机数据,有各种各样的服务允许你这样做,您也可以选择专门定制,呃,某些类型的数据。允许您捕获某些潜在的错误,嗯所以嗯,您可能希望捕获的错误类型,呃,包括um输出可能缺少行,不正确的输出模式,或者你基本上有假阳性或假阴性,嗯嗯,不正确的重复数或输出可能没有正确排序,因此。
这些是您可能出现的错误类型,这当然不是一个详尽的列表,我只是想给你一个暗示,您可能需要注意的错误类型,当你试图解决维生素或项目中的问题时,嗯,这是我在测试SQL查询方面的建议,并确保你的呃。SQL查询结果对所有实例都为真,好的,这么高级的总结,嗯,SQL是一种声明性语言,我们很快就涵盖了很多功能,p,强烈建议你花很多时间做练习例子,问题,呃,你的维生素,你的项目是为了很好地掌握SQL。现在用声明的方式思考事情的好处是,你不需要担心事情是如何执行的,所以我们不担心这个,一点都没有,这是我们将要担心的事情,在班上的其他人中,所以那个会担心这件事的人,是你吗,在某种意义上。使SQL成为可能的底层实现细节,也为一大堆其他数据系统提供动力,从NoSQL到其他可伸缩的数据应用程序,比如数据挖掘,可伸缩机器学习等,所以总的来说,我们将在实施方面谈论什么,是可伸缩计算的工具箱,呃。
从广义上讲,还有一个我们根本没有讨论的话题,如何设计一个好的模式,这里出现了各种各样的问题,比如说,如何维护数据,随着数据的变化,以及如何最大限度地减少冗余,见,等等,这些都是我们以后会讨论的话题。
所有的权利,我这边就这样,嗯,有什么问题吗,当我当我们切换到阿尔文,我想可能还有问题,好的,我会的,我会回答他们的,嘿,让我。
好的,对不起,让我确保我得到了正确的切片。
好的,所以呃,在剩下的12分钟里,我们只想给,简要概述下一节课的内容,我们刚刚讨论完执行SQL查询,所以你们现在应该都是这方面的专家了,下面是我们将要讨论的其余主题的课程概述。所以我们刚刚讨论完第一单元,这只是一个改造,然后呃,编写SQL查询,我们将从数据库引擎本身的细节开始,通过首先讨论事物实际上是如何存储在数据库中的,在文件组织和记录方面,诸如此类,然后我们要往上走。可以这么说,呃,通过讨论查询实际上是如何在文件系统顶部执行的,查询实际上是如何优化的,等等,然后我们实际上进入了更多的R主题,如何并发运行查询,我们如何从失败中恢复过来。以及我们如何真正设计数据库应用程序,所以这将在本学期剩下的时间里存在,好吧那么嗯,就在我第一次谈论,呃,数据库管理系统的体系结构,所以你现在已经玩过SQL Light了,稍后。
您实际上正在自己构建数据库系统的一部分,因此,首先讨论数据库管理系统中的组件实际上是有意义的,首先,所以这里有点像一个图片插图,在这些应用程序中通常会发生什么,所以我们首先有一个用户,嗯。他基本上是在对续集客户端发出查询,例如,可以成为你的续集,呃,提示,所以您对提示符按Enter写查询,然后基本上只运行查询,所以SQL之类的基本上接收你的文本,然后运行查询,嗯,在文件系统上。您的数据实际上是在背诵,所以实际上放进这个大盒子里的是,这就是我们下几节课要讲的内容,所以首先你的查询,您的查询通过,它也得到了优化,所以你知道我们输入这段文字,然后它基本上被翻译成所谓的关系查询计划。所以到目前为止,别担心细节,或者你不明白这些没关系,就像你知道的,想想这个,编译查询的文本表示,变成某种图形或数据流形式,所以在这个例子中,我把它表示为一堆盒子,每个框基本上都是所谓的查询计划,呃。
查询执行器,计划操作员,抱歉作为查询运算符,所以不要再担心细节了,关于这个,好的,所以我们得到了一堆呃,查询运算符,在这种情况下,因为我们谈论的是关系数据库,所以我们有一堆关系查询运算符。这些运算符对文件系统执行,所以这就是数据的地方,例如存储,然后深入到细节,基本上就是一堆呃,页面缓冲区,或者你知道数据实际上是如何从磁盘检索到主存的,就呃而言,试图执行这些关系运算符。最后它会进入磁盘对吧,所以如果我们在主存中找不到数据,它还没有进入主存,然后我们试着进入磁盘,实际获取我们试图检索的数据,原来如此,我是说,所以这些有点像数据库管理系统的组件,你可以从这个图中看到。基本上是分层组织的,对吧,每一层都有这种抽象,把下面这一层,所以这基本上就是数据抽象的原理,你们从61 A学到的,它具有这样的特性,即您能够轻松地管理复杂性,您还可以轻松地对底层的性能进行推理。
这是我们作为好的系统设计而激发的,我们只是用关系数据模型作为一个例子,但实际上许多不同种类的数据管理系统,不管它们是否有关系,实际上组织得是否相似,所以它们都是分层的。然后层基本上从屏幕上看到的东西开始,然后对着这些层,两个不同的方面,这是一种横切,它们基本上穿过所有这些不同的层,其中之一是并发控制,意思是当多个用户试图针对,的,呃,对同一数据库发出不同的查询。另一个方面是当某件事失败时会发生什么,可能你的磁盘坏了,也许你的主存失败了,也许就像,你知道吗,呃,你的,你的程序崩溃了,那么在恢复方面会发生什么,所以这两个方面实际上跨越了所有这些不同的层,对呀。因为有些话要说对,假设这些不同层的恢复,所以我们也会谈到这两个,但首先让我们回到堆栈,然后试着从最底层开始,在试图理解事物是如何自下而上组织的方面,然后我们会讨论这两个不同的方面,好的。
让我们首先谈谈存储介质,就像在,就像你知道数据实际上驻留在哪里一样,对呀,所以你可能会说像磁盘,对呀,你说的磁盘是什么意思,嗯,现在实际上存在着许多不同类型的磁盘,到目前为止最常见的实际上是所谓的磁盘。或者像旋转的圆盘,所以这些基本上是这样的,你和我可能在我们的笔记本电脑和服务器上,嗯,它们是一种机械,呃,呃,呃,呃,历史文物,如果你愿意,因为它们已经发展了相当长的一段时间,呃现在。但我们现在还在谈论它们的原因是因为,就像这些机械磁盘的设计,实际上在硬件和软件方面影响了许多不同的新磁盘设计,所以这就是为什么我们想用它作为一个例子,就像你知道如何抽象一种方法,例如,对呀。由磁盘提供的服务,有什么影响,嗯,第一个是API,它被所有这些不同的存储介质公开,例如,他们中的许多人实际上,所有这些都只会暴露一些被称为read的东西,然后是其他被称为写对的东西。
所以这些基本上是API调用,你可以发布,存储单元通常是一个页面,每个存储介质将在大小方面对此页进行不同的定义,或者就它的含义而言,但为了本课的目的,你基本上可以把它看作是呃的呃的最后一个单元,储存的。所以每次我们只从磁盘中提取几页数据,我们只将几页数据写入磁盘,这基本上就是我们在这门课上要做的,你需要记住的是我们不能随意读写,所以你总是可以总是给你一个起点,但你基本上想做呃,从一个点依次开始读。然后在那之后继续顺序阅读,或者像你知道的,从写作的角度出发,然后按顺序写,而不是跳来跳去,我马上就会讨论这意味着什么,是啊,是啊,最后一件事就是你知道这两个都很慢,所以你们可能知道这样很慢,这是真的。好吧,就像你知道的那样,当然这些核心呼叫的感觉,你可以避免更好的权利,呃,查询执行效率,如果你真的想了解更多关于这些的信息,呃,就它们的实际设计而言,或者像你知道的,实际上进入它的硬件是什么,嗯。
选162是一个很好的课程可以教你所有这些不同的抽象,为什么它们是这样设计的,你甚至可以参加EE课程,如果你有兴趣学习如何自己建造一个,所以我在说,呃,具有不同类型的存储设备,对呀。所以这有点像典型的等级制度,当人们谈论存储时,你会看到,所以你从这样的东西开始,你知道,寄存器基本上位于CPU内部,所以实际上顶部的这三层,一直到缓存,实际上都坐在CPU里,例如,呃,与倒数三名相比。基本上就像独立的设备,对呀,所以RAM基本上是一块主存,你可以购买,UM和固态硬盘,SSD右,或者像这样的东西你可以在旁边买一个,正确并添加到您的系统中,所以它们在速度和大小上都有差异,对呀,所以嗯。例如,从CPU中的寄存器中获取一些东西只能让我们少到,就像5纳秒和你知道的,主存可能需要100纳秒,然后呃,如果要从磁盘读取兆字节,它实际上可以一直走到20毫秒,所以这实际上是一个巨大的时差。
就试图访问数据而言,那么人们实际上使用这些层次结构是为了什么呢,对于公羊,右或主存,您可能想,就像,你知道的,您将,基本上,呃,在那里经常提到,如果可能的话对,所以人们基本上把习惯,呃。那里经常使用的数据,相对于,呃,同样适用于闪存盘,因为他们往往更快,然后对于像磁性或分裂盘,所以你基本上试着把次要的东西,或者喜欢,你知道吗,备份或锁,呃,如果可能的话,但当然,现在像你这样的大数据。对呀,不是所有的东西都能装进你的公羊里,愿他们可以,它们甚至可能不适合你的固体磁盘驱动器,所以在这种情况下,你必须,我们必须一直到磁盘,在储存东西方面是正确的,事实上,如果你的老学校是对的。你甚至可能听说过一种叫做,呃,磁带右,所以这些实际上是像,你知道的,有点像盒式磁带对吧,除了他们有点像,你知道大一千倍,我想在大小上它们实际上仍然存在,嗯,人们仍然使用这个权利。
但当你试图访问存储在磁带上的东西时,你真的需要给某人打电话,去拿带子可能是对的,就像,你知道一些,一些图书馆或一些地方,然后把它放进某种磁带读取设备里,在你真正读取数据之前,所以说到时间,对呀。所以对于胶带来说,我们可以谈论像,你知道的,几天后你就可以像一兆字节的大象一样阅读了,作为比较的权利,所以这只是给你一个规模感,就像我们正在谈论的对吧,从点五纳秒到几天,如果不是几周,好的。所以这里就像一点经济学,两年前为了一千美元,你可以买到40TB的磁盘存储,而你只能得到80G的主存,所以这也给了你一种重要的感觉对吧,所以人们基本上你基本上得到报酬,您基本上为检索数据所需的时间付费。所以这就是为什么你知道,在这种情况下,如果我们保持这个量,美元不变,以下是你得到的存储大小,好的,所以嗯,我只是很好,简短地谈谈这个,然后我们将详细讨论,呃,下周在组件方面,所以就像你知道给你一张照片。
这里就像一张光盘的图片卡通插图,嗯磁盘实际上看起来我们从拼盘开始,基本上就是这些盘子,看起来事情是对的,所以这些磁盘中的每一个实际上都有这些字母,实际上有一堆,然后呃,他们实际上有,我们也有,呃。让我们看看什么是所谓的手臂组件,基本上是一个,呃,你可以把它想象成一顶芦苇帽,对呀,所以我有点喜欢想象他们在这里,把他们想象成有点像,呃,旋转圆盘,你知道,听音乐,我是说就像,你知道。我想有些人可能习惯于听音乐,虽然呃,用它们之类的,你知道的,这就像以前的CD对吧,很明显,所以这基本上是有效的,同样的方法,所以我们有这只猫,它们基本上延伸到我们的每一个盘子上,然后我们然后我们。我们就是这样读取数据的,然后就像概念上的轨道就在轨道上,就在每个圆盘头的下面,基本上组成了一个圆柱体,所以我只是在这里图形化地说明,我是说他们并不存在,但你可以把这看作是一个圆形区域。
我们将从右边读取数据,因为那是磁盘的地方,呃目前居住,嗯,就像你知道的,任何时候只有一个头可以阅读,对呀,所以我们不能违背我们所有的折扣,其中只有一个可以活动,然后实际的碎片。我们从磁盘上读出的实际弧线称为扇区,然后就像你知道的,很明显,每个扇区基本上都有固定的规模,因为你知道我们不能一直把数据从磁盘上移走,呃从折扣,所以基本上磁头会从每个扇区缓冲一定数量的数据。然后基本上把它传递给CPU和你的系统,所以我们在这里定义这个较小的单元,就像磁盘扇区,但这只适用于,呃,旋转圆盘,所以这就是为什么这是一个特定的术语,我们在这门课上谈论的更常见的事情是。基本上我之前说过的块或页面大小,它基本上由多个这样的练习组成,好的,所以我就到此为止,其实,所以我们刚刚讨论完磁盘的结构,下次我会试着给你们看一些照片,我们如何从它中读出东西。
当我们试图从磁盘上读取东西时,实际上会发生什么,所以请在这里闲逛,如果你有任何问题,否则我们星期二见,度过一个愉快的长周末,每个人,如果你要去什么地方,但要确保你是安全的,也只是为了验证。当我们说像你这样的续集,只是一组查询,我们存储在视图中,所以我思考一个观点的方式基本上就是这种表达,这只是一个续集查询,这是一个方便,允许我引用续集查询,如果你不想想结果。这只是我思考SQL查询的一种方式,然后在后续查询中引用该SQL查询,而不必将其展开为更大的查询,我明白了,谢谢。我应该暂停录制还是停止录制另一个。
对不起。
P4:讲座 4 磁盘、缓冲区、文件 I(续)+ 磁盘、缓冲区、文件 II - main - BV1cL411t7Fz
186号。所以,首先有一些公告。第一,第二次作业将会在讲座结束后发布,截止时间是下周四。
第一个项目将在本周四截止。我希望你们都在享受、玩得开心并且写出优秀的 SQL 查询。今晚在太平洋时间7点到9点之间,还会有一个项目答疑时间和聚会。所以,项目聚会实际上是我们在助教网站上尝试的新形式。我们鼓励你们相互合作,并且寻找项目合作伙伴。
希望这个方法可行。如果你们对这个功能有任何反馈,也请告诉我们,因为这是一个相对新的尝试。正如你们所知,我们会在每周初发布每周公告,确保大家能够跟上进度,了解事项。
提到了截止日期等等,请确保在课堂之外也查看这些内容,除了课堂上的公告。好了,回到材料部分。上周我们在讨论数据库管理系统中的不同组件。对吧。所以我们采用了这种分层方法,展示了所有这些不同的。
从顶部到底部解析输入查询,一直到管理文件系统中的磁盘缓冲区,数据实际上就存储在这里。所以我们从谈论数据库管理系统中最底层的组件开始。
基本上是在讲磁盘。然后,上周四我讲了这种你们可能已经熟悉的层次结构。所以我们从最快速的数据检索机制开始,通过将数据存储在与CPU本地相关的寄存器中,一直到最底层,甚至像传统的写入操作,虽然它可能是“过时的”写入方式。所以,检索数据所需的时间差异是巨大的。你可以看到这里的差距,从 0 开始。
从 5 纳秒到 20 毫秒,这中间的时间差非常大。所以这些不同层次之间的时间差非常显著。而且我还开始提到磁盘的不同组件,包括盘片和臂组件,或者说这些由磁盘头组成的臂组件。基本上,磁盘会旋转,盘片围绕它转动,而磁盘上标记的区域,或者说磁盘上的圆圈部分,就被称为。
圆柱体。现在,我们基本上在讲解如何从扇区中读取数据。所以基本上,当我们旋转到唱片上我们想要读取的特定区域时,然后将磁头降低,读取位于其下方的数据,然后扫过的区域就是扇区。如果这些概念对你来说有些陌生。
我想一个不错的类比其实是唱盘,可能你们中的一些人已经很熟悉了。所以在这个类比中,我们可以看到黑胶唱片就是我们的磁碟。这个组件也被称为臂组装,在它的最尖端,基本上是磁头。
其实这和我们现在所看到的完全相同。只不过可以想象,我们现在是3D的结构。所以我们有多个层次的这些磁碟,这就是你在前面图片中看到的。我们仍然有相同的磁碟,设置、臂组装、样本。
同样的磁头,除了在旧式的情况中,我们讲的是这些黑胶唱片,而不是磁碟,但其机制基本是相同的。那么我们到底是怎么读取的呢?其实我们必须像唱盘一样,旋转磁碟。
臂组装部分会移动到正确的位置,然后将磁头移到我们实际想要读取的位置,接着将磁头降低,基本上接触到唱片。所以在磁盘的情况也是一样的,在这里。基本上我们将其分解为三个不同的部分,根据读取这个区块所需的时间。首先,我们有寻道时间,正如我所说,这基本上是将臂移动到我们实际想要读取的位置。
然后是层的旋转,因为我们基本上需要等待磁碟旋转到我们实际想要读取的扇区,正如黑胶唱片的情况一样。最后是传输数据所需的时间,一旦我们将磁头降低,就像经典的唱盘一样。
现在,最后一部分,比如说在传输时间方面,我们其实无法控制,因为我们必须从磁碟上读取数据。我们无法控制任何事情,但前面提到的两部分是我们真正想要最小化的,因为它们严格来说是开销,我们不需要这些任何多余的操作。
但当然,事情并不总是按预期的那样发展。因此,这些是你在谈论磁盘读取时需要知道的基本组件。那么闪存呢?或者说固态硬盘呢?你们中的一些人可能已经在自己的笔记本电脑上看过这种硬盘了。所以这里的技术不同,首先它们不是按盘片的方式组织的,而是按单元格组织的,我在这里用红色标出了其中一个,这基本上就是固态硬盘上的一个单元格。
然后,在当前一代使用NAND闪存技术的设备中,它的不同之处在于我们可以进行随机的读取和写入。所以我们不再像旋转盘那样等待磁盘旋转到正确的位置,然后再将磁头放下,所有这些操作都已经不再需要了。
除了读取和写入实际上是不同粒度的这一点。我们可以对单个单元格进行非常细粒度的读取,但对于写入,实际上最好是一次写入多个单元格。这就像固态硬盘(SSD)的背景技术一样。还有一点你需要知道的是,读取虽然非常快,但写入也很快,因为我们不需要等待机械臂的移动,也没有机械装配,我们不在移动任何东西。
我们不需要等待东西旋转,这点很好,而且它也非常可预测,因为我们只是从任意一个单元格中检索数据,然后所有单元格基本上都有读取机制,所以我们不需要等待任何东西旋转。
这很好,但遗憾的是,从时间上来说其实并不是那么可预测。为什么呢?事实证明,我们在固态硬盘或闪存驱动器上每个单元格的写入次数是有限的。现在的典型数量大约是3000次左右,超过这个次数后它就会失败。失败的意思是不能再擦除它了,因为如果我们要向其中写入数据,首先必须擦除原来的数据,然后再写入新数据。
所以,事实证明我们不能无限次地做这个操作,只能做有限次数。当一个单元格“死掉”或无法再擦除时,我们实际上需要找到另一个可用的单元格来写入数据。这是一个被称为“均衡写入”的过程,正因为我们不能预测哪个单元格会失败,所以当我们尝试写入大量数据时,可能会跳来跳去。
在找到足够的存储位置之前,实际上会有很多单元被擦写。这就是为什么在一般情况下,对于固态硬盘来说,擦写次数并不那么可预测。有任何问题吗?我有一个问题。是的。那是不是意味着固态硬盘的使用寿命非常短,因为我猜测两千到三千年之类的,硬盘可能就会坏掉。非常短?非常短的使用寿命?就是说,如果我有一个固态硬盘,是否意味着它可能会在几年内就坏掉?
其实不是,因为你实际上有大量的存储单元,而且这些硬盘通常内置冗余机制。因此,通常当他们告诉你存储是比如说1TB的时候,实际上存储单元是比1TB数据所需的多一些,因为它们会有冗余。所以,但最终还是会坏掉,因为你知道,存储单元不能无限次地重写。
哦,酷。谢谢。是的。还有其他问题吗?嘿,擦除我们写入SSD上的信息会花费很多时间吗?这取决于我们讨论的技术,比如NAND GAKES。我的意思是,通常擦除的时间并不像磁盘那样长。而且在本课程中,你基本上不需要担心这个问题。
我的意思是,就像你知道的,你唯一需要记住的事情可能就是,它们的擦写时间可能会不同,尤其是写入时间,特别是写入时间可能是不可预测的。好的,谢谢。哦,是的,我有问题。是的。
Pansita。与机械硬盘相比,在相同的使用条件下,哪个会更耐用?这个问题很难回答,通常闪存驱动器如果你比较单个单元的话,它的衰退速度会比磁盘要快一些,如果你拿它与磁性硬盘做对比的话。但随着技术的进步,现在真的很难说,因为你知道我们每年都有更新的闪存技术。
而且现在有越来越多的冗余机制内置在其中,这样它们就可以容忍更多的错误。所以很难说,我想这就是底线。我明白了。谢谢。酷。好的。所以对于本课程而言,你只需要了解这些。就像我上次在星期四说的,如果你对这个感兴趣,实际上有很多课程你可以参加,既有计算机科学的,也有电气工程系的课程。
你实际上也可以自己设计内存,实际上有一些课程可以帮助你学习这些技术。好吧,像你知道的那样,对于我们来说,我们想讨论的是空间管理。所以现在你知道了这些技术的基本原理,我们该如何讨论如何管理这些空间呢?
嗯,我们首先谈论的是写入数据块,基本上是数据块的写入。正如你所想的那样,将顺序数据写入到相邻的位置通常是最快的。实际上,这有不同的原因。对于磁盘来说,原因是因为我们实际上在等待磁头旋转,就像想象一下唱片的转动一样。
再次提到黑胶唱片的例子,对吧?显然,我们可以读取相邻的数据,或者写入相邻的位置,这很好,因为那样我们就不需要等待磁头的移动,或者不需要等待磁碟的提升。
抬起,然后像你在适当的地方再次降低对吧?所以这就是为什么它很棒,然后对于右边的部分,我的意思是,类似的原因也适用于闪存磁盘上读取顺序数据。
因为顺序读取比起随机写入(例如写入闪存磁盘中非连续的位置)要更快,原因是正如我之前跟你们提到的,闪存有磨损均衡机制。
所以这些基本上是一些原理,那么人们通常怎么做呢?他们基本上会尝试预测行为。既然我们知道顺序读取和写入是比较好的,那么他们是怎么做到的呢?他们通过尝试缓存可能在不久的将来会被使用的内容来实现这一点,这就叫做预取。它们会尝试缓存那些频繁使用的数据,希望它们能够很快被重复使用,这就叫做缓存。
然后他们还会尝试缓存写入操作,确保即使我们最初似乎在写入到随机的块中,但如果我们收集足够多的数据块,最后可能会以顺序的方式写入,这是很好的,这叫做缓冲写入。
所以我们在实际上课时会看到一些这些技术。但现在,像你知道的那样,我们先尝试统一一下吧,因为现在有这么多不同的技术存在。我是说,让我们尽量想出一些共同的术语,以便我们能谈论这些技术。所以我们将使用“块”作为所有不同磁盘的基本传输单元,因此无论是磁性硬盘还是固态硬盘,或者其他类型的磁盘,都没关系。
你知道,典型的数字范围大概是64千字节,而你的书中提到的是4千字节,我觉得这有点随意。每种技术和每种设备都是不同的。现在我们只想要一些共同的术语,这样我们才能一起讨论这些问题。
然后,为了让自己更加困惑,人们还使用了“页面”这个术语,实际上它是块(block)的同义词。你可能会在不同的教科书中看到这种情况,所以要注意一下。有些教科书甚至把页面当作主内存的单位来使用,这样就更容易让人困惑了。
但是,我们会确保在不同的语境中使用这个术语时,语境是明确的。现在,我们把这两个术语视作同义词。好的,对于一个数据库管理系统来说,我们所说的磁盘空间管理,实际上就是我们管理所有可用于读写数据的磁盘块的方式。它将页面映射到磁盘上的实际物理位置。因此,我们向上层暴露的抽象就是基本的页面读写操作。
但是在实现中需要重点考虑,我们需要弄清楚到底应该在哪里写入磁盘,反之亦然,我们还需要能够通过查找具体的页面在磁盘上的位置来加载页面,然后根据需要加载到内存中。
如此等等,高级别的基本上使用这个接口来读取它们,写入一个页面,它们也可以利用这个接口来确定分配多少页面,等等等等。所以我们需要关注的是这一空间,这一空间的管理。
这里是一个常见的机制或媒介,我们会使用这个组件。例如,假设“sailors”是我们要读取的表的名称。我们实现或使用这种磁盘空间管理组件的方式是,首先从“sailors”表中的一个页面开始。假设是该表中的第一个页面,因为我们想要读取其中的所有数据。
我们得到一个页面后,基本上会进入一个循环,这个循环会遍历每个我们读取的页面。我们会进行一些处理,比如过滤或其他我们想要完成的查询。然后我们调用“下一页”来获取我们想要处理的下一页,直到处理完成。完成的标准可以是查询完成,或者所有数据已经写回到磁盘,等等。
这里需要注意的事情是,我们假设下一页(即这个调用)是快速的。这和我之前说的呼应,我们假设顺序写入页面对不同类型的设备是有效的。所以我们暴露给系统更高层的接口基本上是这样的,你知道,获取指针。比如说我们想要的第一页或任何其他页面,接着是一个接一个的下一页调用。
所以这是我们会看到的 API,这也是当人们尝试使用空间管理组件时常见的做法。这是有道理的。好,现在我们来谈谈如何实现。既然这是我们暴露给更高层的 API,那我们究竟该如何实现这个底层机制呢?
在这里我想提到两个建议。第一个是我们直接与存储设备进行交互。所以如果我们有一个磁盘驱动器,那么我们就去了解如何移动磁臂,如何控制旋转速度,以及何时降低读写头,然后转移数据,举个例子。
所以这很简单,很棒。事实上,如果你非常了解你的设备,那其实真的很不错。所以你可以实际实现你自己的设备驱动程序,专门为我们正在构建的数据库管理系统量身定制。这太棒了。然而,缺点是每种设备,每种类型的设备往往都有自己的 API,比如你在移动磁臂,或者在读取闪存驱动器上的一个单元,或者找到另一个位置来存储数据。
所以你基本上需要掌握所有这些不同的 API,并且高效地实现它们。那么如果我们往系统中加入新东西,会发生什么呢?所以现在我们基本上需要更改我们的磁盘管理组件。
所以这是一个缺点。当然,优点是,这种方式是高度专门化的,所以如果你知道如何操作,它可以带来非常好的性能。所以我们可以与操作系统进行交互。和操作系统交互的原因是操作系统已经为我们管理了文件系统。
从某种意义上来说,通过使用操作系统,我们把自己从这些低层设备驱动管理问题中隔离开了。所以,实际上我们是在绕过操作系统,我们不希望操作系统为我们直接管理文件。
因为操作系统可能实际上会在我们尝试处理查询时,写入文件的某些部分,或者读取或预取文件的不同部分。而且操作系统可能并不知道我们在某个给定的文件上想要执行什么样的查询或处理。
另一种实现这个管理系统的方案是基本上通过要求操作系统给我一个巨大的文件存储在空磁盘上,从而绕过操作系统。然后,我们暂时假装自己是这个文件的拥有者,直到剩余的时间结束。
所以只要我们在处理查询,我们就假装告诉操作系统,看,我的意思是,我们正在管理这个巨大的文件。不要碰它。我们不会把任何数据返回到磁盘,我们会通过调用操作系统接口明确地做到这一点。
就像你知道的,不要预取任何东西,我们会告诉你该怎么做,诸如此类。所以在某种意义上,我们通过进行自己的磁盘管理,实际上是劫持了操作系统。而且这可以非常好,因为现在我们从低级设备驱动程序的细节中抽象出来。我们基本上在做自己的块级管理,页面级管理,如果你愿意的话,就像是文件管理。
所以这就是另一种实现磁盘空间管理系统的方式。而且,你知道,这个文件可能跨越多个磁盘甚至机器,只要操作系统提供了接口来为这样的文件分配空间。
然后我们就告诉操作系统,闭嘴,然后我们就自己管理整个事情。总的来说,我们刚才讨论的就是一些非常简单的技术,涉及到磁带和磁性磁盘。
然后我开始讨论做不同事情的成本问题。所以需要记住的一点是,访问磁盘的成本相较于从寄存器本地读取数据到CPU中可能非常高。
我们还简要讨论了磁盘存储管理器可以暴露的不同接口,并简要介绍了如何实现这些接口。然后就这些。现在我将切换到其他内容。如果你有任何问题,请随时举手。
非常清楚。
是的,读取顺序很重要,是的。好,大家能听到我吗?好,行。那么,接下来我们继续之前的内容。我们正在讨论现在我们已经知道底层发生了什么,基本上在关系中,数据可能存储在多个由文件系统管理的文件中。
从数据库的角度来看,考虑到这种抽象,你可以想象以下几种划分方式:每个表或每个关系存储在一个或多个操作系统文件中。每个文件可能包含多个页面。我们之前讨论过页面和块是类似的,所以每个文件会包含许多数据页。
每个页面包含许多记录。好吧,因此,“记录”是元组(tuple)的同义词。因此,每个页面将包含许多这样的记录。因此,这种页面或块的概念基本上是数据库系统多个层次之间共享的“通用货币”。
在磁盘上,它由磁盘空间管理器进行管理,磁盘空间管理器负责将页面分配到磁盘上的文件。因此,这些页面会被读入或写入磁盘文件中。这些文件可以是固态硬盘(SSD)或传统硬盘(HDD)。在内存中,它由缓冲区管理器进行管理。没错。
所以,它由缓冲区管理器管理,数据库系统的更高层次则操作内存中的数据。为了操作数据,操作的是页面。你需要先将页面加载到内存中才能操作它。因此,缓冲区管理器的角色就是将页面加载进来。一旦页面进入缓冲区,
它可以操作这些内容。好吧,接下来,让我们讨论一下如何在给定文件的不同页面之间布局给定的关系。好吧,现在我们先讨论单一表格,所以我们先不担心多个表格,多个表格的相同概念也适用。所以现在讨论一个单一表格。
我们将讨论数据库文件的概念,这是一个抽象,表示单个表格的信息。
在这里我们讨论的是一个集合,这个文件包含一组页面或块,每个页面或块包含一组记录。因此,对于数据库系统的更高层次,它们基本上可以使用更高层次的抽象来引用页面上的信息,而不需要担心页面具体的位置。例如,可以通过引用所谓的记录ID来获取特定记录,记录ID是一个指针,编码了包含信息的配对。
它的标识符是页面ID以及页面中的位置。我们可能需要的另一个重要API调用是扫描所有记录。在扫描所有记录时,你可能还会对需要检索的记录施加一些条件。
所以,你想要获取所有GPA大于3.5的学生,对吧?这可能就是你在检索记录时可能施加的条件。这是读取API,然后你会有更新API,你可能需要插入、删除或修改记录。我们现在只讨论一个单一表格,一个表格可能跨越多个操作系统文件,实际上甚至可能跨越多个机器。
但现在不必担心这些细节,我们先谈论一个包含多个页面记录的单一文件。在深入探讨这些页面如何布局之前,有多种不同的数据库文件结构,因此给定关系的信息可以通过多种不同的方式存储。我们现在将主要讨论一种方式——无序堆文件。
无序堆文件基本上没有对记录在页面上的放置位置做出实际约束,它是无序的。再说一遍,记住关系是一个多重集合,所以这种匹配有点类似于多重集合的抽象。如果你想提高访问效率,你可以例如基于某些有意义的方式对数据进行聚类,使得记录和页面以某种有意义的方式分组。
或者你也可以更严格一些,根据某个属性进行排序。例如,你可以根据Calnet ID对学生记录进行排序。除了存储记录本身之外,你还可以有索引文件,这些通常是辅助结构,允许你更高效地检索记录或页面。
所以它们在某些情况下可能包含记录本身,或者指向其他文件中的记录。好了,现在我们不必担心这些其他三种聚类、排序和索引文件的方式,你只需要关注无序堆文件。
我们将在下一节课回到索引的内容。好的,那么文件顺序的问题,基本上我们想要的抽象是无特定顺序的记录集合。这与您在算法课上可能学到的深度数据结构不同,我们这里只是为了确保高效的最大值和最小值操作,并在插入和删除时保持这些最大值和最小值,这并不是我们现在要讨论的内容。
所以不要混淆这两者。基本上,这是一个无特定顺序的记录集合。在这样的文件中,随着文件的扩展,页面的分配也会发生变化,因此在新增或删除记录时,可能会增加或删除更多的页面。
因此,为了支持在关系中发生的各种操作,我们需要做几件事。我们需要捕获给定文件中所有页面的位置。我们需要捕获每个页面中存在的空闲信息量,以便确定是否插入新记录。我们还需要跟踪页面中的记录。
那么我们从最高层次的抽象开始。我们来谈谈文件中的页面。一个堆文件,假设我们将其实现为一个列表。为了将堆文件实现为列表,我们可以考虑有一个特殊的头页,它基本上是指向列表的起始指针。然后这个头页是堆文件的位置,也就是说,整个文件的位置。
以及页头页面在文件中的位置,可以作为所谓的元数据存储在某个地方。所以这可以作为关于数据的数据存储在系统目录中。数据库系统目录就是存储有关你数据的数据的地方。例如,你可能存储关于各种关系的模式信息,意味着存储信息。
例如,像这样的事情。那么,一个页面是如何工作的呢?在给定的关系中,哪些文件被引用,页头页面存在的位置在哪里?例如,明白吧。所以,这可能是存储此页头页面在文件中位置的一个可能位置。好的,所以页头页面基本上只是告诉你这个链表的起始位置,然后你有两个独立的链表。
一条已满页面的链表。所以这是这条链表,另外一条链表,是那些有空闲空间的页面。好的,再次强调,你可能需要前后指针,这样当页面被添加或删除时,你可以正确地维护这些指针。
所以这个页头页面有指针,抱歉,指向已满页面列表的开始以及空闲页面列表的开始。
每个页面,每个数据页都有指向列表中下一个页面以及前一个页面的指针,在这种情况下,第一个页面指向页头。那么,为什么我要将这些分成已满页面和空闲页面呢?好处在于,如果我想插入一些新记录。
我只需要查看这些页面,而不是这些页面,对吧,这样我就不需要查看这些已满页面,因为我知道我无法在其中容纳新记录。我只能将其放入那些有空余空间的页面中。是的。问题。哦,对不起,你刚刚回答了我的问题,但我想另一个问题是,一旦空闲空间链表中的页面填满了会怎样?
你必须将其移动到已满页面吗?这会带来什么样的成本?对吧,嗯,关于成本我们还没有真正讨论,但我们可以从概念上讨论一下这会是什么样子。好吧,你必须,如果你有一个最初在空闲空间链表中的数据页面,想要将其移动到已满。
已满页面列表,你基本上需要确保指针指向正确的地方。对吧,例如,如果我想将这个页面移动到已满页面列表,我需要更新前一个页面和下一个页面的指针。一旦我找到一个合适的位置来放置它。
我可以把它放到这个列表的末尾,或者把它放到这个列表的开头。再次强调,我必须更新前后页面的指针,对吧?所以基本上我可以说,你需要更新四个页面及其指针的顺序,才能完成这个更改。
但我们来谈谈为什么这实际上不是一个很好的解决方案。好吧,为什么这不是一个很好的解决方案呢?嗯,如果我想找到一个页面,它有足够的空间来存储特定的记录。然后我只能按照这个空闲空间的页面列表一个一个查找,直到我找到一个有足够空间来存储我的记录的页面。而且没有其他方法可以做这个,对吧?所以这基本上是我如果采用这个简单的链表实现时唯一的做法。
所以,这就是这种方法的缺点。那么,我该如何思考这个问题呢?一个缺点是,我有点是在查看包含数据的页面,以确定是否有空闲空间,但我可以将关于哪些页面有空闲空间的信息,提取到一个单独的页面中,这个页面叫做页面目录。好吧。因此,这个页面目录可以包含多个这样的头页面。
每个页面编码指向给定页面的指针以及该页面中存在的空闲空间。因此,它编码的是指针和空间的配对。所以这个目录可以有多个这样的头页面,每个页面基本上包含这些指针。好吧,指针和空间信息。现在,这么做的好处是,你可以在单个头页面中打包关于许多页面的信息。
因为你并没有在这些头页面中存储任何数据,只是存储关于页面的信息,指向页面的指针以及页面中的空间。所以可以有多个这样的头页面,这些头页面实际上访问得相当频繁,因此它们可能会比其他页面更常出现在缓存中,所以它们会在内存中。而找到一个合适的页面来存放记录所需的页面加载次数,比在链表中要少得多。
记得我刚才提到的链表吗?我必须跟随指针链,直到找到一个有足够空间的页面。现在,我有一个头页面,它可以显示多个页面的空闲空间。所以它基本上包含了更多页面的信息,但我并没有在这些头页面中存储任何数据。
我只是在存储一堆页面的空闲空间量。实际上,你可以将这个思路推得更远,通过以一种更“优化”的方式来组织页面目录,从而尝试进行压缩。你可以根据每个页面中的空闲空间量来排序指针。但所有这些方法并不一定能提供比使用页面目录和头页面这一简单方法更多的附加收益。
所以页面目录和头页通常足以满足我们的需求。好的。所以这里的最高抽象层次是一个表格,它被编码为文件,文件是页面的集合。然后,页面目录提供了页面的位置以及每个页面中的空闲空间,以便我们基本上可以添加新页面。所以如果有新的记录,我们可以将它们添加到这些页面中。接下来我们来谈谈页面布局。好的。
为了讨论页面布局,我需要说明如何在有限的屏幕空间中展示页面。内存或磁盘上的数据是按线性顺序存储的。你可以记住,当你从磁盘读取数据时,它是按顺序读取的,沿着圆周的方向,也就是磁道或者扇区。就是这样读取的。现在,这些信息不一定能很好地适配屏幕,因为我无法横向展示,所以我的思路是将这种线性顺序转化为矩形的形式,基本上是这样展示的。
然后我将回到这一点,依此类推。好的,这只是一个约定,用来表示这些页面,而不用让我跳出页面(跳出幻灯片)的情况。
好的,我们来谈谈页面的样子。页面的第一部分是我们需要考虑的头部。头部是关于编码页面内容的信息。再次强调,这是元数据,因为它是关于数据的数据,所以它可能包含各种信息,比如页面中记录的数量和空闲空间的大小。你可能有下一个或上一个指针,或者你可能有指针、位图和槽表,我们将讨论这些是否需要,以及哪些需要。
但我首先想说的是,我们可能会有一个头部,就像我们在页面目录中有头页那样,指向页面,我们也可能有一个页面头部,它编码了页面中记录的信息。在这个意义上,随着你逐步深入,你仍然保持相同的结构,即有一个头部部分,编码着关于数据的元数据。好的,所以我们有一些决策要做,第一项决策是我们是否有固定长度记录或可变长度记录。
我们已经更新过你,但有方法将可变长度字段编码为固定长度字段,稍后我们会讨论。但现在我们来谈谈你是拥有固定长度记录还是可变长度记录,这是一个可能的决策,并且已经提供给你了。例如。
如果你没有任何字符串类型,那就是整数和浮点数。这是一个固定长度的记录,因为你知道每个字段所需的存储量。另一方面,如果你有很多字符串,它将是可变长度的记录,特别是如果你使用VARCAER,这是一种我们在讨论数据定义语言时学到的语法。
好的,这就是第一个决策,即记录长度。第二个决策是我们是否想使用打包布局,也就是记录紧密排列在一起,还是记录之间有空闲空间。这个就是第二个决策,打包方式。我们需要考虑的因素是,我们希望能够根据记录ID找到记录,而记录ID被编码为页面ID和页面内位置,我们会讨论这些页面结构中每种位置的含义。
我们需要确保能够高效地添加和删除记录,并考虑到每个操作所涉及的开销。好的,那么让我们考虑第一种替代方案。第一种替代方案是将记录密集打包。对,这适用于固定长度的记录,相对于可变长度的记录,这实际上是你能拥有的最简单的方案。所以,如果你将记录密集打包,你基本上会有这个页面头部,然后是第一条记录,第二条记录,第三条记录,第四条记录,依此类推。
它只是按顺序排列。那么,记录ID是什么样的呢?记录ID基本上被编码为页面ID和页面位置的两部分。这里页面的位置基本上可以通过页面中与记录编号对应的信息完全捕获。
一旦我们有了记录编号,我们实际上可以通过简单的算术运算确定记录在页面中的确切位置。对,通过使用从页面开始的偏移量,可以计算出该记录在页面中的位置。
基本上就是页头大小加上记录大小乘以n减去1,如果我试图找到页面中的第n条记录。例如,第一条记录从这里开始,所以基本上是页头加记录大小乘以1减去1,结果是0。
对, 所以这只是一个标题,第二条记录会从这里开始,抱歉,第二条记录会从这里开始,第三条记录从这里开始,依此类推。通过简单的算术可以得出这一点。好的,那么我们如何处理添加和删除呢?嗯,添加很简单。如果我们有空闲空间,我们只需将记录追加到页面末尾。
如果有空闲空间,我们可以直接在页面末尾添加新记录。删除则有点挑战性。假设我们删除第二页。所以这是第二页。第二页的第三条记录,意味着我要删除这条记录。好的,所以我现在可以删除这条记录并释放空间。由于这是一个密集布局。
我需要重新组织。好的,所以这个记录是页面二记录四,现在需要移动回并变成页面二记录三,对吧?因为这是一个打包表示。所以打包表示意味着我需要重新排列原本我所说的页面二记录四,现在需要更新为页面二记录三。所以,这样的缺点是,如果我把页面二记录四作为该记录的标识符,并且我在其他文件中引用它。
现在我需要更新这个。所以这并不是理想的,对吧?所以如果那些信息在其他文件中,我也需要去更新那些文件中的信息。好的。那就是这种打包表示法的一个缺点。现在我们来谈谈解包表示法,看看它是否能缓解一些这些问题。
但在此之前,我有任何问题吗?Shiro。是的。我只是想确认一下,我们讨论的这个页面是否与内存中的实际页面有关。那里的确有页面表和其他一些东西。没有。所以,基本上我们讨论的是你从磁盘上读取和写入信息时的粒度。
对,所以,这就是页面,它是上层数据库系统和下层磁盘管理器之间的共同货币。所以,它基本上是你将要使用的交换单位。好的。
所以它和操作系统中的页面是一样的,对吧?是的,是的,好的,视情况而定,但肯定是。好的,谢谢。所以你说当我们使用堆时,它会检查所有页面并执行一个非常低效的检查过程,对吗?
但这是否意味着它也是一种非常低效的存储结构,尤其是在空间上,因为我可以想象一种情况,页面并没有完全填满,但由于没有足够的空间,它就无法继续写入。
但如果我们以不同的方式重新排列数据,就像你之前提到的打包问题一样。它可以适配。那么这是否意味着他在存储东西方面也很低效呢?那么,你是在讨论堆文件吗?你是指有一个头文件,然后被分成两部分,其中有空闲页面和已满页面,空闲页面看起来就像是存储东西的一种低效方式,是吗?
空间,我能在脑海中想象出一种情况,会有很多空间我们不会使用,原本我们可以以不同的方式分配这些空间。是的,所以你可能会从定期重新组织空闲页面中的信息中受益,特别是如果有大量删除操作。这样你就可以。
你可能会从中获得一些好处,我们接下来会讨论一些碎片整理问题和碎片化问题,所以希望我们能回到这个问题。我不认为这是一个仅仅影响这个特定方案的问题,我认为它同样影响页面目录方案。
所以,如果你有很多数据,例如,如果你添加了很多记录,并且分配了很多页面,然后删除了其中一半记录,那么你现在有,假设是其中一半页面为空。你最初分配的每个页面现在有一半是空的。
或者说,这些页面中的大约一半记录现在已被删除。你可以将它们压缩成只有一半数量的页面,但你不能这样做,除非你决定进行碎片整理,对吧?所以,这个方案对于这个情况并不是独特的。
链表方案,我认为这个方案也会受到影响。页面目录方案也会受到影响。太棒了,非常感谢。是的。我有一个关于延迟的问题,正如你之前提到的。如果我们将页面延迟到记录3,那么我们将重新排列页面为记录4,对吧?
那么像页面到记录5这样的记录怎么办,因为现在我们已经安排好了记录,现在帮我们安排记录5。我们是否也需要将记录文件回滚,以便成为记录5呢?是的,这是我的问题。是的,基本上,如果你最终删除了一个记录。
所以在该页面中,所有后续记录的记录ID需要更新,对吧?因此,任何存储在其他页面并引用该记录ID的信息也需要更新,所以你说得对。所以基本上,不仅是记录文件6的记录,页面中的其他记录也需要更新。好的,谢谢你,Felix。嘿,Jerry。
这个问题部分得到了回答,但考虑到删除的情况,是否有任何因素阻止我们?例如,将该区域标记为无效,或者页面布局是否要求记录必须是有效记录且必须连续?好的。所以你提到的基本概念是我接下来要讲的内容,先记住这个想法。
谢谢。好的,所有问题就这样吧。如果聊天中有任何需要回答的问题,请举手。我没有非常密切关注聊天。好的。那么,跟进Felix的建议,就是基本上指示记录已经被删除。我们通过指示该记录区域已经被删除来实现。
我们不一定需要将其他记录向上移动,从而避免更新所有这些记录的记录 ID。因此,使用这个思路,我们如何实际实现呢?一种实现方法是使用位图,它编码了页面中记录是否存在。再次强调。
我们正在讨论固定长度记录,无论该记录是否有效。因此,这个位图就是在编码这一点。位图表示这些插槽,每个插槽对应一条记录。记录 ID 就是对应于页面 ID 位置和页面内位置。这里的页面将讨论插槽 ID。所以插入很简单。我们找到位图中的第一个空插槽。
然后我们在这里插入一条记录。删除也很简单。我们只需清除那个位。因此,在这种情况下,如果我想删除第三条记录,我只需要清除这个位。现在,我已经删除了记录。这样做的好处是,这些记录(记录四、记录五、记录六、记录七等等)不需要移动。它们的记录 ID 也不需要更新。
因此,不需要重新组织。它基本上只是表示记录是否有效。你可以通过一个非常高效的位图来表示这一点。位图只是一个零一的表示,可以极其紧凑地存储。好的,这是固定长度记录的解包表示。现在我们来谈谈可变长度记录。那么,可变长度记录会发生什么呢?
你还需要了解记录的长度。因此,我们将尝试弄清楚如何编码可变长度的记录。我们已经看到,固定长度记录的情况非常好。现在,我们将考虑解包的情况。那么,这里是我们需要关注的几个考虑因素。
好的,我们需要弄清楚每条记录的开始位置。我们还需要弄清楚每条记录的长度。我们还需要弄清楚在添加和删除记录时会发生什么情况。好的,为了弄清楚如何处理可变长度记录,我们要做的第一件事是将元数据,也就是头部,移到尾部。好的,我们将它移到末尾。稍后我们会谈到为什么这么做是有道理的。然后我们会有插槽。好的,这是一个插槽目录。插槽目录中的第一个插槽只是一个指向空闲空间起始位置的指针。
所以它基本上是在说,从此位置开始,你有了空闲空间。好的,这就是蓝色箭头。然后你有一堆插槽。所以这是记录 ID 一、记录 ID 二、记录 ID 三、记录 ID 四。好的,这就是第二页的记录四。例如,每个插槽编码了两位信息,它编码了记录的大小。
它编码了指向记录开始位置的指针。这里记录四从这一点开始,然后是,假设,12个字节。好的,两个信息,长度和指向记录开始的指针。你将这些记录ID按倒序存储。所以它在这个方向上递增。
所以这是一个,二,三,四。所以记录ID实际上就是长度和位置,来自右侧的槽表。我们来谈谈删除。假设我要删除页面上的第四条记录。我只需要删除该特定槽中的信息。所以就是这个槽。
然后我把这个记录的零清除掉。例如,我不需要做什么。我将这个槽的指针设置为“现在”。所以它不再指向任何地方了。这个删除方法的好处是,我不需要更新其他记录的指针。不需要进行任何内部重组。如果我删除一条记录。
我不需要移动其他记录的位置。现在我是否需要更新任何其他外部指针?答案是一样的。我可以以相同的方式引用记录ID,而不必担心这些删除。因此,在我删除这个记录ID之后,记录ID五仍然会保持为记录ID五。好的。
那么记录页面二,记录五。好的,如何进行插入呢?一种方法是将其直接放置在这个自由空间的末尾或开始处。所以记住,自由空间的起始位置由这个蓝色箭头指示。所以我可以把记录放在这里。我需要做的第二件事是找到一个槽。
这里我有一个空槽,然后添加一个指针指向该记录的起始位置。同时编码该记录的长度。所以我在我的槽目录中的下一个空槽里创建一个指针-链接对。接着,我还需要更新我的自由空间指针。对吧?所以之前这里的蓝色箭头现在需要指向谁?
好的。
对不起,我不想走这一步。
好的。好的,那么我需要更新蓝色箭头,之前是我的自由空间起始指针,现在它需要指向我刚刚添加的这条新记录的末尾。所以这就是为什么我需要更新这个自由空间指针。正如我之前提到的,对于这个刚填充的槽,我需要提供指向该记录开始位置的指针,并填写该记录的长度。
现在一个问题是,如果你删除了一些记录,同时你不断地在末尾添加记录。基本上在自由空间的开始处,你可能会有很多之前已被删除的记录,它们仍然占用着该页面上的空间。那么我们该如何处理这个问题呢?这是一个碎片化问题。解决这个碎片化问题的一种方法是基本上识别页面上的数据。
对,你基本上是把这个占据了该空间的记录移动到这个位置。如果那个位置有空间的话。如果我把这个记录移动到那个位置,我需要更新槽位目录中该记录的指针,并更新空闲空间的起始位置。好,这就是我如何移动这个记录并基本上解决碎片化问题的方法。
现在,这种移动实际上是安全的,因为它只会影响这个特定的槽位。它只影响这个槽位。任何其他槽位都没问题,因为记录的ID至少在外部是引用页面号和槽位的,而槽位并没有改变。因此,这不会影响任何外部文件。
那么现在有一个问题是我应该什么时候进行重新组织?对吧?一种选择是我在每次删除时进行重新组织和基本的碎片整理。或者,我可以等到碎片化严重到无法再添加其他记录时再进行重新组织。因此,通常这种懒散的方法是首选。
尤其是当页面没有太多新记录时。因此,这种懒惰的方法——只在绝对需要时才进行操作——通常是首选的。这样做比在每次删除时进行重新组织要好,因为后者非常激进,且通常没有必要,尤其是当页面没有更多记录时。那么,如果我们有时需要更多槽位呢?当你尝试插入一条记录,而你只有五个槽位时,我们该如何添加这些新的槽位呢?我们来讨论一下。
为了跟踪这些槽位,我们需要在页脚中添加额外的元数据。第一项信息是我们可以在槽位目录中存储槽位的数量。在这里,我们存储了五,因为有五个槽位。我们还能够根据槽位的数量以及每个槽位中存储的内容,判断它们是空的还是满的。
对。如果所有槽位基本上都被填满,也就是说每个槽位都已满,且你需要添加更多内容时,你就扩展槽位目录。对吧?所以在这种情况下,我决定扩展槽位目录,因为我添加了一个额外的记录。我原本有五个槽位,现在想再添加一个。所以我又添加了一个额外的记录。再一次,像往常一样。
我指向记录的开始位置,然后我编码记录的字节数,例如16字节。所以这些信息存储在我的槽位目录中。我更新了空闲空间指针的起始位置,因此所有这些都已经更新。最后我需要更新的是槽位数量信息,我可以更新它并增加六。
所以,要扩展插槽目录其实很简单。插槽从页面的末端向内增长,而记录从页面的开头向内增长。一旦它们在中间相遇,就意味着你不能再往页面中添加任何信息了。所以,记录向下增长,插槽向上增长,允许它们都能有一定的扩展空间。对吧?
所以,如果插槽比记录多得多,或者记录比较小,那么插槽目录无法获得更多的空间。如果记录相对较大,并且记录数量较多,那么记录就会占据更多的空间。基本上,你有两个方向的增长,这样可以让你在记录方面和插槽方面都能有一定的扩展空间。
好的,有个问题。是的,重新来过,问问题时要问对。我的问题是关于我们之前提到的如何识别插槽的。所以,当我们在做类似固定镜头插槽的事情时,如果某个插槽是空的,我们如何跟踪哪些插槽是空的?是通过一个大地图吗?或者是什么呢?嗯,你可以,例如,使用位图来编码哪些插槽是空的,或者另一种稍微粗糙一点的方法是直接检查该插槽的指针,查看插槽的指针现在是什么。如果插槽的指针现在是空的,那么你就可以确定该插槽是空的。
对,所以位图方法是一个完全可行的方法,正如你所说的,或者你也可以直接遍历插槽列表进行检查。好的,谢谢。那么,很多这些策略的权衡和成本效益似乎都与内存分配器非常相似。你认为这是否像在页面上分配记录与在内存页面上分配大量内存之间的差异?还是这个说法忽略了一些在这两种情况下不同的权衡呢?
我认为这是一个公平的说法,而且它是有道理的。其实这并不奇怪,因为在某种意义上,数据库页面的一个显著特点是,我们在磁盘上使用的布局与内存中的布局是相同的。对吧?因此,相同的信息在内存中的编码方式与在磁盘上的编码方式是一样的。所以,从某种意义上来说,相同的考虑因素也适用。所以,考虑内存分配的方式在这里非常适用,因为我们无论是在内存中还是在磁盘中,都是以相同的方式处理这些信息。
这样做的好处是我们不需要支付任何序列化和反序列化的成本,正如我稍后提到的,信息在磁盘上的编码方式与在内存中的编码方式是分开的。所以,正如我所说,把页面看作一个共同的单元,并且在内存中就像在磁盘上一样处理它,这是一种相当贴切的隐喻。
就像你提到的,正因为如此,在内存中页面的处理方式就像它们在磁盘中一样,所以内存分配器也类似。这是一个合理的比较点,因为碎片化、碎片整理以及垃圾回收等问题都适用。
太好了,谢谢!Felix,还有其他问题吗?在这个模型中还有其他问题吗?因为我们只是做了一个假设,就像我们假设页脚只包含插槽目录、自由堆栈指针和插槽目录计数器,因为在图片中它看起来像是覆盖了某些东西。
就像是没有敏感数据被放在这些插槽的左边。然后我没有跟上。你能再重复一遍吗?哦,所以在这个模型中,我们页脚里唯一的东西是插槽目录、插槽的自由堆栈指针和插槽目录计数器。因为就像在图片中你可以看到,它有点像是向左移动了。
那我们没问题吧,是否还有敏感数据留下来?所以,我的意思是,事情是这样的,我并没有指定我的记录和页脚从哪里开始,而这是这种方案的一个优点。所以这些都会自动捕捉到。我不需要为记录预分配这么多空间,只需为页脚预分配一定的空间。然后,随着我们添加或删除记录,这个空间会动态增长。所以,这就是这个方案的特点。
所以没有预先分配的区域,我会说这就是页脚,所以没有任何东西。尽管在我的图片中看起来像是16覆盖了某些东西,但实际上并没有这样的事情。我的意思是,16是在一个之前是空闲的区域分配的,否则它就不会被分配,否则如果没有空间,你也不会在这里添加额外的记录。所以我会这样理解这个问题,这样理解对吗?嗯,明白了,这帮助很大。
我想这与我的问题是相关的。是不是有什么,我们需要做的事情,比如说,处理页脚空间不足的情况,或者在插槽中的记录空间不足时?我们是否有任何类似的预期?所以,我们会确保页脚永远不会与记录冲突,因此我们永远不会在存在这种冲突风险的情况下插入记录。
如果我们遇到一个想插入记录的情况,页面上没有剩余的空闲空间,我们可能会根据空指针的数量考虑重新组织页面,也就是基本上进行页面碎片整理。所以,确保所有的空闲空间都位于记录列表的末尾,而不是中间。
另外,如果我们有一堆非常小的记录,我们填满了页脚的空间。那页脚能扩展吗?是的,这将是一个非常特殊的情况,因为很少有记录会小到让页脚比记录还要大,但当然,页脚是可以扩展的。对,页脚可以并且会扩展,随着新记录的加入,你会发现为记录分配的区域和为页脚分配的区域都会扩展。
因为你在添加额外的槽位和记录,它们都会扩展。对,谢谢。那么,我想我应该继续,因为时间不多了。好吧,这是我想说的关于槽位扩展的内容。我想我已经讲过这个了。所以,这是关于有槽位页面的总结。就是这样。
这个概念被称为有槽位的页面,因为页面中有槽位。有槽位的页面非常好。它确实是处理变长记录的一个不错的折衷方案,同样也适用于固定长度记录。这是因为固定长度记录通常包含空字段。如果你有空字段,这些空值可以通过标志来表示,从而避免存储整个属性长度。
如果你有一个整数,但它可能为空,你可能想对其进行压缩,使用一个标志存储它,而不是存储整个数据,这样无论它是否为空,都占用相同的存储空间。如果你只有非空字段,那么这种固定长度格式就可以节省存储空间,不需要指针等额外开销,因为位图方法在这种特殊情况下通常更为高效。
好的,我们从文件讲到页面。我们讨论了页面和页面内的记录。现在让我们来谈谈记录是怎样的。每个表或关系中的记录都有一组固定的类型组合。这是由模式决定的。正如我之前也提到的,关系型数据库在磁盘或内存中的数据使用相同的页面格式。这是因为你想节省转换的成本——即序列化和反序列化的成本,不想支付将内存中的对象转换到磁盘上的成本。
我想在这里提到的另一点是,系统目录(数据库系统目录)存储着模式,因此它存储着每个关系的类型组合。这样,你就可以避免将类型信息与记录一起存储,从而节省空间。
所以你不需要编码记录的类型信息,你可以将类型信息单独存储,等你完成后再存储。这个系统目录在大多数数据库系统中通常是另外一个表。如果你有兴趣,我们可以分享这些表的名称,我相信在 SQLite 中它叫做 SQLite Master。那么我们对编码这种记录格式的目标是什么?我们的目标首先是快速访问字段。
那么为什么我们要快速访问字段呢?通常你并不关心整个记录,你只关心记录中的某些组件。所以,如果你说选择 GPA 从学生表中,你只关心 GPA,而不是其他内容。因此,你希望能够快速访问这些字段。第二个目标是,你希望记录尽可能紧凑,以便你可以在一页中存储更多的记录,并在文件中存储更多的页。所以我们讨论了两种情况:固定长度和可变长度,固定长度的情况很简单。
所以这是一个很好的开始。如果你有一个固定长度的记录。例如,假设一个整数,一个双精度浮点数,一个布尔值,另一个整数和一个长度为 7 的字符。即使是 I feel 也很简单,我们可以直接做算术运算,然后你可以获取 I feel。所以,例如,如果我想获取这个布尔值,我可以跳过前 12 个字节,然后获取到这个值。
所以在某些情况下,我可以使它更紧凑。如果所有字段都不为空,我就无法进行压缩;如果字段允许为空,那么我就必须应用可变长度方案。字段允许为空时,本质上就像可变长度一样。接下来我们讨论可变长度方案。好吧。
那么如果字段是可变长度的,会发生什么呢?除了你的字符型和整数型,你还可能有一个 bar 字符串。这是我们编码可变长度字符串的一种方式。所以一种方法是通过填充来存储它,也就是说,你基本上存储这个 Bob,并填充它,使其占据某个预定长度的字符串,比如说 20。
然后,这条大街(假设是 Bob 的地址)被扩展到了长度 18。所以你已经预先确定了 20 和 18 作为这两个字符串的长度截断值。现在,这种做法的缺点是非常直接的。首先,你必须首先考虑到可能的最大字符串,其他字符串可能不会那么长,因此这会导致浪费。
或者你可能更加保守,或者更为激进,但你最终会在遇到更大的字符串时不得不重新安排数据,因此这将变得低效。所以这种填充的方法并不是一个很好的方案。你可能考虑的另一种方法是使用逗号,或者其他分隔符来分隔这些字段。这样做的缺点是,确定一个字段的结束位置和另一个字段的起始位置变得更加困难。
所以,如果你想查找,我觉得这会更加困难。而且有时确保逗号不是字符串的一部分也很困难,如果逗号是字符串的一部分,你需要找到一种方法来转义它们,这会更加麻烦。那么我们如何处理可变长度记录呢?我们将采用的方法是每条记录一个头部。所以记录头将指向这些可变长度字段的开始或结束,在这种情况下是结束。
在这里,你有一个指针指向第一个可变长度字段的末尾。这里你有一个指针指向第二个可变长度字段的末尾。通过利用固定长度字段后跟可变长度字段的事实,你可以准确知道每个可变长度字段的开始和结束位置。这个可变长度字段的开始和结束你知道,因为你知道固定长度部分的长度,并且知道这些字符串和所有可变长度字段的结束位置。
这是我们将采用的方案,即记录头方案。这个记录头方案使得访问字段变得更加简便,几乎是你能期望的最紧凑形式。可以预期,头部到固定长度字段的编码方式与可变长度字段一样,只是需要在这些指针中加入一些额外的信息。同样的方法也可以用于压缩包含多个空值的固定长度空字段。
所以,即使在考虑固定长度字段时,如果你现在允许它们,它们实际上最终会变成类似于可变长度字段的形式,因此你可以使用压缩方法来处理它们。下面是我们用于表示信息的方案概述。给定一个关系,这些关系被编码到一个文件中,这些文件代表磁盘上的一个或多个文件。
每个文件包含多个页面。一个给定的记录使用这种编码格式表示,包括记录头和固定长度及可变长度字段。头部允许你指向这些字段的结尾。然后,将这两者结合起来就是插槽页的概念,基本上是编码一个页面级别的头部,该头部编码了记录在页面中的位置。
而我在这里没有展示的一件事是页面目录。那是另一种元数据,编码了有关哪些页面有空闲空间的信息。所以总体而言,这就是表示部分的总结。我喜欢将其看作是在每年提取时的方式。
有一个头部的概念,也有一个细节的概念。如果你以这种方式考虑它,就会更容易理解。所以,正如我所说,一个数据库文件包含页面,而这些页面内包含记录,我们谈论了这些堆文件(heap files)用于无序记录。并且你有页面目录,可以高效地定位这些页面。页面布局。
我们讨论了固定长度方法,包括压缩格式和解压格式。对于可变长度,我们只讨论了使用起始页和在页面满时进行页面内部重组的解压方法。例如,我们还讨论了记录格式,允许访问字段并处理空值。到目前为止,有什么问题吗?
伊恩。是的,所以在我们讨论可变长度记录格式的每一张幻灯片上,你提到我们可以使用分隔符存储数据,比如逗号,但这不是一种好方法。你能详细说明一下如何使用逗号吗?因为这对我来说没有意义。好的,假设这样吧。
好的。所以在这里,你可以比如说,把它表示为“Bob,逗号,大街”,然后如果你想更明确地标示这是字符串的开始,你可以用引号包围它。然后你可以做“M 329,703”之类的。对,所以你确实需要有办法区分这个逗号和那个逗号。
这个逗号是分隔符,而这个逗号是字符串的一部分。所以一种做法是用引号来转义这些字符串,之前写的已经丢失了。对,当然还有其他方式,但基本上你需要确保有一种独特的方式来读取记录。
这种方法的缺点是显而易见的,如果我有“Bob,逗号,大街”,逗号什么什么,我就不知道在哪个位置开始查找第二个字段。对吧。所以基本上,在这种字节表示中,我不知道从哪里开始读取,然后我需要读取这么多字节才能得到第二个字段,所以在这种表示方式下很难做到这一点。
所以使用逗号时,我们仍然需要扫描整个场景。是的,你仍然需要扫描整个内容,或者你需要找到一种其他方法来编码每个字段的位置,这最终看起来像是一个头部。对吧,届时你可以直接使用头部,不再需要分隔符。好的,谢谢,路易斯。路易斯,如果你想提问的话,你还没有解除静音。
我觉得我们可能不该开始下一个话题。如果你们还有问题,欢迎留下来。感谢大家的到场。谢谢大家,星期四见。[沉默]。
P5:第五讲 成本模型和索引 + B+ 树 - main - BV1cL411t7Fz
说到186讲座。
今天我们将讨论成本模型和索引。在此之前,我希望大家继续使用聊天窗口,顺便提一下。我认为我们在聊天中收到了很多很棒的问题。如果你们甚至想在那里开些玩笑,我觉得那也很好。
只要这对这门课来说是合适的,那就没问题。所以,是的。我认为我们可能仍然会使用Piazza进行这次讲座,无论如何。没问题。我先考虑一下。我们很快会创建一个讨论帖。好吧,无论如何。是的,所以也可以随时在那里提问。我的意思是,或者我们可能会将你们移到那里去。
如果这个问题变成了最长的问题,确保我们不会丢失任何信息。如果你觉得舒服,请尽量开启视频。看到你们的面孔很好。而且也很高兴看到有人点头,似乎在希望——我们基本上希望你们能够理解材料。
所以我们可以得到一些视觉提示,那将是很棒的。当然,如果我们尝试开玩笑,那么最好有人能露出笑容,对吧,稍微笑一笑。所以很酷。好吧,那我们开始吧,对吧?让我们开始吧。所以下次Ditio基本上是在讲不同类型的文件组织方式,对吧?这就是我们的位置。所以早些时候。
我在上次讲座中提到过磁盘空间管理,之后我们也讨论了文件组织方式。所以回顾一下,对吧?上次我们讨论了如何在文件中组织元组的不同方法。其中一个例子是堆文件,基本上是一个无序的记录或元组集合。然后,就像你们记得的那样。
在磁盘讲座中,我们讨论过的这些设备,仅仅暴露了读和写,作为唯一的API,对吧?所以基本上,这就是我们将要使用的内容。那么,我为什么要提这两点呢?嗯,今天我们实际上想要问的问题是:成本是什么?成本是指当我们尝试做一些操作时的成本。
数据库,对吧?所以当我说“插入、删除或修改的成本,甚至仅仅通过特定ID或特定属性获取记录的成本”时,这就是我所说的意思,对吧?
我们还想看看当我们尝试扫描所有记录时,成本会是什么样子。为什么我们关心成本?嗯,首先,对吧?你们也记得上次讲座的内容。实际上有很多方法,我们可以用来存储文件中的元组,对吧?
人们还记得我们上次讲座除了最后提到的保持文件,我们实际上讲了什么吗?
这是一个小测验。不,我开玩笑的。有人吗?是的,某种文件类型,对吧?好的。所以这是其中的另一个,右?所以,你知道,这只是其他选项之一。所以除了保留文件之外,还有另一种文件类型。然后,像,今天以及下一讲,我们还将讨论聚簇文件和索引。
所以我们有这么多种存储数据的方式。那么我们试图弄清楚,像,知道什么是最好的方法,对吧?这是有意义的。
通常这会针对给定的查询工作负载进行优化。因为如果我们不知道会在这些不同类型的文件上执行什么样的查询或操作,那么我们真的很难比较它们每一个,对吧?所以在本课程的背景下。
我们总是会讨论,比如说,我们想做插入操作。比如说,我们想做,像,扫描所有记录之类的事情,对吧?
然后我们稍后会讨论,但我们如何获得那种工作流信息,对吧?或者我们怎么知道我们要运行,像,你知道的,扫描,和,像,你知道的,为什么不呢?比如说,找到一个特定的元组。所以从大体来看,对吧。在谈论成本时,我们基本上是想找到一种估算与之相关的成本的方式。
对于每种数据访问类型,再说一次,数据访问,在这种情况下。意味着从查找和插入单个记录,到删除或修改,甚至是基本上查找我们存储在文件中的不同类型数据之类的任何操作。好吧。
我们只想要大致的估算,对吧?因为在这种情况下。我们不是想深入到,像,你知道的,最精确的成本。对吧?我们不是在与,像,外部毫秒级别进行比较。我们实际上只是想得到一个简短的估算,比如。
我们可以用这个来决定我们应该使用哪种类型的文件,当我们尝试存储数据时。例如。那么,尽管如此,保持一些纪律性也是有意义的,对吧?
那么我们说的是什么意思呢?我们意思是,我们希望能够清楚地陈述我们事先希望做出的假设类型,然后尝试以一种系统化的方式来估算成本。实际上,这将为查询优化打下基础,而查询优化将在大约两周后,当我们讨论时进行。
如何在所有这些不同类型的文件和操作等情况下优化查询。然后,你知道,正如我所说的,对吧,我们需要一种方法,准备好在决定哪个最好之前进行比较。所以这是我们今天这堂课前半部分的重点。明白吗?举手。竖大拇指?是的?[听不清],不?酷。好的。
那么现在我们来谈谈,像,稍微定量一些,对吧?
我们说的成本模型是什么意思?所以我们再次尝试对与这些数据操作相关的成本进行粗略估算。所以我们来稍微做点数学。我们将在这里定义三个不同的值。B代表文件中数据块的数量。
所以我们在上一节课已经讲过了块或页面的概念。然后我们将使用大写字母R来表示每个块中记录的数量。D则表示读取或写入磁盘块的平均时间。所以这些只是粗略估计,对吧?例如,如果我们要……
根据我们处理的文件类型,每个块的记录数可能不同,对吧?所以,现在我们只是把它作为一个大致的概览,或者说是计算成本的广泛方法估算。所以这张图是用来提醒你,对吧?所以现在我们要在这里用黑色表示每个文件。
然后每个文件基本上会有不同数量的块或页面。每个块基本上由记录组成,对吧?所以这是一个提醒。所以我们的目标是能够计算这些不同类型工作负载的平均情况。再次强调,工作负载,对吧?我们在谈论插入、删除操作。
修改、扫描或搜索特定记录。假设是什么?
所以现在我们完全忽略了顺序和随机IO之间的差异。所以忽略我在上节课说的内容,例如,哦,如果你是顺序读写,确实会有额外的好处。暂时抛开这个。我们也会完全忽略任何与缓存、预取等相关的内容。
所以我们假设每次都需要将数据从磁盘加载到内存中。没有缓存,我们不做记录,也不做预取操作。然后我们也会完全忽略与执行操作本身相关的成本,对吧?例如,如果我们想要……
获取某个特定的元组或记录,我们需要检查我们加载到内存中的内容是否符合我们的标准,对吧?所以这肯定会有成本,对吧?它不是免费的,对吧?所以,运行CPU计算确实需要时间。但现在,我们暂时完全忽略这一点。然后……
我们始终假设数据必须先加载到内存中,才能进行操作。所以我们不能直接在磁盘上操作数据。一切必须先加载到内存中才能操作。同样,这也意味着,如果我们做了修改,比如更新了某些内容,我们也需要之后将其写回磁盘。
这将是所有这些内容的一个趋势。我们声称,这已经足够好,能够展示不同类型文件的不同趋势。我们认为这对你们来说太简单了。我们会在课后重新审视这些假设,对吧?
然后我挑战你们,像你们知道的,试着去消除这些假设之一,然后做出我将在接下来的几张幻灯片中展示的计算,然后你会大致了解这将有多容易或多困难。好吧。所以关于我们正在处理的操作类型的更多假设。现在,我们只假设我们将插入一条记录。
或者每次都删除一条记录。所以如果我们插入一堆记录,我们会多次执行这个操作。你们会知道真相的。然后对于像,嗯,查找记录的操作,我们假设只有一条完全匹配的记录。所以你可能在查找,假设是,像,嗯,某个学生的记录。
ID等于某个值,对吧?或者你可能在找一个水手的记录,对吧?
我们说ID等于其他某个值。所以现在,我们只是假设只有一条记录会匹配,尽管实际上可能有多条记录。而对于堆文件,我们假设我们总是会将记录追加到末尾。这也是我们在上节课中讨论过的内容。对于排序文件也是如此。
我们总是假设我们会根据搜索关键字进行排序。可以把它想象成我们正在搜索的内容。比如SID,ID以及其他相关信息。正如我所说的,如果你想要更大的挑战,或者觉得这对你来说太简单,我是说,你知道的。
随时可以尝试放宽这些假设,然后我们将进行相关计算。然后你会看到,像,嗯,你会有更深的理解。而且还有很大的提示,对吧?这可能会出现在考试中。好吧,好吧。现在我们来看看这些操作,嗯?所以,像你知道的。
这只是我们接下来几张幻灯片中要展示的内容。所以堆文件,正如你们记得的,是无序的,对吧?所以现在,我们基本上看到了一堆不同的页面,然后像你知道的,现在我们假设记录只是数字。所以在堆文件的情况下,你会看到这是完全无序的,而在这种情况下则不一样。
由于我们只存储一个数字或者排序文件,我们就会根据那个特定的数字进行排序。所以。然后只是提醒一下,嗯?在这种情况下,我们有五个不同的块。每个块有两个记录,然后我们假设某个数字D,即读取和写入所需的时间。好。
所以这是我们想要填充的表格类型,你知道的。在接下来的15到20分钟内。好的。所以我们有两种不同的操作要做。然后我们想要计算在这些参数下,实际需要多少时间。哦,好的,第一个,这是一位女士。假设我们想扫描所有记录,对吧?
所以只是为了说明,对吧?第一个,当我们尝试浏览堆文件时,你基本上可以看到这里的红色条。我只是代表一个手指,对吧?
我们基本上是在扫描所有内容。然后对于排序文件,我们也在扫描所有内容。所以在这种情况下,你们觉得我们需要读取的这些数据块数量的成本是多少?好的,太棒了。
所以我看到你可以随意大喊,是吧?或者你也可以在聊天窗口里输入。所以我在说人们的死亡,很棒。就是说,像是,你知道的。我们基本上会浏览所有的内容,对吧?所以没有逃避的余地。我们要扫描所有内容。所以显然你需要阅读所有的块,对吧?没有逃避的余地。
所以我们基本上会读取数据块的数量。然后由于每个块都需要D,对吧,它们的平均读取时间,那么,总成本将是B乘以D。顺便说一下,我们这里只是将时间作为测量单位,作为所谓的成本,对吧?
所以如果你在 AWS 上运行这些操作,例如,我们可能更关心的是钱,对吧?你需要多少美元,以及如何执行这个操作,对吧?
但为了简化起见,我们现在只使用时间。到目前为止,有什么问题吗?
这是最简单的情况。好的。然后我们继续。那么现在假设我想做等值查找,对吧?好的。那么等值查找是什么呢?
所以假设我想找一个包含数字八的记录,对吧?
所以我希望我已经说服你们,这是我们要寻找的内容,对吧?
然后它是怎么工作的,我们试图读取文件中的最后一页,对吧?
然后有个学生,安基特说,能不能稍微慢一点?安基特,你想提问吗?是的,有什么不清楚的吗?很清楚。我只是需要把所有的内容写下来。哦,好吧。抱歉。好的。好的。那么回到这个,对吧?所以好的,我看到有人在问关于平均情况还是最坏情况的问题。
所以在这种情况下,我们实际上更关心的是平均情况。稍后你会明白为什么。好的,回到这个,对吧?所以我们想要获取一个包含数字八的记录。那么在堆文件的情况下我们该怎么做呢?我的意思是,我们只能扫描所有内容,对吧?没有别的办法。我的意思是,这是唯一的方式,我们基本上需要扫描它然后找到它。
现在我们来做一下平均K分析。首先,你们认为这里的最坏情况是什么?对吧?最坏情况是怎么样的?
当八号页面在最后时。对,当八号页面在最后时,对吧?
所以基本上,我们需要扫描所有内容。所以显然,成本就是B乘以D,对吧?
但实际上,这就是这里计算平均值的方法,对吧?因为在最坏的情况下,所有的内容都像是我们需要阅读的内容。所以没什么特别有趣的。那么这也回到我们为什么更关心平均情况的问题。那么,平均情况是什么呢?对吧?所以我们再引入一点数学。首先。
我将说P(I)是某个特定的键,或者说记录,出现在特定页面的概率。我们这里有B页,每一页都有同等的概率包含我们要找的记录。所以,所寻记录在某页的概率就是我们要找的内容在该页的概率。
页面编号I,比如说,它只是B的倒数,表示各个页面的概率相同。然后我们定义T(I)为我们实际需要触及或阅读的页面数,以便达到第I页。对于第一页,我们只需要读取一页就能到达。对于第二页,我们需要阅读两页。
对,因为我们总是从文件的最左边或开头开始。如果我们需要一直读到最后,我们就需要读取所有内容。那么这里的平均值是什么呢?平均值实际上就是一个期望值。你可能还记得你在数学课上学过,对吧?它就是将我们需要读取的页面数与记录相乘。
实际上,我们正在寻找的是第五页,对吧?最后一页。所以我们需要触及的页面数是五。
然后概率将是B的倒数,对吧?因为是1/5的概率,我猜。哦,这种情况,对吧。所以这就是概率。这个基本上给了我们其中的一个项。对吧,这是期望值。但接下来我们需要对所有可能的情况求和,因为我们要找的记录可能是第一页,也可能是第二页。
如此类推。所以这就是为什么我们需要从1加到B进行求和。我们记得,B是页面的数量。这样理解吗?好,如果你真正计算这个数学问题,T(I)就等于I。因为对于第五页,我们需要读取,嗯,我们需要。
需要翻阅5页。如果是10页,那我们就得翻阅10页。所以第一次应该不言自明,然后I over B就像是你在重复。我之前和你说过的这个数字。然后如果我计算没错,这个结果正是这个表达式。
工程近似值基本上是B除以2。所以这实际上符合我们的直觉,对吧,意思是,像你知道的,平均来说,我们基本上需要翻阅,大约是B除以2的页数,才能找到我们想要的记录。换句话说,像你知道的。
我们基本上需要翻阅文件的一半才能找到我们想要的东西。好吧,顺便说一下,如果你真想得到具体时间的话,我们基本上是通过把这个数字乘以D来计算的,不过我为了简化没有在这里展示。
所以这个B除以2的数字会一遍又一遍地出现,对吧,确保你理解这一点。对吧?所以你知道,重复一下,平均而言,我们基本上预计需要翻阅文件的一半才能找到我们想要的东西。好吧。那么我们来看看另一个例子。所以在这种情况下。
我们必须排序一个文件,这可能更复杂,对吧?
那么我们怎么找到数字8呢,对吧?
所以我们基本上从中间开始,因为我们可以使用二分查找,对吧?我是说,文件已经排好序了。所以你们还记得61 B吗?那个算法叫二分查找。所以我们不必从最左边开始。我们实际上可以从中间开始,抱歉,我们不需要从文件的最左边或开始处。
我们实际上可以从中间某个地方开始,因为我们将使用二分查找。提醒一下,二分查找的工作原理是,我们从中间开始,然后选择。我们要走哪个方向。在这种情况下,由于8比中间页要大,对吧?所以我们基本上会往右走。那么接下来的步骤是。
你会看到我们正在向右移动,对吧?
然后我们基本上会找到我们想要的那个,对吧?就这样。那么现在我们先弄清楚这里的最坏情况是什么。这里的最坏情况基本上是我们需要翻阅每一页才能找到我们想要的记录。然后从61 B开始,你记得吧,就像,你知道的。
我们需要的跳跃次数基本上是以B为底的对数。但那么,平均情况如何呢,对吧?这里的平均情况再次是,像,知道的,平均来说,我们预计翻阅多少页?它也是以B为底的对数,对吧?那么我们实际上尝试通过这个第一个图示来解释。
所以在这里,我已经用不同的颜色表示了每个记录,就像上一个页面一样,只是作为示范。你会注意到我们实际上现在有比上一页更多的记录,对吧?所以暂时不用担心这个,这只是为了说明。再一次,在二分查找的情况下,我们基本上是从中间的页面开始。所以在这种情况下。
蓝色页面在这里。然后我们基本上选择向左或向右跳,对吧?
右侧的所有内容,记录我们要找的。所以假设我们要向右跳。那么我们基本上就移动到青绿色页面,对吧?
因为我们从这里开始。所以范围基本上是右边的页面范围,页面值的范围。然后我们就选择基本上跳到那个整个范围的中间,对吧?
然后我们再进行一遍相同的过程,对吧?除非这基本上就是我们正在寻找的页面,通过现在尝试遍历其他页面来找到的页面,对吧?比如说,接下来我们要做的是右边的页面,然后我们也会跳到中间。现在我们跳到白色页面,例如,接着我们看看我们想要的记录是否实际上在白色页面上,对吧?
然后如果没有,我们再跳一次,对吧?在这种情况下,我们基本上是一路跳到叶子节点。所以现在我们基本上到达了黑色页面,假设我们从白色页面向左跳。希望这能成为一个提醒,对吧?
从61B开始,关于我们如何在页面块的数字上做二分查找。所以如果B是数据块的数量,那么我声称我们想要的页面的平均读取次数是这个庞大的表达式。所以你可能会问,发生了什么,对吧?让我来解释。所以再一次,对吧。
我们有B个数据块,我在页面上展示给你们。所以查找我们需要的记录,位于这些页面中的任何一个的可能性是一样的。因此,这就是为什么概率是1除以B。但成本呢?为了访问那个特定页面,我们需要的成本是多少?
所以不同于堆文件,对吧,它就像是,知道吗,它只是左侧的若干页面,对吧?所以在堆文件的情况下,一切都是无序的。我们只是从开始读第五个页面,那么成本就是五。但在这种情况下,不是的,对吧,因为我们现在在文件中跳来跳去。
所以以第一个页面为例,对于蓝色页面,成本就是一,对吧?因为我们只需要一个单独的页面,我们可以把它载入内存,然后我们就能检查,记录实际上位于哪里。但对于第二个,第二个页面呢?
那么这个青绿色的页面上,对吧,我们实际上需要经过多少步骤才能到达那个页面?
举手。你们觉得怎么样?或者打字告诉我?两个,对吧?
因为我们需要先去蓝色的页面,然后现在我们跳到青色的页面,对吧?
所以,我们需要经过的页面数实际上是两个。但请记住,这里实际上有两个青色的页面,对吧?所以我们可以跳到左边,或者我们可以跳到右边,对吧?从蓝色页面开始。所以因此,我们实际上到达我们想要的页面的概率是1除以B乘以2。
因为实际上有两个可能的页面,对吧?就是记录实际所在的地方。然后每一个都需要两个I/O操作,或者两个读取才能到达。所以这就是为什么我们将那个数字乘以2。所以这样向下走那一层,对吧?
所以下一个步骤,现在是关于白色页面的,白色页面,对吧?同样的思路。所以现在我们基本上会向下走三层,对吧?
我们需要读取蓝色页面、青色页面,然后一直读取到白色页面,这样我们就能到达那个页面,好吗?然后你可以从这个图像中数一下,对吧?
我们在这个文件中有四个不同的白色页面,对吧?因此,白色页面的期望值实际上是3,乘以I除以B再乘以4,对吧,因为我们有四种不同的可能性。然后同样的情况对于黑色页面。所以如果你对这种线性表示的图像感到困惑。
让我尝试以另一种方式绘制它,作为你们可能在61 B中见过的典型二叉树,对吧?所以对于蓝色页面的第一层,实际上成本是一个绿色页面。并且也到达,因为这是我们首先尝试的页面,对吧,位于文件的正中间。然后第二层的每个页面都需要两个I/O操作。
对,或者两次读取。首先必须经过蓝色页面,然后再去青色页面。然后同样的,针对这些其他的层级。那么大家还记得从61 B的内容吗?如果我们试图放入的总数是B,二叉树或二叉查找树预计会有多少层?
太棒了。是的,log以2为底的B,对吧,记得吗?即使在计算机科学中我知道我们只讨论二进制的东西,但有时对于那些不在这个领域的人,你基本上需要提醒他们,好吗?所以是log以2为底的B,对吧?
所以这是我们能够推导出的公式,用来计算在一个二进制排序文件中查找记录的平均总成本。所以我们将从第一层开始加总,一直到log以2为底的B,对吧,这基本上就是你预期的总层数,对吧?然后再一次。
我们将乘以每个层级所需的磁盘访问次数。在这种情况下是i,对吧?i就像是层次,对吧?所以如果这对于蓝色的一个是二,白色的一个是三,依此类推。那么我们要找的记录在某一层次的页面中的概率就是二的i减一次方除以b。
你可以算出数学公式,对吧?所以,你知道,对于第一个,蓝色页面这里,对吧?它就是一除以b。只有一个。但第二层次这里将是二除以b,正如我在前面的幻灯片上展示的,对吧,正是这里。所以这就像是,嗯,二的i减一次方除以b。
我不会让你们厌烦这些数学内容,所以你们可以基本上看一下这个。然后最终我们将得到像这样的表达式。然后,再次提醒,对吧,工程近似告诉我们我们可以忽略这个项,对吧。因为b减一除以b就像,嗯,1。
然后通过另一个近似,你基本可以把这个看作是,嗯,类似于以2为底的对数b。对吧?所以我再次强调,我没有展示实际读取操作所需的时间,对吧?然后,如果你想要完整的时间公式,对吧,我们基本上只需要把这个乘以b。所以在前一页,我会给你展示这个网格的数字,对吧。
如你所见,这里。它大约是以b为底的对数。所以我们已经可以看到差异了,对吧,堆文件和排序文件之间有区别,记住这一点,对吧?
所以现在我们基本上要看到这两种文件之间的区别,这将基本上帮助我们决定何时使用哪种类型。好的。接下来我们来看下一个。那么我们有范围搜索。范围搜索是什么意思呢?举个例子,假设我们要找到所有介于七和九之间的记录。那么对于堆文件,还是一样的。
对, 我说我们总是需要扫描所有内容。有人能告诉我们为什么吗?
有人吗?是的,然后抱歉,Carlos,没错。是的,基本上人们说的,他们是无序的。我们不知道顺序是什么,所以我们需要遍历所有的记录才能找到。没错,正是这样。我们不知道顺序,对吧?所以我要找到七到九之间的所有记录。所以仅仅在找到七之后停下来是不够的,对吧?或者像,嗯。
我们在找到10之后,对吧,因为你怎么知道某个值是否存在于我的范围内呢?这个值是否实际存在呢?对,这个问题。好的,所以我们不能实际做到这一点。我们总是需要扫描所有的内容。然后,怎么样呢,排序文件呢?
同样的问题。所以我们就不做了。那么我们到底该如何做呢?
有人有建议如何在堆文件中实际执行此操作吗?
我知道我们可以扫描所有的内容。是的。但是,嗯,像这样,让我们尝试一个更聪明的思路,对吧?
所以,你知道的,我们还可以做些什么呢?或者我们可以做些什么更好的方法来查找它?
是的,对吧?所以我们可以找到第一个,然后找到最后一个,对吧?是的。所以我们可以。实际上,这里还有另一种提议,对吧?就像,你知道的,你可以进行两次二分查找。是的。或者你也可以像这样做,对吧?
所以首先找到起始点,然后扫描其余的部分,对吧?
因为这个东西是有序的,对吧?所以我们实际上不需要再进行二分查找,因为我们已经找到了起始点,对吧?在这种情况下,七,然后我们只需要扫描到右边,直到文件的末尾,或者直到你知道的,我们实际想停下的地方,对吧?在这种情况下,是九。所以我们基本上就像这样,你知道的,在这里停下。例如,好吧。那意味着什么?
这基本上意味着我们只需要找到范围的开始,然后我们只需扫描右边直到停止。那么成本是多少呢?
所以你记得我有跟你们提过这个B除以二的数字,对吧?
所以B除以二就是我们预计需要扫描的页面数量,对吧?
为了像这样进行查找,对吧?所以在这种情况下,我们将在这里调用它。实际上,保持这个想法。那么在这种情况下,成本实际上只是进行二分查找以定位起始点,然后扫描右边的所有内容的成本,对吧?
然后,我就会挥挥手,说我们需要扫描到右边的页面数量是这个神奇的变量,叫做页面(pages)。所以它取决于你的范围实际结束的地方,对吧?所以如果你觉得可以的话,可以将其近似为B除以二。如果你认为,这在平均情况下是这样的,你知道的。
需要扫描的页面数量,或者如果你知道某个特定查询的页面数量,也可以使用更精确的方法。当你实际进行范围查找时。但在一般公式中,它只会是你知道的,所需的成本。
首先定位起始点,然后扫描所需的页面数量,直到我们到达终点。关于范围查找有问题吗?再说一次,你能看到这两者的区别,对吧?所以,你知道的,堆文件的成本基本上是常数。
这样计算所需的成本,与文件的大小相比,实际上可能已经更高效了。好吧,所以现在如果没有问题,那我们继续讲插入操作。所以我们也想在这两种不同类型的文件中插入。那么我们怎么做呢?对于堆文件,你知道的,记得上次讲座时提到过。
你可以在不同的位置插入,对吧?但是为了本课程的目的,为了简化事情,我们总是假设我们将插入到最后,或者最后一页。所以在这个假设下,插入堆文件的相关成本,我认为是。
就是说,它将是2乘以D。那么大家能否思考一下为什么这里有这个神奇的数字2呢?
为什么它不是D呢?我的意思是,你知道,我们不是把它直接加到文件末尾吗?对吧?是的。很好。好吧,所以它既是读取又是写入,对吧?因此,这实际上会对很多不同的操作造成巨大的影响。所以与之前只扫描或读取磁盘上的数据的操作不同,在这种情况下,我们实际上是在进行修改。所以记住。
我在讲座一开始说过,如果我们实际上进行了修改,光在主内存中是远远不够的。我们需要把它全部写回到磁盘。所以因此,在这种情况下,成本不仅仅是一次单独的页面读取。还包括之后的写入操作。
之后的写入操作。所以这就是为什么它会是2倍的D。D再次是指读取或写入单个页面到磁盘的平均时间。好吧。有问题吗?嗨,抱歉,你能再重复一遍吗?因为我没完全理解为什么是2。好的。是的,因为我们需要同时进行读取和写入,对吧?所以它将花费我们,嗯,你知道的。
一页,对吧,一次磁盘读取,读取最后一页,对吧?所以我们试图插入4.5,对吧?所以这将至少花费我们一次读取操作,读取那最后一页,将其放入内存中,对吧?然后我们还需要将其写回磁盘。所以这将再花费我们一次操作,再次进行磁盘操作,对吧?因此,总体的成本是。
这将涉及两页的页面数。好吧。也就是说,你的意思是我们需要同时写入内存和磁盘。所以我们需要两页。对,因为如果你只写入内存,之后如果关机怎么办,对吧?
好的,谢谢。是的。所以我们需要让它持久化。因此,我们需要将其写回文件。所以这就是为什么会是2。嗯。还有其他问题吗?好的。好的,谢谢,Kari。那么我们继续吧,对吧?
所以现在让我们来讨论如果我们尝试插入到一个排序文件会发生什么。思路一样,对吧?
所以基本上,现在我们要运行什么,嗯,应该在这种情况下运行什么呢?我们应该先做什么?
二分查找,对吧?我的意思是,嗯,不,嗯,我的意思是,这有点像主题,对吧?
所以这整个类,嗯,类似于文件二分查找,对吧?为什么?
因为我们想要找出记录到底放在哪里。我们想要保持排序,对吧?所以在这种情况下,我们不能只是把它附加到文件末尾,对吧?
因为那样会破坏排序文件。所以我们实际上想先进行二分查找。在这种情况下,它可以帮助我们确定我们实际上要插入记录的位置。所以从前几张幻灯片来看,你可能已经记得成本是。只是会是对数基数为2,再乘以B的。
然后我们得到与查找记录相关的实际时间。但这还不够,对吧?
因为我们实际上需要插入我们想要插入的记录。在这种情况下,4.5。然后将之后的所有内容写回,对吧?在这种情况下,“所有内容”是指,像你知道的那样,右侧的所有页面,直到最后,对吧?因为我们要进行移位。基本上是通过插入一个新的记录来移位,然后像这样。
你知道的,需要写回与此文件相关的其他所有后续页面。这样说清楚了吗?所以再重复一下,对吧?在这种情况下,我们从,知道的,5和6在一页上,7和8在另一页上,以此类推。我们要插入4.5。所以我们需要将它放到与5同一页上,对吧?因为我们想保持排序顺序。
我们不能在中间有间隙,除了在最末尾,对吧?在这种情况下,你会看到这里有一个间隙。但除此之外,我们需要基本上把它放到包含5的页面,然后将所有内容移到另一个页面,对吧?然后我们需要从6一直到10的内容。然后将所有内容写回磁盘,对吧?所以记住。
你总是需要将所有内容写回磁盘。对吧?
那么,与此相关的成本是什么呢?找记录。我已经告诉过你了,它将是对数基数2,乘以D,或者如果你关心时间的话。然后是插入,对吧?
移动文件的其余部分就只是,分成两部分,这样的话。我再次提到的是,我们需要触及的页面的平均数量,对吧?
从讲座的前部分。然后,你知道的,将它乘以D,并且还要乘以2。2是因为我们需要同时进行读写,就像之前的情况一样。所以因此,总成本将是这三者相乘的结果。
对于插入和移位。所以,它就是B乘以D。然后总成本将是这两项的总和。所以如果你算一下数学,它就是你在屏幕底部看到的这个数字。所以现在我们有了,知道的。
完善这个插入的过程,对吧?所以你可以看到,对于堆文件,它只是一个简单的,单一的读取和单一的写入,因为我们总是将内容追加到文件的末尾。而对于排序文件,我们实际上需要做更多的工作,对吧?
所以我们需要运行二分查找,然后还要移位操作。我们已经可以看到,这之间存在权衡,对吧?
并不是说排序文件总是优于堆文件,对吧?我的意思是,事实上,如果真是那样的话,那我们为什么还要教你们堆文件呢?所以我只是教你们,像你知道的,最优的解决方案,然后那样会成功。但实际上,在这种情况下,情况并非如此。所以我们基本上希望对这两种情况都进行分析。好的。
那删除呢,对吧?对于删除,情况类似。所以对于堆文件的情况,也只是像你知道的,扫描所有内容。因为我们需要找出它的位置。然后,像你知道的,删除记录并把数据写回去,对吧?
所以找到记录所花的平均时间,我从之前的幻灯片中提出的说法是。大约是以2为底的B,按读取次数的平均情况来算。最坏情况,当然,是全部。平均情况下,在这种情况下,应该是B除以2。然后我们从页面中删除记录,然后再把它写回去。
所以这就是为什么你会看到这里有一个数字,对吧?
所以我们读取所有内容直到找到它,然后我们把记录偷走,然后把那一页写回磁盘。至于排序文件,对吧?情况也类似。所以我们首先,再次运行二分查找来找出记录实际的位置,进行实际的删除,然后,像你知道的,写回其他所有页面,因为。
现在我们也需要将所有内容移位。所以二分查找的成本再次是以2为底的对数B,然后,像你知道的,我们现在还需要处理移位操作,并将其写回。这个问题。Carlos。我有点困惑为什么是加一。我知道你已经解释过了。
但如果你能解释一下。好的。是的。加一是因为我们需要将这个空页写回磁盘,对吧?所以我们丢弃了4.5,但然后我们需要把它记录到,像你知道的,文件里,对吧?我们在内存中删除它,这还不够。但我们需要把空页写回文件,以确保我们确实已经去除了它。谢谢。好的,没问题。是的。
所以这里基本上类似。我们首先需要找出它的位置,然后将所有内容移位。移位操作将会是,就像你知道的,乘以二,和插入时的情况相同。所以总成本再次是将这两个项加在一起,就像你在。
这张幻灯片上的内容将会是排序操作的相同内容,同时也适用于插入和删除操作,目的是类似的。是的,Carrie。Carlos,你有问题吗?没有,那个是之前的问题。嗨。所以我想知道,当你删除一个文件时,如果它正好在堆文件的中间,你是否需要…
我的意思是,当你删除堆文件中间的文件时,是否需要将它后面的文件移动到前面?因为你只是删除了堆文件中的一个文件?好的,暂时假设我们不会这么做,我们就直接把它留空一个空隙。你记住,下次课的时候这没问题,对吧?这没问题,对吧?对于堆文件来说,嗯。
对于这个问题,我们就假设你不需要将所有内容都移动。不过我声明,甚至即使我们将所有文件都移动,你应该现在已经知道如何进行这类分析了,对吧?好的,谢谢。嗯,好的,非常好。那么现在我们已经看到类似的情况了,你知道的,流畅的辉煌。
对这两种不同类型的文件进行各种操作。接下来的问题是,实际上我们能否做得比这里的运行时间估算更好?答案结果是肯定的。我的意思是,这就是我们为什么要谈论索引的原因之一。但是,在我们讨论实际的索引类型之前,我们先要介绍一下这个概念。
课程开始之前,我们先回顾一下索引到底是什么意思。其实这个概念并不陌生。我们在日常生活中其实都接触过这种类型的索引。所以你们当中有些人可能已经足够老了,知道像这样的东西。它基本上是一本我从网络上下载的黄页电话簿。
现在它绝对可以算作一种历史遗物,对吧?
但本质上,电话簿只是一个索引,对吧?它只是我们用来查找的方式,比如说,知道一个人的名字后,告诉我那个人的电话号码。所以,这是一种索引方式。你也可以在教科书的末尾看到类似的索引,比如说,它基本上是查找某个话题或作者的名字,然后告诉你该话题在哪一页。
你可以看到这样的索引——话题名称或者作者名称,然后基本上告诉你该话题所在的页码。
所以这些就是索引的类型,在数据库的情况也没有什么不同。但在数据库中,我们当然非常关注效率。所以,单纯拥有某种可以用来查找的东西是不够的。我们希望能够快速地进行查找。比如说,显然对于这个产品目录,你肯定不希望…
查阅整个索引,并找到你想要的东西,对吧?我的意思是,如果是这种情况,我的意思是,为什么要使用索引呢?我就是说,直接扫描所有内容就行了。所以显然,我们需要一种机制,既能让我们通过关键字查找东西,又能高效地做到这一点。所以我们实际上之前已经见过这个。
我们已经看过实际的不同类型的内存数据结构。例如,我们有不同类型的搜索树,我们有不同类型的哈希表,希望你能从你的 61B 经验中回忆起这些内容,对吧?但现在我们不谈论内存了,我们实际上是在谈论这些基础数据结构,对吧?
因为我们实际上是把东西存储在磁盘上。所以显然,如果我们想把索引存储在磁盘上,那么我们如何将像哈希表或二叉搜索树这样的结构存储到磁盘上,对吧?所以这基本上就是问题所在。再说一次,对吧?在这门课上,我们将其建模为页面。
所以基本上我们想要一种分页的数据结构来表示索引。而我们之前在 SQL 课程中谈到过这个,对吧?所以你记得我在课件中使用的那个水手表,对吧?
所以我们声明某些东西时,会使用主键。所以现在你知道了,对吧?
你知道这个主键,对吧?我们所说的主键是什么意思,对吧?
所以当我们谈论 SQL 时,我们是在谈论如何使用它来查找其余的记录,因为这应该是一个唯一标识符。在这种情况下,像水手 ID 就是一个唯一标识符。这很好。但是你知道的,接下来我们怎么存储这些数据?我们如何存储水手的数据,对吧?
因为我们要维护基于水手 ID 的索引。所以这就是我们接下来要讲的内容。所以正式来说,索引只是一个数据结构,我们可以用它来根据搜索键快速查找或修改内容。所以查找,我们已经在之前的幻灯片中看过不同的例子。
对吧?所以你知道,它可以通过等式来查找,对吧?
我正在查找 ID 等于 42 的水手。它可以通过几何区域来查找。我正在尝试查找当前在舞台上的所有火灾,对吧?比如给定一个纬度和经度。例如,你也可以像我们在主键的情况下讨论 SQL 时那样,使用搜索键,对吧?
我们用来查找的键实际上可以是任意列的组合。我只是从上一讲中提取了一些内容,对吧?在这里,你可以将名字和姓氏组合起来,形成一个搜索键。然后像你知道的那样。
我们要再次问的问题是,我们实际上希望如何存储与索引相关的数据,对吧?我的意思是,一种明显的选择是将索引与其余数据完全分开存储,但我们也许可以做得比这个更好。
所以我们将看看这些情况的需求会发生什么。那么今天,我们将假设现在我们需要存储你知道的,K,这就是我们要查找的键,不管它是什么。我们可以说,ID可以是名字、常见的姓氏的组合,然后是一些记录 ID。
然后我们就可以使用它,来查找实际的记录或与之相关的数据。例如,我们可以想象将索引完全存储在与实际数据完全分开的文件中。那么在这种情况下,索引就可以仅仅存储,比如你知道的,哪个记录号是我们需要查找的,和它实际所在的文件,对吧?
然后根据这些信息,我们现在可以去实际存放元组的文件,然后将该页面加载到磁盘上,接着继续进行查询处理。同样的,如何实际进行修改呢,对吧?再次强调,我们希望能够非常快速地进行插入和删除,不仅仅是在基础表中。
但是在索引中,对吧?因为每次当我们更新实际数据时,我们也需要修改相关的索引,对吧?否则,它们会不同步,然后出现问题。所以这些都是我们在这门课中要讨论的内容。正如我所说,数据结构的索引有很多种类型。
接下来我们将集中讲解 B+ 树,这是我们今年接下来的内容。但在这之前,我能回答其他问题吗?哦,大家都完全明白了吗?一次,真的?我们现在来个小测验吗?哦,有人在线问了问题。好,请重复一下问题。好,索引没有实际数据,对吧?
这是一个很好的问题,Billy。所以这取决于情况。实际上,这就是我们今天的讲座以及下周讲座要讨论的内容。简短的回答是,这取决于情况。它可以是任意一种。
好的,没有其他问题。我将停止共享,接下来让T I接管。稍等一下。所以要找到一根绳子。对了,Nicholas在问一个问题,比如,我们如何实际使用这个索引?所以正如我之前所说的,使用索引的一种方式是,假设索引存储一个键和记录号。
如果是这种情况,那么我们首先在索引中查找,然后,像你知道的那样,索引会返回记录号、页面和文件名,这些是我们实际想要查找的两条记录所在的地方。然后我们只需要使用我们之前讨论的算法来实际获取我们感兴趣的两条记录。但这是一种方法,但不是唯一的方法。
它取决于数据实际是存储在索引中还是不在索引中。现在我们在等待T设置好时,还有没有其他问题?好的,开始吧。你能看到我和听到我吗?好的。是的。对的。所以我们在讨论索引。接着Alvin给我们的讲解。正如Alvin所说。
索引是一类数据结构,它允许通过特定的键进行查找,同时也支持高效的修改。对吧?所以你需要能够通过键添加和删除记录,也能够通过键查找数据。对吧?所以你需要能够通过键来识别数据。好的。
所以我们先从一个简单的想法开始,看看为什么这个简单的想法会失败,然后我们会尝试讨论今天要讲的最终索引结构,即B+树索引。好的,所以我们从简单的输入堆文件开始。然后假设不使用索引,我只是对我的堆文件进行排序。
为了确保有空间可以添加潜在的新记录,我可以在每一页中留下些空余空间。对吧?所以我按照Alvin描述的方式对我的堆文件进行排序。排序后的文件。我可以按照这种顺序存储这些页面。
所以它基本上按照排序的顺序存储,允许我按顺序访问这些页面。好的,假设我做到了这一点。正如我们之前所描述的,正如Alvin通过数学所展示的,维护变得非常痛苦。对吧?所以随着新记录的添加或删除,这变得有问题。
这就导致了B次更新,其中B是最坏情况下的块总数。对吧?所以在最坏的情况下,你最终需要将所有内容往下移动或者往上移动。好的。然后我们也讨论了这个二分查找的步骤。对吧?所以假设你想查找某个特定的记录,你当然可以进行二分查找。
我们也做了类似的数学推导,表明这是B的对数,B是块的数量。如果你想计算时间,而不是读取的页面数量,可能还要乘上D。D是时间。如果你想计算时间,而不是读取的页面数量。现在,这种方法有两个缺点。对吧?
所以第一个缺点是这是一个二叉树的展开,这意味着它是一个非常深的树。对吧?所以这是一个深树,且有很多的IOS操作。第二个缺点是你在查找过程中实际上是读取整个记录才能读取键。对吧?所以你不是直接读取键,而是需要读取一整页,里面充满了记录,仅仅为了判断你是否在读正确的页面,是否应该往左走,或者应该往右走。
对吧。所以有大量冗余数据被读取。我们更希望这能够是 K 的对数,而不是 B 的对数,因为 K 是键的数量,而这个数可能远小于 B。对吧,因为 B 很可能非常大,因为通常不仅仅有键作为一部分。
你的记录中,还有很多其他属性。对吧?好的。那么让我们尝试修复这些假设。好的。假设我们做了第一个放宽条件,即我们不存储,正如 Alvin 所提到的,我们使用堆文件。因此,我们不以排序的方式存储记录。
我们保持原样,基本上我们有非常紧凑的键查找页面,并且我们将它们按顺序排列。好的。所以这些键查找页面基本上维持了一个键到记录 ID 的映射。这里应该是页面 ID,然后应该是槽 ID,而不是记录 ID。所以它基本上是告诉你,或者你可以想象这就是页面中的记录。
所以页面中的槽。例如,这个键,这里指向这个特定的页面页面三,以及该页面中的特定槽,这让我能够检索到相应的记录。好的。所以现在我基本上拥有这组紧凑的键查找页面,我不需要再…
不再对我的堆文件进行排序了。我可以直接存储这些内容,而不必担心排序我的堆文件。然而,在此之前,我仍然可以在这些键查找页面上使用二分查找,如果我想找到某些东西。所以,如果我想找到与 27 对应的页面和记录,我会进行二分查找,可能从这里开始,然后移到左边,或者可能移到右边。
对,最终识别与 27 对应的记录和页面。现在,这仍然存在与前一张幻灯片相同的缺点,因为这仍然会是一个深树,因为分支度是 2。所以仍然会有很多文件,我们稍后会看到如何修复这个问题。
另一个缺点是,维护这些键查找页面是很麻烦的,对吧?
我基本上是按顺序存储所有这些,如果我想插入新的键,因此新的记录,我必须在最坏的情况下,将所有页面往下推,对吧?
所以,在插入或删除的情况下,这种维护是一个痛苦。Bhavana,你有什么问题吗?是的,我想知道你为什么说 K 远小于 B,因为在你的图中,看起来键的数量大于页面的数量。好的,实际上,在这个情况下,我并没有展示所有与我的 1,2,3,4,5,6,7,8,9,10,11 相对应的记录。
12,13,14,15,16,17。所以至少有 17 个键,因此应该有 17 页。我这里没有展示所有的。所以这是第一个观察。第二个观察是,通常记录比两个属性要长得多。你可能有 20 个,50 个属性,因此你可能无法在一页中装下这么多记录。
另一方面,如果你存储的只是一个键和一个指向页面的指针以及页面中的槽位,你可以装下更多的记录,对吧?所以仅仅是三个位的信息。三个位是宽泛地说,但这里指的是键、页面ID和槽位ID,而你对每个在这些页面中的键都这么做。
另一方面,对于每个记录,你可能有50个属性,对吧?
所以你可能无法装下太多的记录,因此B可能会大大大于K。所以我想我不太清楚这些变量的含义。所以B是页面数量?总块数,是的。是你数据存储的块数,对吧?
所以你的记录是存储在。想象一下,想一想以下方式,对吧?
假设我有一个学生表,我根据学生姓名为这个学生表建立索引。这个学生表还有50个其他的属性,对吧?
现在,假设我们做一个简单的估算,存储整个学生表所需要的页面数量将是存储学生姓名所需的数量的50倍,假设是这样。对吧?这就是我们所说的数量级差异。这就是K和B之间的区别,对吧?K是你键值中不同值的数量级,而B通常会大得多。
因为你需要存储所有的数据。好的,好的。很好。还有其他问题吗?好,好的。那么,这仍然有相同的问题,对吧?所以它仍然有相同的问题,那就是存储这些按顺序排列的键查找页面的维护仍然是昂贵的。那么我们该怎么解决这个问题呢?好的。那么,让我们将应用到这些无序堆文件上的相同思路应用到这个问题上。
键查找页面,看看我们能把它推进多远。所以我们基本上可以做的是,Instead of having just one set of key lookup pages, we can have lookup pages for these lookup pages and then look up pages for the lookup pages for the lookup pages and so on, right? And now that I have these lookup pages and look up pages for the lookup pages.
我不再要求这些查找页面按照顺序排列,对吧?
所以它基本上已经消除了这个需求,现在我的页面可以放在任何地方,对吧?
这些页面,所以这个页面基本上是一个键查找页面或键查找页面到键查找页面到键查找页面,可以存储在任何地方。同样,这个可以存储在任何地方。只要正确的指针被维护着,任何地方都可以存储。好的。如果这让你想起了二叉搜索树,那确实看起来像是。
然后我们稍后会将其与二叉搜索树联系起来,一旦我们了解这种索引结构是如何使用的。好的。这就是基本的想法。再说一遍,如果你没有理解这个结构是如何工作的,我们稍后会深入探讨。别担心。我在这张插图中做的另一件事是,我修复了扇出,确保它是合适的。
实际上,扇出要大于二,我们即将讨论的索引结构中,扇出会比二大得多。好的。所以,仅仅使用查找页面,然后为查找页面查找页面,以此类推,基本上就是加号树背后的想法,而加号树基本上使得它非常高效。
非常高效地添加和删除记录来维护它,同时确保提供相同的渐近保证,并且修复扇出。对吧?因此,它的扇出更高,这使得我们不需要一个非常深的树。它实际上是一个非常宽的树,我们将在介绍第一部分时讨论所有这些内容。
你有问题吗?是的。那么,最底行的 27 如何精确地映射到页面三呢?
对。所以基本上是,这就是一个关键字,对吧?
27 是一个关键字,在这里你有这个记录,包含两个属性,27 是第一个属性,Joe 是第二个属性。所以在这个特定的实现中,我们稍后会讨论各种实现方式。在这个特定的实现中,这个页面中的这个位置也有指向这个特定记录的指针。
页面。所以基本上是在这种情况下编码页面 ID,即页面 ID 为三,以及页面中的槽 ID。所以我这里有点不严谨,但其实是在页面中的槽 ID,我们之前谈过过槽式页面布局,对吧?因此,记录 ID 本质上是记录指针,是页面 ID 和槽 ID 的组合。这就是 27 所指向的内容。好的。
所以这个 27 有类似页面编号的内容,你已经大致知道了页面编号,不对,它指向的就是这个页面编号。并不是。所以在这里,你可以看到 24 其实没有其他任何内容出现在这个图片中,但假设我们假设一下,这是 24。24 当然可能指向另一个页面。
所以这并不意味着它与单一页面完美对应。它可能分散在各处。明白了吗?好的。所以你确实需要维护页面 ID,以及页面中的槽 ID。好的,谢谢。好的。还有其他问题吗?好的。那么接下来,我们讨论 B+ 树。B+ 树实际上是一种索引结构,旨在处理所有这些问题。
我们之前讨论的方差就是指这个。B+ 树无处不在,你到处都能看到它。它是一种非常强大的索引结构,是一种动态树索引。它允许你非常有效地修改、添加和删除记录。它始终保持平衡。所以这是B+ 树提供的一个很好的保证,它保持这种平衡。
即使数据被插入或删除,B+ 树也能保持其结构的稳定性。它还具有较高的分支因子,这意味着这些树的宽度较大,而不是深度较深。这一点很重要,因为如果到叶节点的路径较短,就意味着I/O操作更少。对吧?所以,如果你到叶节点的路径较长,就意味着会有更多的I/O操作。你肯定不希望这样。这里我想提到的最后一个特点是它支持高效的插入和删除操作。
如果你想基于关键字添加或删除记录,你当然可以这么做。B+ 树的一个有趣的细节是,它并不是在叶节点处增长,而是在根节点处增长。所以我们稍后会看到为什么会这样。那么,为什么是 B+ 树?“+” 代表什么呢?这些术语来源于 B+ 树的前身叫做 B 树。
中间节点也存储数据。B+ 树存储的是数据条目,基本上是指向叶子中记录的指针。这是因为它在范围查询时非常有用,我们稍后会谈到为什么会是这种情况。所以现在几乎总是使用 B+ 树,而不是 B 树。
所以这是我们要研究的那个节点。好的,现在我们来谈谈如何读取 B+ 树。好的,我们从这个内部节点开始。正如你所看到的,抱歉,我得退回去。好的,这个内部节点正如你看到的那样,有指向这些叶子页的指针。
所以我把它们标成灰色,而那些蓝色和浅蓝色的部分是内部节点。这里的读取方法是,5 和 13 之间的指针表示在这个指针下方的叶子页面中的值的关键字大于或等于 5 且小于 13。好的,这对这个例子是成立的。
所以这是 5、7 和 8。好的,那么在这种情况下,你有一个指向这个叶子页面的指针。这里的指针指向的值介于 13 和另一个端点之间,而另一个端点没有上界,因为这个槽位还没有填满。好的。同样,在这个例子中,你有一个上界但没有下界。所以你有小于 5 的值。
这仅仅适用于这两个值。好的,这是读取 B+ 树内部节点的一种方式。接下来我们来谈谈另一种读取 B+ 树内部节点的方法,那就是把一个内部节点看作一对值和指针。好的,这是其中一对值和指针。这是另一对值和指针。
这是值和指针的第三种组合。那么这个值和指针实际上是什么意思呢?
好了,值和指针。每个内部节点有一堆值和指针,这里浅灰色的值没有显示。它可以被有效地省略,这就是为什么它在这些内部节点中被省略以节省空间,但你可以把它想象成在这个例子中是零,这对我们的例子是有效的。就像我说的,这些内部节点是值和指针的组合。
所以它是一个键值和一个指针,B+ 树提供的保证是,K L,即键值,必须小于或等于 K,也就是该指针指向的子树中的所有键。好的,如果在这种情况下,这个子树,它指向的这个指针,所有的。
值大于或等于零。对吧,所以在这个简单的例子中,在这个特定情况下,这个子树中的所有值都大于或等于五。同样地,对于这个指针,所有的值都大于或等于 30。好的,这也是另一种方式来读取 B+ 树中的内部节点,我们将会继续。
使用以下的片段表示法。所以我只是说明当我谈论五和对应五的指针时,这个子树的样子。好的,那么整个 B+ 树的样子是这样的。现在暂时不需要担心底部的这些指针。
让我们来谈谈树的结构以及每个节点中的值。这里是内部节点,那里是叶子节点。然后,B+ 树提供的占用保证是每个内部节点至少部分填满。好的,提供的保证是,节点的数量介于 D 和 2D 之间。
好的,所以 2D+1 是内部节点中指针的最大数量。在这种情况下,2D+1 等于五。好的,B+ 树提供的保证是每个中间节点中的条目数量会介于二和四之间。同样,它在插入和删除发生时保持这个保证。所以这个 D 值。
这是下界,称为树的阶。好的,所以它被称为树的阶。所以这个规则的一个例外是根节点。根节点不需要遵循这个保证。正如你在这个例子中看到的,根节点只有一个值,没有两个值。
所有其他的内部节点都有两个值。我认为这是键的位置。所以在这种情况下,内部节点不包括根节点,对吧?这是你们使用的约定吗?是的,我把根节点当作内部节点的一部分,但我确实提到过,根节点不需要遵循这个规则。所以也许更好把内部节点称为“节点”,而不是根节点或叶子节点。
是的,我想我们可以这么说。取决于惯例。所以我刚才是指内部节点,作为所有非叶子节点。但我们也可以把它分开,分别称为根节点,中间的所有节点以及叶子节点。这样也可以。还有其他问题吗?有一个问题是为什么值等于二。好的。
所以在这种情况下,你可以做的反向计算是,你可以查看。你希望每个节点都至少是半满的。你有四个槽位用于值,五个指针,因此你希望它是半满的,因而你希望它有两个值。好的。这个保证就是B+树提供的。
所以根节点不需要遵守这个规则,因此它只有一个值。它可以有更多值。所以这是树内部唯一能够消散这个不变性的节点。好的。那么相同的规则也适用于叶子节点,至少是这样。所以这些叶子节点必须至少是半满的,就像这些内部节点一样。
叶子节点的处理值在这个特定实例中可能不同,当前值是相同的。所以再次强调,所有这些叶子节点都至少有两个值或者两个键,以及它们指向记录的指针。好的,所以这些叶子节点也遵守相同的不变性。这些不变性,指的是每个节点至少半满,根节点除外。
这个特性是始终保持的。好的。B+树的另一个特性是,如果你回到第一张幻灯片,底部的叶页面不需要按顺序存储,对吧?因此,它们可以存储在任意页面中。在这个特定的例子中,第一个叶子节点存储在页面2,第二个存储在页面3。
然后是我的内部节点,存储在页面4。接着是页面5,页面6,页面7,页面8,页面9。没有规定这些叶子节点需要按顺序存储在磁盘上。另一方面,因为我们没有按顺序显示它们,所以有这些。
指针,允许我们按顺序遍历这些叶子节点。所以下一个和上一个指针将有助于按顺序检查它们。我们稍后会讨论为什么这很有用。好的,那么让我们来谈谈这个扇出和它如何影响B+树,当我们增加层数时。以这个特定的例子来说,我,我的。
我的树的阶数是2,我的扇出是5。因此,我可以支持的记录数是20。好的。所以这里有五个指针,每个叶页面可以存储四个记录。所以这个B+树可以支持20条记录。好的。
所以这些指针每个都指向一个特定的记录。让我们稍微增加一下这个值。假设我有一个高度为三的 B+ 树。所以再次强调,扇出度 D 是 2。因此,这棵 B+ 树的阶数保持不变。扇出度再次是 5。所以,从根节点到中间节点或内部节点有五个指针。
从每个节点,我有五个指针。而从每个节点,我又有五个指针。好吧,基本上是五乘五乘五乘四,因为每个叶子页包含 0.24 条记录。好吧,这样包含四个键的相关信息。所以总的来说,我有 500 条记录,或者说 500 个键的相关信息。好,有什么问题吗?好。
那么,让我们做一些简单的数学计算,来理解 B+ 树在实际中的表现。D 等于 2 是一个非常不现实的数字。所以在现实世界中的 B+ 树,节点只有 4 个槽位的情况是非常不可能的。通常来说,槽位会多得多。例如,页面的大小可能是几百千字节。假设每个值和指针对大约占 40 字节。
对了,记住,一种思考内部节点的方式是将其视为一组值-指针对。那么,粗略地说,这大约对应每页 3200 个值-指针对。所以每个内部节点,举个例子来说。然后,这大致对应于 2D 或 2D+1。我们不那么精确地假设是 2D。好吧。所以既然是 2D。
所以阶数是 D,将是 1600。再假设在现实世界的 B+ 树中,我的 2/3 的条目已被填充。再次提醒,我们在这里有点草率,假设每个内部节点的占用情况是这样的。所以我就草率地假设一下。
假设 2/3 的这些条目已被填充。如果这些条目有 2/3 被填满,那么平均的扇出度大约是 2100。实际上,这意味着如果我有一棵高度为 1 的 B+ 树,那么我可以支持大约 450 万条记录的查询。如果是高度为 2 的 B+ 树。
那么我可以支持最多 100 亿条记录。所以这是一个大幅增加。而且,这对于一棵相对较小的 B+ 树来说,已经是相当大量的记录了。这一切都因为扇出度非常大。所以最大扇出度大约是 3000。但在这里我们假设填充度为其 2/3,对吧?
所以,和 3200 相比,大约是 2000。即使如此,你也可以支持大量记录,同时保持树的深度较小。所以在这种情况下,深度为 5 和 6 的 B+ 树实际上并不需要,对吧?
除非你有一个非常非常大的数据集,对吧?即使是高度为2,也就是有两层内部节点,然后是一堆叶子节点,你也可以支持100亿条记录。好吧,有没有关于这个的问题?Ditya,有人问这个D到底指的是树的高度,还是条目数。
对吧?我认为这是树的高度,对吧?它是指D,或者说是高度。你能重复一下问题吗,Evan?人们在问这个D指的是什么?
哦,这里的D,这个D,指的是树的阶。基本上,它是B+树为内部节点提供的占用保证。我们正在讨论上一张幻灯片,那个包含D+树的。我们在问这个问题,因为树的高度是三,但我们在讨论的是。
在问D等于什么。哦,D仍然是2,对吧?所以它仍然是树的阶。它基本上确保你至少有两个值填充在所有内部节点中,而不是根节点。所以这是占用,最小占用,对吧?
它不是树的高度,对吧?是的,D不是指树的高度。它只是树的阶,至少在我遵循的约定中是这样。所以在这个具体的例子中,如果我们特别讨论我这里的例子,让我们来做一下这个数学计算,对吧?我有一个根节点,对吧,根节点有两个,1,4,4。
假设有指针向外,然后每个都有两个,1,4,4个指针向外。然后我有每个都是叶子节点,对吧?
所以我想我在有些地方稍微宽松地定义了高度。抱歉,我在这里是不是犯了个错误?不,抱歉,我指的是这个,而不是那个。
好的,让我重新开始,重新试一次。
哎呀,继续。好吧,好吧,重新开始,重新尝试。好的,所以高度是1,我有一个根节点。我只有叶子页,对吧?所以我的扇出从根节点开始大约是2,000,然后从每个叶子页开始大约是2,000。所以这是一个高度为1的树。我只是在计算内部节点,或者说树的内部部分,而不是。
到达叶子节点,这就是我为什么称它为高度。如果我愿意,我也可以称之为深度,称它为2。这样,这棵树只有一层,这一层对应根节点,然后有一层。对应叶子节点,它可以支持大约2,000乘2,000的记录,对吧?
而这棵树有三层,根节点,然后一层内部节点,再然后一层叶子节点,它可以支持2,000乘2,000乘2,000的记录,对吧?大约。大致上说,这大约是100亿条记录。明白了吗?
有人问21,44是如何得到的。好的,所以我之前说得有点随便。我的总插槽是3200。好的。所以这是我的2D,这是我可以指向的所有内容的总数。我做了一个假设,假设其中三分之二的插槽已满。
所以这就是为什么我从3200减到大约2000的原因。那么为什么是3200 2D呢?嗯,那是因为我,如果我回到这个话题,我有一个页面的大小,然后我可以在这个页面中编码的信息量,对吧?
比如说,我可以在每个页面中编码的值指针对的数量,我需要40字节。将128千字节除以40字节,得到3200个插槽。这是我可以使用的总插槽数,用于指向其他内容。这是插槽的最大数量。所以这就是我的2D,或者如果你想计算指针的话,就是2D加一。
所以这大致是2D的阶数。好的,再次说明,我这里说得很随便。我只是想让你对规模有个大致的了解,但希望你能理解。所以这里的主要结论不是67%或21,4,4,或2D或3200或9,8,5,5,4,0,1,9,8,4。而是一个事实:用一个非常浅的树,你可以访问大量记录。
所以你不需要一个很深的树结构就能访问很多记录。这是这里的主要点。如果你没有完全理解细节也没关系。Carlos,嗨,我只是想澄清一下什么是D。所以D是内部节点的最大占用率还是最小占用率?它是最小占用率,对吧?通常是半个占用率。所以基本上你提供的保证是树的阶数。
也就是说,最小占用率是你拥有的插槽数的一半。所以,在这些例子中,插槽的数量是四个,并且你保证至少有两个插槽是满的。好的,明白了。那么接下来的问题是,选择的D值是给定的还是随机的?是随机的。我们在设计一个大加树时选择它。对的。
在接下来的幻灯片中,我们将讨论如何为叶子页面和内部页面选择不同的编码方式等等。但总的来说,D的定义是树的阶数。它是一个保证,D加树保持的性质。
通常,它是节点中插槽数量一半的大小,无论是叶子节点还是内部节点。难道2D加一规则只适用于内部节点和根节点吗?所以像叶子节点那样,最末端的节点可以有任何数量的条目。对的。所以,嗯,不是的,我认为,嗯,内部节点有一个特定数量。
值的数量和一定数量的指针。对吧。所以值的数量是2D。扇出是2D加一,对吧,因为你每个值都有一个对应的左边和右边的指针。对。所以在这个特定的例子中,你有一个,两个,三个,四个,五个指针,而只有四个值,一个,两个,三个,四个。对吧。
所以在每两个值之间,左边和右边的最右侧值之间。所以这就是为什么你会有2D和2D加一,分别对应值的数量和指针的数量。现在,叶节点是不同的,对吧。所以叶节点你有这些键值,然后是指向记录的指针。
所以基本上,指针就相当于你有一个一对一的对应关系,指向记录的指针和键值本身。所以在这个特定的例子中,2D,即叶节点的大小,仍然是四个。我们提供的保证是其中一半将被占用。
那么任何B+树的记录数将是2D加一的高度次方再乘以2D。是这样吗?像我说的,嗯。好的。谢谢。嗯嗯。这里发生的事情正是如此。对。所以我们可以将树的高度乘以四。就是2D。嗯。那为什么是2D?我不明白这个和之前的例子有什么不同。
2D。所以,如果你考虑一下,嗯,嗯。如果你考虑内部节点或叶节点中的值,假设它们的数量是相同的,对吧。那么你在内部节点中有2D,在叶节点中也有2D。现在,内部节点中的指针数量是一个,即值的数量加一。
而在叶节点中,你有的指针数量和值的数量是一样的。所以这就是为什么内部节点在这个例子中基本上有五个。它是2D加一的高度次方再乘以2D,就是指针的数量。来自叶节点。这有道理吗?我需要定义根节点吗,还是,还是。
根节点来自哪里,对吧?所以像内部节点都有五个,而你说叶节点只有四个。四个。是的。哦,好吧。所以,当我们定义D时,叶节点只有四个元素。为什么是四个?因为叶节点的结构与其他节点非常不同,对吧?
所以结构,叶节点,内部节点有相应的指针。所以如果你看一个内部节点,它基本上是说,假设这是值V1,V2,V3,V4。那么这个指针指向小于V1的内容。这个指针指向介于V1和V2之间的内容,小于或等于V1,然后小于V2。
对不起,大于或等于V1小于V2。同样对于这些指针。因此,它是一个加上值的数量。另一方面,叶子节点基本上有键V1,V2,V3,V4。每个键直接指向记录,对吧?所以不是。你没有一个加上值的数量。你有的和值的数量一样。
所以它基本上是一个不同的结构。是的,这是一种思考方式。它是一个不同的结构。而且,通常,我的意思是,指针也可能看起来不同,对吧?
所以这取决于情况。稍后我们会讨论设计叶子页面的不同方式。你可以使用三种替代方案。我们稍后也会讨论这些。谢谢你。现在,我们假设所有的叶子节点都是四个。并不一定。这是最大记录数,对吧,在这种情况下。所以实际上,叶子节点,再次强调。
你对它们半满的保证是类似的。教授,您能解释一下高度的约定吗?所以在这种情况下,在这张图中,高度3就像是你在内部节点有两层,然后你有一层用于叶子节点。下一张幻灯片就像是你有。
下一张幻灯片的底部是1的高度,像2,144的平方。这就像是你只把叶子节点留在那。那就是高度。总的来说,内节点的数量加上叶子节点层次,从叶子节点的节点来看,还是吗?
是的,我想我在下一张幻灯片时搞混了。但基本上,我想表示的是,高度是指除了叶子节点以外的所有内容。所以叶子节点是一个单独的层次。因此,在这种情况下,你有三层节点到根节点。所以这是第一层,这是第二层,然后是第三层。所以是5的3次方乘以4。
这是针对叶子节点的,对吧?所以就是这一张幻灯片的内容。然后对于这一张幻灯片,我有。对于高度1,我基本上只有根节点,就这些,对吧?
所以树中没有其他内容,其他的一切都是叶子节点。就是这样,才可以支持。所以这两张幻灯片的约定是一样的,只是我搞混了。没错,谢谢你。我想水。是的,所以我想问一下,我们什么时候讨论使用B+树代替B3树的权衡?
我们会在讨论如何使用B+树来获取数据时谈论这个,尤其是范围谓词。所以在最高层次上,如果你基本上想找到位于某个范围内的值,那么把所有指向数据的指针放到底部是高效的,因为一旦。
一旦你找到匹配的第一页,就可以直接扫描。那可以帮助你避免必须回到树的上层然后再下来。太棒了,谢谢你。好的。为了澄清我的理解,如果我们遵循上一张幻灯片的约定,对于一个高度为1的B+树,我们的扇出是2144。
我们假设最大记录数是2144乘以3200,对吗?不。所以最大记录数是3200,对吗?因为那是2D乘以3200,大约是这样。也许这应该是3200加3200,再加上1次3200之类的。但是大体上是这个数量级。我知道。最大记录数也假设每次分支都是最大数量的页面。
谢谢。是的,沃伦,你还有其他问题吗,还是这就是全部问题?
这就是整个问题。好的。好了。我们周二见。你们还有时间问一个问题吗?当然。那么这是一个已经回答过的问题,就是关于K的大小与B的大小,就像几张幻灯片之前提到的。我没有完全理解。我的直觉是每个块可以包含多个记录。
所以这让我觉得K应该大于B。那么我在这里错过了什么?是的,我的意思是,好吧。
所以我认为那个相同的K可以…就像数据块可以包含多个记录一样,键块也可以包含多个键。
所以在这里,我试图传达的是,你可以存储的东西要少得多。再说一次。回到我的学生例子,你有对应学生名字的键,并且有很多属性。你能够放入给定页面的记录数量是有限的,因此你可能需要很多页面。另一方面,这些页面。
在这些关键查找页面中,你基本上只需要存储键和值指针。就这样。你不需要担心其他任何事情。你不需要存储剩余的属性。因此,你可以在给定的页面中放入更多的键。另一方面,你不能将太多信息放入相应的页面中。
至于数据,因为你必须存储所有的属性。好的。那么哪部分不清楚呢?
好吧,所以你刚才的回答听起来像是每页的记录数与每页的键数之间的比较。但我以为比较的是键的总数与页面的总数。对。所以那是它的影响。如果你能在一个页面中放入更多的键,你就需要更少的页面来存储所有的键。如果你能放入更多的记录。
如果你只能在页面内放入更少的记录,那么你需要更多的块,因此B会更大。这就是我们所说的数量级差异。因此,K基本上是键的数量。我在做出这个声明时,嘿,假设你有…我不知道,这是情况,但我该怎么说呢?好的。K是键的数量。
你需要的页面数来存储与这些 K 键对应的信息,可能远远小于你需要的页面数来存储所有记录的信息。所以至少在这个层面,希望这是清楚的。好的,嗯,关于这一页中的 K 和 B 的问题,对吧?
这一页中 K 和 B 的例子是什么?这里我猜我在传达的是这里的总键数,约 40。但我实际上没有展示所有数据页。所以我不能真的做出那个声明。我不知道需要多少数据页。所以我不知道在这个例子中 B 会是多少。
嗯,K 仍然是我们想要存储的键的数量,对吧?
然后 B 是需要的页面数,对吧?Nathan,这回答你的问题了吗?
好的,是的。抱歉,我有另一个问题。没问题。那么在这里,我会说索引中的 B 加三是指那些不包含任何记录的节点,对吧?
我再说一遍吗?所以在那些中间节点中,它们不包含任何记录。那为什么不呢?
我的意思是,如果某些记录在中间节点中,那我们可以直接获取该记录,而不需要向下遍历到叶节点。对,这就是一种权衡,对吧?有一个好处是你不必额外获取页面并向下遍历。但我认为在大多数情况下,按顺序查找记录的好处更大。
对于范围扫描,权衡考虑到从中间节点查找记录的好处。好的,我们将在下节课讨论范围扫描。哦,我明白了。而且,如果你真的将记录存储在内部节点中,那么你就减少了实际上可以存储的指针数量,对吧?所以分支因子实际上会更低。
是的。所以存储记录通常不是一个好主意,因为你减少了存储其他事物的空间。但假设你的意思是存储指向记录的指针,而不是记录本身,那么即使如此,这也是个坏主意。或者至少它不如支持范围扫描那么好。好的,所以,这里有一个权衡,如何选择分支因子,分支因子的大小应该是多少,对吧?
是的,没错。我的意思是,这取决于你拥有的数据量。事实上,你要做的项目中,你将根据存储的信息类型来确定分支因子。我明白了。好的,最后一个问题。那么我们如何分配索引,这些索引是否在某种程度上反映了某些顺序?
记录是怎么生成的,或者说,它们是怎么生成的?抱歉,我不太明白你的意思。那么这里的索引,比如根节点中的 17,5 和 13,它们是怎么生成的?它们是否反映了某种顺序,还是……?
不,它反映了你关系中的某个属性,对吧?
所以你基本上是在尝试检索与你的关系中的某个属性对应的数据。所以,Alvin提到过为什么你的内容,索引实际上是做什么的,对吧?它基本上是为了支持对应某个属性的高效访问。所以这个属性可以是像名字这样的字符串,也可以是某个数字。例如。
它可以基于,我不知道,学生ID。也可以基于其他什么东西,对吧?
它可以是任何东西。不,但通常它是你关系中的一个属性。或者你认为是主键的东西,对吧?比如在SQL中。是的。好的。但是看,像索引,有些索引并不是任何记录中的属性。你可以基于给定记录中属性值的组合来构建索引。
这些被称为复合索引。所以你当然可以这么做。你甚至可以存储部分索引之类的东西。事情变得相当复杂,但通常对应于你的关系中的某个特定属性或某种方式的属性组合。好,好的。是的。暂时是这样。
就把它当作你在表中定义的键的值,对吧?所以在三明治的例子中,那就是你看到的海员ID,对吧?像17,对吧?是的。所以基本上。你想用什么来检索你的数据?对吧?以这种方式来考虑。我是说。
这是另一种思考方式。你唯一确定的是什么,唯一决定你关系中行的是什么。所以一个键或者你可能需要用来检索数据的东西,对吧?所以如果你经常想根据学生ID来检索数据,那么就使用它。
当你使用它来构建索引时,例如。好,明白了吗?好,好的。对。听起来不错。Alvin,你想稍微同步一下吗?好的,当然。好的。好的。好的,谢谢大家。下周见。
再见,大家,下周见。再见大家。
P6:Lecture 6 Indices & B+ Tree Refinements - main - BV1cL411t7Fz
一切都好,好的,看起来还好,所以我要继续我停止的地方,有三个索引,呃,但在我开始之前,我想宣布几点,所以嗯,主要公告是要查找的公告是,TS发布的每周公告,太棒了,嗯,这是号码,呃披萨店。你应该去仔细看看,它有所有的细节,呃,本周所有相关的事情,两项特别通知,一个是讨论维生素答案和脚踏现在是允许的,所以在147号帖子中提到了这一点,如果你已经开始看项目二,我建议你开始考虑项目。你一定要上完今天的课,如果你还没有,呃,有很多辅助材料,比如一个演练视频和图表,帮助您理解代码库,嗯,那是链接在,呃,一号广场,四一,好的,这是三个公告,有什么问题吗?呃在我走之前,是啊,是啊。这样我们就能拥有在这个项目上工作所需的一切,今天的讲座之后,那将是希望,是呀,是啊,是啊,如果我们,如果我们,如果我们保持稳定的夹子,恢复我们想要的,是啊,是啊,它倾向于是的,好的,所有的权利,所以嗯。
我想重温B加树的样子,因为快下课的时候我觉得有点匆忙,我觉得有某种,也许你知道一些问题出现了,我无法充分回答,这就是a b加3的样子,好的,所以你有,这是b加b加3的高度,这些是内部节点。所以这是两个层次,这些是叶节点,好的,这是一个特殊的节点,称为根节点,嗯,和我们的惯例,我们将在所有的图表中遵循,这些内部节点将具有蓝色,而这些叶节点会是灰色的,所以首先要认识到的是,叶子页。这些页面不一定按顺序排列,大家可以看到,第一页实际上对应于根节点,第二页对应于叶子节点,可能三个对应于之后的叶节点,那么第四页对应于这个节点,所以没有真正的秩序是由,呃,与B加树相对应的页面,好的。所以什么都没有,嗯有点像,B加没有相应的顺序,但是代码中的B加T,因此,它允许您根据,呃,命令,即使与B加3相对应的页面没有按顺序排列,它仍然为您提供了按键查找数据所需的一切,好的。
所以这里要找的另一件事是这些指针,所以这个指针在24到30之间,引导到这个叶节点中的所有键,四十岁和三十岁,在两个四边有一个小于或等于,嗯,关于B树要注意的另一件事是,位于,底部有指向记录的指针。所以每把钥匙,呃,指向与该键相对应的记录,好的,所以记录上有三个三点对应于那个键,好的,这就是你读B加树的方法,所以有内部节点和叶节点,它有一个规则的树状结构,现在,让我们谈谈B树的性质,呃,服从。好的,所以第一个性质是B加树中的节点必须服从,占用不变量,好的,所以说,这让我们可以保证,B+树上的查找成本实际上是有界的,那么这个不变量是什么样子的呢,嗯。不变量表示内部节点中的每个节点都超过了某个最小值,因此每个内部节点都有一定的最小条目填充量,在这种情况下,通常每当你听到b加树,这个数字通常是条目的一半,所以它至少是半满的,所以这个数字。
这是每个内部节点的最小值,装满了,通常被称为树的顺序,所以这是一个用来指填充的术语,每个呃的最小填充系数,B加Stri内的节点,所以在这里你可以看到,有四个条目,一个,两个,三个,每个内部节点中有四个。所以参赛作品的最大数量是四个,就这样,它的一半是二,好的,然后呃,所以嗯,所以我们的保证,任何内部节点的条目数都在d到2之间,d,好的,在这种情况下,它在两到四之间,不需要遵守此属性的节点是根节点。所以根节点不需要服从这个不变量,同样的不变量实际上也适用于叶节点,所以至少有一半的叶节点,对不起,每个叶节点必须至少半满,所以d值可能不同,但至少在这里是一样的,所以这是第一个属性,所以是这个不变量。它确保B加树中的每个节点,除了根节点是特殊的,至少是半满,第二个属性是,正如我们前面看到的,底部的叶子页不需要按逻辑顺序存储,好的,因为它们不是按逻辑顺序存储的,我们仍然需要一种方法来按顺序引用它们。
然后这些指针,红色和绿色指针将帮助我们按顺序检查它们,这将是有用的,我们将在后面看到,好的,那么让我们来谈谈a b加t的刻度,所以给定a b加t,它能有多少记录,呃点它能索引吗。所以这是一棵高1B加的树,因为只有一层内部节点,然后是叶节点,这里只是根节点,叶子和一组叶子节点,那么这个高度1B加上树索引可以很好地索引多少条记录,你可以看着它,把它弄清楚。所以你有一个扇形的五个从根节点,然后每个叶节点有四个指针,嗯,所以是五乘四对吧,所以是二十块,所以我前面提到的最大条目数是四个,扇形出去是五个,因此,对于一棵高的树来说,看起来像这样。你在根部有五个指针,叶子上的四个槽,因此你可以,嗯,向上看,你可以捕捉,你可以索引,你可以指着二十条记录,让我们把它放大一点,让我们来谈谈一棵高三b+树,它有一个类似的,呃D和两个D好的。
所以在这种情况下,每个内部节点的指针数是五个,每个内部节点的最大条目数为4,好的,那么有多少节点,有多少记录,我们能从B树指向,好吧,再来一次,你可以把这个数学做对,所以根节点是五个,然后五为二级节点。然后是三级节点的五个,然后对于每个叶节点,你有四个记录是正确的,所以这是一个数学,所以你从根部有五个,第二层五个,三楼五个,然后四个指针,从根到记录的每一个,所以总的来说我们有五个立方体。呃乘以四乘以四,也就是这棵B+树可以指向的500条记录,我想有问题,所有的权利,问题嘿,你能不能,嗯,我有个问题,在前面的幻灯片中,树中的数字有什么意义吗,有十七个和一个根节点,例如。像第5页、第13页和第4页,是呀,绝对正确,所以这些是你的钥匙,呃,你呃,使用B加树进行索引,所以呃,我们将讨论如何阅读B加树,当你试图查找一个值,或者你试图插入一点,所以呃,如果还不清楚的话。
我们将通过例子来讨论,这些数字意味着什么,它们是如何有用的,好的,好的,数字本身并不指向任何东西,只是数字外面的指针,所以这些这些,这些数字并不能说明什么,指针用叶节点编码,嗯,在内部节点上有指针。这些指针是显式绘制的,至少在我展示的图表中,嗯所以,如果你有五个风扇,为什么叶子节点是四,所以我们说的是参赛作品对吧,所以这里和条目,我再次保留它,这是这是一个特殊的图表,这实际上取决于,呃。可以在叶节点中填充多少项,在这个特殊的图表中,i或在这个特定的b加树中,我在叶节点中有四个条目,每个内部有四个,否,好的,只是为了让事情变得简单,所以我在整个b+树中有相同的顺序,也就是两个,因此。有两倍于此的条目,也就是四个,嗯,扇出是一次,加上内部节点,这有道理吗,好的,谢谢你的澄清,好吧,假设没有问题了,我没看到有人举手,所以好吧,所以我们可以用这棵高3b+的树指向500条记录,好的。
所以事实证明,呃,B加上实践中的树比这大得多,这是因为每个内部节点和每个叶节点,指出更多的东西,我们有更多的参赛作品,所以让我们做一些角度计算,试图计算出我们实际上可以索引多少记录,用一个现实的B加树。如此逼真的V再加上3,我喜欢,我希望这最后一次是草率的,返回包络计算,我在重复这个,只是因为我觉得,嗯,我我有点呃,也许没有我解释得那么好,呃想,假设我有,呃,一个两个8千字节的页面,好的。我有每一个条目,因此每个值指针对,我用40个字节来编码,然后通过简单的除法,我可以在一个页面中编码的最大条目数,这可能是一个内部节点,或者这可能是一个叶节点,嗯是二十八千字节除以四十字节,大约三千。好的,马扇出将是三千,加一个右,所以它是一加条目数,但我们不要担心这个,我们也叫它三千吧,好的,所以整体参赛作品呈扇形展开,你会叫它三千,好了,现在,假设这些条目中的三分之二平均再次填写。
这是信封的背面,计算计算和马虎,我知道一半的条目会被填写,因为这是B+树给我的保证,但我说好,假设平均三分之二的人被填满了,好的,所以再一次,这只是一个假设,对嗯,所以有了这个数字,中间节点的平均扇出。或者平均参赛人数大约是两千人,好的,所以有了这些能力,一个怎么,一个高的能有多少记录,加三点二,你有两千个来自根的指针,所以这是一个从根部出来的扇形,对呀,根部指向的叶节点数,然后每片叶子有两千个条目。对呀,所以这两千乘以两千,那大概是四百万,好的,所以这是一个高度,一棵B加树,它已经,呃,指向索引,呃,允许您查找400万条记录,如果你有一个高二p加三,你可以做得更对,呃。所以你有2000个来自根的指针,对呀,所以这就是,然后第二级的两千个指针,然后每片叶子两千,对呀,所以有80亿行或80亿条记录,对呀,那是大量的记录,你可以用B加树的高度向上看,所以基本上是三个深度。
三个或更多的高度将允许我们索引大量的记录,如此频繁,b加上实践中的树木,你看不到B加上深度五度及以上的树,因为你不经常需要它,好的,现在让我们来谈谈我们如何解释这些数字,我们实际上是如何使用B树的。好的,那么我们如何搜索,呃,在B加树中,所以过程是你看每个节点,您可以在每个节点上找到要遵循的正确拆分,然后按照指向下一个节点的指针,在下一个层次,那么我们如何操作它,假设我想找两个七,好的。所以我从根节点开始,然后我说,好的,比十七岁少二十七岁,更新于十七,嗯,大于等于十七,对呀,所以我要沿着这条路走,我沿着这条路径到这个节点,好的,所以这个内部节点现在是二十七,24岁以下。二十四至三十岁,或大于30,嗯,现在是二十四到三十岁之间,对呀,所以我要沿着中间的路径下到这个叶节点,我在这个叶节点中找到了我的记录,这是与二十七相对应的记录,所以现在我找到了这个叶节点和指向,记录。
指针实际上有什么帮助,这对我有什么帮助,这个指针实际上会是一个呃,为了那个为了那把钥匙二十七,它将是与该键相对应的记录ID和记录ID,就像我们以前看到的那样,在我们讨论被屠杀的页面时。所有的记录ID都是对应于页ID和槽ID的一对,呃为了呃,对应于这个未排序的堆文件,好的,所以这是针对这个未排序的堆文件,这个叶节点条目将告诉我在哪里查找,在那个未排序的堆文件中。它会告诉我页面ID和页面中相应的插槽ID,我需要抬头,那太好了,所以我,我用这棵B+树找到了我要找的记录,和内部呃,这里的值确实为我指明了正确的方向,对呀,所以它只是把我引向正确的方向,好的。所以没有找到对应27的单一记录,假设我想找到27岁及以上的所有记录,所以我想找到所有对应于,呃,大于或等于二十七,所以我们按照两个步骤,所以第一步是,我们找到对应于2 7的记录,如果存在的话。
然后我们按照指针遍历树叶,这就是这些指针派上用场的地方,好的,所以我们按照指针遍历树叶,检索对应于2的记录9到3 3 4 3 8,三九等,这实际上是所谓的范围扫描的一个例子,好的。所以这是当你在一定范围内查看值时,a逗号b,在这种情况下是,呃,二十七,逗号无穷大,让我们说的权利,那么拥有这些指针的好处是什么,为什么我更喜欢直接看树叶,嗯,好处是我不需要对每一个键都重复这个。我想抬头看,我不需要重复这个,回到树的开始,然后再往下走两九,3。三也是这样,3。三也是这样,3。四也是这样,3。三也是这样,3。四也是这样,4。以此类推,对呀,所以我不需要做所有这些,我可以留下来。嗯,在树叶水平,然后查找所有感兴趣的记录,在对叶节点的一次扫描中,好的,所以我不需要回到树上,好的,这就是搜索任何关于搜索的问题,是呀,B加树有什么特别之处,就像。
为什么我们不能说使用范围最小查询进行范围搜索,对不起,范围最小查询是多少,RMQ,数据结构,范围最小查询,它会给你,呃,根据范围返回,这是范围的常见结构,我只是想知道B+树有什么特别之处,所以B加上树。好的,那么B+树的特别之处在于,将其与您过去可能了解过的其他数据结构进行比较,有几个方面,第一个方面是它是,嗯,它是,我是说,在某种意义上,它类似于类型和其他类型的数据。但是您过去可能看过的其他类型的数据结构,对呀,例如,它可能让人想起二分搜索树,对呀,所以它类似于二分搜索树,但是有一个更大的风扇,因此它现在是一棵更短更宽的树,嗯,还有一个我还没有描述的方面。B+树在你支持的情况下是相当有效的,当您添加和删除时,呃,记录,我现在就要谈这个,与B+树相对应的另一个方面,使其特别适合于数据管理,那是呃,我们正在操作的每个粒度都在相同的页面上,对应于磁盘上的页。
所以这种对粒度的思考,你用它仰望,具有相当有效的索引结构的磁盘上的数据,所以从某种意义上说,它相当适合,并意识到这样一个事实,即您需要这个索引结构是超快的,它需要支持非常非常大的访问,然后呃,同时。每个磁盘IO都很珍贵,对呀,所以你不想有,比如说,一棵很深的树,谢谢。还有其他问题吗,所以补充一下,安德鲁,就像你提到RMQ的时候,我是说这基本上是一种查询,所以回答这个问题的方法。你还不如为此使用B加树,但你不必,显然你也可以使用标准的B树,就像在,嗨,是啊,是啊,嗯,所以我想这个问题已经在聊天中问过了,但是如果这些页面不需要按顺序存储,为什么能为雨教堂,为什么我们能就这样走。我们能假设什么,我们可以从27点开始跟着前面的指针,是啊,是啊,所以我的意思是,至少对于给定的叶节点,你有东西,它都存储在一个节点中,对呀,全部存储在一页中,所以我不需要查任何其他页面对吧。
所以至少如果我没事,所以回到那个例子,呃,二十九,我不需要查另一页,仍在同一页中,嗯,让我删除其中的一些注释,所以也许我想这就是这里令人困惑的地方,好的,所以在这里你可能看到了,有这些指针,对呀。红色指针,它是一个后指针和一个绿色指针,它是一个前进的指针,这让我知道我应该查的下一页,这一页对吗,对应于这组叶子条目,到节点项,这样我就可以知道下一页我应该查哪一页,所以我遵循这些指示。我知道接下来该看什么,即使它们可能不是按顺序存储的,谢谢。嗯,假设没有更多的问题,嗯,让我们谈谈插入,好的,那么我们如何插入,呃A B加树,假设我想在这个b+树中插入两个6,嗯。我从遵循与搜索相同的程序开始,所以我沿着这条路走,因为这是,呃,二六等于二十四到三十岁之间,所以我找到了右边的叶子,嗯,如果这片叶子里有空间,我可以添加条目,好的,所以我有能力添加这个新条目,然后呃。
我快做完了,嗯,但在我完成之前,我可能也想排序,所以我可能想对叶子中的条目进行排序,嗯,只是为了确保我有一个有效的方式,如果我想抬头,呃,呃,叶页中的键,好的,所以让我们再举一个例子。所以这是一个简单的例子,我有一个槽,在那里我可以插入一些东西,假设我想把它插入到这个特定的树中,所以我又一次找到了正确的叶子,所以在这种情况下,它是左手边的第一片叶子,我想把它插进这片叶子里。所以不幸的是这片叶子已经满了,它不能容纳更多的人进入,所以我需要把这片叶子劈开,所以我要创造另一片新的一页,这让我可以移动这个真正完整的叶子中的一些条目,在这个新的叶子上,呃叶子。然后在两片叶子之间均匀地重新分配入口,好的,所以我在这两片叶子里有五个条目,所以我要在第一个里面放两个在第二个里面放三个,好的,所以我在第一个留了两个,在第二个留了三个,然后我要修复下一个和上一个指针。
我们在树底部的红色和绿色指针,对呀,所以我要五七八,后面的叶子后面的两片三片叶子,好的,所以我去插了这个,但我们做得不太对,呃,这棵树有什么奇怪的,嗯,没有任何东西指向这个特定的叶节点,所以我们。这个叶节点基本上是经常,你要确保鱼礁叶节点可以通过,树内部或树内部的节点,所以为了补救这个,我要做的是从叶子上复制,中间的键和指向孤儿叶的指针,好的,所以现在这片叶子是孤儿。所以我需要找到一种方法来确保它是可访问的,所以我会拉起来,或者至少我会复制这个中间键5和一个指向这个的指针,呃叶结,这就是我们访问它所需要的,好的,这是这些,这对是我们访问这个叶节点真正需要的。然后我要试着把这个合并起来,这个呃这个条目,这个中键和鹦鹉的指针,不幸的是,这位特殊的父母也是完全正确的,所以这个特定的父级有两个D项,有四个条目,现在你想写第五个条目,两个两个D加一个条目。
所以我没有空间做那件事,所以我要做的是,我又要分开了,所以这里将是一个索引节点拆分,或内部节点拆分,所以我必须在这里创建一个额外的节点,然后我要把最右边的d+1键移到,右边的节点,所以我重新分配了钥匙。我现在有两个在第一个,第二个三个,我还得把这个家伙合并到这个节点上,但我们稍后会处理这个问题,但这里的另一个问题是,我们现在有两个根,我们再也没有根了,所以我们需要想办法看看,从单个根查找树。所以要做到这一点,我们添加一个新的根节点,所以我们创建一个新的根节点,然后我们把中间的节点向上推,到根节点的中键,好吧再来一次,播放那个动画。
好的,所以再一次,我创建了一个根节点,然后我向上移动中键到这个根节点,这样我就知道这个子树中值的起点是什么样子的,好的,所以这个插入的净效果是我有,左右D键,D加1,二加一,他已经向上移动到根部了。事实上,我的钥匙在左边和右边,意味着不变量现在满足,这是伟大的,对呀,所以我们想开始,我们从满足不变量开始,我们插入了一些东西,现在我们又以不变量为结束,所以我还有最后一步,我想把这五个合并成这个。内部节点,我还没有完全那样做,所以这很容易,我可以去做那件事,现在我做对了,在这个特殊的例子中,我们最终在中间分裂了节点,产生了一个新的根节点,这实际上是相当罕见的,对呀,这是相当罕见的增加深度。因为我们看到典型的B+树,你没有,啊啊啊,你可以用三到五个做两个,三到五层的深度,或者三到五岁的高度,你实际上并没有增加深度,你不会经常添加新的根,好的,所以这是相当罕见的,这种事很少发生。
在这个特殊的例子中,它确实发生在我们的例子中,所以这里有一个特别的细微差别,我想指出,细微差别的第一部分是我们复制的叶条目,因为我们想保留原来的钥匙,所有的钥匙都需要在树叶里,也就是,呃。允许我们正确查找所有记录的保证,所有的键都必须在最小的,然而,索引条目,中间的入口,当我们有两个D加一个或一个过满的中间节点时,中间的条目实际上是向上推的,而不是向上复制的,所以那个入口是向上推的。它被推高的原因是我们不再需要它了,用于在此节点中路由,我们在这个节点上不需要它,因为它没有给我们任何额外的信息,所以这是一个有助于记住的细微差别,一次又一次,在你计算出几个例子后。你就会得到b+树的直觉,所以我鼓励你去,在家试试,嗯,尝试几个插入的示例,看看结果是什么,嗯,如果有问题,当然啦,请在广场上询问,好的,所以让我们给你一个高级算法,草图um所以B加树插入算法。
第一步是找到正确的叶子,这是标准的搜查程序,你沿着叶子往下走,直到找到正确的叶子,呃,沿着树走下去,直到你找到了合适的叶子,如果你在那片叶子里有空间,然后你把它放在那里,然后你就完蛋了,好的。如果你没有空间,然后你需要把L分开,好的,所以你把L分成L和一个新的节点,我两个,现在,您需要确保您仍然可以从上面的节点指向l 2,所以嗯,所以说,你做的第一件事就是。你在l和l两个之间均匀地重新分配条目,你复制中间的键,也就是,呃,指向l 2的指针,中间的键是l 2中的值的开始,然后将指向l 2的索引项插入l的父元素中,好的,所以你,呃。通过将指向l 2的索引条目插入l的父条目,2。你要保证找得到我,因为你所做的只是做了一个新的节点,你没有任何机制来查它,通过向l的父级添加l 2的索引项,你确保可以看,有时您没有此索引项的插槽,因此。
您需要递归地正确执行此操作,因此,您最终可能会拆分该索引节点,您均匀地重新分配条目,在拆分索引节点的情况下,你把中间的节点向上推,和指向已创建的新索引节点的指针,好的,所以你不抄袭,你把,呃。在树的内部,这就是拆分索引节点时发生的情况,另一个值得记住的直觉是分裂,基本上最终长出了这棵树,但是树的生长方式真的很有趣,所以它不会以自上而下的方式生长,它实际上是以自下而上的方式生长的。所以树越来越宽,如果可能的话,从下往上,如果它在某个时候变得太宽,它基本上最终增加了另一个层次,呃,用一个新的根,好的,B+树就是这样生长的,这种自下而上的生长确保了平衡。所以我们想确保在任何时候更深的树都是平衡树,嗯,还有,呃,余额,加上您有这个占用不变量,确保对数保证,所以为了我们的插入,我们有一棵高度为1的树,只有内部的一个根节点,然后在插入后,你有高度2。
对所以基本上我们把它分成两份,嗯这里,然后我们把这个一分为二,然后我们增加了一条新的路线,所以这就是最终发生的事情,我们是自下而上做的,所以我们将跳过删除,好的,所以在实践中,我提到的占用不变量。在删除期间通常不强制执行d约束,您只需删除叶子条目,并留下空间,所以这只是你在练习中有点马虎,在删除期间,嗯,您可能想要这样做的一个原因是,如果新插入,来到那些地方,伟大的权利,你有那些插槽。你可以填写,呃,那些条目,我们这么做的另一个原因是,有点马虎也没关系,因为保证还是很好的,所以嗯,如果页面变得完全空,你可以把它完全删除,父母可能会因此变得满员,如果呃,如果您最终删除了一些叶页。那也没关系,所以总的来说,你得到的保证,即使你对删除做得不多,你只是有点马虎,你实际上没有强制不变量仍然很有吸引力,你还有一个日志,嗯,插入到树中的总数,对,嗯,所以这仍然是一个相当有吸引力的数字。
尤其是因为扇形很大,教科书描述了一个相当复杂的算法,删除时的再平衡和合并,所以你经常有偷窃的想法,呃,他来自邻近的节点,然后合并附近的两个节点,或者兄弟姐妹嗯,为了强制占用不变量,教科书描述了这些算法。这不是我们在这门课上要重点讨论的问题,好的,所以我们谈到了搜索,插入和删除,所以我们讨论过一次插入一个记录,和删除,我们有点跳过删除,但我们说过一次插入更像是一个记录,嗯,我们谈谈散装货吧。以及为什么散装装载是有价值的,所以假设我们想从头开始构建一个索引,好的,所以你有一个很大的表,你想从头开始构建一个索引,你要怎么做呢一个选项就是反复调用INSERT,对呀,所以对于你拥有的每一把钥匙。您只需重复调用INSERT,为什么这是个坏主意,我们将不得不使用更多的iOS来回到页面,我们要把它,是啊,是啊,我们必须使用更多的iOS才能进入页面,我们想把它插入右边,所以基本上你会从根部开始。
一直到叶子节点,对于您插入的每一条记录,那是很多iOS,对呀,还有其他原因,第一个原因是你需要不断地从根目录搜索,嗯,第二个原因是你喜欢修改随机页面,对呀,所以如果你有一棵像这样的树。前一分钟你可能会在这里插入一些东西,下一分钟你就在这里插入了一些东西,所以您正在查找的相应页面,堆文件和呃,在树中随机选择,你在这里的现金效率会很差,另一个缺点可能不如前两个重要。但仍然重要的是树叶利用率很低,通常最后是半空的,好的,所以这些都是为什么,如果要从头开始创建索引,简单地一次插入一个不是一个好主意,所以有一种方法可以通过批量加载B+树来避免这些问题。这种从下到上的方式构造了一个B+树,嗯,我们会看看这个方法是如何工作的,好的,所以这种方法的第一步是按键对输入记录进行排序,好的,所以拿着钥匙,然后按这个对记录进行排序,嗯我们会的,呃。
我们还没有完全弄清楚如何进行分类,当数据非常大时,我们很快就会讨论的,也许从现在开始的几节课上,我们学习了一种很好的基于磁盘的排序算法,那么好吧,所以嗯,所以第一步是按键开始输入记录。然后一旦你按键对输入记录进行了排序,我们开始填充叶子页,我们填充叶子页,直到一些填充因子,嗯,让我们说四分之三,嗯,这也可能是六分之五,嗯或三分之二,所以任何,可以使用对应用程序有意义的任何因素。当这些叶页被填满时,相应的父级也会得到更新,所以内部节点也会得到更新,并且更新此父级,直到父级满为止,到目前为止,我加了一二三,我加了四五六,我在这里加了四个,这样我就可以抬头看了,在这两页之间。当我添加更多的叶页时,我向父级添加更多的增量,所以现在我父母已经吃饱了对吧,所以这个根节点,这个内部节点现在已经满了,我需要创建一个新节点,如果我想容纳这个新的一页,所以要做到这一点。
我遵循了插入过程中用于索引拆分的相同方法,我把那个节点分成两个节点,然后在上面增加另一个层次,在这种情况下对吧,所以在这个特殊的情况下,我们不得不在,所以我就像我说的你创造了一个新的兄弟姐妹。你复制了一半以上,你向上移动中间的指针,嗯,就像你做的索引,呃,用于插入的节点拆分,批量装载的有趣之处在于当你不断添加东西时,树的左下部再也没有碰过了,好的,所以一旦这个被填满。树的这一部分再也不会被碰了,以此类推对吧,所以你一直有树的部分再也没有碰过,还有这个呃,此方法还保持占用不变量,以防你担心插入什么,工厂的一个记录肯定保持占用不变,bug批量加载也保持占用不变,有多好。第一个保证是叶子填满了,呃超越d,因为我们确保了,至少在这种情况下,四分之三的条目是填写的,其余节点由插入拆分过程管理,如果你超越,嗯二加一,它会分裂成d d d,然后把中间的键向上移。
所以我们将继续这样做,并再次确保不变量保持不变,好的,所以在这个特殊的例子中,只是通过更多的动画,你可以,呃,当新的一页页被填满时,您不断添加,您不断添加到父,然后现在又一次,这个家长现在已经满了。因此,您最终可能会创建一个新的父级来容纳下一组叶子页,你可以移动其中的一半,呃,这里有一半的条目,中间的就会移到鹦鹉身上,好的,所以中间的值将移动到父,所以在这个特殊的情况下,你不需要创建一条新的路线。因为根部已经,呃,已经,呃,已经,呃,空闲插槽,好的,那么散装的好处是什么呢,这里有三个好处,首先,与插入到随机位置相比,您可以获得更好的缓存利用率,你就是你,你有点。你一次在一棵树的一个协调部分上工作,一旦你完成了树的一部分,你忘了这件事,你移动到下一部分,您可以更好地利用叶节点,因此较浅的树木,这是另一个好处,叶子页更按顺序组织,好的,所以当你取页的时候。
你有更多的地方,尤其是,例如,如果你在做范围扫描,好的医生,所以总的来说,B Plus树是一个超强的动态平衡索引结构,至于索引,插入和删除到B加树,并确保树保持高平衡,成本是对数,呃,f,呃。其中n可能表示节点总数,呃,插入的记录总数,这个高度和扇形意味着你通常没有一个B的高度,不超过三四个,有一个相当宽的B的另一个好处,加树是更高的级别经常被频繁访问,它们以现金形式存在。你最终避免了昂贵的磁盘,i,o,对于那些较高的水平,a,B加三几乎总是比维护一个排序的文件好,做这件事的间接费用不值得,呃,其中的好处,好的,尤其是在面对插入和删除时的维护。B加T比排序文件处理得更流畅,所以出于这个原因,树在数据库系统中应用非常广泛,我们还谈到了散装,批量加载可以比创建b+树的重复插入快得多,在一个非常大的数据集上,这就是b加上树的所有结果,在我开始之前。
关于这一点的任何问题,呃,索引文件,呃,呃你好,我只是在想,呃,恐惧因子只适用于叶节点吗,呃,呃,我在过去的幻灯片,是啊,是啊,所以在这个特殊的案例中,我们最终,呃,只对叶节点应用填充因子。在中间节点中,我们最终得到了标准顺序,好好照顾它,所以基本上它们会装满一半,就像B加树中的典型情况一样,你当然也可以让叶节点有一半,如果你愿意,嗯,如果您想更好地利用叶节点,那就更好了,把它装满。我也在想,呃,呃,我们为什么要用呃,恐惧因素,呃,对于叶节点,是啊,是啊,所以说,这只是一种优化,以确保你最终不会,比如说,非常稀疏的叶节点集,所以如果所有的叶节点都只是,你会有更多的叶节点,因此。高于它的水平会更高,我也是,对呀,所以它只是,只是更多的开销,所以有一个交易,你如果是一半,一半的条目都填了,你有更多的插槽来添加新的东西,所以这是一个好处,但是树木,树变大了,所以这里有一个权衡。
是啊,是啊,好的,谢谢。嗯,我在想,呃,如果您继续批量装载到,就像,呃,您展示的示例,我们会不会以,呃,就像一个不平衡的,不平衡B加树,否,好的,所以好吧,让我试着把这个例子画得更清楚一点,好的。所以说,好的,我要摆脱,呃,不不不不不,我想我的问题是我们不用左手,最,你喜欢那个音符,好的,我无法清除笔迹,不太理想,好的,让我看看我是否能继续前进,好的,所以假设我有,呃,二十五,呃,二十六。二十七,好的,这是我的下一个叶节点,现在我想,现在我想在父母身上加二十五,是的因为我需要25英镑来引导搜索,所以我最终要做的是,我最终会把这个节点分成两个节点,一个对应十三,第二个对应于。二十二和二十五,然后我会有呃,中间值,也就是十九,会被推到这个里面,呃,根节点有意义吗,好的,好的,明白了,谢谢。呃米饭,呃,是呀,所以我有两个问题,所以一个问题是对于叶子来说,现在我们要解决它吗。
嗯是的,所以我们保存,呃,叶节点中的键按排序顺序,这样我们就能更有效地查找,所以您可以在叶节点上使用二分搜索,在叶节点内按键查找记录,但是如果,如果活动节点数很少,我看不出解决它有什么好处。我是说在这种情况下,对不起,我不太明白,所以你的问题是,如果你有少量的叶节点,那么你就看不到正确排序的好处,因为如果你有大量的深层节点,在这种情况下你最大的参赛资格是,然后我我我的意思是。如果你在做二分搜索和线性扫描,我想这肯定不会有什么不同。是啊,是啊,我想我是认真的,看情况吧,所以如果在这个特殊的例子中,也许它太小了,无关紧要,但是如果你有大量的指针,数以万计的订单。您可以从按排序顺序维护它中受益,那是设计上的选择,对呀,这样你就可以避免以排序的顺序存储它的前期成本,但在你抬头的时候付出惩罚,或者您可以按排序的顺序维护它,然后让它看起来更便宜,好的。
从lib节点的实现细节来看,呃,使用双,呃,使用双链路,增加,呃,因为我的意思是,i,所以这将使用双链接列表,呃,执行呃,在离开节点中,是啊,是啊。您可以把它看作是每个叶节点都有一个指向下一个叶节点的指针,对呀,呃,这样你就可以做这个范围扫描,好的,呃,最后一个快速问题,所以每一个淋巴结的每一个入口,它会给你一页,或者它会指向报纸上的记录,是啊。是啊,所以我们,呃,所以既然这是,呃,所以最终目标是查找记录,所以我们要做的是对叶节点中的每个键,我们将有与该密钥关联的记录ID,因此记录ID将是页面ID的组合,以及该页中的插槽ID,好的。所以让我们就像指向一个,呃,一页的一部分,然后指向一个嗯,记录,本质上是一个记录,是呀,好的,谢谢。
让我们进入下一组幻灯片,好的,到目前为止,我们一直在讨论a b+树索引,呃,指向堆文件中无序的页,好的,所以事实证明,这并不是我们能采取的唯一方法,还有一些设计选择,嗯,第一个设计选择是叶节点的选择。所以这是,呃,您可以将其视为索引和数据之间的接口,所以这是叶子节点,嗯,这是一个选择,呃,您希望叶节点看起来像什么,第二个是你希望实际数据是什么样子的,好的,所以我们将讨论各种替代方案。因为叶子节点有三种基本的替代方案,这也适用于其他类型的索引,当您有相应的数据条目时,对呀,所以当你从键到数据,这个,同样的论点,呃,替代办法也同样适用于这些情况,当然我们会关注b加3。所以我们在B加树的上下文中看它,但一般的讨论也适用于其他索引,所以这是三个基本的选择,教科书也对这些备选方案使用了相同的编号,所以我们按顺序看它们,所以第一个选择是按价值,第二种选择是通过引用,嗯。
这是我们已经看到的,嗯,第三种选择是通过引用列表,那么我们该怎么办,这些都是什么意思,因此,第一种选择是按值在叶子页中存储信息,这意味着叶页直接存储记录,他们不储存,指向记录的指针,他们直接存储记录。所以在这个特殊的例子中,这是你的亲戚。和叶子页,这里的这几页,他们直接存储了这种关系的记录,例如,此页存储为逗号,乔,和和三个收敛,比如说,所以这样做的好处是你不需要正确地遵循指针,你跳过一步。您已经将数据直接存储在叶节点中,您不需要查找指向记录ID的指针,然后从未分类的热管中查找一页,另一种选择是我们到目前为止一直关注的另一种选择,是通过参考对,所以这里是每把钥匙。我们将匹配数据记录的记录ID存储为一对,每个叶子中的索引项都包含键逗号记录ID对,所以在这个特殊的例子中,我们有,嗯所以,比如说,键2有一个与之相关联的记录ID,记录ID基本上说查找第一个。
所以这是记录ID,记录ID是页面ID,我们说的对是k逗号,这就是叶节点处的索引所包含的内容,所以叶子中的每个条目都是k逗号记录ID,这是一对,记录ID是页ID和槽ID,这是我们之前用的。这种有参考熊的表示,基本上干净地将索引与数据解耦,所以这个数据保持原样,索引指向数据右,因此此数据保留在未排序的堆文件中,索引指向数据,好的,另一种选择是通过引用列表,如果您有多个与给定密钥匹配的记录。这将很有帮助,因此,此上下文中的每个叶条目都是键值,嗯,匹配数据记录的记录ID列表,所以在这个特殊的例子中,你有三个记录对应于密钥二,好的,所以你有乔,Jim和K,所以你有一个每个记录的记录ID列表。对呀,所以你有一个对应的乔,一个对应吉姆,一个对应K,他们每个人依次是一对,也就是页面ID和插槽,这个替代方案比替代方案更紧凑,对并将其存储为单个列表,所以你不会重复同一个键多次对吧,所以它更紧凑。
另一方面,这个替代方案有更多的簿记,呃,因为如果你有一个很大的,呃,记录ID列表,所以如果你有一个重复许多的钥匙,很多次,这个大的记录ID列表可以跨越多个块,所以你需要知道你没有关闭括号,呃。在记录ID列表中,所以这是你需要管理的一种簿记事情,如果您有一个跨越多个块的记录ID列表,所以这是你唯一需要担心的事情,如果你有参考对,你不需要那么担心,因为你永远不会有那个问题,一切都是一对。备选方案2和备选方案3中的备选方案1很重要,所以备选二和三呃索引数据通过引用,所以他们把索引保存得很好,呃,与数据完全解耦或分离,如果我们希望通过引用支持每个表的多个索引。所以使用替代免费的替代实际上是必要的,否则我们最终将复制整个元组,所以如果你有两个索引都对应于另一个索引,您最终将为这些索引中的每个元组存储一个副本,对呀,按值方法的一个问题。
所以问题是为什么这是一个问题,那么为什么复制元组是一个问题呢,我们可以一致地获得我们的数据,是啊,是啊,所以基本上复制通常是个问题因为,保持嗯,副本,保持两份副本的一致性,对呀,因此。如果您在多个地方重复相同的数据,这个问题在以后的课程中也会重复,我们有相同的数据,呃,在多个地方,如果你对其中一个进行更新,你也需要对另一个进行更新,当您进行这些更新时,这会产生更多的开销。所以总的来说,你想最小化,呃,复制,因为这会产生更多的开销,好吧嗯,它也增加了存储空间,但那是另一回事,所以存储通常是一个小问题,与维护的开销相比,元组的副本彼此一致,因此。复制会导致更新过程中的复杂性,因为这个原因你想避免它,这是因为您需要确保数据的所有副本都保存在,好的,所以我们讨论了叶节点,让我们谈谈堆文件,好的,所以嗯,我们可能想如何更好地组织它,所以通过参考索引。
因此,备选方案2和3可以聚集在一起,也可以不聚集,实际上,这实际上是与索引关联的堆文件的属性,而不是索引本身,所以聚集索引,基本上,呃,确保堆文件记录保持大部分有序,好的。所以大多是根据索引中的搜索键排序的,所以这个堆文件顺序不需要是完美的,它不需要绝对完美,排序顺序,这只是在,你想说,呃,到这个到你想保留的数据库系统,呃,尽可能按排序,所以即使这是一个建议或暗示,嗯。好处是相当大的,因此,通过聚集索引检索数据记录的成本要小得多,比通过不受信任的索引检索它的成本高,我们明白为什么,所以注意,这里,这与您可能在其他设置中看到的集群有点不同。尤其是在数据科学、人工智能或数据挖掘聚类方面,通常在那种情况下,好的,所以这是聚类这个术语的不同用法,我们谈论的是数据的一个非常物理的性质,对呀,在磁盘上相邻聚集的数据也是如此,比如说,好的。
那么我们如何构建聚集索引,嗯,所以要建立一个聚集索引,我们基本上首先对堆文件进行排序,我们在每个块上为将来的插入留出一些空闲空间,然后我们在未来尽可能地尊重这个秩序,所以对堆文件进行排序的过程。然后为以后的插入留出一些空间你以前在,这个讲座,是的,所以有人回答,呃,关于我们在讲座中看到的聊天,基本上是散装,所以批量加载实际上是产生聚集索引的一个很好的方法,好的。所以这是集群索引的一个很好的起点,问题是你如何随着时间的推移保持这一点,我们会谈谈的,好的,所以聚集索引,您尽量尊重排序的顺序,在非聚集索引中,你没有这个限制,好的,所以一个集群,聚集索引如下所示。你有你的索引,你的叶子页在底部,叶子页经常指向,磁盘上连续页序列的排序,呃,在堆文件中,所以说,比如说,这个叶子页只指向两个,呃,堆文件页,这一个只指向一个堆文件,在非聚集索引中,所有的赌注都取消了。
所以你有一堆链接,从叶页到磁盘上与堆文件相对应的页,对呀,所以基本上,只是嗯,无拘无束,呃,用这个叶子页,比如说,指向一二,三个,四页,就在呃,这一页至少指向这两页,这三页,可能好吧,那么这意味着什么。在实践中,假设我想在这个特定的键范围内找到记录,在集群指数中,呃,事情好一点了,对呀,所以您只需要在堆文件中查找这三页,在非聚集索引中,你需要查找这五个关键页面,好的,那是因为指针一般是纵横交错的。解簇可以任意坏对,所以在最坏的情况下,如果你阅读的范围足够大,非聚集索引,基本上会告诉你查看文件中的所有页面,那根本没用,好的,所以这些就是聚集索引的好处,那么你怎么处理,呃,插入件,对呀,因为嗯。如果数据保持静态,聚集索引很棒,但是如果将数据添加到聚集索引中,然后事情就变得有点棘手了,所以对于处理插入,一种方法是在文件末尾为插入分配块,好的,所以说,让我们说,让我们看一个例子,所以让我们说。
我有这个特别的,我顺着索引往下走,我发现我要插入的这个特定的键对应于这个叶子页,它没有免费的条目,嗯,或者更确切地说,这个特定的数据页面没有免费条目,嗯,所以我基本上在最后分配了一个页面,呃,堆文件。我把这个新记录添加到那个页面,然后我得到的指针都指向这个蓝页,它是满的以及呃,文件末尾的页面,所以这还不算太糟,因为对于这个特定叶子页的大多数数据,我可以查到这个蓝色的条目。或者我可能会看到文件末尾的几个街区,查阅余下的纪录,所以数据记录的顺序往往接近,但与排序顺序不相同,因为你最终会有这些插入物,那么聚集索引的利弊是什么呢,主要的优点是它对范围搜索非常有效,因为地方利益。您将获得顺序访问福利,还有预取,所以如果你知道你要在磁盘上查找下一个块,您可以预取它也支持某些类型的压缩,我们稍后会讨论压缩,缺点是什么,嗯,维护权利的成本更高,以便在插入数据时。
您需要弄清楚如何维护此群集属性,如果你不保持,如果你有很多插入物,它最终看起来更像是一个未聚集的索引,而不是多次插入后的群集索引,所以要保持,我们基本上需要周期性地更新堆文件的顺序,我们有两种选择。另一种选择是我们只是在飞行中做这件事,这意味着每次更新更贵,嗯,但这确保了查找性能良好,呃总体来说,还是懒洋洋地做对了,所以每次更新都更便宜,但在多次插入后,性能可能会很好,从而降低维护成本。堆文件通常只打包为,呃,三分之二或四分之三,以容纳额外的插入件,所以这就是我在这一部分的全部,在这一点上我有什么问题吗,阿尔文,你想开始分享,或者真的很快,当你说它懒洋洋地更新的时候,那是懒惰的意思吗。从某种意义上说,它是更新的,就像,就像在其他一些事情上,就像溪流一样,它说的是懒惰,是在那个意义上吗,或者你不维护它。
否,所以基本上我想的是,是你吗。
比如说,将其添加到堆文件的末尾,嗯,当您进行插入时,然后你周期性地重组和度假,好的,所以它就像断断续续的维护来保持它的秩序,但你不是每次插入都这样做,准确地说,好的,所以你你这样的,嗯。确保如果超过某一点有一定的退化,您不再获得群集的好处,你说,好的,我要重组,然后你支付组织的一次性成本,好的,伟大,谢谢,呃,还有一个问题,您能再解释一下如何使用批量加载来构建聚集索引吗?当然。所以基本上,聚集索引的属性,您的深度文件页是按顺序排序的,大部分和嗯,就是这样,所以它是堆文件的属性,而不是索引和批量加载时的批量加载,您要做的第一件事是对堆文件进行排序,对了,你在对堆文件进行排序。然后你在之后构造B+树,所以大批量装载基本上自然地给了你一个聚集的索引,好的,我明白了,好的听着,我想我们应该去找艾尔文,如果还有其他问题,我很乐意和他们聊天,好的,所以我想说,所以现在让我们看看,呃。
我做的所有这些手术的费用,我之前说的是B+树,正如你所记得的,这是我从呃复制的一张桌子,两节课前不同类型的呃文件对吧,所以在这种情况下,我们有窥视文件,我们已经对文件进行了排序,然后希望你记得。这些是我们上次讨论过的数字,特别是我们前面定义的这些常数,当我们谈论堆文件和排序文件时,所以我们基本上想重复同样的练习,但现在我们想重复,做那些做那个,索引的计算,对于B树索引。我们再次对普通案例感兴趣,因为最坏的情况通常很无聊,对呀,我是说就像平等搜索,最坏的情况是我们只需要扫描一切,但那并不有趣,因为这并不区分我们拥有的不同类型的文件和索引。所以这就是为什么我们对一般情况感兴趣,还记得阅读和写作的权利都要付出代价,iOS,我们把磁盘上的东西带到主存里,我们给它做手术,然后我们可能需要写回来,在插入或删除的情况下的权利,对于这两种类型的操作。
呃,我们需要招致,iOS和iOS是我们在这门课上关心的成本,到目前为止,我们一直假设CPU计算是免费的,所以在我们把页面带进记忆之后,那么扫描的实际成本,扫描所有元组,就像,你知道,与,如果如果呃。其中任何一个满足查询,依此类推,诸如此类,所有本质上是免费的,当然啦,你知道的,实际上那不是真的,但我想这会让事情变得足够复杂,好的,所以嗯,为了计算的目的,为了这个目的,呃,讲座的一部分。我们将假设我们使用的是替代方案二,我刚才说过的阿迪蒂亚,从某种意义上说,我们在索引的叶子页中存储引用,所以你在下面看到的所有这些红色箭头,屏幕中基本上是指针或记录ID,对呀,呃为了真正的臀部锉。所以这些指针基本上都会告诉我们,臀部文件中的插槽ID,还有我们说的是哪个臀部文件,当我们想要检索一个特定的元组时,我们还将再次假设这是一个带有,呃,三分之二的堆页面失败了,这就是我们所拥有的场因子。
你知道作为一个提醒,从最后几分钟开始,Aditya基本上是在说如何聚集,基本上意味着堆文件被正确排序,这就是为什么你在叶子上看到的所有指针,在索引的叶子上支付的是一种非常好的排列,它们都是平行的。扇出是我们在这节课开始时已经讨论过的一个数字,所以这就像内部叶节点的分支因子,所以在这种情况下,我们基本上会假设散开是一种,按每块记录数的顺序,就像你可能想的那样,2。这个案子为什么会这样,在实践中。这实际上通常是一个很大的数字,就像在每一个内部,呃,索引页中的内部节点,在每个内部节点中,在索引中会有很高的Fano数量,因为有很多不同的,呃,它指向,很多不同的,呃,不同的页,为什么那口井,首先。我们通常使用的页面大小的数量,在实践中通常就像你知道的,不小,对呀,所以我们通常用4K或者1024K,甚至和一页的大小一样,所以在这种情况下,因为对于索引A的内部节点,我们基本上只是存储指针和密钥。
键的值,所以就像你知道的,我们可以储存相当多的,在单个内部节点内,阿迪蒂亚,我已经说过了,这意味着什么,对吧,根据可以指向的记录的实际数量,所以这就像你知道的,呃,假设。然后我们在这种情况下也假设静态撞击,因为没有更新,我们不喜欢在查找时更新或插入任何东西,所以我们不需要担心,就像其他人试图同时修改树一样,费用是多少,所以当我们真正谈论插入和删除时。我们只担心插入和删除,到目前为止对此有什么问题吗,所以这些基本上是我们之前做的假设,呃,当我们在谈论,当我们谈论索引时,好的,所以说,如果没有,让我们试着填满这张桌子的其余部分,对呀。所以同样类型的五个,我们要在索引上执行的五种类型的操作,就像我们想要在堆文件和排序器文件上执行的那样,然后对我们将要使用的不同概念进行相同的定义,第一个作为热身,我们想扫描所有的记录,好的,我们怎么做。
任何接受者,有什么猜测吗,只是顺序地穿过它们,是啊,是啊,我只是想通过它们,所以这有点像一个诡计问题,对呀,因为在这种情况下,我的意思是,我们真的需要使用索引吗,答案是不不对,我是说我们真的不需要索引。我是说我们手里拿着堆文件,我们只是从头开始,一直扫描到最后,就是这样,你可能以为成本会是B乘以D,但实际上会比这稍微多一点,对吧,为什么好,请记住,我们目前假设堆文件是正确的,堆文件中的每个页。只会被填满三分之二,所以这意味着为了保持与我们相同数量的记录,当我们没有索引的世界里,我们现在实际上需要使用更多,呃页面和臀部文件,到底会有多少人,呃一点五x多,对呀,如果你认为三分之二是四。所以这就是为什么我们的第一个数字是,你知道一点,一分,五次,到目前为止b乘以d,这很糟糕,对呀,因为这基本上向我们展示了,堆文件和排序文件的性能都优于索引,那么为什么要正确地使用它呢,哦。
等到你看到这里剩下的数字,好的,好的,现在让我们来谈谈平等搜索,对呀,我们想执行相等搜索,只是提醒一下,这意味着,我们基本上想首先使用索引来找出实际的页面,我们要查找的记录ID。然后我们基本上进入堆文件获取,呃,我们感兴趣的实际记录的页面,如果你还记得我们之前的两节课,我们,在这种情况下,假设总是只有一个记录匹配,在这种情况下,平等搜索,这样我们就不用担心这里的重复了。那么与此相关的成本是多少,用这两个步骤对,所以第一步是,呃,在雅思考试方面,对呀,这将是指数的高度,加一个,指数的高度是多少,我马上就会解释它的优点,嗯,指数的高度是多少。或者索引的高度将基于扇出的对数,对呀,我们有的扇形,嗯,给定整棵树的叶子数,那么整棵树的叶子有多少,嗯,整棵树的最少叶子数是b乘以r,除以e,其中b乘以r正好是记录的总数,E只是一片叶子上记录的平均数。
所以你记得从你61岁开始,就像你知道的,的数量,二分搜索树的高度,对呀,它将是对数基,二叉搜索树中节点数的两个,所以在这种情况下,它只是相同的,除了分支因子是f而不是2,因为我们正在发现更多。然后叶子的总数就由这里的这个数字计算出来,我们被e除,为什么有一加一的井,因为就像你知道的,为了找到实际的,为了真正找到找到嗯,呃,找到发现,我们感兴趣的记录,但我们也必须检索堆文件。所以这基本上会产生额外的成本,对吧,事实上,如果你还记得我们在谈论,树的高度,对呀,我们实际上只是在谈论所有的事情,直到叶子水平,我给你们举个例子,例如,如果我们四个中有一个风扇。然后如果我们有16片不同的叶子,我们想储存的权利,如果你这么做了,如果你算出数学,它只是一种二级树,对呀,根据Aditya在讲座的前一部分谈到的定义,加上树叶的高度。
我们从来没有把树叶的高度算作树的高度的一部分,如果你回到上一个切片,所以这就是为什么喜欢,在这种情况下,通过索引的总成本权将是高度,正如我们在前面的幻灯片中定义的那样,在这种情况下加上一个是叶子,呃。树中的叶节点,我们还没去臀部档案,对吧,所以这将是第二步,但在这种情况下请记住我们有这样的偏差,呃事情是对的,所以在这里总结一下这个例子,对呀,所以在这种情况下,呃,如果我们四个中有一个风扇。然后如果叶子总数是十六,然后这个高度,根据这个类的定义,它将是对数基,十六个中的四个,也就是两个,虽然我们实际需要的iOS数量是三个,因为我们需要穿过树叶,中间这个节点,最后是叶节点。所以这就是为什么总是加一,这有道理吗,所以这将是与第一步相关的成本,是呀,问题,这是否意味着平均来说,我们只需要获得树叶,不是两片叶子,每um两个不同的数据条目,通过这个搜索,就像日志十四六。
对数底座四十六,嗯,那只是我编的一个数字,不一定要对,我是说就像你知道的,树叶的数量可能有一百万,所以对于这棵树来说,特别是,我只是说,就像平均你会采取两个访问,我倾向于说左高通常是两个。因为根据阿迪蒂之前说的,就扇出的平均数量而言,和平均数,呃,页面大小,您可以存储相当多的记录与高度到树,所以在这种情况下iOS的数量通常是三个,好的,有道理,谢谢。是呀,是呀,好的,但我们做得不对。就像我说的,呃,早些时候,平等搜索需要两步,第一步是使用索引转到叶子页,在我们要检索的索引中,第二步实际上是去堆文件中找到步枪,对呀,所以我们刚刚谈到了第一步的成本,第二步呢,第二步其实很简单,对呀。因为我们已经从索引中得到了记录ID,它会告诉我们它会像,你知道的,槽号块和臀部文件将是一个创纪录的数字,在那个特定的页面里,所以与这一步相关的成本正好是一个IO,所以如果你,如果我们计算iOS的总数。
只是两者中的一些加在一起,乘以d,其中d只是获取页面所需的平均时间,这就是为什么在这种情况下我们得到这个特定的数字,所以这就是我们放在这里的,所以在这一点上,它仍然有点模棱两可,喜欢的权利,你知道吗。好的,那么这比深度文件和排序文件好吗,或者就像工作一样,嗯,这取决于你实际上在锁里有什么,对呀,但我们以后会看到的,就像另一个一样,其他数字也是如此,所以让我开始吧,然后我们可能就完成不了了,呃。因为我们还剩一分钟,我们走着瞧,好的,现在让我们来谈谈平等搜索,所以在这种情况下,我们试图找到所有的记录,右与键,键在两个不同的值之间,对呀,所以让我们在两节课前从三节课到七节课。我基本上给你看一个简单的方案,我们只是喜欢,你知道吗,在堆文件中查找开头,然后继续向右扫描,在这种情况下,因为这是聚集的,我们假设这已经排序了,所以我们基本上一直扫描到最后,对呀,所以这很容易。
如果我们有一个索引呢,嗯,我们使用类似的方案对吧,我们基本上是用索引,首先找出我们应该从臀部文件中读取的第一页,然后我们要扫描索引,我们需要从索引右页阅读的所有其他页面,然后基于此,我们要去堆文件。并按照索引中叶页的指示扫描所有页面,然后检索我们喜欢的所有记录,第一步的iOS数量正是我们之前谈到的,对吧,我们需要用一个索引探针来计算,就像,哪一页,是我们需要从堆文件中实际检索的第一个。这只是以前的数字,然后第三步向右,第三步是三步,呃,这将是一分,页数的五倍,在这种情况下,如果你还记得前两节课,这个页数是占位符,我们需要检索的实际页数,为了满足这个相等查询,所以在这种情况下。它只是三个,因为我们有三页来存储三到七页的记录,为什么又是三半好吧,那是因为那是以平均数为基础的,嗯,呃,低因素,我们对每一页都有权利,我们只假设会有三分之二满,对于每个堆文件页,所以我们需要,呃。
拿更多的页面,以满足,嗯,找出所有实际满足此相等查询的记录,然后为了争论,第二步,对呀,我们实际上还需要阅读多少索引页,以便找出相应的HE文件页,我只是要挥舞我的手字面上,然后假设这和第三步是一样的。这意味着我们只需要扫描页面的次数,就像,你知道的,呃,一点五,就在实际中这不会像那样糟糕对吧,因为呃,用于存储,呃,叶子,呃,索引的节点可以存储比类似的更多的指针,你知道的,括号的数量。存储在HIP文件中的相同数量的记录,所以在实践中,这实际上会比我们第二步需要的页面少得多,但就像你知道的,为了近似,为了让数学变得简单,我们只是假设这是同样的情况,呃,第二步,那么总的权利。它只是这三样东西加在一起的总和,现在有一个诀窍,在这种情况下,我们有一个负一,为什么会有一个负的,那里有一个负的,因为我们数多了,呃,索引的叶页,所以第一步已经发现了,你知道,呃。
我们需要扫描的叶页是什么,然后是台阶,嗯两个右,我们实际上要扫描所有的叶子页,相应地,对于这个特定的查询,但这已经算不清了,因为就像你知道的,我们需要扫描的第一页已经在第一步中被说明了。所以为了安全起见,我们只需减去一个,使算术看起来很好,或者让会计看起来不错,其实,这有道理吗,所以我就到此为止,所以如果有人有问题,请随时询问,嗯,举起你的手,或者像你在聊天中知道的那样,嗯。但如果不是,然后我们将继续插入和删除,星期四,是啊,是啊,呃,我有个问题你是关于,呃那个,替代方案和替代方案有什么区别,它只是备选方案三允许你用相同的呃存储多个记录ID,键值否。所以在两个备选方案中都有三个备选方案,您可以在备选方案二中存储具有相同键值的多个记录ID,你会把它作为对存储在备选方案三,您将其存储为,呃名单,如果不是这样,它将如何工作,让我们说,如果我只是二三五。
是啊,是啊,所以你只需要说5然后记录ID对应于5的第一条记录,然后五个逗号记录ID对应于第二个和五个逗号卡,随便吧酷,然后对于替代方案,三个将是一个列表,嗯嗯所以福利,所以我认为这两个方案很相似。与基于价值的方案非常不同,但很相似,主要的区别是备选方案3更紧凑一点,因为你要摆脱,呃,记录I很抱歉,每次都重复的键,对嗯,但代价是更多的维护,因为这些记录ID列表在某些情况下可能很长。可能跨越多个页面,所以你需要多做一点簿记,但这是一种更有效的表示,酷,谢谢。嗯,哦呃在是的,所以我也有一个问题,所以我想他提到了第三种选择,2。现在我们需要做一些记帐工作,那么在这种情况下。你到底要做什么簿记,是啊,是啊,想象一下,比如说,你有一个非常非常长的记录列表,这样它就不适合一个块了,嗯,所以它必须溢出到另一个街区,所以你有几个选择,一种选择是说,我要在第一个街区截断它。
有一个记录列表,然后在第二个块中使用相同的键重复另一个记录列表,好的,但后来我的我的,我的搜索算法需要记住没有一个记录列表,对应于该值,它需要,呃,隔着街区看,如有需要,另一种说法是,我会打开我的括号。有一堆记录ID,然后再次关闭下一个块中的括号,我需要想出如何表现这一点,并确保这不会造成任何问题,所以它更像是,它是,它不是,这不是一个巨大的开销,只是再多一点,呃,你需要小心一点,因为更容易表现。对呀,你只是你可以储存,你可以储存,把大量的对打包成一个块,你不需要担心在哪个街区,如果你,如果具有相同密钥的多个记录,嗯对不起,如果你有,是啊,是啊,匹配同一密钥的多个记录,只需跨越多个街区,它不是。没什么大不了的,我明白了,谢谢。我还有一个问题要问阿尔文,所以这就是我们如此一丝不苟的原因,就像所有的加1和减1对吧,我们不必,我是说,只是想确认一下,好吧,首先,只是为了折磨你们,好吧不不不。
只是为了确保我们真的明白,你知道背后的概念,就像你知道的,正在取什么页,你以后会看到,我们将把所有这些东西抽象成大O符号,所以所有这些都很重要,加一减一在这一点上并不重要,好的,谢谢。是啊,是啊,戴维。嗯,我可能会在这里插话说,加一个,当我们计算iOS的确切数量时,减一有时很重要,喜欢讨论,甚至考试,所以有时候这并不重要,好的,谢谢。大卫,呃,我们可以用哈希表作为索引吗,是啊,是啊,有不同类型的索引。嗯,肯定有基于哈希的索引,嗯,通常的好处,虽然在实际的数据数据库系统中,与um相比,基于哈希的索引不太受欢迎,与B加树相比有很多原因,其中一个是哈希片,索引对于范围扫描没有那么好,第二个原因是。至少从并发的角度来看,这是我们稍后将讨论的主题,呃,做得更好,所以这就是为什么基于散列的索引不是首选的两个主要原因,但是是的,接下来是基于哈希的,有很多研究,以及基于哈希索引的实际实现。
使用它的一个原因可能是因为你实际上没有,嗯,那是数字,这么说吧,你的记录就像,你知道的,所有的弦都对,那么好,我是说,如何比较字符串,我是说,比较字符串是一项昂贵的操作,所以在这种情况下。你可能只想把事情搞砸,然后呃,然后将其存储在哈希表中,我是说,也有使用b加树存储字符串的机制,只是耶,到耶,是啊,是啊,呃,是呀,我有问题,所以如果我们在V3上用于索引的键不是唯一的,怎么这么好。如果两个记录有相同的键,如果我想要其中一个,我只是喜欢搜索它,然后浏览列表,是啊,是啊,你有,是啊,是啊,你看所有匹配的,然后弄清楚你在找哪一个,对嗯,如果有匹配特定密钥的,如果你在找一个特定的。你必须申请和编辑,你得到了匹配的记录集合,第一个条件,它是基于钥匙,然后根据另一个条件进行筛选,对嗯经常嗯,你使用B加树的方式是,在其中一个谓词上使用B加树,嗯,可能是构建B+树的谓词。
在检索记录后应用其他谓词,这是使用b加树的常见方法,当有多个谓词时,我明白了,所以一个例子可能是这样的,你知道杰瑞刚才在聊天时说了什么吗,对呀,假设索引实际上是建立在,就像你的mod实际上是建立在。一个水手在销售之前,对呀,所以在这种情况下,你绝对可以想象两个不同的水手,航行了同样多的里程,现在让我们假设您的查询实际上试图寻找,你知道,水手身份证,就像,你知道十个,对呀。所以这实际上是一个唯一的数字,但您的索引恰好建立在,不仅仅不是那个特定的属性,对呀,它实际上是建立在其他东西上的,所以现在你能做的就是,你知道,尝试使用索引查看类似,你知道,也许我在找像,你知道,水手。呃,一键三连,你知道,那些被出售的人,是呀,我想出售,我做了十个,但他们也必须有销售,就像你知道的,特定英里数,所以你能做的就是,您可以首先使用里程索引,找出所有符合标准的水手。
然后在检索完所有这些记录后扫描它们,看看他们中是否有人真的有身份证,就是这样,两步过程,谢谢。哦呃,Felix,嘿嘿,所以我有一个更一般性的问题,不太像讲座内容,但更多的是像这样。这些像在这个像索引中这些是索引吗,类结构,有点像用硬件实现的,或者就像真实的生活,就像这些,我想我们有这些东西的原因,磁盘上的所有恒星显然是为了持久化,好吧,如果你出现在你的电脑上,然后你再把它打开。您希望能够一遍又一遍地使用索引,但我发现这样作为个人经历,我在过去的某个时候,我基本上从一台旧电脑和硬盘上提取了数据,你知道我把它插到我的新电脑上了,因为我想拿旧文件什么的,我注意到它就像,就像重建。从某种意义上说,它就像弹出了一个新的喜欢窗口,比如重建索引,所以我只是想知道这种喜欢,如果有一个标准化的系统,还是像依赖,就像操作系统一样,就是那种,可以为这些东西创建索引,或者如果有任何类似的系统。
类似的是普遍的,和,是啊,是啊,所以我认为这是个好问题,所以我想嗯,所以我认为你所说的指数是,比如说,最大构建,如果你,如果你重新安装什么的,就是你说的那个吗,具体的我不太清楚,但就像我说的数据磁盘。就像,只是我在获取数据,所以还好,好的,所以嗯是的,你说的对,我的意思是索引是持久的,因为它们本质上是数据,对,它们是数据,它们是辅助数据,但它们是数据,因此,您将它们保存在磁盘上,它们注定要永远存在。除非用户说我不再想要那个索引了,对,你当然不能为了另一种选择而这样做,但你可以为第二个和第三个选择这样做,你可以说用户可以说我不再想要索引了,这样您就可以删除索引,但如果这不发生,索引保持不变。索引旨在,呃,在停电和故障面前留下来,也可能是并发编辑,对呀,所以所有这些事情都可能发生,索引应该保持,我知道那很酷,嗯,我想我的我的更多,我的问题更多的是关于,就像,比如说,就像说。
在极端情况下,你的Mac上有第二个硬盘,然后你喜欢拔掉它,你喜欢断开数据,你在连接你的Windows电脑,就像知道这样的索引,Mac构建在磁盘的某个地方,像不像点赞,当我的意思是,我的标准是。与喜欢相比,窗户有可能吗,使用相同的索引,还是只是操作系统唯一,喜欢吗,所以是的,所以我认为有,嗯,我想回答这个问题的方法是,Postgres很难解释SQLite构建的索引,对呀。所以我认为跨数据库系统的索引和数据表示,不一定像你想要的那样便携,因为他们每个人都做出了自己的设计决定,以及如何表示索引,所有这些,所以说,我不认为它像,就像Windows和Max做出了不同的决定。我认为不同的数据库实现做出了不同的决定,但那说如果,比如说,你在旋转同一个数据库,又在波斯特雷斯,对呀,所以你有一个关于Postgres的数据库,再次旋转Postgres,它肯定能读到续集。
生活可能读不懂,但Postgres肯定能读懂,对,我明白了,好的,所以我想这两个是为了兼容性而建造的,所以你不必喜欢,太担心不,不幸的是不好,我明白了,所以他们必须重建他们的结局。它喜欢每个人在相同的数据上建立自己的索引,这不仅仅是一个索引问题,这也是一个数据表示问题,我不知道他们是否能读取对方的数据,没有加载方案,并导入数据库表示格式,我明白了好吧,否,他们通常不是。他们肯定不兼容,像Postgres和MySQL,是啊,是啊,他们使用不同的内部格式来存储实际记录,就像你知道这不是,它不是互读的,你必须做,呃,进出口过程,以便,按顺序,按顺序。以便一个数据库从另一个数据库读取数据,好吧,伙计们,这更像是真实的记录和东西,但如果你有类似的东西,像普通文件一样,就像你知道的,像PDF或像文档或其他什么,假设你只是喜欢一个普通的硬盘。
然后就像你的新电脑重建和导出,这不成问题,是啊,是啊,当然是的,但是是的,因为有规范的表示,我不认为数据库制造商同意这样的说法,是啊,是啊,当然啦,好的,我明白了,即使对于文本文件,那不是真的,对呀。所以你知道,就像你知道的,呃,windows使用一种呃,表示的字符,就像你知道的,呃,马车向右返回,而不是像你知道的那样,Mac和UNIX是不同的,所以如果打开相同的文本文件。你基本上看到所有这些就像你知道的,像这样的卡姆东西,呃,到处都是对的,这是因为Windows使用了不同的方式来表示,呃,行返回,谢谢你问我问题,是啊,是啊,当然可以,好的,我想我们星期四见。
P7:第七讲 缓冲管理 - main - BV1cL411t7Fz
好的,我想它正在录制。
好的,酷。好的,太好了。那我们开始吧。大家好,欢迎来到星期四的 186 课程。今天,我想总结一下我们上次星期二关于 B+ 树的讨论,然后我们将进入下一个主题。
这就是缓冲管理的内容。我想做一些复习。这是我们在星期二展示的第一张幻灯片,讨论的是计算与索引相关的不同操作的开销。在这一页中,我们只展示了堆文件和排序文件的情况,正如我们上周讨论的那样。我想让你们记住的一点是,对于某个操作,…
我们关注的是平均情况的开销,因为最坏情况总是一样的,无论是对于文件还是排序文件,当然也包括索引。所以那样比较无聊。另外需要记住的是,读取和写入磁盘都会产生 IO 开销。
然后,像你们知道的,br 和 D 基本上是我们之前定义的三个常数。在这门课的索引部分,我们将假设我们有一个聚集索引。所以所有内容都是排序的。我们假设一个二分之三的因子。然后,fan out 这个变量,我在这里定义为 F,基本上是树内的内部分支因子。
这也是我们在星期二已经讨论过的内容。除非我们讨论的是插入和更新操作,否则我们假设这是一个静态索引。在我们尝试读取它时,没有其他人会修改索引。
我们已经实际讨论了前三种类型的操作,在星期四的课上。所以我不会重复前两种类型。我可以,但现在让我们再回顾一下这个范围查询问题,尝试计算与这个操作相关的开销。
正如你们回忆的那样,我们使用索引进行范围查询的方法如下。第一步,我们使用索引来确定我们需要读取的对应堆文件页面作为起始页。记住,在这个例子中,我们试图找到所有 3 到 7 之间的记录。
这些操作可能会跨越多个堆文件,或者堆文件页面。因此,我们首先会使用索引来确定需要读取的第一个页面。然后我们会扫描索引中的叶子页面,找出其他我们希望读取的堆文件页面。所以记住,在索引中,我们仅仅存储的是指针。
我们实际上并没有存储实际的记录,因此我们首先需要扫描索引中的叶子页,以确定哪些堆文件页存储了记录,在这种情况下是三到七之间的记录。然后实际上从堆文件中读取相应的页面。因此,在这种情况下,你可以看到它出现在屏幕底部。
那么与这三步操作相关的I/O成本是什么呢?第一步基本上就是我们之前看到的内容,对吧?因此,这个锁号对应的是我们需要通过索引访问的次数,以便到达第一页的叶子页。
所以再次注意到这里有一个加一的操作,因为在这节课中,我们将树的高度定义为除去叶子层级之外的所有内容。因此我们实际上需要加回这个加一的操作,以考虑到我们正在获取第一页的事实。
第一个叶子索引页对应的是键值为三的记录。第三步,我们上次讨论过的,就是页数的数量,这里的页数对应的是存储键值在三到七之间的记录的页数。
在这种情况下,它是三。然后我在这里展示的三分之二数值就是为了考虑到每个堆文件页假定只有三分之二满的情况。因此,我们需要实际获取更多的堆文件页,以便存储与之前相同数量的记录。
所以这就是为什么我们在这里有这个三分之二的原因。最后,关于扫描索引中叶子页的IO数量,我将其近似为与步骤三相同。这个近似值肯定是一个过度的估算,因为索引叶子页显然可以存储更多的指针。
信息量显然比堆文件存储的要多,因为堆文件实际存储的是记录。但在这节课中,我们将做这个近似处理,使得数学看起来更简单。所以这项操作的总成本就是将这三步加起来。因此你会注意到,在这种情况下,我们实际上需要减去一。为什么要减去一呢?因为我们重复计算了从索引读取的第一个叶子页。
对的,这里加一是因为我们需要考虑读取索引中叶子层级的实际第一页。但是对于步骤二,我们实际上也已经计算了这个数字。因此,为了避免重复计算,我们需要减去一,以确保账目正确。所以这就是为什么我们最终会看到屏幕右侧这个数字的原因。
所以这里是总成本,实际上我给你展示的总成本是我从前一页计算出来的这个数字。到目前为止,关于范围查询有任何问题吗?很酷。那么现在我们来谈谈插入操作。
那么我们应该如何进行插入操作呢?假设我要将记录4.5插入到数据库中。请注意,我们也需要经历三步。第一步,我们需要通过索引来确定实际修改哪个页面以插入这条记录。第二步,我们读取相应的键文件页面,并进行实际修改。
在这种情况下,我们需要修改这里用蓝色标记的页面。最后,我们需要修改索引的叶子页面和我们在底部标出的蓝色页面。所以,我们需要更新索引中的叶子页面,因为我们已经插入了一条新记录,并且我们必须插入一个新的指针。然后,我们显然也需要更新键文件,因为我们已经向数据库中插入了新内容。
对的。那么这三步的成本是什么呢?第一步和之前一样。我们需要基本上上下索引,所以这将是索引的高度加一,因为我们需要读取索引的叶子页面。
然后第二步的成本其实就是1,因为我们已经知道需要读取哪部分键文件,而且在这种情况下我们只插入一条记录,因此只需要抓取一个页面。
最后,第三步,我们实际上需要执行两个I/O操作,因为我们需要将修改后的键文件页面和索引修改后的叶子页面都写回。这样理解吗?抱歉,你能重复一下为什么要进行两次操作吗?第二次操作是因为我们需要将修改后的键文件页面(在蓝色部分看到的)和索引中的叶子页面都写回,因为我们已经向数据库中插入了新记录。
键文件。因此,索引叶子页面需要存储该记录实际所在的位置。请记住,在这种情况下,我们假设不对索引进行拆分,不会增加树的层级,也不会增加高度,因此目前这部分都可以忽略。所以我们纯粹是将另一个指针添加到索引底部。好的。那么总成本就是你在屏幕上看到的这个数字。
将三步的总和加起来。就这样。所以这是与插入相关的成本,然后我不会展示细节,插入操作基本上是相同的处理方式,我们只需要进入第三级,然后修改我们需要修改的页面。
然后,正确地返回相应的页面。因此,上一节课结束时,有一个非常好的问题被提出,问到,嗯,我们真的需要那么精细,真的需要经过这些实际的细节并做所有这些不同的计算吗?
对的,为了弄清楚实际的成本是什么。答案,嗯,不完全正确。所以我们其实可以使用你们可能已经学过的大O符号来做这个。这样,如果我们用大O来抽象这些,你会看到这里有一个非常清晰的图景。对吧?举个例子,如果我们和相等查找进行比较。
你可以清楚地看到,使用索引实际上比使用某种文件要好得多。对吧?这里的区别基本上是这个对数的基数。如果你还记得上节课的内容,这个数字F通常会是一个非常大的数字。所以通常情况下,对于每个叶节点内部的节点,我们可以指向许多不同的叶节点。
因此,这个F会比类似2之类的数值大得多。所以,如果我们使用索引,与使用某种文件相比,相等查找的成本会低得多。当然,如果与使用堆文件比较,结果也是一样的。在某些情况下,堆文件实际上会占优,比如在插入的情况下,堆文件是常数成本,因为我们只需要把新记录附加到堆文件的末尾,仅此而已。
相比之下,索引聚集的情况,我们需要更新索引,首先要在索引中进行搜索。然后我们还需要支付在堆文件中某个地方进行附加写操作的成本。这样一来,成本会更高。所以这就是为什么在这种情况下,大家都会说,没有一种方法适用于所有情况。对吧?这取决于使用场景以及模式。
实际上我们讨论常数因子的原因是有原因的。就像我之前向你们展示那种复杂的算术图景。原因是,因为如果你还记得磁盘讲座的内容,顺序IO的成本实际上和随机IO的成本是完全不同的,至少对于磁性旋转磁盘来说是这样的,如果你还记得的话。
假设比例是1比100,对吧?所以我们做一次随机IO,我们可以做100次顺序IO,假设是按页面读取的。我显然是在胡编乱造,但为了讨论的方便,假设一下。那么这意味着什么呢?在B+树中,我们需要遍历树中的不同层级。每一个内部节点的页面读取,基本上都会是一次随机IO。对吧?
肯定不是顺序的,因为我们可能会跳到树的另一端。所以每一个IO都会是随机的。因此,如果我们找到这个论点,那么与其做一个单独的随机IO,我们本可以做更多的顺序IO。事实上是1比100。所以这意味着什么呢?这意味着我们在从B树读取页时最好非常挑剔。
因为否则我们可能就能做得更好。如果我们只做堆文件。事实上,在这种情况下,给定1比100的比例,这基本上意味着我们最好是访问非常少的页,进行随机IO。换句话说,你知道这个索引对吧?希望它没有很多页,我们需要遍历才能到底到底叶子页。
否则就不成立了,对吧?为什么我们要使用索引?我意思是,直接使用键文件然后扫描所有内容不就行了。通常有两种方式来确保这种情况。首先是尽可能使用聚集索引。基本上这意味着我们只需要在初始成本上做一次遍历,查明哪个是要读取的叶子页。
然后,一旦我们到达堆文件中的第一页,我们就可以根据需要获取这一页,如果我们在做等值查询,或者我们可以根据范围查询的情况进行顺序读取。所以这是一个可能性。另一个可能性,你可以称之为一种变通方法,就是基本上不使用旋转文件。你知道,使用闪存驱动器,因为闪存驱动器有一个特点,就是顺序读取和随机读取的成本是一样的。
所以在这种情况下,我们就可以摆脱困境了。所以这不是你能在这里玩的小把戏,只是我想提到的两点中的一部分,目的是给大家提供一个大致的理解。罗曼,你有问题吗?是的,你可能提到过,但我不知怎么漏掉了,什么是全表扫描?B+树本质上是一个表的数据结构吗?
树是索引的数据结构。所以对于全表扫描,实际上我们不需要使用树,因为我们只是扫描所有的堆文件。所以即使我们有索引,它也不会被使用。因此,唯一会产生差异的情况是如果我们正在执行前一张幻灯片上显示的这些操作之一。
所以假设我正在进行插入操作。那么在这种情况下,需要同时更新索引和堆文件。所以这时会产生额外的成本。然后如果我想进行等值查询,类似地,决定是否使用堆文件或排序文件是基于用户的,还是取决于数据。
这是个很好的问题。在大多数数据库实现中,这个选择实际上通常是交给用户的。所以作为用户,我们实际上可以定义或者声明我们想用哪种类型的数据结构来存储堆文件。
堆文件,比如用来存储实际数据的部分。以及你用来存储索引的数据结构。所以我们可以声明聚集索引,也可以声明非聚集索引,这由我们作为用户来定义。然后我们就能真正回答这个问题,因为这基本上是把问题抛给用户,让用户自己解决。
所以实际上通常发生的情况是,数据库管理员需要提前了解工作负载的特点。我们是在讨论插入密集型的工作负载,还是查询密集型的工作负载?如果是这样,你就可以根据这一点选择合适的数据结构。
所以我们会随着时间调整它们。也许最初我们为某个特定属性声明了一个聚集索引,但事实证明人们并没有对这个特定属性进行太多查询,因此我们可能会选择另一个属性来代替作为聚集索引。
这也可能发生。是的,你有问题吗?请再说一遍“selective”是什么意思?是的,这里“selective”只是意味着你不要读取太多的页面。这不是一个技术术语,基本上就是在说,不要做太多随机的I/O操作,只是为了论证这个比例,如你在这个幻灯片上看到的。好的,谢谢。那么,如果SSD在随机读取时几乎有相同的成本,这是否意味着SSD使用的是源文件或热文件?
是不是这样,或者是其他结构在使用呢?这意味着在这种情况下,使用什么数据结构并不重要,所有这些我们正在使用的数据结构实际上都是软件实现的。
所以这不像是硬件方面的事情,我们做的这些基本上都是软件实现的。好的,明白了。是的。但当然,如你所知,快速驱动器更贵,所以你实际上是在为正确的做法支付额外的费用,所以需要注意这一点。
所以总的来说,在这部分课程中,在过去的两节课里,我们讨论了不同类型的树形结构,我们也讨论了数据存储,TIA基本上提到的就是这些不同类型的索引以及实际数据是如何存储的。
然后刚才我们也讨论了,比如B+树和与之相关的成本模型。所以显然,正如你可能知道的那样,实际上有很多不同类型的索引结构,而不仅仅是B+树。
所以,如果你感兴趣的话,我鼓励你进一步探索。实际上,很多东西是特定于领域的,也取决于你有什么样的预期使用场景。所以,如果你准备好了,或者有问题的话,随时可以提问,如果没有问题的话,我就会切换到下一个讲座的幻灯片。
这期间还有其他问题吗?
好的,那么如果没有问题的话,我再试着分享一次。
很酷。那么,对于下一个讲座,我们将要讨论的下一个主题是缓冲区管理。这里是你们之前看到的关于数据库内部的系列讲座的大图。我们已经讨论过 SQL 客户端了,你们从第一个项目开始就已经接触过它了。你们也已经玩过 SQL 类似的工具,或者说你们知道一个 SQL 客户端是什么样子的。
我们已经讨论过磁盘,谈到了一些相关的技术,还讨论了这些是如何组织的。我们讨论了索引,讨论了如何以不同的方式组织文件,也讨论了不同的索引结构。
那么现在让我们来谈谈缓冲区管理。我假设你们中的大部分人可能不太明白“缓冲区管理”是什么意思。其实它只不过是另一个抽象层次。对吧,正如常说的那样,计算机科学中的任何问题都可以通过增加一层抽象来解决。那么如果我们已经有了磁盘的抽象,为什么不也为缓冲区,或者换句话说,为内存做一个抽象呢?
类比是这样的。对于磁盘空间管理,我们有磁盘,我们有磁盘空间管理,它基本上抽象化了磁盘。现在我们在讨论如何抽象化主内存。对,在这种情况下,主内存就像 RAM,通常情况下是这样的。好吧,这就是我们所说的缓冲区管理。
它是如何工作的?它是如何组织的?正如你们记得的,磁盘空间管理器基本上有所有这些不同的页面,在它的实现中。接下来的几次讲座中,我们基本上是在讨论如何将页面加载到内存中。
所以我们谈到的磁盘抽象基本上就是这种结构,你知道,页面结构作为最底层的部分。因此,对于主内存,我们也要做同样的事情,但我们叫它别的名字,这样就不会让我们在理解页面的概念时感到困惑。所以我打算使用“框架”这个术语,它也是你的教科书和下文中缓冲区管理器所使用的抽象,来表示主内存。
就像我们有数据页面一样,我们也可以将数据从磁盘加载到主内存中的这些框架中。例如,在这种情况下,我已经将三个不同的页面从磁盘加载到了缓冲区管理器中的三个不同框架中。所以,和磁盘空间管理器一样,缓冲区管理器也试图创造一种错觉,让我们像操作内存中的框架一样操作,而不是需要处理多个层级的RAM,或者是分布式的,或者是你知道的本地RAM等等。
所有这些都被抽象化为框架和一些内容。好的。在API方面,其实与磁盘空间管理器提供的非常相似。所以我们称磁盘空间管理器提供的API是读取和写入页面。类似地,缓冲区管理器也会提供这个API,用来做一些你知道的事情。
快速读取一页,或者写入一页,或者写入一份,嗯,或者写入一个框架,例如。通常操作是这样进行的。所以数据库实现中的更高层级会调用缓冲区管理器的API,基本上是说:“我想从磁盘读取第一个页面。”于是缓冲区管理器会检查页面编号一是否已经存在于他的某个框架中。如果存在那是这样,但在这种情况下并不存在。
它将去与磁盘空间管理器进行交互,请求它获取页面编号一,并将其放入缓冲区管理器的一个框架中,然后返回给更高层级。一样的事情,你知道的,如果有另一个请求,比如请求读取页面编号二,我们就照做,去磁盘读取,因为它还不在内存中。
将其加载到其中一个框架中,然后像之前一样返回,依此类推。你们可能会想到,你们的缓冲区管理有什么问题呢?在这个框架下,涉及到两个问题,首先,如何处理30个页面。其次。
当我们运行完所有的帧时会发生什么呢?正如Alex所说的。那么,我说的脏页面是什么意思呢?我们将对数据库进行修改,比如更新记录、插入或删除内容。因此,所有我们加载到主内存中的页面都会被标记为脏,意思是它们已经被修改。所以我们需要能够处理这些页面,将它们写回磁盘,以便保持持久性。
那么,缓冲区管理器如何知道哪些页面实际上已经修改了呢?一个解决方案是始终将它拥有的每一个帧都写到磁盘上,但这会造成浪费。所以我们需要做一点点账务管理。我们将在每个已加载到主内存中的页面中加入一个脏位。
然后,我们将标记该脏位为1,如果数据库实际上修改了这个页面。对于脏页面,我们需要将它写回磁盘管理器,这一点我们在上一讲已经讨论过了。
到目前为止,你们应该已经是专家,能够评估执行这些操作时的成本了。那么,在我们进入“如果我们运行完所有帧会发生什么”这个问题之前,可能还会有两个问题需要我们讨论。
第一个问题是,如果对一个已经加载到内存中的页面进行多个并发操作会发生什么?这种情况怎么发生呢?也许你有两个不同的查询同时运行,试图更新不同的记录,甚至是同一条记录,位于那个已加载到某个帧中的页面上。
好的,记得我之前给你们展示的那张图吗?有一个跨越性模块,叫做并发控制机制,我们将在本学期后续讲解,它负责处理这种特定情况。所以,如果你有关于这个的疑问,耐心等待。
那么如果系统在缓冲区管理器能够将数据写回磁盘之前实际上崩溃了,会发生什么呢?这也是不好的,因为我们试图插入一堆记录,你知道,它们被放入了内存并进行调整。但然后,整个系统崩溃了,数据并没有被真正写入磁盘。那么,这就是为什么我们还需要另一个跨越性的组件——恢复管理器。
这两个方面的操作都是跨越性的,因为它们不仅仅涉及缓冲区管理器,还涉及磁盘管理器。如你所见,磁盘管理器试图写入某些数据,而就在它实际写入数据之前,数据还没有写到磁盘的某个盘片上。
整个系统崩溃了,对吧?那现在怎么办?同样的问题。然后如果像你知道的那样,多个页面,多个API从磁盘管理器发出调用,试图将同一页面写回磁盘,会发生什么?
所以我们需要有一个并发控制机制。但正如我所说,稍后学期的后半部分我们会详细讨论这些内容,所以现在不用担心,如果你的问题是关于这个的。对,是的。那么我的问题是,操作系统中的缓冲区管理和这个有什么不同?非常好的问题,稍等一下,我会尽力回答,如果没有的话,就像你知道的,可能又是一个种族问题。
好的,谢谢,没有其他问题。那么,让我们来看看缓冲区管理器的实现到底是怎样的,内部到底发生了什么。如我所说,我们需要跟踪一堆页面。所以它通常的工作方式是,缓冲区管理器调用操作系统,尝试分配大量的内存。这也回答了兴趣的问题,知道吗,和操作系统有什么不同。
它非常相似,除了就像在这个管理器的例子中,如果你记得我前两周的讲座,讲的是我们如何绕过操作系统来进行内存分配,比如说分配一个巨大的文件,然后声称我们自己在管理它。这是同样的道理。
我们请求操作系统基本上给我们需要的所有内存,然后我们只告诉操作系统请离开。如果你上过162课或者任何一个162的同学在这里,你们可能会说你知道的,你们可能会说操作系统其实也能管理这些,为什么我们要在数据库管理器内部做这个?
数据库系统对工作负载有更精确的信息,这在某些情况下比操作系统更好,知道吗,比如我们什么时候想把特定页面写回磁盘。因此,对于我们所知的大多数数据库系统实现,我们基本上只是试图通过请求操作系统分配一大块内存,像在这个管理器的例子中一样,来接管操作系统。
所以这不是唯一的设计,你完全可以选择不依赖操作系统,如果那是你想要的。但我在这里声称,数据库系统通常对如何使用这些页面的工作流程有更多的应用知识或了解,因此它可能比操作系统更好地利用这些资源。
这是缓冲区管理器的第一部分。我们需要跟踪所有这些不同的帧,并且需要做一点账务管理。正如我所说,我们需要以某种方式存储这个固定计数位,基本上表示每个被载入帧的页面是否已被更新。为了实现这一点,我们会使用一个小的数组或者类似哈希表的结构,这完全是在内存中进行的。
所以这基本上就是为每个帧存储我们实际上获取了哪个页面,如果有的话。然后我们基本上会记录,比如说它是“被固定”的,或者是否为30。这部分的固定计数我们稍后会详细讨论。
好的,接下来是问题。脏位基本上表示我们是否修改了已载入主内存的某个特定页面。一个典型的操作是这样的:我们将所有这些页面从磁盘载入到这些帧中。
其中一些页面会被数据库修改,作为更新、插入或删除的一部分。还有一些可能会被数据库读取,因为它试图响应查询。因此,在所有这些情况下,我们都不希望丢弃那个页面。我们不能将页面写回或驱逐它,或者清空那个帧,因为其他查询仍在操作它,无论是写操作、读操作还是其他操作。所以,这就是固定计数派上用场的地方。
所以,固定计数基本上会告诉我们,某个查询实际上在操作这个页面。因此,请不要清空它,也不要驱逐它。请注意,这与脏位是不同的。
因为查询可能只是读取页面,而实际上并没有修改它。即使在这种情况下,我们也不希望该页面被驱逐。因此,绝对可以确定,如果有人因为写操作而进行修改,我们肯定不希望通过像清空页面或将其写回等操作来移除该页面,因为操作可能尚未完成。
对于读取操作也是一样的。所以我们使用这个固定计数来跟踪多少个查询实际上在操作一个单一页面。仅仅使用一个二进制的零或一是不够的。你们觉得这是为什么呢?为什么我们需要记录它作为一个数字,而不仅仅是一个二进制位来表示“是”或“否”?有谁知道吗?我们需要确保在驱逐页面之前,所有在页面上的用户都已经完成了操作。
完全正确,假设我们有多个查询试图读取某个特定页面,比如第六页。那么即使其中一个查询已经完成,我们仍然需要确保在驱逐第六页之前,其他查询也已经完成。
所以,这就是为什么仅仅使用一个数字作为二进制变量并不足够。做得好。那么接下来会发生什么呢?如果请求一个页面,正如我所说,如果我们发现一个页面实际上不在缓冲池中,那么我们首先要做的就是弄清楚是否有一个页面可以替换,可能是一个没有被固定的页面。
然后,如果页框实际上是脏的,那么我们就把它写回磁盘,然后标记该位置为可用。接着,我们从磁盘管理器那里读取数据,对吧,然后就这样,之后我们将它返回,经过固定操作,因为我们现在知道有人正在操作我们刚刚带入内存的那一页。
现在,如果我们能以某种方式预测哪些页面将会被使用,那将会是非常好的。同样,对于磁盘管理器来说也是如此,如果我们能预取它们,那就太棒了。因为正如你记得的那样,所有与顺序读取相关的操作都很有利,因此我们可以进行顺序扫描,这对磁盘来说是非常好的。对于缓冲管理来说也非常有利,因为我们知道需要多少个插槽才能分配,因为我们…
因为如果这样做的话,我们可以预测将有多少个页面会被操作。所以,当然,百万美元的问题是,如果缓冲池实际上已经满了会怎样?除此之外,我们还需要驱逐一些已经存在的页面。为了有系统地处理这个问题,我们决定到底驱逐哪个页面是基于一个叫做页面替换策略的东西。
有许多不同的页面替换策略,为了本课程的目的,我们只描述了两种当前实际实现中使用的最简单策略。第一个叫做最近最少使用(least recently used),或者简称 LRU。
这里有一个叫做时钟策略(clock policy)的近似方案,实际上它是你的一种修改。然后还有一个叫做 MOU 的东西,我们将在本课程中讨论。现在,所有这些实际上都会对 IO 的数量产生巨大影响,正如你可以想象的那样,因为根据我们将驱逐哪一页,决定了我们能够带入内存的页数。而我们将页带入内存的方式,取决于我们是否正在进行顺序读取或随机读取,正如我们已经看到的那样,这对整体成本有很大的影响。
那么 MOU 策略是什么呢?其实很简单。我们基本上是尝试驱逐最近最少使用的页面,正如名字所示。所以我们基本上首先要做的是,不会驱逐任何被固定的页面,因为我们知道这些页面不能被替换。然后我们会以某种方式追踪每个页框自上次固定以来已经过去的时间。
然后,我们就选出最后使用时间最久的帧,也就是那个最久未使用的帧,接着驱逐它。例如,在这个例子中,我添加了一个“最后使用”列,这也是缓冲池管理器所保留的元数据。
然后,你知道在这种情况下,我们应该驱逐哪一个?我们只需要驱逐那个使用最少的条目,就像你看到的这个111,对吧?你应该能回想起来,这其实是在61 C课程中已经讲过的内容。
对,为了提醒自己这个是什么意思,我实际上去了61 C课程的网站检查了一下,确认这部分内容确实已经讲过。只是这次是在不同的上下文中讲的,在这门课上他们是在讨论如何确定驱逐哪些缓存条目。
这里的情况类似,区别在于我们不是驱逐缓存条目,而是从缓冲池管理器中驱逐页面或帧。某些情况下,实际上两者是相似的。而且为了确保我们理解,我想起了某些古老的学期内容,实际上这些内容是在春季学期讲过的,只是为了确保我们都在同一页上。
好的,正如我所说的,在这种情况下,我们将驱逐第一个最少使用的页面,也就是根据最后使用时间来看,帧号四的页面,它基本上是最少使用的。如果我们需要第二个页面,我们将驱逐第二个最少使用的页面,在这个例子中就是那个使用了15次的页面。
显然,这是一种非常简单的策略,你可以想象得到。而且它对时间局部性也很有效,就像我们在61 C中讨论的那样。可是,这种策略实际上是有成本的,因为我们需要不断地找出最少使用的页面。为了做到这一点,我们不仅仅需要保留这个“最后使用”列。
你还可以运行一个查询,换句话说,找出最后使用的页面。解决这个问题有不同的方法。例如,有一些专门的数据结构可以用来找到最小值。例如,优先队列(priority heap)就可以使用,或者我们也可以扫描所有的页面,找出哪个是最少使用的。我意思是,你可以使用你喜欢的任何算法。所以,我们还可以做一些近似处理。
假设我不想根据最后使用的编号来记录每个人,但我仍然希望近似实现最近最少使用(LRU)。我该怎么做呢?我们可以使用一种叫做时钟策略(clock policy)的方法。以下是缓冲池管理器的状态,就像我们之前讨论的那样。
我们使用这个时钟机制的目的是,它基本上是一种近似的方案,用来找出最不常使用的页面。大致来说,它并不总是会返回真正最不常使用的页面,但它可以返回一些实际上并非最常使用的页面。
但在维护这个数据结构的开销方面,它会更简单。所以让我们看看它是如何工作的。正如其名,它叫做时钟,因为我们实际上有一个时钟指针。它基本上是下一个我们将考虑作为驱逐候选的页面。所以。
在每个页面中,我们将添加一个额外的位,叫做参考位。它基本上是我们用来跟踪这个页面是否是最近被使用过的内容。你很快就能明白它是怎么工作的。所以我们不再追踪这个数字了,对吧?我们追踪的是它最后一次被使用的时间。
我只是修改这个,只使用一个位,就像我之前说的那样。然后我们会保持这个额外的变量,叫做时钟指针,正如我之前提到的,来确定我们应该检查的下一个页面是什么。
所以,接下来工作的是这样的。假设现在有某个请求,想要读取页号七,而它当前不在缓冲区。所以我们将查看时钟指针,看看是否应该驱逐当前时钟指针指向的、已经固定的页面。
所以这页并没有被驱逐,因为它已经被固定了。有人正在查看这页,因此我们不能驱逐它,所以跳过这一页。然后我们将时钟指针移动到下一页。所以接下来的页面现在也要进行相同的检查,意思是它是否被固定?不,它没有被固定。
但是这个参考位在这里实际上是一个标志。所以它基本上意味着这可能是最近被使用过的内容。所以我们也将跳过这一页。我们将再次前进时钟指针。然后我们会找到一个页面,在这个例子中是页号四,它没有被固定。
并且也不会根据这个参考位来设置,意味着它是某个被读取或使用过的内容,且距离现在的时间已经有一段较远的距离了。所以在这种情况下,这实际上就是我们将用页号七来替换的页面。所以,现在我们已经将页号七带入缓冲池,我们需要确保在接下来的周期中不会将其驱逐。就像你知道的那样,假设在下一个周期。所以,这正是通过设置这个固定标志,因为有人正在读取它,同时也设置你在这里看到的这个参考位来实现的。
对。在这种情况下,我们设置了第七页的参考位,因为这表示我们最近将这个页面加载到主内存中。然后我们会依赖它,因为我们知道其他地方也会使用它。
就像你知道的,返回之后。对。我们只是推进时钟,因为基本上这意味着下次当我们需要驱逐另一页时,我们不会立即驱逐第七页,而是会查看下一页。在这种情况下是第五页。然后我们会把指针返回给第七页,传递给上层,然后我们就完成了。
大家能理解吗?有没有人对这个策略有问题?我有个问题,关于参考位的,能重复一下这是怎么回事吗?参考位基本上是一个近似值,用来表示某个页面最近是否被使用过。哦,所以这是取代之前的方法,我们不再统计页面被访问的次数,而是只用一个位来表示。
所以如果我们访问一个没有固定但参考位已设置的页面,比如这个例子中的第三页,我们不会驱逐它,因为这表示它最近被使用过。但我们会取消设置这个位,对吧?因为下一次访问时,它就不再是最近使用的页面了。不管“最近使用”是什么意思,反正这就是一种近似值。所以如果你遍历这些页面而不使用它,就会取消设置这个位。
这就是它们被移除的方式。嗯,所以一页需要先取消固定,然后再取消设置,之后才会被视为候选页来进行驱逐。明白了。那么取消固定是在查询完成后进行的,对吧?这点我们理解了。
然后这个取消设置位的操作是由时钟指针完成的。所以每次访问一个取消固定但参考位已设置的页面时,我们会做的事情就是取消设置那个位,然后继续前进。我们不会立即处理那个页面。所以这就是这里的近似方法。Felix。
那么这种方法是像约定俗成只使用一位吗?还是有什么原因不允许我们使用两位或三位,比如说当页面数量很大时?是的,没错。实际上,一个位就足够了,基本上可以通过它来通知时钟指针,表示这个页面最近被使用过。
所以你刚才问的问题是,为什么不使用更多的位来表示呢?当然。实际上,我们可以使用32位。若使用32位,那么我们就回到之前的策略。我们只是精确追踪某个页面最近是否被使用,就回到整数的表示方式。谢谢。是的,明白了,实际上我们就是在用32位、64位或者其他任何单一位来近似表示。
所以,你知道,如果有一种情况,所有页面都被固定了。有没有办法让其他页面进入呢?是的,那么在这种情况下,我们真的很糟糕,对不起。如果所有页面都被固定了,那就意味着某个查询正在触及所有这些页面。所以我们能做的唯一事情就是推迟处理下一个查询。
谢谢。我会再买一些。再见。所以这个机制。你怎么知道下一个应该是哪个呢?是的,所以这有点任意对吧,我只是用这种图示来表示一个旋转的时钟,基本上就是绕一圈,检查哪个页面应该成为下一个受害者。
但你基本上可以使用你自己的机制来搞清楚如何遍历这些页面,对吧。显然,我们不想总是只访问其中一个或两个页面,因为那样基本上意味着只有这两个页面会被淘汰,其他页面则不会,这样就不好了。
所以我们绝对想要遍历每个页面。但至于如何遍历,我是说,这有点任意。哦,然后H,抱歉,我不确定你的名字是什么。是的,挺好奇的,最不常用的数据结构是否曾经与LRU进行过比较,为什么LRU比LFU更受欢迎?
至少是我看到的最不常用的,所以一切都取决于对吧,事实上,这就是我们接下来想要讨论的内容,对吧,什么时候这种策略适用于这两种不同的策略,对吧,你已经可以看到这里的区别了。所以我实际上会认为,对于对流行页面的重复访问,这种策略其实是有效的,因为对于你和时钟策略来说都是如此。
我们不会淘汰那些页面,但我并不认为这是理想的情况,实际上,一切都取决于我们所拥有的访问模式,对吧。但至少在比较LRU和时钟策略时,你已经可以看到,时钟策略更便宜,因为我们不需要找到最近最少访问的确切页面。
最近最少使用。并且它的成本也较低,因为我们用一个单独的位来逼近数字,对吧。就像你知道的那样,这基本上是其中一个思路,然后你基本上可以尝试找到它们实际不同的情况,我实际上鼓励你去做这个,对吧,假设你想要和L进行比较。
F、U 或者其他任何方案。但即使在你的情况下,时钟策略也已经存在问题了。人们会思考这两种策略何时特别差。也许是当你访问像循环这样的东西时,但就像,是的,如果你正在访问某些内容,然后在一段时间后再次访问它,但这是一个重复的过程,你只会继续读这些东西。
没错。那么这是什么样的情况呢?假设我要重复扫描。那么我指的是什么呢?你看,屏幕底部我们有一个磁盘管理器,里面有七个不同的页面。假设我想按从第一页到第七页的顺序反复扫描。也许我们正在反复执行同一个查询,对吧?那么让我们来做个实验。
我们的内存里有六个页面,磁盘上有七个页面。如果我们先读取第一页,那就太好了,我们将它带入缓冲区。第二页也是如此,对吧,你明白了。我们基本上填满了这个缓冲池,里面是前六个页面。但现在有趣的部分来了,问题出在第七个页面上,因为我们需要逐出一个页面。
对了,现在我们每次都会错过对吧,为什么呢?因为我们尝试将第七页带入。然后根据我们的操作,我们将逐出第一页,因为第一页是最近最少使用的。好吧,所以在第七次尝试时,我们将带入第七页,逐出第一页。但是在这种情况下,我们会反复扫描这七个页面,从磁盘按这个顺序不断读取。
所以我们想要带回第一页,但不幸的是,第一页已经不在了,对吧?它不在缓冲池中。所以我们需要逐出一些东西。接下来要逐出什么呢?我们需要逐出第二页。对,我是说你明白我的意思吧?所以在这种情况下,我们基本上不会再得到任何缓存命中。对吧,因为我们总是需要带入一些内容。
就像你知道的,你知道我的意思吧?所以我们基本上只会不断地失去。因为这种情况发生得很频繁,所以有一个术语来描述它,叫做顺序洪泛。我们只是将缓存用各种不同的页面填满,但实际上对我们没有任何帮助。没有命中缓存,我们没有有效利用所有带入的不同页面。所以我们不如每次都直接去磁盘管理器。
那么我们怎么摆脱这种情况呢?在这种情况下缓存命中率是零,对吧?但是就像我说的,这种情况其实很常见,因为可能是同一个查询反复执行。
事实上,你会在处理连接时看到类似的情况。那么我们怎么才能做得更好呢?这就是为什么需要引入另一种方法——最近最常使用(MRU)策略。它的做法恰好与我们想要做的相反。在这种情况下,我们又被困住了,需要将第七个页面带入。
所以对于第七个页面,我们实际上不会淘汰最久未使用的页面,而是淘汰最近最少使用的页面。在这种情况下,它会是页面六。太好了,我们淘汰了页面六,加载了页面七。然后再次循环,我们需要读取页面一,太好了,页面一已经在缓冲池中了。
所以,页面一已经在缓冲池中了,我算作一个缓存命中。对吧。你可以称之为缓冲命中或者框架命中,随你怎么叫。基本上就是在说我们不需要去磁盘管理器加载那个页面。同样,页面二也命中了。
所以你看到这里的计数在增加,第三个页面也很棒,已经加载进来了。第四个页面也是一样,你知道了。所以,唯一需要加载的页面是第六个页面,因为在这种情况下,第六个页面不在缓冲池中。
我们需要淘汰最近最少使用的页面。这个页面就是页面五。所以我们要替换掉页面五。太好了,因为接下来我们要读取的是页面七,而它又在缓冲池中了。
所以这也是会成为一个命中的。你明白这个意思了吧,所以我们就会重复这个操作。然后就像你知道的,我们只会去淘汰最近最少使用的,然后我们仍然会在缓冲池中获得相当多的命中,因为许多页面。
已经驻留在缓冲池内。所以尽管我很喜欢点击我之前制作的整个动画,但现在还有其他关于这个策略的问题吗?实际上,我们可以更泛化一点,或者尝试用更多的数学来处理这个问题。对吧?所以在这种情况下,我们有B个页面在缓冲区中。
在这种情况下,我们有更多的页面,我们正在尝试加载。这对于顺序读取工作负载来说,前面的页面我们加载进缓冲池时会没有命中,因为那些页面不在缓冲池中,我们需要填充缓冲池,所以实际上会没有命中。但接下来几次读取时,除了某些页面之外,我们都会命中。
对。所以你在前一张幻灯片上已经看到过这个对吧,我不打算再回去讲了。你已经看到我们只需要淘汰一个页面,对吧?那太好了。同样地,这个模式会在每次读操作中重复,对于每一批接下来的页面,我们都会做同样的事情。只是被淘汰的“受害者”每次都会是不同的页面,但在上一张幻灯片中的所有情况中,受害者总是单一的页面。
所以平均而言,我们每次尝试都会得到 b-1
除以 n
。对吧?因为我们在不同的尝试中得到了 b-1
次命中,所以平均来说,我们大概会得到这个命中率。与最近最少使用策略相比,我们得到了零,所以其实这个表现相当不错。对吧?因为我们实际上得到了一些命中,这是很棒的。
所以我们实际上可以做得更好。如果我们能提前知道这是一个顺序读取查询,我们就可以提前加载所有相关页面。我们甚至可以让缓存管理器提前加载整段页面,这样我们就不需要检查它们是否已经在缓存池中了,因为我们知道它们一定会在缓存池中。
对。所以这个例子就是,如果我们请求读取第一个页面,我们也可能会加载第2到第5页。正如我所说,这怎么有帮助呢?它有帮助是因为我们知道随机I/O操作是很昂贵的。如果我们能做到顺序读取,那就太好了。另一个方面是,如果我们能在尝试同时回答查询的同时,也进行顺序读取,那就更加棒了。
假设我们有两个不同的线程,例如,这也会非常棒,因为我们可以在后台预取数据,同时用另一个线程处理查询。但正如我所说的,没有一种方法适合所有情况,所以不幸的是,这也不是我们问题的终结。事实上,我认为我们需要一个混合策略,因为有时我们自己的方法实际上会更好。对吧?
假设我们确实想对页面进行随机访问,而不是顺序读取所有页面。我们实际上只想跳来跳去。那么在这种情况下,我们的US策略实际上会更好,你可以在课后再尝试解决这个问题。如果你感兴趣的话。而且,实际上在重复顺序读取的情况下,US策略也更好。我们已经见过这种行为,例如,同一个查询反复扫描所有页面,而我们运行这个查询多次。
不用多说,大家已经提出了许多更复杂的策略,从完全随机、最不常使用到最近未使用,甚至现在流行的使用神经网络来预测,这些都是很有意思的做法。
很热门且非常流行,但使用神经网络预测哪个页面最可能是最少使用的,或者哪个页面应该被驱逐,然后基于这个做出决策。这听起来确实像是285顶级策略。对吧?但需要担心的一点是,你需要追踪这些策略运行的成本。
关于时钟算法,吸引人的地方是,例如你知道的,它们非常容易实现,而且在运行时也不算太昂贵。我们只需要简单地推进时钟,然后检查引用位是否被设置,基于这一点,我们就能弄清楚该淘汰哪个页面。如果你做的是像更新网络这样的复杂操作,那么情况就不一样了。
然后也许推理所需的时间已经足够昂贵,足以让你决定直接从磁盘读取所有内容,因为记住,随机访问会非常耗时。所以在这种情况下,如果你做一些复杂的操作,比如说鸟类算法或一些疯狂的神经网络来做决定,那么这是你需要担心的事情。所以即使是像随机访问这么简单的方式,也可能奏效,因为它就像是你说“算了,我不想承担计算哪些页面需要被替换的所有成本,干脆随机选一个页面”,然后就这样做,结束了。
哦,所以这就是我想说的全部内容,总结一下,我告诉你们的关于这个的内容就是各种不同的缓冲区管理策略,比如你知道的,保持试探页面计数、保持试探30位这些方面。我认为这两点我们是需要的,无论我们想要什么策略,因为我们需要弄清楚当前到底是哪些页面在被使用,以及哪些页面实际上是脏的,因为我们需要把它们写回磁盘。
所以我认为这两点是我们无法避免的,但就像其他任何事情一样,我们可以使用很多不同的策略来实现实际的页面替换算法。我鼓励你们去查看一下维基百科页面,或者甚至回去看看61C的幻灯片,了解其他可用的策略。需要注意的是,数据库的情况可能会特别复杂,因为查询往往会读取所有页面,特别是像select *
这样的查询,顺序泛滥确实可能成为一个问题。
所以在这种情况下,我们需要想出一些方法,至少能够应对像顺序泛滥这样的情形。我对于缓冲区管理有一些看法。有人有问题吗?是的。那么我们知道数据库的一般访问模式是什么样的吗?数据库的一般访问模式是什么样的呢?
我们知道数据库的访问模式是什么吗?是的。其实这完全取决于使用数据库的应用程序是什么,或者是用户是谁。如果是单一用户,并且你提前知道这个人会发出什么样的查询,那对你来说就太好了。但通常情况下,情况并非如此。所以,很难说。
但确实有很多研究尝试预测和研究查询中的模式识别,这样他们可以预测哪些页面会被使用。好,感谢。是的。
因为我们需要更多不同的实现方式。对吧。比如说,我们需要每页的固定计数,也需要每页的30位。你可以将其存储为与帧相关的内容,或者将其存储为单独的表格,甚至存储在一个数组中,存储在哈希表里,随你喜欢。
但它需要被保存在缓冲区管理器内部的某个地方。谢谢。谢谢。谢谢。很好的问题,所以在本课程中,我们就假设这一切都是像软件一样的软件实现。但实际上确实有一些工作尝试将所有这些东西都实现到混合硬件中。所以你还记得在61c课程中,我们讨论过类似缓存管理的内容,它通常是在硬件内部处理的。
如何划定界限是完全随意的。对吧。就像你知道的那样,有人做出决定,缓存是所有应用程序,包括操作系统和数据库,都需要使用的。因此,决定将其实现到硬件中,因为它已经变得足够普遍了。对吧。
但也许并没有那么多数据库服务器,或者需求并不够高,导致人们不愿意将其嵌入硬件。因此,过去确实有一些工作,尝试构建他们所称之为“数据库机器”的硬件。
所以所有这些机器支持基本上都与关系操作有关。对吧。我们在186中讨论的所有内容。所以在这种情况下,尝试将一些操作嵌入硬件中是有意义的。但当然你将会看到权衡,就像在61c中一样,我们将某些东西嵌入硬件之后,就无法再进行修改,除非你愿意付出更多的代价。所以,实际上我们可以选择保持灵活性,进行更改。因此,选择你的毒药。
我猜。酷。谢谢。是的,没什么是免费的,不幸的是。那时我说的是另一个问题吗?酷。好的。如果没有其他问题,我将切换到另一个话题。
各位能听到我吗?是的。
好的。好的。那么现在我们将讨论关系代数。
我知道关系代数的幻灯片还没有放到网站上,我原本没打算覆盖关系代数的内容,但我们成功地快速讲完了缓冲区管理,因此今天我们有机会向你们介绍关系代数,这真是太好了。
所以,幻灯片会在讲座后不久发布。好的。那么,迄今为止我们学到了什么关于数据库系统的知识呢?我们讨论了磁盘空间管理,讨论了今天所看到的缓冲区管理。我们讨论了文件和索引是如何管理的,对吧?
我们还从一开始就讨论了SQL客户端,对吧?以及如何向数据库发出查询,针对我们的DML和DDL查询,它们允许你定义架构并修改数据。好了,现在我们要讨论的是中间的部分。对吧?
所以查询解析和优化以及关系操作符。今天我们特别要讨论的是关系操作符。我们将停留在逻辑层面。我们将讨论关系操作的定义,并随后讨论它们的实现。这个过程会很有趣,因为它将帮助我们理解数据库系统是如何在内部推理一个SQL查询的。
对。SQL查询只是一个用户的表示方式。它是用户与系统之间的通信方式。数据库系统在不同的层级上运行,而那正是我们今天要讨论的层级。好的,那么在我们继续之前,让我们先讨论一下,当你有一个SQL查询时,它是如何被转换成我们所说的这个内部表示的。它会经过几个阶段。第一个阶段是,假设用户输入一个SQL查询。
我们有一个查询解析器和优化器,它将查询转换为类似于这样的表示形式。所以这被称为关系代数表达式。这个关系代数表达式会被转换为一个逻辑查询计划,它基本上描述了这些操作执行的顺序。对吧?在这个特定的情况下,我们将特别讨论这些操作符。
在这个特定的案例中,你正在进行一个关于预留表(reserves)和水手表(sailors)的连接操作,然后你会在最大条件下选择一些元组,然后你决定展示给用户的一组属性。再次强调,不必担心如果你不了解这些希腊符号的含义,这些符号会是我们今天关注的重点。
好的,我们从关系代数到达了这个逻辑查询计划。最后一步是物理查询计划,这就是实际执行的部分。哪些物理算法会在数据上执行。对吧?再一次,数据将基本来自通过缓冲区管理器加载到内存中的页面,然后你将在这些页面上再次进行操作。
这一页涵盖了很多内容。如果你不理解也完全没问题。我只是想给你提供一个鸟瞰视角,了解从SQL查询到数据库实际执行的过程。好的。所以总体来说,数据库系统最终会创建一个所谓的物理查询计划,这涉及到操作关系实例的物理操作符。
好的,先来说一下,这只是一个非常高层次的概述。从大约10,000英尺的视角来看。你从SQL查询开始,基本上是用户说他们想要这个结果,在这个特定的例子中,他们想在某些条件下对reserves
和sailors
进行连接。
用户只需要声明他们想看到什么,他们不需要指定如何完成。这是数据库系统的工作,数据库系统内部会生成一系列操作来表示SQL查询的结果。
然后,生成一系列操作,基本上这系列操作叫做查询计划。好的,系统会执行这个查询计划并为用户生成结果。好的。那么SQL和关系代数之间有什么区别呢?好的,实际上我们有两种不同的表示方式可以思考。第一种是SQL。
这就是我们之前讲过的内容,而这就是人类所表达的,正确吗?人类喜欢SQL,因为它是声明式的,你只需说你想要什么,而不必说明如何获得它。系统的任务是找出如何执行它。所以当我们谈到SQL时,我们并不担心它是否会成为一个高效的查询,或者数据库系统执行查询的速度有多快。
甚至连它是如何执行的,这对我们来说并不是一个关注点。系统内部会使用关系代数来表示SQL查询,然后组合出SQL查询的结果,而关系代数表达式看起来大致是这样的,带有这些希腊符号。
关系代数对于关系型数据库系统来说更容易操作,因为它描述了如何操作数据以计算查询结果。所以总体来说,数据库系统内部将SQL转换为关系代数表达式,然后进行操作和简化。
然后找出最优的操作机制来计算SQL查询结果。所以接下来的几节课我们会专注于这个内容。现在让我们特别讨论关系代数,因为它将为我们后续关注的查询处理和优化的内部机制打下基础。关系代数基本上就是一种代数。它有着非常严谨和正式的基础,它是操作关系实例的代数,这些就是关系的实例。
就像任何其他代数一样,无论是高中代数还是线性代数,在初等代数中,你都有变量进行操作,结合算术运算。在线性代数中,你操作的是矩阵,对吧?你可以进行矩阵乘法等操作。
就像关系代数一次操作一个关系一样,它组合表达式,然后再将这些表达式组合成更复杂的表达式,这是一个组合性的框架,允许你从更简单的表达式组合出更复杂的表达式。
在这个具体的例子中,我稍后会讲解这些符号,但目前有一些操作发生在RNS上。这是两个关系,一些表达式正在对RNS进行计算。然后,另一个表达式正在对该结果进行计算,最后,最后一个操作应用于这个结果。所以这就是一个代数表达式,在这种情况下是一个关系代数表达式。
那么这个代数有什么优良的特性呢?第一个特性是结果也是一个关系实例,也就是说它也是一个关系。你的输入是一个关系,输出也是一个关系。这使得我们可以组合这些关系代数表达式。例如,你可以将两个表达式联合起来,或将它们连接在一起,或者对某个表达式进行选择操作,稍后我们会讨论这些操作。
这就像你使用关系代数一样,抱歉,是线性代数的操作。线性代数中的矩阵运算会返回一个矩阵。对吧?并且它是组合性的,你可以取一个表达式然后进行乘法运算,得到的表达式可以与另一个表达式组合,甚至组合成更大的表达式,结果是封闭的,也就是说,得到的结果仍然是一个矩阵。因此,这是代数在这种特定情况下所提供的一个重要特性,即你操作的关系的输入模式(schema)决定了输出模式(schema)。
这非常重要,因为你可以静态检查查询是否合法,因此你实际上不需要查看数据就能测试查询是否合法,你只需要查看模式来判断。
我是说,线性代数的学习也有类似的情况,对吧?所以输入的大小(行和列)决定了输出的大小,因此对于关系代数和线性代数来说其实没有太大区别。纯关系代数实际上具有集合语义。记住我们之前提到的集合语义与袋语义的区别,当我们讨论SQL时,纯关系代数实际上具有集合语义,在关系中没有重复的元组。
这将占据我们大部分的讨论,我们将重点讨论集合语义,SQL当然如我们所见有袋语义或多重集语义。我们将讨论多重集语义,尤其是在系统讨论中,因此当我们谈论这些操作符时,也会讨论它们与多重集的关联。
好的,关系代数操作有不同的形式,有一元操作作用于单一关系,也有二元操作作用于多个关系。我们先讨论一元操作,基本有三种基本的一元操作。好的。
第一个操作是投影。它只保留输入关系中的所需列,基本上是进行垂直选择。下一个操作是选择操作,它选择一个行的子集,所以这是一个水平操作,基本上是保留某个行的子集。然后是重命名操作,它允许你重命名关系的属性以及关系本身的名称。
好的,接下来,哎呀。
所以这些操作也有符号,譬如投影操作用π表示,选择操作用Sigma表示,重命名操作用row表示。好的,所有这些都是一元操作。我们还有二元操作,二元操作作用于一对关系。
所以,第一个版本,或者第一个操作是并集。简单来说,就是在一个关系中的元组或者在另一个关系中的元组。集合差是指那些在一个关系中但不在另一个关系中的元组。笛卡尔积或交叉积则允许我们通过获取两个关系中元组的所有组合来合并两个关系。再次强调,当你在学习时,你可能会立即看到SQL中的相似之处,我会尽量将两者联系起来。
在我描述这些操作时,有什么问题吗?这些符号很直接,你有一个合并操作符“∪”表示并集,一个“-”表示集合差,和一个“×”表示笛卡尔积。好的,那么当我们说关系代数处理集合时是什么意思呢?这就是形式化的定义,关系代数通常在集合的上下文中定义,当然在实际操作中。
由于SQL是基于多重集的语义,大家肯定会转向讨论如何在多重集的关系上进行操作,针对多重集的相同操作是什么样的,我们也会讨论这个问题。
还有其他问题吗?好的。你还有一些其他操作,它们不是基本操作的一部分,但它们是派生操作,是我所说的宏,用来表达上述六个操作中的某些常见操作。
对的。所以其中一个例子是交集,当然你可能已经意识到,交集可以通过并集和差集来表达。所以交集有这个操作,它是你的逆操作。然后就是连接操作,我们之前在 SQL 中讲过连接。所以连接通过使用“and”来表达,并且连接有多种类型。
我们稍后会讨论所有这些类型。好的,连接,顾名思义,是将关系结合在一起,同时满足特定的谓词。对吧,我们讲过自然连接,讲过内连接和外连接。这些都是我们会讲的连接类型。我们还会加入 theta 连接和等式连接作为特殊情况。好的。
所以这是一个从全局视角看待这些操作。这些是基本操作,这六个操作。而这些是派生操作,它们是常用表达式的快捷方式,可以通过这些基本操作组合得出。到目前为止有任何问题吗?我不确定这是否完全在我们已经覆盖的范围内,但我们如何定义选择和投影的参数呢?是的,我们现在就会讲到。也就是说,当我更详细地讲解这些操作时,我们会讨论。
所以这不是我第一次提到这些操作。我会讲例子,并讲解这些操作的正式基础。好的,还有其他问题吗?好的,那么我们开始讲解投影或 π 操作。好的,下面是投影的一个例子。假设我有我的关系实例 S2,并且我正在做。
所以 S2 有四个属性:S ID、S name、rating 和 age。我正在进行一个投影,并列出 S name 和 age。好的,这基本上意味着我只希望你给我 S name 和 age 作为输出的一部分。所以我从 S 中选择了 S name 和 age。好的,所以输出将是 S name 和 age。
所以,如果这听起来很熟悉,嗯,是的,它应该很熟悉,因为我们之前在 SQL 中讲过这个。这基本上就是 SQL 中的 select 操作,对吧,select 操作允许你列出你希望在输出中看到的属性。
这基本上就是投影操作所做的事情。所以模式基本上由这个属性列表的模式决定。所以基本上通过列出 S name 然后是 age,它基本上是在说:我希望输出包含这两个属性,按这个顺序排列。
所以名称和类型基本上是从输入中选出的这个列表。名称必须对应输入中的名称,类型也会从输入中继承。所以 S 到这里的输入,你基本上是在做这个操作中的列的垂直选择。好的,所以这里有一个微妙之处,就是当出现重复时会发生什么。
对。所以由于关系代数假设集合语义。如果你对年龄做投影,最终会得到一个作为中间结果的多重集合。对吧。所以你基本上丢弃了这些列,只保留了这一列,就是年龄。现在你有两个副本或三个副本的 35。所以这个投影的最终结果必须将这个多重集合转换回集合。
所以,这是进行关系投影时的正确结果,如果你意味着集合语义的话。集合语义下的投影通常会导致较少的行,特别是当你在投影某些列之后仍然有重复项时。当然,正如我们在 SQL 中所看到的,真实系统并不会去除重复项。对吧。并且它。
不去除重复项反而更便宜,并且这是用户对 SQL 所期望的语义。对吧。你会怎么去除重复项,有什么想法吗?就是这个。对,但是那是查询。其实我更在想算法。你会使用什么算法来去除重复项?有些人提到过使用哈希。嗯,那是其中一种方法。
另一种方法是使用排序。这两种方法都是有效的。对吧。但这两者都很昂贵。对吧。所以你当然可以使用哈希来尝试确定多个元组是否哈希到同一个值。或者你可以对所有这些元组进行排序,输出元组,以确定是否有两个元组。
有一系列相同的元组,在这种情况下,你保留一个副本,丢弃其余的。好的。好的。好的。所以我想我会在这里停一下。剩下的内容我将在下节课讲解。我有任何问题吗?当处理多重集合时,是否完全是另一种关系代数,所有定义都不同?是的,本质上是的,我是说,操作和语义上都有平行的类比。
正如我们将要讨论的那样,对于重写规则,也有适用的类比。对吧。对于集合语义和多重集合语义,可能适用不同的重写规则。所以,区分这两者是非常重要的。重写规则,我指的基本上是将表达式简化,就像你简化代数表达式或矩阵表达式一样。
对不起,代数表达式或矩阵表达式。再说一次,这里我们要简化这些关系代数表达式,并且不同的简化规则适用于集合与袋(bag)之间。哦,所以这其实,实际上,让我问一个小知识问题,为什么你们认为我们用这些希腊字母名称来表示这些操作呢?比如,为什么投影是用π(pi)表示?是π。对,希腊字母。
确切地说,你觉得怎么样?你知道的,这个是什么?它在做什么?实际上这是一个非常好的,实际上是一个非常好的新模型。是的,实际上是的。恶魔的,抱歉,实际上是非常好的,恶魔的。选择正确。是的。是的。我认为我认为更多的是像 SQL 中的 where
子句。所以,不过是的。
我猜这有道理。用正确的,是的,正确的关系代数术语是选择,对吧?是的。所以非常令人困惑,我认为我们应该。鉴于这个问题随时间发展,我们应该停下来说,这个非常令人困惑的地方,我会在下一课中再次提到,就是选择(σ)实际上并不对应于 SQL 中的 select
子句。那只是一个不幸的命名选择,所以 SQL 中的 select
子句实际上对应的是投影。
本质上是对的,但是 where
子句对应的是选择。所以这一切有点令人困惑,这是我们下次讨论的内容。是的,然后是富兰克林的那个,挺好的,他们就这样去做,我是说,抓住你的想法,我们看看接下来会发生什么,实际上是在下一讲中。不过,好吧,总之,这就是我的触发问题的结尾。也许我们应该停止录制。好的。
这个问题是对的。感谢大家。再见。
如果你有问题,请留下来。
P8:第8讲 关系代数 - main - BV1cL411t7Fz
我应该。好,那么这个东西会有预演吗?是的,实际上会有预演。
如果你参加预演,你会获得额外的加分。所以实际上参加预演是很好的。我们会在场,确保你的设置是合理的,并对其做出评论。所以我只是想先简要介绍一下这次考试监考的背景。
这篇帖子很长,对吧?有很多细节,我们真的希望你能花时间仔细阅读所有这些细节,因为我们花了很长时间才得出这个方案。我们过去三周一直在努力处理这个问题,你无法想象Jerry、Alan、Ethan以及其他很多人为了找出最好的做法而交换了多少信息。
从某种意义上说,你们要优化三个维度,对吧?第一个维度是公平。我们想要公平,我们希望那些认真参加考试的人能够得到奖励。我们想尽量减少隐私泄露,因此我们尽量避免侵犯隐私。第三,我们想要尽量减少开销。
所以我们想让它尽可能少给我们带来负担。优化这三个目标真的很痛苦,我们花了很长时间才得出这个方案。希望这是一个能够带来一些不适感,但同时提供隐私和公平性好处的方案。所以希望你们能耐心配合我们,一起试验这个。
我们再次从其他班级收集了反馈,试图找出最好的做法。我们认为你们已经提出了一个不错的方案。虽然不能做到完美,但还是不错的。所以,如果你有任何建议,觉得某些地方不适合你的设置,请告诉我们,我们可以尝试找出例外情况。
告诉我们,我们会尽力满足你的需求。尽管这个公式非常复杂,但是,我们将进行一次预演,希望你能参加,因为你将因此获得积分。我们还会评估你的设置,并说,嗯,随便,然后我们可以从那里开始。好的。
如果你对考试有任何问题,随时可以提问。那么,每个人问:我们需要扫描什么内容来提交吗?还是考试是在线的?考试是在线的,所以你不需要打印任何东西,也不需要扫描任何东西。所以考试将会在线进行。
我们还考虑了防故障的措施,如果你有Wi-Fi等问题,所有这些内容也在说明中。好的,其他公告。和往常一样,查看每周的帖子而不是…好的。我可以拿着笔和纸帮助自己解决问题,但没有平板电脑,对吧。你可以带任何你需要的书写材料,只要它不是…
它不是电子书,也不是教材,对吧。所以我们确实有备忘单,但不允许使用教材。好的,查看每周的帖子了解更多公告。我们正在做的一件事是,尝试确保助教能更稳定地帮助学生,方法是坚持要求学生提供…
办公时间的工单描述符合最低要求,对吧。所以基本上,我们希望你能够明确指出你需要的支持,所提问题的描述,遇到的具体问题或错误,你遇到的困难,以及你已经尝试过的解决方法等。
这是GitHub仓库的链接,对吧。我们会坚持要求你提供更多信息,以便助教在办公时间内有效地帮助你。因为问题是,如果你带着没有完整信息的问题出现,而课程工作人员正在帮你解决,这会花费很长时间来处理这些问题。
他们可能无法在生活中与下一个人建立联系。对。所以这总体上是为了提高系统的效率,并确保每个人在启动问题时都能得到公平的机会。好的,更多细节请参见第19篇帖子。最后,但同样重要的是,我鼓励你们如果可以的话打开视频,当然没有压力,但我们很喜欢看到你们。
我们喜欢看到你们的反应。好的,如果有什么让你困惑的,它会显示在你脸上,我可以尽量尝试纠正。是的,否则我们就看着一堆字母,这样可不有趣。是的,反之,我根本不知道这些内容是否能够传达给你。对吧,好,好的。
很棒。所以我们在讨论关系代数操作,对吧。关系代数是数据库系统中我最喜欢的话题之一。所以每次讲解这个都让我感到非常开心。所以,我们在讨论关系代数操作时,它应该让你联想到算术和线性代数,因为它基本上允许你通过按顺序使用这些操作来组合更大的操作。
对,我们讨论了各种基本操作,至少在高层次上。投影、选择、重命名、并集、差集、笛卡尔积,我将以更详细的方式讲解每一项操作。我们还讨论了有两个复合操作,即交集和连接。好的,回顾投影,我们上次已经讲过这个,但只是快速回顾一下。
所以,投影是用π符号表示的。好的,使用方法是你说π,然后列出你想在输出中获得的属性,再给出输入关系作为操作数。这个操作,π就是作用于这个输入关系实例的。好的。所以,从这个输入关系实例来看。
这个π操作基本上只保留了S姓名和年龄。好的。所以它去掉了评分,去掉了S ID。好的,这就是你在这个例子中最终得到的结果。所以,这个投影操作对应于SQL中的select列表,基本上就是你在select子句中列出的属性列表。
投影操作的输出模式由属性列表的模式决定。基本上你列出了这两个属性,因此那就是你的输出模式。名称和类型再次对应于输入属性。所以,这个S姓名基本上和这个S姓名是一样的,这个年龄和这个年龄是一样的。
因此,衍生自输入属性的名称和类型。投影基本上是选择一部分列,现在是在进行垂直切片。它就是在做垂直切片。好,那么如果有重复项会发生什么呢?这就是事情变得有些棘手的地方。所以,如果你举个例子,你对年龄进行投影。
你去掉了其他列,如ID、姓名和评分。剩下的只有一个属性关系输出,就是年龄。对吧?现在如果你注意到,有一些重复项,对吧?比如35重复了三次。记住,在关系代数中,你遵循集合语义。
所以,你最终要做的是去掉重复项,最终只剩下两个输出工具。好的,所以集合语义意味着,如果在投影过程中有重复项,你最终会得到较少的行。正如我们所见,真实系统并不会自动去除重复项,因此SQL并不强制去除重复项。这是因为在操作中一致的语义。
以及出于性能考虑,去重是非常昂贵的。对吧?所以,一种可能的方法是进行排序,而排序是昂贵的,或者使用哈希等操作,每一个这样的操作都是昂贵的。好的,接下来我要讲的下一个操作是另一个统一操作,就是σ选择。在这种情况下,我应用了σ操作,即选择评分大于8的输入关系S2。
好的,基本上,我强制要求这个特定的属性大于它。这意味着这个拉取和这个拉取会从输出中被消除,你得到的输出只有这两个。所以,选择操作可以看作是投影的转置,对吧?它基本上是在水平切片,选择行的子集。
选择操作对应于SQL中的WHERE子句。所以在这里,你列出一些谓词,这基本上是你可以在选择操作中作为下标进行操作的内容。选择操作的输出模式基本上与输入相同。你并没有丢弃任何属性,基本上继承了输入的属性。选择操作的情况。
如果输入是一个集合,那么在选择操作之后你不需要进行去重,因为输出也将没有重复项。好吧,所以并不一定需要在选择操作中进行去重。与投影操作不同,在投影的情况下,因为你丢弃了一些列,剩下的列结合在一起可能会有重复项。选择操作的情况。
你不需要做这件事,你基本上可以免费得到它。这里有一个例子,展示了如何将选择和投影组合成一个更复杂的表达式。记得我提到过,关系代数允许你将关系实例的基本单元组合在一起,然后用这些操作按顺序组合成更大的表达式。在这个特定的例子中。
我有s2,然后我在其上应用选择操作,再在其上应用投影操作。那么这样做的结果是,我丢弃了评分大于8的水手,或者说,我保留了评分大于8的水手。
然后我丢弃了除了S name之外的所有其他列。所以在这个例子中,我首先会执行选择操作。评分大于8。所以我丢弃了这个元组和那个元组。然后我会丢弃剩下的不需要的属性,包括评分、年龄和边界。那这个其他的表达式呢?好的。
比如说,我首先对S name做投影,接着是评分大于8。这样理解对吗?如果不对,为什么呢?沃伦。在这种情况下不对,因为你选择的结果和你知道的那种很相似,对吧,完全正确。所以在你已经将其他列投影出去后,只剩下了S name。
你不能真正应用这个谓词“评分大于8”。所以这是一个格式不正确或错误的关系代数表达式,不是正确的。好的。所以你不能这样做,这并不适用,因为它包含无效的类型。这个选择操作的输入不包含评分列。所以这不是一个二元操作。
好的,那么并集操作表示为这个符号,你是一个警察。并集操作是什么样子的呢?并集操作基本上是对两个元组集合进行集合并集操作。对吧。所以你有s1作为一个关系实例,s2作为另一个关系实例,而这些。
这些元素,属于这个集合,它们是两个集合。s1并集实际上是所有这两个集合的组合。所以我在这里使用的是集合表示法或文氏图。为了正确地做这件事,两个输入关系需要是兼容的。好的,这是。
它们必须有相同的属性顺序,并且属性必须有相同的类型。否则,合并这两个关系就没有意义,因为最终结果会有一个奇怪的模式。所以如果一个关系有三个属性,另一个关系有四个属性,那么不能合并这两个关系,因为这两个关系的合并将导致一个没有三个属性的关系。
不,或者说对于属性来说。对,差不多就在中间。所以基本上,我们坚持要求两个关系必须是兼容的。它们必须有相同的属性集和类型。所以我们看到的SQL表达式是并集。对。所以在并集下的重复消除问题,在状态语义下是这样的。
我们会进行重复消除,因为可能会有多个相同的元组。所以在这个例子中,我有s one和s two,我正在通过关系代数做s one并集s two。这个表达式有五个输出元组,而两个输入分别是三和四,总共七个输入元组。对,这就是因为这个元组和那个元组被重复了。
对。所以你最终去掉了每个重复元组中的一个副本。我们看到的SQL表达式是并集或并集所有,union
会去除重复项,union all
保留重复项。好吧,到目前为止有什么问题吗?好吧。那么,关于并集为什么会产生更多的规则的问题。嗯,并集实际上并不会。
所以如果你对这个关系和那个关系做并集,理想情况下你应该得到七个元组。但然后你去掉了重复项,所以剩下五个元组。这回答了你的问题吗?成熟了。好吧。那么集合差,另一个二元操作,类似于并集,在于它们都是集合导向的操作,两个输入关系必须兼容。
对。所以它必须有相同的模式。所以基本上,这意味着相同的属性集和相同的类型。那么在我的维恩图中它看起来是什么样子呢?嗯,如果你有s one和s two,s one减去s two就是保留这个弯月形区域作为输出。好了,这就是s one减去s two的计算方式。我们看到的SQL表达式是except
。
那么让我们通过一个例子来进行演示。在这个例子中,我的关系实例是s one和s two,我正在计算s one减去s two。所以我保留所有在s one中但不在s two中的元组。所以遍历这个列表,22显然不在s two中,31在s two中。
所以这不会成为输出的一部分,58也在s two中。所以我要去掉它。所以总体来说,我剩下一个输出元组,就是22,对应于22的那个元组。你可以自己在家试试,s two减去s one有两个输出元组。这就是到目前为止的集合差。我们需要像在关系代数中做并集时那样去除重复吗?
这是给你们的问题。对吧,我在聊天中看到了一些答案,基本上是没有的。对,答案是没有。这是因为我们所做的只是从集合s1中取出一些内容,然后将其删除。对吧?由于s1中不包含重复项,我不需要进一步从结果中删除重复项。对,所以是s1。
我从s1开始,然后删除了一些内容,s1中没有重复项。因此,在删除一些内容之后,我仍然不会有重复项。好的。好的,这不是必需的。我们之前讨论过SQL表达式,除了我们早些时候讨论过的内容,还有“except”和“all”操作,这与SQL中的并集或并集所有操作语义相同,在关系代数中,如果存在重复项,它们将被删除。
当然,它们在输入关系中并不存在。对吧?所以输入关系中没有重复项。这就是为什么我们在计算结果后不需要进行去重操作。好的,现在我们进入一些更有趣的操作。让我们来看看笛卡尔积。好的。
所以,两个关系的笛卡尔积,即r1和s1,意味着r1中的每一行都与s1中的每一行配对。所以如果我有这两个关系,我基本上就是计算这两个关系的二分图,并计算所有配对。对吧?所以我有六个输出结果。这就是笛卡尔积。那么我结果中有多少行?假设r1的大小是这个。
通常这样表示的是这个。我们s1的大小是这个。我的结果中有多少行?是的,s1与s2的笛卡尔积,或者说s1乘以s2,基本上是这两个关系大小的乘积,因为每个关系中的每一行都会与另一个关系中的每一行配对。我们需要担心模式兼容性吗?
这两种关系并不完全正确,它们可以有任意的模式,因此我们不需要担心它们是否以某种方式兼容。我们需要做去重操作吗?嗯,再次强调,这并不是必要的,因为这两个输入没有重复项。因此,您应该能够说服自己,即使在输出中也不会有重复项。
好的,到目前为止有问题吗?好的,好的。好的,那么这是另一个有趣的操作。这是重命名操作,通常表示为这个P形状的希腊符号,也被称为角色。好的,这就是它的发音。好的,很好。那么,重命名如何工作呢?当笛卡尔积发生时,正是我现在要讲解的内容。
Tommy说,我可能漏掉了什么,但为什么我们的关系中没有重复项呢?嗯,输入没有重复项,对吧?在关系代数中,这些关系是集合。因此,我们遵循集合语义。所以输入中没有重复项。在这个特定情况下,在执行笛卡尔积之后,你也不会有重复项。好的。
所以重命名允许你重命名关系及其属性。这对于避免当两个关系在属性集上重叠时的混淆很方便。好的。如果我们不想重命名输出,可以省略输出名称。好的,抱歉。如果我们不想重命名输出。假设R一个看起来是这样的。
所以我有sid,bid和day。这是我的三个属性。我可以使用这种语法将它们重命名为sid,ID和day。你可以看到这是输出关系的名称。这是这三个属性。基本上,我在这里按R一个中列出的顺序列出了它们。好的。
所以sid被重命名为sid,ID被重命名为to,day被重命名为day。好的,所以这里我没有改变day,保持不变。因为day没有被重命名,我可以在这里使用简写。虽然在这个具体实例中并没有真的变得更短,在某些其他情况下它可能会更短,我说,嗯,输入的sid被转换为输出中的sid。
我使用这个箭头符号来表示这一点。好的,这两种表示法基本上是一样的。如果我不想重命名关系,可以省略这个R,这样也可以。好的。那就是如果我不想重命名关系,它将保留相同的名称R一个。还有一种重命名的方法,另一种简写方式。
这是使用列表位置。所以这是在需要的情况下的问题。如果我做了一个叉积,例如,其中一些属性名称在两个输入关系中是相同的。这样我就能用简写来引用这些属性名称,然后重命名它们。所以这里我做了R一个和S一个的叉积。我在R一个中有一个叫做sid的属性。
我在S一个中有一个叫做sid的属性。我不想得到一个包含两个相同名称属性的关系。那样是不理想的,我不知道怎么分别引用这两个属性。所以解决这个问题的一种方法是,如果我想的话,可以重命名输出。我可以称之为temp一个,然后指向第一个属性,并将其重命名为sid一个,第四个属性并将其重命名为sid两个。
所以这个叉积,约定是先列出R一个的属性,然后再列出S一个的属性。好的,所以这里在重命名之后,输出将有这个名字temp一个。sid一个作为sid的重命名版本,sid一个和sid两个作为第二个sid的重命名版本。好的。所以再说一次,如果我不想重命名输出,可以省略输出名称。
对于这个特定的情况,另一种选择是,在完成叉积之前就进行重命名,而不是等到做完叉积之后再重命名,而是重命名输入关系本身。所以如果我想将这两个输入关系中的属性区分开来,我可以重命名它们。
我可以在源头上给它们不同的名称,对吧。只需在源头上更改名称,然后做交叉积。这样看起来像这样。对吧,所以我可以,例如,先重命名 S1,然后做交叉积。那也是可以的。对吧。总是最好加上括号,以显示你希望操作执行的顺序。
我们稍后会讨论这个问题。好了,这些是基本操作。好了,现在我们将讨论可以使用基本操作表达的复合操作。所以我们先从交集开始。交集就像并集或集合差异一样,输入关系必须是兼容的,也就是说它们必须具有相同的模式。也就是相同的属性。
相同的类型。交集的 SQL 表达式是 intercept,对吧,在集合表示法中,我们试图计算的是这两者重叠的部分。所以,重复项的问题是什么,对吧?在这里,由于两个输入关系没有重复项,当你做交集时,结果也不会有重复项。对吧。
所以,如果输入是集合,就不需要去除重复项。所以在关系代数的集合语义下,交集不需要担心重复项。好了,我们已经知道,交集用倒立的 U 表示。以后你们会称之为 cap。这是一个复合操作符。那么,你该怎么做呢?
那么,如何使用我们迄今为止描述的其他操作来表达这个呢?有什么想法吗?那么。再说一遍,猜测交叉减去并集。所以交叉积减去并集。不幸的是,交叉积最终会改变模式。对吧。所以你不希望那样发生。一旦你改变了模式。
你不能再应用集合导向的操作了。集合导向的操作必须保持模式不变。对吧。你必须,你必须。好了,我看到一些复杂的表达式。尼古拉斯。等等,我刚刚想到了什么,但又忘了。好了。别担心。好了,让我们走一遍这个过程,看看,好的,克里斯,我想他有解决方案。
所以让我们走一遍这个过程,看看这个对大家有没有意义。好了,让我们从讨论这个部分开始,对吧。重叠的部分,看看我们如何得到它。对吧。得到它的一种方法是从 S1 开始,然后去掉现有部分。其实。
你也可以反过来做。对吧。你可以从 S2 开始,然后去掉 S2 中对应的现有部分。但看看我们为什么决定从 S1 开始,然后去掉相应的部分。对吧。所以你有这个完整的圆圈,你想去掉现有部分。剩下的是这个灰色的部分。你见过这个现有部分吗?嗯,是的,我们见过,对吧。
所以我们知道如何计算这个当前部分。这个当前部分,就是这部分。抱歉,这部分基本上就是S1减去S2。对吧。所以总体上,我有的是S1减去当前部分,而当前部分本身就是S1减去S2。好的。这个是一个相当简单的表达式。同样地,我可以做S2减去S2减去S1,且你可以希望自己也能证明这是可行的。
好的,接下来讲连接。好的,连接是复合操作,就像交集一样。通常,连接是这样的,你有一个笛卡尔积,然后做一些选择。我们可能会在这个过程中重命名一些东西。好的,让我们来研究一些替代方案,然后看看何时需要重命名。所以连接有越来越高的专门化程度。
所以最简单或者更准确地说,最强大的连接形式是θ连接。θ连接基本上有一个下标θ,它基于逻辑表达式θ进行连接,这个符号——蝴蝶结符号就是连接的通用符号。好的。
等值连接基本上是一个θ连接,其中θ是等式的合取。还有一个更特殊版本的连接是自然连接,我们在SQL讲座中见过这个。自然连接没有下标。它只有蝴蝶结符号。这个自然连接是对所有匹配列名的等值连接。好的。
并且你能免费获得的保证是,只有一个匹配列名的副本会保留在结果中。好的,让我们接下来通过这些例子来学习。好的,在此之前我想说的是,连接是非常关键的。笛卡尔积是我从中推导出的数据库系统中的超级关键原语。
它们在不同表之间关联信息时非常重要。我们将花很多时间关注如何高效地执行连接。好的,所以接下来的许多讲座将重点讨论如何高效执行连接,特别是当你同时连接很多表时。
我们肯定想要避免的事情是执行笛卡尔积,笛卡尔积会不必要地膨胀输出。如果可能的话,除非用户明确请求,我们会尽量避免它。我们会尽量找出绕过它的方法。所以,正如我所说,我们将花大量时间研究如何设计高效的连接算法。
好的,从第一个连接变体——θ连接开始,下面是θ连接的语义。对吧?这是如何使用其他操作表达θ连接的方法。所以θ连接很简单。它基本上就是我们的笛卡尔积与S相乘。然后你做一个σ,即使用θ的选择。也就是说,先应用笛卡尔积,然后筛选出不满足θ条件的两列。
所以,如果theta只包含带有and子句的相等条件,这就叫做等值连接。好吧,这是一种特殊的theta连接,theta只包含相等的谓词。这里是一个theta连接的例子。假设我想找出人们预定的船只。
好的,我有我的输入关系R1,我有我的输入关系S1。我想在它们之间做一个theta连接。我说,嘿,知道吗?我会选择sID等于sID。对吧?这是我希望输出的样子。我想将两个关系中相应的记录关联起来。对吧?
所以,这两个记录就是输出的一部分。那么,这就是问题所在,对吧?我无法真正解读这个子句,因为我不知道每个sID指的是哪个,因为s1和S1中每个都只有一边。对吧?所以这个表达式很难解读。那么,我们该如何解决这个问题呢?有没有想法,使用我们之前讨论过的操作?重命名它们。对。
所以,你可能想重命名其中一个关系。那么,怎么重命名呢?嗯,一个选择是重命名S1或R1。假设我决定重命名。假设我说,嘿,我要取sID。
好的,没问题,我再试一次。我将sID取出来,然后映射到sID1。好的,我对所有的1都做了这个操作。所以现在,所有的1都将sID转换为sID1。然后,我可以对sID1和sID做theta连接。好的,现在我知道每个属性是从哪里来的,sID1来自这里,sID来自那里。
好的,问题已经解决了。好了,这是我之前的理解,我认为解决这个问题就是这样。对,我基本上是重命名了两个输入关系中的一个。这正是我在这里所做的。我们再试一个例子。这是一个自连接的例子。我有一个水手表格,我称之为s1,我想为每个水手找出符合条件的数据。
我想找出比那个水手资历更高的其他水手。对吧。那么我们怎么做呢?这实际上是输出的样子。查询大致是这样的。我基本上是取了一个s1的副本,然后重命名它。重命名的原因是因为我不希望这个副本的属性和原来的重复。
然后我做了一个关系连接,另一个关系发生了冲突。对吧。我拿了一个s1的副本,然后重命名了它的所有属性。我又有了另一个s1的副本。然后我做了一个theta连接,条件是age1小于age,对吧,这就是我要强制执行的条件。我想为每个水手找出符合条件的记录。
我想找到那些更资深的水手。因此,添加重命名操作以改变中间关系的字段名在关系代数中是非常常见的。所以,确保你在执行条件时对中间关系进行重命名,这一点要习惯。
对于联接或选择操作,随后的操作没有歧义。对吧?在这种情况下,age1和age的含义很明确,因为它们分别来自输入关系中的单一副本。所以在这个特定的例子中,在age1小于age的条件强制执行之前,结果将如下所示。这实际上是s1和s1的重命名副本的笛卡尔积。
所以我有sid、name、rating、age这些列分别来自左边的表和右边的表。接下来,我使用条件丢弃一些元组,因此我可以丢弃这些元组,依此类推。
最终我只剩下这三个元组。好的,关于这个有任何问题吗?接下来是自然联接。自然联接是等值联接的一个特殊情况,要求两个关系之间匹配的所有字段都必须相等,并且重复的字段会被去除。
对。所以你只有每个字段的单一副本。从概念上讲,流程大致如下:你计算R和S的笛卡尔积,选择在两个关系中出现且值相等的行,然后投影到唯一字段的集合上。这就是自然联接的概念流程。下面是一个自然联接的例子,这里我正在做一个reserves和sailors之间的联接。
我从做笛卡尔积开始。基本上就是将这里的每个元组与这里的每个元组关联。因此,我在这个中间结果中得到了六个元组。然后我查看共同的属性,唯一共同的属性是SID,我尝试保留SID相同的元组。对吧?所以这两个列中的值基本上是相同的。
所以我丢弃了一些元组,因为它们不符合条件。现在我只剩下两个结果元组,但我想确保列名是唯一的。所以我丢弃了其中一个副本的sid。最终我只剩下sid、bid、day这些列,所有这些都来自我们的第一个表,然后是S表中的name、rating、age。
这是一个非常常用的操作,尤其是在外键联接中。这里我有一个从R1到S1的外键,正好是在sid字段上。所以这是自然联接的一个常见用例。现在有一个问题是,我们知道联接并不是所有的联接变体都是复合操作符。
我们如何用其他运算符表达一个自然连接与S1的操作?再一次,我们可能最终做的是重命名操作,以确保我们可以区分两列的副本。然后,我们可能会做一个交叉乘积,接着投影出公共属性。这将是我们的操作步骤。所以,我们将从S1开始。
我们将重命名公共属性sid为sid1,进行交叉乘积。然后我们会做选择操作。接着,我们将投影出属性,直到我们只剩下每个属性的一份副本。希望这能让你明白。另一种读取方式是,虽然这与当前示例无关,但一般来说,你首先处理S1,然后重命名你要匹配相等性的字段。你做交叉乘积,然后匹配两个输入中共同的字段。
然后你会保留每个字段的一份副本作为输出的一部分。所以我们已经讨论了这三种连接变体,还有我们在后续讲座中介绍的其他连接变体,比如左外连接、右外连接和全外连接。
这些符号的样子就是这样,它们是蝴蝶结的小变体。例如,你表示即使左侧的两个帖子与右侧的任何内容都不匹配,它们仍然会被添加到输出中,并且会填充空值。
所以,你表示这些投影就是那种从照片中突出出来的部分。现在我们已经讨论了关系代数中的基本操作,已经涵盖了基础操作和复合操作,总共有大约八个操作。
现在让我们谈谈如何组合更复杂的表达式。一般来说,代数、线性代数、关系代数和初等代数都允许你以自然的方式表达操作序列并进行组合。因此,在初等代数中,你可以像这样做,例如:你正在乘以两个独立的操作,它们是两个独立的操作数,这些操作数本身是其他操作的结果,比如加法或减法。
关系代数允许你做同样的事情。你可以通过三种不同的方式来表示这个操作:一种是赋值语句的序列,另一种是带有多个运算符的表达式,还有一种是表达式树的形式,我们将看到这些每种方式的例子。所以,当你有一个非常复杂的关系代数表达式时,赋值序列是很方便的。
所以你基本上会创建临时的关系名称。也就是说,你拿到中间结果然后给它起个名字。对吧。所以,关系和属性名称的命名可以在你做赋值时通过列出属性名来完成。你可以拿到我们的关系 R1,然后说,知道吗,我要把 R1 的属性重命名为 X 和 Y,通过这种语法,R1 自身也重命名为 R3。
如果我想在 R1 和 R2 之间做一个 θ 连接,假设 θ 的条件是 C。我可以分两步来表示这个操作。我可以说,R4 是 R1 和 R2 的笛卡尔积,接着 R3 是应用选择操作,θ(θ = C) 到 R4 的结果。好的,我们已经看过一些包含多个操作的表达式。
如果你有一个包含多个操作符的表达式,你需要讨论优先级。好的,所以这里的第一个经验法则是:不用担心这些优先级规则,只需要使用括号。这是我的建议,直接使用括号。这是最直接表达优先级、表达你如何阅读关系代数表达式的方式。
但是如果你想懒得加括号,这是我解读关系代数表达式的方式。我会从单目运算开始。比如选择、投影和重命名,它们具有最高的优先级。接着是乘积和连接,跨乘积和连接。
然后最后是集合操作。好的,这是一个常见的约定,通常用于更复杂的关系代数表达式。但我建议还是使用括号来强制规定你选择的顺序。好了,我们来谈谈表达式树。这是第三种表示方法。
这在我们讨论如何进行查询规划和优化时可能会派上用场。所以在表达式树中,叶子节点是操作数。也就是说,在我们的语境中是关系,而内部节点是运算符。它们会应用到自己的一个子节点(如果是单目运算符)或多个子节点(如果是双目运算符)。所以如果我有两个关系,假设是 S reserves 和 sailors。
而我想要找到所有评级大于 5 或者预定了 100 的水手的 sid。好的,我该怎么做呢?我将这个分成两部分。对吧。我想要找到所有评级大于 5 或者预定了 100 的水手的 sid。我意识到评级信息在 S 中,而船只信息在 R 中。
所以我需要将这两个关系之间的信息结合起来。假设我先从这一部分开始。好吧,我做的是所有评级大于 5 的水手。我从 S 开始,然后我可以举个例子,做一个选择操作(σ),然后说,我想要那些评级大于 5 的水手。接着我把其他信息投影出去,只保留 sid。
好的,我已经计算出了结果的一部分。结果的第二部分来自那些预定了100号船的人。所以对于这个,我将从r开始。然后我会有一个选择条件,就像我在S中做的那样,表示BID等于100。再次,我会有一个投影操作,只保留sid。好的,保留sid,因为这是我在输出中需要的内容。最后。
我可以做一个联合操作,将两边的结果合并。好的,这基本上就是结果。好的,这有问题吗?好吧,这和我之前画的东西是一样的。所以稍微回顾一下,我想重新审视一下我们为什么要学习关系代数,以及我们为什么要花很多时间讨论这些关系代数表达式。关系代数表达式,就像在高中学代数、线性代数一样。
这些对数据库系统来说很容易操作。而且操作符的数量也很少,所以很容易处理、简化、重写。为了弄清楚如何重写和简化线性类型的关系代数表达式,数据库系统使用了各种启发式方法来进行这些重写。
以及各种成本函数,用来在不同的重写方式之间做选择。好的,我想给你们一些数据库系统可能考虑的重写方式的例子。我们将在课程后面深入讨论这些重写方式,以及哪些更有意义。好的,这里是一个简单的重写。对,这个简单的重写表示,嘿。
我有两个条件,表达式一和表达式二,我正在将它们同时应用于某个操作。我当然可以把它们分开处理。通过将表达式一这个条件应用于表达式二的选择结果,我同样可以在表达式一之后应用表达式二这个条件。
而且这些实际上并不会改变结果,好的,为两个质量符号道歉。好的,这些都是一样的,它们都是等价的重写。另一个例子。一个非常强大的例子是改变连接运算的顺序。我们可以先连接r和s,然后再与T做连接。
我可以先连接s和t,再与r做连接。好的,哦,我看到很多关于"和"的疑问,这只是"和"与"D"的简写形式。好的,还有其他问题吗?好的,好的,这里是另一个非常酷的重写例子。好的,这个叫做谓词下推,非常重要,现在学了将来还会再学一次。
假设我想查找年龄大于40岁的水手的预订信息,并且我有这两个关系,r和s,r1和s1。我可以通过这个表达式来实现。对,所以这里我基本上是想对这两个关系做一个自然连接。然后我想去掉所有年龄大于40岁的水手。基本上是这样做的。
我做自然连接,再做sigma。好的。另一种重写方法,我会给你答案,而不是问你,就是先在s1上做sigma,然后再做自然连接。之所以这样可以,是因为这个条件“年龄大于40”仅适用于s1。好的,“年龄大于40”仅适用于s1。因此。
你可以先这样做,再做自然连接。这个重写操作,首先进行sigma选择操作然后再推到下方,叫做谓词下推。这非常重要,因为它基本上减少了中间结果的大小。对吧。如果比如说,经过这个选择操作后,s1中的结果非常非常小。
所以你从一百万个元组开始,在s1中剩下了10个元组。那是一个显著的减少。对吧,因此进入连接时,你需要处理的输入元组少了很多。连接和笛卡尔积非常昂贵,对吧。
因为你实际上是在进行二次排序的增加。这样很尴尬,因为左侧的每个元组都与右侧的每个元组匹配。所以,尽量减少中间结果的大小是很有意义的。因此,通过将选择操作推到下方来实现这一点。
这样可以减少中间结果的大小。所以这个重写是非常强大的,数据库系统通常会使用。这里是另一个重写的例子,消除我们在SQL中已经看到的一个关键词。对吧,所以我们看到的not in关键词,它在允许我们表达相对复杂的需求时非常强大。
好的,所以在这里,我想找出那些没有预定某个船只的水手。说出这个的最方便的方法是说,选择blah,blah,blah。条件是s_id不在某个集合中,我在查看这个预定表中与船ID 103相对应的s_ID。好吧,那么我们如何用关系代数来表示这个呢?对吧。
因此,表达这个的关系代数方法是这样的,对吧。所以你从水手表中取出……实际上,这里应该是s,而不是r。对,应该是s。好的,取出水手,将除了s_id之外的所有内容都投影出去。然后扔掉那些已经预定了船ID 103的水手。
如何找到那些已经预定了船ID 103的水手呢?嗯,我取出与船ID 103相对应的两个点。我与s做自然连接。这基本上就是所有与船ID 103的预定和水手ID相连接的元组。然后我去掉除了水手ID之外的所有内容。其实。
我知道有一种方法可以进一步简化这个。你有什么想法吗?Janelle。抱歉,我以为有问题。好吧,让我按一下按钮。哦,好吧,没关系。所以,进一步简化的一种方法,我本应该意识到,就是简单地做出选择,条件是sigma b_id等于103的幂。
我甚至不需要做自然连接。那样更简单。好吧,还有其他想法吗?所以,内森(Nathaniel)说,为什么不把派去外面?那样可以吗?我能做吗?
那我们就试试吧。对吧?所以我想做 s id 和 s 减去 sigma b id 等于 103 的 R。我可以这样做吗?不行,对吧?我不能这样做,因为这个模式与这个模式不相同。对吧?所以我不能这样做。所以这个可以,这个不行,这个也可以,但这涉及到一个不必要的连接,我没有意识到。好了,内特。
你想描述一下你的问题吗?我在找水手的名字。所以似乎我们需要在找到 s id 后再获取名字。好的,我错误地假设应该是 s id。好的,那么如果是名字的话,我就需要做一个连接。好的,没错。如果我要获取名字的话。
这将是派 s 名称(s name)减去派 s 名称(s name)的 sigma b id 等于 103 的 R,与 us 做自然连接。这样有道理吗?好的。好的,这两个适用于 s id,而这个适用于 s name。那么 Felix 的问题是,查询优化的表面层是在连接和交叉产品之前的操作上进行的吗?
是的,亦是如此,我认为这是许多优化之一。确定连接顺序也是一个关键步骤。还有其他优化方法,但不仅仅是,仅仅减少中间结果。这是一个重要的考虑因素,但这不是唯一的考虑因素。
好的,接下来是关于扩展关系代数的幻灯片,超出了简单操作和派生操作的范围。我们还可以进行分组操作,符号是 G 的伽马(gamma)。因此,你可以说,我想计算水手的年龄和按年龄组计算的平均评分。对吧?所以这里,由于我没有对年龄进行任何聚合函数运算。
这最终成为了分组列。我也可以添加一个 having 子句,所以我可以说,按年龄分组,计算平均评分,且想要计数大于二。这隐式地将“分组”和“having”结合起来,基本上也是选择条件的结合。对吧?就像在 years 中,你实际上是子选择了输出中的每个平均评分。
所以这就是为什么需要使用选择(select)子句。having 子句基本上就是这个。好了,下面是我们今天所涵盖内容的总结。关系代数基本上是说,这里有一小组操作,你可以通过多种方式组合它们,这些操作将关系映射到关系,所以输入是关系,输出也是关系。这些操作是“操作性的”的,因为你需要明确指定操作执行的顺序。
这些运算符是闭合的,这意味着你计算出的结果仍然是一个关系。关系代数中的表达式非常容易操作、重写和简化,而关系代数中的表达式也非常强大,对吧?这些少数几种操作就可以封装很多SQL的功能。所以,就像我们看到的not in子句,你实际上可以通过简单的重写来去掉它。好。
我们已经覆盖了六个基本操作,五个基本操作,我们已经覆盖了六个。好的。应该有一行覆盖了六个基本操作,以及一些复合操作,比如交集和蝴蝶结。然后我们要做连接,关于这些有问题吗?好的,我会……你想去吗?我可以回去一页吗?好的。问题是什么?我,答案是肯定的。
我们是在问问题吗?我回去了一页,应该是。我已经回去了。还需要再回去吗?好的,我猜那就是问题了。好吧。酷。好的。我想我做对了。是的,我觉得某些幻灯片还没有上传到公开网站。我已经打印了当前版本,并上传到云端,但如果你做了任何更改……
随时更新它。好的。我以为我已经添加了最新版本,但是……不知为什么,最后几张幻灯片没有显示。我不知道为什么。好的,是什么时候突然中断了,然后后面什么都没有。好的,我认为它在嵌套切片之前停止了。好的。所以我就……
所以这就是为什么我认为你可能有另一个版本,另一个你发布的版本。好的。
好的。好的。好的。好的。
等等。
好的。
好的,太好了。关于关系代数还有其他问题吗?好的,我们还有15分钟。所以,嗯,如果没有,我有一个小问答题,对吧?
所以我觉得上次有些人已经听说过这个了。正如你们记得的,我们有一个助记系统,对吧,用来记住这些不同的关系代数运算符。所以,比如说,这是选择运算符,对吧。那么,大家还记得我们为什么选择了希腊字母sigma吗?我们并不是随便选的,对吧?在最初的论文中有解释。
所以为什么选择sigma作为选择运算符?好的。所以,这就是我们选择它的原因,让我猜猜看。我显然猜错了。但因为我们做的是代数,而代数起源于希腊人。你应该使用希腊字母。斯宾塞,对吧?好的。就这么定了。好的,没问题。对。所以我们用sigma来表示选择运算符。我们用pi表示投影,对吧?
所以我觉得大家已经看到这个模式了,对吧?好的,那么这里就有一个问题,对吧?所以我认为我们没有按权重来覆盖这个内容,我们也覆盖了这个,对吧?所以gamma是按分组来做的,对吧?基本上就是分组,对吧?所以有一个操作符我们在这堂课中没有涉及,那就是tau,它代表排序。比如,如果你使用order by
,你会看到tau基本上就会出现。
所以我们有一个模式,对吧?我们有s,我们有P,我们有g,对吧,那么tau怎么回事?第二个常量很简单,是吧?因为s已经被占用了,你看到过了,对吧?所以我们不能使用s。那么还剩下什么呢?我们就选择一个紧跟在s后面的字母,结果发现是t,所以这就是为什么我们最终会用tau。所以,你们也许会想,关于这个还有什么疑问吗?
所以我们称这个为连接(join),对吧?但是,至于我所知道的,这不是一个希腊字母。那么怎么回事呢?对,应该是J,但在场有没有人懂希腊语?希腊字母里有J吗?没有,希腊字母里没有J。所以他们不得不想出其他的方式,然后他们选择了这个,我也不知道为什么。所以,是的,好的,今天的趣味问答就到这里。
所以我希望你们能享受一下这个笑话。好的,太好了,没错,因为希腊字母中没有J,这就是为什么会出现那个问题。所以,是的,不是的。然后,我听过的关于蝴蝶结的一个解释是,它看起来像一个二分图。所以你有两端从一边到另一边的两个点,之间有边连接,它有点像那样。
但我还没有听到比这更好的解释,你是说像这样吗?对,没错,但它就像是。对,好吧,你们的想象力比我强,所以你们能想到更好的答案。好的,接下来我们来谈谈排序和哈希,那么为什么我们要讨论这个呢?首先,首先是这个大概的框架,这是我们在这部分课程中讨论的大图景。
所以实际上,我们已经完成了DBMS中很多不同的组件,不管你信不信。现在,我们其实在讨论的就是我们刚刚完成了关系操作符的部分。接下来我们要进入的阶段是创建解析和优化。在我们谈论解析和优化之前,我们首先必须讨论一下你们从上一讲看到的每个关系操作符到底是怎么实现的。
我们在这门课上讨论关系代数的一个原因是,因为这些操作符几乎都可以很好地映射到我们在第三个项目中实现的函数。你可以想象,选择操作就像一个函数,你传入一个关系和一个谓词,然后返回另一个关系。
依此类推,对吧?但为了理解如何优化,我们首先必须学习如何实际实现我们在上节课中看到的每一个操作符。这就是为什么我们要讨论排序和哈希的原因。
我在想排序和哈希是如何与查询优化,或者说与关系代数运算符的实现有关的。我们并没有明确讨论排序操作符,而且显然也没有哈希操作符,对吧?
那么我们为什么要讨论这两个问题呢?事实证明,这两个操作非常常见,它们在许多不同的关系代数操作中都会出现。第一个就是排序。有时候这个操作在一些称为“随机操作”的操作中使用得非常频繁。
例如,当我们尝试去除重复项(比如 distinct)时,实际上是将多个关系合并在一起,然后去除那些重复项。如果我们能以某种方式对它们进行排序,就能很容易实现去重操作。
我们就这样排序,从头开始遍历,然后每次看到相邻的重复项时,就将其删除。显然,分组操作也是如此,这就是我们刚才讨论的伽玛操作。
所以为了形成分组,我们首先需要能够确定在关系本身中,什么是分组。然后为了做到这一点,为什么不直接排序呢?如果我们能对它进行排序,那么我们就能通过逐行遍历来识别出分组的位置。接下来我们将在这节课后讨论一些内容。
比如说,使用一种叫做排序合并的特定算法来实现连接,正如其名所示,排序合并算法需要在这个过程中进行排序。所以我们也在这里讨论排序。有时在扩展的关系代数中,排序也是显式要求的,就像我之前提到的,关系代数中有一个称为“排序”的操作,它实现了 ORDER BY
,这就需要排序。
今年我们一开始讲了索引的批量加载,后来也讲了排序。我们这节课解决的问题是如何在有限的内存下对大量数据进行排序。所以你们已经学习了不同的排序方法。
我希望这能唤起你们在61课上学到的内容,比如归并排序、快速排序、冒泡排序,我们已经学过了这些内容。所以,是的,我们不会重复讲解这些算法,但我们面临一个不同的问题,就是磁盘上有大量的数据。
而且相比之下,内存非常有限。那么我们该如何进行这种方式的排序呢?如果你们修过162课程,你们可能会认识到有一种叫做虚拟内存的技术,没错,我们可以通过虚拟内存加载所有的记录到内存中,然后在内存中应用我们最喜欢的排序算法。
那么为什么那样行不通,或者说它行得通吗?是的,尼古拉斯说这是会引起抖动的,对吧?所以记住在虚拟内存中,抽象的基本概念是,我们会进行折衷。我们通过相同的接口隐藏了从磁盘加载数据的过程,仿佛一切都已经在内存中。但实际上,在后台,操作系统正在进行所有随机I/O操作,以便将所有内容加载到内存中。
抱歉,加载到内存。所以,如果我们采用这种方式实现排序,这实际上会是一种非常昂贵的操作。我挑战你们尝试实现它,就像你们在162的Pintos作业中做的那样,如果你们真的这样做,你们就会明白我的意思。
所以为了正确地解决这个问题,我们在这节课上讨论了所谓的核心算法。它被称为“核心外”,因为主内存或RAM曾经被称为核心内存。就像以前主内存或RAM曾被称为核心内存或主内存一样,这个名称至今仍在使用。所以,既然我们在讨论的是排序数据,这些数据目前存在于磁盘上,而不是已经加载到内存中。
所以它们被称为“核心外”。这就是为什么你有时会听到这个名字,它有点过时。有时你也会称之为外部算法,外部指的是外部于主内存。所以,我们再次提到的是磁盘。我们将讨论的这些算法的主题,第一个是,你会经常看到一种模式,我们基本上是单次传输数据,从磁盘流到RAM。
所以从磁盘读取一页数据,在RAM或主内存中处理它,然后再写回磁盘。这是一种模式,在这节课中你会反复看到它。然后你还会看到分治法的应用,你们在61课程中学到的分治法,在我们将数据加载到内存之后。可能你会对它如何运作感到好奇,为什么它就是分治法,我们会看到它实际上非常酷。
所以让我们先谈谈单次流式处理。正如我所说,目标是基本上尝试从磁盘中一页一页地读取数据到内存中,处理它们,然后再写回磁盘。所以假设我想实现映射操作,对吧?
函数映射。所以实际上我有一个函数F,我正试图应用到磁盘上的每一条记录上。然后这里的挑战是,正如我所说的,我们必须最小化我们使用的RAM量,因为我们没有足够的RAM来存储所有主内存中的记录。
所以我们解决这个问题的方法是从磁盘上读取数据块。这里是磁盘,我只是用这种卡通式的表现方式。我们从磁盘上读取数据页到主内存中,你看到的是这个矩形。我一次读取一页数据,对吧?到这个输入缓冲区。
应用这个函数F效果,然后把结果写回磁盘,对吧?例如。第一次从磁盘读取时,我可能会读取三张矩形,想象它们是记录。我将每一个应用到函数F上,然后生成这些五边形。然后我再读入下一页,继续生成更多的五边形。
所以请注意,五边形和矩形右边的大小是不同的。因此,矩形在输入缓冲区中占据了三个位置,但五边形看起来比较小,我实际上能够把它们放得更多,更多地放入输出缓冲区中。
对,这种方式比如说。比如我一直重复这个过程,直到输出缓冲区满了,然后我将所有数据写回磁盘。对吧?那么,为什么我要拖延这个过程呢?为什么不在收到新的输出时立即把每个五边形写入磁盘呢?
延迟这个过程的优势是什么?是的。减少IO,对吧?为什么呢?因为如你所记得的那样,顺序IO是好的,对吧?随机IO是坏的,对吧?所以如果我把所有内容一个个写入,那将会产生大量的IO,对吧?而且它们可能还会。
是随机的,但如果我把整页写出来,那就是顺序IO了,对吧?所以如果我们使用的是磁性旋转盘,例如,这将是很好的。所以这是一种你会不断看到的模式,对吧?所以我们基本上填满输出页面,然后再像你知道的那样,再写一次,然后我们就重复整个过程,直到从磁盘上处理完所有其他页面。
所以如果我们聪明一点,我们其实可以尝试运行被称为双缓冲的技术。这样我们就不仅仅有一个输入缓冲区,我们可以有两个。为什么呢?因为我们可以启动一个第二个线程,它的责任仅仅是从磁盘读取或者将一个完整的页面写回磁盘。而且我们可以将它与当前执行映射函数的线程并行执行。
这种并行性是可行的,因为你知道,从磁盘读取需要时间,写入磁盘也需要时间。如果操作系统支持多个线程,那么我们就可以启动两个线程,同时有一个线程在运行映射函数。
我们有一个专门的线程来处理 I/O。这个模式在这个类中是非常常见的。当任一缓冲区已满时会发生什么呢?比如空缓冲区已满,或者写缓冲区已满,那么我们就交换这两个缓冲区,切换两个线程。
当我们计算映射函数时,这个线程就会处理 I/O。因为你知道,我们有一个完整的页面,可以将它写回磁盘,或者我们有一个空页面,可以从磁盘读取。然后我们只需切换另一个线程来执行实际的计算任务。
所以这将非常有用,接下来的讨论也会围绕这个展开。我们假设你有额外的资源可以用来进行双缓冲。所以实际上这是一个如此常见的模式,以至于我们以后不再提及它,假设它会在后台自动发生。这有意义吗?到目前为止有什么问题吗?好的,我们刚刚已经讨论了这个模式:你通过一个专门的 I/O 线程进行流式处理。
所以,现在让我们回过头来重新审视这个排序和哈希的问题,我们到底在这里试图做什么呢?我们来正式地定义一下,这里是关于排序的最初规格。我们的目标是生成并输出一个单一的文件 F_of_S,存储我们关心的关系 R 的内容。
然后,所有记录必须按照我们给定的排序标准进行排序。实际上我们要做的是,什么是哈希?哈希,实际上是我们试图生成一个输出文件,F_of_H。在此文件中,它保存了与输入中相同的内容,所以我们并没有丢弃任何数据。
除了我们有一个要求:任何两个具有相同哈希值的记录不能被另一个记录分隔。如果两条记录的哈希值相同,它们应该在磁盘上的输出文件中连续存放,这个文件我们将称之为 F_sub_H。
再次回想一下我们为什么要做哈希处理。因为我们想用哈希来实现去重。所以为了做到这一点,我们需要将所有相同的记录,特别是重复的记录,尽可能放置在文件中的相邻位置。
如果我们能够做到这一点,那么我们就可以使用顺序IO来实现去重,这是非常有效的。所以我们的目标是,生成一个文件,其中所有哈希值相同的记录必须连续存储在磁盘上。另一方面,我们得到的是一个输入文件F,它包含了多个记录的多重集。我们假设这个文件F以块或磁盘上的页面形式存储。
我们假设我们有大量的页面和磁盘上可以使用的空闲存储块,比我们所拥有的主内存还要多。事实上,如你所知,我们将简化之前的表示法,只说我们有足够的RAM块可以从磁盘中读取数据。
这样说清楚了吗?那么在过去的一分钟里,让我简单介绍一下如何实现排序。这里有一种简单的方法,当我们只有有限的RAM但数据量很大时,我们尝试进行排序。我会分两步来做。第一步是我们从主内存中读取一个页面,然后进行排序,最后将其写回。这是如何工作的呢?
我们一次从磁盘读取每个页面。然后我们会将它们放入主内存中进行排序。此时,你可以使用你喜欢的排序算法,因为一切数据已经在主内存中了。所以我们假设使用快速排序。然后我们会将数据直接写回磁盘。
到目前为止一切都还不错,但到目前为止我们仅仅是分别对每个单独的页面进行了排序,对吧?但我们的目标是生成一个包含所有内容的单一巨大文件,其中一切都已经排好序。因此,现在这种方法并不起作用,因为最终我们只是对单独的页面进行了排序,并没有对整个文件进行排序。
那么接下来我们该怎么做呢?想一想你学过的归并排序。对吧?这就是我们在这种情况下可以做的事情。我们基本上是将这些已经排序的页面重新读入主内存中。此时,我们需要三个缓冲区页面,因为我们要一次从磁盘读取两个页面。
这些页面已经排序了。然后我们将它们合并到输出缓冲区中,每次合并一页。对吧?这和归并排序算法是一样的,我们只是从两个页面开始读取数据。
然后,我们基本上根据排序顺序决定哪个应该先排,接着将其放入输出缓冲区。然后,当输出缓冲区满时,我们会将其写回磁盘。对,就是这样。所以这就是我们在做的事情。这是一个流式算法,正如你在前一张幻灯片上看到的,我们基本上是每次从输入磁盘中流式拉取两个数据块,然后以某种方式处理它们,再将其发送到输出。
除了这种情况,我们是通过两次处理来完成的。首先,我们先对各个页面进行排序,然后我们基本上通过将所有内容读回主内存并写回磁盘来应用归并排序。最终,我们将得到一个完全排序的巨大文件。
但现在还没结束,因为到这个时候我们只合并了两个页面,我们还需要合并更多的页面。我们必须继续合并所有页面,直到得到一个单一的文件。所以接下来我们会继续讨论这个问题。如果有任何问题,请留下,否则我们星期四见。
我们在这里使用了清晰的“favities”插入符号。
P9:Lecture 9 Sorting & Hashing - main - BV1cL411t7Fz
到局部的右边。
是的,也设置为本地,好的,所以你好,大家好,欢迎来到186的周四讲座,在我们开始之前,我只想重复一下Aditm在,星期二,所以第一件事是如果你决定和助教一起工作,请尽量描述一下。就像你的项目需要的那种帮助,嗯,这样助教们就可以尝试合并不同的请求,或者喜欢人们问类似的问题,嗯,所以团队真的很感激你们这么做,因为就像你知道的,我们在办公时间收到的罚单数量将会大大增加。所以对你们来说,请试着想出,就像一个关于,就像,你有什么问题,你知道我们并不想这么做,但就像在最坏的情况下,就像你知道的,如果队伍真的变长了,人们没有描述,我们将不得不执行,让你们回到队伍里等待下一轮。如果你不为你的票写上说明,所以我提前道歉了,但这是我们唯一能处理的方法,嗯,就像你知道我们预计以后项目的门票数量会越来越多,然后也就像我刚才说的,你知道的,请尝试打开你的视频,如果你不介意的话,呃。
它给了我们喜欢,你知道的,在见到你们方面有很多很好的反馈,就像你知道的点头,或者像你知道的那样看起来像不困惑,这基本上是告诉我们讲师放慢速度的一种方式,或者谈谈,呃,或者试着再解释一遍,嗯,别担心。大家可以看到,我什么都没穿,不错嘛,嗯,你不需要那么做,我是说我还不如穿着睡衣,对所有人的权利,我们关心,就像你知道的,我们没有录下你们的脸,你们的脸也没有出现在演讲视频中,所以别担心,只有我出现了。所以我必须穿睡衣以外的衣服,是啊,你没有相机,很抱歉,但如果你这么做了,请尝试,请尝试加入我们,好的,好的,所以呃,只是为了回到材料上,所以周二我们开始讨论,关系代数,我们也开始谈论排序和哈希。你可能还记得,我们谈论的原因,这是因为我们想了解这些关系代数操作实际上是如何实现的,顺便说一句,你也会在项目3中看到,原来排序和散列是两种非常常见的操作,呃,在不同的关系代数运算符实现中显示。
所以这就是我们谈论这个的原因,呃,在这次讲座中,整个目标基本上是开发不同的算法,我们可以使用,这样,稍后当我们谈论查询处理或查询优化时,然后我们可以讨论如何在不同的实现中进行选择,所以两个主题。有点浮现,或者它会在本课结束时出现,事实上,我们经常流媒体,从磁盘流式记录,嗯进入主存,处理它们,然后把它带回磁盘,另一个是你们在61年就已经学过的东西,就是分而治之,作为一种算法。我们将在本课中反复看到,所以在周二,我开始谈论双重缓冲的想法,那个,你可能还记得,讨论了从磁盘加载数据的想法,你在屏幕左下角看到的,将它们加载到主存内的输入缓冲区中,通过调用这里的某个函数来处理它。我一般指的是x的f,再把它们写回记忆中,就在输出缓冲区中,然后最后把它们填满,或者你把它们写回你在右边看到的磁盘上,我们称之为双缓冲的原因是,因为你知道我们可以更聪明。
我们不必一次一个记录地浏览所有的东西,按顺序,我们实际上可以有多个输入缓冲区和多个输出缓冲区,其中我们有一个线程专门用于执行,或处理已在内存中的输入元组,也就是x的f读,然后另一个单独的专用线程。其唯一的工作是从磁盘读取元组或将元组带入内存,或者在右手边处理完元组后写出元组,我们之所以可以使用两种威胁是因为,在其中一个buff输入缓冲区完全处理后,我们实际上不需要等待从磁盘中引入新的元组。其实我们可以,你知道的,把两个威胁交换对,所以我们只是交换了两个威胁,所以以前从磁盘上带来两个帖子的威胁,现在可以更改以运行处理器,可以这么说,或者在已经引入的两个帖子中的每一个帖子上的X的F。然后另一个用来处理的线程,现在可以加载元组或编写元组,所以这个通用的概念被称为双缓冲,我们可以这样做,你知道的,我们将在这节课剩下的部分中讨论的所有实现,事实上,这只是一件很常见的事情。
我只是不打算把它画出来,但你可以想象这基本上就是正在发生的事情,呃,在被子后面,罗曼,你有问题吗?我不是很熟悉你说的线程,这就像你听说的多线程处理器一样吗,还是这里发生了什么,是啊,是啊。所以这正是多线程的概念,所以你可以想象有两个工人对流程的威胁,如果你想打电话给他们,嗯,一个进程或一个线程,基本上只致力于做元组处理,另一个线程或另一个进程专门用于执行i o操作,这正是它的意思。现在回到排序和散列,就像,你知道的,刚才的双缓冲只是一个通用的框架,我们将运行,排序和哈希算法,关于这两个,um操作,这是我们周二也通过的正式规范,所以排序正是我们想象中的正确做法,就像你知道的。我们的目标是在一个巨大的文件中读取,然后生成输出文件,文件中的所有内容都根据我们试图做的任何标准进行排序,然后是散列,um是将输入元组拆分为不同桶的想法,呃,除了我们试图强制执行,事实上。
没有两个具有相同哈希值的记录被,嗯,一个不同哈希值的记录,这意味着如果两张唱片最终,um哈希到相同的桶或具有相同的值,那么它们不应该被另一个具有不同哈希值的记录分隔,我们想这么做的原因是。因为我们试图使用散列来实现,呃,重复照明,正如你所记得的,这是呃,SQL操作的人记得什么,我们怎样才能得到相同的不同,是啊,是啊,这个东西对,例如说对,所以这正是我们如何实现不同的,在SQL中。如果我们能实现哈希,那是如此,这就是我强制执行这方面的原因,对呀,我们不想要任何两张唱片,具有相同的哈希值,由其他任何东西分隔,因为如果我们那样做,那么我们就无法判断它们是否是重复的。而不必读入整个文件,这很糟糕,因为正如你所记得的,顺序,I O是好的,随机,伊娥不是,所以说,这就是为什么我们希望文件以这种方式布局,我们得到的,另一方面,是包含要发布的输入文件,我们只是假设需要n个。
不同的页或存储块,以便将它们保存在磁盘上,我们有,假设无限多的磁盘大小,所以我们不担心磁盘空间用完,虽然另一方面,我们确实有固定数量的RAM可以使用,特别是我打电话给你,就像你在用,B对了,用喜欢。你知道从上一节课,至于,呃,我们可以玩的主存块的页面,这有道理吗,所以设置就像你知道我们有一个巨大的文件,我们只有很少的内存,或者至少在方面与输入文件的大小进行比较。我们的工作基本上是对这个巨大的文件进行分类和散列,现在,如果你有足够的主存来读取整个文件,那这就不用动脑筋了,当你刚运行你最喜欢的算法从61 b对,我是说,就像你知道你可以运行快速排序。你可以运行泡泡或任何类型的,你也可以在主存中构建一个内部哈希表,然后我们就做对了,但我们不是生活在那个世界里,所以这有点挑战性因为我们只有有限的内存,与文件大小相比,与我们试图排序哈希的文件的大小相比。
嗯,所以让我们从开始,这里有一个简单的算法,我们将运行多个通行证,第一遍包括从磁盘上一个接一个地读取每一页,对特定的页面进行排序,所以我们从磁盘中读取了一页,现在在主存中,我们将在主存中对其进行排序。然后我们将把排序的结果写到磁盘上,就在另一个文件里,假设现在我们已经对每个单独的块进行了排序,或文件的每一页,但那不是我们做得不对,我们希望对整个文件进行排序,不是单个块。所以我们现在需要再次对文件进行多次传递,所以你可能还记得合并排序的想法,也就是,你知道的,我们从两个推列表开始,然后我们试着把它们组合起来或合并起来,只需从两个列表中的任何一个中提取第一个元素。取决于哪个实际上是顺序中的第一个,然后将它们放在合并列表中的沉浸式版本中,所以这里,我们可以这样做,我们的磁盘文件对,所以我们要朗读,一次两页,所以这相当于两个呃,我们已经单独排序的两个磁盘页,对呀。
然后我们将在主存中运行合并排序,就为了这两页,好的,所以我们要弄清楚,第一页或第二页的两篇文章中,哪一篇实际上是最不正确的,取决于排序准则,然后我们要把它写出来,第三页,我们将继续这个过程,对呀。我是说我们有两个,但是我们只有一个输出页,所以输出缓冲区会被填满,在我们在一两个元组中详尽地遍历所有元组之前,所以一旦输出缓冲区被填满,然后我们就把它写到磁盘上或者把它们抽干。所以现在输出缓冲区3变成空的,我们再次进行合并过程,我们再次重复这个过程,直到我们用完输入缓冲区的第一页或第二页,在这一点上,我们然后把,就像你知道的,我们能我们能。我们基本上可以把以前排序过的其他页面,然后现在再做同样的事情,但就像我说的对,那不是,我们做得不对,因为在这一点上,经过一个过程后,我们基本上已经将磁盘上的两个页面合并到一个更大的运行中,由两页排序的。
呃,记录,但我们需要对整个文件进行正确的排序,我们不仅仅是对其中的两页进行排序,所以我们基本上需要再次重复同样的过程,通过阅读这些二的运行,然后重复同样的过程,通过合并它们来创建四个页面的运行。如此等等,直到我们把所有的东西都合并在一起,所以这里有一个图片的方式来说明整个过程,我称之为征服和融合,因为我们有第一步,我们只是单独分类所有的东西,然后我们开始合并过程,所以这里的第一步。我把它们称为concurve阶段,或者你想叫它什么,它基本上包括对块的单个页面进行排序,好的,所以在这里,就像你知道数字一样,每个黄色的盒子,橙色方框与实际对应,然后右边这个紫色的,你可以考虑。就像需要去的头或元数据一样,呃在文件的开头,所以在第一页之后,或者在第一次通过或通过零之后,我们只需对单个文件进行排序,所以你可以看到这里最左边的第一页被排序了,左边的第二页也得到排序,以此类推。
现在我们需要合并,就在第一次传球之后,我们该怎么办,我们把前两页,将它们读入主存,然后合并它们,这样你就可以看到了,所以我们开始合并三个,四和二六,所以这就是为什么我们得到元组,两个先。因为两个先于三个,然后是三个,因为三个在三个之后,呃,四,然后我们剩下的元组也是如此,我们从两页开始,所以我们要在最后写另外两页,现在你可以看到,在这个例子中,我们现在有一个两页的运行,或者。我们一开始只是对单个页面进行排序,这有意义吗,所以如果我们有更多的缓冲页,我们会像这样读到的,你当时知道更多的页面,但在这种情况下,因为我们只有像,你知道吗,输入的两页,所以这就是我们读的原因。你知道第一页,一次只读前两页,然后合并,合并,然后把它写回磁盘,现在我们只做了前两个街区,你可以想象剩下的步骤我们要做什么,台阶内侧,在第一次传球的右边,即我们要把接下来的两页,把它们读到记忆中。
对它们进行分类,然后在我们浸入它们后立即将它们写回来,等等,等等,请注意,现在我们已经创建了排序正确的两个页面或两个页面运行,你也可以看到我们是,我们在排序文件的大小方面都在增长。我们也在减少跑步的次数,就在第一次传球或零次传球之后,我们最终有八次不同的跑步,它们中的每一页都正好由一页组成,在第一次传球之后,我们现在有四次跑动,对呀,对不起,呃是的,我们现在有,呃,四分。每份由两页组成,是啊,是啊,我可以告诉人们,是啊,是啊,向右重复,是啊,是啊,就这样重复,第二次传球或第二次传球,我们再把前两页,现在的前两次跑步,由两页单独组成,然后我们将再次运行这个合并过程。现在我们创建四页的运行,然后另一个也是这样,对,最后一次通过将完成合并或抱歉,整理整个第八页第五页,所以让我们对每一个通行证做一点计算,嗯,我们直接从输入文件中读写每一页,所以就IO成本而言。
这将使我们付出正确的代价,我们浏览整个文件,然后我们把每一页都读一遍,然后我们也把每一页都写出来一次,所以那是n,但现在我们需要重复多少次这个过程,我是说在这种情况下你可以数数,我是说嗯。但就像你知道的,我要去,别拿数学来烦你,基本上说这是n的对数基数2对吧,其中n是磁盘块的总数,我们试图处理的这个特定文件,然后加上一个,因为我们基本上需要考虑第一个,嗯,第一关,对呀,一个一个,是啊。是啊,第一遍基本上只需要读出,就像你什么都知道一样,然后你知道你可以在这里看到的传球次数,随后只是由日志基给出结束罗马,你有问题吗,关于图表的一个快速问题,堆叠的文件有意义吗,呃,而不是在水平面上。那意味着什么吗,你从,哦,你是说像那样,然后你以,是啊,是啊,准确地说,你从水平到垂直,对呀,所以我试着用垂直的来显示那种版本,好的,但它如何影响记忆或类似的事情没有意义,否,否,否,否,否,好的。
是啊,是啊,严格来说,它是为了显示每次运行的大小,也是为了表明它们是排序的,我在聊天时问了一个问题。我想有人刚刚回应了,但我还是想问,嗯,那么我们实际上如何像合并多个页面运行在一起。如果我们只有有限的记忆,因为在最后就像,我怎么猜像,在最后说,如何将这两个四个客户合并在一起,而不将其全部加载到内存中,是啊,是啊,所以请记住,在四页中,像你知道的一个案例,单个文件已经排序,对呀。所以这个文件已经排序了,另一个文件已经排序了,所以我们只需将这两个文件的第一页读入主存,一次一页,然后我们在主存中合并,所以我们先试着,所以也许这个是一个不好的例子,所以如果你回去让我们看看,就像。你知道这就像,你知道的,是的,是的,就在这里跑,所以我们试着合并二三四六,另一个文件有四个七,八和九,所以我们在有限的内存下这样做的方式,因为我们不能把所有东西都加载到主存中,因为我们只有三页。
在主存中,其中一个已经被输出消耗,所以要做到这一点,基本上只需阅读第一页,这两个运行的权利,我们可以负担得起正确的,因为我们有两页可以读,所以我们读了这两篇文章的第一页,然后我们在主存中依次合并它们。所以我们基本上看到,你知道的,二比四,所以我们先取两个,然后我们基本上把记录放入输出缓冲区,然后我们取三个右,因为三也在四之前对吧,但是在我们用完左边的前两个元组之后。然后我们意识到第一个缓冲区现在是空的,那么会发生什么,然后我们基本上把第二页读入输入缓冲区,然后我们再次进行相同的合并过程,好的,是啊,是啊,我现在明白了,谢谢。是啊,是啊,我的意思是同样的输出向右。你可以问,我是说,输出仅由一页组成,那么我们如何写出一个由四页组成的输出文件,对呀,答案是我们向右转,我们填满输出页面,一旦填好了,然后我们基本上加满了,然后我们基本上把它写到磁盘上,清空该文件。
以便我们可以写入该文件的后续部分,好的,Felix,嗯是的,所以在这个例子中,看起来我们基本上使用的是隐式的输入缓冲区,就像一页大,嗯,所以看起来这是合乎逻辑的,我们可以把这些缓冲区稍微大一点。那样似乎是这样吗,比如说,就像我们合并的情况一样,就像八页中的四页运行,如果我们有两个大小的输入缓冲区,就像呃,每页尺寸两页,那么同样的过程将在一周内适用,事实上,这是下一张幻灯片,好的,是啊,是啊。所以如果你看两页,假设在左边我们有两个三个四个十个,那就意味着我们首先,呃将输入,或者我们将在输出磁盘上写入2-3,然后我们比较四个十和四个七,所以我们会得到四个四个,这次,所以在那种情况下。两个缓冲区,所以缓冲区1只剩下6只缓冲区2只剩下7只,所以在这种情况下我们只需要翻半页不,我们只需继续,直到其中一页被消耗掉,所以你写出4和4,然后剩下的是,你说呢,六和十,是啊,是啊,哦,我是说。
十和七,是啊,是啊,十和七,好吧那么下一个要走的是七个,是的,所以写作设置完全完全,使用其他页面中的一个,就在其中一页,所以我们就把下一页拿进来,好的,所以回到这个计算,对所以嗯,从61 b开始。你们基本上了解到这和呃,呃,呃,就像任何精细的浸没排序一样,所以基本上是我们需要通过每一个这些的次数,就像你知道的,输入文件将是日志基,n中的两个,其中n是这个文件占用的块数。然后额外的一个将解释第一次传递,在那里我们对每个单独的块进行排序,所以总成本,在这种情况下,是这个数字的两到n倍,对呀,因为两个n是每个通行证的成本,因为这个问题是早些时候提出的对吧。所以这里有一个通用的算法,对呀,因为早些时候我说好吧,如果我们只有两个输入缓冲区,我们可以在输入文件中读取,那么我们一次只能合并两个块或两次运行,对我是说你看你看这里所有的箭头,基本上只有两个,对呀。
因为我们只受到两个人的限制,两个输入缓冲区,对呀,如果我们有更多的输入缓冲区,那么我们可以做的一件事就是同时合并更多的运行,对呀,事实上在上一张幻灯片上,如果我们有输入缓冲区。我们也可以把过去两年的所有文件合并在一起,然后我们就不用跑过三个了,对呀,但在这种情况下,因为我们只有两个,所以我们得再跑一次,这就是代价,如果我们有更多的内存,我们实际上可以经营更多的东西,所以呃。首先这基本上意味着,我们可以在第一次通过时创建更大大小的文件,就在上一张幻灯片上,我是说,好的,我们的输入缓冲区只有一个,所以我们将读取文件的一个块,一次一个然后分类然后就像你知道的那样直接到磁盘上。但是如果我们有更多的页面可以用来保存元组,显然我们不必分类,就像每一个方块一个接一个对吧,我们可以读取文件的十个块,然后在过去的一个中对它们进行排序,这也会减少我们需要通过的次数,然后另一方面。
如果我们有更多的页面,然后我们也可以合并更多的文件,对不起,同时也有更多的运行,对,这就是你在合并阶段看到的,所以一般的算法基本上就像我在这里描述的那样,因此。为了对具有n个页的文件进行排序并成为缓冲页,在第一次通过时,我们将使用我们能使用的所有页面,然后生成运行的,每一页都是对的,那么运行次数正好是n除以b,呃右边的天花板。因为嗯n可能不是一个可以被b整除的数,然后对于随后的通行证,我们基本上把b减去一,呃同时运行因为我们有B,呃缓冲区总数,然后最后的缓冲,为什么不是b而是b减去一,因为我们需要为输出保存一个缓冲区。这就是这张照片所显示的,那么一般的公式是什么呢,计算iOS的数量,合并排序,嗯,我声称这是这个数字,是这个公式,对吧,所以注意到一个不同,所以基本上我们把对数乘以b的次方减去1,因为我们不做。
就像你知道我们不是一次合并两个运行,我们实际上是在合并B-1同时运行,然后我们需要经历的次数是n除以b,呃,仅仅因为我们的运行次数,在过去数字零的最开始,你还记得在上一张幻灯片上,我们有n个。相对于n除以b右,因为我们假设每次我们只排序一个,过去零点期间的单页记录,所以总的io是2,然后乘以这个数字,好的,所以这部分没有变,所以只是为了给你一种喜欢的感觉,现实地说,这些数字应该是什么样子。假设我有五个缓冲页,我正在整理磁盘上108页的文件,所以第一次传球我们会产生两次两次跑动,每篇都有五页长,因为我们的缓冲池由五个页面组成,虽然像上一个,最后一次运行将是三页,因为一个零,八不是数字。可被三整除,抱歉五点,然后对于后续的传递,我们只需要继续进行这个合并过程,就在我们一次合并四次运行的地方,因为我们需要保存其中一个缓冲页或,呃,输出的一个缓冲页,然后你可以看到三次跑步,对不起。
四次传球,包括第一个,嗯,我们把这个文件整理好,然后如果你把它插入公式,这也检查以及通行证的数量,你有问题吗,是啊,是啊,所以我想把这个和双重缓冲结合起来,如果你有六个,假设你有六个缓冲页。如果要进行五路外部合并排序,你仍然需要等待大约五页才能从磁盘加载到内存中,所以是快还是慢,如果你喜欢,我猜像双缓冲双向合并排序,还是我们可以做好的事情,所以就木卫一而言,这不像改变,那改变不了什么。对呀,因为我们还在提,我们是,因为我们仍然一次一页地带来每一页,所以就IO成本而言,这是不改变的,但就现在的内存成本而言,现在我们需要更多的缓冲页,对呀,因为我们基本上有一个集结地。我们在他们真正使用之前就把页面带进来了,所以这是关键的区别,所以基本上这意味着我们不能用完所有的牛肉缓冲页面,就像你知道的那样做合并,对呀,因为我们需要拯救他们中的一些人,嗯。
在我们进行合并的同时进行双重缓冲,但在实践中,我想如果喜欢,这可能是一个更快的解决方案吗,是啊,是啊,所以这肯定比你快,正如你指出的,对呀,因为我们不必等待,伊奥,对呀,假设其中一个输入页。我们刚刚消耗了一切,所以如果我们不是在做双重缓冲,我们基本上停滞不前了,我们基本上需要拖延等待I O,在我们真正继续之前,但如果有双重缓冲,然后我们实际上可以,呃,我们不必等待。但我们在这里基本上是在交换内存,对呀,因为我们需要额外的一页,呃,双重缓冲威胁的生效,当我们做合并的时候,是啊,是啊,所以为了快速增加尼古拉斯,我想这也像你为这个保留了一些页面,或者这个的一些框架。你基本上也在减少并行性,好吧,你可以带来更多,您可以合并更多的运行,如果您对每次运行的代表使用这些缓冲区,对呀,所以这是艾伦提到的一个权衡,呃,嗯哼,嗯,所以这里有更典型的数字,对呀,所以我就像。
你知道的,用这个公式来计算我们需要做的iOS的数量,嗯,大家可以看到,用很少的运行对,所以在这种情况下,就像你知道的,合适的游泳池大小是257,我们基本上可以,十亿,呃,数据块,对呀,所以这很酷。只用四次传球,这就像B+树的讨论一样,我们基本上说对于for,因为b+树有一个很高的,非常高的扇形,因此,我们可以存储大量的元组,用一棵很浅的树,所以在这种情况下,我们不是在谈论。你知道这里的树或数据结构,对呀,但如果你看看合并,我几张幻灯片前展示的一张照片,基本上看起来你知道,就像一棵树对吧,除了它像时间里的一棵树,不是在太空中,因为我们讨论的是算法的不同阶段,但概念相似。因为我们能够融合很多不同的,嗯,同时进行很多不同的跑步,呃,有大量缓冲页的,所以我们不需要运行很多,为了把整件事整理好,经过了很多通行证,所以我觉得这很酷,因为这就像你知道的,空间和时间之间有一个类比。
好的,所以现在让我们再问一个问题,对呀,所以假设我不满足于像这样运行这些,你知道的,多次传球正确,我只想跑两圈,这意味着我获得了对每个单独页面进行排序的第一道关卡,或者我们一直称之为零。然后我只想像一次传球一样奔跑,所以没有后续的通行证,我不能那样做,所以让我们来计算一下,在这两个过程中,我们实际上可以排序多少数据,就像我说的对,所以我们基本上合并了。我们首先从磁盘中对单个页面进行排序,创建B页长的运行,然后在第一遍中,我们基本上合并b减去其中的一个,这基本上意味着我们可以排序的数据量的数量,就这两遍是b乘以b减去1,或者基本上接近b的平方,对呀。按排序的总数据计算,我们用B空间,对呀,B就像缓冲区里的页数,那么我为什么要做这个计算,因为我想知道这个比例是如何正确的,所以现在基本上意味着如果我们想对x个数据量进行排序。
那么我们需要x的平方根作为缓冲页,如果我们只想跑两趟,如果你想用完垫子,多次后续传递,那就继续吧,但如果我只想跑两次传球,我可以用x空间的平方根来做,那也很不错,因为我们不需要,你知道的,呃。我们实际上可以这样,呃,我们实际上可以整理很多东西,就用那个,就像你可以看到b和x的平方根是如何缩放的,谢谢,所以对于那些喜欢的人来说,你知道谁知道复杂性,你基本上可以看到,呃,它实际上是如何生长的。你有问题吗,这里是绿色,再解释一遍,我们如何得到x的平方根,是啊,是啊,就像你知道的,我们知道在使用B页时,右b,就缓冲池的大小而言,我们只需两次就可以对B平方量的数据进行排序,对呀。所以我只是在做反向计算,这意味着如果我们通常有x个数据量,我们想分两次做,那么我们只需要用x空间的平方根,在这里,我们假设两次传递将完全排序数据,准确地说,我只能做两遍,因为我想看看效果如何,好的。
谢谢。是啊,是啊,对我,你知道就像,你知道的,呃,比线性慢的x尺度的平方根,对然后x所以,因此,这实际上是一个相当不错的交易,关于现在开始还有什么问题吗,好的,如果没有,然后让我们继续哈希。所以这里的想法,就像我说的对,我们在努力做一些我们不一定总是需要排序的事情,我们就可以,比如说,尝试检测重复,或者我们试图通过查询在组中形成组,所以在这些情况下,我们实际上不需要真正的顺序,一切的权利。所以哈希正是这样做的,因为它基本上,呃,放进去,呃,具有相同哈希值的记录,在同一个桶里,所以这足以让我们基本上成群结队,或在重复照明下,这个问题,然而,对呀,就像之前一样,我们如何做到这一点。这意味着我们有有限数量的缓冲页,但是我们有一个很大的文件,我们正试图哈希,现在让我们用下面的方法来做这件事,所以我们首先要做一个除法,我们将使用哈希函数从磁盘流式传输记录。
然后把它分成不同的块我们写出来,所以现在所有具有相同哈希值的东西都将被放入同一个磁盘,分区,嗯,每个分区将有一个混合,呃,不同的价值观对吧,然后就像之前一样对吧,如果呃,如果输出缓冲区页的空间不足。然后我们就把它写出来,呃到磁盘,所以这里有一个图片描述,所以我们要再来一次对吧,所以我们在磁盘上有输入关系,嗯,在不同的页面,我们将阅读在每一页,一次一个,然后调用我们自己选择的哈希函数。然后把它们分成不同的桶,因为我们总共有B页,其中一个正在被输入缓冲区消耗,我们可以创造B减去一个不同的桶,然后如果这些桶中的一个或任何一个装满了客人,然后我们就把它们写回磁盘。就像以前一样到目前为止一切都好,所以这里有一个例子,假设我有一个由元组一组成的输入页,二和三,我应用一个哈希函数,将第一个元组放在第一页,第二页第二页,但是因为哈希值可以正确地碰撞。
所以我们最终可能会把第三个记录放在第一个请愿书中,因为它实际上也可能具有与第一个相同的哈希值,所以我们把它们写到磁盘上,然后我们就往右走,我们现在在文件的第二页读到。所以让我们假设第二页由记录五和一组成,所以我们再次应用相同的哈希函数,它可能会把第五个,的,其中有五个的元组在第三个分区中,然后它会把记录的另一份副本,一个权利在第一个分区到目前为止,一切都很好。除了它违反了我们的限制,您知道具有相同价值的记录,具有相同哈希值的右应该在磁盘上连续背诵右,但现在既然我们一页一页地做这件事,我们最终把三张放在一的两张唱片之间,这都是因为事实。我们不能同时看到整个文件,我们一次只能看到一个街区,所以谁知道我们会遇到更多的还是更多的,你知道两个和五个之后,呃,在只看到其中一个磁盘页后,所以这就是问题所在对吧,我是说如果不是,然后我们就读进去。
我们会把整个文件读入主存,创建桶,然后我们就都做对了,但在这种情况下,我们负担不起那样做,这就是为什么我们有这个问题,这告诉我们什么,这基本上告诉我们,我们没有完成后,就像,你知道的,呃,哈希第一个。第一页右边,我们还没有完成后,只是哈希每一页,个别地,我们需要重复一下,或者换句话说,我们需要读对,我们以前散列过的每个分区,然后尝试一下,再试着把它们散列一下,相同过程权,我们将应用另一个哈希函数。希望能把东西放进不同的桶里,就像以前一样对吧,然后同样地,我们将读出内存中的哈希表,然后将它们写入磁盘,所以现在,我们可以确保重复的值现在在磁盘上是连续的,所以我们从第一阶段开始,对呀。我们怎么有这些不同的桶,我用这个,这些呃,红色长方形,然后我要读这些请愿书中的每一份,一次一个,这里有一个很大的警告,对呀,我只是假设,每个分区的大小都小于B,这意味着整个分区实际上可以容纳在主存中。
为什么,因为如果我们能做到这一点,然后我们现在可以创建像,使用我们最喜欢的哈希哈希算法,因为我们现在把所有东西都放在主存里了,我们现在可以运行我们典型的哈希构建哈希表构建算法。然后用这种方式将桶写到磁盘上,从我们之前的例子开始,所以从第一阶段开始,我们最终收到了三份请愿书,第一个有一个,三加一,和,另外两份请愿书有两份和五份,分别是,所以对于第二阶段或重复阶段。我们要把这些请愿书一次一个地拿进来,然后构建我们自己的内存哈希表,所以现在使用不同的哈希函数,我们希望能分开就像你知道的1和3,然后从现在开始他们是,他们是我们做了,然后我们这样输出桶。我们确信我们不会出现我们以前见过的问题,因为我们已经解决了1和3的所有记录,第一阶段之后对吧,第一阶段会把所有的记录,和所有的记录三个到同一个分区,所以这就是为什么我们可以负担得起第二阶段的重复。
我们下一个就完成了,你有问题吗,是啊,是啊,所以我想你回答了这个,但我们之所以确定是因为我们在主存中做了第二个,我得到了,是啊,是啊,还有一个很大的警告,对就像,你知道的。我们假设轮询分区实际上适合B页,我们拥有的,到目前为止,这是一个两面派,呃,两相算法对,我们只是把它捣碎,单独写请愿书,然后像你知道的那样重复一次,然后我们就完成了,沃伦,你还有一个问题,是啊,是啊。呃,有没有可能我们不得不太多的一个,这样的,它并不都适合一个分区,是啊,是啊,所以这就是为什么我一直这么说,就像,你知道的,我们假设每一份请愿书实际上都适合B页,是啊,是啊。你会看到如果我们没有那种情况会发生什么,呃再次,我们也会问接下来会发生什么,如果不合身,是啊,是啊,好的太好了,这些都是很好的问题,就拿着那个,但我只是想确保每个人都明白,到目前为止我们一直在谈论。
适用于以下情况,如果每个请愿书的大小小于B页,任何其他问题,好的,所以在我继续谈论之前,如果失败了会发生什么,让我们首先谈谈这个版本的成本,呃,哈希算法,所以其实很简单,对呀,所以就像,你知道。两次又一次,对我们所做的每一次传球,因为每通过一次,我们基本上读所有的元组,我们也写出所有的元组,所以就IO成本而言,就这样结束了,然后因为我们只运行两次,所以总成本是n,现在让我们再问一遍同样的问题。那么我们实际上可以使用多少数据,只有这两道,我是说就像我们问一个问题来排序一样,所以让我们再做一遍同样的练习,对了,第一次传球,我们可以创建B减去一个分区,最大的权利,因为就像你知道的。我们需要保存其中一个缓冲页以便在输入元组中读取,然后就像你知道的,每一个都假设这些分区中的每一个都小于,b页数,我一直在,呃,你知道吗,有点挥舞着我的手,然后说好,你知道吗,我们就假设是这样吧。
那么我们可以使用哈希的数据总量,就像你知道的这两步是b的平方,b减去一份请愿书,然后由最多B页组成的每个分区中的每个,所以b减去1次,我们就叫它b平方吧,然后有趣的是,同样的论点再次出现,对呀。所以我们现在有一个论点,我们有像你的B空间,主存中的页数,然后我们可以根据数据量对b平方进行哈希,所以翻转它,反过来只是另一种说法,如果我们想哈希呃,x数据量,然后我们还需要x的平方根,以页数为单位。如此正确,就像你知道的,第一个警告是,这些请愿书中的每一份都只有B页,最大值,然后我们也假设,另一种说法是,就像你知道的那样,我们假设哈希函数,也把唱片均匀地分布在右边,所以现在让我们问正确的问题。所以如果我们有一张更大的桌子会发生什么,有人知道我们能做什么吗,那样的话,所以现在,我们说的是这样一种情况,你在这里看到的任何一个分区,在屏幕中间,如果其中一个比B大。
那么我们就不能在第二阶段对该分区的所有内容进行哈希,对呀,那么我们能做什么呢,我会递归,直到我们能把它放入内存,完全正确,我们可以向右递归,那是我们的朋友,对呀,这就是我要说的。就像递归分区一样给你一个,给大家一个提示,所有的权利,我们擅长什么,所以我们再次运行相同的第一阶段,对呀,我们只是把它们分成不同的分区,我是说这就像是,这只是像往常一样,除了我们有一个更大的请愿书。它占据了比像,你知道的,b页长,那么我们做得好的是什么呢,我们何不再做一次除法,就在那个分区的右边,所有其他分区,就那件事而言,它不会影响任何其他分区,对呀,但我们可以再做同样的事情。希望在运行第二阶段后,就在跑完分相之后,我们就能把它放进每个窃听器里,每个分区的大小小于B页,因此我们将能够运行,同意或以其他方式称为重复阶段,构建内存中哈希表的权限,这对人们来说有意义吗,现在。
正如你所看到的,我把它称为一个单独的哈希函数,因为如果你再次使用相同的哈希函数,那对我们没有任何好处,对呀,因为这会,基本上只是把这个大请愿书中的所有元组再次放入同一个分区中,对呀,那不会有帮助的。所以说,我们肯定会用一个单独的,后续传递的不同哈希函数,我可能有,呃,我是说你可能已经报道过了,但是哈希函数是单向函数,那你怎么把它找回来,当你需要把它拿出来的时候,你打算怎么办,否。我们不需要把它拿出来,对呀,我们基本上只是使用哈希函数将元组划分为不同的分区,所以我要说的是,就像其他哈希函数一样,只是基本上使用一种不同的机制来分发记录,记录,这样它们就不会都在同一个分区中结束。好的,是啊,是啊,我们没有任何花哨的,你知道,这里的哈希函数的反向或,我是说那会很好,但不不,我只是说,基本上,好的,我们有一个巨大的隔板需要处理,所以说,只需使用一个单独的哈希函数希望能将新的,呃。
哈希函数将分发记录,与第一个哈希函数不同,这样我们就不会得到一个大于b大小的分区,哦,边缘的情况怎么样,记录都是一样的,因此,即使您使用不同的哈希,好好想想,好的还有其他问题吗。所以你已经可以看到类似于,呃,带有排序权,这就是为什么我们一直在谈论,喜欢有喜欢,你知道排序和散列,就像,你知道,就像在排序,我们基本上是做这种多阶段的合并,在这种情况下,我们基本上是在做多阶段除法。我觉得这很酷,对吧,因为这有点像排序和哈希之间的类比,即使他们完成了一些完全不同的事情,嗯好吧,所以一个人在问一个问题,重复的呢,对呀,那么我们在这里谈论什么,假设我有一个具有非常频繁键值的数据集。对一个典型的例子是,比如说性别,对呀,所以我们可能只有三个值,你知道男性,在这种情况下会发生的是我们最终会有很多赛车手,对呀,也许就像前两份请愿书一样,也许现在。
即使我们试图通过使用单独的哈希函数来进行递归分区,这对我们没有帮助,因为不管我们用什么哈希函数,因为你因为我们在讨论性别,我们只是要把相同的记录再次放在相同的分区中,对,这并不重要,我们做了多少次,呃。递归分区,那么这里的解决方案是什么,我们可以做的一件事就是做一个单独的检查,对于这些重复的值,如果是这样的话,那我们就不用这个了,就像,你知道的,呃,递归分区算法,对呀。因为如果他们认识到一切都是相同的价值,它们都哈希到同一个哈希表,那么我们实际上根本不需要运行递归,我们已经做了,我们已经让所有的男性和女性在各自的请愿书中,以及所有其他人,所以我们是。我们已经完成了我们开始做的事情,我们不需要运行递归,我们可以停止递归,我们唯一需要做的就是认识到,这确实是时候停下来了,因为如果没有,我们只是停止螺丝对,因为我们会,我们一直在不同的地方运行,你知道。
定位算法,那很糟糕,罗曼,你还有别的问题吗?还是之前的,好的,我想他是刚才的问题,所以人们有什么问题吗,我真的很喜欢你们问的这些问题,但看起来他们都来自,就像有选择性的学生,所以我有点感觉。其他人可能根本没有得到它,所以有人能问一个问题吗,如果你感到困惑,我我有个问题,是呀,我很难理解,像这样做的全部意义是什么,哈希就像呃,比如说,就像呃,早些时候,有一个一三一的例子。所以我们是在努力适应一切,哈希到相同的东西到相同的喜欢的页面,是不是这个想法,是啊,是啊,整个原因是因为我们试图实现,例如在SQL中,是啊,是啊,所以我们需要调用重复的值,对呀。所以递归的基本情况是页面中的所有内容,呃是相同的值对吧,是啊,是啊,基本情况,好的,对呀,所以当我们到了那个地步,我们完成了,因为我们可以判断是否有重复,我看到我看到酷,是啊,是啊,任何其他问题。
我有个问题,是呀,所以就像,嗯,这真的有任何意义喜欢有点像以前,因为我们不知道,如果我们的数据集包含的非常频繁或不正确,所以喜欢什么会有意义,就像,运行一些额外的,像开销算法来计算。就像如果我们有一个频率,这会有帮助吗,嗯,你的意思是,就像有办法检测到,比如频繁值,或者呃,是啊,是啊,完全正确是的,所以这绝对是需要的,就像在这种情况下,即使像这样简单,你知道这个性别案件,对呀。所以我们肯定需要那个,所以通常的做法是,在这些划分阶段中的一个完成后,基本上检查这些分区中的每一个,嗯,如果所有的请愿书都在B页内,那我们就好了,嗯嗯,但后来就像,你知道的,我们还需要检查一下。这是否是一些请愿书可能大于B的情况,然后我们可能需要做递归,如果一切都不一样,还是一切都已经像,你知道,放进各自的桶里,那我们也做对了,就像,你知道,不管怎样,我们都可以检查一下,这样就有道理了,谢谢。
是啊,是啊,是啊,是啊,好的,所以让我们来谈谈这个类比吧,所以我一直在说,就像哈希和排序之间的类比,所以这里有另一种思考的方式,所以我之所以称之为分而治之,因为在哈希右中,所以你可以看到,在第一阶段。我们试图分成不同的分区,然后就像你知道的在第二阶段我们试着把它们合并起来,在创建内存哈希表方面,然后将其写入磁盘,所以这有点像分而治之,对吧,然后这里的成本是NIOS的,对吧,只是为了最初的阅读。和最后的权利,然后对于排序来说,这只是一个翻转,如果你记得排序,我们首先阅读每一页,然后尽可能地排序,创造这些运行,我们随后合并,也就是说我们只是在翻转,我们首先读到的两件事是大块的东西,对它做些什么。然后将它们写入磁盘,在第二阶段,我们只是合并,就像,你知道的,这些运行的多个一起,正确的排序,对战,在哈希的情况下,基本上是相反的,所以这也很整洁,对,因为在这种情况下,有两个阶段,呃,分拣。
我们也得到了相同的IO成本,所以这又是,我谈论这个案子的另一个原因是,假设我们只限于做两个阶段,对呀,两张通行证,会发生什么,事实证明,如果我们只做两遍,2。他们俩的费用是一样的,所以这也很整洁。我以为,只是为了把肛门带回来,好的,所以现在让我们谈谈试图让事情瘫痪,所以嗯嗯,我们已经讨论过双重缓冲,但事实证明,我们甚至可以让事情变得更瘫痪,所以说,在这种情况下,我们有大量的唱片。就像这些不同的光盘,我给你看这些红色的罐子,右边左边这里,所以假设我想做的是哈希,所以有一件事我们可以做对,在这种情况下是首先应用一些哈希函数来喜欢,你知道吗,在不同的机器上分发数据,是呀,是呀。在此之前,卢卡斯有一个问题,是呀,请继续,在这种情况下,我们如何定义机器,是呀,是呀,所以在这种情况下,我只是像一个服务器一样调用机器,好的,谢谢。是呀,是呀,所以我们想并行地做这个散列。
所以你可以注意到现在我们有多个服务器,对呀,每个服务器都有自己的,有自己的主存,那么我们能并行运行这个吗,如果我们实际上有多个服务器,事实证明,我们可以纠正我们这样做的方式,正如我所说的。我们首先尝试从磁盘上分发元组,对呀,从三个各自的磁盘到我们拥有的三个各自的机器,我们使用哈希函数来做到这一点,然后我们就运行我们的好旧,呃,每个单独磁盘上的外部散列算法,或者我们能负担得起的每一台机器。因为,第一个哈希函数,已经在这三台机器上分发了元组,对呀,所以所有键值为1的记录都将驻留在其中一台机器上,就说第一个吧,我们不需要担心,其他一些机器也可能有同一密钥的另一个记录。因为第一个哈希函数已经这样为我们分配了工作,所以现在基本上这意味着这三台机器现在可以,um流,就像,你知道吗,就像我们之前说过的,然后我们可以平行地穿过,同时,这有道理吗。
所以现在我们基本上可以运行三个版本的三个副本,呃,同时哈希算法,和,我们,不需要担心,任何人撞到别人的桶里,因为这里的第一个哈希函数,n的h已经为我们分发了数据,所以呃,魏晓在问,这是地图缩减方案吗。你可以把这看作是地图缩小,事实上是对的,所以你可以想到第一个分布,呃相位作为地图相位,可以这么说,然后接下来的整个,你知道的,呃,散列外部散列在单个机器上有点像,我会说另一张地图对吧。因为我们并没有把它们结合起来,所以你可以想象还有另一种还原阶段,我们只是喜欢,你知道的,将所有数据复制到一个文件中,所有的桶都排成一个文件,变成一个单独的磁盘,如果我们想,但在这种情况下,我们不需要对。因为我们只是在做散列,所以如果他们依靠三个不同的任务,也许那很好,是啊,是啊,这是一张地图,地图方案,那么排序正确呢,所以我们讨论了,排序和哈希之间有所有这些类比。
所以这意味着也必须有一种方法来并发地进行排序,或者平行,对呀,你可以,我们可以做同样的事情,所以我们可以试着,呃,整理不同机器上的数据,嗯,但现在有一个问题,所以在哈希阶段,没关系,就像,你知道的,呃。但是我们如何实际分区数据,只要我们是一致的,并将所有带有密钥1的记录发送到同一台机器,对呀,那很好,但在这种情况下,既然我们排序正确,其实很重要的是,呃,你知道哪台机器实际上得到了什么,呃,什么元组。因为我们试图在最后产生一个文件,对呀,因此,我们并不试图产生驻留在不同磁盘中的多个桶,一种方法是不使用哈希函数,但实际上使用所谓的范围分区函数,所以在这种情况下,我们可以任意决定,就像你知道的。10以下的所有括号都将驻留在第一台机器上,和所有中等范围的记录,在11到100之间的人将辞职第二个,然后其他一切都发生在第三个,好吧,如果我们能做到这一点。
然后我们可以在个人身上运行我们良好的旧外部排序算法,呃机器然后继续走那条路,这里有一个问题,就像我在幻灯片上说的那样,对呀,有一个,我们有机会得到倾斜的技能,在某种意义上,让我们说像,你知道的。结果只有很少的记录在11到100之间,很多唱片实际上就像,假设不到十个,所以在这种情况下,那么第一台机器实际上会得到大部分记录,随后的大部分工作在排序权,与其他两台空闲的机器相比,嗯,这是个问题对吧。你可以想象,所以在这个方案中,我是我们唯一的承诺,你就像一种平行运行事物的方式,但我们不承诺任何关于,就像工作的平均分配一样,如果事实证明我们在这里的这个范围划分,是个好计划,然后就像,你知道的。三台机器的工作量都是一样的,希望在排序方面,那是理想的情况,但不能保证这将是理想的情况,对呀,但至少这给了我们一个逃跑的方法,跨多台机器并行排序,这有道理吗,所以有人能想想,你知道的。
我们如何才能真正避免数据技能,呃,所以我们实际上得到了最多的,这样我们就得到了并行运行的好处,也不需要担心,呃,工作分配的这个特殊问题,似乎您可以只使用哈希并假设它足够随机,然后用mod输出。不管有多少,呃,你想要的桶,或者你有多少服务器,我想是的,我们真的能做到吗,我是说如果像哈希函数,实际上分发像,你知道,嗯,第一台机器上有一个五和十,但你知道,2个6和9到第二台机器,然后我们运气不好。是啊,是啊,对呀,我是说,其他人对此有什么想法吗,我是说,我们先计算一下,我们需要存储的数据的直方图,我们什么都没开始,我们在计算直方图,对呀,如果我们有分布,所以让我们假设你知道。我们在这里做一个直方图,就像你典型的高斯,对呀,所以如果我们最终得到这样的东西,假设这些是,就像,你知道吗,X或我们试图开始的东西,然后我们可以试着基本上开始,就像,你知道吗,以一种方式分割。
这样每台机器都能均匀地分配工作,对呀,所以假设这就像,然后这里的x轴可以是这样的,你知道,从十岁到二十岁,三十,四五十,对呀,如果我们看到这样的分布,然后假设我们有三台机器。那么也许划分工作的一种方法是基本上把它更改为,像这样的东西对吧,所以也许这是一号机器,然后中间的桶进入2号机器,或者中档去2号机,然后其他的都进入三号机器,对呀,例如,所以计算直方图相对容易,呃。计算比较,所以说,让我们说,就像你在运行排序,所以在我们真正做任何工作之前,我们可以负担得起,然后在我们看到分布实际上是什么样子之后,然后我们就可以用这种方式划分工作了。通过以更均匀分布的方式基本上划分范围,所以这是一种方法,还有其他的方法,呃,我们可以下课再谈,到目前为止,如果你对此有什么问题吗?很整洁,对吧,所以我们得到了两个最好的工作,这样我们就可以并行运行。
也得到了不需要处理数据的好处,好的,所以总而言之,我们在这节课中讲的,对呀,排序和哈希之间的二重性,无论是神圣的脸还是征服的脸,也是在成本方面,对和呃,我们也谈到有时,呃,你实际上不需要做完整的排序。例如,如果我们只是想做,呃,重复照明,对呀,但是如果查询实际上要求排序,那么这将是一场胜利,对呀,因为我们需要把事情整理好,嗯,所以别忘了我们谈过的所有这些不同的事情,就主题而言,对呀,流媒体呃。对事情的模式,我们如何阅读,我们如何将单个元组读入输入缓冲区,然后把它们写出来,因为它们被填满了,以及对高延迟的双重缓冲的想法,嗯,现在你可能有一个问题,对吧,那么我们为什么要谈论这些外部核心外的算法。对呀,我是说,这些天我们不是已经有太字节的内存了吗,嗯,答案有点对,我是说至少我没有把它放在笔记本电脑上,对呀,所以你可能把它放在你的服务器上,对你有好处,如果是这样的话,如果没有。
那我们还活在这个世界上对吧,我们有大量的数据,但就服务器上的内存量而言相对较少,所以这仍然是一个问题,事实上,你知道对吧,实际上记忆是有层次结构的,又不是说,只有一种记忆,然后然后磁盘对。我们已经在谈论这个了,当我们谈论设备时,对呀,所以我们可以有多个层次的内存,每一种都有不同的成本,也不同,时机对了,所以在这种情况下,我们仍然生活在一个我们试图把东西带进来的世界里,变成更快的内存。变成更便宜的内存,但代价是一开始就没有那么多页,因此,我认为这些算法即使在今天也仍然相关,顺便说一句,它们最初是在我们谈论,你知道的,呃,有一兆字节的RAM,对呀,所以这就像,你知道吗,三十年前。如果你这么想的话,但不知何故,即使在今天,它们仍然相关,即使我们只是扩大我们拥有的内存和数据量,当然人们永远不会满足于,就像,你知道只有启动兆字节的数据,就在他们可以分类兆字节之后。
然后要求排序千兆字节,在他们可以存储千兆字节之后,现在开始对TB进行排序,出口自行车,只是永远不会有正确的结局,所以说,因此,这些创新将伴随着我们,这就是我想说的,和罗马,我相信你有个问题,是啊,是啊。当我跟上的时候,所以这有点像几张幻灯片之前的样子,但我在想怎么会,如果在多台机器之间分配工作负载,什么是排序,最初的数据是什么,离得太远了,你知道你会得到1和10去第一台机器。然后你可能每秒得到五百和一百,然后你知道两三百到第三个,所以排序会很远,就像我错过了其中的一部分,你在那里谈到这口井,所以你可以试着这样做,你知道的,有办法确保数据是均匀分布的,即使他们最初插入它。但你不能总是保证情况是对的,假设你是亚马逊,我是说,你不能强迫人们去另一台机器购物,对,因为如果你住在,就像我们一样对吧,你可能不想去欧洲或亚洲的服务器,购买,因此,数据可能已经自然地以熟练的格式出现。
已经,所以我们,我们可以把它分类到像一棵树一样的地方,有点像深树,几乎要不同了,是啊,是啊,不同不同的面孔,但在这种情况下,我们得跑不同的路,对对,就像当你试图查三个,它可能在第一个,也可能在第二个。因为或者在第三个,因为范围太高了,所以这也违背了整件事的目的,它让它,所以你是在问,就像,如果我们有一个巨大的文件,然后在一切都整理好之后,你怎么能真正查找,您实际上试图正确查看的键的值。所以你在多个通道之间分配它,可以这么说,是啊,是啊,从多个驱动器之间,但你在每个驱动器上的范围基本相同,一开始有什么意义呢,所以通常会发生什么来正确排序,就像一切都整理好之后,然后会保留一些元数据,对。这就像,你知道哪个磁盘有什么数据,钥匙的范围是多少,它有所以基本上说第一张光盘有,你知道从零到一千的所有键,然后第二张光盘有从一千零一张到喜欢的所有东西,你知道两个性感的,两千什么的。
所以当你在这种情况下进行查找时,然后你可以点赞,你知道要经过哪个磁盘或步骤或其他方法,那就是喜欢你说的对,构建一个跨显著磁盘的B+树,所以这也是另一种方法,好的。
任何其他问题,如果没有,当我停止分享我的屏幕时,我要切换到Aditya,但如果你有问题,拜托了,拜托了,呃,请随时询问。
好的,好的,好的,所有的权利,所以嗯,这仍然是一个话题,呃,阿尔文之前展示的图片的一层,所以我们还在,查询处理与优化,但现在我们开始有点像我们谈到的核心算法,我被描述为排序和哈希的核心算法。现在我们要回顾一下操作员,我们在几节课前描述过的运算符,并了解如何在关系数据库系统中物理实现它们,这就是我们今天的重点,嗯,所以我们将讨论迭代器接口,然后谈论呃,各种运算符的物理实现排序。我们将从阿尔文描述的核心碳中吸取教训,所以我们将从,确定排序和哈希,好的,所以让我们潜入呃,这个,所以我想再次回顾一下,这张幻灯片,所以这张幻灯片讲述了正在发生的事情的大局,尝试执行SQL查询时,对呀。所以你有一个查询是在预备队和水手之间进行连接,你得到呃,它翻译成一个关系代数表达式,现在你知道西格玛和圆周率了,这个BOI的意思是一个关节,呃,它被
我们讨论过的关系代数的一种变体,呃,在上一堂课里,然后今天我们要稍微关注一下这个,所以我们将讨论物理查询计划,其中,对于每个运算符,您都有该运算符的实现,呃,这将决定操作员实际上在做什么,在执行查询时。好的,所以这个物理查询计划中的每一个运算符都有相应的实例化,它在做一些活动,我们将讨论这个活动是什么样子的,好的,因此,简要回顾一下关系运算符和查询计划,所以呃,假设你有一个,呃,呃。像这样的关系代数表达式,所以这是连接,呃,三张桌子,和和各种各样,呃,选择和预测,等等,这个特定的查询可以使用如下所示的表达式树来表示,和查询计划,因此,执行查询的计划通常表示为,呃,树状结构。那么在这个树状结构中,边缘意味着什么,它们编码元组流,所以你可以想象元组起源于叶子,它们在查询计划中流动,这里的顶点是关系代数算子对吧,所以基本上这棵树中的每个节点都是关系代数算子。
源顶点基本上是访问表的权利,所以访问表,在某种程度上,它可以顺序扫描表格,或者它可以使用索引访问表,比如说,因此在许多情况下也被称为数据流图,原因显而易见,因为它描绘了这里的数据流,数据将对应于元组。因此,查询计划的数据流图的概念,如果您愿意,则不是特定于数据库系统的,在其他NoSQL或大数据系统中也是如此,以及机器学习系统和深度学习系统,对呀,所以说,呃,呃,所以说,比如说,张量流可能有,呃,呃。如下所示的数据流图,好的,如果你听说过张量流,比如说,那么数据库系统的各个组件的作用是什么呢?嗯查询优化器,它的目标是,它的作用是选择要按顺序运行的运算符,所以它基本上会说,好的,查询计划是什么样子的。好的,查询执行器将通过创建这些运算符的实例来运行这些运算符,我们谈论,呃,这些是什么例子,嗯,所以,比如说,每一个呃,此查询计划中的顶点为,uh是由查询执行器实例化的,实现一个称为迭代器接口的特定接口。
它被称为迭代器,因为它遍历元组,好的,所以这些,呃,这些运算符实例基本上执行运算符逻辑,比如说,如果它是一个选择,最终扔掉一些元组,如果是投影,可能最后会扔掉一些柱子等等。然后它将把这些元组转发到查询计划中的下一个运算符,对呀,所以调用它的操作员,所以查询计划中较高的运算符,因此父运算符,嗯,给我一秒钟,所有的权利,所以在这个特殊的情况下,大家可以看到,有一些例子,呃。呃,对于这些呃,算子,所以这是一个在飞行中的项目操作员,这是另一个在飞行的项目运营商,这是一个连接,呃实例,它是一个索引嵌套循环连接,我们一会儿再谈这个,底部的这些是索引扫描。一个与你如何从水手那里取回元组相对应的交易者,好的,所以这两个迭代器中的一些是在动态迭代器上指定的,我们稍后会讨论这意味着什么,所以这些关系运算符正在实现。
呃,基本上是作为这个叫做迭代器的类的子类实现的,所有这些都是,这是一个抽象类,他们都必须,呃,有点,呃,支持以下方法,好的,所以第一个方法是设置方法,基本上是配置,呃,输入,呃到,呃给接线员,好的。所以这将是,例如,对于运算符,它将是查询计划中的子运算符,好的,所以这是呃,一种简单的设置函数,基本上说谁是,呃,我在消耗谁的输出,呃,作为我的投入,好的,运算符实现中更重要的部分来自其余三个函数。第一个是呃,初始化在里面,然后呃,这是被调用的,呃,当操作符被实例化时,然后你有,嗯,所以通常这最终会建立一些状态,呃,所以一些初始化,有的在有的,所以你可以设置一些变量,然后呃。在您之前将它们设置为某些起始值,呃,开始处理,处理是通过下一个函数触发的,下一个函数返回一个元组,所以元组是这个函数的返回参数,这个函数是为给定运算符反复调用的,它由父运算符调用。
这就是要求更多的子运算符元组,所以下一个函数是,基本上,它的目标是从该运算符生成一个输出元组,并通常提供给父母,也就是叫它,好的,然后关闭函数,基本上就是说,好的,我处理完了。我现在可以去掉这个运算符实例,那么这个接口的特殊之处在于,迭代器模型是基于推送的,基于拉力的计算模型,好的,因此,您首先实例化这个查询计划的根,查询计划说,给我更多的东西给它的孩子和孩子们。反过来会要求,给我更多的东西从它的孩子等等,等等,直到从基关系中提取元组,一直到查询计划的根,然后产生输出,然后由用户使用,这可能是,比如说,在仪表板中,报告或控制台中,或者你有什么,好的,例如。控制台可以,呃,在查询计划的根运算符上调用它,然后嗯,随后它将调用下一个,所以基本上说,给我下一个啊,下一个元组,给我下一个元组,给我下一个元组,基本上要求越来越多的元组,一次一个元组。
如果这个元组还没有立即准备好,呃,下一个请求通常向下传播,呃,递归地查询计划,呃,直到它开始从基关系中读取数据,所以这些,呃,所以当你有一个在里面或下一个电话,这可能会导致所谓的动态算法。我们看到了一些例子,在更详细的查询计划图中,我有那个操作员的阻塞算法,所以动态算法是流算法,基本上每个电话做一点工作,下一个的上,好的,所以通常,呃,他们可能会以某种方式处理元组,然后产生一个输出。所以他们每次打电话不做很多工作,另一方面,阻塞算法不产生任何输出,直到它从它的孩子那里消耗了所有的输入,好吧嗯,所以这是,呃,这样,顾名思义,阻塞运算符,基本上说,在我处理完下面的东西之前。我不能产生任何输出,流媒体,呃,算子,另一方面,不断向父运算符提供结果,并从子运算符流式传输元组,好的,这就是流运营商或流算法的区别,和一个阻塞算法,相应地,流运算符和阻塞运算符。
你能想到我们讨论过的运算符关系运算符的例子吗,这将是流和阻塞,可以选择一个阻塞的例子,Select实际上是流媒体的一个例子,因为您不必这样做,你不必等到整个关系都给了你,让你产生一个输出。所以这就是流媒体和阻塞的真正区别,对呀,所以说,嗯,您只需要读取足够的元组就可以进入下一个元组,满足谓词的,然后你可以产生正确的,所以你不需要等到你看到了一切,所以任何阻止运算符的示例。什么加入费用阻塞,那是另一个问题,对呀,您真的需要查看您的全部输入吗,因此,参与连接的两个关系,呃,生成输出元组,不太对,如果你能,呃,在一个关系中找到一个元组,并想放入另一个连接的关系。你可以产生一个输出,但这实际上取决于我们用来连接的算法,它是,比那个内特更棘手一点,好像我们会有一个精选的,选井投影,都会流媒体,订购,会挡住,嗯连接,你刚才说可能是,是啊,是啊,所以嗯,绝对如此秩序。
所以选择,投影都是流运算符的例子,嗯,Order by或Group by都是阻塞运算符的示例,好吧,如果你在做排序,嗯,然后你要等到,在您可以产生任何类似的结果之前,对结果进行排序,如果你在做一个小组。不能返回组的部分结果,您需要等到查看了该组的所有元组,这样您就可以正确地计算聚合,然后为,呃,请求它的父运算符,好的,我我看到呃,撒迪厄斯,提到,呃计数和最大值,呃,当流媒体这些,是啊,是啊。那些绝对挡住了,下一个请求向下传播意味着什么,如果元组还没有准备好,所以你会看到,呃,并举例说明,基本上,父运算符会说我现在应该产生一个输出,让我,以便产生一个输出,如果父已经有,所以想象一下。如果父级是一个组,我是对的,所以通常它会等到所有的输入都产生了,然后它会做一些计算,然后它可以为,父母很快就打电话给它,对呀,另一方面,如果是选择,那么呃,它会产生,如果它没有准备好元组。
它会要求孩子生成一个元组,然后检查谓词,如果不匹配,我们将从孩子那里查看更多的元组,直到它找到一个匹配的,所以保持这种想法,我想我们会的,呃,通过几个例子,希望能澄清,考虑到现在是六点半。也许我不该继续,呃,但是嗯,我会给你,我将通过几个例子来说明,呃,各种关系运算符的排序,以及迭代器接口如何查找它们,或者更确切地说,眼睛器实现是如何寻找它们的,我有什么问题吗。