maybe daily dev notes

私の開発日誌

MariaDBコントリビューション録その4 - GDBをVSCodeから使う

前回のあらすじ

MDEV-24582 は難敵だ。前回の成果は修正箇所を特定する方法を特定したのみ。今回はデバッガで処理を追いながら修正対象を見つけていく。

tmokmss.hatenablog.com

GDBの導入

大規模なコードベースの挙動を理解するためには、実際に実行してデバッガで処理を追うのが手っ取り早い。MariaDBC++ではGDBというデバッガが人気なので、まずはこれを使えるようにする。

CLIによるやり方は、この記事が詳しい。ちなみにGDBはここまで環境構築できていれば既に入っているはずで、明示的にインストールする必要はない。

nayuta-yanagisawa.hatenablog.com

かいつまんで書くと、まず mysql-test-run.pl--manual-gdb オプション付きで実行する。

./mysql-test/mysql-test-run.pl innodb.MDEV-24582 --manual-gdb

すると、以下のような表示が出てくる。ここに書いてあるコマンド (gdb -x ...) を別のシェルで実行すれば、gdbのコマンドを入力できるようになる。

To start gdb for mysqld.1, type in another window:
gdb -x /home/ec2-user/MariaDB-server/bld/mysql-test/var/tmp/gdbinit.mysqld.1   /home/ec2-user/MariaDB-server/bld/sql/mysqld

gdbのコマンドはこのサイトなどにまとめられているが、何分CLIは苦手なのでGUIでやりたい。特にVSCodeからgdbを操作できれば、非常に便利である。

VSCodegdbを扱うためには、vGDBという拡張機能を利用すれば良い。 (他もいろいろ試した中でこれだけは必要十分なオプション指定ができた。)

marketplace.visualstudio.com

launch.json には以下を記述する。注意点として、program には↑のgdbコマンドの第3引数のパスを、 debuggerArgs-x のオプションをを転記する。これはもともとできなかったが、vGDBの開発者に頼んだらすぐに実装してくれた。

{
    "version": "0.2.0",
    "configurations": [
        {
            "type": "vgdb",
            "request": "launch",
            "name": "C/C++ Debug Launch",
            "debugger": "gdb",
            "program": "./bld/sql/mysqld",
            "debuggerArgs": ["-x", "/home/ec2-user/MariaDB-server/bld/mysql-test/var/tmp/gdbinit.mysqld.1"]
        }
    ]
}

これで、VSCodeからデバッガを操作できるようになる。デバッグするときは、まず mysql-test-run.pl --manual-gdb を実行してから、VSCodeからデバッガを起動するようにすること。

ちなみにDEBUG CONSOLEを使えば、任意のgdbコマンドを実行することもできる。例えば関数を実行した評価結果を見たいような場合に使える。

これで装備は整ったので、いよいよ処理を追っていこう。 なお、mysql-test/suite/innodb/MDEV-24582.testには以下のテストコードを書いてある。今回のバグが再現する最低限のコード。以後、このテストコードを使ってデバッグを行う。

CREATE TABLE t1 (a VARCHAR(3), v VARCHAR(3) AS (CONCAT('x-',a)) VIRTUAL);
INSERT INTO t1 (a) VALUES ('foo');

処理を追う

とりあえず特定すべきは、virtual columnの値を計算している部分だろう。INSERTする文字列の長さでValidatonするためにはまず挿入する値を知る必要があるので、判定ロジックを追加するとしたらそれ以後になるはずだ。

まずは前回示唆された TABLE::update_virtual_fields 関数内にブレークポイントを置き、そこから周辺を調べることにする。実際この関数は Compute values for virtual columns used in query という説明も書かれており、非常にあやしいのだ。

色々変数の中身を見ていると、まず気になったのは vfieldfield_length。これは何の長さだろうか。実験してみると、どうやらカラム v の型の文字列長だということが分かった。以下はテーブル定義を VARCHAR(4) にしてみたときの図。field_length も4になっているので、そういうことだろう。つまり、Validationするときは、この値と書き込む値の長さを比較すれば良いということ。必要な値の片方が分かったのは大きい。

次は元の目的に立ち返って、virtual columnの値を計算している部分を特定しよう。しかし、TABLE::update_virtual_fields 関数の動きをつぶさに追ったのだが、なんとそのような計算を実行している様子はなかったのだ。コードはそれっぽく見えるが、実際はほとんどの分岐に入ることがなくほぼ何もしない関数だった。一旦は他のところを見てみよう。… どこから見ようか?

コールスタックから攻める?

コールスタックを見ると、この関数に至った関数呼び出しの流れが分かる。各スタックをクリックすることで、スタックを自由に行き来できる。これを参考にして、関連するコードを探そう。

しかし、結果的にはこのアプローチはうまくいかなかった。幅優先探査的なアプローチで一通り追ったが、目的の箇所を特定するには各関数呼び出しを深く追う必要があり、効率が悪い。何かよりボトムアップなアプローチがないだろうか。

