# ALGORITHM=INPLACE の DDL の内部実装 mariadb の説明 https://mariadb.com/kb/en/innodb-online-ddl-overview/#inplace-algorithm > INPLACEアルゴリズムは、操作をインプレースで実行し、可能な場合はテーブルのコピーと再構築を回避することで、これを回避する方法として導入されました。 よくわからん...table rebuild なしにどうやって回避しているのか... DDL の実行関数っぽいところ → https://github.com/mysql/mysql-server/blob/1bfe02bdad6604d54913c62614bde57a055c8332/sql/sql_table.cc#L16162 ```cpp bool mysql_alter_table(THD *thd, const char *new_db, const char *new_name, HA_CREATE_INFO *create_info, Table_ref *table_list, Alter_info *alter_info) ``` INPLACE で止まりそうなところにブレークポイントを置く → https://github.com/mysql/mysql-server/blob/1bfe02bdad6604d54913c62614bde57a055c8332/sql/sql_table.cc#L17249 ``` (gdb) b sql/sql_table.cc:17249 Breakpoint 1 at 0xaaaac59afe44: file /mysql-8.0.32/sql/sql_table.cc, line 17249. ``` client から実行した DDL ``` mysql> alter table employees add hoge int, algorithm=inplace; ``` 止まる ``` Thread 40 "connection" hit Breakpoint 1, mysql_alter_table (thd=0xfffedc001070, new_db=0xfffedcab3ad0 "employees", new_name=0x0, create_info=0xffff5067b688, table_list=0xfffedcab3480, alter_info=0xffff5067b7c0) at /mysql-8.0.32/sql/sql_table.cc:17249 17249 bool use_inplace = true; (gdb) ``` おもむろに引数を見てみる ``` (gdb) i args thd = 0xfffedc001070 new_db = 0xfffedcab3ad0 "employees" new_name = 0x0 create_info = 0xffff5067b688 table_list = 0xfffedcab3480 alter_info = 0xffff5067b7c0 ``` 中身見れた、ポインタなので * 付けてみればいいだけだった感 ``` (gdb) print *table_list $4 = {next_local = 0x0, next_global = 0x0, prev_global = 0xfffedc0044d0, db = 0xfffedcab3ad0 "employees", table_name = 0xfffedcab2b80 "employees", alias = 0xfffedcab3470 "employees", target_tablespace_name = {str = 0x0, length = 0}, option = 0x0, opt_hints_table = 0x0, opt_hints_qb = 0x0, m_tableno = 0, m_map = 1, m_join_cond = 0x0, m_is_sj_or_aj_nest = false, sj_inner_tables = 0, natural_join = 0x0, is_natural_join = false, join_using_fields = 0x0, join_columns = 0x0, is_join_columns_complete = false, next_name_resolution_table = 0x0, index_hints = 0x0, table = 0xfffedc0eac00, table_id = {static TABLE_ID_MAX = 281474976710655, m_id = 0}, derived_result = 0x0, correspondent_table = 0x0, table_function = 0x0, access_path_for_derived = 0x0, derived = 0x0, m_common_table_expr = 0x0, m_derived_column_names = 0x0, schema_table = 0x0, schema_query_block = 0x0, schema_table_reformed = false, query_block = 0xfffedcab27e8, view = 0x0, field_translation = 0x0, field_translation_end = 0x0, merge_underlying_list = 0x0, view_tables = 0x0, belong_to_view = 0x0, referencing_view = 0x0, parent_l = 0x0, security_ctx = 0x0, view_sctx = 0x0, next_leaf = 0x0, derived_where_cond = 0x0, check_option = 0x0, replace_filter = 0x0, select_stmt = {str = 0x0, length = 0}, source = {str = 0x0, length = 0}, timestamp = {str = 0x0, length = 0}, definer = {user = {str = 0x0, length = 0}, host = {str = 0x0, length = 0}, current_auth = {str = 0x0, length = 0}, uses_replace_clause = false, retain_current_password = false, discard_old_password = false, alter_status = {update_password_expired_fields = false, update_password_expired_column = false, use_default_password_lifetime = true, expire_after_days = 0, update_account_locked_column = false, account_locked = false, password_history_length = 0, use_default_password_history = true, update_password_history = 111, password_reuse_interval = 0, use_default_password_reuse_interval = false, update_password_reuse_interval = false, failed_login_attempts = 0, update_failed_login_attempts = false, password_lock_time = 0, update_password_lock_time = false, update_password_require_current = Lex_acl_attrib_udyn::UNCHANGED}, first_factor_auth_info = {plugin = {str = 0xaaaac7ec2db8 "", length = 0}, auth = {str = 0x0, length = 0}, generated_password = {str = 0x0, length = 0}, challenge_response = {str = 0x0, length = 0}, nth_factor = 1, uses_identified_by_clause = false, uses_authentication_string_clause = false, uses_identified_with_clause = false, has_password_generator = false, passwordless = false, add_factor = false, modify_factor = false, drop_factor = false, requires_registration = false, unregister = false, init_registration = false, finish_registration = false}, mfa_list = {<base_list> = {first = 0xaaaacaaedf90 <end_of_list>, last = 0xfffedcab36f0, elements = 0}, <No data fields>}, with_initial_auth = false}, updatable_view = 0, algorithm = 0, view_suid = 0, with_check = 0, effective_algorithm = VIEW_ALGORITHM_UNDEFINED, m_lock_descriptor = {type = TL_READ_NO_INSERT, action = THR_DEFAULT}, grant = { grant_table = 0x0, version = 0, privilege = 2147483647, m_internal = {m_schema_lookup_done = true, m_schema_access = 0x0, m_table_lookup_done = true, m_table_access = 0x0}}, outer_join = false, join_order_swapped = false, shared = 0, db_length = 9, table_name_length = 9, m_updatable = true, m_insertable = true, m_updated = false, m_inserted = false, m_deleted = false, m_fulltext_searched = false, straight = false, updating = true, ignore_leaves = false, dep_tables = 0, join_cond_dep_tables = 0, nested_join = 0x0, embedding = 0x0, join_list = 0x0, cacheable_table = true, open_type = OT_TEMPORARY_OR_BASE, contain_auto_increment = false, check_option_processed = false, replace_filter_processed = false, required_type = dd::enum_table_type::BASE_TABLE, timestamp_buffer = '\000' <repeats 19 times>, prelocking_placeholder = false, open_strategy = Table_ref::OPEN_NORMAL, internal_tmp_table = false, is_alias = false, is_fqtn = false, m_was_scalar_subquery = false, view_creation_ctx = 0x0, view_client_cs_name = {str = 0x0, length = 0}, view_connection_cl_name = {str = 0x0, length = 0}, view_body_utf8 = {str = 0x0, length = 0}, is_system_view = false, is_dd_ctx_table = false, derived_key_list = {<base_list> = {first = 0xaaaacaaedf90 <end_of_list>, last = 0xfffedcab3838, elements = 0}, <No data fields>}, trg_event_map = 0 '\000', schema_table_filled = false, mdl_request = {type = MDL_SHARED_UPGRADABLE, duration = MDL_TRANSACTION, next_in_list = 0x0, prev_in_list = 0xfffedcab4128, ticket = 0xfffedc0be0a0, key = {m_length = 21, m_db_name_length = 9, m_object_name_length = 9, m_ptr = "\004employees\000employees", '\000' <repeats 366 times>}, m_src_file = 0xaaaac866e810 "/mysql-8.0.32/sql/sql_parse.cc", m_src_line = 6093}, view_no_explain = false, partition_names = 0x0, m_join_cond_optim = 0x0, cond_equal = 0x0, optimized_away = false, derived_keys_ready = false, m_is_recursive_reference = false, m_table_ref_type = TABLE_REF_BASE_TABLE, m_table_ref_version = 96, covering_keys_saved = {map = 0}, merge_keys_saved = {map = 0}, keys_in_use_for_query_saved = {map = 0}, keys_in_use_for_group_by_saved = {map = 0}, keys_in_use_for_order_by_saved = {map = 0}, nullable_saved = false, force_index_saved = false, force_index_order_saved = false, force_index_group_saved = false, lock_partitions_saved = { bitmap = 0x0, n_bits = 0, last_word_mask = 0, last_word_ptr = 0x0}, read_set_saved = {bitmap = 0x0, n_bits = 0, last_word_mask = 0, last_word_ptr = 0x0}, write_set_saved = {bitmap = 0x0, n_bits = 0, last_word_mask = 0, last_word_ptr = 0x0}, read_set_small = { 2408550287, 2408550287}, write_set_small = {2408550287, 2408550287}} ``` ステップ実行したら、fill_alter_inplace_info() に移る https://dev.mysql.com/doc/dev/mysql-server/latest/sql__table_8cc.html#a99999ae4beee5347de1e3adefce512ac https://github.com/mysql/mysql-server/blob/1bfe02bdad6604d54913c62614bde57a055c8332/sql/sql_table.cc#L11800 [Alter_inplace_info](https://dev.mysql.com/doc/dev/mysql-server/latest//classAlter__inplace__info.html) に DDL 発行前後での差分を詰める関数っぽい Alter_inplace_info のインスタンスは storage engine (SE) に inplace で実行できるか判断してもらうために渡される 色々 INPLACE で実行できるか判断した後に、行けると判断したらここに飛びそう → https://github.com/mysql/mysql-server/blob/1bfe02bdad6604d54913c62614bde57a055c8332/sql/sql_table.cc#L17405 ## mysql_inplace_alter_table() >mysql_alter_tableがテーブルをコピーする必要がない場合、ストレージエンジンが変更について知る必要がなく、frmだけが変更されるalterテーブルであるか、ストレージエンジンがmysqlがテーブルをコピーすることなく、インプレースで直接alterテーブル操作を実行することをサポートしているかのどちらかです。 https://github.com/mysql/mysql-server/blob/1bfe02bdad6604d54913c62614bde57a055c8332/sql/sql_table.cc#L13199 途中で失敗したら goto cleanup する。 cleanup のセクションでは [close_temporary_table()](https://github.com/mysql/mysql-server/blob/1bfe02bdad6604d54913c62614bde57a055c8332/sql/sql_base.cc#L2358) が呼ばれている。 最初に、ロックを exclusive, shared_no_write などのレベルにアップグレードする。 > EXCLUSIVEロックにアップグレードする: > - ストレージ・エンジンから要求された場合 > - または、ストレージ エンジンが準備フェーズだけに排他ロックを必要とする場合。 > - またはユーザーによって要求された場合 > >LOCK TABLESの下で、ストレージエンジンが準備段階で排他ロックを必要とする場合、ステートメント全体で排他ロックが必要な場合と同じ方法で処理することに注意してください。 >以下の場合、SHARED_NO_WRITEロックにアップグレードする: >- ストレージ・エンジンが書き込みを全期間ブロックする必要がある場合。 >- またはユーザーから要求された場合 >LOCK TABLESでは、すでにSHARED_NO_READ_WRITEが設定されていることに注意してください。 外部キーの変更をする場合、名称のロックを取る? > 新しい外部キーの名前のロックを取得する。INPLACEアルゴリズムが元のテーブルのデータベースに新しいテーブル定義を作成する。 外部キーの名前が競合すると面倒なことになるっぽい > ALTER TABLEの結果、外部キー名の競合が発生するかどうかを、高いコストになる可能性のあるINPLACE ALTERのフェーズを開始する前に確認する。 [ここまで終わったら table の lock を取る。](https://github.com/mysql/mysql-server/blob/1bfe02bdad6604d54913c62614bde57a055c8332/sql/sql_table.cc#L13323) ```cpp // It's now safe to take the table level lock. if (lock_tables(thd, table_list, alter_ctx->tables_opened, 0)) goto cleanup; ``` [online かはここで決めてそう。](https://github.com/mysql/mysql-server/blob/1bfe02bdad6604d54913c62614bde57a055c8332/sql/sql_table.cc#L13386) exclusive (書き込みも読み込みも lock) や shared (書き込みのみ lock)だと online にならない。 ```cpp switch (inplace_supported) { case HA_ALTER_ERROR: case HA_ALTER_INPLACE_NOT_SUPPORTED: assert(0); [[fallthrough]]; case HA_ALTER_INPLACE_NO_LOCK: case HA_ALTER_INPLACE_NO_LOCK_AFTER_PREPARE: switch (alter_info->requested_lock) { case Alter_info::ALTER_TABLE_LOCK_DEFAULT: case Alter_info::ALTER_TABLE_LOCK_NONE: ha_alter_info->online = true; break; case Alter_info::ALTER_TABLE_LOCK_SHARED: case Alter_info::ALTER_TABLE_LOCK_EXCLUSIVE: break; } break; case HA_ALTER_INPLACE_EXCLUSIVE_LOCK: case HA_ALTER_INPLACE_SHARED_LOCK_AFTER_PREPARE: case HA_ALTER_INPLACE_SHARED_LOCK: case HA_ALTER_INPLACE_INSTANT: break; } ``` [ha_prepare_inplace_alter_table()](https://github.com/mysql/mysql-server/blob/1bfe02bdad6604d54913c62614bde57a055c8332/storage/innobase/handler/ha_innopart.h#L269) が呼ばれる。 >(Check_if_supported_inplace_alter() が HA_ALTER_INPLACE_NO_LOCK を返さなかった場合)同時書き込みがブロックされた状態で、InnoDB が内部構造を更新できるようにします。これは inplace_alter_table() の前に起動される。 中で呼ばれてるのは [ha_innobase::prepare_inplace_alter_table_impl](https://github.com/mysql/mysql-server/blob/1bfe02bdad6604d54913c62614bde57a055c8332/storage/innobase/handler/handler0alter.cc#L5427) [prepare_inplace_alter_table_impl で呼ばれてる処理の例](https://github.com/mysql/mysql-server/blob/1bfe02bdad6604d54913c62614bde57a055c8332/storage/innobase/handler/handler0alter.cc#L5579) ```cpp /* Check that index keys are sensible */ error = innobase_check_index_keys(ha_alter_info, indexed_table); ``` [innobase_check_index_keys](https://github.com/mysql/mysql-server/blob/1bfe02bdad6604d54913c62614bde57a055c8332/storage/innobase/handler/handler0alter.cc#L2407) では、追加しようとしている index が問題ないかをチェックしている。名前が重複がないかのみチェックしている。 [ok_to_rename_column](https://github.com/mysql/mysql-server/blob/1bfe02bdad6604d54913c62614bde57a055c8332/storage/innobase/handler/handler0alter.cc#L713) では、カラム名の変更をするときに既に存在するカラム名に変更しようとしていないかチェックしている。 ちょくちょく出てくる [innobase_need_rebuild](https://github.com/mysql/mysql-server/blob/1bfe02bdad6604d54913c62614bde57a055c8332/storage/innobase/handler/handler0alter.cc#L920) は ALTER TABLE がテーブルリビルドが必要かどうか決める関数。 INNOBASE_ALTER_REBUILD という定数である場合 rebuild が必要で、以下に rebuild が必要な DDL が書いてある。 https://github.com/mysql/mysql-server/blob/1bfe02bdad6604d54913c62614bde57a055c8332/storage/innobase/handler/handler0alter.cc#L119 → mysql_inplace_alter_table() に戻る ロックのダウングレードをしている >ストレージ・エンジンから、排他ロックは準備フェーズにのみ必要であると言われ(LOCK TABLESの下でない場合を除く)、ユーザーが明示的に排他ロックを要求していない場合、ロックをダウングレードする。 https://github.com/mysql/mysql-server/blob/1bfe02bdad6604d54913c62614bde57a055c8332/sql/sql_table.cc#L13418 ha_inplace_alter_table() を実行 https://github.com/mysql/mysql-server/blob/1bfe02bdad6604d54913c62614bde57a055c8332/sql/sql_table.cc#L13437 → [ha_innobase::inplace_alter_table_impl](https://github.com/mysql/mysql-server/blob/1bfe02bdad6604d54913c62614bde57a055c8332/storage/innobase/handler/handler0alter.cc#L6124) に辿り着く。 ## 2/25 (日) 答えの書いてそうな doc を見つけた https://klouddb.io/understanding-how-online-ddl-inplace-works-in-mysql/