マッスル・メモリー

筋肉エンジニアのブログ

頭のいい人が話す前に考えていること 要約・感想

要約

「知性」と「信頼」を同時のもたらす7つの黄金法則

1.とにかく反応するな

  • 人は怒っている時は頭が悪くなる。
  • 何か言いたくなった時、冷静になるために口を閉じる。

2.頭の良さは他人が決める

  • 人は頭の良い人の話を聞こうとする。
  • 相手が何を求めているのかを常に想像しながら生活する。

3.人は「ちゃんと自分のことを考えてくれる人」を信頼する

  • 中身のない賢いふりは人の心を動かさない。

4.人ではなく課題と戦え

  • 賢い人は論破しない。
  • ちゃんと考えて話すということは相手の話す内容の奥に潜む思いを想像して話す。

5.伝わらないのは話し方でなく考えが足りないせい

  • 賢いふりはバカに見られる。

6.知識は誰かのために使ってはじめて知性になる

  • 何かを話したくなった時に自分の知識の披露ではなく、相手のためになるかを考える。

7.承認欲求を満たす側に回れ

  • 相手の承認欲求を満たしてあげる。
  • 他者からの承認欲求は結果によってもたらされることを認識する。
  • 結果を出した上で人に親切にする人はカリスマと呼ばれる。

一気に頭のいい人になれる思考の深め方

1.「客観視」の思考法

話が浅い人の3つの特徴

  • 根拠が薄い
    • 人間は見たいものしか見ない(確証バイアス)
    • 結果論なら誰でも言える(後知恵バイアス)
    • 逆の意見と統計データを用いることで話を深くする。
  • 言葉に鈍感
    • 言葉の定義を明確にし思考の解像度を上げる。
  • 成り立ちを知らない
    • 成り立ちを知ることで、人と違うアイディアも深い議論も生まれる。

2.「整理」の思考法

  • 頭のいい人は物事の本質を理解しているからわかりやすく話すことができる。
  • 結論から話す。結論とは相手が一番聞きたいこと。
  • 事実と意見を分ける。

3.「傾聴」の思考法

  • 相手の言いたいことを考えながら聞く
  • 肯定も否定もしない(そうなんですね、なるほど)
  • 相手を評価しない
  • 意見を安易に言わない
  • 話が途切れたら沈黙する
  • 自分の好奇心を総動員する

4.「質問」の思考法

グーグルも使う本質をつかむ質問術

  • 導入質問1. 過去の行動についての質問
    ->「直面した困難な状況にどう対応しましたか?」
  • 導入質問2. 家庭の状況判断に基づく質問
    ->「仮に~な状況だったらどう対応しますか?」
  • 深堀質問1. 状況に関する質問
    ->「その時の状況を具体的に教えてください」
  • 深掘り質問2. 行動に関する質問
    ->「その状況のとき具体的にどう行動しましたか?」
  • 深掘り質問3. 結果に関する質問
    ->「行動の結果、どのような変化がありましたか?」

5.「言語化」の思考法

言語化を習慣にする方法

  1. ネーミングにとことんこだわる
    ->ネーミングは思考の出発点。小さなことでも名前を付けることで言語化する習慣が身に着く。
  2. 「ヤバい」「エモい」「スゴい」を使わない
    ->どんな感動もこの3つで処理できるので、これらを使わないことで脳の思考スイッチが入る。
  3. 「読書ノート」「ノウハウメモ」を作る
    ->本を読んで面白かったで終わらせず、自分の言葉でまとめる。

感想

この本は「頭のいい人は話す前に相手の欲求について考えている」ということが言いたいのではないでしょうか?

逆を言うと自分の欲求に従って

  • 自分の話したいことを
  • 自分の話したい順番で
  • 自分の使いやすい言葉で

話す際にはそれが欠けていそうです。
「相手が聞きたいことは何か」を考えるのを面倒くさがらずにそのコストを支払うことが知性に繋がるのではと思いました。

個人的にこの本で響いたのは客観視の思考法です。
自分は筋肉に対しては理解が比較的高いがそれ以外のことに関してはうっすら知っているだけということが多々あります。
自分とは逆の意見を想定する、成り立ちについて調べるは今後実践していきたいです。

