xxxさんの備忘録

xxさんのブログです.xxさんはネカマです.

【第七章~第九章】失敗から学ぶRDBの正しい歩き方

7.1意味を含んだID(cf:論理ID,スマートカラム)

cf.

7.2類似

「EAV」、「Polymorphic Associations」->『SQLアンチパターン

会員table

id name
1 neko

会員情報テーブル

会員id 属性名
1 age 24
1 hobby game
1 business CRE

このような二つのtableが設計されている場合、

  • どのような属性・値の組み合わせのデータでも保存できる という利点がある

  • その属性名に対する値があるのか

  • その属性名があるのか

  • 属性名と値の組み合わせは正しいのか

  • 属性名の一覧

などは実際に取り出すまでわからない。

また、

  • 必須属性やデータ型が指定できない

  • 正規化されていないために外部キー制約を強制できない

(ex:属性名「都道府県」には「東京」と「東京都」が入る可能性がある。)

  • 属性名を補う必要がある

メモ:

EAVの代価案になりうるjson

Mysql5.7 PostgreSQL9.3以降ではjson型を保存できる という問題が生じる。 ->

  • 汎用性を高めた結果、RDBが本来持っているメリット失っている状態といえる

propertiesテーブル(子)

id name adress 参照先
1 株式会社neko 東京都 shop
2 tako 東京都 user

shopテーブル(親)

shop_id properties_id 従業員
1 1 2

Userテーブル(親)

user_id properties_id 年齢
1 2 25
  • 外部制約キー制約が使えないため、参照整合性は担保できない

7.3仕様変更の際に明らかになる隠された状態について

  • アプリ側では一見して状態を判断できず、仕様変更の際に手間取る

  • バグが起きて不正なデータが生まれた場合には整理が難しい

  • アプリ側で防げないため、DBが無防備(check項目などの制約が入れられない)

  • ドキュメントがない場合は、どのような法則があるのかをコードを追うしかなくなる

  • idの制約を変える場合には大規模なアプリ側の改修を入れる必要が出て来る

7.4RDBには事実のみを保存する->責務が複数ある場合は保存先を分けていく

-> 交差テーブルを用意するのが妥当

propertiesテーブル

id name adress 参照先
1 株式会社neko 東京都 shop
2 tako 東京都 user

(properties->shop)中間テーブル

shop_id properties_id
1 1

shopテーブル

shop_id 従業員
1 2
2 500

(properties->user)中間テーブル

user_id properties_id
2 2

Userテーブル

user_id properties_id 年齢
2 2 32

7.5アンチパターンのポイント

  • 今回のアンチパターンの状態は隠された状態であることが問題だった

  • 中間テーブルを用意することは作ることが億劫なため忌避する人も多いが、「外部キー制約、トランザクションのパフォーマンスの問題」などが顕在化して対策では遅く、元のテーブル設計の際に解決できることが多い(かつ良い)

アンチパターンを防ぐコツ

  • データに複数の意味を持たせない

  • 1つのデータの責務を小さくする

  • 常に状態が見れるように、事実のみを記載する

隠された状態が存在することは、アプリ側にとっても影響があるアンチパターンのため、問題として大きくなりやすい。

早めのリファクタリングを心がける。

メモ: 問題が顕在化してリファクタリングするやつに多い問題のように感じた。

現場で知っていること(設計時に気が付けるか)、(例示リファクタのようなきれいな案を提案)できるようになっていることが

鍵のように思えた。

  • トリガー

隠された状態に近い機能が用意されておりそれがトリガーである。

  • デメリット

  • アプリ設計者や運用者から見えず、振る舞いが予想できない。(永続的に影響を与える場所でトリガーを使う->隠された状態同等の問題を発生させる)

  • メリット(使ってもいい場面)

  • パフォーマンス的メリット

  • アプリ側の実装が大幅に削減できる

  • 既存のアプリの振る舞いを維持したまま、仕様を変更できる

メリットと隠された状態を生むデメリットがある場合は、採用を見送る。

