SQLアンチパターン 第5章を読んで
を読んでの要約、感想です。
目的
同じ属性を持ちつつも、例えばSmartphoneというオブジェクトがあるとして方やandroid_version、方やios_versionを持っている場合に対応したいというのが今回の目的です。
アンチパターン
別テーブルをつくり属性を行に格納してしまうというものです
smartphone_id | os_version | os_version_value(VARCHAR) |
---|---|---|
1 | android_version | oreo |
2 | ios_version | 12 |
3 | blackberry_version | 7.1 |
いちいちSmartphonesテーブルに列を増やさなてくてもいいですし、急なOSの登場にも耐えられます。
デメリット
- 対象の行を指定してから取り出すして使う必要がある。(これは織り込み済みのデメリットの気もしますが)
- 行に属性がはいるのでデータ型なんかは整合性はとれず文字列をいれるしかありません。もしくはデータ型に対応するカラムをたくさん用意するかです。
- 行として属性をもっているので内部結合しようと持っていない属性を指定してしまうと結合できず、何も取り出せないので外部結合するしかありません。必然的に扱う行の数は多くなります。
使用してもいい場
SQL以外の非リレーショナルな技術をつかう時とあります。確かに。
解決策
- シングルテーブル継承
サブタイプの数が限られていれば
smartphone_id | name | os | android_version | ios_version |
---|---|---|---|---|
1 | iphone6 | ios | oreo | null |
2 | honor9 | android | null | 12 |
ActiveRecordを使うような単純な設計の時◯です。
- 具象テーブル継承
変化する属性ごとにテーブルをつくります。
smartphone_id | name | os | ios_version |
---|---|---|---|
1 | iphone6 | ios | 12 |
smartphone_id | name | os | android_version |
---|---|---|---|
2 | honor9 | android | oreo |
nullがなくなります
- クラステーブル継承
smartphone_id | ios_version |
---|---|
1 | 12 |
smartphone_id | android_version |
---|---|
2 | oreo |
共通要素を元テーブルで持ち、違う要素をそれぞれ別のテーブルに切り出すというものです。その名の通りクラス継承と同じ形です。
- 半構造化データ
smartphone_id | name | os | version_attributes |
---|---|---|---|
1 | iphone6 | ios | android_version: oreo |
2 | honor9 | android | ios_version: 12 |
LOB列を作りそこにJSON等の形式で格納してしまおうというものです。SQLでアクセスできなくなるので流石に無理がある気がします。
感想
メタデータを格納させると大変だよという章だと思いました。現実的にはシングルテーブル継承が一番使いやすいかなと思います。
SQLアンチパターン 第四章を読んで
を読んでの要約、感想です。
目的
データベースのアーキテクチャを単純にしたい データベースのアーキテクチャ、複数のテーブルが複雑にからみあう中で、その関係をもっと柔軟にしたいとういのが今回の目的です。
アンチパターン
外部キー制約をしない
デメリット
- 関係してるテーブルを確認してから削除や追加をして、という作業をしている間に新たな行が作られたりしてぼっちの情報ができるのを防ぐことができない
- 外部キー制約がない場合全ての参照を調べなければならない
使用してもいい場合
外部キー制約が用いれない場合は仕方ないと本書にはあります。
解決策
外部キー制約を使用する。
ミス防止は投資以上の効果を発揮する事例が紹介されていました。外部キーを使用するのはミスを防ぐのでとても有用であるとのことです。
例えばRailsでもvalidationを定義するのはエラーメッセージ等レールにのれるというメリットもありますが、それ以上にミスを防げるというのがメリットだというのと同じだと思います。
制約を作ることは柔軟性を下げるかもしれませんがそれ以上にバグを減らすメリットがあるということですね。
カスケード更新もできるから柔軟性もあるとあります。確かにカスケード更新はよく書かれていますね。
関係ないですがrailsのdependent: :destroy
だけだと単に探して消してくれているだけです。
Guide Load (0.3ms) SELECT "guides".* FROM "guides" WHERE "guides"."champion_id" = $1 [["champion_id", 5]] Guide Destroy (0.3ms) DELETE FROM "guides" WHERE "guides"."id" = $1 [["id", 3]] Champion Destroy (0.3ms) DELETE FROM "champions" WHERE "champions"."id" = $1 [["id", 5]] (0.1ms) RELEASE SAVEPOINT active_record_1
感想
外部キー制約を使いなさいという章でした。手軽にできてバグを大幅に減らしてくれるなんてなんて素敵な制約なんだ…
SQLアンチパターン 第三章を読んで
アンチパターン
行の存在の重複をさけるため、主キーを定めることが多くあります。idというカラムを作って主キーの役割をもたせることも多いです。
今回のアンチパターンは全てのテーブルはidというカラムを持たせる、というものです。Railsなんかでもデフォルトではidつけますね。
idカラムの特徴は
- 数字が入ります(正確に言うと32bit or 64bitの整数)
- 同じ数字が入ることはありません
当たり前だと思っていたidもアンチパターンになることがあります。
デメリット
- 冗長なキーができてしまう
ポケモンでテーブルを作成すると
id | pokemon_name |
---|---|
1 | フシギダネ |
2 | ゼニガメ |
3 | ヒトカゲ |
この場合nameは必ず一意になりますしUNIQUE制約もつけられるのでidカラムは必要なキーとは言えなくなります。
また他のテーブルの主キーの組み合わせのを保存する交差テーブルを作っる時にその組み合わせにidをふっても組み合わせの一意にはならないので、組み合わせ2つのカラムにUNIQUEをつける必要がありますがそうなるとidカラムの意味は…ということになります。
- idという名前はかならずしもわかりやすくない idよりpokemon_nameのほうがわかりやすいです。
- USINGが使用できない カラムにpokemon_nameを持ってる例えばTrainersテーブルではUSING(pokemon_name)と使えますが全てのテーブルの主キーがidだとTrainersもidがあるのでusingにidが使えません。
使用してもいい場合
ORMフレームワーク(Ruby on Rails等)では設定より規約ということで、idという整数の疑似キーのカラムがある前提で便利な機能がついています。レールから外れるとその分大変です。ですが疑似キーをid以外の名前(pokemon_idとか)にすることもできることは覚えておいたほうが良さそうです。
Railsも主キーを変更することは公式でサポートされています。
https://railsguides.jp/active_record_migrations.html
解決策
状況に応じて適切に調整しようとあります。そりゃそうだという気持ちになったのは私だけでしょうか。
- 主キーをわかりやすい名前にする
idをproduct_idにするとかですね
- 自然キー、複合キーの活用
例のpokemon_nameのように自然に主キーとして使えるものもあるでしょう。こういった自然キーがない、もしくは将来的に信用できない時に擬似キーを使うべきです。 複合キーもカラムの組み合わせが行を識別する時に最適というときには使うといいでしょう。
感想
規約は必要不可欠というわけではない、柔軟に対応しようねといった章だと思いました。 idというカラムを主キーにすることが悪というよりはそれ以外の主キーの設定もあると知らないのが悪という感じですね。そもそも主キーが連番である必要もデータ型である必要もないというのをデータベース実践技術入門で読んだことがあります。
SQLアンチパターン 2章
アンチパターン
この章ではツリー指向のデータ構造を格納する際のアンチパターンになります。
ツリー指向のデータ構造では、ノードは一つ以上の子ノード及び一つの親ノードを持ち一番上がルート、一番下がリーフとなります。文章だと難しいので視覚で、
このような構造です。ディレクトリなんかと同じ構造ですね。 この構造を格納するためにparent_idカラムをつくり格納すると以下のようになります。
parts_id | parts_name | parent_id |
---|---|---|
1 | SQLアンチパターン | |
2 | 一章 | 1 |
3 | 二章 | 1 |
4 | 一節 | 2 |
5 | 二節 | 2 |
6 | 一節 | 3 |
7 | 二節 | 3 |
8 | 一項 | 6 |
この構造は隣接リストと呼ばれています。これが今回のアンチパターンです。
デメリット
- 全ての子ノードを取得したい時に(例えば東京から下のノードをすべて取りたい時)に一つ下の子ノードしか辿れないので、一つ一つジョインしていくしかありません。これでは階層が深くなった時に対応できません。
- ノードの追加や移動は簡単ですが、削除するには削除したいノードの子ノードも全て探して削除しなければなりません。またノード単体を移動したいときも、子ノードは一つ上の親に依存しているので親ノードが移動すると子ノードとその下のノードは全てツリーが移動してしまいます。
使用してもいい場合
ノードの削除が要らなかったり親と子だけ別れば良いときには使用しても問題ありません。家系図なんかは追加はしても削除はしないので、使用しても問題はないと思います。
解決策
他のツリーモデルを利用することです。本では3つの構造があげられています。
経路列挙
parts_id | parts_name | parent_path |
---|---|---|
1 | SQLアンチパターン | |
2 | 一章 | 1 |
3 | 二章 | 1 |
4 | 一節 | 1/2 |
5 | 二節 | 1/2 |
6 | 一節 | 1/3 |
7 | 二節 | 1/3 |
8 | 一項 | 1/3/6 |
UNIXのパスと同じ形で上のツリーのノードを記録する構造です。 この構造のメリットは子ノードを簡単に取得できます。1/3/%といった形で一章の子ノードを階層がいくら深くても全て取得できます。
入れ子集合
parts_id | parts_name | nsleft | nsright |
---|---|---|---|
1 | SQLアンチパターン | 1 | 16 |
2 | 一章 | 2 | 7 |
3 | 二章 | 8 | 15 |
4 | 一節 | 3 | 4 |
5 | 二節 | 5 | 6 |
6 | 一節 | 9 | 12 |
7 | 二節 | 13 | 14 |
8 | 一項 | 10 | 11 |
子孫の集合に関する情報をそれぞれの行に入れています。
nsleftには下のノード全ての値より小さい値、nsrightには逆に下のノード全ての値より大きい値が入っています。
ノードAの下のノードを全て特定したいときはAの子ノードの範囲内にnsleftが入っているノードを検索することで達成できます。逆にAの上のノードを全て検索したいときはAのnsleftを範囲内に持つノードを検索することで取得できます。
またあるノードを削除しても子ノードや親ノードを検索することに不都合はなく、削除されたノードの子ノードは親の親ノードの子であるとみなされるので削除も容易です。
しかしノードの挿入や移動は他の構造より複雑になります、全てのノードが親だけに依存しているわけではないので移動するといちいち値の計算のし直しです。
閉包テーブル
parant_id | child_id | parant_id | child_id | parant_id | child_id |
---|---|---|---|---|---|
1 | 1 | 2 | 2 | 5 | 5 |
1 | 2 | 2 | 4 | 6 | 6 |
1 | 3 | 2 | 5 | 6 | 8 |
1 | 4 | 3 | 3 | 7 | 7 |
1 | 5 | 3 | 6 | 8 | 8 |
1 | 6 | 3 | 7 | ||
1 | 7 | 3 | 8 | ||
1 | 8 | 4 | 4 |
テーブルの親と子の組み合わせを深さに関係なく全て格納するテーブルを作成する構造です。ノード自身を参照する組み合わせも格納します。前回の関係のためだけの中間テーブルに似ている気もします。
下のノード全てを検索するのは簡単でparant_idが4の組み合わせを取り出して元のテーブルをjoinすればいいです。先祖もchild_idから特定できます。
一ノードを追加するとその親とその上のノード全てを取得して新しいノードをchild_idとして登録することになります。削除では削除したいノードをchild_id持つ行を全て削除することになります。全て削除とかは機械は得意なのでいいですね。
もちろん閉包テーブルを削除しても章の情報は残ったままです。これはノードの関連付けを柔軟にできることにも繋がります。
どの設計を使うべきか
それぞれメリット・デメリットあります。
- アンチパターンとして挙げられた親ノードだけ持つ隣接リストは、下のノードがすぐわかればいい、削除等は基本行わないという用途に適していると思います。上記にも書きましたが家計なんかでしょうか。
- 親ノードまでの階層の道のりを全て持つ経路列挙は、道のりに変なものが入らない保証はできない、階層の深さに制限がかかる、といったデメリットがあります。住所なんかはパンくずリストから一気に参照したいことが多いでしょうし適していると思います。
- 子ノードの範囲を行に入れる入れ子集合は、削除はできますが挿入がしにくいです。ツリー構造の中のツリーを検索することが容易なので、本の分類なんか適していると思います。社会学とか数学とか分類は決まっていて、数学のツリーを見たいといった操作に適していると思うからです。
- 子ノード及びその下のノード全ての関係を別テーブルに保存しておく閉包テーブルは、検索や削除、挿入容易ですし、外部キー制約が使えるので整合性も良いですが、別のテーブルを使う、行数そのものは多くなるという欠点が生じます。これは用途というよりスペックの問題な気がします。
感想
この章はだいぶアルゴリズムを感じました。
調べる中でアルゴリズムの学習らしき論文っぽいものが多くでてきて、「これがアルゴリズムを学ぶことなのか」と感じました。実際SQLアンチパターンでも最後にアカデミックな資料をおすすめされています。
階層に対するクエリの実行だとかはデータベースとビューをつなぐ役割を持つバックエンドとしては必須の知識感ありますね(まだ全くないんですが…)
ここ理解違うんじゃない?みたいなのがあれば是非お願いします!
SQLアンチパターン 第一章を読んで
アンチパターンのテーブル例
Books
book_id | book_name | tag_id |
---|---|---|
1 | SQL-anti-pattern | 1, 2 |
2 | Readable Code | 2 |
Tags
tag_id | tag_name |
---|---|
1 | SQL |
2 | O'Reilly |
このテーブルの関係では、本はタグを複数持っていてタグは複数の本につけられています。この多対多の関係を、中間の関係を示すテーブルなしで外部キー、つまりtag_idを一つのフィールドにカンマ区切りで格納しています。
これが今回のアンチパターン「信号無視」です。
デメリット
特定のタグをもつ本を探したくてもtag_id = 1 のような形で探せないため、1があるかを正規表現なんかでパターンマッチする必要があります。これだと式の組み方を間違えてしまう可能性も高く、そもそも式の組み方がMySQLやPostgreSQL等RDBMSで違ってきます。
特定の本についているタグを結合したくても、その本のtag_idフィールドでパターンマッチしなくてはならないので効率的ではありません。
集約クエリ(COUNT等)は複数の行に対して行うように設計されているので、例えば本についているタグの数を数えたい時なんかにそのまま使用ができません。
tag_idをカンマ区切りのリストの末尾に追加するのは必ずしも番号順ではないためソート順が維持できなくなるかもしれません。一つタグを削除するのも古いtag_idをまるごと捨ててと新しいtag_idをまるっと入れねばなりません。
tag_idにはVARCHARで文字列なら何でも入れられます。危険です。
リストの長さがVARCHAR(30)等で簡単に限られてしまいます。
使ってもいい場合
なにかで区切られたデータが必要で、かつその個別の要素にアクセスが不要な場合はこの方法を用いても良い場合に挙げられています。
少しそんな場合を考えたのですが、多対多でそんなケースありえるんでしようか? 思いつきませんでした。
解決方
交差テーブルを作成する。いわく中間テーブルってやつですね。
BookTaggingsテーブルとかでしょうか。
book_id | tag_id |
---|---|
1 | 1 |
1 | 2 |
2 | 2 |
こうすることでデメリットを以下のように解決できます。
1及び2
特定の本のタグを検索するときにはTagsテーブルを結合してbook_idで絞り込めばいいですし、その逆もしかりです。インデックスを有効に使えます。3
行が別になっているので、例えば本についているタグの数を数えたい時はbook_idでGROUP BYしてそのまま数えることができます。4
BookTaggingsテーブルに行を挿入、もしくは行を削除すればいいだけです。5
フィールドの内容が同一なので、BookTaggingsテーブルのbook_idカラムにはBooksテーブルのbook_idしか入れられないという外部キー制約をかけることができます。6
一つのテーブルに格納できる行数以外の制約はもうありません!
感想
中間テーブルって何で必要なんだろう?という疑問をSQLの視点から答えてくれる章でした。中間テーブル、大事です。まだ業務では作ったことないですが。
情熱プログラマーを読んで
はじめに
僕が好きなVtuberできりみんちゃんさんという方がいます。小学生です。
話し方から伝わる人柄の良さとか努力の過程を公開している感じが好きです。この方が動画でおすすめしていた本が情熱プログラマーという本です。
エンジニアとしてのあるべき姿勢とそこにたどり着く具体的なアクションについて書かれています。
ただ自分は職業エンジニアになってからまだ一週間なので、実感できない部分もありました、なので全ての感想というよりは、少ないですが自分の中で納得感の大きかったトピックの感想を書いていきます。
一番の下手くそでいよう
このトピックでおすすめされているアクションに転職等がすぐに難しい人向けの具体的な方法に、優秀そうな人のOSSを読んで周りのコードを真似しながらなにかコードを書いてみようというものがありました。
自分が一番下手くそな状況というのは、自分以外に優れている誰かがいないと成立しない状況なので、実際にOSSで公開されている自分より経験のある人が書いたコードの中でコーディングするのは、転職等よりも遥かに敷居低く自分を一番下手くそにできると思います。
自分ひとりだけで自分が一番下手くそな状況というのはありえないので、独学でもこの姿勢は大事にしていきたいですね。本を読むのもこれが理由だと思います。語学とか他の勉強でも使えそう。
現職も間違いなく自分が一番下手くそなので、恵まれていると思っています。
新しい言語を学ぶ
自分の知性に投資しようというトピックにあった具体的なアクションです。
勉強するというより面白そうだからやるっていう言語のほうが良いみたいですね。
自分はKotlinに手を出しています。アプリをつくりたいという欲求と、今のスマホのOSがAndroidなことと、きりみんちゃんが推してるのが理由です。とりあえずKotlinスタートブックを買ってみました。
Rubyぐらいしかある程度時間を投資した言語がないので、そもそも静的型付け言語みてもよくわからないという悲しさが現在あります。その解消にもなるかも。
よくスマホみたまま寝落ちして睡眠時間がよくわからない感じになるので、スマホの画面つけている時間とつけてない時間記録して寝ている時間を計測してまとめてくれるアプリ欲しい‥
あわてるな
パニック日記!この発想はなかったです。
私は自覚があるくらいにはよくパニックになると思っているのですが、振り返ると、人に評価されている、値踏みされていると感じる時が一番パニックになっていると思います。悪い評価になるのが怖いし自分をできるだけよく見せたいからです。
このパニックをより細かく噛み砕けたら良さそう。カレンダーに早速スケジュールを追加してみました。
一年後はまた別の感想になりそう
開発者として使命を持つ、とかビジネスを知る、といったトピックはまだピンと来なかったです。
自分が開発者として少し経験を積んだらまた違った感想を持つと思うので、時間立ってから読み返したいと思った一冊でした。ベターエンジニアを日々目指していきたひ。
メタプログラミングRuby 6章コードを記述するコード
eval(string)
evalメソッドに文字列を渡すと実行してくれます。
irbもevalで実行されています。
式展開してメソッドを定義したいときなんかに便利。
しかし文字列をコードとして実行してしまうということはコードインジェクションに弱くなってしまうので、evalには自分で設定するもの以外は渡さないほうが無難です。
手順4のクイズ
require ‘test/unit’ module AttrCheckedModule def attr_checked(*attribute*, &*validation*) define_method(“#{attribute}=") do |value| raise 'Invalid attribute' unless yield(value) instance_variable_set("@“{attribute}",”value) end define_method("“{attribute}") do instance_variable_get("@#{“ttribute}") ” end end end class Person extend AttrCheckedModule attr_checked :age do |v| v >= 18 end end class TestAdd < Test::Unit::TestCase def setup @bob = Person.new end def test_accepts_valid_values @bob.age = 20 assert_equal 20, @bob.age end def test_accepts_invalid_values assert_raises RuntimeError, ‘Invalid attribute’ do’ @bob.age = 17 end end end
書のようにClassクラスのインスタンスメソッドとして定義してもいいですが、少し汚し過ぎかなと思うのでextendでクラスメソッドにしてみました。
フックメソッド
クラスの継承やモジュールのインクルードのイベントが起きた時に、それをキャッチしてメソッドを実行することができます。
class String def self.inherited(*subclass*) puts “#{self}は#{subclass}に継承された" end end class Mystring < String; end # -> StringはMystringに継承された
モジュールをミックスインした時やメソッドを作ったときなんかもフックできます。
クイズで
module CheckedAttribute def self.included(klass) klass.class_eval do extend CheckedAttributeMethod end end
と書きましたが、extendはクラスメソッドなのだからそのまま実行させればよかったですね。
7章読んでのポエム
メタプログラミングというものは存在しない
全てはただのプログラミングじゃ。
まだ自分は悟りを開けてはいない。しかしこの本を読んでいなかったら読めないコードは確かに存在したので、忘れられるほどは賢くないのだろう。