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

lev*_*evi 51 xml sql json normalization structured-data

当使用SQLMySQL(或任何关系数据库)时 - 我知道将数据保存在常规列中更适合索引和其他目的......

加载和保存JSON数据有时候要简单得多.并使开发更容易.

是否存在用于JSON在DB中保存原始数据的"黄金规则" ?

这样做是绝对错误的做法吗?

摘要

给出了非常好的答案,但毫无疑问,最有组织的是@Shnugo给出的答案,值得赏心悦目.

还想指出@Gordon Linoff和@Amresh Pandey给出的解释其他特殊用例的答案.

感谢上帝,大家好!

Shn*_*ugo 67

主要问题是

  • 你打算怎么处理这些数据?和
  • 你是如何过滤/排序/加入/操纵这些数据的?

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

XML/JSON和1.NF

规范化的第一条规则规定,永远不要将多于一位的信息存储到一列中.您会看到一个"PersonName"列,其值为"Mickey Mouse"?你指着这个并哭泣:马上改变它!

那么XML或JSON呢?这些类型是否打破1.NF?嗯,是的,不...... 

如果它实际上是一位信息,那么将完整的结构存储为一位信息是完全可以的.您获得SOAP响应并希望存储它,因为您可能需要它以供将来参考(但您不会将此数据用于您自己的进程)?只是按原样存储!

现在想象一个代表一个人复杂结构(XML或JSON)(其地址,更多细节......).现在你把它放在一列中PersonInCharge.这是错的吗?这不应该生活在具有外键引用而不是XML/JSON的正确设计的相关表中吗?特别是如果同一个人可能出现在许多不同的行中,那么使用XML/JSON方法肯定是错误的.

但现在想象存储历史数据的必要性.您希望在特定时刻保留此人的数据.几天后,这个人会告诉你一个新地址?没问题!如果您需要,旧地址将存在于XML/JSON中...

结论:如果您存储数据只是为了保留它,那没关系.如果这个数据是一个独特的部分,那就没问题......
但如果你需要定期内部部件,或者如果这意味着冗余的重复存储,那就不行了......

物理存储

以下内容适用于SQL Server,可能与其他RDBM不同.

XML不会存储为您看到的文本,而是存储为层次结构树.查询这是令人惊讶的表现!此结构未在字符串级别进行解析!
SQL Server(2016+)中的JSON存在于字符串中,必须进行解析.没有真正的本机JSON类型(就像有一个原生的XML类型).这可能会在以后发生,但是现在我假设,JSON在SQL Server上的性能不如XML(参见更新2部分).任何需要读取JSON的值都需要大量的隐藏字符串方法调用...

这对你意味着什么?

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

  • JSON很可能会破坏1.NF
  • JSON可能会及时更改(相同的列,不同的内容).
  • JSON不容易阅读,并且很难过滤/搜索/加入或排序.
  • 这样的操作会将一些额外的负载转移到可怜的小型DB服务器上

有一些解决方法(取决于您使用的RDBMS),但其中大多数不按照您喜欢的方式工作...

简而言之,你的问题的答案

  • 如果您不想使用存储 JSON中的数据进行昂贵的操作(过滤/加入/排序).
    任何其他你可以存储这个仅存的内容.我们将许多图片存储为BLOB,但我们不会尝试使用花朵过滤所有图像...
  • 如果你不打扰内部的东西(只是存储它并将其作为一点信息阅读)
  • 如果结构是可变的,这将使创建物理表变得更加困难,然后使用JSON数据.
  • 如果结构是深层嵌套的,那么物理表中的存储会产生很大的开销

没有

  • 如果你想使用内部数据,就像你使用关系表的数据(过滤器,索引,连接......)
  • 如果要存储重复项(创建冗余)
  • 一般来说:如果您遇到性能问题(肯定会在许多典型情况下面对它们!)

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

UPDATE

在此处查找有关性能和磁盘空间的一些想法:https://stackoverflow.com/a/47408528/5089204

更新2:更多关于表现......

以下内容涉及SQL-Server 2016中的JSON和XML支持

用户@ mike123指出官方微软博客上一篇文章似乎在实验中证明,查询JSON 比在SQL-Server中查询XML 要快10倍.

一些想法:

一些与"实验"的交叉检查:

  • "实验"的措施很多,但不是XML与JSON的性能.执行相同的操作重复相同(未更改)的字符串不是一个现实的情况
  • 对于一般性陈述,测试的例子非常简单!
  • 读取的值始终相同,甚至不使用.优化器会看到这个......
  • 关于强大XQuery支持的一句话!在阵列中查找具有给定ID的产品?JSON需要读取整个批次并在之后使用过滤器WHERE,而XML允许内部使用XQuery predicate.不要谈论FLWOR......
  • 在"实验"的代码作为是我的系统上带来了:JSON似乎是快3倍(而不是10倍).
  • 添加/text()XPath减少到不到2倍.在相关文章中,用户"Mister Magoo"已经指出了这一点,但点击诱饵标题仍未改变......
  • 使用"实验"中给出的这样一个简单的JSON,最快的纯T-SQL方法是SUBSTRINGCHARINDEX:-D 的组合