あとこの本のことではありませんが、ある物事について「なぜ?」という疑問を常に持つことも解像度を上げるのに役立つと思うのでそれもやっていきたいなと思いました。

2023年を振り返る!

昨年に引き続き今年も1年の振り返りをしたいと思います!

2022年を振り返る!(筋肉8割エンジニア2割) - マッスル・メモリー

期間別振り返り

1月-3月 減量、仕事追い込み

毎年同様、昨年の12月頭から減量を開始、過去の減量の経験から比較的順調に進んでいきました。 しかし、仕事に関しては3月頭にリプレイスするプロダクトの追い込みで2月の中旬から後半にかけて少し忙しかったです。 リプレイスとはいえ細かい仕様を事前に詰めていなかったせいで後からこれも対応、あれも対応となってしまいました。

4月-5月 大会

毎年5月頭の東京ノービスを初戦としていましたが今年は4月のマッスルゲートを初戦に。 理由としては5月の大会に向けて早めに仕上げたいとプレッシャーを自分自身にかけたかったからです。 正直4月の時点はかなり甘かったので本来の目的は果たせなかったのですが、出場人数が少なかったのもありボディビル、クラシックフィジークともに優勝することができました🥇

続く東京ノービスではもう1kgほど絞り最低ラインの絞りまではいけたのかなと思います。ギリギリフリーポーズが行える6位入賞ができました。

東京ノービス

マッスルゲート

6月-8月 初海外旅行、転職決定、人生の夏休み

6月には筋肉エンジニアのメンバーと3人でタイ🇹🇭へ旅行に行きました。 自分は初海外でしたが一緒に行ってくださったお二人とも英語が堪能だったので大きなトラブルもなく楽しめました。(1人で行動していた時にトゥクトゥクのおじさんにぼったくられそうになりましたがジャパニーズ舐めんな精神で負かしてやりました👊) 元々自分は出不精なのですがたまに旅行するのは心のリフレッシュと新しい発見がありとてもありだなと思いました。

タイから帰国後、リファラルで採用面接を受けさせていただいた会社から内定をいただきました。 他は人生初の富士山にも挑戦し山頂でご来光を見てきました🌅

タイ

9月-12月 新天地へ

9月から新しい会社にお世話になりました。 前職はエンジニアは自分1人でしたが新天地ではエンジニア20人ほど在籍しており機能別にチームが分かれています。 なので基本バックエンドもフロントも両方やる感じですが自分はフロントがほぼメインのチームに配属されました。 今までReactを触っていましたがここではVue×TypeScriptをやることに。 元々バックエンドの方が得意だったりチームでの開発に不慣れなため不安でしたが周りの協力もあり意外とスムーズに仕事を進めていけています。

11月はレブルに乗ってソロキャンプ🏕️や久しぶりの技術書典📕に参加 12月はジムの仲間と記録会を行い夢だったデッドリフト200kg達成🎉 つい先日はコミケに初参戦も果たしました!

ソロキャン

冬コミ

今年良かったこと、悪かったこと

良かったこと

  • 減量で大会コンディションまではいけるようになってきた
  • 初挑戦のことが多かった
  • 毎朝トイレで技術書を読む習慣ができた
  • レーニング前のコンディショニングをやり続けられた
  • デッドリフト200kgの夢が叶った

悪かったこと

  • 減量で仕上げ切るまでいけない
  • 自分でフォームが乱れていることを自覚できずにパーソナルの間が開いてしまった(特にベンチプレス)
  • 新しい人との交流が少なかった
  • 筋肉エンジニアの会を開催をしなかった

来年に向けて

筋肉

減量

減量もう何度も行っているのでステージに上がるコンディションまではいけるようになってきました。 ただ本当に仕上げるまでが難しいので今までのやり方にプラスして何かやる必要がありそうです。 有酸素運動だったり、減量のコーチをつけるなど。 ただ来年はボディビル出場しないので大会への減量というよりピリオダイゼーション的な感じで短期間だけ行うと思います。

