我什么时候可以在 SQL Table 中保存 JSON 或 XML 数据

When can I save JSON or XML data in an SQL Table

当使用 SQLMySQL(或与此相关的任何关系数据库)时 - 我知道将数据保存在常规列中对于索引和其他目的来说更好...

重要的是加载和保存 JSON 数据有时要简单得多 - 并使开发更容易。

是否有任何 "golden rules" 用于在数据库中保存原始 JSON 数据?

这样做绝对是一种不好的做法吗?

如果它“绝对错误”,那么大多数数据库都不会支持它。好的,大多数数据库都支持 FROM 子句中的逗号,我认为这是“绝对错误的”。但是对 JSON 的支持是新的发展,而不是向后兼容的“功能”。

一个明显的例子是 JSON 结构只是一个传递回应用程序的 BLOB。然后就没有争论了——除了存储的开销 JSON,这对于每条记录中都有公共字段的结构化数据来说是不必要的冗长。

另一种情况是“稀疏”列情况。您的行包含许多可能的列,但这些列因行而异。

另一种情况是当你想在一个记录中存储“嵌套”记录时。 JSON很厉害

如果 JSON 在您要查询的记录中有公共字段,那么您通常最好将它们放在适当的数据库列中。但是,数据很复杂,JSON.

等格式也有它的位置。

主要问题是

  • 你打算用这些数据做什么?和
  • 你filtering/sorting/joining/manipulating这个数据怎么样?

JSON(如 XML)非常适合数据交换、小型存储和一般定义的结构,但它不能参与您 运行 在 RDBMS 中的典型操作。在大多数情况下,最好在需要时将 JSON 数据传输到 普通 tables 和 re-create 和 JSON .

XML / JSON 和 1.NF

规范化的第一条规则规定,永远不要将多于一位的信息存储到一列中。您是否看到“PersonName”列的值类似于“Mickey Mouse”?你指着这个哭:马上改!

XML 或 JSON 呢?这些类型会破坏 1.NF 吗?嗯,是和不是...

完全可以将一个完整的结构存储为一位信息,如果它实际上是一位信息。您收到一个 SOAP 响应并想要存储它,因为您可能需要它以供将来参考(但您将 不会将此数据用于您自己的流程 )?只需 原样存储!

现在想象一个 复杂结构(XML 或 JSON)代表一个人(及其地址、更多详细信息...)。现在你把这个 作为 PersonInCharge 放到一栏中。这是错误的吗?这难道不应该存在于设计合理的相关 table 中,并带有外键引用而不是 XML/JSON 吗?特别是如果同一个人可能出现在许多不同的行中,那么使用 XML/JSON 方法肯定是错误的。

但现在想象一下需要存储历史数据。您想要 保留 该人在给定时刻的数据。几天后这个人告诉你一个新地址?没问题!如果您需要,旧地址位于 XML/JSON 中...

结论:如果只是为了保存而存储数据,那还行。如果这个数据是 unique 部分,没关系...
但是如果你经常需要内部部件,或者如果这意味着冗余的重复存储,那就不行了......

物理存储

以下内容适用于 SQL 服务器,在其他 RDBM 上可能有所不同。

XML不是存储为你看到的文字,而是存储为层次树。查询这是非常好的表现!此结构未在字符串级别进行解析!
JSON in SQL Server (2016+) 存在于字符串中,必须进行解析。没有真正的原生 JSON 类型(就像有原生 XML 类型一样)。这可能会稍后出现,但现在我假设 JSON 在 SQL 服务器上的性能不如 XML(请参阅 UPDATE 2[=166 部分) =]).任何需要从 JSON 中读取值都需要大量的隐藏字符串方法调用...

这对你意味着什么?

你的 可爱的 DB 艺术家 :-D 知道,存储 JSON 原样,违反了 RDBM 的共同原则。他知道,

  • JSON 很可能会损坏 1.NF
  • JSON 可能会随时间变化(同一栏,不同内容)。
  • 一个 JSON 不容易阅读,很难 filter/search/join 或按它排序。
  • 这样的操作会将相当多的额外负载转移到可怜的小数据库服务器上

有一些变通方法(取决于您使用的 RDBMS),但其中大部分都无法按照您希望的方式工作...

