第二章:数据模型与查询语言
数据模型可能是软件开发中最重要的部分,它不仅仅影响着软件的编写方式,而且影响着我们的解题思路。
一个复杂的应用程序可能会有更多的中间层次,每个层都通过提供一个明确的数据模型来隐藏更低层次中的复杂性。
关系模型与文档模型
最著名的数据模型可能是SQL。它基于Edgar Codd在1970年提出的关系模型:数据被组织成关系(SQL中称作表),其中每个关系是元组(SQL中称作行)的无序集合。
关系数据库起源于商业数据处理,在20世纪60年代和70年代用大型计算机来执行。典型的事务处理(将销售或银行交易,航空公司预订,库存管理信息记录在库)和批处理(客户发票,工资单,报告)。
NoSQL的诞生
采用NoSQL数据库的背后有几个驱动因素,其中包括:
- 需要比关系数据库更好的可扩展性,包括非常大的数据集或非常高的写入吞吐量
- 相比商业数据库产品,免费和开源软件更受偏爱。
- 关系模型不能很好地支持一些特殊的查询操作
- 受挫于关系模型的限制性,渴望一种更具多动态性与表现力的数据模型
不同的应用程序有不同的需求,一个用例的最佳技术选择可能不同于另一个用例的最佳技术选择。关系数据库会与各种非关系数据库一起使用 - 混合持久化(polyglot persistence)
对象关系不匹配
如果数据存储在关系表中,那么需要一个笨拙的转换层,处于应用程序代码中的对象和表,行,列的数据库模型之间。模型之间的不连贯有时被称为阻抗不匹配(impedance mismatch)。
对象关系映射(ORM object-relational mapping) 框架可以减少这个转换层所需的样板代码的数量,但是它们不能完全隐藏这两个模型之间的差异。
我们可以使用JSON模型减少了应用程序代码和存储层之间的阻抗不匹配,但是JSON作为数据编码格式也存在问题。
JSON表示比数据库的多表模式具有更好的局部性(locality)。
多对一和多对多的关系
存储ID还是文本字符串,这是个 副本(duplication) 问题。当使用ID时,对人类有意义的信息(比如单词:Philanthropy)只存储在一处,所有引用它的地方使用ID(ID只在数据库中有意义)。当直接存储文本时,对人类有意义的信息会复制在每处使用记录中。
使用ID的好处是,ID对人类没有任何意义,因而永远不需要改变:ID可以保持不变,即使它标识的信息发生变化。
任何对人类有意义的东西都可能需要在将来某个时候改变——如果这些信息被复制,所有的冗余副本都需要更新。这会导致写入开销,也存在不一致的风险(一些副本被更新了,还有些副本没有被更新)。去除此类重复是数据库 规范化(normalization) 的关键思想。
数据库通过连接来进行多表关联查询,来使得数据变得更加互联。
文档数据库是否在重蹈覆辙?
在多对多的关系和连接已常规用在关系数据库时,文档数据库和NoSQL重启了辩论:如何最好地在数据库中表示多对多关系。
20世纪70年代最受欢迎的业务数据处理数据库是IBM的信息管理系统(IMS),设计中使用了一个相当简单的数据模型,称为层次模型:文档数据库使用的JSON模型有一些相似之处。它将所有数据表示为嵌套在记录中的记录树。
同文档数据库一样,IMS能良好处理一对多的关系,但是很难应对多对多的关系,并且不支持连接。
提出了两种解决方案来解决层次模型的局限性:
- 关系模型(relational model)(它变成了SQL,统治了世界)
- 网络模型(network model)
网络模型
网络模型被称为CODASYL模型。
在层次模型的树结构中,每条记录只有一个父节点;在网络模式中,每条记录可能有多个父节点。
网络模型中记录之间的链接不是外键,而更像编程语言中的指针(同时仍然存储在磁盘上)。访问记录的唯一方法是跟随从根记录起沿这些链路所形成的路径。这被称为访问路径(access path)。
访问路径类似遍历链表:从列表头开始,每次查看一条记录,直到找到所需的记录。但在多对多关系的情况中,数条不同的路径可以到达相同的记录。
关系模型
关系模型中一个 关系(表) 只是一个 元组(行) 的集合。
你可以选中符合任意条件的行,读取表中的任何或所有行。你可以通过指定某些列作为匹配关键字来读取特定行。你可以在任何表中插入一个新的行,而不必担心与其他表的外键关系。
在关系数据库中,查询优化器自动决定查询的哪些部分以哪个顺序执行,以及使用哪些索引。
关系模型的一个关键洞察是:只需构建一次查询优化器,随后使用该数据库的所有应用程序都可以从中受益。如果你没有查询优化器的话,那么为特定查询手动编写访问路径比编写通用优化器更容易——不过从长期看通用解决方案更好。
与文档数据库相比
文档数据库还原为层次模型:在其父记录中存储嵌套记录,而不是在单独的表中。
在表示多对一和多对多的关系时,关系数据库和文档数据库并没有根本的不同:在这两种情况下,相关项目都被一个唯一的标识符引用,这个标识符在关系模型中被称为外键,在文档模型中称为文档引用。
关系型数据库与文档数据库在今日的对比
文档数据模型:架构灵活性,局部性而拥有更好的性能,更接近于应用程序使用的数据结构。
关系模型:更好的支持多对一和多对多的关系
文档模型中的架构灵活性
文档数据库有时称为无模式(schemaless),但这具有误导性。一个更精确的术语是读时模式(schema-on-read)(数据的结构是隐含的,只有在数据被读取时才被解释),相应的是写时模式(schema-on-write)(传统的关系数据库方法中,模式明确,且数据库确保所有的数据都符合其模式)
读时模式类似于编程语言中的动态(运行时)类型检查,而写时模式类似于静态(编译时)类型检查。
在应用程序想要改变其数据格式的情况时:
- 在文档数据库中,只需开始写入具有新字段的新文档,并在应用程序中使用代码来处理读取旧文档的情况。
- 在“静态类型”数据库模式中,通常会执行以下 迁移(migration) 操作。
读时模式优势:
- 存在许多不同类型的对象,将每种类型的对象放在自己的表中是不现实的。
- 数据的结构由外部系统决定。你无法控制外部系统且它随时可能变化。
查询的数据局部性
文档通常以单个连续字符串形式进行存储,编码为JSON,XML或其二进制变体(如MongoDB的BSON)。
局部性仅仅适用于同时需要文档绝大部分内容的情况。
更新文档时,通常需要整个重写。只有不改变文档大小的修改才可以容易地原地执行。因此,通常建议保持相对小的文档,并避免增加文档大小的写入。
为了局部性而分组集合相关数据的想法并不局限于文档模型:
- Oracle类似地允许使用一个称为 多表索引集群表(multi-table index cluster tables) 的类似特性。
- Bigtable数据模型(用于Cassandra和HBase)中的 列族(column-family) 概念与管理局部性的目的类似。
文档和关系数据库的融合
大多数关系数据库系统(MySQL除外)都已支持XML。这包括对XML文档进行本地修改的功能,以及在XML文档中进行索引和查询的功能。
随着时间的推移,关系数据库和文档数据库似乎变得越来越相似,数据模型相互补充,如果一个数据库能够处理类似文档的数据,并能够对其执行关系查询,那么应用程序就可以使用最符合其需求的功能组合。
数据查询语言
SQL是一种 声明式 查询语言,而IMS和CODASYL使用 命令式 代码来查询数据库。
命令式语言告诉计算机以特定顺序执行某些操作。
在声明式查询语言(如SQL或关系代数)中,你只需指定所需数据的模式 - 结果必须符合哪些条件,以及如何将数据转换(例如,排序,分组和集合) - 但不是如何实现这一目标。
声明式语言往往适合并行执行。
命令代码很难在多个内核和多个机器之间并行化,因为它指定了指令必须以特定顺序执行。
MapReduce查询
MapReduce是一个由Google推广的编程模型,用于在多台机器上批量处理大规模的数据。
MapReduce既不是一个声明式的查询语言,也不是一个完全命令式的查询API,而是处于两者之间:查询的逻辑用代码片断来表示,这些代码片段会被处理框架重复性调用。它基于(也称为)和(也称为或)函数。
map和reduce函数在功能上有所限制:它们必须是纯函数,这意味着它们只使用传递给它们的数据作为输入,它们不能执行额外的数据库查询,也不能有任何副作用。
图数据模型
一个图由两种对象组成:顶点(vertices)(也称为节点(nodes) 或实体(entities)),和边(edges)( 也称为关系(relationships)或弧 (arcs) )。多种数据可以被建模为一个图形。
图中的所有顶点代表了相同类型的事物。图提供了一种一致的方式,用来在单个数据存储中存储完全不同类型的对象。
属性图
在属性图模型中:
每个顶点(vertex)包括:
- 唯一的标识符
- 一组 出边(outgoing edges)
- 一组 入边(ingoing edges)
- 一组属性(键值对)
每条 边(edge) 包括:
- 唯一标识符
- 边的起点/尾部顶点(tail vertex)
- 边的终点/头部顶点(head vertex)
- 描述两个顶点之间关系类型的标签
- 一组属性(键值对)
可以将图存储看作由两个关系表组成:一个存储顶点,另一个存储边。
关于这个模型的一些重要方面是:
- 任何顶点都可以有一条边连接到任何其他顶点。没有模式限制哪种事物可不可以关联。
- 给定任何顶点,可以高效地找到它的入边和出边,从而遍历图,即沿着一系列顶点的路径前后移动。
- 通过对不同类型的关系使用不同的标签,可以在一个图中存储几种不同的信息,同时仍然保持一个清晰的数据模型。
Cypher查询语言
Cypher是属性图的声明式查询语言,为Neo4j图形数据库而发明。
SQL中的图查询
查询可变长度遍历路径的思想可以使用称为递归公用表表达式。
三元组存储和SPARQL
在三元组存储中,所有信息都以非常简单的三部分表示形式存储(主语,谓语,宾语)。例如,三元组 (吉姆, 喜欢 ,香蕉) 中,吉姆 是主语,喜欢 是谓语(动词),香蕉 是对象。
三元组的主语相当于图中的一个顶点。而宾语是下面两者之一:
- 原始数据类型中的值,例如字符串或数字。在这种情况下,三元组的谓语和宾语相当于主语顶点上的属性的键和值。例如,(lucy, age, 33)就像属性{“age”:33}的顶点lucy。
- 图中的另一个顶点。在这种情况下,谓语是图中的一条边,主语是其尾部顶点,而宾语是其头部顶点。例如,在(lucy, marriedTo, alain)中主语和宾语lucy和alain都是顶点,并且谓语marriedTo是连接他们的边的标签。