しぐまろぐ

勉強したことや読んだ本について書きます。

『達人に学ぶDB設計徹底指南書』を読んだ感想とメモ

『達人に学ぶDB設計徹底指南書[初版]』を読んだので、感想と読書中に書いたメモを残しておきます。

良かったところ

  • 論理設計だけではなく物理設計ついてもページが割かれているところ
  • バッドノウハウ、グレーノウハウの例が豊富で、著者の実体験にも基づいており、わかりやすかった
    • いくつかは現場で見かけたことがあって悲しい気持ちになった

難しかったこと

  • 物理設計はしたことがないのでイメージがわきづらかった
  • B-treeインデックスや統計情報といっった内部処理の話はイメージしづらく難しく感じた
  • リレーショナルデータベース以外に触れたことがないので、他のデータベース構造と比べてどの程度木構造の扱いが苦手なのかよくわからなかった

学んだこと

以下は読書中に記録したメモです。

はじめに

  • 論理設計と物理設計は強いトレードオフの関係にある
  • 理想の論理設計/物理設計を実現するとき、何が犠牲になるのかを学んでいく

第1章 データベースを制する者はシステムを制す

システム開発におけるデータベースの重要性を学んだ。

  • DOA(Data Oriented Approach)とは、システム開発の際にプログラムより先にデータの設計から始めること

    • 現在の開発の主流
    • 複数のプログラムで重複したデータを持つことを避けられる
    • データ設計の失敗はシステムの品質に大きく影響を及ぼす
  • 三層スキーマ

    • 外部スキーマ
      • ユーザーが画面や入力データから見るデータベースの姿を決める
      • テーブルの中身やビューに相当する
    • 概念スキーマ
      • データベースに保存するデータの要素やデータ同士の関連を決める
      • 論理設計(テーブル定義)に相当する
    • 内部スキーマ
      • データ(ファイル)のDBMS内部への配置やインデックスの定義を決める
      • 物理設計に相当する
  • 概念スキーマは何のためにあるのか?

    • もし概念スキーマがなかったらどうなるか?」を考えればわかる
      • 外部スキーマと内部スキーマの依存度が高くなってしまい、お互いに変更の影響を受けてしまう
  • DBMSのマニュアルは無料で公開されているので、適宜熟読すること

第2章 論理設計と物理設計

論理設計と物理設計のポイントを学んだ。

  • 論理設計の論理とは、"物理層の制約にとらわれない"という意味
  • 論理設計 -> 物理設計の順で行う

  • 論理設計のステップ

    • エンティティの抽出
      • 後のステップでテーブルを作成するために、まずは必要なエンティティを洗い出す
      • どのようなデータが必要なのかを考える作業
        • システムの要件定義の作業と重なる
    • エンティティの定義
      • 各エンティティが保持するデータを決める
    • 正規化
      • システムでスムーズに利用できるように整理する
    • ER図の作成
      • 各エンティティの関係をわかりやすくするために作成する
  • 物理設計のステップ

    • テーブル定義
      • 論理設計で作成した概念スキーマをテーブルに落とし込む
    • インデックス定義
    • ハードウェアのサイジング
      • データサイズを見積もってストレージの容量やCPU、メモリの性能を決める
      • 処理速度の速さと処理性能の良さを決める
    • ストレージの冗長構成決定
      • どの程度冗長化して信頼性・可用性・性能を確保するかでRAID(Redundant Array of Independent Disks)のレベルを決める
    • ファイルの物理配置決定
      • データをどのファイルに配置するか
      • DBMSが自動でやってくれるが、開発者は各ファイルの用途を理解しておく必要がある
  • DB設計においてはデータの整合性とパフォーマンスに強いトレードオフの関係がある

  • スケーラビリティ(scalability)とは拡張性のこと

  • SQL実行時にすぐにデータファイル(テーブルのデータが格納されているファイル)が変更されるわけではなく、一旦ログファイルに変更を書き込み、一括して変更している

    • ログファイルはOracleではREDOログ、MySQLではバイナリログとも言われる
      • 障害復旧作業の時にログファイルが使われる
  • 障害発生時の復旧手順

    • リストア
    • リカバリ
      • 差分(または増分)バックアップしていたログファイルを適用する
    • ロールフォワード
      • 未バックアップのログファイルを適用する
  • DBサーバのクラスタリング構成

    • Active-Standby方式
      • ホットスタンバイ
      • コールドスタンバイ
    • Active-Active方式
      • シェアードディスク
        • サーバ間でリソースを共有する
      • シェアードナッシング
        • サーバ間でリソースを共有しない
        • DBサーバの場合は実現が難しい

第3章 論理設計と正規化〜なぜテーブルは分割する必要があるのか?

第一正規形から第五正規形まで、各段階の目的と理論を学んだ。

  • 正規化の目的
    • データの冗長性をなくし更新時のデータ不整合を防止すること
    • 別の言い方をすれば、テーブルの全ての列が関数従属性を満たすよう整理していくこと
  • 正規化を進めれば進めるほどデータの整合性は高まるが、検索性能が低下する
    • 通常は第三正規形までで十分
  • 正規化は必ず元に戻せなければならない(無損失分解)

    • 結合することで元の状態に戻せる
  • テーブルとは、共通点を持ったレコードの集合である

  • 第一正規形

    • テーブル内で1つのセルに1つの値しか含まないようにする
      • セルに複数の値を許すと、主キーが各列の値を一意に決定できなくなってしまう
  • 第二正規形

    • テーブル内で部分関数従属をなくし、完全関数従属のみのテーブルを作る
    • 別の言い方をすると、異なるレベルのエンティティをテーブルとしても分離する作業
  • 第三正規形

    • テーブル内の推移的関数従属をなくす
  • ボイス-コッド正規形

    • 非キーからキーへの関数従属をなくす
  • 第四、第五正規形

    • 関連エンティティに対して行う
      • 関連エンティティは主キーしか持たないのが特徴
  • 第四正規形

    • 多値従属性が複数存在するテーブルを分離する
      • 多値従属性とは、1つのキーに対して複数の値(つまり集合)が存在する状態
    • 具体的には、関連エンティティに含まれる関連を一つだけにする
  • 第五正規形

    • 関連がある場合は、それに対する関連エンティティを作成する

第4章 ER図〜複数のテーブルの関係を表現する

