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


70

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

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

是否有用于JSON在数据库中保存原始数据的“黄金法则” ?

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


2
我想象过查询JSON本身内的特定属性可能会导致瓶颈。如果在JSON中需要查询某些特定字段,则它们可能是提取到其自己列中的候选字段。有些数据库甚至具有“ json”数据类型,尽管我不知道使用该类型可以进行哪种优化。
Kritner '17

感谢您提出这个问题。从NoSQL迁移到SQL DB时,这对我来说很困难,这为您省去了很多麻烦。
-L先生

Answers:


90

主要问题是

  • 您将如何处理这些数据?和
  • 您如何过滤/排序/合并/处理此数据?

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

XML / JSON和1.NF

规范化的第一条规则规定,永远不要将多于一位的信息存储到一列中。您看到带有“ Mickey Mouse”之类的值的“ PersonName”列吗?您指向此并哭了:立即更改!

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(请参阅UPDATE 2一节)。任何需要从JSON读取值的操作都将需要大量隐藏的字符串方法调用...

这对您意味着什么?

可爱的DB艺术家:-D知道,按原样存储JSON违反RDBM的通用原则。他知道,

  • JSON很可能会破坏1.NF
  • JSON可能会随时间变化(同一列,不同内容)。
  • JSON不易阅读,并且很难对其进行过滤/搜索/加入或排序。
  • 这样的操作会将相当多的额外负载转移到可怜的小型DB服务器上

有一些解决方法(取决于您所使用的RDBMS),但是大多数方法都无法按照您希望的方式工作...

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

  • 如果您不想使用存储JSON中的数据以进行昂贵的操作(过滤器/联接/排序)。
    您可以像存储任何其他仅存在的内容一样存储它。我们将许多图片存储为BLOB,但是我们不会尝试过滤所有带有花朵的图片...
  • 如果您完全不打扰里面的内容(只需将其存储并作为一小部分信息阅读)
  • 如果结构是可变的,这将使创建物理表变得更加困难,然后将其与JSON数据一起使用。
  • 如果该结构是深层嵌套的,则物理表中的存储将产生大量开销

没有

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

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

更新

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

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

以下内容解决了SQL-Server 2016中的JSON和XML支持

用户@ mike123指向微软官方博客上一篇文章,该文章似乎在实验中得到了证明,与在SQL Server中查询XML相比查询JSON的速度快10倍

关于此的一些想法:

与“实验”进行一些核对:

  • “实验”的措施很多,但不是XML与JSON的性能。反复对相同(不变)的字符串重复进行相同操作是不现实的情况
  • 一般而言,经过测试的示例非常简单
  • 读取的值始终相同,甚至不使用。优化器将看到此...
  • 关于强大的XQuery支持一言不发!在数组中找到具有给定ID的产品?JSON需要读取全部内容,并在使用之后使用过滤器WHERE,而XML允许使用内部过滤器XQuery predicate。不说FLWOR...
  • 在“实验”的代码作为是我的系统上带来了:JSON似乎更快(但不是10倍)为3倍。
  • 添加/text()到会将其XPath减少到小于2x。在相关文章中,用户“ Magento先生”已经指出了这一点,但是点击诱饵的标题仍然没有改变。
  • 使用“实验”中提供的简单JSON,最快的纯T-SQL方法是SUBSTRINGand CHARINDEX:-D的组合

以下代码将显示更实际的实验

  • 使用一个JSON和一个以上具有多个XML的相同XML Product(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、8GB Ram上的SQL Server 2016 Express)

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


您可以在DATEDIFF中放入纳秒而不是毫秒吗?
Jovan MSFT

@JovanMSFT确保:json:2281502100和xml:1296990300。这意味着XML的速度几乎快一倍...
Shnugo

您正在使用什么版本的2016、2017?在SQL 2017 Express上,我得到的数字越来越接近:json 1918864000 xml 1807237200
Jovan MSFT

@JovanMSFT如上所述,我为此使用了SQL Server 2016 Express。我刚刚在每个数组/同级节点中重复了10个条目,并获取了第9个条目。Xml的速度是后者的两倍以上。现在,您自己已经有了接近的数字(前面是XML),但是您的博客的呼喊速度仍然比XML快10倍。我的评论甚至没有出现?如果ProductID=1234存在带有JSON的产品,您是否尝试搜索成千上万的JSON ?您是否尝试操纵JSON?您是否尝试过使用更深层的嵌套层次结构?
Shnugo

