innodbのレコードの構造

レコードのフォーマットはrem0rec.cに詳しい説明がある。
OLD STYLEとNEW STYLEの2種類がある。
(MySQL Internals Manualの説明はOLD STYLEのもの)

index->table->compが1だとNEW STYLE。
compはdict_mem_table_create()で設定している。
ha_innobase::create -> create_table_def -> dict_mem_table_create

create tableで、row_format=redundantのとき、compがFALSEになる。
それ以外(default, compressedなど)のときは、compはTRUEになる。
指定しなかったときもTRUEになる。
システムカタログ(SYS_TABLESなど)は、comp = FALSE。

MySQL 5.0.3 Release Notes
---------------------------------------------------------------------
InnoDB: Introduced a compact record format that does not store
the number of columns or the lengths of fixed-size columns.
The old format can be requested by specifying ROW_FORMAT=REDUNDANT.
The new format (ROW_FORMAT=COMPACT) is the default. 
---------------------------------------------------------------------

OLD_STYLEのRecord Format

Record Formatは、大きくわけて3つのデータからなる。
(1)データへのオフセットリスト
最終フィールドから順に設定される。
(2)レコードの情報 (6bytes)
各種フラグや次のレコードへのポインタなど、レコードの情報が格納される。
(3)データ
データリスト。各フィールドのデータは、オフセットリストで指定された位置に存在する。

レコードの先頭アドレス(Origin of the record)は、データリストの先頭位置が使用される。オフセットリストに指定されるオフセットは、これが基点になるので、先頭フィールドのオフセットは0。

以下のようなレイアウトになる。

 最終フィールドのデータ末尾のoffset
 ......
 2番目のフィールドのデータ末尾のoffset
 先頭フィールドのデータ末尾のoffset
 レコード情報(6 bytes)
                        <---- ここがOrigin of the recordのポインタ
 先頭フィールドのデータ
 2番目のフィールドのデータ
 ......
 最終フィールドのデータ

オフセットについて:オフセットの先頭ビットはNULLフラグとして使用される。レコード長(データの長さの合計値)が127byte未満のときは、各フィールドのオフセットは1byteで表現される。それ以上のときは2byte使う。オフセット長が1byteか2byteかどうかは、レコード情報のフラグに設定される。
また、2byteのときは、2番目のビットはデータが別ページに保存されたことを示すフラグとして使用される。
オフセットには各フィールドの末尾の位置が設定されるので、あるフィールドの先頭のオフセットを知りたいときは、1つ前のフィールドのオフセットを見る。

レコード情報のレイアウトは以下のとおり。

 未使用 (2 bits)
 削除フラグ (1 bit)
 min_rec_flag (1 bit)
 このレコードが保持しているレコード数 (4 bits)
 レコード番号 (13 bits)
 フィールドの数 (10 bits)
 1byteオフセットフラグ (1 bit)  1だったらオフセットは1byte。0だったら2byte。
 次のレコードへのポインタ (16 bits)

NEW_STYLEのRecord Format

NEW_STYLEはレコードサイズが小さくなるように、以下の工夫をしている
(1)各レコードにフィールド数をもたない
(2)オフセットではなく、データ長をもつ
(3)各レコードに固定長のデータ長はもたない
(4)NULLフィールドのデータ長はもたない

レコードのレイアウトは以下のようになる。

 可変長でNULLではない最終フィールドの長さ
 ......
 可変長でNULLではない先頭フィールドの長さ
 NULLフラグ(NULLが許可されているフィールドにつき1bit。byte単位で確保される)
 レコード情報 (4 bytes)
   <-- Origin of the record
 先頭レコードのデータ
 ......
 最終レコードのデータ

フィールド長:
 テーブル定義のフィールド長が255bytes以内のときは、1byte(0〜255)。
 フィールド長が256bytes以上のときは、以下のようになる
  データ長が127bytes以内のときは1byte。(0〜127)
  データ長が128以上のときは2byteが使用される。先頭ビットは常に1で、
  2番目のビットはextern storage flagとして使用される。データ長として
  使用できるのは14 bits(128〜16383)。

レコード情報:
 未使用 (2 bits)
 削除フラグ (1 bit)
 min_rec_flag (1 bit)
 このレコードが保持しているレコード数 (4 bits)
 レコード番号 (13 bits)
 レコードタイプ (3 bits)
 次のレコードへのポインタ (16 bits)

レコードタイプ:
 000: conventional
 001: node pointer (inside B-tree)
 010: infimum
 011: supremum
 1xx: reserved

システムカラム