CONCATから攻める?

再現に使っているテーブルでは、Virtual columnとして CONCAT 関数を用いている。virtual column の値を計算する際は、必ずCONCATの計算がされるはずなので、CONCAT計算のコードを特定すれば手がかりになりそうだ。

CREATE TABLE t1 (a VARCHAR(3), v VARCHAR(3) AS (CONCAT('x-',a)) VIRTUAL);

CONCATでgrepしてみよう。

ここで出てきた Create_func_concat という関数に当たりをつけさらにコードを追うと、 Item_func_concat というクラスから、 item_strfunc.cc というファイルに行き着いた。このファイルは This file defines all string functions ということで、今回探している文字列連結CONCATの処理も実装されていそうだ。正直どの関数が呼ばれるかコードからはよくわからないので、すべての関数にBreakpointを配置する。これには次のgdbコマンドを使う。

rbreak sql/item_strfunc.cc:.

が、駄目……っ! 文字列を連結するような処理が引っ掛かることはなかった。

その後も色々と考えて手を尽くしたものの、有益な手がかりは見つけられず、今日中にブログを書き終えたい私は焦りが募るばかり。

ふと思いだす

ここで、師が意味ありげなことをつぶやいていたのを思い出した。

SELECT時に計算、そういうのもあるのか… たしかに上のテストコードではSELECTを発行していないので、計算処理を見つけられなかったのは合点がいく。ためしにSELECT文も含めてみる。

CREATE TABLE t1 (a VARCHAR(3), v VARCHAR(3) AS (CONCAT('x-',a)) VIRTUAL);
INSERT INTO t1 (a) VALUES ('foo');
SELECT * FROM t1;

…ビンゴ!SELECTクエリの処理中に、TABLE::update_virtual_fields 関数の中で先程目をつけていた item_strfunc.ccItem_func_concat::val_str 関数が呼ばれた。なぜテストコードからSELECTを省いてたか考えると、再現には影響しないものと勝手に思い込んでいたためだ🤦  思い込みは本当に良くない(教訓。)

ということで、計算処理はSELECTで走ることが分かった。こうなると、当初想定していた、INSERT時にカラムの最大長と挿入する値の長さを比較してエラーを出す方法は使えない。今一度方針を立て直す必要がありそうだ。

INSERT時にエラーを吐くには、今までやっていなかったvirutal columnの値計算を追加することになる。大きな変更となるが、本当に設計・実装できるだろうか?

次回へ続く。

MariaDBコントリビューション録その3

前回のあらすじ

3つ目のIssueは強敵だったが、新しく覚えた「perrorを手がかりに調べる」技でどうにか解決した。__builtin_expect も仲間に加わり、tmokmssの冒険はまだまだ続く。

tmokmss.hatenablog.com

今回のIssue

[MDEV-24582] INSERT silently truncates too long value for a virtual column without warnings or errors - Jira

Generated columnの文字列長が長すぎる場合、エラーも警告もなくTruncateされてしまうというバグ。

まずは再現

一旦DockerでMariaDBを立て、同クエリを打ってみる。

version: '3.8'
services:
  mysql:
    image: "mariadb:5.5"
    ports:
      - "3306:3306"
    volumes:
      - "mysql_data:/var/lib/mysql"
    environment:
      MYSQL_ROOT_PASSWORD: password
      MYSQL_USER: admin
      MYSQL_PASSWORD: password
volumes:
  mysql_data:
CREATE TABLE t1 (a VARCHAR(3), v VARCHAR(3) AS (CONCAT('x-',a)) VIRTUAL);
INSERT INTO t1 (a) VALUES ('foo');

vx-foo になるので VARCHAR(3)の文字数を超過するが、たしかにエラーもなく INSERT に成功した。x-f と勝手にTruncateされる模様。

ちなみに同じクエリをMySQL 5.7に対して打つと、以下のエラーが帰ってくる。

Data too long for column 'v' at row 1

とりあえず、Issueに書いてあることは再現できた。

二分探査でバグ発生コミットを特定

と、いうのが定石なのだが、今回はうまくいかなかった。以下に顛末を記す。

二分探査するには、まずバグがないコミットとあるコミットを見つける必要がある。あるコミットは先程再現して見つかっているので、バグがないコミットの方を探そう。このため、適当にさかのぼってクエリを実行してみる。

5.5はダメなので、5.4以下を探していこう。ところで実はMariaDB 5.4というのは存在しない。経緯は謎だが、欠番となっている。このため、5.3から試していく。

調べるとこの人が5.3と5.2のイメージを作ってくれているので、利用しよう。 ちなみにさっきからDockerを使っているのは、MariaDBの昔のGitコミットではいつものビルドコマンドが使えず、必要なビルド方法を調べるのが面倒だったため。粗い検証方法ではあるが、それなりの参考にはなるはず。