您的问题的简短答案

  • 如果您不想使用存储在中的数据,您的JSON用于昂贵的操作(filter/join/sort).
    您可以像存储任何其他 仅存在 内容一样存储它。我们将许多图片存储为 BLOB,但我们不会尝试过滤所有带花的图像...
  • 如果你根本不在意里面有什么(只是存储它并作为一个信息读取它)
  • 如果结构是可变的,那么创建物理 [​​=290=] 将更难处理 JSON 数据。
  • 如果结构嵌套很深,物理tables的存储开销太大

  • 如果您想像使用关系 table 的数据(过滤器、索引、连接...)一样使用内部数据
  • 如果您要存储重复项(创建冗余)
  • 总的来说:如果您遇到性能问题(肯定会在许多典型场景中遇到它们!)

您可以从字符串列中的 JSON 或 BLOB 开始,并在需要时将其更改为物理 table。我的魔法 crystal 球告诉我,这可能是明天 :-D

更新

找到一些想法性能和光盘 space 此处:https://whosebug.com/a/47408528/5089204

更新 2:有关性能的更多信息...

以下地址JSON和XML支持SQL-Server2016

用户@mike123 指出了一个 article on an official microsoft blog 似乎在实验中证明,查询 JSON 是 10 倍快 然后在 SQL-Server.

中查询 XML

一些想法:

一些cross-checks“实验”:

  • “实验”衡量了很多,但不是 XML 与 JSON 的表现。重复对同一个(不变的)字符串执行相同的操作是不现实的
  • 测试的示例对于一般陈述来说太简单了
  • 读取的值始终相同,甚至未被使用。优化器会看到这个...
  • 强大的XQuery支持只字不提!在数组中查找具有给定 ID 的产品? JSON 需要读取整个批次,然后使用 WHERE 使用过滤器,而 XML 将允许内部 XQuery predicate。更不用说FLWOR...
  • “实验”代码 原样 在我的系统上出现:JSON 似乎快了 3 倍(但不是 10 倍)。
  • /text() 添加到 XPath 将其减少到小于 2x。在相关文章中网友“Magoo先生”已经指出了这一点,但是click-bait标题还是没变...
  • 有了“实验”中给出的如此简单的 JSON,最快的纯 T-SQL 方法是 SUBSTRINGCHARINDEX 的组合 :-D

下面的代码将展示一个更真实的实验

  • 使用 JSON 和具有多个 Product 的相同 XML(JSON 数组与兄弟节点)
  • JSON和XML稍有变化(10000运行宁数)插入tables.
  • 为了避免 first-call-bias
  • ,table 有一个初始调用
  • 读取所有 10000 个条目并将检索到的值插入另一个 table。
  • 使用GO 10将运行通过此块十次以避免first-call-bias

最终结果清楚地表明,JSON 比 XML 慢(不是那么多,在一个仍然非常简单的例子中大约是 1.5 倍) .

最终声明:

  • 在不适当的情况下使用过度简化的示例 JSON 可能比 XML
  • 更快
  • 对JSON的处理是纯字符串动作,而对XML进行解析和转换。这在第一个动作中相当昂贵,但一旦完成,一切都会加快。
  • JSON 在 one-time 操作中可能更好(避免创建 XML 的内部分层表示的开销)
  • 用一个仍然很简单但更现实的例子XML简单阅读会更快
  • 每当需要从数组中读取特定元素、过滤数组中包含给定 ProductID 的所有条目或在路径中上下导航时,JSON 无法保持向上。它必须完全从字符串中解析出来——每次你都必须抓住它...

测试代码

USE master;
GO
--create a clean database
CREATE DATABASE TestJsonXml;
GO
USE TestJsonXml;
GO
--create tables
CREATE TABLE TestTbl1(ID INT IDENTITY,SomeXml XML);
CREATE TABLE TestTbl2(ID INT IDENTITY,SomeJson NVARCHAR(MAX));
CREATE TABLE Target1(SomeString NVARCHAR(MAX));
CREATE TABLE Target2(SomeString NVARCHAR(MAX));
CREATE TABLE Times(Test VARCHAR(10),Diff INT)
GO
--insert 10000 XMLs into TestTbl1
WITH Tally AS(SELECT TOP 10000 ROW_NUMBER() OVER(ORDER BY (SELECT NULL))*2 AS Nmbr FROM master..spt_values AS v1 CROSS APPLY master..spt_values AS v2)
INSERT INTO TestTbl1(SomeXml)
SELECT 
N'<Root>
    <Products>
    <ProductDescription>
        <Features>
            <Maintenance>' + CAST(Nmbr AS NVARCHAR(10)) + ' year parts and labor extended maintenance is available</Maintenance>
            <Warranty>1 year parts and labor</Warranty>
        </Features>
        <ProductID>' + CAST(Nmbr AS NVARCHAR(10)) + '</ProductID>
        <ProductName>Road Bike</ProductName>
    </ProductDescription>
    <ProductDescription>
        <Features>
            <Maintenance>' + CAST(Nmbr + 1 AS NVARCHAR(10)) + ' blah</Maintenance>
            <Warranty>1 year parts and labor</Warranty>
        </Features>
        <ProductID>' + CAST(Nmbr + 1 AS NVARCHAR(10)) + '</ProductID>
        <ProductName>Cross Bike</ProductName>
    </ProductDescription>
    </Products>