増量

昨年からパワーリフターの方からパーソナルを受けて、デッドリフトのフォームが固まり重量も伸びてきています。 昨年の夏150kg5,6発くらいだったのが先日175kg5発を楽にできて200kgも上げることができました。(筋トレを始めた時からスクワット、デッドリフトを200kgあげるのが夢でした。) ベンチはまだ今までのクセが抜けきらないところがありスクワットも大転子の痛みがありまだまだこれからという段階ですが確かな成長を感じています。

来年の2月にマッスルゲートのパワーリフティング大会に出場予定なのですがそこでトータル500は超えたいと思っています💪

2月マッスルゲート目標
スクワット: 190kg
ベンチプレス: 125kg
デッドリフト: 210kg

来年中の目標
スクワット: 200kg
ベンチプレス: 140kg
デッドリフト: 240kg

エンジニア

ここ数年スキルアップの勉強が少し疎かになっていましたが今年は毎朝トイレで技術書を読むことが習慣になりました。 こういう小さな積み重ねはバカにならないので続けていきたいです。

今まで突出して自分の得意な領域がなかったのと現在フロントをメインで携わっているのでこの領域を自分の得意なものにしていきたいです。 また今はpdmや先輩エンジニアから振られたタスクをやっていく感じですが設計も自分で行っていけるようになりゆくゆくはチームのマネジメントにも携われるようになりたいです。

プライベート

自分自身新たな出会いをきっかけに人生が好転してきたと思っているので関わりが少ない人、新しい人との交流をもっと増やしていきたいです。 来年は必ず筋肉エンジニアの会を開催します。

【Shell, Linux】特定のディレクトリ内のファイル名でgrepしたい

開発中不要なファイルを削除したいことがよくありますが、そのファイルが他の場所で使用されているかどうかを確認する必要があります。 特に特定のディレクトリ内で一括して調べるには手動で1ファイルずつ調査するのは非効率的です。

そこで、以下のコマンドが役立ちます。

$ ls -F dirname | grep -v / | xargs -I {} grep -rl {} | sort | uniq

コマンドの詳細

このコマンドの各部分を順に見ていきましょう。

ls -F dirname | grep -v /: 指定されたディレクトリ(dir1)内のファイルの一覧を表示します。-Fディレクトリに/が付与されるようになるのでそれをgrepで除外します。

xargs -I {}: xargsは、前のコマンドの出力を引数として次のコマンドに渡すために使用されます。-I {}プレースホルダとして{}を指定し、後続のコマンドで使用できるようにします。

grep -rl {}: grepはテキスト検索ツールで、-rオプションは再帰的に検索、-lオプションは実際の行の内容は表示せずファイルのパスを表示する役割を果たします。ここで、{}は前のコマンドから取得したファイル名と置き換えられます。

sort: sortは行をソートするためのコマンドです。これにより、grepの出力がソートされます。

uniq: uniqは連続する重複した行を1つにまとめるコマンドです。これにより、同じファイルが複数回表示されることを防ぎます。

結果として、このコマンドは指定されたディレクトリ内のファイルに対して、それぞれで検索されたテキストが存在するかを再帰的に検索し、結果をソートして一意のファイルパスのリストを表示します。

使用例

まずはテストする環境を用意

$ ls -R
dir1 dir2 dir3

./dir1:
bar.txt fuga.txt hoge.txt

./dir2:
piyo.txt

./dir3:
dir4

./dir3/dir4:
foo.txt

$ cat dir2/piyo.txt
hoge.txt
aaaaaa

$ cat dir3/dir4/foo.txt
bar.txt
bbbbb

今回dir1ディレクトリ内のbar.txt, fuga.txt, hoge.txtの3ファイルの名前が他のファイル(dir2/piyo.txt, dir3/dir4/foo.txt)で使用されているのかを調べます。

この場合2つのファイルで使用されていると確認できます。

$ ls -F dir1 | grep -v / | xargs -I {} grep -rl {} | sort | uniq
./dir2/piyo.txt
./dir3/dir4/foo.txt

念の為片方のファイルを空にすると、

