テーブル作成時のSQL文をバッチ実行して高速化
create_table で発行される複数のSQL文を束ねて一括実行することで、大規模スキーマのロード時間を大幅に短縮します。1000以上のテーブルを持つPostgreSQLデータベースでの実測では、スキーマロードが約120秒から約25秒に改善されています。
背景
大規模なPostgreSQLデータベースで db:schema:load の実行時間が問題になっていました。PR作者の環境では1000以上のテーブルを持つスキーマのロードに約120秒かかっており、その原因が create_table 内で発行される複数のSQL文にありました。
これまでの create_table の実装では、1テーブルの作成に対して次のSQL文が個別に実行されていました:
-
DROP TABLE(forceオプション使用時) CREATE TABLE-
CREATE INDEX(supports_indexes_in_create?が false の場合、インデックスごと) -
COMMENT ON TABLE(コメントがある場合) -
COMMENT ON COLUMN(コメントのあるカラムごと) -
COMMENT ON INDEX(コメントのあるインデックスごと)
これらのSQL文が各テーブル作成のたびに個別のラウンドトリップとして発行されており、テーブル数に比例してオーバーヘッドが蓄積していました。
技術的な変更
create_table 内でSQL文を配列に蓄積し、最後に execute_batch で一括送信する方式に変更されました。
変更前:
td = build_create_table_definition(table_name, ...)
if force
drop_table(table_name, force: force, if_exists: true)
else
schema_cache.clear_data_source_cache!(table_name.to_s)
end
result = execute schema_creation.accept(td)
unless supports_indexes_in_create?
td.indexes.each do |column_name, index_options|
add_index(table_name, column_name, **index_options, ...)
end
end
if supports_comments? && !supports_comments_in_create?
change_table_comment(table_name, table_comment) if table_comment
td.columns.each do |column|
change_column_comment(table_name, column.name, column.comment) if column.comment.present?
end
end
変更後:
statements = []
if force
statements << drop_table_sql(table_name, force: force, if_exists: true)
end
schema_cache.clear_data_source_cache!(table_name.to_s)
td = build_create_table_definition(table_name, ...)
statements << schema_creation.accept(td)
unless supports_indexes_in_create?
td.indexes.each do |column_name, index_options|
index_definition = build_create_index_definition(table_name, column_name, **index_options, ...)
statements << schema_creation.accept(index_definition)
if supports_comments? && !supports_comments_in_create?
statements << change_index_comment_sql(index_definition.index) if index_definition.index.comment.present?
end
end
end
if supports_comments? && !supports_comments_in_create?
statements << change_table_comment_sql(table_name, table_comment) if table_comment
td.columns.each do |column|
statements << change_column_comment_sql(table_name, column.name, column.comment) if column.comment.present?
end
end
execute_batch statements
この変更を実現するために、SQL文字列を返す新しいヘルパーメソッド群が導入されています。これらは実行まで担っていた既存メソッドから「SQL生成」の責務を分離したものです:
-
drop_table_sql:DROP TABLE文を返す(MySQL・PostgreSQLそれぞれに実装) -
change_table_comment_sql:COMMENT ON TABLE文を返す(PostgreSQL) -
change_column_comment_sql:COMMENT ON COLUMN文を返す(PostgreSQL) -
change_index_comment_sql:COMMENT ON INDEX文を返す(PostgreSQL)
既存の change_table_comment・change_column_comment などのパブリックメソッドは、内部でこれらの新しい _sql メソッドを呼び出す形にリファクタリングされており、外部からの呼び出しインターフェースは変わりません。
変更前後で schema_cache.clear_data_source_cache! の呼び出し位置にも注目できます。変更前は force の有無で分岐していましたが、変更後は force の有無にかかわらず常に呼び出されるようになっています。
設計判断
既存の execute・drop_table を直接呼ぶ代わりに、SQL文字列を返す _sql メソッド群を新設する設計が採用されました。
副作用(DB実行)とSQL生成を分離することで、create_table の文脈ではSQL文字列のみを収集し、最後にまとめて送信できるようになっています。一方で change_column_comment のような既存のパブリックAPIはそのまま残され、_sql メソッドに委譲する形をとることで後方互換性を維持しています。
テスト面では、test_create_table_uses_batched_statements が追加されており、コメント・インデックス・カラムすべてを含む create_table(force: true)が クエリ1回 で完了することを assert_queries_count(1, include_schema: true) で保証しています。また、MySQLのスキーマテスト用スタブにも execute_batch のオーバーライドが追加され、バッチ実行に対応したテスト環境が整備されています。
まとめ
本PRは、create_table 内の複数SQL発行を execute_batch による一括送信に集約することで、大規模スキーマの読み込みを大幅に高速化した変更です。SQL生成と実行を分離する _sql ヘルパー群の導入により、既存APIへの互換性を保ちながらバッチ最適化を実現しており、テーブル数が多い環境ほど効果が顕著に現れる設計となっています。