Laravelのpaginatorで件数カウントとデータの取得が同時に行われるのを防ぐ

### 何がしたいか
ページが重いので軽くしたい。paginate使ってるのでデータ取得と件数カウントで同時に2発クエリが走っていて、データ取得が遅いクエリであればある程鬱陶しい。というかそもそもなんでpaginate使ってるの・・・？これページングはしないよ・・・？というのはあるんですけど、まぁそこ愚痴っても仕方ない。

### 前提
Laravel 6　古いです。変えたいです。
DBはmysql 8.0（昔なのでパッチバージョンは忘れました）

### どうするか
まぁ大体これが全てなんですが

https://zenn.dev/third_tech/articles/3f24490d062d56

Eloquentの方のpaginateがこう
```PHP
/**
 * Paginate the given query.
 *
 * @param  int|null  $perPage
 * @param  array  $columns
 * @param  string  $pageName
 * @param  int|null  $page
 * @return \Illuminate\Contracts\Pagination\LengthAwarePaginator
 *
 * @throws \InvalidArgumentException
 */
public function paginate($perPage = null, $columns = ['*'], $pageName = 'page', $page = null)
{
    $page = $page ?: Paginator::resolveCurrentPage($pageName);

    $perPage = $perPage ?: $this->model->getPerPage();

    $results = ($total = $this->toBase()->getCountForPagination())
                                ? $this->forPage($page, $perPage)->get($columns)
                                : $this->model->newCollection();

    return $this->paginator($results, $total, $perPage, $page, [
        'path' => Paginator::resolveCurrentPath(),
        'pageName' => $pageName,
    ]);
}
```

そしてQuery Builderの方のpaginateがこう
```PHP
/**
 * Paginate the given query into a simple paginator.
 *
 * @param  int  $perPage
 * @param  array  $columns
 * @param  string  $pageName
 * @param  int|null  $page
 * @return \Illuminate\Contracts\Pagination\LengthAwarePaginator
 */
public function paginate($perPage = 15, $columns = ['*'], $pageName = 'page', $page = null)
{
    $page = $page ?: Paginator::resolveCurrentPage($pageName);

    $total = $this->getCountForPagination();

    $results = $total ? $this->forPage($page, $perPage)->get($columns) : collect();

    return $this->paginator($results, $total, $perPage, $page, [
        'path' => Paginator::resolveCurrentPath(),
        'pageName' => $pageName,
    ]);
}
```

要は内部でforPageで部分的にgetしてその結果をセットしたLengthAwarePaginatorを返せばええんやろ、ってことで、こんなのを書きました。~~$builder参照渡しにしてるのは「重いんじゃね？」というイメージ先行です。激重クエリ書いてなければ別段必要じゃない気がするのでその辺はお好みで。~~
（2024/08/16追記：PHPはそもそもclassだとコピーしないよってコメント貰ったんで直しました）

```PHP
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Pagination\Paginator;
use Illuminate\Database\Query\Builder as QueryBuilder;
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;

class CustomPaginator
{
    public static function withoutCountQueryPaginate(
        // &は要らない
        // QueryBuilder|EloquentBuilder &$builder,
        QueryBuilder|EloquentBuilder $builder,
        $perPage = 15,
        $columns = ['*'],
        $pageName = 'page',
        $page = null
    ){
        $page = $page ?: Paginator::resolveCurrentPage($pageName);
        $results = $builder->forPage($page, $perPage)->get($columns);
        $total = 0;

        if($results->isNotEmpty()){
            $total = $results->first()->aggregate;
        }
        return self::paginator($results, $total, $perPage, $page, [
            'path' => Paginator::resolveCurrentPath(),
            'pageName' => $pageName,
        ]);
    }

    /**
     * Create a new length-aware paginator instance.
     *
     * @param  \Illuminate\Support\Collection  $items
     * @param  int  $total
     * @param  int  $perPage
     * @param  int  $currentPage
     * @param  array  $options
     * @return \Illuminate\Pagination\LengthAwarePaginator
     */
    protected static function paginator($items, $total, $perPage, $currentPage, $options)
    {
        return Container::getInstance()->makeWith(LengthAwarePaginator::class, compact(
            'items', 'total', 'perPage', 'currentPage', 'options'
        ));
    }
}
```

### 注意
元記事にもあったけど、distinctやunion使ってると全体をサブクエリ化してカウント、みたいな周りくどい方法にならざるを得ず、場合によっては激遅になるので注意。

そもそもpaginateが遅いのもmysql8.0だったので
> どうやら、 MySQL 8.0 ではテーブル全件に対するSELECT COUNT(*)の処理で、バッファプールがいっぱいになり古いデータページを追い出す必要がある状況下の処理に問題を抱えている様子 です。

https://zenn.dev/hmatsu47/articles/mysql80-count-slowdown

という問題だった可能性大。
以下の記事の様な対処も可能かもですが、indexを動的に指定しなければいけないとDB::rawとか使わざるを得ずLaravel、というかWebアプリケーションフレームワーク使っていると難しい気もする。

https://ca-srg.dev/8185854ff1dc44a28ffe1f8cfb15dd14

リプレースできるならとっとと変えちゃうと良いような気もします。

https://ca-srg.dev/dba9a0f2aa9049259a9610274b258bf2