Query BuilderとEloquentどっちがどれくらいなぜ速いのか証明しようず！

## はじめに

通勤中駅に向かっていた時の話です。
目の前の男子高校生4人組の内一人が荷物を仲間に預けて、来た道と逆に爆走して僕の隣を風のように駆け抜けていきました。
僕が駅に着いたとき男子高校生は3人組のままで、彼の荷物だけが運ばれていくのでした...

はい。プログラムも人も走る速度は大事ですねという話ですw
ちゃんと比較していきますよ〜

## 速度比較の為にまずデータを作ろう ☆

※ 今回Laravelは使わずEloquentのみをInstallしています。
postsテーブルに10万件、commentsテーブルに100万件データを突っ込んでいます。
ちなみに使用したDBはMySQLです。

``` sql:migration.sql
CREATE DATABASE q_speed_test;

CREATE TABLE posts (id INT NOT NULL AUTO_INCREMENT, title VARCHAR(191) DEFAULT NULL, PRIMARY KEY (id));
CREATE TABLE comments (id INT NOT NULL AUTO_INCREMENT, post_id INT NOT NULL,  content VARCHAR(191) DEFAULT NULL, PRIMARY KEY (id));

CREATE INDEX ux_post_id ON comments(post_id);

delimiter //
 create procedure bulkinsert_posts()
 begin
    SET @count = 0;
    SET @limit = 100000;
    WHILE @limit > @count DO
      INSERT INTO posts (title)
      VALUES (CONCAT('小松七', @count));
      SET @count = @count + 1;
    END WHILE;
 end
//
delimiter ;
call bulkinsert_posts();
DROP PROCEDURE IF EXISTS bulkinsert_posts;

delimiter //
 create procedure bulkinsert_comments()
 begin
    SET @count = 0;
    SET @limit = 1000000;
    WHILE @limit > @count DO
      INSERT INTO comments (post_id, content)
      VALUES (1, CONCAT('小松七', @count)); -- post_id 1で固定
      SET @count = @count + 1;
    END WHILE;
 end
//
delimiter ;
call bulkinsert_comments();
DROP PROCEDURE IF EXISTS bulkinsert_comments;

```

## 速度比較をしていきます ☆

### 比較用プログラム

``` php:index.php
<?php ini_set('memory_limit', '4000M');
require_once "vendor/autoload.php";
require_once "db_conn.php";

use Illuminate\Database\Capsule\Manager as DB;
use Models\Comment;
use Models\Post;

function process_benchmark(string $conn_type, $func)
{
    $time_start = microtime(true);

    $func();

    $time_end = microtime(true);
    $process_time = $time_end - $time_start;
    echo "{$conn_type}: {$process_time}" . PHP_EOL;
}

//
echo "-- 1,000件の取得 (posts)" . PHP_EOL;

process_benchmark('SQL         ', fn() =>
    DB::select("SELECT * FROM posts WHERE id <= 1000")
);

process_benchmark('QueryBuilder', fn() =>
    DB::table('posts')->where('id', '<=', 1000)->get()
);

process_benchmark('Eloquent    ', fn() =>
    Post::where('id', '<=', 1000)->get()
);

//
echo '-- 99,999件の取得 (posts)' . PHP_EOL;

process_benchmark('SQL         ', fn() =>
    DB::select("SELECT * FROM posts WHERE id > 1")
);

process_benchmark('QueryBuilder', fn() =>
    DB::table('posts')->where('id', '>', 1)->get()
);

process_benchmark('Eloquent    ', fn() =>
    Post::where('id', '>', 1)->get()
);

//
echo PHP_EOL . '100,000件の取得 (comments)' . PHP_EOL;

process_benchmark('SQL         ', fn() =>
    DB::select("SELECT * FROM comments WHERE id <= 100000")
);

process_benchmark('QueryBuilder', fn() =>
    DB::table('comments')->where('id', '<=', 100000)->get()
);

process_benchmark('Eloquent    ', fn() =>
    Comment::where('id', '<=', 100000)->get()
);

//
echo '1,000,000件の取得 (comments)' . PHP_EOL;

process_benchmark('SQL         ', fn() =>
    DB::select("SELECT * FROM comments")
);

process_benchmark('QueryBuilder', fn() =>
    DB::table('comments')->get()
);

process_benchmark('Eloquent    ', fn() =>
    Comment::get()
);

//
echo '1,000,000件の取得 (posts join comments)' . PHP_EOL;

process_benchmark('SQL         ', fn() =>
    DB::select("SELECT * FROM posts INNER JOIN comments ON posts.id = comments.post_id")
);

process_benchmark('QueryBuilder', fn() =>
    DB::table('posts')->join('comments', 'posts.id', '=', 'comments.post_id')->get()
);

process_benchmark('Eloquent    ', fn() =>
    // queryあり/なしで意味も結果も同じです。
    Post::query()->join('comments', 'posts.id', '=', 'comments.post_id')->get()
);

//
echo '100件の取得 (comments)' . PHP_EOL;

process_benchmark('SQL         ', fn() =>
    DB::select("SELECT * FROM comments WHERE id <= 100")
);

process_benchmark('QueryBuilder', fn() =>
    DB::table('comments')->where('id', '<=', 100)->get()
);

process_benchmark('Eloquent    ', fn() =>
    Comment::where('id', '<=', 100)->get()
);
```


