「テーブルを開く」とは何ぞやの話
追うぞ
今回は「テーブルを開く」とは何を指す言葉なのかを追う。 fopenとやっていることは同じなら、ハンドラを作成してそのハンドラを使って各種テーブル操作ができる状態にすることである(と思う)。同じ感覚でいいのか確かめていく。
ha_innobase::openまでのバックトレース
全体像を確かめていく。そのためにそれっぽいところまでのバックトレースを眺める。
以下は、FLUSH TABLE
&testというテーブルにinsertを行った際のha_innobase::openまでのバックトレースである。yokuさんの記事に記載があるように、データディクショナリにアクセスするタイミングで1回目のブレークポイントがやってくる。ここではcolumn_statisticsというテーブルをopenしている。
(gdb) bt #0 ha_innobase::open (this=0x50000ffff, name=0x0, open_flags=0, table_def=0x100000000) at /mysql-8.0.28/storage/innobase/handler/ha_innodb.cc:6995 #1 0x000056101dab35b8 in handler::ha_open (this=0x7f2b48ad0110, table_arg=0x7f2b4818d250, name=0x7f2b48ac0ce0 "./mysql/column_statistics", mode=2, test_if_locked=2, table_def=0x7f2b4813cde8) at /mysql-8.0.28/sql/handler.cc:2812 #2 0x000056101d87e8a4 in open_table_from_share (thd=0x7f2b480012d0, share=0x7f2b48ac0930, alias=0x7f2bc27d5b40 "column_statistics", db_stat=39, prgflag=8, ha_open_flags=0, outparam=0x7f2b4818d250, is_create_table=false, table_def_param=0x7f2b4813cde8) at /mysql-8.0.28/sql/table.cc:3175 #3 0x000056101d5d39f1 in open_table (thd=0x7f2b480012d0, table_list=0x7f2b4819bd18, ot_ctx=0x7f2bbc6f34d0) at /mysql-8.0.28/sql/sql_base.cc:3367 #4 0x000056101d5d7732 in open_and_process_table (thd=0x7f2b480012d0, lex=0x7f2b48004320, tables=0x7f2b4819bd18, counter=0x7f2bbc6f35b0, prelocking_strategy=0x7f2bbc6f3550, has_prelocking_list=false, ot_ctx=0x7f2bbc6f34d0) at /mysql-8.0.28/sql/sql_base.cc:5034 #5 0x000056101d5d92d5 in open_tables (thd=0x7f2b480012d0, start=0x7f2bbc6f35a8, counter=0x7f2bbc6f35b0, flags=18434, prelocking_strategy=0x7f2bbc6f3550) at /mysql-8.0.28/sql/sql_base.cc:5842 #6 0x000056101d5e6cb0 in open_tables (thd=0x7f2b480012d0, tables=0x7f2bbc6f35a8, counter=0x7f2bbc6f35b0, flags=18434) at /mysql-8.0.28/sql/sql_base.h:455 #7 0x000056101edfd2da in dd::Open_dictionary_tables_ctx::open_tables (this=0x7f2bbc6f3650) at /mysql-8.0.28/sql/dd/impl/transaction_impl.cc:107 #8 0x000056101ecf9072 in dd::cache::Storage_adapter::get<dd::Item_name_key, dd::Column_statistics> ( thd=0x7f2b480012d0, key=..., isolation=ISO_READ_COMMITTED, bypass_core_registry=false, object=0x7f2bbc6f3760) at /mysql-8.0.28/sql/dd/impl/cache/storage_adapter.cc:170 #9 0x000056101ecf12a8 in dd::cache::Shared_dictionary_cache::get_uncached<dd::Item_name_key, dd::Column_statistics> (this=0x5610227194c0 <dd::cache::Shared_dictionary_cache::instance()::s_cache>, thd=0x7f2b480012d0, --Type <RET> for more, q to quit, c to continue without paging--c key=..., isolation=ISO_READ_COMMITTED, object=0x7f2bbc6f3760) at /mysql-8.0.28/sql/dd/impl/cache/shared_dictionary_cache.cc:113 #10 0x000056101ecf1051 in dd::cache::Shared_dictionary_cache::get<dd::Item_name_key, dd::Column_statistics> (this=0x5610227194c0 <dd::cache::Shared_dictionary_cache::instance()::s_cache>, thd=0x7f2b480012d0, key=..., element=0x7f2bbc6f37c8) at /mysql-8.0.28/sql/dd/impl/cache/shared_dictionary_cache.cc:98 #11 0x000056101ec101c4 in dd::cache::Dictionary_client::acquire<dd::Item_name_key, dd::Column_statistics> (this=0x7f2b48004ca0, key=..., object=0x7f2bbc6f3848, local_committed=0x7f2bbc6f3845, local_uncommitted=0x7f2bbc6f3846) at /mysql-8.0.28/sql/dd/impl/cache/dictionary_client.cc:909 #12 0x000056101ebed4e6 in dd::cache::Dictionary_client::acquire<dd::Column_statistics> (this=0x7f2b48004ca0, object_name="kubo\037test\037id", object=0x7f2bbc6f3970) at /mysql-8.0.28/sql/dd/impl/cache/dictionary_client.cc:1281 #13 0x000056101dad3d12 in histograms::find_histogram (thd=0x7f2b480012d0, schema_name="kubo", table_name="test", column_name="id", histogram=0x7f2bbc6f3a58) at /mysql-8.0.28/sql/histograms/histogram.cc:1336 #14 0x000056101d5cc5d0 in read_histograms (thd=0x7f2b480012d0, share=0x7f2b48ae8bb0, schema=0x7f2b4810e108, table_def=0x7f2b48116578) at /mysql-8.0.28/sql/sql_base.cc:591 #15 0x000056101d5cd0ff in get_table_share (thd=0x7f2b480012d0, db=0x7f2b48ab8e00 "kubo", table_name=0x7f2b480eadc8 "test", key=0x7f2b48ab8baf "kubo", key_length=10, open_view=true, open_secondary=false) at /mysql-8.0.28/sql/sql_base.cc:813 #16 0x000056101d5cd595 in get_table_share_with_discover (thd=0x7f2b480012d0, table_list=0x7f2b48ab8790, key=0x7f2b48ab8baf "kubo", key_length=10, open_secondary=false, error=0x7f2bbc6f3fac) at /mysql-8.0.28/sql/sql_base.cc:884 #17 0x000056101d5d32b6 in open_table (thd=0x7f2b480012d0, table_list=0x7f2b48ab8790, ot_ctx=0x7f2bbc6f4480) at /mysql-8.0.28/sql/sql_base.cc:3202 #18 0x000056101d5d7732 in open_and_process_table (thd=0x7f2b480012d0, lex=0x7f2b48004320, tables=0x7f2b48ab8790, counter=0x7f2b48004378, prelocking_strategy=0x7f2bbc6f4508, has_prelocking_list=false, ot_ctx=0x7f2bbc6f4480) at /mysql-8.0.28/sql/sql_base.cc:5034 #19 0x000056101d5d92d5 in open_tables (thd=0x7f2b480012d0, start=0x7f2bbc6f44f0, counter=0x7f2b48004378, flags=0, prelocking_strategy=0x7f2bbc6f4508) at /mysql-8.0.28/sql/sql_base.cc:5842 #20 0x000056101d5dae47 in open_tables_for_query (thd=0x7f2b480012d0, tables=0x7f2b48ab8790, flags=0) at /mysql-8.0.28/sql/sql_base.cc:6722 #21 0x000056101d76fb82 in Sql_cmd_dml::prepare (this=0x7f2b48ab90c0, thd=0x7f2b480012d0) at /mysql-8.0.28/sql/sql_select.cc:367 #22 0x000056101d770601 in Sql_cmd_dml::execute (this=0x7f2b48ab90c0, thd=0x7f2b480012d0) at /mysql-8.0.28/sql/sql_select.cc:528 #23 0x000056101d6e5611 in mysql_execute_command (thd=0x7f2b480012d0, first_level=true) at /mysql-8.0.28/sql/sql_parse.cc:3553 #24 0x000056101d6eabba in dispatch_sql_command (thd=0x7f2b480012d0, parser_state=0x7f2bbc6f5bd0) at /mysql-8.0.28/sql/sql_parse.cc:5174 #25 0x000056101d6e098c in dispatch_command (thd=0x7f2b480012d0, com_data=0x7f2bbc6f6bc0, command=COM_QUERY) at /mysql-8.0.28/sql/sql_parse.cc:1938 #26 0x000056101d6deb2b in do_command (thd=0x7f2b480012d0) at /mysql-8.0.28/sql/sql_parse.cc:1352 #27 0x000056101d8e4375 in handle_connection (arg=0x56102549bee0) at /mysql-8.0.28/sql/conn_handler/connection_handler_per_thread.cc:302 #28 0x000056101f75b137 in pfs_spawn_thread (arg=0x561025540740) at /mysql-8.0.28/storage/perfschema/pfs.cc:2947 #29 0x00007f2bd3590609 in start_thread (arg=<optimized out>) at pthread_create.c:477 #30 0x00007f2bd2cde133 in clone () at ../sysdeps/unix/sysv/linux/x86_64/clone.S:95
2回目のブレークポイントでtestに関するopenが為される
(gdb) bt #0 ha_innobase::open (this=0x0, name=0x0, open_flags=0, table_def=0x100000000) at /mysql-8.0.28/storage/innobase/handler/ha_innodb.cc:6995 #1 0x000056101dab35b8 in handler::ha_open (this=0x7f2b48acb5e0, table_arg=0x7f2b480fff10, name=0x7f2b48ae8f58 "./kubo/test", mode=2, test_if_locked=2, table_def=0x7f2b48116578) at /mysql-8.0.28/sql/handler.cc:2812 #2 0x000056101d87e8a4 in open_table_from_share (thd=0x7f2b480012d0, share=0x7f2b48ae8bb0, alias=0x7f2b480ebe18 "test", db_stat=39, prgflag=8, ha_open_flags=0, outparam=0x7f2b480fff10, is_create_table=false, table_def_param=0x7f2b48116578) at /mysql-8.0.28/sql/table.cc:3175 #3 0x000056101d5d39f1 in open_table (thd=0x7f2b480012d0, table_list=0x7f2b48ab8790, ot_ctx=0x7f2bbc6f4480) at /mysql-8.0.28/sql/sql_base.cc:3367 #4 0x000056101d5d7732 in open_and_process_table (thd=0x7f2b480012d0, lex=0x7f2b48004320, tables=0x7f2b48ab8790, counter=0x7f2b48004378, prelocking_strategy=0x7f2bbc6f4508, has_prelocking_list=false, ot_ctx=0x7f2bbc6f4480) at /mysql-8.0.28/sql/sql_base.cc:5034 #5 0x000056101d5d92d5 in open_tables (thd=0x7f2b480012d0, start=0x7f2bbc6f44f0, counter=0x7f2b48004378, flags=0, prelocking_strategy=0x7f2bbc6f4508) at /mysql-8.0.28/sql/sql_base.cc:5842 #6 0x000056101d5dae47 in open_tables_for_query (thd=0x7f2b480012d0, tables=0x7f2b48ab8790, flags=0) at /mysql-8.0.28/sql/sql_base.cc:6722 #7 0x000056101d76fb82 in Sql_cmd_dml::prepare (this=0x7f2b48ab90c0, thd=0x7f2b480012d0) at /mysql-8.0.28/sql/sql_select.cc:367 #8 0x000056101d770601 in Sql_cmd_dml::execute (this=0x7f2b48ab90c0, thd=0x7f2b480012d0) at /mysql-8.0.28/sql/sql_select.cc:528 #9 0x000056101d6e5611 in mysql_execute_command (thd=0x7f2b480012d0, first_level=true) at /mysql-8.0.28/sql/sql_parse.cc:3553 #10 0x000056101d6eabba in dispatch_sql_command (thd=0x7f2b480012d0, parser_state=0x7f2bbc6f5bd0) --Type <RET> for more, q to quit, c to continue without paging--c at /mysql-8.0.28/sql/sql_parse.cc:5174 #11 0x000056101d6e098c in dispatch_command (thd=0x7f2b480012d0, com_data=0x7f2bbc6f6bc0, command=COM_QUERY) at /mysql-8.0.28/sql/sql_parse.cc:1938 #12 0x000056101d6deb2b in do_command (thd=0x7f2b480012d0) at /mysql-8.0.28/sql/sql_parse.cc:1352 #13 0x000056101d8e4375 in handle_connection (arg=0x56102549bee0) at /mysql-8.0.28/sql/conn_handler/connection_handler_per_thread.cc:302 #14 0x000056101f75b137 in pfs_spawn_thread (arg=0x561025540740) at /mysql-8.0.28/storage/perfschema/pfs.cc:2947 #15 0x00007f2bd3590609 in start_thread (arg=<optimized out>) at pthread_create.c:477 #16 0x00007f2bd2cde133 in clone () at ../sysdeps/unix/sysv/linux/x86_64/clone.S:95 (gdb) n 6999 bool cached = false; (gdb) p name $7 = 0x7f2b48ae8f58 "./kubo/test" (gdb)
実際、FLUSH TABLE
後にINSERTを実行し、openしているテーブルを確認すると次のようになることが確認できる。
mysql> show open tables; +----------+-------------------+--------+-------------+ | Database | Table | In_use | Name_locked | +----------+-------------------+--------+-------------+ | mysql | column_statistics | 0 | 0 | | kubo | test | 0 | 0 | +----------+-------------------+--------+-------------+ 2 rows in set (0.00 sec)
TABLE
まずは詳解MySQLを参照してTABLEについて理解を深めつつ、ソースを見て「テーブルを開く」の意味を掴んでいく
TABLE構造体はテーブルディスクリプタを定義する。 テーブルは「開いた状態」か、「閉じた状態」の2通りで存在する。「テーブルが開く」たびにテーブルディスクリプタが作成されて、テーブルキャッシュに配置される。
実際、ha_innobase::openの前段のopen_tableとopen_table_from_shareでは以下のようにTABLEを準備している様子が伺える。この少し前段ではTABLE_SHAREをテーブル定義キャッシュから取得している場面もあったりする。
/* make a new table */ if (!(table = (TABLE *)my_malloc(key_memory_TABLE, sizeof(*table), MYF(MY_WME)))) goto err_lock; error = open_table_from_share( thd, share, alias, ((flags & MYSQL_OPEN_NO_NEW_TABLE_IN_SE) ? 0 : ((uint)(HA_OPEN_KEYFILE | HA_OPEN_RNDFILE | HA_GET_INDEX | HA_TRY_READ_ONLY))), EXTRA_RECORD, thd->open_options, table, false, table_def);
nt open_table_from_share(THD *thd, TABLE_SHARE *share, const char *alias, uint db_stat, uint prgflag, uint ha_open_flags, TABLE *outparam, bool is_create_table, const dd::Table *table_def_param) { .... error = 1; new (outparam) TABLE(); outparam->in_use = thd; outparam->s = share; outparam->db_stat = db_stat; outparam->write_row_record = nullptr; MEM_ROOT *root; if (!internal_tmp) { root = &outparam->mem_root; init_sql_alloc(key_memory_TABLE, root, TABLE_ALLOC_BLOCK_SIZE, 0); } else root = &share->mem_root; /* For internal temporary tables we allocate the 'alias' in the TABLE_SHARE's mem_root rather than on the heap as it gives simpler freeing. */ outparam->alias = internal_tmp ? strdup_root(root, alias) : my_strdup(key_memory_TABLE, alias, MYF(MY_WME)); if (!outparam->alias) goto err; outparam->quick_keys.init(); outparam->possible_quick_keys.init(); outparam->covering_keys.init(); outparam->merge_keys.init(); outparam->keys_in_use_for_query.init(); ...
また、open_table内の処理でopen_table_from_shareを呼び出した後すぐに、以下のようにテーブルキャッシュに保存している様子が伺える。
{ /* Add new TABLE object to table cache for this connection. */ Table_cache *tc = table_cache_manager.get_cache(thd); tc->lock(); if (tc->add_used_table(thd, table)) { tc->unlock(); goto err_lock; } tc->unlock(); }
ここまでを見ると、「テーブルを開く」はテーブルディスクリプタを作成してテーブルキャッシュに載せる事であると言えそうだ。 これで解散するのも惜しいのでもう少し続ける。
handler
もう少しopen_table_from_shareの処理の一部に注目する。ここからはInnodb側の処理に寄っていく。
以下のようにTABLEを構成している中で、get_new_handlerでhandlerを作成している。
/* Allocate handler */ outparam->file = nullptr; if (!(prgflag & SKIP_NEW_HANDLER)) { if (!(outparam->file = get_new_handler(share, share->m_part_info != nullptr, root, share->db_type()))) goto err; if (outparam->file->set_ha_share_ref(&share->ha_share)) goto err; } else { assert(!db_stat); }
ここで、また詳解MySQLでhandlerについて参照する。 テーブルハンドラはストレージエンジンとオプティマイザの間のインターフェースであり、 このインターフェースはhandlerという名前の抽象クラスを通して実装される。この抽象クラスはテーブルオープン・クローズ、レコードのシーケンシャルスキャン・レコードの取得・格納・削除といった基本的なメソッドに対する処理を提供する。個々のストレージエンジンはhandlerのサブクラスを実装する。
実際、作成したhandlerを用いてha_openを呼び出し、InnoDBストレージエンジンで実装されているha_innobase::openが呼び出される。
int ha_err; if ((ha_err = (outparam->file->ha_open( outparam, share->normalized_path.str, (db_stat & HA_READ_ONLY ? O_RDONLY : O_RDWR), ((db_stat & HA_OPEN_TEMPORARY ? HA_OPEN_TMP_TABLE : (db_stat & HA_WAIT_IF_LOCKED) ? HA_OPEN_WAIT_IF_LOCKED : (db_stat & (HA_ABORT_IF_LOCKED | HA_GET_INFO)) ? HA_OPEN_ABORT_IF_LOCKED : HA_OPEN_IGNORE_IF_LOCKED) | ha_open_flags), table_def)))) { /* Set a flag if the table is crashed and it can be auto. repaired */ share->crashed = ((ha_err == HA_ERR_CRASHED_ON_USAGE) && outparam->file->auto_repair() && !(ha_open_flags & HA_OPEN_FOR_REPAIR)); switch (ha_err) { case HA_ERR_TABLESPACE_MISSING: /* In case of Innodb table space header may be corrupted or ibd file might be missing */ error = 1; assert(my_errno() == HA_ERR_TABLESPACE_MISSING); break; ... default: outparam->file->print_error(ha_err, MYF(0)); error_reported = true; if (ha_err == HA_ERR_TABLE_DEF_CHANGED) error = 7; else if (ha_err == HA_ERR_ROW_FORMAT_CHANGED) error = 8; break; } goto err; /* purecov: inspected */ }
/ha_innodb.cc /** Open an InnoDB table. @param[in] name table name @param[in] open_flags flags for opening table from SQL-layer. @param[in] table_def dd::Table object describing table to be opened @retval 1 if error @retval 0 if success */ int ha_innobase::open(const char *name, int, uint open_flags, const dd::Table *table_def) {
ここまでを見ると、ストレージエンジン側でもopenの概念があり、open_table_from_share内ではhandlerを通してその操作が為されると分かる。
ha_innobase::openの実装
さて、ha_innobase::openを要所要所覗き見していく。
/* Get pointer to a table object in InnoDB dictionary cache. For intrinsic table, get it from session private data */ ib_table = thd_to_innodb_session(thd)->lookup_table_handler(norm_name);
まずはこの部分。 コメント内では、以降ディクショナリーキャッシュからテーブルオブジェクトのポインターを手に入れるぞいと宣言しているのが確認できる。ここでテーブルオブジェクトとは上記のib_tableの型でもあるdict_table_tを指す。
また、コメントから「intrinsic tableの場合」はセッションプライベートデータから読み込むとの内容が確認できる。
実際、thd_to_innodb_sessionとlookup_table_handlerのFile Referenceを読むと InnoDB セッション固有のプライベートハンドラーを取得し、そのハンドラを使ってテーブル名と一致するテーブルオブジェクトを取得している処理と確認できる。
intrinsic tableとは何か。また別の機会に調べてみよう。 ※5.7ではあるがintrinsic tableに関するブログアーカイブは見つけた
if (ib_table == nullptr) { dict_sys_mutex_enter(); ib_table = dict_table_check_if_in_cache_low(norm_name); if (ib_table != nullptr) { if (ib_table->is_corrupted()) { /* Optionally remove this corrupted table from cache now if no other thread is still using it. If not, the corrupted bit will keep it from being used.*/ if (ib_table->get_ref_count() == 0) { dict_table_remove_from_cache(ib_table); } ib_table = nullptr; cached = true; ... } else if (ib_table->discard_after_ddl) { reload: btr_drop_ahi_for_table(ib_table); dict_table_remove_from_cache(ib_table); ib_table = nullptr; } else { cached = true; if (!dd_table_match(ib_table, table_def)) { dict_set_corrupted(ib_table->first_index()); dict_table_remove_from_cache(ib_table); ib_table = nullptr; } else { ib_table->acquire_with_lock(); } } }
次にセッションプライベートデータからオブジェクトを取得できなかった場合の処理の一部。 この場合、mutexを取得してディクショナリーキャッシュ内を探索する。 キャッシュされていた場合、そのテーブルが破損してないか・DDL後でないかをチェックする。該当すれば、キャッシュから取得したオブジェクトを使わないことにしたりキャッシュから削除したりする。
※refresh_fkのチェックもあったが、何のチェックをしているかイマイチまだ分かっていない
if (!cached) { dd::cache::Dictionary_client *client = dd::get_dd_client(thd); dd::cache::Dictionary_client::Auto_releaser releaser(client); ib_table = dd_open_table(client, table, norm_name, table_def, thd); if (!ib_table) { set_my_errno(ENOENT); return HA_ERR_NO_SUCH_TABLE; } } } else { ib_table->acquire(); ut_ad(ib_table->is_intrinsic()); }
そしてキャッシュされていない場合、 データディクショナリーからテーブルオブジェクトを取得する。
以降はテーブルオブジェクトの細やかなチェックと設定がおこなれていく。
if (ib_table != nullptr) { /* Make sure table->is_dd_table is set */ std::string db_str; std::string tbl_str; dict_name::get_table(ib_table->name.m_name, db_str, tbl_str); ib_table->is_dd_table = dd::get_dictionary()->is_dd_table_name(db_str.c_str(), tbl_str.c_str()); }
例えば、取得したテーブルオブジェクトについて、テーブルがデータディクショナリテーブルであるかどうかのチェック。
/* m_share might hold pointers to dict table indexes without any pin. We must always allocate m_share after opening the dict_table_t object and free it before de-allocating dict_table_t to avoid race. */ if (ib_table != nullptr) { m_share = get_share(name); if (m_share == nullptr) { dict_table_close(ib_table, FALSE, FALSE); return HA_ERR_SE_OUT_OF_MEMORY; } }
他には、テーブルオブジェクトの競合を避けるための処理。 コメントから察するに、dict_table_tを取得した後にm_share(INNOBASE_SHARE)を割り当てて、これを解放してからdict_table_tを閉じるという順番とのこと。
if (ib_table != nullptr && ((!DICT_TF2_FLAG_IS_SET(ib_table, DICT_TF2_FTS_HAS_DOC_ID) && table->s->fields != dict_table_get_n_tot_u_cols(ib_table)) || (DICT_TF2_FLAG_IS_SET(ib_table, DICT_TF2_FTS_HAS_DOC_ID) && (table->s->fields != dict_table_get_n_tot_u_cols(ib_table) - 1)))) { ib::warn(ER_IB_MSG_556) << "Table " << norm_name << " contains " << ib_table->get_n_user_cols() << " user" " defined columns in InnoDB, but " << table->s->fields << " columns in MySQL. Please check" " INFORMATION_SCHEMA.INNODB_COLUMNS and " REFMAN "innodb-troubleshooting.html for how to resolve the" " issue."; /* Mark this table as corrupted, so the drop table or force recovery can still use it, but not others. */ ib_table->first_index()->type |= DICT_CORRUPT; free_share(m_share); dict_table_close(ib_table, FALSE, FALSE); ib_table = nullptr; }
そして、tableとib_tableのカラム数の情報との乖離がないかチェックをしている様子。つまり、データディクショナリーやキャッシュから引っ張ってきた情報と違いがないかを確認している。以降も様々なチェックをしている(途中で力尽きた)
ここまでで、InnoDB層での「テーブルを開く」はディクショナリーキャッシュからテーブルオブジェクトを取得し、もしなければデータディクショナリから引っ張ってくる事と言えそうだ。
要するに
さて、順番に箇条書きにしてまとめると、以下がInnoDB層の挙動も含めた「テーブルを開く」であると思われる
テーブルディスクリプタ(TABLE)を作成する
テーブルハンドラ(handler)を作成する
InnoDBハンドラ(ha_innobase)を通してディクショナリキャッシュもしくはデータディクショナリからテーブルオブジェクトを取得する
テーブルキャッシュにテーブルディスクリプタを保存する
まだモヤモヤする部分がポツポツあったりするがが、勉強していけばその内ハッとくる時が来るであろう。