テーブル間の相互関係を効率的に把握するために必要なER図について学んだ。

  • カーディナリティとは、テーブルの同一カラムに含まれる異なる値のバリエーションのこと

    • 例)「性別」列のバリエーションは男女だけなのでカーディナリティが低い
    • リレーションシップにおいては項目の対応関係が「1対1」「1対多」「多対多」の何であるかを指す
      • 「1対1」のテーブルは1つにまとめられるので作成されることはない
  • IDEF1X

    • 角の尖った四角エンティティ
      • 独立エンティティ
        • 他のテーブルのデータに依存しない
    • 角丸四角エンティティ
      • 従属エンティティ
        • 他のテーブルのデータが存在しなければデータを保持できない
        • 主キーに外部キーを含む
    • 関連は⚫️で記述する
      • ⚫️ーー
        • 0以上の多
      • ⚫️Pーー
        • 1以上の多
        • Positive
      • ⚫️Zーー
        • 0以上の多
        • Zero
      • ⚫️nーー
        • 特定の定数
    • カーディナリティが1で、NULLになりうる場合はをつけて点線で結ぶ
    • カーディナリティが1で、NULLになりえない場合は実線で結ぶ
  • 多対多の関係は1対多の関係に分解する

    • この時に作成するエンティティを関連エンティティという

第5章 論理設計とパフォーマンス〜正規化の欠点と非正規化

厳格に正規化すると結合が必要になりパフォーマンスが低下してしまうため、最後の手段として非正規化することを学んだ。

  • 正規化と検索SQLのパフォーマンスには強いトレードオフの関係にある
  • 非正規化はあくまで最後の手段
  • 非正規化は、検索のパフォーマンスは向上させるが更新のパフォーマンスは低下する
  • 正規化による冗長性排除によって起こる問題のパターン
    • サマリデータの冗長性排除
      • レコード数の多いテーブルを結合すると高コストな処理になる
      • "合計数"といったサマリデータをテーブルに持たせると、正規化は崩れるがパフォーマンスは向上する
    • 選択条件の冗長性排除
      • 結合してからWHEREで絞ると高コストな処理になる
      • 選択条件で指定するカラムをテーブルに追加し結合をなくすと、正規化は崩れるがパフォーマンスは向上する

第6章 データベースとパフォーマンス

パフォーマンスを決める要因として、ここまでディスクI/Oの分散(RAID)、正規化を学んできた。この章では、他の要因として、インデックスと統計情報について学んだ。

インデックス設計

  • SQLのチューニング手段として非常にポピュラー
  • インデックスを使うかどうかはDBMSが自動で判断するので、アプリケーションに影響を与えない
  • インデックスはテーブル構造やデータに影響を与えない

  • 最も頻繁に使われるのはB-treeインデックス

    • 検索速度、長期的なパフォーマンス、CRUB処理速度、不等号を使っての検索速度、ソート速度のいずれにおいてもそこそこ優れているから
  • B-treeインデックスの作成に適するケース

    • 大規模なテーブル
    • カーディナリティの高い列
      • 特定のキーを指定したときに値の絞り込みがされやすいから
    • WHERE句の選択条件や結合条件に使用される列
  • B-treeインデックスを利用できないSQL

    • インデックス列で関数や演算を行う
    • IS NULL
    • 否定形<>
    • OR
      • INならインデックスが使用される
    • 後方一致や中間一致のLIKE
      • 前方一致ならインデックスが使用される
    • 暗黙の型変換
      • 明示的にCASTすればインデックスが使用される
  • B-treeインデックスの特徴

    • 主キー、UNIQUEキーの列には内部的に作成されているので、二重に作成する必要はない
    • インデックスを作成すると更新性能は劣化する
      • 不要なインデックスを作らない
    • 定期的なメンテナンスを行う
      • 更新を繰り返すうちに木の構造が崩れて性能が落ちるから
  • その他のインデックス

    • ビットマップインデックス
      • カーディナリティの低い列にも検索性能が良い
      • 更新時の性能が悪い
    • ハッシュインデックス
      • キーとハッシュ値は一対一対応
        • =での検索の性能が良い
        • =での検索以外には利用できない

統計情報

  • 統計情報とは

    • テーブルやインデックスといったデータについてのデータ(メタデータ
    • DBMSはこの統計情報を元にSQLのアクセスパスを決定する
    • 統計情報の更新はなるべく早い方が良いが、処理が重いためアクセスが少ない時間帯にした方が良いという矛盾がある
  • SQL実行までのDBMS内部の流れ

    • SQLを受け取ったパーサが構文をチェックする
    • 構文が正しければオプティマイザというモジュールに送られる
    • オプティマイザは実行計画の参考にするために、カタログマネージャから統計情報を受け取る
    • オプティマイザが統計情報から最短経路を選び出してテーブルへアクセスする

第7章 論理設計のバッドノウハウ

バッドノウハウアンチパターン)の具体例を見て、それらがなぜ生まれるのか、どこがダメなのかを学んだ。

  • バッドノウハウがダメな理由
    • システムの品質に大きく影響する
    • 後からの変更が難しい

非スカラ値(第一正規形未満)

  • 1つのセルに複数の値がある状態
  • 標準SQLにかつて「配列型」が存在し、非スカラ値を保存できた
  • 苗字と名前は分割して保存すべきか?
    • 後から結合することは容易だが、後から分割することは難しいので、分割すべき
    • ただし、意味を損なう分割はしてはいけない

ダブルミーニング

  • 同一列のデータなのに途中から格納する値を変える
    • バグの原因になる
  • 列ごとに格納する値は一貫させる

単一参照テーブル

  • あらゆるタイプのマスタテーブルを1つにまとめてしまう
    • テーブル数は減る
    • SELECT文を共通化できる
  • 文字列長を大きめに設定する必要がある
  • レコード数が多くなりパフォーマンスが低下する
  • コード名を間違えてもエラーが出ないためバグに気づきにくい
  • ER図の可読性を下げる

