MySQL 中 JSON 欄位的使用技巧

Max發表於2018-12-12

mysql5.7.8之後開始原生支援json. 在類似mongodb這種nosql資料庫中,json儲存資料是非常自然的, 在mysql中合理的使用json,能夠帶來極大的便利

Json欄位的使用場景

在讀laravel手冊舉例子時,我們經常會看到 $user->is_admin 來判斷使用者是否為管理員,但是在使用者表中,admin往往只佔很小一部分.如果單開一個is_admin欄位是很沒有必要的行為.資料庫中會有大量的無意義資料儲存, 我們可以為user表建立一個 json 欄位,來儲存我們的is_admin欄位

[
    {
        id: 1,
        username: 'weiwenhao',
        rest: { // 冗餘欄位
            is_admin: 1
        }
    },
    {
        id: 2,
        username: 'eienao',
        rest: null
    }
]

當然即使不使用json,我們也不會使用is_admin來判斷是否為管理員.

可以通過新增admin表或者RABC來標誌管理員

依舊是使用者表, 很常見的一個需求是第三方登入,如果我們使直接在user表新增facebook_id,facebook_email,facebook_phone_number,google_id,....欄位, 可以預見這會造成大量的無意義資料(即使他們不佔用記憶體,或者影響效能)

一種解決辦法是 使用一對多關係來解決, 既建立一個 第三方登入表來儲存第三方登入的id/email/phone_number等

但是我更喜歡使用json欄位來解決這個問題

[
   {
        id: 1,
        username: 'weiwenhao',
        rest: {
            is_admin: 1,
            facebook_id: 2348234,
            facebook_phone_number: 2834723234,
        }
    },
    {
        id: 2,
        username: 'eienao',
        rest: {
            google_id: 2348234,
            google_email: xxx@gmail.com
        }
    }
]

可以看出,使用json欄位使資料表的設計更加自然,集中,業務也相應的更加的簡單方便.

Json欄位在laravel中的使用

首先是遷移檔案 $table->json('rest')->nullable();

laravel對json的使用進行了一定的優化,對於更新和建立我們可以.

$user = new User;
$user->{'rest->google_id'} = 'xxx'; 
# 如果你的rest欄位為null,那麼上面的操作會使 null 會變成 {google_id: "xxx"}, 不需要再做 是否為null的判定啦
# 如果僅使用上面的插入操作,也不需要在使用模型的修改器來吧 json => array, array => json啦

當rest欄位的值為null時,批量操作無法執行, 類似 update(['rest->google_id' => 'xxx']) 這樣的操作執行無效,因此更推薦上面的方式來進行更新操作

對於查詢操作可以方便的使用

User::where('rest->google_id','xxx')->firstOrFail()

關於檢索的效率問題,在後面內容中給出解決方案

Generated Column (生成列)

5.7新增了生成列, 生成列的值是根據列定義中包含的表示式計算得來.官方示例:計算直角三角形的斜邊的長度

CREATE TABLE triangle (
  sidea DOUBLE,
  sideb DOUBLE,
  sidec DOUBLE AS (SQRT(sidea * sidea + sideb * sideb)) # AS (expression) 為生成列的核心語法
);
INSERT INTO triangle (sidea, sideb) VALUES(1,1),(3,4),(6,8);

# 對於上面的插入,查詢可以得到如下結果

mysql> SELECT * FROM triangle;
+-------+-------+--------------------+
| sidea | sideb | sidec              |
+-------+-------+--------------------+
|     1 |     1 | 1.4142135623730951 |
|     3 |     4 |                  5 |
|     6 |     8 |                 10 |
+-------+-------+--------------------+

上面的 sidec的值 是根據sidea和sideb計算得來, 並未實際的儲存在磁碟中.mysql5.7之前我們想要實現上面的需求可能會這樣寫sql語句

SELECT *,(SQRT(sidea * sidea + sideb * sideb)) as sidec FROM triangle;

上面既生成列的主要作用, 實際上生成列有兩種子型別,上面的例子屬於 virtual (虛擬) 型別的生成列, 其並沒有將sidec的值實際儲存在磁碟中.

除了virtual, 生成列還支援 stored型別,其建立語句為

#...
sidec DOUBLE AS (SQRT(sidea * sidea + sideb * sideb)) STORED # stored不指定則預設為 virtual
#...

當行建立或者更新時, 會重新計算 sidec並將其儲存在磁碟中

生成列的另一個重要的特性是可以根據生成列表示式的計算結果建立索引. 其建立索引的方式和普通欄位建立索引的方式一致.1

CREATE TABLE triangle (
  sidea DOUBLE,
  sideb DOUBLE,
  sidec DOUBLE AS (SQRT(sidea * sidea + sideb * sideb)) # AS (expression) 為生成列的核心語法
  INDEX(`sidec`)
);

索引本身也是儲存在磁碟中的實際存在的物質, 因此 virtual 生成列 + 索引,可以達到儲存空間的最有效利用.

對於stored 生成列 + 索引, 通常不會訪問到儲存在磁碟中stored 生成列,而是直接訪問索引.因此沒有必要使用stored生成列

使用生成列為json中的欄位新增索引

已user表的rest.google_id為例,建表操作

#...
`rest` json NULL,

# JSON_EXTRACT(`rest`,'$.google_id') 等價於 `rest`->'$.google_id'
# 5.7.13版本後 
# JSON_UNQUOTE(JSON_EXTRACT(`rest`,'$.google_id')) 等價於 `rest`->>'$.google_id'
# 使用生成列為json新增索引時,請務必使用 JSON_UNQUOTE(JSON_EXTRACT(`rest`,'$.google_id'))/->>
`google_id` varchar GENERATED ALWAYS AS (`rest`->>'$.google_id')) NULL

UNIQUE INDEX(`google_id`)
#...

在laravel遷移檔案中

$table->json('rest')->nullable();

$table->string('google_id')->nullable()->unique()->virtualAs('`oauth`->>"$.google_id"');

有了索引後,當我們執行查詢操作

select * from users where `rest`->'$.google_id' = 'xxx' # 通常使用這種更加簡單的形式
select * from users where `rest`->>'$.google_id' = 'xxx'

# 上面兩種表示式會被mysql的優化器在查詢階段自動優化為 select * from users where google_id = 'xxx'

一些補充

  • virtualAs(oauth->"$.google_id"'); 使用 ->符號來建立生成列會出現無法使用索引的情況, 原因不是很明瞭,需要繼續研究一下手冊. 另外對於建立語句 GENERATED ALWAYS的作用也不是很明瞭.

  • 關於null, 經常會看到一種言論是mysql中使用null作為欄位預設值會出現無法索引的情況.但經過查詢瞭解,發現這是一種老中醫理論. 我更傾向於使用null作為預設值, 而不是 ''/0/0.0 ,我認為null的表達性更好, laravel中也無時無刻不在提現這種思想.
  • 關於json的使用, 最近的專案中,我大部分核心表都有一個json欄位,做一些非核心資料的儲存和冗餘.

相關文章