ykamezの技術ブログ

日々学んだことをアウトプットしていきます。Ruby/Rails/グロース/分析/施策提案

レビューで指摘されがちな点(命名のやり方、適切なコメント、マジックナンバー、メソッドへの切り出し)

概要

同じような点をレビューで指摘されないようにするために、自分がされたレビューをメタ的に振り返ってみようと思います。 自分がレビューで指摘されがちな点を大きく以下のように分けられそうです。

  1. 単純にBugがある(間違っている)
  2. 間違ってないが、改善の余地がある
  3. 書き方
    1. 命名が適切でない
    2. コメントの適切性
    3. マジックナンバー
    4. 適切な単位でメソッドへの切り出しが行われていない
  4. パフォーマンス
  5. 書くべきところに書いていない(RailsMVC, Serviceクラスの使い分け etc.)

今回は「書き方」という観点で、自分のしがちなミス、それを避けるための考え方について考えてみます。

「書き方」が違うとは?

概要にも書きましたが、書き方が違うには色々なタイプの間違いがあります。

  1. 命名が適切でない

メソッド名、変数名、テーブル名などがミスリーディング、実態と異なっている

  1. コメントの適切性

必要な箇所にされていない or コードを読めば十分なことが説明されている

それぞれのケースについて、

  • なぜそれがよくないのか?
  • どうすれば回避できるか?

を考えていきます。

命名が適切でない

  • 変数名が実態と異なっている

以下のようなケースでは、user_id の配列を取得しているので、user_idsとすべきです。

users = User.where(type: hoge).pluck(:id)

このような書き方をしていると、usersUserモデルのArrayが入っていることを期待して、以下のようなコードを書いてしまう可能性があります。

users.pluck(:name)

サンプルだけ書いていると、するはずがないようなミスに思えますが、コードが複雑になり、他のメソッドの引数としてusersが渡され…というようなことをしているうちに、すっかりusersの実態を忘れ、ミスを引き起こしてしまいます。

  • メソッド名が過剰

以下のようなケースでは、引数としてnameが渡されており、Usernameによって絞り込んでいるのは自明なので、get_users_by(name)とすべきです。

def get_users_by_name(names)
  User.where(name: names)
end

以上のように書けば、絞り込みたい引数が増えた時でも、

def get_users_by(name, age, area)
  ...
end

といった形で拡張することが容易になります。

命名まとめ

以上の例をまとめると、「変数名は実態を必要十分に表していること」が重要だとわかります。

Rubyでは、明示的なデータ型の宣言がないので、変数名が唯一の手がかりになる

とう指摘が非常に納得できる理由でした。

コメントの適切性

  • 必要なケースにコメントされていない

過度に複雑なロジックであったり、一時的な知識に基づくもの には、適切なコメントをしてあげる必要があります。 何をしているか?というロジックの部分は読めばわかることが多いので、なんのためにしているか?などを説明してあげると良さそうです。

# NOTE:
# TODO(ykamez):
  • 過剰にコメントされている

以下のようなケースであれば、メソッド名で十分説明しているので、コメントは不要です。

# Get registered users only
def get_registered_users
  ...
end

プログラマが知るべき97のこと』で、以下のように書かれている通り、理想はメソッド名や、変数名でやりたいこと、やっていることを表現してある状態が理想なので、必要なケースと見極めをつけて、コメントをしていく必要があります。

コードに書けないことのみをコメントにする

不適切なコメントが残っていれば、誰かがコードを見る度に集中力が削がれたり、誤った情報を与えてしまうことさえあります。

どうしてもコードに語らせることが不可能な時に、語らせたかったものとコードとのギャップこそコメントに書くのです。

コードに書けないことのみをコメントにする | プログラマが知るべき97のこと

コメントまとめ

コードに書けないことのみをコメントにする

コードに書くことが不可能な時のみ、コメントをする。

マジックナンバー

以下のようなコードをみたとき、10が何を表すのか知るのは難しいと思います。 書いた本人は、表示上の問題として、10件しか必要でないと判断して、10と書いたとしても、後からみた人、そして将来の自分も何を意図して、10としたのかわからなくなる、ということが発生すると思います。

def get_hoge_users
  User.where(hoge).limit(10)
end

そのような時は、 displayable_user_size, displayable_user_limit など変数名や、定数名でその数字が何を意味しているのかを表現してあげると良いです。