テーブル分割

  • 水平分割

    • レコード単位にテーブルを分割する
    • 大量データを分けることでアクセス時のパフォーマンスが向上する
    • 分割する論理的な理由が存在しない
    • 拡張性に乏しい
    • パーティション分割で同じことが実現でき、そちらの方が適している
  • 垂直分割

    • カラム単位にテーブルを分割する
    • アクセス時のパフォーマンスが向上する
    • 分割する論理的な理由が存在しない
    • 集約で同じことが実現でき、そちらの方が適している
  • 集約

    • テーブルデータ一部の集計結果を新たなテーブルとして作成する
    • 大きく分けて2種類存在する
      • 列の絞り込み
        • 元のテーブルから使用するカラムだけに絞って新しいテーブルを作成する
          • 定期的に元のテーブルとの同期が必要
      • サマリテーブル
        • レコードを集約するのに毎回SQLの集約関数を実行するとコストがかかる
        • 事前に集約を行ったテーブル(サマリテーブル)を用意しておく
          • 定期的に元のテーブルから値を取得し直す必要がある

不適切なキー

  • 主キー、外部キー、結合キーについて、
    • 可変調文字列は不向き
    • 違うデータ型を指定しない
    • 固定長文字列のコードが望ましい

ダブルマスタ

  • 同じ役割を果たすマスタが2つ存在する
  • SQLが複雑になりパフォーマンスが悪化する
  • システムの統廃合で生まれやすい

バッドノウハウのどこが悪いのか

  • 複雑になると人間にわかりづらく運用にコストがかかる
  • アプリ開発側への影響が大きい

マテリアライズドビュー

  • 普通のビューとの違い
    • 実データを持つ
    • アクセス時にSELECT文が実行されない
    • インデックスを作成できる
    • リフレッシュ(元のテーブルとの同期)の管理が必要
    • ストレージを消費する

第8章 論理設計のグレーノウハウ

バッドまでは行かないグレーノウハウについて、利点と欠点を学んだ。

代理キー

  • テーブルにあるキーだけでは主キーを決められず、内部的に連番を振ったカラムを代理キーとして主キーにする場合がある
    • 主キーが存在しない場合
    • 一意のキーはあるが、使いまわされている場合
    • 一意のキーはあるが、途中で指す対象が変わる場合
  • 代理キーは便利だが、論理的には必要のないキーであり、使わなくてもなんとかなるので、論理家からは批判されている

  • 代理キーを使わないようにするには?

    • 主キーが存在しない場合
      • DB側ではどうしようもないので業務仕様を調節する
    • 一意のキーはあるが、使いまわされている場合や途中で指す対象が変わる場合
      • いつからいつまで使われたのかわかるよう、時期がわかるカラムを持つ

列持ちテーブル

  • 社員ID、社員名、子供1、子供2、子供3...のように繰り返し項目のあるテーブルのこと
  • ぱっと見はシンプルでわかりやすいが、列の増減が大変
    • 基本的には行持ちテーブルにすべき
  • 列持ち<=>行持ちの変換は簡単にできる

アドホックな集合キー

  • 都道府県テーブルに対してつける地方コードカラムなどのような場当たり的につけるキーのこと
  • テーブルに場当たり的にキーを追加していくとパフォーマンスが低下する
  • 他の対応方法
    • 地方コードのテーブルを作成する
      • 結合処理が必要なのでパフォーマンスはあまり改善されない
    • ビューを作成する
    • SELECT文で対応する
      • CASEで分けて地方コードを割り振る

多段ビュー

  • ビューは単なるSELECT文が書かれたファイルに過ぎないので、実行するとオリジナルのテーブルにアクセスしにいく
    • ビューを多段で構成すると中で複数のテーブルにアクセスしにいくため、パフォーマンスが低下する

データクレンジングの重要性

  • データクレンジングとは、業務で利用されているデータをデータベースに登録できる状態にすること
  • 代表的なデータクレンジングの内容
    • 一意キーの特定
      • テーブルの中でデータを一意に特定できるキーを探す
    • 似たような名前を集めて名寄せを行う
  • これ以上汚いデータが増えないよう、統一された入力フォーマットを用意するのが大切

ビジネスロジックの実装箇所

  • 主キー、参照整合性制約、NOT NULL制約といった基本的なルールはデータベース側で実装する
  • それ以外のルールは、データベース側で実装できない場合もあるので、まとめてアプリケーション側で設定する方針に統一する

一時テーブル

  • 一時的にしか使わない一時テーブルは、更新した後に統計情報の収集が必要なため処理時間がかかる
  • 一時テーブルは一時ファイルに割り当てられるため、ストレージの性能はそれほどよくない

第9章 一歩進んだ論理設計〜SQL木構造を扱う

リレーショナルデータベースが表現するのが苦手な木構造をどうやって克服してきたかについて学んだ。

  • リレーショナルデータベースで木構造を表現する方法
    • 隣接リストモデル
      • ノードのレコードに親ノードの情報(ポインタ)を持たせる
      • 最も古くからある方法でポピュラー
      • 更新や検索のSQLが複雑かつパフォーマンスが悪い
    • 入れ子集合モデル
      • ノードを円とみなし、子を含む
      • 更新対象以外のレコードも影響を受けるため、パフォーマンスが低下する
    • 経路列挙モデル
      • ノードをディレクトリとみなし、対象ノードまでの絶対パスを格納する
      • 検索のパフォーマンスが良い
      • 経路に主キーを使うと格納する文字列が非常に長くなる恐れがある
      • 親ノードの追加が大変
    • 上記以外にも新しいモデリング方法が誕生している
      • 本書出版から10年以上経っているので、他にもっと有用なモデリング方法が誕生しているかも?

終わりに

  • 論理設計は物理設計に優先すべき
  • 最終的に整合性を犠牲にせざるを得ない場合もあるが、最初から妥協してはいけない

9月の振り返りと10月の抱負

はじめに

SE5年目のしぐまと申します。
2023年6月11日にHappiness Chainというプログラミングスクールに入会し、約3ヶ月半が経過しました。

入会の経緯については以下に書きました!

wsigma.hatenablog.com

9月が終わったので、9月にやったことを振り返り、10月の目標を設定します。

9月にやったこと

Happiness Chainの課題

目標99時間に対して約108時間。

  • Docker
    • Udemy Docker講座動画の残り5%
    • インプット教材1
    • アウトプット課題
    • インプット教材2(任意)
  • Ruby
    • インプット教材
    • アウトプット課題5点
  • SQL
    • SQLのインプット教材と演習

課題外

  • アウトプット記事作成
    • ブログ記事1本
    • Qiita記事3本