8.1jsonデータ型のアンチパターン

  • 全ての値をjsonデータ型で保存したこと

メモ: 「一番ミニマムな形からできるサービス展開をしたい時」(https://qiita.com/yuno_miyako/items/fad33456d9c32d8f4483)でのコメント欄で目立ったように感じたのが確か「nosqlを学ぶコストが高い」で同じような問題に感じる。 mysqlにも5.7.8以降(?)json型が加えられたそうだが、indexできないなどやはり難しいように思える。

8.2「なんでもjson」の危険性

  • ORMが使えない(jsonを取り出すためのSQLを手書きする必要があり、難しい)

  • データの整合性が取れない(EAVの代案でありながら、問題点がある)

  • 必須属性の指定が難しい

  • データの中身を指定できない

  • ex>yyyy/mm/dd と yyyy-mm-ddは何もしないと取り込まれてしまう。(型を揃えてくれない。)

  • ex>"123456"(文字列)と 123456 は、型違いでエラーが出る。

->データ型で守られていたことが失われる

  • 参照整合性制約を強制できない。上の「データの中身を指定できない」と似ているが、外部キー制約を使うことができない

  • 都道府県というkeyに対して東京と東京都が入る可能性がある

8.3 RDBの責務を失わないためには

  • json型は汎用性を高められる一方でデータを守るができなくなる

それらを踏まえたうえでjson型のメリットを見る

  • jsonそのものに対応している

->「web apiの戻り値」「phpのcomposer(設定ファイルが.json)」の場合など

->データ構造を無視して保存できること

(※RDBではできない設計ではあるが、この条件が必要な場合はRDBMS以外の選択肢も考える。

パフォーマンスやスケーリングに課題が残ってしまいがちなため。->無視できるデータサイズ場合は問題ない)

webapiの戻り値

twitter_account

id screen_name token account
1 akino hoge fallakiakino

twitter_account_detail

twitter_account_id setteings created_at
1 {protected:false,.......} 2021-3-15
  • twitter側は予告なくapiの変更をする可能性があるため戻り値がerrorにならずに保存されている時も対応できるように

  • 必要なデータは正規化し、その他も含まれるjsonは別テーブルに分けることでパフォーマンスの向上と、必要なデータが取れない際にerrorで気が付ける

メモ: webapiの返した値をログのようにjson型で持っておくことで、障害時などに見返せる利便性があるとういう理解でいいのだろうか?

  • os情報
host_id host_name ip os detail
1 host1 192.168.0.1 CentOS {...}
  • 補足

  • os情報にはディストリビューションによって特有のものがあり、detailカラムをjsonすることでディストリビューションごとの違いを吸収している

  • この設計にした理由

  • レコード数が少なく数千程度だった

  • 一度保存したレコードに対してupdateをすることはほとんどない

  • 取り出すときはjsonを丸ごと取得

8.4アンチパターンのポイント

EAVの代替案として出されることが多い(EAVの代替案として優秀である一方でEAVと同じような問題を解決できていない)

EAVからjsonデータ型に置き換えるときはどのような点に気を付けるべきか

  • 正規化することはできないか

  • jsonに対して頻繁に更新を行いたいか

  • 検索情報としてjson内の属性が固定できないか

->1つでも該当する場合はjsonデータ型を採用すべきではない

(jsonデータ型はRDBMSの機能と引き換えに柔軟性を与える切り札のように思うべきである)

  • jsonデータ型の他の使い道

  • レコード->json

アプリ側でループを回すことなく、テーブルの情報を取り出せる

  1. json->レコード

アプリ側から渡されたjsonをストアドなしに適切に扱うことができるようになる

なんとなくだけども2の方法は一度jsonを取得してキレイなテーブルをサクッと作りたい時には便利そうな気がする (1は使いどころがパフォーマンス向上なのでいまいちわかってない。)

9.1 制約に関して

  • ex: email を保存する型にdomainで生成した(email_address型)を使用した ->RFCに準拠してないアドレス(ex:test.@example.com)を弾いてしまった

  • リファクタリングの例  alter文を流して、email_address型からtext型に変更する。  (仮にこの場合のテーブルのindexがemail_addressに貼られていた場合、再構築が実施される。  -> ロックがかかり、長時間メンテナンスになってしまう恐れがある)

9.2似たアンチパターン

  • 外部キー制約(mysql)

    • 子テーブルの更新すると、親テーブルの共有ロックを自動的にとる
    • 対策 排他ロックを取る (排他ロックは正しく順番を待たせるため、パフォーマンスのボトルネックになる)
    • cf>ギャップロック
      • index値を持つ行と行の間にあるギャップ
      • 先頭のindex値を持つ行の前のギャップ
      • 末尾のindex値を持つ行の後のギャップ にもロックをかけることがある(アプリが正しく更新していない時に起きる。パフォーマンスが落ちる)
  • domainに似たenum型(mysql) -> 

    • メリット 表記の揺れを防ぐ
    • デメリット データ型の変更にはalter文が必要 仕様変更に弱い 制約されいる文字列を知る方法が限られている。
  • 状態を持つcheck制約

    alter table emails add column update at timestamp with out time zone check(update >=now());

    • テストデータ作成時 テスト用のデータは常に「update_atが最新日付・時刻のもの」になってしまう。 そのため、update_at を利用した where 句のテストなどに使うテストデータを定期的に作り直す、 が難しくなる

    • 論理バックアップからデータをリストアするとき 元々入っていたデータは更新日が突然過去の状態でdumpされる。 そのため、insertで失敗する。 バックアップからデータを戻す際には緊急事態で理由がわからず慌ててしまうため、弊害といえる。

9.3過剰に制約を掛けないためには

  • 制約と規約

    • 規約 ->バリデーションやFWの機能など

      • デメリット
        • アプリのバグとヒューマンエラーに弱い(->ここを補うのがRDBMSの制約)
    • 制約 ->RDBMSのもの これら二つをうまく使うことが大事

  • データは成長する生き物なので変化に対応する必要があり、製薬は枷になりやすい。 (規約は変化には強いのでアプリ側に持たせる)

  • 制約の段階 |段階 | 説明| |-------------|------| |制約なし | 何でも自由にデータが入れられる状態 |弱い制約 | not null、unique制約、外部キー制約などデータ構造を守るための最低限の規約 |強い制約 | check制約、exclude制約などRDBMSの機能に依存するが、適切に使えばデータを的確に守ることができる  強すぎる制約 | 制約の内容が「システムの仕様」や「ビジネスルール」に基づいて記述されている状態

  • 表の「強すぎる制約」になっていないかを今一度確認する必要がある。

    • DBの設計にビジネスロジックやシステム仕様を混ぜると、アプリケーション改修時に影響が出る。

9.4 制約を「正しく扱う」とは

  • 強すぎる制約はRDBMSの問題点であり、安易に避けると大きな問題を起こす。この制約のバランスについて日々考えていくことが必要

  • postgreSQLの遅延制約

    • postgreSQLには少しだけ楽をするための方法として、外部キーの制約を遅延して評価させる仕組みがある。

    • 外部キーをチェックしない

      set constraints all deffrred;

    • 外部キーをチェックする

       set constraints all immediate;

    tabel作成時等に

    1. DEFERRABLE INITIALLY DEFERRED (-トランザクション開始時からDEFERRABLEモードになる)
    2. DEFERRABLE INITIALLY IMMEDIATE (-set constraintsを使用すると振る舞いを変更できる )
    3. NOT DEFERRABLE (-set コマンドの影響を受けない)
        を宣言する必要がある(宣言しない場合はデフォルトで「NOT DEFERRABLE」になる)

 * 遅延制約を使いたい場合は(1),(2)をあらかじめ指定しておく必要がある。

 * mysql 外部キー制約のみ > set foreign_key_checks=0;