メソッドへの切り出し

先ほど紹介したコードに書けないことのみをコメントにする | プログラマが知るべき97のことにも以下のようにありました。

関数が長くて分かりにくいせいでコメントが要るのなら、関数を小さく分割して、どういう関数かがすぐに分かる名前を個々につける方がいいでしょう

例えば、以下のようなコードを みたとき、一見してなんの処理を行うかわかりづらくなると思います。

def get_hoge_users
  (処理1)
  if ...
    (複数行にわたる処理2)
  end
  if ...
    (複数行にわたる処理3)
  end
end

処理1, 2, 3とやりたいことが別になってるのであれば、それぞれをメソッドに切り出すことで、コードの見通しをよくすることができます。

def get_hoge_users
  処理1method
  処理2method if ...
  処理3metohd if ...
end

達人に学ぶDB設計指南書読書メモ

概要

『達人に学ぶDB設計 徹底指南書』の読書メモです。

f:id:kysyogun:20190715184927p:plain
達人に学ぶDB設計 徹底指南書 初級者で終わりたくないあなたへ

学生インターンしていた頃に一度オススメされて読んだのですが、だいぶ時間が空いてしまったので、基本に帰るためにもう一度読んでみました。

アウトプットとして、業務でのDB設計などに活かすことを目標として、重要だと思ったことや、新しい気づきなどをまとめておきます。

1. 概論

データベース設計が重要な理由として

  1. プロセスであるソフトウェアは変化するのに対して、データは永続的であること
  2. どのようなプログラムが必要になるかは、データがどういうフォーマットで保存されているかに依存するということ

の二つが挙げられていました。 またデータは大きく分けて、三層に分けれており、

  1. 外部スキーマ = Viewの世界
  2. 概念スキーマ = テーブルの世界であり、この設計のことを論理設計という(正規化など)
  3. 内部スキーマ = ファイルの世界であり、この設計のことを物理設計という(インデックスの定義など)

そもそもの前提として、概念スキーマが必要な理由として

概念スキーマがないと、ユーザーからの見え方を変更したい場合に、内部スキーマを変更する必要が生じ、変更に対する柔軟性がなくなってしまう

ということが挙げられていました。

少し本筋とはずれますが、このようにある概念の存在意義がわからないときは、「それがないとどうなるか?」という問いについて考えてみると、その有用性が明らかになります。

2. 物理設計

データベースにおいては、データの整合性とパフォーマンスの間に強いトレードオフが存在する

このようなトレードオフの中で、この二者の平衡点を見つけていくことがデータベース設計と言えます。

具体的ない物理設計のステップ:

  1. テーブル定義
  2. インデックス定義
  3. ハードウェアのサイジング
  4. どれだけ速いか、どれだけ多いか という要件を元に決定
  5. ストレージの冗長構成決定
    • RIAD: 複数の独立したディスクを用いることによ理、信頼性と性能を向上させる技術
  6. ファイルの物理配置決定
  7. データファイル、インデックスファイル、システムファイル、一時ファイル、ログファイル
  8. 理想としては、ファイルのI/Oを分散させるために、異なるディスクに配置するのが良い

3. 論理設計と正規化

そもそもテーブルとは、共通点を持つレコードの集合であると定義されている。 ゆえに、

テーブルは同じ種類のものの集合であり、テーブル名は全て複数形または複数名詞で書ける by ジョー・セルコ

と言える。

3.3 正規化

正規化はデータの冗長性をなくしていく作業であり、更新時のデータの不整合を防止することを目的としている

また正規化を進めるほど、データの整合性は向上していくが、検索性能が劣化していくため、通常は第三正規化まで行えばOK

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

正規化の最大の欠点は、SQLパフォーマンスの劣化です。 その対処方法としては、

  1. SQLに対するチューニング
  2. 非正規化

の二つが考えられますが、著者のミックさんは

原則としては、非正規化は許さない

という立場を取っています。

基本的に正規化はテーブルを分割することであり、それゆえにデータアクセス時には、テーブル同士の結合(JOIN)が必要になるため、パフォーマンスの劣化につながりやすくなります。 もちろん結合する際に、なるべく絞り込んでから結合するなどすれば比較的パフォーマンスの劣化を防ぐことはできます。

5.2 非正規化とパフォーマンス

  1. サマリーデータの冗長性 正規化を崩し、サマリーデータをカラムとして持つことでパフォーマンスの向上は見込めるが、更新において問題が発生しうるというケース。