感想

目標時間も達成でき進捗も順調だった!Rubyまで終わらせたいと思っていたが、SQLまで終わらせられた。

Dockerは初めて触れることもあり、アウトプット課題で思いの外苦戦したが、インプット教材を復習し自分なりにまとめ直すことで理解を深められた。
特に最後に行ったインプット教材(任意)は、新しい知識はそれほどないが、それまでの知識を整理するのにとても役立ったので、HC生はぜひ取り組むことをお勧めしたいです。

Rubyは(今のところ今後Railsはやらない方針なので)言語そのものというよりは、コーディング能力向上のために、アウトプット課題中心に取り組んだ。やりがいのある課題で、久々にものすごく集中した。純粋に楽しかった!

SQLでは設計にあまり慣れていなくて演習でも多く間違えた。DB設計は次に取り組むのでそこで知識を深めたい。

課題外でもアウトプット記事を4本書けたのも良かった。説明能力の向上と、同じ問題で困っている見知らぬ誰かのために、積極的に記事を書いて公開していきたい。

英語

約31時間。

  • 英単語クリティカル+
    • mikanを使って隙間時間に毎日最低50単語ずつやるのが習慣になった
  • 【最短最速】中学英語完全攻略【永久保存版】
    • 動画、PDF資料をそれぞれ2周した
  • 英文法入門10題ドリル
    • 2周
  • 英文法基礎10題ドリル
    • 1周目第26講まで
  • 英文法読解入門10題ドリル
    • 1周
    • 簡単だったので2周目はせず、間違えた問題だけ復習
  • 発音マスタークラス
    • Chapter2の途中

感想

単語・文法・読解は概ね順調だが、発音は予定の半分も進まなかった。
発音マスタークラスのChapter1で発音記号を1つずつ教わり、ELSA Speakingで練習したのだが、イマイチできている気がしない。練習不足だと思う。
できないと続かないので、別のやり方を考える。

10月にやること

Happiness Chainの課題

  • DB設計
    • インプット教材2つ
    • アウトプット課題
  • REST
  • テスト技法
  • JavaScript初級
  • JavaScript中級
    • インプット教材 途中まで

抱負

DB設計は復習として演習中心に取り組みたい。

JavaScriptは、ちゃんとわかっていないままなんとなく現職でフレームワークをいじっている感じなので、今回ちゃんと理解して使えるようになりたい。

学習時間の目標は、平日2時間、休日6時間として合計99時間確保したい。

英語

抱負

目的は、技術系の動画を自動字幕ありで見られるようになることと、技術系のドキュメント・エラー文を読めるようになること。

10月は英文法基礎10題ドリル終了後、キク英文法をメインに使って文法・リスニング・発音を同時に身につけていく。

『スッキリわかるSQL入門』を読んだ感想とポイント

はじめに

『スッキリわかるSQL入門[第3版]』を読んだので、感想と特に勉強になったポイントを書きます。

良かったところ

  • 各章の終わりに易しい章末問題があり、章の内容が身についたかチェックできる
  • 付録としてDBMS製品ごとの互換性のポイントがある
    • 実務でも役に立ちそう
  • 本の終わりにドリルが256問ついている
    • 学んだ内容を大量の演習で身につけられる
    • SQL入門の本で設計の問題まであるのは珍しいと思う

悪かったところ

  • テーブル名、カラム名が全て日本語であること
    • 初心者にもとっつきやすいように日本語にしていると思われるが、かなと英数の入力切り替えがとても面倒だった
  • ドリルの回答がPDFであること
    • 改行がされておらず見にくい
    • コピペしづらい

難しかったこと

  • やはり設計が難しかった
    • 特に与えられた資料から非正規形にするのが難しい
    • 絶対的な正解があるわけでもないのが余計ややこしい
    • 経験を積んで慣れていくしかない

学んだこと

今まであまり使ったことのない機能や、普段使用しているMySQLではサポートされていない機能を中心に学んだ。

第2章 基本文法と4大命令

  • 文字列や日付はシングルクォーテーションで囲む
    • MySQLではダブルクォーテーションでも良い
    • Oracleではダブルクォーテーションだと引用識別子となる
      • ASで別名をつけるときなどにダブルクォーテーションを使う

第3章 操作する行の絞り込み

  • WHERE句で= NULLのようにNULLと比較してはいけない理由

    • WHERE句ではTRUEと判定された行だけを返す(処理対象になる)
      • NULLとの比較結果はUNKNOWNになってしまい、処理対象にならない
  • 単なる文字として%_を使いたい場合はESCAPEを設定する

  SELECT * FROM table_name WHERE column_name LIKE '%100$%' ESCAPE '$'
  • ANY演算子
    • 値リストのそれぞれと比較し、いずれかが真なら真
    • INと同じ
  column_name = ANY (値1, 値2, 値3)
  • ALL演算子
    • 値リストのそれぞれと比較し、すべて真なら真
    • NOT IN<> ALLは同じ
  column_name = ALL (値1, 値2, 値3)
  • ANYALLは計算式や副問い合わせと組み合わせて初めて真価を発揮する

  • NOT IN<> ALLで比較する対象にNULLが含まれると、判定結果もNULLになってしまうので注意

  -- 比較結果はすべてNULLになってしまう
  SELECT * FROM table_name
  WHERE column_name NOT IN (A, B, NULL)
  • 論理演算子の優先順位
    • 高い順にNOT, AND, OR

第4章 検索結果の加工

  • OFFSET - FETCH
  OFFSET 先頭から除外する行数 ROWS
  FETCH NEXT 取得行数 ROWS ONLY
  • 今まで使っていたMySQLではサポートされていないので知らなかった

    • MySQLでもLIMIT 取得件数 OFFSET 開始位置のように開始位置を設定できる
  • 集合演算子

    • UNION 和集合
    • EXCEPT 差集合
    • INTERSECT 積集合
    • 注意点
      • 使用するテーブルの各カラムの列数とデータ型が一致している必要がある
      • ORDER BYは最後のSELECT文の後ろに書く
      • 演算子の後ろにALLをつけると重複を排除しない

