存储 系列 数据模型和查询语言

在可预见的将来,关系数据库可能仍将继续与各种非关系数据存储一起使用,这种思路有时也被称为混合持久化。

Posted by lichao modified on January 30, 2022

数据库=数据模型+查询语言

数据模型发展历史

  • 1965年:网络模型(Network Model)数据库——IDS发布;
  • 1968年:层级模型(Hierarchical Mode)数据库——IMS发布;
  • 1970年:Edgar Codd发表关系模型(Relational Model)论文;
  • 1974年:IBM基于Codd的概念,开发原型系统System R;
  • 1978年:标准化的查询语言——SQL(Structured Query Language)诞生;
  • 1979年:关系型数据库——Oracle Release 1 发布;
  • 1985年:对象模型(Object Model)发布;
  • 1995年:开源的关系型数据库——MySQL Sybase IQ发布;
  • 2007年:图数据库(Graph DataBase)——NEO4j发布;
  • 2009年10月:KV模型(Key-Value) 数据库——Redis发布;
  • 2009年11月:文档模型(Document Model)数据库——MongoDB发布;
  • 2011年:宽列存储模型(Wide column)数据库——HBase稳定版发行;

层次模型

较早期的层次模型(IMS)将所有数据表示为嵌套在记录中的记录(树结构),可以很好地支持一对多关系,而多对多关系则有些困难;为了解决层次模型的局限性,业界提出了很多解决方案,比较著名的有关系模型(relational model)和网络模型(network model);

网络模型

网络模型(CODASYL)是在层次模型的基础上,其子节点支持多个父节点,从而形成一个网状结构;和层次模型一样,节点可以嵌套的类型 是固定的;而获取特定记录需要遍历其中一条访问路径才行(没办法直接定位到);

关系模型(SQL)

定义:关系模型是指用二维表的形式表示实体和实体间联系的数据模型。

关系模型

特征:

  • 关系模型有坚实的设计理论和运算基础,在近三十年内一直处于主导地位。
    • 数据被组织成关系(relations),在 SQL 中关系称之为表-table、关系中的元组(tuples)被称之为行-row。
  • 关系必须是规范化的关系,即每个属性是不可分割的实体,不允许表中表的存在。
  • 查询语言 SQL 已经一统江湖成为标准。
    • 支持丰富的关系联结(1 对 1、1 对多、多对多)和事务处理。

优点:

  • 普及度高;关系型数据库无论在 大学教学 还是工程应用覆盖率都很高;
  • 概念简单,操作方便;
  • SQL高度抽象,屏蔽了底层复杂的逻辑,操作数据非常简单;
  • 支持一对一、一对多、多对多等复杂关系;联表查询也十分方便;
  • MySQL、Oracle等对单机事务有成熟的支持;

缺点:

  • 横向拓展支持不成熟;引入中间件做分片则会损失一定性能,或丢失部分功能(如事务);
  • 支持多对多时,需要引入第三张表
  • 严谨的Schema要求,维护表结构会有一定成本;

在面向对象编程时,对于关系表数据,需将代码中的对象与表、行和列的数据库模型之间需要一个转换层。对象关系映射(ORM)框架减少了此转换层所需的样板代码量,但是并不能完全隐藏两个模型之间的差异。

关系模型的目标就是将实现细节隐藏在更简洁的接口(SQL)后面。

图模型

定义:用有向图表示 实体和实体之间 的联系的数据结构模型称为 图模型。 图模型

特征:

  • 节点之间的关联通常没有约束,想联则联;
  • 可根据唯一id定位任意节点;
  • 属性图:
    • 每个顶点包含:唯一标识符、出边集合、入边集合、属性的集合(键值对)
    • 每条边包含:唯一标识符、边开始/结束顶点、描述两个顶点间关系类型的标签、属性的集合(键值对)

优点:

  • 可以更加方便自然 地表示复杂的多对多数据模型;
  • 遍历高效:给定某个顶点,可以高效地得到它的所有入边和出边,从而遍历图。
  • 节点较为独立,可以随意删除和增加,而无需像层次模型那样级联动作;
  • 通过对不同类型的关系使用不同的标签,可以在单个图中存储多种不同类型的信息,同时仍然保持整洁的数据模型。
  • 支持图相关概念的查询,如简单路径、最短路径等;

缺点:

  • 图模型是一种导航式的数据模型,操作路径和记录目标数据较为复杂;
  • 图查询语言较多,尚未有很好的统一;
  • 成熟的开源实现较少,多为公司自研。

图模型(Neo4j/ByteGraph)和网络模型有着相似的特点,但是更加灵活。任何节点之间均可以建立连接(边);只需要有唯一ID即可确定起始节点;现已称为主流的数据模型,以更加自然快速地应对复杂的多对多关系。

文档模型

定义:文档模型是用树状结构来组织数据的数据模型。

文档模型

特征:树的性质决定了树状数据模型的特征

  • 一棵树(如单个文档记录)中,除了根节点其他节点都要有一个父节点;
  • 子节点不能脱离父节点而单独存在;删除父节点时,子节点也要同时删除;
  • 每个记录类型有且仅有一条从父节点通向自身的路径;
  • 可以使用XML/JSON等格式表示树结构并进行存储。

优点:

  • 结构简单清晰,很容易看到各个实体之间的联系;
  • 数据完整性高,父节点删除,子节点也删。
  • 查询效率较高,单次查询可以拉取整棵树的数据,无需额外的联结动作;
  • 操作数据库语句简单,学习成本低;
  • 增减字段无需修改Schema(以MongoDB为例),降低运维成本。
  • 支持超大数据集或超高写入吞吐量。