結果的には、5.3でも5.2でも同様にバグが再現した。5.1はイメージが見つからず試せていないが、あとから知ったがそもそも virtual column は5.2から実装された機能だったらしい。

ここで疑うのは、そもそもこのバグ最初からあったんではということ。こうなるとバグの可能性があるコード片は簡単に絞り込めないため、難易度が跳ね上がる。一旦二分探査は諦めて、方針を改めよう。

MySQLのコードを参考にする

次に予想したのは、MySQLではバグが再現しないことから、MySQLでは同バグを修正したのではということ。つまり、MySQL側の修正コミットを特定できれば、それを参考にMariaDBのバグも修正できるかもしれない。

これも2分探査で見つけられるはずなので、まずはMySQLでバグが再現するコミットを見つけることを試みる。冒頭では5.7で再現しなかったので、MySQL 5.6にクエリを打つ。

ここで意外な事実が発覚。なんとvirtual columnはMySQLでは5.7から実装された機能だった。5.6では CREATE TABLE クエリに対して無情な構文エラーが返ってくる。

You have an error in your SQL syntax; check the manual that 
corresponds to your MySQL server version for the right syntax 
to use near 'AS (CONCAT('x-',a)) VIRTUAL)' at line 1

WikipediaによればMySQL 5.7は2015年にリリースされたものである。一方MariaDB 5.2は2010年なので、MariaDBMySQLとでは全く別の時期にリリースされたことが分かる。

つまり、2つは似たような機能ではあるものの、実態は全く別のコードで動いている可能性が高い。これだとMySQLのコードを参考にMariaDBのバグを直す方針も難しいだろう。どうする?

助けを求める

膨大なコードから手がかりを見つけることが困難なため、独力では詰みに近い。 師に指示を仰いだところ、見るべきコードが啓示された。

たしかに、今回の処理と関係してそうである。次回は、この辺りの処理をデバッガで追うことにしよう。

次回へ続く。

MariaDBコントリビューション録その2 - builtin_expect による分岐予測

前回のあらすじ

初のMariaDBコントリビューションを達成し気をよくした俺。順調に2つ目のIssueもマージされ、浮かれ気分で次なる敵に挑むのであった。

tmokmss.hatenablog.com

今回のIssue

[MDEV-28599] EXCHANGE PARTITION on view causes ER_CHECK_NO_SUCH_TABLE instead of ER_WRONG_OBJECT - Jira

ALTER TABLE v EXCHANGE PARTITION p0 WITH TABLE t2 というクエリが何らかの原因で失敗した際のエラーメッセージを、よりわかりやすいものに変更するという目的のようだ。

Fix Version/s の中で最も古いのは 10.3 なので、そのブランチから作業を始めれば良いらしい。

また、事前情報として perror というコマンドも教えてもらっている。

修正箇所の特定

とりあえず、現行のエラーコードで perror してみる。

$ extra/perror 1177
MariaDB error code 1177 (ER_CHECK_NO_SUCH_TABLE): Can't open table

ER_CHECK_NO_SUCH_TABLE というのがエラー名の模様。おそらくこれが呼び出されている部分が対象の修正箇所なのだろうと予想し、grepする。

数カ所発見。大量に出てくるとかではなくてホッとした。この中から対象箇所を特定するため、Issueを読み返す。

まず EXCHANGE PARTITION というクエリ では、あるテーブルから他のテーブルに指定したPARTITIONを移動する機能らしい。で、この移動先のテーブルがTABLEでなくVIEWだとエラーになるが、その時に表示されるエラーがユーザーからは原因が分かりづらいと。 おそらくは 移動先がVIEWかどうかチェックしている箇所がありそうなので、それを探す。

それらしいのはここしかないのだけど、Viewというワードが出てこないので自信が持てない。当初の予想とは異なり、まだVIEWか否かのチェックは実装されていないのかも? とりあえず、ここのエラーコードを変えてからテストを実行してみよう。

上のgrep 結果から、対応するテストコードが mysql-test/main/partition_error.test であることは特定済みなので、次のコマンドでテストを実行できる。

$ cd bld/mysql-test
$ ./mysql-test-run.pl main.partition_error

main.partition_error                     [ fail ]
        Test ended at 2022-05-29 03:23:46

CURRENT_TEST: main.partition_error
mysqltest: At line 20: query 'ALTER TABLE t1 EXCHANGE PARTITION p0 WITH TABLE v1' failed with wrong errno 1178: 'The storage engine for the table doesn't support (null)', instead of 1177...

The result from queries just before the failure was:
drop table if exists t1, t2;
#
# Bug#60039: crash when exchanging a partition on
#            nonpartitioned table with a view
#
CREATE TABLE t1 (a int);
CREATE OR REPLACE VIEW v1 AS SELECT * FROM t1;
ALTER TABLE t1 EXCHANGE PARTITION p0 WITH TABLE v1;