</Root>'
FROM Tally;

--insert 10000 JSONs into TestTbl2
WITH Tally AS(SELECT TOP 10000 ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) AS Nmbr FROM master..spt_values AS v1 CROSS APPLY master..spt_values AS v2)
INSERT INTO TestTbl2(SomeJson)
SELECT 
N'{
    "Root": {
        "Products": {
            "ProductDescription": [
                {
                    "Features": {
                        "Maintenance": "' + CAST(Nmbr AS NVARCHAR(10)) + ' year parts and labor extended maintenance is available",
                        "Warranty": "1 year parts and labor"
                    },
                    "ProductID": "' + CAST(Nmbr AS NVARCHAR(10)) + '",
                    "ProductName": "Road Bike"
                },
                {
                    "Features": {
                        "Maintenance": "' + CAST(Nmbr + 1 AS NVARCHAR(10)) + ' blah",
                        "Warranty": "1 year parts and labor"
                    },
                    "ProductID": "' + CAST(Nmbr + 1 AS NVARCHAR(10)) + '",
                    "ProductName": "Cross Bike"
                }
            ]
        }
    }
}'
FROM Tally;
GO

--Do some initial action to avoid first-call-bias
INSERT INTO Target1(SomeString)
SELECT SomeXml.value('(/Root/Products/ProductDescription/Features/Maintenance/text())[1]', 'nvarchar(4000)')
FROM TestTbl1;
INSERT INTO Target2(SomeString)
SELECT JSON_VALUE(SomeJson, N'$.Root.Products.ProductDescription[0].Features.Maintenance')
FROM TestTbl2;
GO

--Start the test
DECLARE @StartDt DATETIME2(7), @EndXml DATETIME2(7), @EndJson DATETIME2(7);

--Read all ProductNames of the second product and insert them to Target1
SET @StartDt = SYSDATETIME();
INSERT INTO Target1(SomeString)
SELECT SomeXml.value('(/Root/Products/ProductDescription/ProductName/text())[2]', 'nvarchar(4000)')
FROM TestTbl1
ORDER BY NEWID();
--remember the time spent
INSERT INTO Times(Test,Diff)
SELECT 'xml',DATEDIFF(millisecond,@StartDt,SYSDATETIME());

--Same with JSON into Target2
SET @StartDt = SYSDATETIME();
INSERT INTO Target2(SomeString)
SELECT JSON_VALUE(SomeJson, N'$.Root.Products.ProductDescription[1].ProductName')
FROM TestTbl2
ORDER BY NEWID();
--remember the time spent
INSERT INTO Times(Test,Diff)
SELECT 'json',DATEDIFF(millisecond,@StartDt,SYSDATETIME());

GO 10 --do the block above 10 times

--Show the result
SELECT Test,SUM(Diff) AS SumTime, COUNT(Diff) AS CountTime
FROM Times
GROUP BY Test;
GO
--clean up
USE master;
GO
DROP DATABASE TestJsonXml;
GO

结果(SQL Acer Aspire v17 Nitro Intel i7、8GB Ram 上的 Server 2016 Express)

Test    SumTime 
------------------
json    2706    
xml     1604    

我会挥舞我的魔杖。噗!使用 JSON:

的黄金法则
  • 如果MySQL不需要查看内部 JSON,应用程序只需要一些东西,那么JSON 很好,甚至可能更好。

  • 如果您要搜索 中的数据,您有 MariaDB 10.0.1 或 MySQL 5.7(带有 JSON 数据类型和函数),那么 JSON 可能 是实用的。 MariaDB 5.3 的 "Dynamic" 列是这个的变体。

  • 如果你在做"Entity-Attribute-Value"的事情,那么JSON是不好的,但它是几大坏处中最小的。 http://mysql.rjweb.org/doc.php/eav

  • 对于按索引列进行搜索,没有将值隐藏在 JSON 中是一个很大的优势。

  • 对于索引列的范围搜索,或 FULLTEXT 搜索或 SPATIAL,JSON 是不可能的。

  • 对于WHERE a=1 AND b=2,"composite"索引INDEX(a,b)很好;可能无法接近 JSON.

  • JSON 适用于 "sparse" 数据; INDEXing 可以工作,但效果不佳。 (我指的是许多行的 'missing' 或 NULL 值。)

  • JSON 可以给你 "arrays" 和 "trees" 而无需诉诸额外的 table(s)。但是在app里arrays/trees只有,在SQL.

    [=57里没有 =]
  • JSON 比 XML 好很多。 (我的意见)

  • 如果你不想进入 JSON 字符串,除了从应用程序,那么我建议压缩(在客户端中)它存储到 BLOB。把它想象成 .jpg -- 里面有东西,但 SQL 不在乎。