第5章 式と関数

  • 関数はDBMS製品による差異が大きいので、都度調べる必要あり

  • 文字列連結は||関数を使う

    • ただし、MySQLではサポートされていないのでCONCAT関数を使う
  • 型が違うデータを組み合わせるにはCASTを使用する

  • 式や関数はSELECT 式や関数という形式でFROM句なしで実行できる

    • ただし、OracleDb2ではサポートされていないので、FROM句にダミーのテーブルを指定する

第6章 集計とグループ化

  • COUNT(column_name)とすると、カラムがNULLの行を無視してカウントする

第7章 副問い合わせ

  • 副問い合わせには検索結果の行数と列数によって以下の種類がある

    • 単一行副問い合わせ(1行)
    • 複数行副問い合わせ(1列n行)
    • 表の結果となる副問い合わせ(n列m行)
  • 相関副問い合わせ

    • 「主問い合わせがテーブルから行を絞り込む過程で、各行について抽出の可否を判断するために、繰り返し副問合せを実行する」処理
    • 通常の副問い合わせよりは処理負荷が大幅に増える
  SELECT column_name FROM tableA
  WHERE EXISTS
  (SELECT * FROM tableB WHERE tableA.column = tableB.column)

第8章 複数テーブルの結合

  • 完全外部結合は左右の表の全行を必ず出力する
  SELECT ~ FROM left_table
  FULL [OUTER] JOIN right_table
  ON 結合条件
  • MySQLMariaDBにはFULL JOINがないので、LEFT JOINした結果とRIGHT JOINした結果をUNIONをつけて代用する

第9章 トランザクション

  • 複数の利用者が同時に処理を実行しようとした時の代表的な副作用

    • ダーティリード
      • まだコミットされていない未確定の変更を他の人が読めてしまう
    • 反復不能読み取り(アンリピータブルリード)
      • あるテーブルに対してSELECTした後、他の人がUPDATEしてしまったため、次にSELECTした時に結果が変わってしまう
    • ファントムリード
      • あるテーブルに対してSELECTした後、他の人がINSERTやDELETEしてしまったため、次にSELECTした時に結果の行数が変わってしまう
  • 副作用を発生させないためには、各トランザクションを分離する必要がある

    • ただ、他人の処理が終わるまで絶対に待たせる、とすると処理速度が遅くなってしまう
    • どの程度分離するかを設定するとDBMSが適宜処理してくれる
  • SQLで指定した対象を明示的にロックすることもできる

  • ロックには2種類の強度がある

  • 通常SELECT文を実行した場合は共有ロックが自動的にかかる

  • 行に対して排他ロックをかけるには以下を実行する
    • NOWAITオプションを指定すると、アクセスしようとした行がすでにロックがかかった場合、待たずに即エラーを返す
  SELECT ~ FOR UPDATE [NOWAIT]
  • 表に対してロックをかけることも可能

第10章 テーブルの作成

  • 人的ミスを防ぐために、意図しないデータが追加・更新されないよう制約をかけておく
  • CREATE TABLE文で指定する制約
    • NOT NULL制約
      • DEFAULT指定と組み合わせて利用させることが多い
    • UNIQUE制約
    • CHECK制約
    • PRIMARY KEY制約
      • NULLも重複も許されない
  -- 主キー制約の指定(単独列)
  CREATE TABLE table_name (
    ID INTEGER PRIMARY KEY, 
    ...
  )
  -- 主キー制約の指定(複合主キー)
  CREATE TABLE table_name (
    ID INTEGER,
    name VARCHAR(40) UNIQUE,
    PRIMARY KEY (ID, name)
  )
  • FOREIGN KEY制約
    • 参照整合性(外部キーが指し示す先にあるべき行が存在しリレーションシップが成立していること)
    -- 外部キー制約の指定方法1
    CREATE TABLE table_name (
    column_name データ型 REFERENCES 参照先テーブル名(参照先列名)
    ...
    )
    
    -- 外部キー制約の指定方法2
    CREATE TABLE table_name (
      ...
      FOREIGN KEY (参照元列名)
       REFERENCES 参照先テーブル名(参照先列名)
    )
    

第11章 さまざまな支援機能

  • 検索を高速化するためにインデックスを追加する

    • 高速化が見込まれるパターン
      • WHERE句による絞り込み条件でインデックスを作成した列を指定した場合
        • 完全一致検索や前方一致検索では効果がある
        • 部分一致検索や後方一致検索では効果なし
      • インデックス列をORDER BY句に指定した場合
      • インデックス列をJOINの結合条件に指定した場合
    • インデックスのデメリット
      • 索引情報の保存にディスク容量を消費する
      • テーブルデータの作成・更新・削除時にインデックスも書き換える必要があり、オーバーヘッドが増える
  • OracleSQL Serverなど一部のDBMSでは、連番を管理してくれる専用の道具(シーケンス)を利用できる

    • シーケンスは裁判した最新の値を常に記憶し、シーケンスに指示すると「現在の値」や「次の値」をすぐ取り出せる
    • 複数のトランザクションからの利用を想定し、シーケンスから値を取り出すと、その操作はすぐに確定し、ロールバックしても元に戻らない
  • ACID特性

    • データを正確かつ安全に取り扱うための4つの特性
      • 原始性
      • 一貫性
        • データの内容が矛盾した状態にならない
        • 型や制約が保証
      • 分離性
        • 複数の処理を同時実行しても副作用がない
        • 分離レベル、ロックが保証
      • 永続性
        • 記録した情報は消滅せず保持され続ける
        • バックアップが保証
  • バックアップファイルの取得には時間がかかる

    • 実行されたすべてのSQL文を保持するログファイルを高頻度でバックアップしておく
    • バックアップファイルとログファイルで復旧できる