エラーコードを 1177から1178 に変えてみたのだが、期待通りテストに失敗してくれた。どうやらここが該当のエラーコードを出力しているところということで間違いないらしい。これで修正箇所を特定できた。

修正方針の検討

最終目標はIssueに書いてある通りで、移動先にVIEWが指定された場合は 1347: 'test.v' is not of type 'BASE TABLE' というエラーを表示すること。 *1

そのためには、次のようなことをやれば良さそう。

  1. 既存のエラーを吐く条件 if (unlikely(!part_table || !table)) の意図を考える
  2. コード上のどこでViewを判定するか考える
  3. 入力がViewかどうか判定する方法を調べる

前のIssueと比べると全然自明じゃない…!とりあえず1から。

if (unlikely(!part_table || !table)) だが、これはただのnullチェックをしているだけ。 現行のエラーはこの条件から生じているので、ナイーブに考えれば、ここの ER_CHECK_NO_SUCH_TABLE を1347に書き換えれば良さそう。しかしそれでは他のケースでも1347エラーになる場合が生じそうで、都合の悪いことがわかる。つまり、移動先がViewであることとtableがnullであることは必要十分条件じゃないだろうという予想。ここは正直コードだけから判断するの至難なので、予想だけしておいて判断はPRレビューに任せることにする。

ということで、この部分はそのまま放置して、ここよりも前の処理で移動先がViewであるかどうかのチェックをする方針に。最悪レビュー受けてからまた調整しましょう。ちなみに今回コードはこの辺りを見ている

上記の考察を踏まえて、以下のコードを追加した:

...
  // これを追加してみた ifの条件はTBD
  if (swap_tableがViewなら)
  {
    my_error(ER_WRONG_OBJECT, MYF(0), table_list->db.str,
              swap_table_list->table_name.str, "BASE TABLE");
    DBUG_RETURN(TRUE);
  }

  // 既存のValidation
  if (unlikely(check_exchange_partition(swap_table, part_table)))
    DBUG_RETURN(TRUE);
...

変数 swap_table_list は、移動先のテーブルの情報が入った構造体 (TABLE_LIST) のようなので、これを見てViewかどうか判定すれば良さそう。また、 ER_WRONG_OBJECT は既存のコードを参考にそれっぽく穴埋めして書いている。これも帰納的プログラミング。

tmokmss.hatenablog.com

これで2も完了。あとは3だけで、if文の条件を考えれば良い。 しかしこれもコード読むだけだと正しい判断をするのは難しいと予想*2。一旦少なくともテストケースはパスするコードにしておき、PRレビューで担保してもらうことにする。ちなみにここの条件を間違えていたら、今まで問題なかったクエリでもエラーを吐くことになる。なかなか重要な変更になるぞ…

とりあえずで提出したif条件はこれ。swap_table_list->viewがnullでなければ、移動先がViewであるとみなす。他の箇所でも似たような判定をしていたので、それを流用しただけ。ちなみに詳しい人は「なぜ likely ?」と思うかもしれないが、この時点では likely(x)で xが0でないときに真を返すものだと勘違いしていた🤦 。

if (likely(swap_table_list->view))

これで PRを作成。レビューを待つことにしよう。

レビューを受ける

安定安心の nayuta-yanagisawa によるレビュー。以下のようなレビューを受けた。

  1. ifの条件は問題ない
  2. テストケースがいまいちだから新たに追加しておいて
  3. likelyじゃなくunlikelyじゃない?

1はこれで安堵できる。理屈としては、変更の影響範囲はごく狭いため、自動テストによる検証で担保する方針でOKとのこと。

2は順当に追加。たしかによく見ると既存のテストは2つのエラー要因があり、今回のテストケースとしてはふさわしくないものとなっていた。

3が問題のlikely。そもそもの定義は以下のコードで、自分は当初 __builtin_expect(((x) != 0),1)((x) != 0) == 1 という意味だと早とちりしていた! __builtin_expect のようなよくわからない関数はスルーせずしっかりと調べるべきである (教訓)。

#define likely(x)    __builtin_expect(((x) != 0),1)
#define unlikely(x) __builtin_expect(((x) != 0),0)

__builtin_expect の正体はこれ。 評価結果が真と偽のどちらになる可能性が高いかをコンパイラに教えることで、分岐予測による性能を高めることを狙うものであった。今回は swap_table_list->view は偽である可能性が高いので、 unlikely が正しい。英語を考えると likely は起こりやすい、 unlikely は起こりにくいなので、たしかに明らかでした。

ということで全て修正して、無事マージされたのであった 🎉

まとめ

今回のIssueも無事にクローズできた。しかし難易度は徐々に上がっていることを感じる。次回もVSCodeと共にあらんことを。

