データベース

INPUT · スライド

途中で失敗しても壊さない

01 / 12

振込は 2つの UPDATE

A さんから B さんへ 1万円を振り込む。SQL で書くと、やることは2つだよ。

  • A の残高から 10000 を引く
  • B の残高に 10000 を足す

ここで、1つ目が終わった直後にサーバが落ちたらどうなるだろう。A の残高は減ったまま、B の残高は増えていない。1万円が世界から消える

1つ1つの UPDATE はどれも正しく動いている。それでも、途中で止まった状態がそのまま残ってしまうのが問題なんだ。

だから「この2つはセットで、途中の状態を外に見せない」と宣言できる仕組みが要る。それが トランザクション だよ。

振込 = 2つの更新A から 10000 引くB へ  10000 足す片方だけ成功 → 1万円が消滅

02 / 12

コミットとロールバック

トランザクションの終わり方は 2つしかない

コミット は「ここまでの更新を確定する」という宣言だよ。コミットした瞬間に、変更が正式なものになって他の人からも見えるようになる。

ロールバック は「なかったことにする」という宣言。途中まで書きかえていても、開始する直前の状態まで全部巻き戻す。エラーが出たときや、アプリが自分で「やめる」と判断したときに使う。

大事なのは、途中で終わることはないという点。コミットかロールバックのどちらかに必ず着地する。だから「A だけ減って B は増えていない」という中途半端な状態が残らないんだ。

BEGIN  UPDATE 口座 …  UPDATE 口座 …COMMIT    ← 確定BEGIN  UPDATE 口座 …  UPDATE 口座 … 失敗ROLLBACK  ← 全部戻す

03 / 12

守ってほしい 4つの性質 ― ACID

トランザクションに求められる性質は、頭文字を並べて ACID と呼ばれる。試験では日本語名と英語名の対応がそのまま問われるので、4つセットで覚えよう。

  • 原子性(atomicity) … 全部実行されるか、全部取り消されるかのどちらか。それ以上分けられない
  • 一貫性(consistency) … 決めた規則(主キーや制約)を壊した状態にはしない
  • 独立性(isolation) … 実行中の中身は他のトランザクションから見えない。互いに影響しない
  • 耐久性(durability) … コミットしたあとに障害が起きても、結果は失われない

振込の話は 原子性 そのものだよ。「片方だけ」を許さないのがこの性質だからね。

A 原子性  全部か、全部なしC 一貫性  規則を壊さないI 独立性  他から見えないD 耐久性  障害でも残る

04 / 12

トランザクションがたどる道

トランザクションは開始したあと、いくつかの状態を通っていく。名前だけ押さえておこう。

  • アクティブ … 実行中。まだ確定していない
  • コミット処理中コミット済 … 確定に向かっている/確定した
  • アボート処理中アボート済 … 取り消しに向かっている/取り消した

注目したいのは行ける向きと行けない向きがあること。コミット処理中に障害が起きれば、そこからアボート処理中へ折り返すことはある。

でも アボート処理中からコミット処理中へは戻れない。取り消すと決めて巻き戻し始めたものを、途中から確定に切りかえることはできないんだ。

アクティブ  ├→ コミット処理中 → 済  └→ アボート処理中 → 済コミット処理中  → アボート処理中 はあるアボート処理中  → コミット処理中 は無い

05 / 12

同時に触られると更新が消える

もう1つの困りごとは、2人が同じ行を同時に触ったときだよ。

在庫が 1000 個ある。A さんが 500 個足し、B さんが 300 個足す。順番にやれば 1800 個になるはずだよね。

ところが両方が同じ 1000 を読んでから計算すると、A は 1500 を書き、B は 1300 を書く。あとに書いたほうが勝って、結果は 1300。A の 500 個ぶんが消えた

これを 更新の喪失 という。どちらのトランザクションも自分では正しいことをしているのに、重なった瞬間だけ結果がおかしくなる。だから、重ならないように順番をつける仕組みが必要になるんだ。

在庫 1000A: 読む 1000B: 読む 1000A: 1000+500 → 1500 書くB: 1000+300 → 1300 書く結果 1300(500 が消えた)

06 / 12

ロックには 2種類ある

順番をつける道具が ロック で、その仕組み全体を 排他制御 と呼ぶ。

ロックは 2種類。共有ロック(S)は読むときにかけるもので、専有ロック(X)は書きかえるときにかけるものだよ。

2つが同時に成り立てるかどうかを 両立性 という。覚えることは1つだけ。

  • 読む人同士は共存できる。共有ロック同士は両方かけられる
  • 書く人が絡むと排他になる。専有ロックは、相手が共有でも専有でも一緒にかけられない

理由は考えれば分かる。読むだけなら何人いても値は変わらない。でも書きかえが混ざると、読んでいる途中で値が変わったり、書いた内容が上書きされたりしてしまうからね。

     後から S    後から X先 S   取れる    取れない先 X   取れない  取れないS = 共有(読む)X = 専有(書く)

07 / 12

ロックをかける範囲 ― 粒度

ロックは「どの範囲にかけるか」を選べる。この広さを 粒度 というよ。1行だけにかけることもできるし、表ぜんたいにかけることもできる。

粒度を大きくする(表単位)と、1回のロックで広い範囲を押さえられるので、管理する個数は少なくてすむ。でもそのぶん他のトランザクションが待たされやすくなり、全体のスループットが落ちる