第12章 テーブルの設計

  • データベース設計の流れ

    • 概念設計
      • ユーザから聞いた要件をエンティティと属性に整理し、ER図を作成する
    • 論理設計
      • エンティティをRDBモデルで扱いやすいテーブルに整理する
        • 主キーの整理、正規化
    • 物理設計
      • DBMS選定後に、サポートする型、制約、インデックス、利用するハードウェアの制約を考慮し、すべてのテーブルの詳細設計を行う
        • 最終的にデータベース内に作られるテーブル名やカラム名物理名という
        • 論理設計までの段階までに利用してきた名前を論理名という
  • 主キーが備えるべき3つの特性

    • 非NULL性
    • 一意性
    • 不変性
  • 正規化

    • 第一正規形
      • 非正規形から繰返しの列やセルの結合をなくす
        • 繰り返しの列の部分を別の表に切り出す
        • 新しい表で仮の主キーを決める
        • 元の表の主キーをコピーして複合主キーにする
    • 第二正規形
      • 複合主キーの一部にのみ関数従属する列をなくす
        • 複合主キーの一部に関数従属する列を切り出す
        • 元の表で部分関数従属していた列をコピーする
    • 第三正規形
      • 主キーに関数従属する列に関数従属する列をなくす
        • 間接的に主キーに関数従属している列を切り出す
        • 元の表で直接的に関数従属していた列をコピーする
  • トップダウンアプローチで作成したER図を元に、ボトムアップアプローチで作成したER図から得られる情報を適切に取り込むことが大切

    • トップダウンアプローチ
      • 新システム要件を元に概念設計、論理設計を行う
      • これだけでは現状が反映されない
    • ボトムアップアプローチ
      • 現行システムや資料を元に論理設計を行う
      • これだけでは新システムの要望が反映されない

SQLドリルでよく間違えること

  • 文字列は'で囲み、ASの名称は"で囲むこと
  • CASTしなくても通る場合があるが、実務ではCASTしないとインデックスが使われずパフォーマンスが低下するので、使用するようにすること

LGTM後に行うべき「mainブランチのマージと作業ブランチの削除」についてまとめた

はじめに

HappinessChainではmainブランチから作業ブランチを切ってpull requestすることで課題提出を行っています。

LGTMをもらった(approveされた)後に何もせずにいると、mainブランチには作業した内容が含まれませんし、作業ブランチがどんどん増えていってしまいます。

それを防ぐためにapprove後に毎回行っているmainブランチへのマージ作業と作業ブランチの削除作業についてまとめました。

今回は例としてgolf_scoreブランチを作業ブランチとし、このブランチをmainブランチにマージした後、削除していきます。

注意

こちらはあくまでHappiness Chainの課題がapproveされた後に行う操作です。
現場ではmainブランチに勝手にマージしてはいけなかったり、リモートブランチを削除してはいけなかったりとそれぞれルールがあると思いますので、現場のルールに従ってください。

GitHubでmainブランチにマージする

マージはGitHubで行います。

mainブランチへのマージはPull requestsの画面で実行できます。
レビュワーにapproveをもらうと次のような画面が出てくるのでボタンを押します。

merge pull request

Confirm merge

マージすると『Merged』と表示されるようになります。

Merged

GitHubでリモートブランチを削除する

リモートブランチの削除もGitHubから行えます*1

golf_scoreブランチは今回の作業用に作成したブランチで今後使う予定はなく、変更した分はすでに先程mainブランチにマージしたので、削除します。

Delete branch

ターミナルでpullする

ここからの作業はターミナルで行います。

ローカルのmainブランチをリモートの最新のmainブランチと一致させます。
マージコミットを残したくない場合はpullするときに--rebaseをつけます。

$ git checkout main
$ git pull origin main [--rebase]

ローカルでブランチを削除する

まだローカルにはリモート追跡ブランチもローカルブランチも残っている状態です。

$ git branch -a
  golf_score
* main
  remotes/origin/golf_score
  remotes/origin/main

リモート追跡ブランチを削除する

先程GitHubでリモートブランチは削除したので、削除したことをローカルに反映させることでリモート追跡ブランチ(origin/golf_score)を削除します。

$ git fetch --prune
From https://github.com/wsigma21/hc_practice
 - [deleted]         (none)     -> origin/golf_score

これでリモート追跡ブランチは削除されました。

$ git branch -a
  golf_score
* main
  remotes/origin/main

ローカルブランチを削除する

ローカルブランチも削除します。

$ git branch -d golf_score
Deleted branch golf_score (was d8a92de).

これでmainブランチだけになってスッキリしました。

$ git branch -a
* main
  remotes/origin/main

新たな課題に取り組むときは、またmainブランチからブランチを切って作業していきます。

*1:ターミナルで行うことももちろん可能ですが、ここでは例としてGitHub上で行います

Rubyのoptparseの使い方について

はじめに

HappinessChainの課題でRubyのoptparseを使用したので、使い方について簡単にまとめます。
使用したRubyのバージョンは3.2.2です。
optparseについての詳細は以下の公式リファレンスを確認してください。

docs.ruby-lang.org

optparseとは何か

optparseはRubyの標準ライブラリの一つで、コマンドラインのオプションを取り扱うのに使用します。

optparseを使わない場合

optparseを使用しなくても、ARGVを使用すればコマンドラインの引数を受け取ることはできます。

たとえば、aオプションとbオプションを使い、それぞれに数値の引数を受け取りたいとします。

$ ruby sumple.rb -a 1 -b 2
# sample.rb
hikisu0 = ARGV[0] # "-a"
hikisu1 = ARGV[1] # "1"
hikisu2 = ARGV[2] # "-b"
hikisu3 = ARGV[3] # "2"

しかし、こちらですと、オプションと値のセット(上記の例だと-a1, -b2)には関連性はありません。
値が数値かどうかのチェックも1から実装しなくてはなりません。

optparseを使えば、与えられたオプションごとに値をチェックすることができます。

オプションを受け付ける

たとえば、上記と同じようにaオプションとbオプションを受け付ける場合は、以下のように書きます。

$ ruby sumple.rb -a 1 -b 2
# sample.rb

# ライブラリoptparseの読み込み
require 'optparse'

# optpaserのインスタンス化
opt = OptionParser.new

# オプションの登録
opt.on('-a')
opt.on('-b')

# 受け取った引数がARGVに格納される
opt.parse(ARGV)
p ARGV # ["-a", "1", "-b", "2"]

すると、a, b以外のオプションを指定した場合はエラーにしてくれます。

$ ruby sample.rb -m 1
invalid option: -m (OptionParser::InvalidOption)
invalid option: m (OptionParser::InvalidOption)

破壊的メソッドにすると、ARGVにオプションは含まれません。

opt.parse!(ARGV)
p ARGV # ["1", "2"]

引数の入力をチェックする

以下のように書くと、aオプションの引数がなかった場合にエラーにしてくれます。

require 'optparse'

opt = OptionParser.new

opt.on('-a VAL')
opt.parse(ARGV)
p ARGV
$ ruby sample.rb -a
missing argument: -a (OptionParser::MissingArgument)

