Sarah Mei 的一篇题为“ Why you should never use MongoDB ”的文章讨论了如果在关系数据库要优越得多的情况下尝试使用 NoSQL 数据库时会遇到的问题。这方面的一个例子是当被认为位于筒仓中的数据需要跨越边界时(关系数据库擅长的是)。另一个例子是,当您将用户名存储在不同的地方以便于访问时,但是当用户更新他们的名字时,您不得不查找所有这些地方以确保他们的信息是最新的。
我制作网站的经验与这种观点一致:除非您的数据对象彼此完全独立(并且您确定在可预见的未来它们会这样),否则您最好使用关系像 Postgres 这样的数据库。
直到最近,您还必须在建模数据时预先做出艰难的决定: 文档数据库还是关系数据库 ?是的,您可以使用两个独立的数据库,将每个工具用于它们最擅长的领域。但是,您会增加应用程序以及开发和服务器环境的复杂性。
Postgres 中的 JSON 支持
Postgres 支持 JSON 已经有一段时间了,但老实说,由于缺乏索引和键提取方法,它并不是很好。随着 9.2 版的发布,Postgres 添加了原生 JSON 支持。您最终可以将 Postgres 用作“NoSQL”数据库。在 9.3 版中,Postgres 通过添加额外的构造函数和提取器方法对此进行了改进。 9.4 添加了将 JSON 存储为“二进制 JSON”( 或 JSONB )的功能,它去除了无关紧要的空格(没什么大不了的),在插入数据时增加了一点开销,但在查询数据时提供了巨大的好处: 索引 。
随着版本 9.4 的发布,JSON 支持试图解决“我使用文档数据库还是关系数据库?”的问题。不必要。为什么不两者兼而有之?
我不会争辩说 Postgres 处理 JSON 和 MongoDB 一样好。毕竟,MongoDB 是专门作为 JSON 文档存储而制作的,并且具有一些非常棒的功能,例如 聚合管道 。但事实是 Postgres 现在可以很好地处理 JSON。
为什么我什至需要在我的数据库中使用 JSON 数据?
我仍然相信大多数数据都可以使用关系数据库很好地建模。这样做的原因是因为网站数据往往是相关的。用户进行购买并留下评论,一部电影有演员在各种电影中扮演角色等。但是,在某些用例中,将 JSON 文档合并到您的模型中非常有意义。例如,当您需要:
- 以与到达您时相同的结构和格式(如 JSON)维护来自外部服务的数据。最终出现在数据库中的正是 API 提供的内容。以 Stripe 的 充电响应对象 为例;它是嵌套的,有数组,等等。您可以按原样存储它(并仍然对其进行查询),而不是尝试跨五个或更多表对这些数据进行规范化。
- 避免在通过 JSON API 返回数据之前转换数据。看看这个来自 FDA API 的药物不良事件的 令人讨厌的 JSON 响应 。它嵌套很深并且有多个数组——在每个请求上实时构建这些数据将对系统造成难以置信的负担。
如何在 Postgres 中使用 JSONB
现在我们已经了解了在 Postgres 中存储 JSON 数据的一些好处和用例,让我们来看看它是如何实际完成的。
定义列
JSONB 列现在就像任何其他数据类型一样。下面是一个创建卡片表的示例,该表将其数据存储在名为“数据”的 JSONB 列中。
CREATE TABLE cards (
id integer NOT NULL,
board_id integer NOT NULL,
data jsonb
);
插入 JSON 数据
为了将 JSON 数据插入数据库,我们将整个 JSON 值作为字符串传递。
CREATE TABLE cards (
id integer NOT NULL,
board_id integer NOT NULL,
data jsonb
);
查询数据
您无法访问的数据相当无用。接下来我们将看看如何查询我们之前插入到数据库中的内容。
CREATE TABLE cards (
id integer NOT NULL,
board_id integer NOT NULL,
data jsonb
);
CREATE TABLE cards (
id integer NOT NULL,
board_id integer NOT NULL,
data jsonb
);
筛选结果
基于列过滤查询是很常见的,使用 JSONB 列,我们实际上可以进入 JSON 文档并根据它具有的不同属性过滤我们的结果。在下面的示例中,我们的“data”列有一个名为“finished”的属性,我们只需要 finished 为 true 的结果。
CREATE TABLE cards (
id integer NOT NULL,
board_id integer NOT NULL,
data jsonb
);
CREATE TABLE cards (
id integer NOT NULL,
board_id integer NOT NULL,
data jsonb
);
检查列是否存在
在这里,我们将找到记录的计数,其中数据列包含名为“ingredients”的属性。
CREATE TABLE cards (
id integer NOT NULL,
board_id integer NOT NULL,
data jsonb
);
CREATE TABLE cards (
id integer NOT NULL,
board_id integer NOT NULL,
data jsonb
);
扩展数据
如果您使用过关系数据库一段时间,您将非常熟悉聚合方法:count、avg、sum、min、max 等。现在我们正在处理 JSON 数据,数据库中的单个记录可能包含一个数组。因此,我们现在可以扩展我们的结果,而不是将结果缩小到一个聚合中。
CREATE TABLE cards (
id integer NOT NULL,
board_id integer NOT NULL,
data jsonb
);
CREATE TABLE cards (
id integer NOT NULL,
board_id integer NOT NULL,
data jsonb
);
关于上面的例子,我想指出三点:
- 即使我们从数据库中查询了一行,也返回了两行。这等于该行包含的标签数。
- 我使用了方法的 jsonb 形式而不是 json 形式。使用与您定义列的方式相匹配的那个。
- 我使用 -> 而不是以前的 ->> 访问 标签 字段。 -> 将以 JSON 对象的形式返回属性,而 ->> 将以整数或文本(属性的解析形式)形式返回属性。
JSONB 的真正好处:索引
我们希望我们的应用程序快速。如果没有索引,数据库将被迫逐条记录( 表扫描 ),检查条件是否为真。这与 JSON 数据没有什么不同。事实上,它很可能更糟,因为 Postgres 也必须介入每个 JSON 文档。
我已将测试数据从 5 条记录增加到 10,000 条。这样我们就可以开始看到在 Postgres 中处理 JSON 数据时的一些性能影响,以及如何解决它们。
CREATE TABLE cards (
id integer NOT NULL,
board_id integer NOT NULL,
data jsonb
);
CREATE TABLE cards (
id integer NOT NULL,
board_id integer NOT NULL,
data jsonb
);
CREATE TABLE cards (
id integer NOT NULL,
board_id integer NOT NULL,
data jsonb
);
现在,5 毫秒的查询并没有那么慢,但让我们看看是否可以改进它。
CREATE TABLE cards (
id integer NOT NULL,
board_id integer NOT NULL,
data jsonb
);
如果我们运行现在有索引的相同查询,我们最终会将时间减半。
CREATE TABLE cards (
id integer NOT NULL,
board_id integer NOT NULL,
data jsonb
);
CREATE TABLE cards (
id integer NOT NULL,
board_id integer NOT NULL,
data jsonb
);
我们的查询现在正在利用我们创建的 idxfinished 索引,查询时间大约减少了一半。
更复杂的索引
Postgres 中 JSON 支持的一个很酷的事情是您可以查询一个数组是否包含某个值。
CREATE TABLE cards (
id integer NOT NULL,
board_id integer NOT NULL,
data jsonb
);
CREATE TABLE cards (
id integer NOT NULL,
board_id integer NOT NULL,
data jsonb
);
CREATE TABLE cards (
id integer NOT NULL,
board_id integer NOT NULL,
data jsonb
);
从 Postgres 9.4 开始,随着 JSONB 数据类型出现了 GIN(广义倒排索引)索引。通过 GIN 索引,我们可以使用 JSON 运算符@>、?、?& 和 ?| 快速查询数据。有关操作符的详细信息,您可以访问 Postgres 文档 。
CREATE TABLE cards (
id integer NOT NULL,
board_id integer NOT NULL,
data jsonb
);
CREATE TABLE cards (
id integer NOT NULL,
board_id integer NOT NULL,
data jsonb
);
CREATE TABLE cards (
id integer NOT NULL,
board_id integer NOT NULL,
data jsonb
);
我们再次看到速度加倍。如果我们的数据集大于 10,000 条记录,这种情况会更加明显。 explain analyze 输出还向我们展示了它是如何使用 idxgintags 索引的。
最后,我们可以在整个数据字段上添加一个 GIN 索引,这将使我们在如何查询数据方面更加灵活。
CREATE TABLE cards (
id integer NOT NULL,
board_id integer NOT NULL,
data jsonb
);
CREATE TABLE cards (
id integer NOT NULL,
board_id integer NOT NULL,
data jsonb
);
我如何在 Rails 中执行此操作?
让我们探讨如何在 Rails 中创建包含 JSONB 列的表,以及如何查询这些 JSONB 列和更新数据。有关详细信息,您可以参考 有关此主题的 Rails 文档 。
定义 JSONB 列
首先,我们需要创建一个迁移,该迁移将创建一个表,其中有一列指定为 JSONB。
CREATE TABLE cards (
id integer NOT NULL,
board_id integer NOT NULL,
data jsonb
);
从 Rails 中查询 JSON 数据
让我们定义一个范围来帮助我们找到“完成”的卡片。应该注意的是,即使 完成的 列是一个 JSON true 值,在查询它时我们将需要使用 String true。如果我们在 Rails 中查看 完成的 列,我们将看到一个 TrueClass 值,并且在查看 psql 中的数据时它也是一个 JSON 真值,但尽管如此,它仍需要使用 String 进行查询。
CREATE TABLE cards (
id integer NOT NULL,
board_id integer NOT NULL,
data jsonb
);
这是将 :finished 范围添加到我们的 Card 类的代码。我们将无法使用我们习惯的常规 where 子句语法,而是必须依赖更 Postgres 特定的语法。应该注意的是,完成的列也需要用 String 包装,这就是您在 Postgres 中引用 JSON 列的方式。
CREATE TABLE cards (
id integer NOT NULL,
board_id integer NOT NULL,
data jsonb
);
CREATE TABLE cards (
id integer NOT NULL,
board_id integer NOT NULL,
data jsonb
);
在 Rails 中操作 JSON 数据
任何定义为 JSON 或 JSONB 的列都将在 Ruby 中表示为哈希。
CREATE TABLE cards (
id integer NOT NULL,
board_id integer NOT NULL,
data jsonb
);
在 Rails 中更新 JSON 数据
更新我们的 JSON 数据非常简单。这只是更改哈希值然后调用模型保存的问题。要将“finished”字段更新为 true,我们将运行以下命令:
CREATE TABLE cards (
id integer NOT NULL,
board_id integer NOT NULL,
data jsonb
);
您会注意到 Rails 和 Postgres 都不能只更新 JSON 数据中的单个“完成”值。它实际上用整个新值替换了整个旧值。
结论
我们已经看到 Postgres 现在包含一些非常强大的 JSON 结构。将关系数据库的强大功能(一个简单的内部联接是一件美妙的事情,不是吗?)与 JSONB 数据类型的灵活性相结合,可以提供许多好处,同时又没有两个独立数据库的复杂性。
您还可以避免做出有时会出现在文档数据库中的妥协(如果您必须在五个不同的地方更新对用户名的引用,您就会明白我在说什么。)试一试!谁说你不能教老狗一些新把戏?