昨年後半から、あるオンラインサロンでチーム開発に参加し、学んだことや調べたことをQiita記事に書いてきました。
当時書いた記事を振り返ってみて、「これも書いておけば良かった」と思う点がありましたので、本記事で追記します。
パフォーマンスチューニングの必要性が高まっているのでは?
私が業務でオンプレのプロジェクトに関わっていた時に、パフォーマンスチューニングについて提言したところ、「ハードウェアの性能も上がり続けてるし、サーバーのスペックを上げればええやん」と返ってきたことがあります。
「それでいいの?」と思いましたが、今回モダンなチーム開発に参加してみて、近年は逆にパフォーマンスチューニングの必要性が高まっていると肌で感じました。
Webサービスにおいて、パフォーマンスチューニングが重要な理由
- コスト削減:リソースの無駄遣いが減って、コスト削減に繋がります。
- UXの向上:応答が速い方がユーザー体験が良いです。
- SEO:検索エンジンはパフォーマンスをランキング要因として考慮します。
個人開発では、特にコスト面が気になりますね。
ORM再考:パフォーマンスチューニングの観点から
今回のチーム開発では、私はデータベース設計とアプリのCRUD実装を主に担当しました。
以下、RDBのパフォーマンスチューニングの観点から、ORMについて再考したいと思います。
パフォーマンス上有利な新型ORMが登場
私のオレオレ用語で申し訳ないですが、ORMを以下のように分類しています。
- 旧型ORM:RailsのActiveRecordに代表される、SQLを隠すタイプのORM
- 新型ORM:SQLを隠さない薄いORM。本記事ではクエリービルダーも新型ORMに含めます。
私はSQLを書きたい人ですから、新型ORMを知ったときは「こういうのでいいんだよ、こういうので」と思いました。
また、私の過去記事ではあまり触れませんでしたが、新型ORMはパフォーマンスチューニングがしやすいです。
ORMの記事一覧
パフォーマンスについて書く前に、ORMについての私のQiita記事を貼っておきます。
今まで4つのORM(Go言語用2つ、TypeScript用2つ)を試しました。
これら4つは、私はすべて新型ORMに分類しています。
私はGo言語のチームに所属していましたので、チーム開発ではsqlcとBunを使用しましたが、今後のGo言語での開発はBunを選ぶと思います。
TypeScriptでの開発は、今のところKyselyに気持ちが傾いています。
Go言語用コードジェネレーター「sqlc」
チーム開発参加の記録【2023-06~2023-08】(2) sqlc + jackc/pgx/v5(v5.4.0)を使ってみた
チーム開発参加の記録【2023-06~2023-08】(3) sqlc + jackc/pgx/v5(v5.4.1)からPostgreSQLの複合型の配列を使ってみた
チーム開発参加の記録【2023-06~2023-08】(4) sqlc + jackc/pgx/v5 からPostgreSQLの複合型の配列を更新してみた
チーム開発参加の記録【2023-06~2023-08】(6) PostgreSQLの複合型の配列の更新について、もう少し煮詰める
Go言語用ORM「Bun」
チーム開発参加の記録【2023-10~2024-03】(1) Go言語用ORM「Bun」をDBファーストで使う試み(SQLite使用)
チーム開発参加の記録【2023-10~2024-03】(2) Go言語からTursoを使ってみた
チーム開発参加の記録【2023-10~2024-03】(4) Go言語用ORM「Bun」をDBファーストで使う試み(PostgreSQL使用)
チーム開発参加の記録【2023-10~2024-03】(5) Go言語用ORM「Bun」でトランザクション、UPSERT、JOINを使ってみた
チーム開発参加の記録【2023-10~2024-03】(6) Go言語用ORM「Bun」で複数のクエリーをまとめてDBサーバーで実行
TypeScript用ORM「Drizzle ORM」
TypeScript用ORM「Drizzle ORM」をDBファーストで使う試み(SQLite編)
TypeScript用ORM「Drizzle ORM」をDBファーストで使う試み(PostgreSQL編)
TypeScript用クエリービルダー「Kysely」
TypeScript用クエリービルダー「Kysely」をDBファーストで使う試み(SQLite編)
TypeScript用クエリービルダー「Kysely」をDBファーストで使う試み(PostgreSQL編)
TypeScript用クエリービルダー「Kysely」でトランザクション、UPSERT、JOINを使ってみた
コードファースト vs. データベースファースト
それでは、上記の過去記事を振り返りながらパフォーマンスについて見ていきます。
過去記事では一貫してデータベースファーストでORMを試し、上記4つのORMのいずれもデータベースファーストで使えることを確認しました。
私がデータベースファーストを採用する理由はこの記事に書きましたが、「DBとアプリを疎結合にしたいから」というのが最も大きいです。
一般的にDBはアプリより寿命がずっと長いし、複数のアプリから共有されることも多いので、DBを特定のアプリやORMに依存させたくありません。
データベースファーストのアプローチは、パフォーマンス面でもメリットがあります。
使用するDBに最適なDDLを書けるので、DBの独自機能を利用してパフォーマンスを上げることが可能です。
一方でコードファーストにすると、ORMで各種DBの違いを吸収できて、DBを替えても同じソースコードで動くメリットが場合によっては生きるのですが、DBの独自機能を利用したパフォーマンスチューニングは制限されるでしょう。
厚いORM vs. 薄いORM
旧型ORMを厚いORMと表現しました。
要はSQLを隠ぺいして、開発者が自由にSQLを操れないORMです。
※ Raw SQLを使えば何でもできますが、ORMのメリットが生きないため評価の対象外としています。
厚いORMは重厚長大で機能が豊富な傾向にありますが、SQLを書きたい開発者にとっては不要な機能が多いです。
SQL生成機能がメインで使われていると思いますが、生成されるSQLの質が必ずしも良くなくて、N+1問題によりパフォーマンスが劣化する話は有名になりました。
生成されるSQLの質を気にしながらORMを使うぐらいなら、最初から自分でSQLを書く方が楽だと感じている開発者も多いと思います。
一方でSQLに薄皮をかぶせただけの新型ORMは、SQLインジェクション対策等のORMのメリットを享受しながら、開発者がかなり自由にSQLを操れます。
DBの独自機能を活用したクエリーチューニングもできて、DBのパフォーマンス向上に有利です。
DBとアプリ間の通信を減らす方法
- 一撃SQLを書く
SELECT文においては、N+1問題の逆をやれば良いです。
なるべく一撃でデータを取れるようなSELECT文を書きます。
問い合わせ回数自体が減りますし、ネットワーク通信回数やSQLコンパイル回数等、各種オーバーヘッドも減らすことができます。
もしそのSELECT文が遅かったら、DBの機能を利用してチューニングするのが基本です。
多くのDBにはチューニングのための様々な機能や、JOINの優れたアルゴリズム等が用意されていますし、場合によってはDB設計を見直すのも有効です。
- ストアドプロシージャを利用する
ストアドプロシージャはDB側で実行されます。
ある程度まとまった処理をストアドプロシージャに書いてアプリから呼び出すことで、DBとアプリ間のネットワーク通信を減らすことができます。
また、ストアドプロシージャはDB登録時にコンパイルされるので、実行時のSQLコンパイル回数を減らす効果もあります。
- アプリから複数のクエリーをまとめてDBに送信して実行する
Go言語用ORM「Bun」で使えるテクニックです。
方法はこちらの記事に書きました。
ストアドプロシージャを使うよりも圧倒的に開発者体験が良いです。
今回のチーム開発でも、DBを更新するバッチ処理でこのテクニックを使いました。
INSERT文、UPDATE文、DELETE文をアプリ側で貯めておいて、都度件数を調べ、クエリーが100件を越えていたらDBにまとめて送って実行する実装にしました。
- (Turso限定)libSQLのユーザー定義関数(UDF)を使う
Tursoで採用されているlibSQLでは、WebAssemblyで動くユーザー定義関数(UDF)を作れるそうです。
https://blog.turso.tech/webassembly-functions-for-your-sqlite-compatible-database-7e1ad95a2aa7
私は試していませんが、Turso版ストアドプロシージャという印象を持っています。
パフォーマンスチューニング以外の話題
チューニング以外の話題も書いておきます。
リレーションとリレーションシップの違い
この記事の最後におまけ的に書きましたが、リレーションとリレーションシップは全く別物です。
Bunもそうですが、Relationshipと命名すべきメソッドをRelationと命名しているORMが多いようなので、注意が必要です。
リレーション | リレーションシップ | |
---|---|---|
モデル | リレーショナルモデル | ERモデル |
日本語訳 | 関係(=数学用語) | 関連、関係性 |
意味 | 集合 | (同上) |
RDBでは | テーブル(表) | ・外部キー制約 ・結合テーブル |
両者はそもそもモデルが違います。
両モデルを考えた人も別人です。
「リレーション」と日本語訳の「関係」は数学用語なので、一般用語の意味で捉えない方が良いです。
大変紛らわしいので、私はリレーションを「関係」と呼ばずに「集合」と呼んでいます。
リレーション(=集合)は、RDBではテーブル(=行の集合)が該当します。
リレーションとリレーションシップを混同することの弊害
エンティティー間の関連(=リレーションシップ)がリレーションだという勘違いが非常に多いのですが、両者を混同することの弊害は、「テーブル=集合であり、SQLは集合を演算するための宣言型言語である」ことへの理解が進まないことです。
広く使われているオブジェクト指向言語とは全く感覚が異なるため、嫌がられることも多いSQLですが、モダンなプログラミングでは宣言的にコードを書くのが良いとされていますから、SQLが宣言型言語であり、関数型言語と感覚が近い高級言語だということは認識してほしいです。
SQLはもともと抽象度の高い宣言型言語な上、ISO規格でも標準化されていますから、オブジェクト指向でオレオレ抽象化し直す必要などなく、そのまま使うのが良いというのが私の考えです。
旧型ORMはクリーンアーキテクチャなのか?
別の話題に移ります。
旧型ORMは、最も疎結合にすべきDBとアプリを密結合させた時点で、もはやクリーンではないと思いますが、「ANSI/SPARC 3層スキーマ」の観点からもクリーンでないと考えます。
3層スキーマとは、データベースのスキーマを「外部スキーマ/概念スキーマ/内部スキーマ」の3階層で定義する方式ですね。
データベーススペシャリスト試験で出題される知識です。
3層スキーマでは、ユーザー(アプリも含む)は「外部スキーマを介してデータにアクセスせよ」と言っています。
外部スキーマとは、ビューや、SQLの結果表のことですね。
一方で、RailsのチュートリアルはアプリのModelとDBの実テーブルを1対1で対応させているそうですが、この実テーブルは概念スキーマです。
3層スキーマに従うなら、アプリからは外部スキーマを見るのがクリーンなはずですが、旧型ORMは概念スキーマと密結合してごにょごにょやっているので、クリーンではないというのが私の意見です。
以上の観点は過去の記事にも書きました。