自分の場合、このような集計が必要な場合は、事前にBigQueryなどで集計したものを使うという形を取ることが多いのですが、データのリアルタイム性などが求められる場合はそうもいかなそうです。ただ正規化を崩し、サマリーデータをカラムに持つことは致命的な問題を引き起こしうると感じるので、 - ユーザーのリクエストの中で処理しない(非同期で処理する) - キャッシュしておく

などの方法で回避できれば、そちらを選んだ方が良さそうです。

  1. 選択条件の冗長性 検索性を高めるために、カラムに検索のための選択条件を追加し、正規化を崩すというケース(第2正規化を崩す)

5.3 冗長性とパフォーマンスのトレードオフ

あらためて非正規化のデメリットは - 検索のパフォーマンスup, 更新のパフォーマンスdown - データのリアルタイム性低下 - 更新のコストが高いために、非同期で更新されることになり、結果としてリアルタイム性が低下する

思ったこと

非正規化は、機械的に行うことができる正規化と異なり、トレードオフを考慮して実施する必要がある難しい仕事である

とある通り、筆者は非正規化を完全に否定しているわけではないと感じました。

正規化を守りつつ、パフォーマンスを向上させるためのありとあらゆる手段を使い、それでもなお要件を満たさない場合にのみ手段として考えうるのが非正規化であり、そのデメリットを明確に理解した上で、本当に必要であれば、時として、取りうる選択肢であると感じました(原則としては、正規化は高次であることが正義)

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

B-Tree Indexを作るべき列

  1. 大規模なテーブル
  2. カーディナリティの高い列に作成する
  3. WHEREの選択条件、または結合条件、ソートの条件に使用されている列
  4. indexがあれば、既にsort済みなので、sort処理が不要になる
  5. WHERE, JOIN, ORDER BY, GROUP BY

memo: データ量が少ない場合は、フルスキャンの方が早いケースがある

B-Tree Indexが効かないケース

  1. 該当の列に対して、演算を行なっている
  2. SQL関数を適用している
  3. IS NULL述語を使っている(基本的には、NULLのあたいはインデックに保持されていない)
  4. 否定形を用いている(利用しても、検索範囲が広すぎるため)
  5. これが常に正しいのかは、あまり自信がない
  6. ORを用いている→INを使えばOK
  7. 後方一致、中間一致のLIKE文を使っている
  8. 暗黙の型変換を行なっている

6.4 統計情報

DBMSがどのようなSQL文を発行するかは、データの統計情報を元に策定している。 ユーザーがデータベースに対して、SQL文を発行した場合、以下のような処理が内部的には行われている。

  1. SQL文のパース
  2. オプティマイザがカタログマネージャから統計情報を受け取り、経路を選択
  3. テーブルへのアクセス

テーブルのデータが大きく変わった場合統計情報が実際のデータの統計情報とずれ、オプティマイザが正しい経路の選択ができなくなるので、注意が必要。

また基本的には統計情報は自動で更新されるため、データ量が増えていくにつれ、実行計画が変更され、急にパフォーマンスが劣化するということが起きうる。

ここら辺の詳しいことは、以下のスライドにもまとまっています。

speakerdeck.com

実際のRailsアプリケーションで意識したいポイントなどは、以下の記事などが参考になりそうです。

morizyun.github.io

# 実行計画をみる
Model.some_query.explain
# 実際に発行されるSQLをみる
Model.some_query.to_sql

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

  1. 配列を入れる
  2. SQLのデータ型としては、配列が取り入られたが普及はしなかった
  3. 原則情報が可能な限り分割する(名前なら、性と名に、メールアドレスなら、アドレス本体と、ドメイン、など)
  4. ダブルミーニング(列の中身が途中から変わる)
  5. 単一参照テーブル OTLT
  6. type という名前を持つカラムがいる場合
  7. ポリモーフィックなテーブル
  8. テーブル分割
  9. 水平分割
  10. 垂直分割
  11. 基本的には分割せず、パフォーマンスの向上のためであれば、パーティションというデータベースの機能を使う

まとめ

全部ではないですが、読んでいて印象に思ったメモをざっとまとめてみました。 まだ業務で大規模なテーブル設計を行うことがあまりないので、自分自身が直面したことのある問題は少ないのですが、意識していないと、やってしまいかねないと感じたもの(一見良さそうに見えるもの)もいく浸かるので、注意しておきたいです。

