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

问题描述 投票:45回答:8

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

事情是加载和保存JSON数据有时更简单。并使开发更容易。

是否有任何“黄金规则”用于在DB中保存原始JSON数据?

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

摘要

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

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

感谢上帝,大家好!

sql json xml normalization structured-data
8个回答
64
投票

主要问题是

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

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

UPDATE 2: More about performance...

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

用户@ mike123指向一个article on an official microsoft blog,它似乎在实验中证明,查询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和具有多个Product的相同XML(JSON数组与兄弟节点)
  • JSON和XML略有变化(10000个正在运行的数字)并插入表中。
  • 对两个表都有一个初始调用,以避免第一次调用偏差
  • 读取所有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

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

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

11
投票

这个评论太长了。

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

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

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

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

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


10
投票

我会挥动魔杖。噗!关于使用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并不关心。

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


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 )
 )

这个简单的结构类似于您可以在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 )

 )

您可以在此“集合”中组织数据,而不是单个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}')

请注意,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))

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/


4
投票

我使用的“黄金法则”是一种手动的方式,如果我需要原始格式的JSON,那么可以存储。如果我必须特别指出解析它,那就不是了。

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


4
投票

你要问的问题是:

我只使用这个数据库吗?

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

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

2
投票

杰森在关系数据库方面并不出色。如果你将json展开成列并存储在数据库中,那就很好了,但是将json存储为blob就是将它用作数据存档系统。

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

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

我同意你的DB艺术家,不要使用RDBMS进行存档。有更便宜的选择。此外,json blob可能会变得庞大并且可能会随着时间的推移开始在数据库磁盘空间中进行打包。


0
投票

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数据。

运算符->按键返回JSON对象字段。

运算符->>按文本返回JSON对象字段。

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

SELECT
 info ->> 'customer' AS customer
FROM
 orders
WHERE
 info -> 'items' ->> 'product' = 'Diaper'
© www.soinside.com 2019 - 2024. All rights reserved.