各レコードの先頭には、以下のシステム用のカラムが追加される。
(1)Row ID (6 bytes)
(2)Transaction ID (6 bytes)
(3)Rollback pointer (7 bytes)

ただし、Primary keyが定義されているときは、Row IDの代わりにPrimary keyが使用される。

Primary keyなし
 Row ID
 Transaction ID
 Rollback pointer
 ユーザー定義のカラム1
 ユーザー定義のカラム2
 .....

Primary keyあり
 Primary key (ユーザー定義のカラム1)
 Transaction ID
 Rollback pointer
 ユーザー定義のカラム2
 .....

gdb memo

callコマンドで関数を実行できる。

例) innodbのrecordの情報を出力する
(1)ブレークポイントを設定

break rec_init_offsets

(2)ブレークしたらデバッグ用の関数を実行

call rec_print(stderr, rec, index)
continue

これでmysqlのerrファイル(mysqlhome/varにある)に出力される

出力例)

PHYSICAL RECORD: n_fields 6; compact format; info bits 0
 0: len 4; hex 80000001; asc     ;;
 1: len 6; hex 000000000302; asc       ;;
 2: len 7; hex 800000002d0110; asc     -  ;;
 3: len 4; hex 80000001; asc     ;;
 4: len 4; hex 80000000; asc     ;;
 5: len 1; hex 41; asc A;;

mysql-5.1のstorage engine

storage engineの実装は、storageディレクトリにある

bdb heap innobase myisam myisammrg ndbがある

storage engineのインタフェースには、handlerとhandlertonがある

handlerはinsertやselectなど、テーブルを操作するインターフェースを定義したclass。sql/handler.hで定義されている。各storage engineは、handlerを継承してstorage engineごとのテーブル操作を実装している。handlerのインスタンスは、各テーブルの操作ごとに生成される。例えばinnodbのAAAというテーブルを検索するとき、AAAを操作するためのha_innobase(innodbのhandler)が生成(new)される。

handlerton(handler + singleton)は、各storage engineにつき1つだけ実体を持つ構造体。sql/handler.hで定義されている。storage engineの情報や、storage engineを操作するためのインタフェースを定義している。handlertonには、handlerを生成する関数や、commit/rollbackなどstorage engine全体を操作する関数が設定される。

innodbのhandlertonとhandlerは、sql/ha_innodb.ccに定義されている。

Allow an alias for the target table in UPDATE/DELETE

UPDATE accounts AS a SET a.abalance = a.abalance + 10 WHERE a.aid = 1;
のようにupdateやdelete対象のテーブルに、別名をつけることが出来るようにするパッチ。

http://www.hi-ho.ne.jp/a_ogawa/document/patches/allow_alias_update.patch

Virtual tuple slots versus TOAST: big problem

TOASTが必要なTupleをinsert/updateした時、backendがcrushする問題について。
http://archives.postgresql.org/pgsql-hackers/2005-11/msg01016.php

(1)ExecInsertにTupleTableSlot(slot)が渡される。
このときslotのvirtual tuple領域(slot->tts_valuesなど)にデータが設定されている。

(2)ExecMaterializeSlotでslotのvirtual tupleから、physical tuple(slot->tts_tuple)に変換する。slotのvirtual tupleはクリアされる。

(3)ExecConstraintsで制約チェックを実行する。
slotのphysical tupleからチェックするカラムのデータを取り出す。このとき、取り出したカラムとそれより前にあるカラムのデータはslotのvirtual tuple領域にcacheされる。

(4)heap_insertにslotのphysical tupleを渡して、ページにtupleを登録する。
heap_insertでは、tupleサイズがTOAST_TUPLE_THRESHOLDより大きい場合、heap_tuple_toast_attrsを実行する。heap_tuple_toast_attrsは、tupleを上書きして再構成するため、tupleのサイズや各カラムのデータの位置などが変わる。
(*)ここでslotのphysical tupleとvirtual tuple cacheの情報が矛盾してしまう

(5)(ExecInsertに戻って)ExecInsertIndexTuplesを実行してindexにデータを登録するとき、index用のtupleを構築するためにslotのphysical tupleまたはvirtual tupleから、indexに必要なカラムのデータを取り出す。
virtual tupleの情報はphysical tupleの情報と矛盾しているため、virtual tupleの情報を参照したときに正しくない位置のメモリを読んでしまい、backendがcrushするか、間違ったデータを取り出してしまう。

updateについても同様の問題が発生する。
当面の解決策はheap_insert/heap_updateの実行後にvirtual tuple cacheを無効にすること。