*1:ちなみにエラーコードを変更する程度であれば、破壊的変更とはみなさないとのこと。

*2:この辺りはMariaDBのドキュメントや資料、あるいはExpert MySQLを読むと分かるかも、とのこと。

DynamoDB、シングルテーブルにするか否か

はじめに

DynamoDBを使っていると、とかくテーブルは1つにまとめるべきという声や、複数テーブルからシングルテーブル設計に移行したという事例を耳にすることがあります。 しかし、その理由を聞いてみると、性能のためだったり管理を簡単にするためだったり、人により異なる印象です。 NoSQLにおける非正規化してデータをもつプラクティスは理解しつつ、その域を超えて全く無関係なItemを1テーブルにまとめる場合もあるようです。

私自身このトピックについて混乱していた中で、先日 The What, Why, and When of Single-Table Design with DynamoDB というブログを見つけました。 それを読んだ上で考えると、割と理解が整理できたので、この記事にまとめてみます。

なお私はDynamoDBの運用経験がまだ十分にあるわけではないので、勘違いや考慮漏れなどあるかもしれません。ツッコミお待ちしております。

シングルテーブル設計とは

あるアプリケーションが使うDynamoDBのテーブル数を1つに集約する設計方針です。 dynamodb single table などでググると、この設計に関する多くの記事が見つかるでしょう。

ちなみに AWS公式のドキュメント にも、このように記載されています。

In general, you should maintain as few tables as possible in a DynamoDB application. (中略) A single table with inverted indexes can usually enable simple queries to create and retrieve the complex hierarchical data structures required by your application.

こちらは as few table as possible という表現ですね。

DynamoDBは基本的にスキーマレスであり、Itemのスキーマを制限するものはPK, SK, GSI, LSIカラム名・型とTTLカラム名のみです。つまり、それらだけ一致させれば、任意のデータセットを1つのテーブルに集約することが可能です。現実ではカラム名PK / SK など汎用的に命名し、型も文字列型にすることが多いでしょう。そうした条件が満たされていれば任意の2テーブルは1つに集約可能なため、理論上あらゆるアプリケーションはシングルテーブル設計を採ることができるわけです。

技術的にはどちらでも良いが片方を選ばなければならない時、人は悩みます。この記事ではシングルテーブル設計にするか否か、比較の観点をできるだけ網羅してまとめることを目指します。

シングルテーブル設計を検討する観点

観点1. データ取得の性能が改善する場合がある

これが最も定番の理由です。

DynamoDBのQuery APIを利用すると、同じテーブル・同じPartition Key(PK)のデータを、1回のAPIコールでまとめて取得することができます。 これにより、複数のテーブルにデータが散在している場合と比べて、少ない回数のAPIコールで必要な情報を取得でき、効率化できる場合があります。

このメリットはRDBMSのようなJOIN機能を持たないNoSQLに共通の観点であり、 非正規化 という言葉でも有名です。 次のブログなどに詳しい解説があります ( Creating a single-table design with Amazon DynamoDB 。) RDBMSのテーブル設計とは全く異なる独特な考え方が必要のため、慣れないととっつきづらい部分ですが、最近は公式ドキュメントでも多くのユースケースにおけるデザインパターンのベストプラクティスが紹介されているため、少し身近な存在にもなりつつあります。

しかしながら、そもそもまとめて取得する要件がないようなアイテム同士は、一つのテーブル上に配置するメリットがあるとは言えません。 そのようなテーブル同士であっても、一つにまとめるメリットはあるのでしょうか。次を見てみましょう。

観点2. テーブル数が減ると、関連リソース数も減る

運用の都合によっては、DynamoDBテーブルに対して以下のような機能が必要になる場合があります:

  • 各種メトリクスの監視
  • テーブルアクセスに対する監査ログの記録
  • バックアップ
  • 別のストレージへのレプリケーション

これらは利用しているすべてのDynamoDBテーブルに共通で必要になることもあるでしょう。その場合はテーブル数に応じて、関連するCloudWatchのアラームやLambda、DynamoDB Stream、さらにはそれらを監視するアラームなど、管理すべきリソースが増えていきます。

リソースが増えることのデメリットとして、以下が挙げられます:

  1. 監視ダッシュボードやマネジメントコンソールが見にくくなる
  2. 構築が大変になる
  3. 固定費のかかるリソースがある場合は、コスト効率も悪化する

シングルテーブル、あるいはテーブル数を最小化するようにすれば、このリソース増大問題を回避・緩和することができます。実際、私が参加していたプロジェクトでも、この観点が主な理由で10個強のテーブルを1つに集約するようなリファクタを開始していました。

とはいえ2のデメリットについては、CDKなどのIaCを利用すれば比較的容易に必要なリソースを反復定義・デプロイできます。また、3のデメリットについても、サーバーレスのサービスを選べば、固定費が掛かるサービスは少ないでしょう。 このため、こうしたデメリットは許容できる場合も十分考えられます。

