MySQL5.7 JSON實現簡介

版權聲明:本文由吳雙橋原創文章,轉載請註明出處: 
文章原文連接:https://www.qcloud.com/community/article/205html

來源:騰雲閣 https://www.qcloud.com/communitymysql

 

本文主要介紹在MySQL 5.7.7開始引入的非結構化數據類型JSON的特性以及具體的實現方式(包括存儲方式)。首先介紹爲何要引入JSON的原生數據類型的支持;接着介紹MySQL給用戶提供的JSON操做函數,以及JSON路徑表達式語法,結合二者,用戶能夠在數據庫級別操做JSON的任意鍵值和數據;以後,重點介紹JSON在服務器側的存儲結構,這也是深刻理解不少其餘JSON特性的根基;在最後介紹JSON做爲新數據類型的比較與排序規則以前,介紹了對JSON類型數據創建索引的原理。sql

爲何JSON的原生支持

  1. 文檔合法性
    在MySQL5.7.7對JSON提供原生類型的支持以前,用戶能夠用TEXT或者BLOB類型來存儲JSON文檔。但對於MySQL來講,用戶插入的數據只是序列化後的一個普通的字符串,不會對JSON文檔自己的語法合法性作檢查,文檔的合法性須要用戶本身保證。在引入新的JSON類型以後,插入語法錯誤的JSON文檔,MySQL會提示錯誤,並在插入以後作歸一化處理,保證每個鍵對應一個值。數據庫

  2. 更有效的訪問
    MySQL 5.7.7+自己提供了不少原生的函數以及路徑表達式來方便用戶訪問JSON數據。例如對於下面的JSON文檔:
    { "a": [ [ 3, 2 ], [ { "c" : "d" }, 1 ] ], "b": { "c" : 6 }, "one potato": 7, "b.c" : 8 }json

    用戶可使用
    $.a[1][0]獲取{ "c" : "d" }
    $.a[1]獲取[ { "c" : "d" }, 1 ]
    還可使用通配符 * 和 ** 來進行模糊匹配,詳見下一段。數組

  3. 性能優化
    在MySQL提供JSON原生支持以前,若是用戶須要獲取或者修改某個JSON文檔的鍵值,須要把TEXT或者BLOB整個字符串讀出來反序列化成JSON對象,而後經過各類庫函數訪問JSON數據。顯然這樣是很是沒有效率的,特別是對較大的文檔。而原生JSON的性能,特別是讀性能很是好。根據Oracle公司針對200K+數據文檔作的性能測試代表,一樣的數據用TEXT和JSON類型的查詢性能差別達到兩個數量級以上,並且用戶還能夠對常常訪問的JSON鍵值作索引,進一步提高性能。JSON數據操做性能的提高是基於JSON數據自己的存儲結構的,下文會進一步介紹。性能優化

JSON的操做接口及路徑表達式

  1. JSON的操做接口
    根據MySQL官方文檔的介紹,服務器端JSON函數的實現須要知足如下條件:服務器

    Requirements:app

    1. Lets users construct JSON data values from other relational data.
    2. Lets users extract relational data from JSON data values.
    3. Lets users minimally introspect the structure of JSON values and text (validity, length, depth, keys).
    4. Works on strings which are utf8mb4 encoded.
    5. Performance should be suitable for read-intensive applications.

    Non-requirements:ide

    1. May produce surprising results on strings which are not utf8mb4 encoded.
    2. There is limited support for decimal values nested inside JSON documents.
    3. Performance may not be suitable for write-intensive applications.

    提供的函數列表具體爲:

    JSON_APPEND() JSON_ARRAY_INSERT() JSON_UNQUOTE() JSON_ARRAY()
    JSON_REPLACE() JSON_CONTAINS() JSON_DEPTH() JSON_EXTRACT()
    JSON_INSERT() JSON_KEYS() JSON_LENGTH() JSON_VALID()
    JSON_MERGE() JSON_OBJECT() JSON_QUOTE() JSON_REMOVE()
    JSON_CONTAINS_PATH() JSON_SEARCH() JSON_SET() JSON_TYPE()

    以上函數的調用規則大多形如:

    JSON_APPEND(json_doc, path, val[, path, val] ...)

    第一個參數json_doc爲JSON文檔,或者是表裏面的某一列,也能夠是JSON文檔裏面的嵌套子文檔變量;
    第二個參數path爲路徑表達式,用來定位要訪問的鍵,path(即路徑表達式)下面緊接着會介紹;
    第三個參數val有的函數可能沒有,如有表示鍵對應的操做數值。

  2. JSON路徑表達式
    爲了更方便快速的訪問JSON的鍵值,MySQL 5.7.7+提供了新的路徑表達式語法支持。前文提到的$.a[1][0]就是路徑表達式的一個具體的示例。完整的路徑表達式語法爲:

    pathExpression> ::= scope  [ ( pathLeg )* ]
    scope ::= [ columnReference ] dollarSign
    columnReference ::= [ [ databaseIdentifier period  ] tableIdentifier period ] columnIdentifier
    databaseIdentifier ::= sqlIdentifier
    tableIdentifier ::= sqlIdentifier
    columnIdentifier ::= sqlIdentifier
    pathLeg ::= member | arrayLocation | doubleAsterisk
    member ::= period ( keyName | asterisk )
    arrayLocation ::= leftBracket ( non-negative-integer | asterisk ) rightBracket
    keyName ::= ECMAScript-identifier | double-quoted-string-literal
    doubleAsterisk ::= **

    仍是以

    { "a": [ [ 3, 2 ], [ { "c" : "d" }, 1 ] ], "b": { "c" : 6 }, "one potato": 7, "b.c" : 8 }

    爲例,再舉幾個例子說明:

    $.a[1] 獲取的值爲 [ { "c" : "d" }, 1 ]
    $.b.c 獲取的值爲 6
    $."b.c" 獲取的值爲 8

    對比上面最後兩個例子,能夠看到用引號包圍的表達式會被看成一個字符串鍵值。

    關於通配符***來進行模糊匹配須要作進一步的說明。

    兩個連着星號**不能做爲表達式的結尾,不能出現連續的三個星號***
    單個星號*表示匹配某個JSON對象中全部的成員
    [*]表示匹配某個JSON數組中的全部元素
    prefix**suffix表示全部以prefix開始,以suffix結尾的路徑

    舉個具體的例子,直接在MySQL命令行裏面輸入:
    select json_extract('{ "a": [ [ 3, 2 ], [ { "c" : "d" }, 1 ] ], "b": { "c" : 6 }, "one potato": 7, "b.c" : 8 }','$**.c');

    獲得顯示結果:["d", 6]