说明您的申请;也许我们可以更具体一些。

我使用的 "golden rule",以一种手摇的方式,是如果我需要原始格式的 JSON,可以存储。如果一定要特意去解析,那就不是了。

例如,如果我正在创建一个发送原始 JSON 的 API,并且无论出于何种原因这个值都不会改变,那么 没问题 将其存储为原始 JSON。如果我必须解析它、更改它、更新它等等……那么就没那么多了。

你要问的问题是:

Am I tied to using only this database?

  1. 如果您可以使用不同的数据库来存储 JSON,请使用文档存储解决方案,例如 CouchDB、DynamoDB 或 MongoDB。
  2. 使用这些文档存储数据库的能力来索引和搜索分层数据。
  3. 为关系数据使用关系数据库。
  4. 使用关系数据库进行报告、数据仓库和数据挖掘。

不要

  1. 如果可能,将 JSON 存储为字符串。
  2. 尝试得出 JSON 数据的最大长度。
  3. 使用 varchar 存储 JSON(如果必须,请使用 text/blob)。
  4. 尝试在存储的 JSON 中搜索值。
  5. 担心转义 JSON 以存储为字符串。

Json 在关系数据库中不是很好。如果将 json 展开成列并存储在 db 中,这很好,但是将 json 作为 blob 存储比将其用作数据存档系统更重要。

不展开 json 并将其存储在单个列中可能有多种原因,但该决定将被视为 json 字段中的值不会用于任何查询(或值已经展开到列中)。

此外,大多数 json 处理(如果查询了字段)将在 sql 环境之外进行,因为 sql 不适合 json 处理.真正的问题变成了,我要把这个 json 存储在哪里,我是否只是让它作为平面文件,并在需要时通过其他系统查询它们 (spark/hive/etc)。

我同意您的数据库艺术家的意见,不要使用 RDBMS 进行归档。有更便宜的选择。此外,json blob 可能会变得很大,并且会随着时间的推移开始使数据库磁盘 space 陷入困境。

新 SQL 服务器提供处理 JSON 文本的功能。格式为 JSON 的信息可以作为文本存储在标准 SQL 服务器列中,并且 SQL 服务器提供可以从这些 JSON 对象中检索值的函数。

    DROP TABLE IF EXISTS Person

 CREATE TABLE Person 
 ( _id int identity constraint PK_JSON_ID primary key,
 value nvarchar(max)
 CONSTRAINT [Content should be formatted as JSON]
 CHECK ( ISJSON(value)>0 )
 )

这个简单的结构类似于您可以在 NoSQL 数据库(例如 Azure DocumentDB 或 MongoDB)中创建的标准 NoSQL 集合,您只需要有代表的键代表 JSON.

的 ID 和值

请注意,NVARCHAR 不仅仅是纯文本。 SQL服务器内置文本压缩机制,可以透明压缩存储在磁盘上的数据。压缩取决于语言,根据您的数据最高可达 50%(请参阅 UNICODE 压缩)。

SQL 服务器与其他普通 NoSQL 数据库的主要区别在于 SQL 服务器使您能够使用混合数据模型,您可以在其中存储多个 JSON同一“集合”中的对象并将它们与常规关系列组合。

举个例子,假设我们知道您集合中的每个人都有名字和姓氏,并且您可以将关于此人的一般信息存储为一个 JSON 对象,并且 phone numbers/email 将地址作为单独的对象。在 SQL Server 2016 中,我们无需任何额外语法即可轻松创建此结构:

DROP TABLE IF EXISTS Person

CREATE TABLE Person (

 PersonID int IDENTITY PRIMARY KEY,

 FirstName nvarchar(100) NOT NULL,

 LastName nvarchar(100) NOT NULL,

 AdditionalInfo nvarchar(max) NULL,

 PhoneNumbers nvarchar(max) NULL,

 EmailAddresses nvarchar(max) NULL
 CONSTRAINT [Email addresses must be formatted as JSON array]
 CHECK ( ISJSON(EmailAddresses)>0 )

 )

