DQLでDynamoDBをSQLライクに操作する

# まえがき
SQLは好きですか？
私は好きです。
業務上よくDynamoDBを使用するのですが、どういうわけかマスタと名の付くテーブルが乱立したDynamoDBに対して一括操作を行う必要性が生じてしまいました。
曰く、「アトリビュート `A` の値がこの一覧表に含まれているレコードだけ、アトリビュート `B` の値を `true` に変更してほしい」とのことです。
もちろんアトリビュート `A` にインデックスなど貼られていません。さあ困った。
私はこれから注意深くその一覧表とアトリビュート `A` の値を見比べながら、一件ずつアトリビュート `B` の値を書き換えていかないといけないのでしょうか？
あるいは、大人しく今回の依頼の為だけに条件に含まれるレコードを全件更新するLambdaなりを書かないといけないのでしょうか？
ああ、これがRDBMSだったら極めて初歩的な数行のSQLを書くだけで実現できるのに！

そんな時に便利なのがDQL(DynamoDB Query Language)です。
DQLはPythonで書かれたSQLライクなDynamoDBのインターフェースで、DynamoDBのコンソールよりも遥かに柔軟なテーブル・レコードの操作が可能です。

端的に言って、DQLは銀の弾丸ではありませんが、それでもあなたの窮地を何度も救ってくれる事は間違いないでしょう。

