こちら読み終わりました。
通称そーだい本というようです。
モチベーション
サーバーエンジニアとしてDBについてのスキルは避けては通れない。避けては通れないが、独学で身につきにくい。設計やアーキテクチャと同様、実際のプロダクトを開発・運用して経験しないと身につきにくい。と、思ってる。
経験豊富なエンジニアの方々はクラウド主流の開発よりも前から手を動かし続けてきた歴戦の戦士なので、DBサーバーの構築、運用、パフォーマンスチューニング、メンテナンスなどなどいろいろ経験されて今があるのだろうが、クラウドから入ったわたしのようなエンジニアはそのような経験をしなくてもDBインスタンスがボタンひとつで構築できる。RDS偉大
しかし、面接なんかで必ずといっていいほど「DBのパフォーマンスチューニングはできるか」といったような質問をされる。さすがに自信持ってできるとは言えない。だから、ずっとDBに対して課題感を抱いている。
なので今までも一応ミックさんの達人シリーズやSQLの基本くらいは技術書読んできたし、普通に業務でSQL書くし、MySQLだけでなくMongoやRedisのようなNoSQLも使ってきたけどスロークエリ検出してクエリのチューニングするような経験はしてきてない。ていうか、これ業務でやらないとやったことありますって言えないからこの質問されると辛過ぎるんだけど。
とはいえ、できることはやっておきたいのでDB関係の技術書で良さげなのは全部読んでおこうと思ったのです。そんなとき以下のような記事を読みました。
知ってるのが多かったけど「失敗から学ぶRDBの正しい歩き方」は初めて知った。どうやらSQLアンチパターンで紹介されているアンチパターンを現代的に紹介してくれてるようで、ちょうどSQLアンチパターンを次に読もうと思っていたのでこちらを読むことにした。
ちなみに、最近TiDBの採用や調査についての事例をよく見かける。DBの基礎知識を追いかけている間に世の中はCloud Spannerから始まったNewSQLと呼ばれる分散データベースという方向に進んでいるようだ。技術は進化し続けてる。Spannerなんかも最近の話かと思ってたけど2020年くらいから議論されてるんですね。全然知らんかった
今後DBの選定をするようなことがあればSpannerは使ってみたい。しかし、分散データベースの知識なんてない。そこで「詳説データベース」という本がいいらしいというのを耳にして読んでみたがわたしにはまだ早すぎた。いつかリベンジできる日が来ることを信じてる
JOINについて
JOINのコストについて考えたことはなかったかもしれない。JOINのアルゴリズムもいくつかあるようでMySQLのJOINアルゴリズムはNested Loop Joinというアルゴリズム。要は一行ずつループして処理する。データ量が多くなればJOINのコストが高くなるわけだ。
多段JOINがどれだけコストがかかるか。もし、JOINを多用するのであればその対象列にはインデックス不可欠だということを学んだ。
多段JOINになるほどの複雑なデータベースを扱ったことがないのと生のSQLを書くこともあるけどORMを使用した実装をすることが多かったのであんまりJOINしているという感覚もなかったのかもしれない。
インデックスについて
チューニングと聞いて真っ先に思いつくのがインデックスを貼ること。貼りすぎもよくないし、貼っても使われないことがあったりすることは一応知っている。カーディナリティのことや複合インデックスの指定する順番だったりが大事というのもなんとなくは知ってた。
本書を読むことでこれらのあいまいな知識の解像度が上がった気がする。
インデックスを使うよりもフルスキャンの方が早くなることがあるという感覚はなかった。貼ればいいわけでもないし、貼らないわけにもいかないしインデックスは難しい。
前にインデックス貼らない業務委託が後からインデックス貼る追加作業でお金とってるずるいみたいなこと言ってる人がいた。
本書を読むと最初から適切なインデックスなんて貼れるわけないのだからむしろ貼るメリットが明らかでない限り貼らないほうがいいだろうと感じる。データベースとは長い時間をかけてチューニングしていく覚悟が必要らしい。
DBはステートレスに保つ
DBには事実のみを記録し、基本的には状態を保つべきではない。いわゆる削除フラグなんかが代表的だそうな。
これはほんとに多用していてenabledとかdisabledとかそういうのつけがち。これはただの反省なのだけど普段ゲーム開発をしていて企画側からの要望であるアイテムが使用されなくなることを見越してマスタデータにenabledみたいなカラムを脳死でつけてた気がする。
そうすると、全てのクエリでこの有効フラグが有効なものに絞るようなクエリを書く必要が出てきて、非常に煩わしかったしバグの原因になりやすい。
上記の話はRDBではなくMongoだったけど十分にアンチパターンだったと思う。これがRDBになるとJOINが複雑になるなどの問題も出てくるらしい。
アプリケーションコードもそうだけどこういうフラグ実装は思いつきやすいし実装が簡単なのでやりがちだけど、気をつけなきゃいけないなという気づきを得た。
ページネーション難しくね?
本書ではソートの話でページネーションの話が出てきたと思う。ソートがJOINと並び高コストでRDBが苦手な操作という話で、RedisのようなNoSQLと組み合わせて使うことで苦手な部分補えるよみたいな話。
それはそうとページネーションってちゃんと実装しようとすると難しくないだろうか?Springで管理画面作ってた時に作ったページネーションはよく見るやつで次へも前への他にページ指定もできるやつ。これはLimit-Offsetで作った。Offsetで飛ばす分結局読み込むことになるのでデータ量が多いとパフォーマンス影響出るためアンチパターンとされている。
Mongoの時にもページネーション作ったけどその時はそのページの最終行のObjectIDを指定して次のページを取得するようなやつを作った。ObjectIDが作成時の日時を持っているので比較できるので最終行のObjectIDより時刻が後(前)のやつみたいにした気がする。
ページ指定がないページネーションならこれでいいけどページ指定する場合、これだと難しい。
で、Redisを使えばいいのかなとも思うのだけど意外とRedisを併用した一覧ページの実装例が出てこない。一応、自分の中で実装イメージがわかないままなのはちょっと気持ちが悪いので考えたのだけどあんまりいい案が思いつかない。
1番簡単なのはデータを作成するときにRedisの方にもidを突っ込んでおく。順序保証が欲しいのでSorted set。で、ページネーションでどの範囲のデータを取るかはこのRedisの方に入っているid群から指定の範囲で抜いてくる。あとはRedisから取ってきたIDでデータをRDBから取得して表示するみたいな感じだろうか?
これの問題点としてまずRedisにExpireを指定しない永続データを保持することになりRedisのデータが飛ぶと困る。Redisにデータがない時はRDBから取ってくるみたいにすればいいのかな。どっちにしろ、この実装Redisに何かあったとき完全にデータがズレる気がする。
あとはID順(作成順)ならいいけど名前順とかソート種別が複数あるときにどう対応するのがいいんだろうか。なんか気合いでできないこともない気がするけど簡単な実装ではすまない気がする。
ここまで考えてこれはもうElasticsearchのような全文検索エンジン使ったりするのかなと思ってきた、使ったことないからできるのかもわからないけど。でもElasticsearch ページネーションでなんかそれっぽい記事でてきたからたぶんできるのだろう。
なので、簡単なページネーション実装はまあシーク法でいけそう。Redisの使い方がいまいちわからない。単純じゃなさそうならElasticsearchのような全文検索エンジンを使うのかもしれない。というくらいの理解で自分を納得させた。
JSONをカラムに保存すること
本書でJSON型の話が出てた。JSON型というのも聞いたことはあったけどMySQLやPostgreSQLで使えるのは知らなかった。これを考えなしに使うのがアンチパターンになるのはなんとなくわかるのだけど過去のプロジェクトのことを思い出した。
かなり汎用的なデータを格納する必要がありtype1, value1, ..., type5, value5のようなカラムを保つテーブルを扱ったことがある。実際にはJSONデータをこのカラムに当てはめることになったのだがJSONの方が複雑すぎてこのカラムに当てはめるのがなかなかの無理ゲーすぎたし、このデータを画面で表示させるのが本当に辛かった。エンジニアになって1番辛い実装だった気がする。
本書を読んでこれはJSON型でよかったんじゃないかなと思った。汎用的なデータを扱うことを避けられないのであればJSONそのまま入れておいたほうがあんなにも苦しい思いをしなくてすんだろうなと。だって、Google Insights APIの結果そのままデータベースに突っ込んでたんだよ?わざわざマッピングさせる苦行する必要ないでしょ。
強過ぎる制約
これも気づきだった。そもそもNOT NULLとかUNIQUEくらいしか制約つけたことなくてCHECK制約とかENUMとか使ったことなかった。
あまりにも強過ぎる制約はビジネスロジックがDB側に漏れ出ており、アンチパターンになるそうな。どこでもビジネスロジックの流出は良くないんだね。
アプリケーション側と違い、データベースでこのようなアンチパターンを踏むと容易に変更ができなくなるのでアプリケーション以上に気をつける必要がありそう。
バックアップ、エラーログ、監視、コンフィグ、バージョン
これらはもうクラウドやSaaSでいい感じにやってもらう時代だと思っていてあんまり自分でごりごりセットアップする機会は少なくなってると思ってる。
RDSとかCloud SQL使っててconfigいじることってあるんだろうか?ログや監視はSRE的な領域の話になりつつあり、その領域をやる必要が出てきたら頑張ろうと思う。otelも。よくわかってないけど。
フレームワーク依存症
本書はたぶんRoRやLaravelの話をしてるんだと思う。Springの時にSpring dataというORMを使って開発していた。たまに生のSQLを書く時もあったけど基本的にはInterfaceに適切なメソッド名を宣言するだけでいい感じにDB操作ができたのでJOINしてる感覚とかもなかった気がする。
RoRのActive RecordなんかはよりDBを意識しないでDB操作する感覚なんだと思うからアプリケーション側のコードの都合がDBの設計に影響してしまうということも感覚的になんとなくわかる。
最近Goを書いていてそういうわかりやすいORMみたいなのがないからこのまま脱ORMの方向に向かってもいいのかもしれない。ORM嫌いな人多いよね?sqlcが開発体験よかったし。
ロックの知識
これもインデックスと並んで知っとかないといけない知識だと思う。明示的にロックを取らないとロックされることないのかと思ってたけど暗黙的にロック取得することもあるんだね。外部キー制約で親テーブルに対しても共有ロックとるのは知らなかった。
マイクロサービスの文脈でもよく見るACIDのI。このIsolationをどう保証するんですかというのが非常に難しいというかそこらへんの理解が求められる気がする。
パフォーマンスとデータ整合性のことを考えトランザクション分離レベルをどこに設定するのか、マイクロサービスの分散トランザクションをどこまで頑張るのか、マイクロサービスが厳しいのでモジュラモノリスにしたとしてもこのトランザクション境界のことは何かしらの答えをもってなくてはいけない。
トランザクションは難しい。
おわりに
本書は実際の開発現場でありがちなアンチパターンを非常に読みやすく説明してくれる良書でした!とにかく読みやすいからサクッと読めた。(オライリーは本当に体力持ってかれる。。)
本書で紹介されているアンチパターンを読んで、だいたい過去の開発のことを思い出してはアンチパターンを思いっきり踏んでたなぁとなりました。
とはいえ、わたしの開発経験なんてまだ2,3個のプロジェクトでほとんど新規開発だった。運用してきたプロダクトの数だけ、苦しんだプロダクトの数だけこのようなDBに関するノウハウ的なものが溜まっていくんだろうなーと思うと先人の先輩方に追いつける日は来るのだろうかと思ったりもする。
何度も言うけどこういう「プロダクトやってきた経験」が直結するスキルを伸ばすのは本当に難しい。まあやるだけなんだけど。ちゃんとやれる場所にいないといけないし、時間もかかる。
ChatGPTやCopilotを使って、爆速キャッチアップできる領域ではない。新しいプログラミング言語やフレームワークの習得は一人でもできるし、そういったスキルに比べると退屈なところも感じなくもないけど、AI補助が当たり前になってきた今こういうエンジニアとしての地力感のあるスキルは早めに習得していきたい。