また普段BigQueryでクエリを書くような癖で、RDBに対して、クエリを発行すると、重大なパフォーマンスの低下を招きうることもヒシヒシと感じられたので、パフォーマンスの章は定期的に読み返したいです。

7/15 - Weekly Vim(移動fとtの違い、無名レジスタ、InsertモードのままPasteする方法)

概要

Vimをちゃんと使い始めて、二ヶ月くらい経ちました。 勉強の仕方としては、

  • したい操作があった時に調べる
  • 『実践Vim』をパラパラ読んで、便利そうなものを覚えていく

という二つの方法を取っています。 学んだことを忘れないように、定期的にアウトプットしておこうと思います。

現在の位置から、)の直前まで削除したいケース

以下のコードの引数を減らす時

# before
get_hoge(foo, bar)
# after
get_hoge(foo)
  1. f, : 今いる行の次の,の上まで移動
  2. dt): 今いる行の次の,の手前まで移動して、delete

とすることで簡単に削除をすることができます。

ftは同じ行のあるcharacterまで移動するという機能自体は似ていますが、その文字の上まで移動するのか(f)、その文字の手前まで移動するのか(t)という点に違いがあり、このようなケースで使い分けると便利です。

yank → deleteした後に、yankしたものをput したいケース

様々な方法がありますが、無名レジスタを使う方法がシンプルなケースでは便利そうです。

まず前提として、Vimでは一般的なCut, Copy, PasteはDelete, Yank, Put という名前で存在します。

  • Delete = Cut (×Delete)
  • Yank = Copy
  • Put = Paste

※ CopyのCは、ChangeのCとして使われていたため、Yankが採用されたそうです。

これらは一般的なそれとほとんど同じような挙動をするのですが、特に"Delete"に関しては一般のDeleteとは少し異なる挙動をするので、注意が必要です。 例えば以下のケースで、build_hogeの第一引数を、hogeに変えたいというケースです。

# before
hoge = get_hoge
build_hoge(fuga, user)

# after
hoge = get_hoge
build_hoge(hoge, user)

直感的には、 1. yank hoge(yiw) iをつけることで単語だけをyankする 1. move to fuga(jww) 1. delete fuga(dw) 1. put hoge(Pw) Pは、その場所からPasteを行う

としたくなるところですが、そのようにすると、削除したはずので、fugaが再びpasteされます。 これは、VimにおけるDelete が、削除してその内容を無名レジスタに記録するために発生する現象です。

