MariaDBカラム取得時の不要なクエリを削減する最適化
MariaDBの関数デフォルト検出を new_column_from_field から column_definitions へ移動することで、非MariaDB接続での余分なクエリを完全に排除し、MariaDB接続でも SHOW CREATE TABLE の発行を最小化しました。
背景
#44654 によるMariaDBの関数デフォルト検出の修正が、大規模スキーマで深刻なクエリ過多を引き起こしていました。MariaDBでは、スキーマダンプ時に関数デフォルト(UUIDの自動設定など)が文字列として誤認識される問題があり、#44654 はこれを new_column_from_field 内で SHOW CREATE TABLE を呼び出すことで解決しました。しかしこの実装では、カラムの数だけ SHOW CREATE TABLE が実行されていました。
問題の本質は、このチェックが MariaDB以外の接続でも実行されていた 点にあります。new_column_from_field はMySQL・MariaDB共通のコードパスであるため、MySQLを使用するアプリケーションでも大量の不要クエリが発生していました。テストコードの変更が示す通り、スキーマ情報取得に必要なクエリ数は4回から3回に削減されています。
技術的な変更
column_definitions メソッドで SHOW FULL FIELDS の結果を受け取った直後に、MariaDB専用の処理を一括で行う update_fields_for_mariadb メソッドが追加されました。これにより、MariaDB接続でのみ追加処理が実行されるようになります。
変更前: column_definitions は単純なクエリを返すだけで、関数デフォルト判定は new_column_from_field 内で列ごとに実行されていました。
def column_definitions(table_name) # :nodoc:
query_all("SHOW FULL FIELDS FROM #{quote_table_name(table_name)}")
end
変更後: column_definitions が update_fields_for_mariadb を呼び出し、フィールド情報を前処理します。
def column_definitions(table_name) # :nodoc:
fields = query_all("SHOW FULL FIELDS FROM #{quote_table_name(table_name)}")
update_fields_for_mariadb(table_name, fields) if mariadb?
fields
end
def update_fields_for_mariadb(table_name, fields)
has_function_default_candidate = fields.any? do |field|
default = field["Default"]
default&.match?(/[a-zA-Z_]\w*\(/) && !/\ACURRENT_TIMESTAMP/i.match?(default)
end
if has_function_default_candidate
table_info = create_table_info(table_name)
fields.each do |field|
default = field["Default"]
next unless default&.match?(/[a-zA-Z_]\w*\(/)
next if /\ACURRENT_TIMESTAMP/i.match?(default)
field_name = field["Field"]
match = table_info&.match(/`#{field_name}` .+ DEFAULT ('|\d+|[A-z]+)/)
if match && match[1].match?(/\A[A-z]/)
field["Extra"] = "DEFAULT_GENERATED"
end
end
end
end
update_fields_for_mariadb の処理は2段階になっています。まず全フィールドをスキャンして「関数デフォルトの候補」(func_name( パターンかつ CURRENT_TIMESTAMP 以外)が1つでも存在するか確認します。候補がある場合のみ SHOW CREATE TABLE を1回だけ発行し、各フィールドに DEFAULT_GENERATED を設定することで、既存のMySQLコードパスへ統合します。
new_column_from_field 側では、削除されたコードも重要です。MariaDB専用だった default_type メソッドが完全に削除され、DEFAULT_GENERATED ブランチがMariaDB・MySQL別に分岐するよう修正されました。
elif type_metadata.extra == "DEFAULT_GENERATED"
if mariadb?
default, default_function = nil, default
else
default = "(#{default})" unless default.start_with?("(")
default = default.gsub("\\'", "'")
default, default_function = nil, default
end
MariaDBの場合はデフォルト値をそのまま default_function として扱い、MySQLの場合は従来通りに括弧付きへの変換とエスケープ処理を行います。これは両者の SHOW FULL FIELDS 出力形式の差異に対応しています。
設計判断
「候補の事前スクリーニング」 によって SHOW CREATE TABLE の発行を条件付きにする2段階の判定が採用されました。
ナイーブな実装では MariaDB 接続のたびに SHOW CREATE TABLE を発行することも考えられますが、本PR では全フィールドの Default を any? でスキャンして候補がない場合は追加クエリを完全にスキップします。関数デフォルトを持たない大多数のテーブルではオーバーヘッドがゼロになります。
また、DEFAULT_GENERATED を Extra フィールドに書き込む設計により、new_column_from_field の既存分岐をそのまま再利用できています。MariaDB専用の検出ロジックを column_definitions 層に隔離しつつ、後段の処理は共通インターフェースで統合するというレイヤー分離の判断といえます。
まとめ
この変更は、#44654 で導入された正しい修正に伴うパフォーマンス上の副作用を、責務の再配置によって解消しています。関数デフォルト検出を適切な抽象レイヤー(column_definitions)に移動し、接続種別によるショートサーキットと候補スクリーニングを組み合わせることで、非MariaDB接続での余分なクエリを完全に排除しつつ、MariaDB接続でも最悪ケースを「テーブルあたり1クエリ」に抑えています。