引数が必須でない場合は[]をつけます。

opt.on('-a [VAL]')

引数の型をチェックする

VALの後に型を指定することができます。

opt.on('-a VAL', Integer)

指定した型以外の値が入力された場合はエラーを出してくれます。

$ ruby sample.rb -a arg
invalid argument: -a arg (OptionParser::InvalidArgument)

『プロを目指す人のためのRuby入門』を読んだ

はじめに

HappinessChainのインプット課題として、『プロを目指す人のためのRuby入門』を読んだので、簡単に感想を書きます。

読み方

事前に著者が公開している動画を視聴して参考にした。

youtu.be

11日間かけて、1日1時間〜5時間程度かけて読んだ。

コードは基本的に読んで理解し、例題と、重要な箇所で理解しづらいところだけは手で写したり自分でアレンジしたりした。

良かったところ

コード例が豊富でわかりやすい

新たな概念の説明ごとにコードがあり、わかりやすかった。

GitHubでサンプルコードも公開されているので、ちょっと動かしたい時や自分で写経してうまく動かなかった時に便利。

既存の内容を扱った箇所を教えてくれる

すでに前の章で説明した内容について、どこで扱ったかを必ず書いてくれているのがありがたい。

「あれ?これ前に出てきたけどなんだったっけ...」となりがちだが、そういう時に必ず「これこれについては7.10.5を読み返してください」「5.6.5を参照」のように、扱った箇所を書いてくれている。わからない時は一旦そこに戻って読み返すことができたので、復習しやすかった。

ハイレベルな内容も扱っている

タイトルに"プロを目指す人のための"とあるように、Ruby初心者から上級者向けの内容も扱っているので、本格的にRubyの開発を進めた後にまた読み返しても、まだまだ新たに学ぶことがありそう。

難しかったところ

全体的に、他の言語に全く触れたことがない人にとっては難しめな本だと感じた。私は一応他言語での開発経験があり、オブジェクト指向言語JavaPythonに触れた*1ことがあるので、ある程度はついていくことができた。

上述の動画で著者が言及しているように、7章以降は難しかった。
特にモジュールとProcはまだ理解が浅く、章の後半は軽く目を通した程度になっている。今後必要な時に適宜読み返したい。

学んだこと

ここからは、各章で学んだことを簡単に記載する。

第1章 本書を読み進める前に

本の概要、Rubyの動かし方、Rubyの公式リファレンスについて学んだ。
公式リファレンスについては、この本の著者が書いている以下のZennの本も参考になった。

zenn.dev

第2章 Rubyの基礎を理解する

数値、文字列、型、メソッドについて学んだ。
Rubyは標準出力だけでputs, print, p, ppと4種類もあって、それらの違いの話が興味深かった。

コード解析ツールRuboCopについて簡単に紹介されていた。今後のアウトプット課題で必要なので導入した。
余談だが、以下のQiita記事を参考にしてフォーマットもRubocopにしてもらえるように設定したり、拡張機能の「endwise」を入れて自動でendが補完されるようにした。

qiita.com

第3章 テストを自動化する

今後作成するプログラムの動作確認のために、Minitestというフレームワークを使ってRubyのテストを自動化する方法について学んだ。

第4章 配列や繰り返し処理を理解する

配列、ブロック、繰り返し処理について学んだ

この章の例題で初めてテスト駆動開発に触れられてとても楽しかった!
実装するよりまずテストコードを書いて、テストがちゃんと失敗することを確かめるという手順を踏むのが新鮮だった。

第5章 ハッシュやシンボルを理解する

ハッシュとシンボルについて学んだ。

ProgateでRubyRailsをやっていて、:name: nameと書いたりname:と書いたりよくミスっていたのだが、シンボルというものであり、文字列とは違うことをようやく理解できた。

第6章 正規表現を理解する

Rubyでの正規表現オブジェクトについて学んだ。

この章で紹介されていた、この本の著者が書いた正規表現についてのQiita記事のシリーズはものすごく勉強になった
今まで仕事で正規表現が出てくるたびにググったサンプルを四苦八苦して組み合わせていたのだが、今回時間をかけて手を動かしてこのシリーズを読んだおかげで、自分できちんと考えて正規表現を書けるようになった。

qiita.com

第7章 クラスの作成を理解する

Rubyでのオブジェクト指向について学んだ。

例題の改札機プログラムでオブジェクト指向の便利さを実感した。独力でこのようなコードをまだ書けないので、今後アウトプット課題で鍛えたい。

書かれた場所によってselfキーワードが指すものが変わるのだが、その辺りが理解しきれなかった。

第8章 モジュールを理解する

モジュールについて学んだ。

Rubyは多重継承はできないが、モジュールをクラスにincludeして機能を追加する(ミックスインする)ことで多重継承っぽいことはできる。

公式リファレンスでよく見かけるEnumerableや比較の時によく使用する<=>についても理解できた。

第9章 例外処理を理解する

Rubyでの例外の捕捉方法について学んだ。

紹介されていた例外処理のベストプラクティスの考え方は多言語でも活かせそうだった。
たとえば、例外処理部分自体をテストしておくこととか。いざ例外処理が発生した時にうまく処理できなかったら、例外処理を書いておいた意味がなくなってしまう。

第10章 yieldとProcを理解する

yieldとProcについて学んだ。

ブロック(手続きのまとまり)を引数としてメソッドに渡したり、Procクラスを使ってブロックをオブジェクト化したりした。

私はJavaScriptのコールバック関数もまだあまり使いこなせていないので、どうも「処理のかたまりをオブジェクトとして扱う」みたいなことが苦手なようだ。

第11章 パターンマッチを理解する

Ruby3.0で新しく導入されたパターンマッチについて学んだ。
第8〜10章までがだいぶ難しかったので身構えていたのだが、あまり苦労せず理解できた。

第12章 Rubyデバッグ技法を身につける

Rubyでのデバッグ方法や汎用的なトラブルシューティングについて学んだ。

第13章 Rubyに関するその他のトピック

付録のような感じで、本編に出てこなかった色々なトピックに触れた。

*1:アウトプット不足なので本当に"触れた"程度

docker-composeでwebアプリをdocker化する方法

はじめに

