DynamoDB影響了我設計MySQL Primary Key的方式

tldr

DynamoDB 是一個需要仔細設計 Primary key 的資料庫
在開發程式之前,需要仔細考慮如何存取資料,以便精心設計 Primary key。

以前寫網頁不會仔細考慮 table 設計,隨便開一開就動手寫程式,
因為我覺得程式才是重點。

在學了 DynamoDB 之後覺得好的 table 設計,不僅讀取更有效率,
甚至可以少寫一點程式。

不過,這邊文章有錯誤

讀完之後可以參考 這篇文章


DynamoDB有什麼不一樣?

DynamoDB 是一個需要仔細設計Primary key的資料庫
可以很簡單的把它看著 Redis 那種 Key-Value 的資料庫。

作為 NoSQL 資料庫,雖然不用事先宣告欄位、也可以對每一個欄位做 SQL 的 Where 查找,
但會掃描整張 table,效率不好而且 DynamoDB 是按照掃描量做收費
所以設計 Primary Key就變成重要工作。

DynamoDB 中有 Query 與 Scan 兩種查詢方式,這邊說的是 Scan

原本寫Laravel時的開發習慣

每當接到新功能,就會要做的事情寫成一張一張便利貼貼在牆上,
其中一定會有一張『Table設計』這樣的便利貼,而且通常預估時間都是1小時。

而且心理預估甚至更快

Laravel裡面怎麼開table?

Laravel 把一次又一次對 table 的異動都寫成 PHP 程式碼,並特別稱作 Migration,
因為是程式碼,所以可以被版本控制

比如說我們有一個類似 FB 塗鴉牆的網站,我們需要做『對文章點讚的功能』。
接到工作之後當然是馬上把 table 開出來,

開table

先下指令來建立 Migration :

1
$ php artisan make:migration create_likes_table --create=likes

剛建立出來的 Migration 長這樣:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateLikesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('likes', function (Blueprint $table) {
$table->id(); // Unsigned integer 的 primary key
$table->timestamps();
});
}

/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('likes');
}
}

在Migration填上欄位

接著才思考我們的功能需要在上面放入什麼欄位…

那麼需要什麼欄位呢,先來看看需求:

  • User 需要登入,在登入之後、載入文章清單時需要一併取得 user 是否對本篇文章點過讚 (article_id, user_id)
  • 讀取速度要快

那麼在讀取文章清單時要怎麼下 SQL 查詢 user 是否按過讚呢?
恩…應該像這樣吧

1
2
3
SELECT `articles`.*, EXISTS(SELECT * FROM `likes` WHERE article_id = `articles`.id AND user_id = 1000) as is_liked
FROM `articles`
WHERE `articles`.id in (500);

因為 likes table 中會需要article_id要用來找個別文章的按讚紀錄,再用user_id確認 user 是否點過讚,
所以至少要有以上兩個欄位,而且articles_id要有 index。

另外如果有一篇文章很熱門、一萬人點過讚,
那麼資料庫從所有按讚紀錄中透過articles_id找出一篇文章的按讚紀錄後,
要在從這一萬筆紀錄之中找到這個user按讚的紀錄,那麼user_id應該也要 index…

那就來改一下 table 吧
(flag)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateLikesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('likes', function (Blueprint $table) {
$table->id(); // Unsigned integer 的 primary key
$table->unsignedBigInteger('article_id')->index();
$table->unsignedBigInteger('user_id')->index();
$table->timestamps();
});
}

/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('likes');
}
}

如此設計的原因

hmmm
我只能說,過去的我很習慣 Mysql 有可以 auto increment 的 primary key
所以通常會留著預設的$table->id()來用

應該量不大的話,這樣設計是不會發生什麼問題的,
但量太小的話,mysql也許連index都不會去用


如果用DynamoDB設計這個table…

關於主鍵

首先!
DynamoDB 是不支援 Auto increment 的 Primary key 的!

不過仔細一想,
一個 user 對一篇文章按讚之後,應該只會有一筆按讚紀錄,
所以每一筆按讚紀錄可以用 article_id 加上 user_id 作為 primary key

Dynamodb獨特的Primary Key設計

在 DynamoDB 中 Primary key 並不是一個欄位,而是一個或者兩個欄位。

一個欄位的PK

一個欄位的狀況是只有 Partition key,而且必須就是唯一值。

兩個欄位的PK

兩個欄位的狀況是 Partition key 加上 Sort key
Partition key 可以重複
但多筆 Partition key 相同的紀錄必須要有不一樣的 Sort key

換個角度說
在一個 partition 下的 sort key 要是唯一值

設計結果

大概 4 john 吧

article_id 作為 partition key
則在每一個 article_id 下 user_id 必須要是唯一值

用access pattern檢視這樣的schema設計

因為我們今天只需要查文章有沒有被特定user點讚過
所以顛倒過來也不會影響它的unique作用

除非今天後台要多一個功能:列出一篇文章下按過讚的user,
這樣的話就不要顛倒過來

或者說,後台要加的功能變成:列出一個user按過讚的所有文章,
那麼以 user_id 為 partition key 的設計會比較有效率!

也可以簡化成一個欄位的Primary key

這樣也4可以達到一樣的作用喇


回頭看mysql的table

首先,primary key 不需要是 integer,所以 auto increment 也不必要
乾脆直接把 primary key 改成 string

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateLikesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('likes', function (Blueprint $table) {
/**
* article_id = 'a111'
* user_id = 'u222'
*
* id = 'a111#u222'
*/
$table->id(); // 會是 string 的 primary key
$table->timestamps();
});
}

/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('likes');
}
}

這樣子也是能查R!
而且這樣子查詢時需要的所有資訊都在 primary key 裡面了,
我們還省掉兩個 index 欸!
朝爽得

hmmm
你說 mysql 的 primary key 也可以是兩個欄位組成的嗎
阿可是 laravel 不支援阿
ㄎㄎ