# 導入
DQLの導入はPythonの実行環境とaws-cliの環境さえ整っていれば極めて簡単です（Python 3系の実行環境だと一部の操作がエラーを吐く可能性があるので、2,7系での実行をおすすめします）。
AWSのクレデンシャルが未設定の人は、ここ( http://docs.aws.amazon.com/ja_jp/cli/latest/userguide/cli-chap-getting-started.html )を参照しつつ設定しましょう。

```shell-session
$ python --version
Python 2.7.13
$ pip install -U dql
$ dql --version
0.5.24
```

いいですね！

# 操作する
東京リージョンで接続しつつ、対話的に操作してみましょう(DQLにはリージョンは選べても、profileを選択する機能が無いので、`aws configure` でデフォルトを該当のクレデンシャルにする必要がある点に注意して下さい！)。
該当リージョンの全テーブルを一覧で出力するコマンドは `ls` です。

```shell-session
$ dql -r ap-northeast-1
ap-northeast-1> ls
Name                   Status Read Write 
foo                    ACTIVE 1    1     
bar                    ACTIVE 1    1  
test                   ACTICE 1    1
```

うまくいってそうです:smiley:
テーブルの作成や削除はこの記事では省きます。
きっと、普通はそれくらいコンソール上でやるでしょう？:upside_down:

# レコードを取得する

`test` テーブルのレコードを全件取得してみましょう。
ハッシュキーが `KEY` 、それ以外にはインデックスは貼っていません。

DynamoDBの操作と同じように、ここでは `SCAN` を使用します。
`;` は区切り文字で、DQLを書く場合は必ず末尾に `;` をつける必要があります。

```
ap-northeast-1> SCAN * FROM test;
------------------------------------
|  A  |   B   |        KEY         |
------------------------------------
| 'C' | True  | 'testtesttest'     |
| 'B' | False | 'testtest'         |
| 'A' | False | 'test'             |
| 'A' | False | 'testtesttesttest' |
------------------------------------


```

SQLのように、 `SELECT` を使用することも可能です。少しだけオプションを操作する必要がありますが。

```
ap-northeast-1> opt allow_select_scan
allow_select_scan: False
ap-northeast-1> opt allow_select_scan true
ap-northeast-1> opt allow_select_scan
allow_select_scan: True

ap-northeast-1> SELECT * FROM test;
------------------------------------
|  A  |   B   |        KEY         |
------------------------------------
| 'C' | True  | 'testtesttest'     |
| 'B' | False | 'testtest'         |
| 'A' | False | 'test'             |
| 'A' | False | 'testtesttesttest' |
------------------------------------

```

想定どおりに動いているみたいです！:smile:

`ORDER BY` だって出来ます！

```
ap-northeast-1> SELECT * FROM test ORDER BY A;
------------------------------------
|  A  |   B   |        KEY         |
------------------------------------
| 'A' | False | 'test'             |
| 'A' | False | 'testtesttesttest' |
| 'B' | False | 'testtest'         |
| 'C' | True  | 'testtesttest'     |
------------------------------------
ap-northeast-1> SELECT * FROM test ORDER BY A DESC;
------------------------------------
|  A  |   B   |        KEY         |
------------------------------------
| 'C' | True  | 'testtesttest'     |
| 'B' | False | 'testtest'         |
| 'A' | False | 'test'             |
| 'A' | False | 'testtesttesttest' |
------------------------------------

```

では、 `WHERE` 句を書いてみましょう。

```
ap-northeast-1> SELECT * FROM test WHERE A = "A";
------------------------------------
|  A  |   B   |        KEY         |
------------------------------------
| 'A' | False | 'test'             |
| 'A' | False | 'testtesttesttest' |
------------------------------------

ap-northeast-1> SELECT * FROM test WHERE A IN ("A", "B");
------------------------------------
|  A  |   B   |        KEY         |
------------------------------------
| 'B' | False | 'testtest'         |
| 'A' | False | 'test'             |
| 'A' | False | 'testtesttesttest' |
------------------------------------

```

:smile::smile::smile:

残念ながら、 `NOT IN` には対応していないようです。

```
ap-northeast-1> SELECT * FROM test WHERE A NOT IN ("A");
SELECT * FROM test WHERE A NOT IN ("A");
                   ^
Expected {";" | StringEnd} (at char 19), (line:1, col:20)
```

ちょっぴり残念です:anguished:


ですが、 `COUNT`は可能です！

```
ap-northeast-1> SELECT COUNT(*) FROM test;
4
```

しかしながら、 `GROUP BY` や `JOIN` なども対応していないようです。
そうした操作が必要な場合は、得てしてNoSQLではなくRDBMSを使えということなのでしょう。

# UPDATEする

複数件のレコードのUPDATEも簡単に出来ます！

```
ap-northeast-1> SELECT * FROM test;
------------------------------------
|  A  |   B   |        KEY         |
------------------------------------
| 'C' | True  | 'testtesttest'     |
| 'B' | False | 'testtest'         |
| 'A' | False | 'test'             |
| 'A' | False | 'testtesttesttest' |
------------------------------------
p-northeast-1> UPDATE test SET B = true;
This will run update_item on all items in the table! Continue? [y/N] y
Updated 4 items
ap-northeast-1> SELECT * FROM test;
-----------------------------------
|  A  |  B   |        KEY         |
-----------------------------------
| 'C' | True | 'testtesttest'     |
| 'B' | True | 'testtest'         |
| 'A' | True | 'test'             |
| 'A' | True | 'testtesttesttest' |
-----------------------------------

```

もちろん、 `UPDATE` だってWHERE句を記述出来ます！

```
ap-northeast-1> SELECT * FROM test;
------------------------------------
|  A  |   B   |        KEY         |
------------------------------------
| 'C' | True  | 'testtesttest'     |
| 'B' | False | 'testtest'         |
| 'A' | False | 'test'             |
| 'A' | False | 'testtesttesttest' |
------------------------------------
ap-northeast-1> UPDATE test SET B = true WHERE A = "A";
Updated 2 items
ap-northeast-1> SELECT * FROM test;
------------------------------------
|  A  |   B   |        KEY         |
------------------------------------
| 'C' | True  | 'testtesttest'     |
| 'B' | False | 'testtest'         |
| 'A' | True  | 'test'             |
| 'A' | True  | 'testtesttesttest' |
------------------------------------

```

当然、 `IN` だって使えます。インデックスを貼っていないアトリビュートに対しても！

```
ap-northeast-1> SELECT * FROM test;
------------------------------------
|  A  |   B   |        KEY         |
------------------------------------
| 'C' | True  | 'testtesttest'     |
| 'B' | False | 'testtest'         |
| 'A' | False | 'test'             |
| 'A' | False | 'testtesttesttest' |
------------------------------------
ap-northeast-1> UPDATE test SET B = true WHERE A IN ("A", "B");
Updated 3 items
ap-northeast-1> SELECT * FROM test;
------------------------------------
|  A  |   B   |        KEY         |
------------------------------------
| 'C' | True  | 'testtesttest'     |
| 'B' | True  | 'testtest'         |
| 'A' | True  | 'test'             |
| 'A' | True  | 'testtesttesttest' |
------------------------------------

```

とても心強いですね！:sunglasses::sunglasses:
DynamoDBは基本的には複数条件での検索が不可能なので、こうしたインターフェースによってその操作が可能になるのはとても喜ばしいことです！

# あとがき
DQLはDynamoDBの無茶なオペレーションを可能にする、素晴らしいインターフェースです。
本記事で紹介しきれなかった機能は、こちら( http://dql.readthedocs.io/en/latest/ )にて参照が可能です。

通常、こうしたオペレーションが要求されるデータベースは本来的にはRDBMSで管理すべきですが、やむを得ない事情があってDynamoDBのみで設計されている場合は、DQLは極めて頼もしい相棒となってくれるでしょう。

ただ、もちろんログの蓄積等の用途の巨大なテーブルに対して `SCAN * FROM test` などのDQLを流すのは愚行ですし、テーブルのデータ規模を見極めつつDQLを実行する必要があるのだとは思います。

まあ、大人しくマスタと名の付くものはRDBMSに任せるべきなのですが:upside_down:

以上です。

# 参考資料
- DQLを使ってDynamoDBをSQL操作する
 http://dev.classmethod.jp/cloud/sql-against-dynamodb-with-dql/