JSON的存儲結構及具體實現

在處理JSON時,MySQL使用的utf8mb4字符集,utf8mb4是utf8和ascii的超集。因爲歷史緣由,這裏utf8並不是是咱們常說的UTF-8 Unicode變長編碼方案,而是MySQL自身定義的utf8編碼方案,最長爲三個字節。具體區別非本文重點,請你們自行Google瞭解。

MySQL在內存中是以DOM的形式表示JSON文檔,並且在MySQL解析某個具體的路徑表達式時,只須要反序列化和解析路徑上的對象,並且速度極快。要弄清楚MySQL是如何作到這些的,咱們就須要瞭解JSON在硬盤上的存儲結構。有個有趣的點是,JSON對象是BLOB的子類,在其基礎上作了特化。

根據MySQL官方文檔的表述:

On a high level, we will store the contents of the JSON document in three sections:

  • A table of pointers to all the keys and values, in the order in which the keys and values are stored. Each pointer contains information about where the data associated with the key or the value is located, as well as type information about the key or value pointed to.
    *All the keys. The keys are sorted, so that lookup can use binary search to locate the key quickly.

  • All the values, in the same order as their corresponding keys.
    If the document is an array, it has two sections only: the dictionary and the values.
    If the document is a scalar, it has a single section which contains the scalar value

咱們來使用示意圖更清晰的展現它的結構:

JSON文檔自己是層次化的結構,於是MySQL對JSON存儲也是層次化的。對於每一級對象,存儲的最前面爲存放當前對象的元素個數,以及總體佔的大小。須要注意的是:

  • JSON對象的Key索引(圖中橙色部分)都是排序好的,先按長度排序,長度相同的按照code point排序;Value索引(圖中黃色部分)根據對應的Key的位置依次排列,最後面真實的數據存儲(圖中白色部分)也是如此

  • Key和Value的索引對存儲了對象內的偏移和大小,單個索引的大小固定,能夠經過簡單的算術跳轉到距離爲N的索引

  • 經過MySQL5.7.16源代碼能夠看到,在序列化JSON文檔時,MySQL會動態檢測單個對象的大小,若是小於64KB使用兩個字節的偏移量,不然使用四個字節的偏移量,以節省空間。同時,動態檢查單個對象是不是大對象,會形成對大對象進行兩次解析,源代碼中也指出這是之後須要優化的點

  • 如今受索引中偏移量和存儲大小四個字節大小的限制,單個JSON文檔的大小不能超過4G;單個KEY的大小不能超過兩個字節,即64K

  • 索引存儲對象內的偏移是爲了方便移動,若是某個鍵值被改動,只用修改受影響對象總體的偏移量

  • 索引的大小如今是冗餘信息,由於經過相鄰偏移能夠簡單的獲得存儲大小,主要是爲了應對變長JSON對象值更新,若是長度變小,JSON文檔總體都不用移動,只須要當前對象修改大小

  • 如今MySQL對於變長大小的值沒有預留額外的空間,也就是說若是該值的長度變大,後面的存儲都要受到影響

  • 結合JSON的路徑表達式能夠知道,JSON的搜索操做只用反序列化路徑上涉及到的元素,速度很是快,實現了讀操做的高性能

  • 不過,MySQL對於大型文檔的變長鍵值的更新操做可能會變慢,可能並不適合寫密集的需求