13

这个评论太长了。

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

一种明显的情况是,当JSON结构只是传递给应用程序的BLOB时。然后就没有争议了-除了存储JSON的开销外,对于在每个记录中具有公共字段的结构化数据,这是不必要的冗长。

另一种情况是“稀疏”列情况。您的行中包含许多可能的列,但是各行的行数不同。

另一种情况是您要将“嵌套”记录存储在记录中。JSON功能强大。

如果JSON在要查询的记录之间具有公共字段,那么通常最好将它们放在适当的数据库列中。但是,数据很复杂,并且存在诸如JSON之类的格式的地方。


12

我会挥舞我的魔杖。of!使用JSON的黄金法则:

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

  • 如果要搜索内部数据,并且具有MariaDB 10.0.1或MySQL 5.7(具有JSON数据类型和函数),则JSON可能是实用的。MariaDB 5.3的“动态”列是此的变体。

  • 如果您正在执行“ Entity-Attribute-Value”(实体-属性-值)工作,那么JSON不好,但是它是最少的弊端。 http://mysql.rjweb.org/doc.php/eav

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

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

  • 对于WHERE a=1 AND b=2“复合”索引来说INDEX(a,b)是很棒的;JSON可能无法接近。

  • JSON适用于“稀疏”数据;INDEXing可以这样做,但效果不佳。(我指的是许多行的“丢失”或NULL值。)

  • JSON可以为您提供“数组”和“树”,而无需使用额外的表。但是只能在应用程序中而不是在SQL中挖掘此类数组/树。

  • JSON比XML更好。(我的意见)

  • 如果您不希望从应用程序中获取JSON字符串,那么我建议将其存储(在客户端中)压缩为BLOB。可以将其想象为.jpg,尽管里面有东西,但是SQL不在乎。

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


子弹很好,如果您可以做出一个独特的“何时”和“何时不”可以使情况变得更好

1
@levi-是的,但是有一些不是绝对的。而是取决于具体情况。
里克·詹姆斯

2
JSON比XML更好。(我的观点)好吧,JSON的字符更少...您能用JSON做些什么,而不能用XML做些什么?最重要的部分是:如何处理这种类型?使用字符串方法解析XML或JSON会很麻烦。将结构转换为对象树将允许更好的方法。SQL Server存储在XML树本身,而是JSON会- AFAIK -生活在一个串...为什么你喜欢JSON作为世界更好
Shnugo

2
@Shnugo-更易于阅读,更短,基本上是一种明确的表示数组的方式。(XML有几种,其中大多数都可以通过复制密钥或其他方式被滥用。)Hash的同上。这使得直接与大多数计算机语言进行映射。(是的,这是我的“意见”。)
Rick James

@RickJames我喜欢说的是“ JSON没有类”(就编程语言中的类而言)-非常适合完全泛型的列表和哈希,但是如果您想定义特定的列表和散列,则会立即变得更加复杂自定义数据结构。在DB的上下文中,如果输入是XML,则XML(如果支持)显然会更好,而不是以某种方式将其转换为JSON(人们这样做,结果永远不会很漂亮)。
IMSoP

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集合进行比较,您可能会注意到,您将可以更快地访问强类型数据(名字和姓氏)。因此,此解决方案是混合模型的理想选择,在混合模型中,您可以识别在所有对象之间重复的某些信息,而其他变量信息可以存储为JSON。这样,您可以将灵活性和性能结合在一起。

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

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

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内容的浏览器),则可以直接返回压缩的内容。注意,这是性能/存储权衡。如果您经常查询压缩数据,则迁移的性能会降低,因为每次都必须对文本进行解压缩。

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

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

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


5

我用一种简单的方式使用的“黄金法则”是,如果我需要原始格式的JSON,则可以存储。如果我必须特别分析它,那就不是。

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


4

您要问的问题是:

我是否只能使用此数据库?

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

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

这是我想要的结构,干净简洁。但是它不包含其他一些答案所解决的一些重要问题。如果可以添加其他情况,那就更好了

2

Json在关系数据库方面并不出色。如果将json展开为列并存储在db中,那很好,但是将json存储为blob就是将其用作数据归档系统。

没有展开json并将其存储在单个列中可能有多种原因,但是由于该json字段中的值将不会用于任何查询(或者这些值已经展开为列),因此将做出决定。

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

我同意您的数据库艺术家的意见,请勿将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'
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.