概要
Bigtableで時系列データを保存する手段として
- 縦長のテーブルを使用する
- 横長のテーブルを使用する
- 列バージョンを利用する
の3種類あるのでそれぞれの特徴、使い方を紹介します。
環境
縦長のテーブルを使用する
各イベントを1行ずつ保持するやり方です。
例として庭の毎日の温度を保存する場合は以下のようになります。
行キー | 列データ |
---|---|
garden#20150301 | daily:temp:20.4 |
garden#20150302 | daily:temp:21.2 |
garden#20150303 | daily:temp:21.0 |
garden#20150304 | daily:temp:25.1 |
garden#20150305 | daily:temp:22.2 |
... | ... |
garden#20150331 | daily:temp:20.4 |
※列データは列ファミリー、列修飾子、データを「:」で区切った形式で示しています
どんな時使うか
Bigtableで時系列データを扱う場合、基本的に縦長のテーブルを使うことが推奨されてます。 理由としては以下です。
- 1 行に 1 つのイベントを保存すると、データに対するクエリの実行が簡単になる
- 1 行に多数のイベントを保存すると、行の合計サイズが推奨最大値を超える可能性が高くなるため
横長のテーブルを使用する
こちらでも紹介したような、1行にイベントを追加していくパターンです。
行キー | 列データ | 列データ | 列データ | 列データ | 列データ |
---|---|---|---|---|---|
garden#201503 | temp:01:20.4 | temp:02:21.2 | temp:03:21.0 | ... | temp:31:20.4 |
どんな時使うか
推奨される縦長テーブルでなく、横長にする場合というのは以下の両方を満たしているときです。
- クエリの最適化
- 特定期間のイベント全体を一度に取得する必要があることが多い
- 推測可能なデータ量
- 1行に保存するイベント数が有限(=あらかじめ決まっている)である
列バージョンを利用する
Cloud Bigtable では、列にタイムスタンプ付きのバージョンを持たせることができます。
そのため、時系列を列の一連のバージョンとして保存することができます。
これは先程の列修飾子による横長なテーブルとは別の概念です。
こんな感じで列データは1つなのですが、バージョン・リビジョンを持つイメージです。
どんな時使うか
誤った値入れてしまったので修正したい、かつその修正履歴が重要であるユースケースでは列バージョンが向いています。
それ以外のユースケースでは非推奨です。縦長テーブルや横長テーブルを使いましょう。
具体的な実装
bigtable emulatorとGoで、各パターンの具体的な実装を紹介します。
bigtable emulator
ローカル検証のためあらかじめbigtable emulatorを起動しておきます。
$ gcloud beta emulators bigtable start
cbtでテーブルを用意します。
$ export BIGTABLE_EMULATOR_HOST=localhost:8086 $ cbt -project my-project -instance emulator createtable my-table $ cbt -project my-project -instance emulator ls my-table
列ファミリーなども用意しておきます。
$ cbt -project my-project -instance emulator createfamily my-table daily $ cbt -project my-project -instance emulator createfamily my-table temp
縦長テーブル(行キー)
書き込み
func writeRow(ctx context.Context, rowKey, columnFamilyName, columnQualifier string) error { client, err := bigtable.NewClient(ctx, projectID, instanceID) if err != nil { return fmt.Errorf("bigtable.NewClient: %v", err) } tbl := client.Open(tableName) mut := bigtable.NewMutation() mut.Set(columnFamilyName, columnQualifier, bigtable.Timestamp(0), []byte(genTemp())) if err := tbl.Apply(ctx, rowKey, mut); err != nil { return fmt.Errorf("Apply: %v", err) } return nil }
各行を追加していきます。
func writeRows(prefix string) { ctx := context.Background() cfn := "daily" cq := "temp" writeRow(ctx, prefix+"#"+"20150301", cfn, cq) writeRow(ctx, prefix+"#"+"20150302", cfn, cq) writeRow(ctx, prefix+"#"+"20150303", cfn, cq) writeRow(ctx, prefix+"#"+"20150304", cfn, cq) }
読み込み
行キーで指定します。
func readRow(rowKey string) error { ctx := context.Background() client, err := bigtable.NewClient(ctx, projectID, instanceID) if err != nil { return fmt.Errorf("bigtable.NewClient: %v", err) } tbl := client.Open(tableName) row, err := tbl.ReadRow(ctx, rowKey, bigtable.RowFilter( bigtable.ChainFilters( bigtable.FamilyFilter("daily"), bigtable.ColumnFilter("temp"), ), ), ) if err != nil { return err } showRow(row) return nil } func showRow(row bigtable.Row) bool { for _, columns := range row { for _, column := range columns { fmt.Printf("row: %s, column: %s, value: %s\n", column.Row, column.Column, string(column.Value)) } } return true }
結果
row: garden#20150301, column: daily:temp, value: 1
行キーをprefixで複数取得したい場合は以下のように bigtable.PrefixRange を使います。
func readRows(prefix string) error { ctx := context.Background() client, err := bigtable.NewClient(ctx, projectID, instanceID) if err != nil { return fmt.Errorf("bigtable.NewClient: %v", err) } tbl := client.Open(tableName) err = tbl.ReadRows(ctx, bigtable.PrefixRange(prefix), showRow, bigtable.RowFilter( bigtable.ChainFilters( bigtable.FamilyFilter("daily"), bigtable.ColumnFilter("temp"), ), ), bigtable.LimitRows(3), ) if err != nil { return err } return nil }
結果
row: garden#20150301, column: daily:temp, value: 1 row: garden#20150302, column: daily:temp, value: 9 row: garden#20150303, column: daily:temp, value: 9
データ
cbtで見ると以下のようになっています。
$ cbt -project my-project -instance emulator read my-table prefix=garden# garden#20150301 daily:temp @ 1970/01/01-09:00:00.000000 "1" ---------------------------------------- garden#20150302 daily:temp @ 1970/01/01-09:00:00.000000 "9" ---------------------------------------- garden#20150303 daily:temp @ 1970/01/01-09:00:00.000000 "9" ---------------------------------------- garden#20150304 daily:temp @ 1970/01/01-09:00:00.000000 "4"
横長のテーブル(列修飾子)
書き込み
先程の関数で
- 行キーは1つにする
- 列修飾子を日付にする
という変更をします。
func appendColumn(prefix string) { ctx := context.Background() cfn := "temp" month := "201505" writeRow(ctx, prefix+"#"+month, cfn, fmt.Sprintf("%02d", 1)) writeRow(ctx, prefix+"#"+month, cfn, fmt.Sprintf("%02d", 2)) writeRow(ctx, prefix+"#"+month, cfn, fmt.Sprintf("%02d", 3)) writeRow(ctx, prefix+"#"+month, cfn, fmt.Sprintf("%02d", 4)) }
ちなみに行も列もビッグ エンディアン順に並べられるので、列修飾子の値を0パディングしています。
読み込み
横長テーブルなので1行でデータをまとめて取得する形になります。
func readRow(rowKey string) error { ctx := context.Background() client, err := bigtable.NewClient(ctx, projectID, instanceID) if err != nil { return fmt.Errorf("bigtable.NewClient: %v", err) } tbl := client.Open(tableName) row, err := tbl.ReadRow(ctx, rowKey, bigtable.RowFilter( bigtable.ChainFilters( bigtable.FamilyFilter("temp"), bigtable.CellsPerRowLimitFilter(3), ), ), ) if err != nil { return err } showRow(row) return nil }
取得する列のうち、一部だけ欲しい場合は bigtable.CellsPerRowLimitFilter() で件数をフィルタできます。
データ
cbtで見ると以下のようになっています。
$ cbt -project my-project -instance emulator read my-table prefix=garden#201505 ---------------------------------------- garden#201505 temp:01 @ 1970/01/01-09:00:00.000000 "25" temp:02 @ 1970/01/01-09:00:00.000000 "11" temp:03 @ 1970/01/01-09:00:00.000000 "18" temp:04 @ 1970/01/01-09:00:00.000000 "20"
列バージョン
書き込み
先程まではmutationのtimestampを0
にして、不要なバージョンを残さないようにしていましたが、今回はバージョンで管理したいのでtimestampを利用します。
func update(prefix string) { ctx := context.Background() cfn := "daily" cq := "temp" month := "201508" updateRow(ctx, prefix+"#"+month, cfn, cq) updateRow(ctx, prefix+"#"+month, cfn, cq) updateRow(ctx, prefix+"#"+month, cfn, cq) updateRow(ctx, prefix+"#"+month, cfn, cq) } func updateRow(ctx context.Context, rowKey, columnFamilyName, columnQualifier string) error { client, err := bigtable.NewClient(ctx, projectID, instanceID) if err != nil { return fmt.Errorf("bigtable.NewClient: %v", err) } tbl := client.Open(tableName) mut := bigtable.NewMutation() mut.Set(columnFamilyName, columnQualifier, bigtable.Now(), []byte(genTemp())) if err := tbl.Apply(ctx, rowKey, mut); err != nil { return fmt.Errorf("Apply: %v", err) } return nil }
読み込み
1行しか取得しませんが、データ自体は横長テーブルのように複数返ってきます。
func readRow(rowKey string) error { ctx := context.Background() client, err := bigtable.NewClient(ctx, projectID, instanceID) if err != nil { return fmt.Errorf("bigtable.NewClient: %v", err) } tbl := client.Open(tableName) row, err := tbl.ReadRow(ctx, rowKey, bigtable.RowFilter( bigtable.ChainFilters( bigtable.FamilyFilter("daily"), bigtable.ColumnFilter("temp"), bigtable.LatestNFilter(3), ), ), ) if err != nil { return err } showRow(row) return nil }
件数指定したい場合は bigtable.LatestNFilter() を使用します。
結果
row: garden#201508, column: daily:temp, value: 22, timestamp: 1598396705122000 row: garden#201508, column: daily:temp, value: 22, timestamp: 1598396705121000 row: garden#201508, column: daily:temp, value: 5, timestamp: 1598396705119000
データ
cbtで見ると以下のようになっています。
列修飾子もすべて同じですが、timestampが異なる別々に保存されています。
$ cbt -project my-project -instance emulator read my-table prefix=garden#201508 ---------------------------------------- garden#201508 daily:temp @ 2020/08/26-08:05:05.122000 "22" daily:temp @ 2020/08/26-08:05:05.121000 "22" daily:temp @ 2020/08/26-08:05:05.119000 "5" daily:temp @ 2020/08/26-08:05:05.117000 "19"
まとめ
bigtableで時系列データを保存する際に、ユースケースに応じて様々な手段があるということを紹介しました。