您可以在此“集合”中组织数据,而不是单个 JSON 对象。如果您不想显式检查每个 JSON 列的结构,则不需要在每个列上添加 JSON 检查约束(在此示例中,我仅在 EmailAddresses 列上添加了 CHECK 约束)。

如果将此结构与标准的 NoSQL 集合进行比较,您可能会注意到您可以更快地访问强类型数据(FirstName 和 LastName)。因此,此解决方案是混合模型的不错选择,您可以在混合模型中识别一些在所有对象中重复的信息,而其他可变信息可以存储为 JSON。这样,您就可以兼顾灵活性和性能。

如果将此结构与 Person table AdventureWorks 数据库的架构进行比较,您可能会注意到我们删除了许多相关的 table。

除了模式的简单性之外,与复杂的关系结构相比,您的数据访问操作也会更简单。现在您可以阅读单个 table,而不是加入多个 table。当您需要插入具有相关信息(电子邮件地址,phone 号码)的新人时,您可以在一个 table 中插入一条记录,而不是在 AdventureWorks Person table 中插入一条记录,以标识列查找将用于存储 phones、电子邮件地址等的外键。此外,在此模型中,您可以轻松删除单人行,而无需使用外键关系进行级联删除。

否SQL 数据库针对简单的读取、插入和删除操作进行了优化 – SQL Server 2016 使您能够在关系数据库中应用相同的逻辑。

JSON 约束条件 在前面的示例中,我们已经了解了如何添加简单的约束来验证存储在列中的文本格式是否正确。尽管 JSON 没有强大的模式,您也可以通过组合从 JSON 读取值的函数和标准 T-SQL 函数来添加复杂的约束:

ALTER TABLE Person
 ADD CONSTRAINT [Age should be number]
 CHECK ( ISNUMERIC(JSON_VALUE(value, '$.age'))>0 )

 ALTER TABLE Person
 ADD CONSTRAINT [Person should have skills]
 CHECK ( JSON_QUERY(value, '$.skills') IS NOT NULL)
First constraint will take the value of $.age property and check is this numeric value. Second constraint will try to find JSON object in $.skills property and verify that it exists. The following INSERT statements will fail due to the violation of constraints:



INSERT INTO Person(value)
 VALUES ('{"age": "not a number", "skills":[]}')

 INSERT INTO Person(value)
 VALUES ('{"age": 35}')

请注意,CHECK 约束可能会减慢您的 insert/update 进程,因此如果您需要更快的写入性能,您可以避免使用它们。

压缩JSON存储 如果您有大 JSON 文本,您可以使用内置的 COMPRESS 函数显式压缩 JSON 文本。在下面的示例中,压缩的 JSON 内容存储为二进制数据,我们使用 DECOMPRESS 函数计算了将 JSON 解压为原始文本的列:

CREATE TABLE Person

 ( _id int identity constraint PK_JSON_ID primary key,

 data varbinary(max),

 value AS CAST(DECOMPRESS(data) AS nvarchar(max))

 )



 INSERT INTO Person(data)

 VALUES (COMPRESS(@json))

COMPRESS 和 DECOMPRESS 函数使用标准 GZip 压缩。如果您的客户端可以处理 GZip 压缩(例如理解 gzip 内容的浏览器),您可以直接 return 压缩内容。请注意,这是 performance/storage 权衡。如果您经常查询压缩数据,则性能会降低,因为每次都必须解压缩文本。

注意:JSON 函数仅在 SQL Server 2016+ 和 Azure SQL 数据库中可用。

更多内容可从本文来源阅读

https://blogs.msdn.microsoft.com/sqlserverstorageengine/2015/11/23/storing-json-in-sql-server/

PostgreSQL 有内置的 jsonjsonb 数据类型

下面是几个例子:

CREATE TABLE orders (
 ID serial NOT NULL PRIMARY KEY,
 info json NOT NULL
);

INSERT INTO orders (info)
VALUES
 (
 '{ "customer": "Lily Bush", "items": {"product": "Diaper","qty": 24}}'
 ),
 (
 '{ "customer": "Josh William", "items": {"product": "Toy Car","qty": 1}}'
 ),
 (
 '{ "customer": "Mary Clark", "items": {"product": "Toy Train","qty": 2}}'
 );

PostgreSQL 提供了两个本机运算符 ->->> 来查询 JSON 数据。

operator -> returns JSON object field by key.

运算符 ->> returns JSON object field by text.

SELECT
 info -> 'customer' AS customer
FROM
 orders;

SELECT
 info ->> 'customer' AS customer
FROM
 orders
WHERE
 info -> 'items' ->> 'product' = 'Diaper'