缺点:

  • 数据结构较为呆板(一颗树状结构),灵活性不够
  • 更适合1:N的数据模型,对于M:N的复杂关系表示并不方便;
  • 查询节点的时候必须知道其双亲节点的,因此限制了对数据库存取路径的控制。

伴随着各个数据库的发展,关系型数据库(如MySQL 5.7、PostgreSQL 9.3)等已经开始支持JSON类型的字段;而文档型数据库(如MongoDB)则逐渐开始支持联结(联表查询)、事务等;关系型数据库和文档型数据库变得越来越接近,选型时也可以将这些新特性纳入考虑范围。

KV模型

KV模型(Redis/Memcached)Key-Value是最简单的数据模型,虽然无法和关系型/图等数据库一样支持复杂的关系,但是正因为简单的模型,Key-Value数据库往往有着更加优秀的性能。

Search engine

Search engine(Elasticsearch)搜索引擎数据库,专注于全文搜索(如日志搜索);

Wide column

Wide column(Cassandra/HBase)宽列存储,专注于OLAP(On-Line Analytical Processing)

Time Series

Time Series(InfluxDB)专注于时间序列的记录存储和分析(如打点记录)

数据查询语言

查询语言主要分两种类型:(1)命令式(2)声明式

命令式查询语言

IMS(层次模型)和 CODASYL(网状模型)都是命令式查询语言。 命令式语言告诉计算机以特定顺序执行某些操作。你完全可以推理整个过程,逐行遍历代码、评估相关条件、更新对应的变量,甚至决定是否再循环一遍。

例子:如果有一个动物物种的列表,可能会写这样的代码来查询列表中的 鲨鱼(Shark): function getSharks() { var sharks = []; for (var i ; i < animals.length; i++) { if (animals[i].family ===”Sharks”){ sharks.push(animals[i]); } } return sharks; }

命令式语言是比较接近底层的语言,可能需要考虑的问题:

  1. 如果数据库想要在后台回收未使用的磁盘空间,则可能需要移动记录,从而改变动物列表的顺序。这种情况下,数据库能否在 不中断查询的情况下 安全地 执行此类操作?
  2. 数据库 无法确定 命令式语言写出的 代码是否 需要依赖于排序,对这些代码优化的空间较小;
  3. 如何合理地支持并发执行?

声明式查询语言

SQL 是典型的声明式语言,它不需要知道底层的具体操作,只需要声明要取的数据即可。 声明式语言 较于命令式的 优势:

  1. 比命令式 API更加简洁和容易使用;
  2. 高度抽象,屏蔽了许多底层东西:
  3. 可以在不改变目标的前提下,进行各种优化(查询优化器)
  4. 引擎底层可以对并发有更好的支持;

混合风格-MapReduce

MapReduce 是一种编程模型,用于在许多机器上批量处理海量数据。MapReduce 既不是声明式查询语言,也不是一个完全命令式的查询语言,而是介于两者之间:查询的逻辑用代码片段来表示, 这些代码片段可以被处理框架重复地调用。它主要基于许多函数式编程语言中的 map( 也称为collect)和 reduce(也称为 fold 或 inject)函数。

Map 和 Reduce 函数对于可执行的操作有所限制。它们必须是纯函数,这意味着只能使用传递进去的数据作为输入,而不能执行额外的数据库查询,也不能有任何副作用。这些限制使得数据库能够在任何位置、以任意顺序来运行函数,并在失败时重新运行这些函数。不管怎样,该功能非常强大,可以通过它来解析字符串、调用库函数、执行计算等。

例子:

  1. 添加记录:你是一名海洋生物学家,每当你看到海洋中的动物时, 就会在数据库中添加观察记录;
  2. 生成报告:看下每个月看到了多少鲨鱼。

使用SQL:

1
2
3
4
SELECT date_trunc('month', observation_timestamp) AS observation_month, 
sum(num_animals) AS total_animals 
FROM observations WHERE family= 'Sharks'    -- 过滤
GROUP BY observation_month;    -- 根据月份分类

使用 MongoDB中 的 MapReduce:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
db.observations.mapReduce(
    function map() {
        var year = this.observationStimestamp.getFullYear();
        var month = this.observationStimestamp.getMonth() + 1;
        emit(year + "-" + month, this.numAnimals);
    },
    function reduce(key, values) {
        return Array.sum(values);
    },
    {
        query: { family : "Sharks" },    // 过滤
        out: "monthlySharkReport"
    }
);

MongoDB 2.2+ 支持了增加了称为 聚合管道(aggregate) 的 声明式查询语言的支持:

1
2
3
4
5
6
7
8
9
10
db.observations.aggregate ([
    { $match: { family: 'sharks'}}
    { $group: {
        _id: {
            year: { $year: "observationTimestamp"}
            month: { $month: "observationTimestamp" }
        },
        totalAnimals: { $sum: "$numAnimals" }
    }}
]);

聚合管道在表达能力上相当于SQL的子集 ,但是它使用了基于JSON的语法,而不是SQL的英语句式语法。或许这种差异仅仅是个品味问题。然而这个故事的寓意是,NoSQL系统可能会发现自 己意外地重新发 明了 SQL,尽管是伪装的。

参考文献

DB-Engines Ranking