$ cp /dev/null dir2/piyo.txt
$ cat dir2/piyo.txt

$ ls -F dir1 | grep -v / | xargs -I {} grep -rl {} | sort | uniq
./dir3/dir4/foo.txt

となり./dir3/dir4/foo.txtでしか使用されていないことが分かります。

注意事項

ディレクトリの階層構造を下って再帰的に調べたい場合はls -Rを使って再帰的に調査できます。

$ touch dir1/dir5/maccho.txt
$ ls dir1/dir5/
maccho.txt

$ vim dir2/piyo.txt
$ cat dir2/piyo.txt
maccho.txt

# これだとmaccho.txtを調べられない
$ ls -F dir1 | grep -v / | xargs -I {} grep -rl {} | sort | uniq
./dir3/dir4/foo.txt

# -Rをつけることで再起的に表示される
$ ls -FR dir1 | grep -v / | xargs -I {} grep -rl {} | sort | uniq
./dir2/piyo.txt
./dir3/dir4/foo.txt

【fnm, node.js】fnmを使ってnode.jsをプロジェクトごとに管理する。

Dockerを導入していないプロジェクトがある場合にプロジェクトごとにnodeのバージョンを管理したい。 そんな時はfnmを使用する。

fnmとは、Rustで構築された高速でシンプルなNode.jsバージョンマネージャ。クロスプラットフォームのサポート(macOSWindowsLinux

github.com

# fnmをインストールする
curl -fsSL https://fnm.vercel.app/install | bash

# 環境変数の設定
eval "$(fnm env)"

fnm --version
# fnm 1.33.1

# node v16.13.0のインストール
fnm install v16.13.0

# v16.13.0を使用する
fnm use v16.13.0

node -v
# v16.13.0

# nodeのバージョンを設定する
touch .node-version
node -v >> .node-version

【DB, MySQL】経路列挙モデルを用いたテーブル

特徴

リレーショナルデータベースでツリー構造のテーブルを表現するための方法の一つ。
ツリー構造とは階層構造になっているもので、リレーショナルデータベースで表そうとすると検索や更新の処理が複雑になったりパフォーマンスが悪化しやすくなる。

経路列挙モデルでは階層をノードまでのパスとして保存することでデータを表現する。(例: /1/2/3/)

+----+-----------+-------------+
| id | path      | name        |
+----+-----------+-------------+
|  1 | 1/        | Books       |
|  2 | 1/2/      | Programming |
|  3 | 1/2/3/    | Network     |
|  4 | 1/2/4/    | Database    |
|  5 | 1/2/4/5/  | MySQL       |
|  6 | 1/2/4/6/  | OracleSQL   |
| 11 | 1/2/4/11/ | PostgreSQL  |
+----+-----------+-------------+

メリット

  • パスとして保存されているので直感的で理解しやすい
  • ノード自身のレコードに親子関係が含まれているので検索しやすい
  • パスはテーブルで一意になるためユニークインデックスによる検索が可能

デメリット

  • パスに主キーを使うとパスの文字列が長くなってしまう
  • 更新が複雑になる

どのような場合に採用するか

更新が少なく大量のデータの高速な検索が必要な場合に採用すると良い。

データの操作

準備

create
database test_db;

use
test_db;

create table categories
(
    id   integer     not null,
    path text,
    name varchar(20) not null,
    primary key (id)
);

desc categories;
+-------+-------------+------+-----+---------+-------+
| Field | Type        | Null | Key | Default | Extra |
+-------+-------------+------+-----+---------+-------+
| id    | int         | NO   | PRI | NULL    |       |
| path  | text        | YES  |     | NULL    |       |
| name  | varchar(20) | NO   |     | NULL    |       |
+-------+-------------+------+-----+---------+-------+

insert into categories
  (id, path, name)
values 
  (1, '1/', 'Books'),
  (2, '1/2/', 'Programming'),
  (3, '1/2/3/', 'Network'),
  (4, '1/2/4/', 'Database'),
  (5, '1/2/4/5/', 'MySQL'),
  (6, '1/2/4/6/', 'OracleSQL'),
  (11, '1/2/4/11/', 'PostgreSQL');

select *
from categories;
+----+-----------+-------------+
| id | path      | name        |
+----+-----------+-------------+
|  1 | 1/        | Books       |
|  2 | 1/2/      | Programming |
|  3 | 1/2/3/    | Network     |
|  4 | 1/2/4/    | Database    |
|  5 | 1/2/4/5/  | MySQL       |
|  6 | 1/2/4/6/  | OracleSQL   |
| 11 | 1/2/4/11/ | PostgreSQL  |
+----+-----------+-------------+

ルートを求める

pathに主キーを使っている場合は区切り文字を削除した文字列とキーが同じになる条件で検索する。 この場合はpathから'/'を取り除いたものが主キーと一致するレコードを取得。

select *
from categories
where id = replace(path, '/', '');
+----+------+-------+
| id | path | name  |
+----+------+-------+
|  1 | 1/   | Books |
+----+------+-------+

リーフを求める

自分のpathが他で使われていないレコードがリーフに当たる。 例えば、Booksのpathは'1/'だが、これは全てのレコードで使われている。 またDatabaseの'1/2/4'もMySQLとOracleSQLで使われている。 なのでその逆の"自分のpathが使われていない"を条件する。

select *
from categories parent
where not exists(
        select *
        from categories children
        where children.path like concat(parent.path, '_%')
    );

+----+-----------+------------+
| id | path      | name       |
+----+-----------+------------+
|  3 | 1/2/3/    | Network    |
|  5 | 1/2/4/5/  | MySQL      |
|  6 | 1/2/4/6/  | OracleSQL   |
| 11 | 1/2/4/11/ | PostgreSQL |
+----+-----------+------------+

ノードの深さ求める

深さを求めるには区切り文字数を数えればよい。 区切り文字の数を数えるには、 (pathの文字列の長さ - 区切り文字を削除した文字列の長さ) 例えば、PostgreSQLの場合 1/2/4/11/の9文字から12411の5文字を引いた4文字になる。

select *,
       length(path) - length(replace(path, '/', '')) as depth
from categories;

+----+-----------+-------------+-------+
| id | path      | name        | depth |
+----+-----------+-------------+-------+
|  1 | 1/        | Books       |     1 |
|  2 | 1/2/      | Programming |     2 |
|  3 | 1/2/3/    | Network     |     3 |
|  4 | 1/2/4/    | Database    |     3 |
|  5 | 1/2/4/5/  | MySQL       |     4 |
|  6 | 1/2/4/6/  | OracleSQL   |     4 |
| 11 | 1/2/4/11/ | PostgreSQL  |     4 |
+----+-----------+-------------+-------+

ノードを追加する

リーフを追加する場合はどの親の配下につけるかさえわかればよい。 リーフではなく中間に新しいノードを追加する場合、新しく追加したノードとそのノードを除いたもので更新をかける必要がある。

name = 'Database'の下に新しいノードを追加して'Database'についていたノードを追加した子のノードにする場合、

-- 新しいノードの追加
insert into categories (id, path, name)
select 7, concat(subquery.path, '7/'), 'SQL'
from (select path from categories where name = 'Database') as subquery;

-- 新しいノードの子にあたるレコードの更新
select *
from categories;
+----+-----------+-------------+
| id | path      | name        |
+----+-----------+-------------+
|  1 | 1/        | Books       |
|  2 | 1/2/      | Programming |
|  3 | 1/2/3/    | Network     |
|  4 | 1/2/4/    | Database    |
|  5 | 1/2/4/5/  | MySQL       |
|  6 | 1/2/4/6/  | OracleSQL   |
|  7 | 1/2/4/7/  | SQL         |
| 11 | 1/2/4/11/ | PostgreSQL  |
+----+-----------+-------------+

-- 子供のパス更新

update categories
set path = replace(path, '1/2/4/', '1/2/4/7/')
where path like '1/2/4/%'
  and path <> '1/2/4/'
  and path <> '1/2/4/7/';

select *
from categories;
+----+-------------+-------------+
| id | path        | name        |
+----+-------------+-------------+
|  1 | 1/          | Books       |
|  2 | 1/2/        | Programming |
|  3 | 1/2/3/      | Network     |
|  4 | 1/2/4/      | Database    |
|  5 | 1/2/4/7/5/  | MySQL       |
|  6 | 1/2/4/7/6/  | OracleSQL   |
|  7 | 1/2/4/7/    | SQL         |
| 11 | 1/2/4/7/11/ | PostgreSQL  |
+----+-------------+-------------+

ノードを削除する

特定のノードとその配下のノードを削除する場合は特定のノードのpathをlikeの条件にして削除する。

delete
from categories
where path like '1/2/4/7/%';

select *
from categories;
+----+--------+-------------+
| id | path   | name        |
+----+--------+-------------+
|  1 | 1/     | Books       |
|  2 | 1/2/   | Programming |
|  3 | 1/2/3/ | Network     |
|  4 | 1/2/4/ | Database    |
+----+--------+-------------+

【Rails】 count, length, sizeの違いと使い分け案

count

キャッシュしない。
毎回sqlのcountを使ってカウントする。
最新の件数を取得できる。

area = Area.first
# 毎回sqlが発行される
area.sub_areas.count
# (0.4ms)  SELECT COUNT(*) FROM `sub_areas` WHERE `sub_areas`.`area_id` = 1
# => 1
area.sub_areas.count
#    (0.4ms)  SELECT COUNT(*) FROM `sub_areas` WHERE `sub_areas`.`area_id` = 1
# => 1

# 毎回sqlが発行されるので最新の件数を取得できる
SubArea.create!(area_id: 1, ja_name: 'hoge')
area.sub_areas.count
# (0.4ms)  SELECT COUNT(*) FROM `sub_areas` WHERE `sub_areas`.`area_id` = 1
# => 2
area.sub_areas.count
# (1.9ms)  SELECT COUNT(*) FROM `sub_areas` WHERE `sub_areas`.`area_id` = 1
# => 2

length

キャッシュする。
2回目以降はクエリが発行されない。
最新の件数は取得されない。

area = Area.first
area.sub_areas.length
# SubArea Load (2.8ms)  SELECT `sub_areas`.* FROM `sub_areas` WHERE `sub_areas`.`area_id` = 1
# => 1

# キャッシュするので2回目以降はクエリが発行されない
area.sub_areas.length
# => 1

SubArea.create!(area_id: 1, ja_name: 'hoge')

# countの場合は最新の件数を取得するが
area.sub_areas.count
# (0.4ms)  SELECT COUNT(*) FROM `sub_areas` WHERE `sub_areas`.`area_id` = 1
# => 2
# 
# lengthの場合は最新の件数を取得しない
area.sub_areas.length
# => 1

size

メモリにデータがあればそれを参照し、なければクエリを発行し続けて最新のデータを取得する。

area = Area.first
# クエリを発行して最新のデータを取得する
area.sub_areas.size
# (0.4ms)  SELECT COUNT(*) FROM `sub_areas` WHERE `sub_areas`.`area_id` = 1
# => 1
area.sub_areas.size
# (0.4ms)  SELECT COUNT(*) FROM `sub_areas` WHERE `sub_areas`.`area_id` = 1
# => 1

SubArea.create!(area_id: 1, ja_name: 'hoge')
area.sub_areas.count
# (1.4ms)  SELECT COUNT(*) FROM `sub_areas` WHERE `sub_areas`.`area_id` = 1
# => 2
area.sub_areas.size
# (0.4ms)  SELECT COUNT(*) FROM `sub_areas` WHERE `sub_areas`.`area_id` = 1
# => 2
area.sub_areas.size
# (0.4ms)  SELECT COUNT(*) FROM `sub_areas` WHERE `sub_areas`.`area_id` = 1
# => 2

areas = Area.all
areas.count
# (3.7ms)  SELECT COUNT(*) FROM `areas`
# => 17
areas.length
# => 17
# メモリにデータがある場合はそれを参照する
areas.size
# => 17

使い分けの案

最新のデータを取得したい時はcount。
最新のデータを取得する必要がない時、eachなどの繰り返し処理で使う時はlength。
sizeはキャッシュの状況によって発行されるクエリが変わるので個人的に使いずらい。

【Rails】 N + 1問題の解決法、preload, eager_load, includes, joinsの使い分け案

N+1問題とは

ループ処理の中でその都度クエリを発行してしまいパフォーマンスが低下してしまうこと。
解決策の1つは、Eager Loading(積極的な先読み)を使用すること。
Eager Loadingでは、関連するデータを事前にまとめてロードすることで、N回のクエリを削減する。

# 使用するモデルのアソシエーション
class Area < ApplicationRecord
  has_many :sub_areas, dependent: :destroy
end

class SubArea < ApplicationRecord
  belongs_to :area
end
# ループの回数分クエリが発行されてしまっている。
SubArea.all.each { |sub_area| sub_area.area }

# SubArea Load (3.8ms)  SELECT `sub_areas`.* FROM `sub_areas`
# Area Load (0.6ms)  SELECT `areas`.* FROM `areas` WHERE `areas`.`id` = 1 LIMIT 1
#   Area Load (0.3ms)  SELECT `areas`.* FROM `areas` WHERE `areas`.`id` = 2 LIMIT 1
#   Area Load (0.2ms)  SELECT `areas`.* FROM `areas` WHERE `areas`.`id` = 2 LIMIT 1
#   Area Load (0.2ms)  SELECT `areas`.* FROM `areas` WHERE `areas`.`id` = 2 LIMIT 1
#   Area Load (0.5ms)  SELECT `areas`.* FROM `areas` WHERE `areas`.`id` = 2 LIMIT 1

preload

アソシエーションを複数のクエリに分けてeager loadingする。
JOINしていないので、アソシエーションの値で絞り込むことはできない。

# 複数のクエリに分けてeager loadingする
SubArea.preload(:area).each { |sub_area| sub_area.area }
# SubArea Load (0.4ms)  SELECT `sub_areas`.* FROM `sub_areas`
# Area Load (0.4ms)  SELECT `areas`.* FROM `areas` WHERE `areas`.`id` IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17)

# アソシエーションの値で絞り込むことはできないのでエラーになる。
SubArea.preload(:area).where(areas: { id: [1, 2, 3] }).each { |sub_area| sub_area.area }
# SubArea Load (0.6ms)  SELECT `sub_areas`.* FROM `sub_areas` WHERE `areas`.`id` IN (1, 2, 3)
# ActiveRecord::StatementInvalid: Mysql2::Error: Unknown column 'areas.id' in 'where clause'
# Caused by Mysql2::Error: Unknown column 'areas.id' in 'where clause'

eager_load

アソシエーションを、1つのクエリ(LEFT OUTER JOIN)でまとめて取得して、eager loadingする。
JOINしているので、preloadと違って、アソシエーションの値で絞り込みができる。

# LEFT OUTER JOINしている
SubArea.eager_load(:area).each { |sub_area| sub_area.area }
# SQL (0.6ms)  SELECT `sub_areas`.`id` AS t0_r0, `sub_areas`.`area_id` AS t0_r1, `sub_areas`.`en_name` AS t0_r2, `sub_areas`.`ja_name` AS t0_r3, `sub_areas`.`created_at` AS t0_r4, `sub_areas`.`updated_at` AS t0_r5, `areas`.`id` AS t1_r0, `areas`.`en_name` AS t1_r1, `areas`.`ja_name` AS t1_r2, `areas`.`created_at` AS t1_r3, `areas`.`updated_at` AS t1_r4 FROM `sub_areas` LEFT OUTER JOIN `areas` ON `areas`.`id` = `sub_areas`.`area_id`

# LEFT OUTER JOINしているので絞り込みができる。
SubArea.eager_load(:area).where(areas: { id: [1, 2, 3] }).each { |sub_area| sub_area.area }
# SQL (1.3ms)  SELECT `sub_areas`.`id` AS t0_r0, `sub_areas`.`area_id` AS t0_r1, `sub_areas`.`en_name` AS t0_r2, `sub_areas`.`ja_name` AS t0_r3, `sub_areas`.`created_at` AS t0_r4, `sub_areas`.`updated_at` AS t0_r5, `areas`.`id` AS t1_r0, `areas`.`en_name` AS t1_r1, `areas`.`ja_name` AS t1_r2, `areas`.`created_at` AS t1_r3, `areas`.`updated_at` AS t1_r4 FROM `sub_areas` LEFT OUTER JOIN `areas` ON `areas`.`id` = `sub_areas`.`area_id` WHERE `areas`.`id` IN (1, 2, 3)

includes

preloadで事足りる場合はpreloadと同じ挙動(クエリを分けて実行) 無理な場合はeager_loadと同じ挙動(LEFT OUTER JOIN)をしてくれる。
アソシエーションが複数あるとき、個別に最適化できないという問題がある。

# preloadと同じ挙動
SubArea.includes(:area).each { |sub_area| sub_area.area }
# SubArea Load (0.7ms)  SELECT `sub_areas`.* FROM `sub_areas`
# Area Load (0.5ms)  SELECT `areas`.* FROM `areas` WHERE `areas`.`id` IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17)

# 絞り込む必要があるのでeager_loadと同じ挙動
SubArea.includes(:area).where(areas: { id: [1, 2, 3] }).each { |sub_area| sub_area.area }
# SQL (1.2ms)  SELECT `sub_areas`.`id` AS t0_r0, `sub_areas`.`area_id` AS t0_r1, `sub_areas`.`en_name` AS t0_r2, `sub_areas`.`ja_name` AS t0_r3, `sub_areas`.`created_at` AS t0_r4, `sub_areas`.`updated_at` AS t0_r5, `areas`.`id` AS t1_r0, `areas`.`en_name` AS t1_r1, `areas`.`ja_name` AS t1_r2, `areas`.`created_at` AS t1_r3, `areas`.`updated_at` AS t1_r4 FROM `sub_areas` LEFT OUTER JOIN `areas` ON `areas`.`id` = `sub_areas`.`area_id` WHERE `areas`.`id` IN (1, 2, 3)

joins

デフォルトでINNER JOINを行う。LEFT OUTER JOINを行いたい時はleft_joinsを使う。
アソシエーションを事前にまとめてロードしないのでN+1問題は起きてしまう。
アソシエーションの絞り込みはできる。

# N+1問題は起きる。
SubArea.joins(:area).where(areas: { id: [1, 2, 3] }).each { |sub_area| sub_area.area }
# SubArea Load (0.5ms)  SELECT `sub_areas`.* FROM `sub_areas` INNER JOIN `areas` ON `areas`.`id` = `sub_areas`.`area_id` WHERE `areas`.`id` IN (1, 2, 3)
# Area Load (0.3ms)  SELECT `areas`.* FROM `areas` WHERE `areas`.`id` = 1 LIMIT 1
# Area Load (0.2ms)  SELECT `areas`.* FROM `areas` WHERE `areas`.`id` = 2 LIMIT 1
# Area Load (0.2ms)  SELECT `areas`.* FROM `areas` WHERE `areas`.`id` = 2 LIMIT 1
# Area Load (0.2ms)  SELECT `areas`.* FROM `areas` WHERE `areas`.`id` = 2 LIMIT 1
# Area Load (0.2ms)  SELECT `areas`.* FROM `areas` WHERE `areas`.`id` = 2 LIMIT 1
# Area Load (0.2ms)  SELECT `areas`.* FROM `areas` WHERE `areas`.`id` = 2 LIMIT 1

それぞれのメソッドの特徴

メソッド eager loading有無 絞り込み
preload ×
eager_load
includes
joins ×

使い分けの案

  • includesメソッドは一切使わない。
  • preloadメソッドで事足りる場合は、preloadメソッドを使う。
  • 無理な場合はeager_loadメソッドを使う。
  • 絞り込みだけ必要な場合はjoinsを使う。

includesを使わないことで意図が明確になるので読み手に優しいコードになる。
preloadよりeager_loadの方が速いケースもあるがレコード数が増えることによって変わることがあるので注意。