観点3. キャパシティ管理のしやすさ

DynamoDBをProvisionedモードで利用する場合、テーブルごとにキャパシティ (WCU, RCUの割当数)の管理が必要になります。

テーブルを一つにまとめるとキャパシティ管理の対象も一つになるので、管理が容易になる場合も多いでしょう。またテーブルをまとめることで、小規模なテーブルのキャパシティを大規模なテーブルのキャパシティの余剰分でまかなうことができるため、コスト効率の面でもシングルテーブルに分があります。この辺りの話は、マルチテナントのリソース共有による効率性の話にも通ずるものがありますね。

Resource sharing is a central benefit in multi-tenant systems. A multi-tenant system handles multiple workloads, such as work from multiple customers at once. This system can also handle low priority, non-urgent workloads along with high-priority, urgent workloads. A single-tenant system, on the other hand, handles workloads from a single customer. Fairness in multi-tenant systems

ただし、今のDynamoDBではキャパシティ管理が(ほぼ)不要なOn-demandモードもサポートされています。この場合は、単純にリクエスト数に応じた課金になるため、テーブルが分かれていても一つでもコストは同じです。従って、On-demandモードで運用する場合は、この観点のメリットは当てはまりません。

とはいえ、サービスの規模が大きい場合などワークロードによっては、On-demandモードよりもProvisionedモードを頑張って運用する方が安くなる場合があります。この分岐点を超えると、On-demandからProvisionedへの移行に踏み切ることもあるでしょう。いずれProvisionedに移行する可能性があるのなら、最初からそれに適したシングルテーブル設計にすべきという考え方もあるかもしれません。

観点4. IAM権限管理のしやすさ

シングルテーブルにすることでIAM権限管理が粗くなるんじゃない?と不安になるかもしれません。しかし、DynamoDBではテーブル単位だけでなく、行単位のIAMアクセス制御も可能です。

Using IAM policy conditions for fine-grained access control - Amazon DynamoDB

これにより、シングルテーブル設計であっても、サービスごとにテーブルを分けているような場合と同等の権限管理を実現できます。

実際のIAMポリシー例は、以下の回答も参考になります。ForAllValues:StringLikedynamodb:LeadingKeys を組み合わせることで、PK が特定のprefixをもつアイテムに対してのみアクセスを許可することができます。

stackoverflow.com

とはいえ、CDKの grantXX 系メソッド はテーブル単位でのみ設定可能、といったように既存のユーティリティがItemレベルの権限管理に十分対応していない事情もあります。この意味では、シングルテーブルにすることでIAM権限管理がやや煩雑になると考えても間違いではないでしょう。

まとめ

DynamoDBのシングルテーブル設計を採るか否かについて、検討ポイントを列挙してました。私自身は、性能に寄与しない場合は無理にシングルテーブルにしなくてよいだろう派ではありましたが、上記の考慮点をケアすると、その結論には至らない場合も多そうです。今後ともケースバイケースで対応していければと思います!

追記

最近(2023年末〜)はシングルテーブル設計に対する懐疑的な意見も多いようです。こちら↓の記事が分かりやすいです (元DynamoDBチームの方による記事)。

Single table design for DynamoDB: The reality — Momento

シングルメリットのデメリットとして、主には以下が挙げられています。

DynamoDBのドキュメントでは今もシングルテーブルが推奨されているため、人により意見の異なるトピックなのでしょう。

In the majority of cases, we recommend that you consider using a single table. source

どちらにしても偏った意見には惑わされず、臨機応変な判断をしていきたいですね。

酒場: お山の大将

はい、ということで今回はお山の大将に行ってまいりました。以下レポートです。

tabelog.com

往路

平日の19時にお店到着。店はそれほど混んでいませんでしたが、カウンター席に案内されました。おそらくテーブル席は予約で埋まっていたのでしょう。今日も大人気です。

お通し

今回のお通しはゆで卵にカレーを掛けたものでした。 大抵はちょっとしたもつ料理が出てくるのですが、このパターンは初めてです。

ここのカレーは初めて食べまして、予想以上に美味しかった。多分モツも煮込まれているんでしょうか、独特の旨味がありました。

いつもどおり酎ハイ180円を頼みます。月火水木金土いつも180円です。安い! この日は酎ハイ2杯とビー酎半分でフィニッシュしました。

おまかせで6本頼みました。カシラ、シロ、テッポウかな… 相変わらず臭みもなくてうまいです。この前自宅で猿真似の砂肝串を手作りしたのですが、 割とクセがあって食べづらかったんですよね。この点、やはりプロの技術は違います。

モツのことは以下の記事も参照。私はまだ識別能力低いです。お山の大将は豚モツがほとんどなので、その事前知識も使いながら識別していく感じになります。

tmokmss.hatenablog.com

サイド