現在在籍しているHappiness Chainというプログラミングスクールの課題の一環として、docker-composeを使ってrailsで作られたwebアプリをdocker化する方法について記載しておきます。

なお、あくまでdocker化に焦点を当てることとし、railsアプリの作成方法については記載しません。

Railsプロジェクトの取得

docker化するRailsプロジェクトのコードをgitから取得します。

git clone <repositoryURL>

Dockerfileの作成

まず、Dockerfileを作成します。Dockerfileはコンテナの中身を記載するファイルで、ビルド時に読み込まれます。

FROM ruby:3.2.2
RUN apt-get update -qq && apt-get install -y \
  build-essential \
  libpq-dev \
  nodejs \
  postgresql-client \
  yarn
WORKDIR /rails-docker
COPY Gemfile Gemfile.lock /rails-docker/
RUN bundle install

Dockerfileの書き方

簡単に中身の解説をします。

FROMで使用するイメージを取得します。
RUNでアプリに必要なライブラリやツールをインストールします。
本アプリを動かすディレクトリをWORKDIRで指定します。ここで指定したディレクトリがコンテナ内に存在しない場合は新規作成してくれます。
Railsアプリで使用するGemfileとGemfile.lockは、今回はすでに作成されているので、それをそのままCOPYでコンテナにコピーします。
最後にbundle installを実行します。これで適切にgemをインストールします。

なお、このコマンドの記載順には意味があります。再ビルド時にはキャッシュをうまく使って時間短縮しており、変更があった箇所だけ再実行されるからです。

例えば、Gemfile、Gemfile.lockを更新したとします。
この場合、改めてbuild-essentialなどをインストールする必要はありませんが、必ずbundle installを行いたいため、上記のような順番で書いておきます。

docker-compose.ymlの作成

次に、docker-compose.ymlを作成します。 docker-compose.ymlはdocker composeの設定ファイルです。コンテナをどのように使っていくかについて記載します。

docker composeのメリット

docker composeについて説明する前に、docker-composeを使わない通常のコンテナの起動について見ておきます。
例えば以下のように行います。

docker run -d \
  --network app --network-alias mysql \
  -v mysql-data:/var/lib/mysql \
  -e MYSQL_ROOT_PASSWORD=secret \
  -e MYSQL_DATABASE=todos \
  mysql:8.0

これは起動時に使用するネットワーク、ボリューム、環境変数、データベースをオプションで指定しているのですが、とても長いコマンドになっていて、打ち込むのも他の開発者に伝えるのも大変ですよね。

docker composeを使えば、このたくさんのオプションはdocker-compose.ymlという設定ファイルに記載しておき、起動時はdocker-compose upだけで良くなります。

起動も楽ですし、他の開発者の方にもdocker-compose.ymlを共有するだけで良くなります。

他にも、docker composeを使うと複数のコンテナを立てやすくなるというメリットもあります。

docker-compose.ymlの書き方

docker composeのメリットがわかったところで、docker-compose.ymlを見ていきましょう。

例えば、railsとpostgresを別々のコンテナに格納する場合、docker-compose.ymlには以下のように設定します。
servicesの下にあるweb, dbがそれぞれrails, postgresのコンテナを指します。

# 現在は大抵バージョン3を指定する
version: '3'

# ホストにボリューム領域を作成
volumes:
  db-data:

services:
  web:
    build: .
    # アプリ起動時に実行するコマンドを指定
    # ここではrailsサーバの起動を実行している
    command: bundle exec rails s -p 3000 -b '0.0.0.0'
    # アプリのコードはホストだけに設置としたいため、ホストのファイルをコンテナにマウント
    volumes:
      - .:/rails-docker
    # database.yml(後述)内で使用する環境変数を指定
    environment:
      - 'DATABASE_PASSWORD=postgres'
    # ホストとコンテナのポートを指定
    # railsの場合はデフォルトが3000
    ports:
      - "3000:3000"
    # 先にデータベースを作成するよう指定
    depends_on:
      - db
  db:
    # 使用するデータベースの種類とバージョンを指定
    image: postgres:12
    # コンテナが削除されてもデータが消えないように、ホストのファイルをコンテナにマウント
    volumes:
      - 'db-data:/var/lib/postgresql/data'
    # データベースの環境変数を指定
    environment:
      - 'POSTGRES_USER=postgres'
      - 'POSTGRES_PASSWORD=postgres'

なお、このコンテナをクラウドで公開する場合は、データベースの環境変数はここに書いてはいけません。
別途環境変数を指定するファイルを作成してそこに記載し、そのファイルはGitで共有しないなど対策をしてください。方法については、例えばこちらを参照してください。

config/database.ymlの作成

次にdatabase.ymlの作成をします。
database.ymlはデータベースの設定値について記載したファイルです。
postgresの場合は以下のように書きます。

default: &default
  adapter: postgresql
  encoding: unicode
  # host名がdocker-compose.ymlのサービス名になる
  host: db
  username: postgres
  # postgresのデフォルトのポートは5432
  port: 5432
  # このファイルをGitに上げたりするので、パスワードは直接書かない
  # 環境変数はホスト側(docker-compose.yml)から読み取る
  password: <%= ENV.fetch("DATABASE_PASSWORD") %>
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>

使用するデータベースの種類によっても異なるため、詳細は割愛します。

ビルドしてデータベースの作成

設定ファイルの作成が終わったので、ビルドしてコンテナ内でデータベースを作成します。

# バックグラウンドで起動するので「-d」オプションを追加
# ビルドが2回目以降の場合はさらに「--build」をつけること
$ docker-compose up -d
# コンテナの中に入る
$ docker-compose exec web bash
# データベースを作る
$ rails db:create
# データベースにテーブル定義を作る
$ rails db:migrate
# コンテナから出る
$ exit
# 再度起動
$ docker-compose up

正常にデータベースが作成できていれば、エラーは出ません。

起動して確認

ブラウザでlocalhost:3000にアクセスできるかを確認します。
また、ボリュームのマウントが上手くいっているかも確認してください。

  1. アプリでデータ登録の処理を実行する
  2. Ctrl + Cでアプリを停止
  3. $ docker-compose upで再度起動
  4. localhost:3000にアクセスし、1で登録したデータが残っているか確認する。

ここで残っていなければ、ボリュームのマウンドがうまくいっていないので、docker-compose.ymlの当該箇所を確認してください。

参考