当使用SQL
或MySQL
(或任何关系数据库)时 - 我知道在常规列中保存数据更适合索引和其他目的......
事情是加载和保存JSON
数据有时更简单。并使开发更容易。
是否有任何“黄金规则”用于在DB中保存原始JSON
数据?
这样做是绝对错误的做法吗?
给出了非常好的答案,但毫无疑问,最有组织的是@Shnugo给出的答案,值得赏心悦目。
还想指出@Gordon Linoff和@Amresh Pandey给出的解释其他特殊用例的答案。
感谢上帝,大家好!
主要问题是
JSON(与XML一样)非常适合数据交换,小型存储和一般定义的结构,但它不能参与您在RDBMS中运行的典型操作。在大多数情况下,最好将JSON数据传输到普通表中,并在需要时重新创建JSON。
规范化的第一条规则规定,永远不要将多于一位的信息存储到一列中。您会看到一个“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的共同原则的。他知道,
有一些解决方法(取决于您使用的RDBMS),但其中大多数不按照您喜欢的方式工作...
是
没有
您可以从字符串列中的JSON或BLOB开始,并在需要时将其更改为物理表。我的魔法水晶球告诉我,这可能是明天:-D
在这里找到关于性能和磁盘空间的一些想法:https://stackoverflow.com/a/47408528/5089204
以下内容涉及SQL-Server 2016中的JSON和XML支持
用户@ mike123指向一个article on an official microsoft blog,它似乎在实验中证明,查询JSON比在SQL-Server中查询XML要快10倍。
一些想法:
一些与“实验”的交叉检查:
XQuery
支持,没有一个字!在阵列中查找具有给定ID的产品? JSON需要阅读整个批次,然后使用WHERE
使用过滤器,而XML
将允许内部XQuery predicate
。不要谈论FLWOR
....../text()
添加到XPath
会将其减少到不到2倍。在相关文章中,用户“Mister Magoo”已经指出了这一点,但点击诱饵标题仍未改变......SUBSTRING
和CHARINDEX
的组合:-D以下代码将显示更逼真的实验
Product
的相同XML(JSON数组与兄弟节点)GO 10
将在此块中运行十次以避免第一次调用偏差最后的结果清楚地表明,JSON比XML慢(不是那么多,在一个非常简单的例子中大约是1.5倍)。
最后声明:
测试代码
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
这个评论太长了。
如果它“绝对错误”,那么大多数数据库都不会支持它。好的,大多数数据库都支持FROM
子句中的逗号,我认为它是“绝对错误的”。但是对JSON的支持是新的开发,而不是向后兼容的“功能”。
一个明显的例子是JSON结构只是一个传递回应用程序的BLOB。然后没有争论 - 除了存储JSON的开销之外,对于每个记录中具有公共字段的结构化数据而言,这是不必要的冗长。
另一种情况是“稀疏”列案例。您有许多可能列的行,但这些行因行而异。
另一种情况是您希望将“嵌套”记录存储在记录中。 JSON很强大。
如果JSON在您要查询的记录中具有公共字段,那么通常最好将它们放在适当的数据库列中。但是,数据很复杂,JSON等格式也存在。
我会挥动魔杖。噗!关于使用JSON的黄金规则:
FULLTEXT
搜索或SPATIAL
上的范围进行搜索,JSON是不可能的。WHERE a=1 AND b=2
来说,“复合”指数INDEX(a,b)
很棒;可能无法接近JSON。BLOB
。把它想象成一个.jpg - 那里有东西,但SQL并不关心。陈述您的申请;也许我们可以更具体。
新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/
我使用的“黄金法则”是一种手动的方式,如果我需要原始格式的JSON,那么可以存储。如果我必须特别指出解析它,那就不是了。
例如,如果我正在创建一个发送原始JSON的API,并且无论出于何种原因这个值都不会改变,那么可以将其存储为原始JSON。如果我必须解析它,更改它,更新它等等...然后不是那么多。
你要问的问题是:
我只使用这个数据库吗?
做
别
杰森在关系数据库方面并不出色。如果你将json展开成列并存储在数据库中,那就很好了,但是将json存储为blob就是将它用作数据存档系统。
可能有几个原因导致不展开json并将其存储在单个列中,但决定是因为json字段中的值不会用于任何查询(或者值已经展开到列中)。
此外,如果在所有字段中查询的大多数json处理将在sql环境之外,因为sql不适用于json处理。真正的问题就变成了,我在哪里存储这个json,我只是让它成为平面文件,并在需要时通过其他系统(spark / hive / etc)查询它们。
我同意你的DB艺术家,不要使用RDBMS进行存档。有更便宜的选择。此外,json blob可能会变得庞大并且可能会随着时间的推移开始在数据库磁盘空间中进行打包。
PostgreSQL有一个内置的json
和jsonb
数据类型
这些是一些例子:
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'