以下代码将显示更逼真的实验

  • 使用JSON和具有多个的相同XML Product(JSON数组与兄弟节点)
  • JSON和XML略有变化(10000个正在运行的数字)并插入表中.
  • 有一个初始调用agaist两个表,以避免第一次呼叫偏差
  • 读取所有10000个条目,并将检索到的值插入另一个表.
  • 使用GO 10将遍历此块十次以避免第一次调用偏差

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

最后声明:

  • 在不适当的情况下使用过于简化的示例JSON可以比XML更快
  • 处理JSON是纯字符串操作,而XML则被解析和转换.这在第一次行动中相当昂贵,但一旦完成,将加快一切.
  • JSON在一次性操作中可能更好(避免创建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
Run Code Online (Sandbox Code Playgroud)

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

Test    SumTime 
------------------
json    2706    
xml     1604    
Run Code Online (Sandbox Code Playgroud)


Gor*_*off 12

这个评论太长了.

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

一个明显的例子是JSON结构只是一个传递回应用程序的BLOB.然后没有争论 - 除了存储JSON的开销之外,对于每个记录中具有公共字段的结构化数据而言,这是不必要的冗长.

另一种情况是"稀疏"列案例.您有许多可能列的行,但这些行因行而异.

另一种情况是您希望将"嵌套"记录存储在记录中.JSON很强大.

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


Ric*_*mes 11

我会挥动魔杖.噗!关于使用JSON的黄金规则:

  • 如果MySQL不需要看里面的JSON,以及应用程序只需要的东西的集合,那么JSON是罚款,甚至可能更好.

  • 如果你对里面的数据进行搜索你有MariaDB的10.0.1或MySQL 5.7(用JSON数据类型和函数),那么JSON 可能是可行的.MariaDB 5.3的"动态"列是此类的变体.

  • 如果你正在做"实体 - 属性 - 值"的东西,那么JSON并不好,但它是几个邪恶中最少的. http://mysql.rjweb.org/doc.php/eav

  • 对于通过索引列进行搜索,没有将值隐藏在JSON中是一个很大的优点.

  • 要按索引列上的范围进行FULLTEXT搜索,或搜索或SPATIAL,则无法进行JSON.

  • 因为WHERE a=1 AND b=2"复合"指数INDEX(a,b)很好; 可能无法接近JSON.

  • JSON适用于"稀疏"数据; 索引工作,但不是这样.(我指的是'缺少'的值或许多行的NULL.)

  • JSON可以为您提供"数组"和"树",而无需使用额外的表.但是在应用程序中挖掘这样的数组/树,而不是在SQL中.

  • JSON比XML更好.(我的看法)

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

陈述您的申请; 也许我们可以更具体.

  • @levi - 是的,但有几个并不是绝对要/不要;相反,它们取决于情况的细节。 (2认同)
  • *JSON比XML更好.(我的意见)*嗯,JSON是更少的字符...你可以用JSON做什么,你不能用XML做什么?最重要的部分是:**如何处理这种类型?**使用字符串方法解析XML或JSON将是一个痛苦的问题.将结构转换为对象树将允许更好的方法.SQL Server本地将XML存储在树中,但JSON将 - AFAIK - 存在于字符串中...为什么您更喜欢JSON作为**世界更好**? (2认同)
  • @Shnugo - 更容易阅读,更短,基本上是一种,_unambiguous_方式来表示一个数组.(XML有几个,其中大多数可以通过复制密钥或其他任何东西来滥用.)同上Hash.这使得与大多数计算机语言的映射变得简单.(是的,这是我的"意见".) (2认同)

AMR*_*DEY 9

新SQL Server提供了处理JSON文本的功能.格式化为JSON的信息可以作为文本存储在标准SQL Server列中,SQL Server提供可以从这些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 )
 )
Run Code Online (Sandbox Code Playgroud)

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

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

SQL Server和其他普通NoSQL数据库之间的主要区别在于SQL Server使您能够使用混合数据模型,您可以在同一"集合"中存储多个JSON对象,并将它们与常规关系列组合在一起.

例如,假设我们知道集合中的每个人都有FirstName和LastName,并且您可以将有关此人的一般信息存储为一个JSON对象,并将电话号码/电子邮件地址存储为单独的对象.在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 )

 )
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

NoSQL数据库针对简单,读取,插入和删除操作进行了优化 - 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}')
Run Code Online (Sandbox Code Playgroud)

请注意,CHECK约束可能会降低插入/更新过程的速度,因此如果需要更快的写入性能,可以避免使用它们.

压缩的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))
Run Code Online (Sandbox Code Playgroud)

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

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

可以从本文的来源中读到更多内容

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


pii*_*ly3 5

我使用的“黄金法则”,在某种程度上,是如果我需要原始格式的 JSON,则可以存储。如果我必须特别强调解析它,那么它不是。

例如,如果我正在创建一个发送原始 JSON 的 API,并且无论出于何种原因该值不会改变,那么可以将其存储为原始 JSON。如果我必须解析它,更改它,更新它等等......那么不是那么多。