跳转到主要内容
Dan 提交于 20 November 2012

你怎样在一本烹饪书中找到一个菜谱?当然是查找目录。按字母排序的菜谱固然便于查找,可是你会去记那个用西班牙香肠和牛油果做的美味沙拉的名字吗?即便你会,烹饪书也一般都是按照某些其他的主题,如季节或场合来排序的,所以就算是记住菜谱的确切名字,也不会有多大帮助。因此,让我们来建立一个基于原料的索引,它包含原料所出现位置的页码,如下:

牛油果 40, 60, 233 西班牙香肠 50, 60, 155

这样有点帮助了,但是要找到任何东西你都需要把每个列出的菜谱过一遍,因为没有任何信息告诉你哪些页面上有些什么。让我们来创建一个先按原料再按名字的字母顺序索引,如下:

牛油果 牛油果酱 40 玉米饼汤 233 暖风西班牙香肠凯撒色拉 60 西班牙香肠 黑豆西班牙香肠卷 50 墨西哥风味炒鸡蛋 155 暖风西班牙香肠凯撒色拉 60

这个索引虽然有用,但它只适用于当你在查找原料或原料和名字的情况。当你要按菜谱名字列出包含某种原料的菜谱时,它也有用。然而,如果你要查找一个名字,它就变得毫无用处了。比方说,如果你要找到墨西哥风味炒鸡蛋,你需要先浏览到牛油果,再是培根菜谱,再到西班牙香肠——比把整个书翻一遍好不了多少。

记住,你一开始是浏览你的烹饪书来查找一道既包含牛油果和又包含西班牙香肠的菜谱的,你要怎样建立一个让这种查找简单而快速的索引呢?如果你以菜谱名字开始索引,那么你需要浏览每个单独的菜谱才能找到它们,那不是一个好主意。如果你像你的第一个索引那样做,你可以轻松找到所有包含牛油果和所有包含西班牙香肠的菜谱。假设你有1000道牛油果菜谱和1000道西班牙香肠菜谱,但只有一道菜是两者均有的,为了找到单独的一个你需要比较2000个数字。要让这种操作变快,你其实需要一个类似于下面的索引:

牛油果 培根 30, 37, 48 西班牙香肠 60 培根 牛油果 30, 37, 48 西班牙香肠 70

那简直太可怕了。它不仅仅是存储需求会非常高,更重要的是,在现实生活中,维护类似这样的数据很快就会变得不切实际。就算一道菜谱平均不过8种原料,你能创建8*7=56个数据对以致于每次更改都要更新56个索引条目吗?!(即使你不同时存储“牛油果-培根”和“培根-牛油果”,仅仅将它减半并不解决任何问题,实际上在查询过程中还会产生一些问题,因为“原料-名字”索引不能反过来用。)这种方式恰恰和SQL存储和索引数据的方式相同,而这也恰恰是SQL规模化中的问题:你不能在大部分跨多表查询中使用索引——而由于SQL的严格你又要被迫使用多个表。让我们来看一个Drupal例子!

在Drupal中,节点有与之相关的评论。节点存储在一个数据表(Node)中,而因为一个节点可以有多个相关评论,评论存储在一个单独的表(Comment)中。如果你要显示最近十个被评论过的“页面”节点,你将面临类似的问题:如果你有一个基于创建日期的索引,那么你可以有一个按创建日期排序的评论列表以及它们所属的节点,而如果你有一个基于节点类型的索引,你可以有一个“页面”节点的列表,但是,假如最后二十万条评论全部都是针对“新闻”节点的,并且你有五万个页面,那么,你要么先将二十万条评论过一遍,以发现它们中没有一个是“页面”,要么你需要将五万个页面过一遍以找到每个的最新评论。那显然不够快。

通过把节点类型存储在评论表中,这个例子可以轻松地得到解决,因为节点类型不会改变。但是,如果你要把节点的更改信息和评论一起处理,那么你将被迫把节点的更改信息存储在评论表中,一次节点编辑可能触发评论表中数百行的更新。这种作法叫作“去规范化(Denormalization)”,且被广泛使用。然而,它应该亮起红旗:你在一个什么样的系统上工作啊?难道它非得抛开最基本的理论(规范化)才能表现良好?对于这个问题,我稍后将展示一个极好的解决方案,但是现在还是让我们继续来列举SQL的问题吧。