粒度を小さくする(行単位)と、別の行を触る人は待たずに進めるので同時実行しやすい。ただしロックの個数が増えるので、それを覚えておく DBMS 側のメモリを多く使う。

つまり 待ち時間と管理コストの交換だよ。どちらかが一方的に良いわけではない。

行ロック(小さい)  待ち少ない / 管理が重い表ロック(大きい)  待ち多い / 管理は軽い

08 / 12

お互いを待ち合う ― デッドロック

ロックを入れたことで、こんどは別の詰まり方が起きる。

A が口座1をロックし、B が口座2をロックした。次に A は口座2を、B は口座1を欲しがる。どちらも相手が離すのを待つので、永久に動かない。これが デッドロック だよ。

ただの「待ち」とは区別しよう。1つの資源をめぐって片方が待つだけなら、先の人がコミットすれば順番が来る。デッドロックは互いに相手のロックを要求して、待ちが輪になっている状態のことだよ。

避け方はいくつかあるけれど、いちばん簡単なのは みんなが同じ順番でロックを取ること。番号の小さい口座から先に押さえると決めておけば、輪ができなくなる。DBMS 側は輪ができたことを検知して、片方を強制的にロールバックさせるよ。

A: 口座1 をロックB: 口座2 をロックA: 口座2 を待つB: 口座1 を待つ  → 両方が永久に待つA: 口座1 → 口座2B: 口座1 → 口座2  同じ順なら輪ができない

09 / 12

何をしたか書き残す ― ログとチェックポイント

ここから壊れたあとの話をしよう。障害から戻すには、何をどう変えたかの記録が要る。それが ログファイル(ジャーナル)だよ。

記録するのは値の前と後の両方更新前ログが変える前の値、更新後ログが変えたあとの値だ。前の値があるから巻き戻せて、後の値があるからやり直せる。

よく似た言葉と混ぜないようにしよう。バックアップはデータベースの中身をまるごと複写したもので、ログとは別物。媒体(ディスク)が壊れたときは、バックアップとログの2つを使って復旧するよ。

もう1つ、チェックポイント も押さえておこう。DBMS はときどきメモリ上の更新をまとめてディスクへ書き出し、そこまでは確実だという印を残している。復旧のときはいちばん新しい印から先のログだけを見ればいいので、読む量が一気に減る。復旧を速くするための印であって、ログの代わりではないよ。

更新前ログ  変える前の値更新後ログ  変えた後の値… ログ … [印] … ログ …     ここから先を見るだけ

10 / 12

ロールバックとロールフォワード

戻し方には向きが2つある。どちらを使うかは、障害のときにトランザクションが終わっていたかで決まるよ。

まだ終わっていなかったなら、中途半端なので消したい。更新前ログを使って開始直前の状態まで巻き戻す。これが ロールバック だよ。

もう完了していたなら、コミット済みなので残したい。でもディスクにはまだ書かれていないかもしれない。更新後ログを使ってもう一度先へ進める。これが ロールフォワード だよ。

前のログで戻す・後のログで進める。この組み合わせを取りちがえた選択肢が必ず並ぶので、向きとログの種類をセットで覚えよう。

試験では、さっきのチェックポイントを起点に向きを決めさせる形で出る。「チェックポイント取得後に完了した」なら、そのトランザクションはコミット済みだけどディスクには載っていない可能性がある。だからロールフォワードで進める、という筋道だよ。

未完了だった  → 更新前ログ  → ロールバック完了していた  → 更新後ログ  → ロールフォワード

11 / 12

インデックスはなぜ速いのか

最後は速さの話。インデックス(索引)は、列の値と、その行がどこにあるかを組にして別に持っておくしくみだよ。

インデックスが無いと、DBMS は先頭から1行ずつ見て条件に合うかを確かめる。100万行あれば100万回だ。インデックスがあると、並べ替えて木の形にしてあるので、たどる回数が数回で済む。本の巻末の索引で語句を引くのと同じ発想だよ。

使われる木は B木(B+木)。枝分かれが多くて背が低い木なので、根から葉まで数段で着く。値の大小で枝を選べるので、= だけでなく 範囲の検索や前方一致にも効く

もう1つ ハッシュインデックス もある。値を計算して置き場所を一発で決めるので = の一致は最速だけれど、並び順を持たないので範囲検索には使えない。ID をピンポイントで引くならハッシュ、それ以外は B木、と覚えよう。

索引なし  1行ずつ全部見る索引あり(B木)      [50]     /    \  [20]    [80]  100万行でも数回

12 / 12

インデックスを張ってはいけない場合

インデックスは万能ではない。張ると遅くなる場面があるので、そこを押さえておこう。

まず、更新のたびに索引も直さなければならないINSERTUPDATE が多い表にたくさん張ると、書きこみが重くなる。

次に、値の種類が少ない列には効かない。「性別」のように2種類しかない列で索引を引いても、半分の行が当たってしまうので、全部見るのと変わらない。

行数が少ない表も同じ。数十行なら全部見るほうが速い。

そして、列を加工すると索引が使われない。条件の中で関数に通したり計算したりすると、並べておいた値と突き合わせられなくなるからだよ。

インデックスは「読みを速くするために、書きと容量を差し出す」取引だと思っておこう。

効く  種類が多い列 / 大きい表効かない  種類が少ない列  小さい表  列を加工した条件