モツ煮込みを頼みました。小・中で量を選べたのですが、中を選択。意外と多かったので、1人だったら小一択だと思います。これも様々なモツが煮込まれていて、見た目にも楽しい。味もやさしい味噌汁のような感じでとても良かったです。

あとは生ピーマン。ここの生ピーマンは豪快で、縦半分に切ったピーマン4つに味の素と塩が振られて出てきます。キンキンに冷えたみずみずしいピーマンと味の素が存外マッチして、大変うまいです。

他にもマグロぶつとか色々食べたような気がします。写真残しておくべきでした。

シメ

野菜炒めを初めて頼んでみました。もやし・ニラ・キャベツだけで構成される、いわく美人になる炒めものらしいです。味は黒コショウと魚粉かな?このお店は魚粉を多用しがちなので、初めてのメニューですが不思議と慣れた味ですね。良い肴になる野菜炒めでした。

会計

2人でたらふく飲み食いして4350円でした。安い!このお店はいつも端数を切り捨ててくれるので、さらにお得感がありますね。

帰路

帰りは近場のおふろの王様に行きました。おふろの王様とお山の大将、名前似てますね。おふろの王様では若者が夢を語り、お山の大将ではおじさんが競馬を語ります。私もいつか語れるものができればと思います。

それでは、また来週。

ワタミ系列店チートシート

背景

以前町の飲み屋に入ったのだが、実はワタミ系列店だったということがあった。 特にワタミがきらいな訳ではないが、そこはかとないガッカリ感は否めない。今後はワタミの店はワタミの店だと覚悟してから入りたいので、この記事ではワタミ系列店の列挙を目指す。

この外観は町の歴史ある大衆居酒屋という感じしかない だがワタミ

結論

ワタミ系列店は以下の15種類くらい。 (2022/5/23現在)

  1. 焼肉の和民
  2. 幸せの焼肉食べ放題 かみむら牧場
  3. 居食屋「炭旬」
  4. Restaurant & American Bar「T.G.I. Friday's」
  5. CHINA BISTRO「WANG'S GARDEN」
  6. 炉ばたや「銀政」
  7. にくスタ
  8. ミライザカ
  9. 三代目 鳥メロ
  10. から揚げの天才
  11. すしの和
  12. こだわりのれん街
  13. テキサス風メキシカン「TEXMEX FACTORY」
  14. しろくまストア
  15. 厳選煮干しらーめん にぼ助

ただし、他にもある可能性は捨てきれないので注意。

調査

まず簡単な調査の結果、ワタミ系列店を列挙する問題は、意外にも自明ではないことが分かった。公式サイトに掲載されていない系列店もあるためだ。

公式サイト

ワタミ公式サイトに列挙されている外食事業(国内) は、以下の12種類。からあげの天才もワタミ系列だったのか…

見ればわかるが、ここには上のしろくまストアが含まれていない。しろくまストアの公式サイトのお知らせによればしろくまストアは間違いなくワタミ系列店である。この点で、ワタミ公式サイトの記述は完全に信頼できるものではないことがわかる。

Wikipedia

公式が当てにならないと集合知に頼るしかない。Wikipediaをみてみよう。

ja.wikipedia.org

現行の展開店舗 というセクションには、10種の店舗が列挙されている。これは公式サイトの12よりも少ないため、これだけでは当てにならなそう。他には、グループ沿革 のセクションに、店舗の種別ごとの出店・撤退イベントが記されていることがわかる。このイベントを追っていけば、今残存しているワタミ系列店舗がわかるかもしれない。

これらのイベントを手で分析し、「撤退イベントが明記されてない店舗」 でかつ 「ワタミ公式サイトにも、Wikipedia展開終了の店舗リストにも 現行の展開店舗リストにも記載されていない店舗」 を割り出した。これが以下の6種類:

  1. 仰天酒場 和っしょい2
  2. まる焼きチキン&セルフBAR「GABURI」
  3. テキサス風メキシカン「TEXMEX FACTORY」
  4. しろくまストア
  5. もつ★りき
  6. 厳選煮干しらーめん にぼ助

これらが、少なくとも今のWikipediaから推測できる、ワタミの公式サイトには載っていないもののワタミ系列店である可能性がある店ということ。

さて、それぞれの行く末をググってみると、店舗が今の現存するものはさらに少なく、次の3種類に絞られる。これらはリンクも貼っているが、2021・22年頃にワタミ系列店であることを確認できるサイトが見つかっている。

一旦、このリスト+ワタミ公式サイトのリストが、ワタミ系列店の完全なリストであるとみなして良いだろう!

この方法の限界は、Wikipediaが出店イベントを取りこぼしている可能性があること。より正確な方法としては、ワタミヒストリー を参照することが考えられる。しかし、これも2020年版までしかなかったり、そもそも面倒くさすぎる、またワタミが特定のイベントを削除している可能性も否めないので、確認は読者の宿題としておく。