これを回避するためには、いくつかの方法があって、

  1. 名前付きレジスタに保存: yankする先を、名前付きレジスタにする("ayw: aという名前のレジスタにwordをyankする → "ap: aという名前のレジスタからputする)
  2. Yank レジスタを使う: yankされたものは、常に無名レジスタに保存されるのと同時に、"0という名前のレジスタに保存されるため("0pとすることで、yankしたものをputすることができる) また直感的には、putの目的語に"0を指定したくなりますが、そうすると、putが実行されてしまうので、文頭で行う必要があります。

  3. yank hoge(yiw)

  4. move to fuga(jww)
  5. delete fuga(dw)
  6. put hoge("0P)

Vim実践』p.148より

Insert モードからPasteしたいケース

以下のテキストで二行目の文末に、hoge fugaという単語を挿入したいケースを考えます。

# before
His name is hoge fuga.
And this book is written by

# after
His name is hoge fuga.
And this book is written by hoge fuga.

結論から言うと、Ctrl + r 0で、Insertモードからyankしたものをpasteすることができます。

  1. Yank words in line 1
  2. Move to line 2
  3. Move to end of line and Insert Mode
  4. Paste from yanked words

という流れで、Insertモードを出ることなく、Pasteを行うことができます。 基本的には、作業をしたら、Normalモードに戻ることが良しとされているVimですが、時には、Insertモードに残ったまま、Pasteなどを行うことで無駄なモードの切り替えを減らすこともできます。

参考

Amazon CAPTCHA

Ruby/Railsのコードレビューで指摘されやすいポイント

概要

Ruby/Railsのコードレビューで指摘されやすいポイントをまとめました。 セルフレビューでも意識できるように、定期的にまとめておこうと思います。

ActiveRecordでクエリを書く時

バッチ処理など、少し複雑なクエリをARで書く時に、パフォーマンスなどの面で指摘されがちな点です。 普段分析業務などで、BigQueryなどを使っているとパフォーマンスに対する意識が薄れやすいので気をつける必要があります。

  • 使うカラムだけ取得しているか? Model.where(...) ではなく、 Model.where(...).select(:some_column) とする。

  • Databaseへのアクセスは何回発生するか?必要最低限になっているか? 例えば、pluckをしたタイミングでto_aが走るため、本当に必要でなければ、select(ActiveRecordのメソッド)で必要なカラムの情報だけ取得して、次の条件の絞り込みに使うなど(SQLを組み立てる段階では、サブクエリとして用いられる)

  • 不必要なオブジェクトの生成をしていないか? ActiveRecordのオブジェクトは割とサイズが大きいので避ける。

  • データサイズより、ネットワーク越しのリクエスト回数(Databaseへのアクセス回数)に注意

エラー処理に関して

  • raise error がされたらプログラムが終了するので、巨大なバッチ処理などの場合などはどこかのレイヤーでrescueしてあげて、適切な処理をしてあげる必要がある

  • rescue で全てのエラーをrescueしてしまうのは危険

重要なエラーさえ握りつぶしてしまう可能性があるので、想定しているエラーのみをrescueするような形にする

  • ensure はエラー発生の有無に関わらず実行される

  • ! がつくメソッドは二つの意味を持つので注意

  • 破壊的メソッド

  • エラーが返され売る時(create!, save!とか)

その他一般的なこと

  • 文言周りに関しては基本的にはi18nを使うようにする 後々の言語対応をしやすくする

  • リンクを渡すときは基本的にヘルパーで タイポを防ぐ、後々変更しやすくする というメリットがある

  • バッチ処理を打つ際のエラー処理

ループなどでエラー処理をする際は具体的にどのオブジェクトでエラーが起きたのか?まで取得しておくと、後々デバッグがしやすい

  • マジックナンバー プログラム中の開発者が決めた数字。後から見たとき意味がわかるように、コメントしておく or 適切な命名をした定数を用いる

まとめ

指摘されたポイントを振り返ると

  • パフォーマンス
  • 後から読んだ時意味がわかるようにしておく
  • 変更しやすい, 壊れにくい
  • トラブル時の対応しやすさ

などの観点が挙げられそうです。 実際にトラブル対応した経験や、レガシーなコードで困った経験などをしたからこそ、持てる観点だと思うので、同じ思いはしない(人にもさせない)ように、気をつけていきたいですね!

はじめてLTする際に準備したこと

今週Meguro.rb #27 ではじめてLTをしてきました。

speakerdeck.com

はじめて、LTをするにあたって準備したこと・振り返りをまとめておこうと思います。

これからLTや発表などに初挑戦する人の参考になれば幸いです。

準備したこと

大まかに以下のことを行いました。

  1. 話すテーマを決める
  2. 大まかなアウトライン(目次)を作る
  3. 実際の中身を埋めていく
  4. スライドを作る
  5. 発表練習

1. 話すテーマを決める

自分はまず「8年目のProjectにRuboCopを導入した」ということを大きなテーマにおきました。 ただ「RuboCopを導入した話」ではよくある話になってしまうので、発表のユニークさを出すために、

対象: 既存のProjectにRuboCopを入れたい人 メッセージ: 既存のProjectに導入する上で大変なこと・工夫したことを伝える

ということをテーマにおきました。対象と、その人に伝えたいメッセージを置くことで、発表に一貫性がでて、話を組み立てやすくなると思います。ここを疎かにしてしまうと、話すことが増えてしまい、突っ込んだところまで話せなくなってしまうので、なるべく明確なターゲットを考えた方が良いと思います。

テーマを決める時は、対象と伝えたいメッセージを明確にする。

2. 大まかなアウトラインを作る

1 で決めたテーマをもとに、発表のアウトラインを書いていきます。 自分の場合以下のような形で書きました。

  1. 自己紹介
  2. 目次
  3. 大変なこと
    1. 一般的な話
    2. 固有の話
  4. 工夫したこと
    1. 一般的な話
    2. 固有の話
  5. 現状
  6. 残る課題
  7. まとめ・伝えたいこと

3. 実際の中身を埋めていく

2 で作ったアウトラインをもとに、中身を埋めていく作業を行いました。 スライドにそのまま使うことを想定しているので、この段階で実際の発表で話したいことを全て書き出すようにします。

4. スライドを作る

3 で作成した中身をスライドに貼りつけながら、スライドを作成していきます。 新卒研修の、プレゼンテーション研修で学んだことを意識しながら、スライド作成を行いました。

speakerdeck.com

自分が特に意識したのは、以下の二点です。

  • 一行の文字はなるべく16文字以内にする
  • 話したいことは全てスライドに書いておく

5. 発表練習

声に出して、発表してみると、話の流れの違和感などにも気がつけるので重要です! また発表時間が決められていることも多いと思うので、練習しておくと良いです。

振り返り

かなり緊張しましたが、会場の温かい雰囲気もあり、無事発表することができました。

今回は割と良い話よりの発表になってしまったので、もっと力をつけて、より技術的な発表をしていきたいと思います!

OSSのスピード感を体感した話

概要

今回Railsのバージョンアップデートをする過程で、simple_enumというGemの不備に気がつき、issueを立てたら、すぐにPRが出され解決されて、OSSのスピード感を自分の身をもって体感しました。

不具合に気がつくまでの流れ

Rails 5.1で、ActiveRecord::Dirtyのメソッドの一部が非推奨になり、Warningが出るようになりました。 取り組んでいるプロジェクトで、非推奨になっているメソッドを書き換える必要があったので、以下のような書き換えを行なっていました。

Userモデルのenumの値を保持するstete_cdというカラムに関して、メソッドのRenameを行なっていました。

state_was → state_before_last_save
state_changed? → saved_change_to_state?

すると、以下のようなエラーにあたりました。

     NoMethodError:
            undefined method `saved_change_to_state?’ for #<User:0x00005xxa>

Userモデルのstate_cdというカラムは、Railsデフォルトのenumではなく、外部のlwe/simple_enumというgemを使用していたため、RailsActiveRecordのメソッド名の変更に追従していなかったのです。

※該当のメソッドは2016年ごろに非推奨になったようです。

Deprecate the behavior of AR::Dirty inside of after_(create|update|save) callbacks by sgrif · Pull Request #25337 · rails/rails · GitHub

その場では、そこまで書き換えの数も多くなかったので、メソッド名を_cdを書き足す形で修正を行いました。

state_changed? → saved_change_to_state_cd?

自分以外にも同じようなことに引っかかる人がいると思ったので、simple_enumのissueを立てました。

github.com

すると翌々日には、

@ykamez I will try and find some time to look into this and get back to you

と返信がきて、その後すぐに変更のPRが作成されました。

そして、CIが落ちてたので、それを直したりし、少しやりとりがあった後、最終的に、

👍will merge and release this weekend. Thanks y’all

と返信がきて、無事にリリースされるようです🎉

自分自身はこの不具合を踏み、コード側を変更することで、回避したので、この変更自体からは、恩恵を受けることはできないのですが、このような形で、自分が踏んだエラーを潰していくことでOSSの世界は維持されているのだなという気づきを得ることができました。

まとめ

  • OSSの世界はスピードが速い。しっかりと問題を報告すれば、解決してくれる。もちろん、自分で解決できたら、もっとベター。

(番外編)Railsのバージョンアップデートをやって思ったこと

  • ライブラリの切り替えコストはかなり高いので気をつける必要がある。

Rails4系から、デフォルトでEnumが追加されたため、simple_enumを使うユーザーは減り、そのためにメンテが滞っていたとも考えられます。 ライブラリの切り替えコストは高いのはもちろんですが、適切なタイミングで最新のデファクトスタンダートに追従していかないと、いつの間にか負債化していく危険性もあるので、Rails側の動向などにキャッチアップする重要性を再認識しました。

今週のRailsに関する有益な情報を得るには、以下のサイトが良さそうです。

BigQueryを用いたデータ分析を業務で行うために勉強したこと

概要

データ分析を業務でするようになって、SQLを書く機会が増えたのでメモ。 実際に業務に入るまでは、先輩方がものすごいスピードでクエリを書くのを見て、ビビりまくっていましたが、一度体系的に勉強したら、かなりクエリを書く力が上がった気がするので、その過程で学んだことをまとめておきます。

勉強する前

  • 簡単な構文ならかけるけど、group_byを使ったり、集計したりすることは自信がない。
  • 業務中データを出したいと思っても、かなりコストがかかりそうなので、少し躊躇してしまう。

といったレベル感で、知識がないために、施策の仮説を立てる上で、重要なデータを出すことに関して消極的になっていました。 業務では、Railsを使っているため、ActiveRecordばかり使っており、生SQLを書く機会がほとんどなかったため、SQLなんて書かず、全てActiveRecordですませたい、くらいに思っていました。

勉強したこと

しかし、ユーザの行動データは議論のベースになるので、非常に重要なので、一度体系的にSQLを勉強することにしました。 自分はこの『SQL実践入門 ──高速でわかりやすいクエリの書き方|技術評論社

gihyo.jp

を使って勉強しました。

以下の項目は、自分が今回新しく勉強したことです。各項目に関しての説明は詳しい記事がたくさんあるので、そちらに説明は譲りますが、自分のメモも一緒に載せておきます。

  1. SQLの実行順序
  2. group_byの挙動
  3. case文の使い方
  4. window関数

SQLの実行順番

クエリを見た目通りの順番に表示されるのではないため、はじめのうちは実行のイメージがつきづらかったのですが、一度実行順序を理解すると、見通しが良くなります。 実際は、from, join, where, group_by, aggregate, having, select, order_by, limitの順番で実行されます。

SELECTをクエリを書く際には、一番最初に書きますが、実際に実行される際は最後の方に実行されます。この実行順書を理解しておくと、

  • selectで指定した別名が、group_byで参照できない理由
  • group_byで指定していない(or 集計関数でもない)列名が、selectで使えない理由 などがわかるはずです。

参考にしたリンク:

group byの挙動

以前はいまいちgroup byに関して理解できていなかったのですが、 SQL実践入門によると、

group_byはピザの分割のようなもの

という文章を読んでから、追加で調べて理解が深まりました。 以下の記事がわかりやすかったので、参考にしてください。

SQLのGROUP BY句の処理を理解する - ぱと隊長日誌

group byの挙動を理解すると、初めの頃怒られがちだった、must appear in the GROUP BY clause or be used in an aggregate functionといったエラーに苦しめられることはなくなります。

CASE文

使い方は複数あります。 特に、区分ごとの集計は便利で、強力な武器になります。

  1. フラグをつける
  2. 階級に分ける
  3. 区分ごとの集計
    • group byなどと組み合わせられることもある。

参考: CASE式で条件分岐をSQL文に任せる - Qiita

window関数

Window関数は、集約機能を省いたgroup_byと考えることができます。

実際の使い方としては以下のどちらかになります(PARTITION or ORDER)

  1. 集約関数 OVER(PARTITION BY key_name) partition_byによって、集合をカットすることができ(カット集合のことをフレームと言います。)、それに対して、集約をかけることができるのが特徴です。

  2. 集約関数 OVER(ORDER BY key_name)

group_byと違って、集計は行わないので、行数が変わらないのがポイントです。

  • count, sumなどの通常の集約関数
  • ::RANK, ROW_NUMBERといった順序関数:: を使うことができます。
MAX(sales) over (partition by user order by year rows between 1 preceeding and 1 preceeding)

Memo: MAX(sale)としているのは、一行しか実質選択していないから、その中からレコードを取得するために便宜上maxとしている(where.firstみたいな)

between 1 preceeding and 1 preceedingは「カレントコードの一行前から一行前の範囲」つまり、一行だけを見る。 →このようにSQLの行間比較にも用いることができます。

参考: window関数を使いこなす 〜分析のためのSQL〜 - Qiita

勉強したことで変わったこと

このようにSQLを一度体系的に勉強する時間をとったことで、わからないまま既存のクエリを変更しながら使うようなことがなくなり、ゼロから抵抗なく自分でクエリを書くことができ、業務の効率が非常に上がりました。

  • 一度体系的に学ぶ。
  • 自分が何を理解していないのか理解する。
  • 業務でどのようなクエリが必要になるのかイメージしながら勉強する。

ことで、理解が深まります。

上のように体系的にSQLを勉強し直したことに加えて、研修としてデータ分析の機会があったのも非常にタメになりました。

speakerdeck.com

まだ、BigQuery固有の便利な関数なども色々あるので引き続き勉強していきたいです!

  • EXTRACT
  • COUNT
  • GENERATE_DATE_ARRAY