JSON的索引

如今MySQL不支持對JSON列進行索引,官網文檔的說明是:

JSON columns cannot be indexed. You can work around this restriction by creating an index on a generated column that extracts a scalar value from the JSON column.

雖然不支持直接在JSON列上建索引,但MySQL規定,能夠首先使用路徑表達式對JSON文檔中的標量值創建虛擬列,而後在虛擬列上創建索引。這樣用戶可使用表達式對本身感興趣的鍵值創建索引。舉個具體的例子來講明:

CREATE TABLE features (
 id INT NOT NULL AUTO_INCREMENT,
 feature JSON NOT NULL,
 PRIMARY KEY (id)
);

插入它的JSON數據的格式爲:

{
   "type":"Feature",
   "properties":{
      "TO_ST":"0",
      "BLKLOT":"0001001",
      "STREET":"UNKNOWN",
      "FROM_ST":"0",
      "LOT_NUM":"001",
      "ST_TYPE":null,
      "ODD_EVEN":"E",
      "BLOCK_NUM":"0001",
      "MAPBLKLOT":"0001001"
   }
}

使用:

ALTER TABLE features ADD feature_street VARCHAR(30) AS (JSON_UNQUOTE(feature->"$.properties.STREET"));
ALTER TABLE features ADD INDEX (feature_street);

兩個步驟,能夠對feature列中properties鍵值下的STREET鍵(feature->"$.properties.STREET")建立索引。

其中,feature_street列就是新添加的虛擬列。之因此取名虛擬列,是由於與它對應的還有一個存儲列(stored column)。它們最大的區別爲虛擬列只修改數據庫的metadata,並不會存儲真實的數據在硬盤上,讀取過程也是實時計算的方式;而存儲列會把表達式的列存儲在硬盤上。二者使用的場景不同,默認狀況下經過表達式生成的列爲虛擬列。

這樣虛擬列的添加和刪除都會很是快,而在虛擬列上創建索引跟傳統的創建索引的方式並無區別,會提升虛擬列讀取的性能,減慢總體插入的性能。虛擬列的特性結合JSON的路徑表達式,能夠方便的爲用戶提供高效的鍵值索引功能。

JSON比較與排序

JSON值可使用=, <, <=, >, >=, <>, !=, <=>等操做符,BETWEENIN,GREATESTLEAST等操做符如今還不支持。JSON值使用的兩級排序規則,第一級基於JSON的類型,類型不一樣的使用每一個類型特有的排序規則。

JSON類型按照優先級從高到低爲

BLOB
BIT
OPAQUE
DATETIME
TIME
DATE
BOOLEAN
ARRAY
OBJECT
STRING
INTEGER, DOUBLE
NULL

優先級高的類型大,不用再進行其餘的比較操做;若是類型相同,每一個類型按本身的規則排序。具體的規則以下:

  1. BLOB/BIT/OPAQUE: 比較兩個值前N個字節,若是前N個字節相同,短的值小
  2. DATETIME/TIME/DATE: 按照所表示的時間點排序
  3. BOOLEAN: false小於true
  4. ARRAY: 兩個數組若是長度和在每一個位置的值相同時相等,若是不想等,取第一個不相同元素的排序結果,空元素最小
  5. OBJECT: 若是兩個對象有相同的KEY,而且KEY對應的VALUE也都相同,二者相等。不然,二者大小不等,但相對大小未規定。
  6. STRING: 取兩個STRING較短的那個長度爲N,比較兩個值utf8mb4編碼的前N個字節,較短的小,空值最小
  7. INTEGER/DOUBLE: 包括精確值和近似值的比較,稍微有點複雜,可能出現與直覺相悖的結果,具體參見官方文檔相關說明。

任何JSON值與SQL的NULL常量比較,獲得的結果是UNKNOWN。對於JSON值和非JSON值的比較,按照必定的規則將非JSON值轉化爲JSON值,而後按照以上的規則進行比較。

小結

本文主要介紹了MySQL在5.7.7以後引入的原生JSON支持的特性,說明了引入JSON類型的好處,並結合具體的示例介紹了MySQL在JSON類型上對外的接口以及引入的新語法規則。此外,還重點介紹了JSON在硬盤上的存儲結構,簡要分析了這種存儲結構的優點和不足。最後還介紹了JSON的索引原理,以及比較和排序規則。相信理解了本文介紹的內容,關於JSON文中沒有提到的部份內容也較容易理解。

相關文章
相關標籤/搜索