まとめ

強烈なワタミアンチみたいになってしまったけど、ブラック企業であったこと以外にワタミにネガティブな印象はない。しろくまストアのてっぺん串(100円) もうまかった。

てっぺん串赤 (100円)

それにしてもワタミ、ステルスしすぎである。しろくまストアの看板は紛うことなく歴史ある大衆居酒屋、TEXMEX FACTORYの公式サイトからにじみ出る中小チェーン店臭は異常だし、にぼ助に至っては関係を裏付ける根拠が株主向けの資料しかない。それほどワタミとの関係性を隠しておきたいのか、あるいは店の運営が安定するまでは喧伝したくないのか、どちらだろうか。

とはいえ、ものすごいスピードで店舗のスクラップアンドビルドを繰り返す姿勢にはリスペクトあるのみ。今後とも安旨の飲食店を提供し続けてください。

帰納的プログラミング

定義

プログラムに変更Xを加えるとき、類似の機能A,B,Cの実装を確認し、その共通部分から、変更Xに必要な実装を導くことがあります。

この一連の作業を、この記事では帰納的プログラミングと呼びます。

帰納(きのう、英: Induction、希: επαγωγή(エパゴーゲー))とは、個別的・特殊的な事例から一般的・普遍的な規則・法則を見出そうとする論理的推論の方法のこと。 出典: Wikipedia

帰納的プログラミングの例

このコミット帰納的プログラミングにより作成されました。

github.com

帰納する際は、次のコミットを参考にしています。

MDEV-28007 Deprecate Spider plugin variables regarding statistics per… · MariaDB/server@f31642e · GitHub

MDEV-28297 Deprecate spider_internal_offset · MariaDB/server@1866fb0 · GitHub

MDEV-27981 Deprecate spider_internal_limit · MariaDB/server@e87c710 · GitHub

帰納的プログラミングの良いところ

帰納的プログラミングを用いることで、開発者は対象のシステム・コードベースに対する知識をほとんど持たない場合でも正しそうな実装をすることができます。そして、知識を取得するコストを掛ける必要がないので、作業スピードも比較的速いです。

これはYAGNI原則に従っていると言えるかもしれません。ある作業が実際に必要になるまでは実行を遅延させることで、将来の不確定さに対するコストを最適化する考え方です。

上の例で言えば、作業者はMariaDBに関する周辺知識を習得するコストを将来に遅延させ、必要最小限のコストでPRを作成したのです。今回は間違いなく良く機能した例でしょう。

一方で、私の過去を振り返ると、必ずしもこれに頼りすぎるのは良くないと思うことがあります。次のセクションでまとめます。

帰納的プログラミングの欠点

帰納的プログラミングが悪く機能する場合も考えられます。この方法の最大の欠点は、既存のコードのパターンから抜け出せないということです。

すべての機能が、帰納プログラミングで既存のコードをコピペしたようなパターンで実装されていったとしましょう。この時、実はこの繰り返しパターンは抽出して共通化すべきだったかもしれません。DRY原則を常に実践する必要は一般的にありませんが、メンテナンス性やコードのアーキテクチャを考えると共通化すべき場面はやはり存在します。帰納的プログラミングを行使して他の方法を検討する手間を省いている場合、このような可能性を見逃すことになります。

あるいは、ある日既存のコードを大規模にリファクタすることになったとしましょう。全体最適なリファクタをするためには、変更対象の箇所に関する知識のみならず、コードベースに対する広範な知識が求められることがしばしばです。 これまで帰納的プログラミングを行使して必要以上の知識習得を遅延してきた場合、この局面になって初めて多くの知識を習得する必要が生じるのです。そうなると、果たして今回のリファクタタスクはいつものスピードで達成可能でしょうか。

帰納的プログラミングに過度に頼ることは、上記のようなリスクをはらんでいます。

欠点を回避するために

帰納的プログラミングで素早くタスクを終えられるのは良いことです。一方で、いつも必要最小限の知識を学ぶだけだと、今後のより高度なタスクに立ち向かえないリスクもあります。

このリスクを回避するためには、その時のタスク遂行には必要ではない周辺知識を学ぶ時間を意識して確保すると良いでしょう。チリも積もれば山になります。個々のタスクで積み上げた知識は、いずれより難しいタスクに立ち向かうときに効果を発揮するに違いありません。

あるいはタスクの割り振り方が非常に優れている場合は、常にタスク遂行に最小限の知識を学んでいるだけでも、順当にステップアップできる場合があります。信頼できるメンターがいる場合は、これに乗っかってしまうのも良いでしょう。*1

まとめ

帰納的プログラミングという言葉を定義し、その良し悪しを考えました。 簡単なタスクだからといってすぐに完了させてしまうよりは、あえて寄り道してみるのも大事かもしれません。

*1:今回のMariaDBのケースもこれに当てはまる気がしています。