### 実行結果
``` text
-- 1,000件の取得 (posts)
SQL         : 0.006281852722168
QueryBuilder: 0.0042169094085693
Eloquent    : 0.007343053817749

-- 99,999件の取得 (posts)
SQL         : 0.063606023788452
QueryBuilder: 0.054216861724854
Eloquent    : 0.35599899291992

100,000件の取得 (comments)
SQL         : 0.057785034179688
QueryBuilder: 0.057592868804932
Eloquent    : 0.31189298629761

1,000,000件の取得 (comments)
SQL         : 0.63482904434204
QueryBuilder: 0.51923608779907
Eloquent    : 4.3933801651001

1,000,000件の取得 (posts join comments)
SQL         : 0.77324414253235
QueryBuilder: 0.73249197006226
Eloquent    : 3.9383239746094

100件の取得 (comments)
SQL         : 0.0004580020904541
QueryBuilder: 0.00036406517028809
Eloquent    : 0.00057601928710938
```

## Query BuilderとEloquentで速度がなぜ違うのか
Query Builderは単純なstdClass Objectしか返さないのに対してEloquentはすべてのRows内のRowに対してモデルオブジェクト生成している為です。
件数の変わらないJoin時とFetch All時で速度に差が無いことからも件数分のnewにコストがかかっていることがわかります。

Query BuilderはstdClass Objectしか返さないので、Query BuilderでFetchしてきたRowからメソッドを叩こうとすると、そもそも存在しないのでエラーになるのに対してEloquentはモデルオブジェクトが生成されてるので呼び出すことができます。

``` php
// Error
DB::table('comments')->where('id', '<=', 100)->get()[0]->hogehogeMethod();
// OK
Comment::query()->where('id', '<=', 100)->get()[0]->hogehogeMethod();
```

## Eloquentのユースケース

コネクション時の速度に差異があるわけではないのでクエリの重さは全く関係なく、取ってくるRowの数に速度が依存します。

一般的なWEB画面では50,000件を一気に表示するという要件は無いと思うのでEloquentでも問題ないという結論になります。

## Query Builderのユースケース
バルクフェッチを行うときです。
もっとわかりやすく言うと大量のユーザにメールを配信する処理では一気にユーザを引っ張ってくると思うのでそういった時です。

※ メール配信の例は会員数によってプロセス分ける可能性あり。

## 生SQL vs Query Builderについて
計測結果からわかるように速度に差異は現れておりません。
これは前述で説明した通り速度のボトルネックがResultのObjectであるため、ここに差異は出るはずがないということになります。

なので明確な違いはSQL文字列のパースをするか・しないかの違いでしか無いです。

※ 計測結果から生SQLが若干遅いように見えますが、実行順序の問題です。速度に差異はほぼありませんでした。

## あとがき
雰囲気でEloquentを使っていた自分から少しだけ卒業できそうです。
プログラムも通学前も、見落としがないか確認が大事ってことですね ☆

## 採用PR

大規模サイトのでPHP開発・二次元コンテンツに興味がある、もしくはチャレンジしてみたいという方は是非一緒に働きましょう！

https://hrmos.co/pages/eisys/jobs/0000191

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/307359/2000f749-3e82-7a47-b1c4-4444e237f79c.png" width=45%>