laravel
laravel5.6

Laravelのバリデーションはexistsもinも遅かった

Laravelのバリデーションexistsルールもinも遅かった

データを用意する

  • 投入データを作成
  • 今回は数だけ欲しいので郵便番号を使います
$ curl -O http://www.post.japanpost.jp/zipcode/dl/roman/ken_all_rome.zip
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 1774k  100 1774k    0     0  4373k      0 --:--:-- --:--:-- --:--:-- 4370k
$ unzip ken_all_rome.zip
Archive:  ken_all_rome.zip
  inflating: KEN_ALL_ROME.CSV
$ cat KEN_ALL.CSV | cut -d, -f3 | sed 's/"//g' > zip_code.csv
  • テーブル定義も郵便番号程度のテーブルを用意
  • ユニーク制約をかけても11万件超あるので検証内容としては十分
mysql> show create table zip_codes\G
*************************** 1. row ***************************
       Table: zip_codes
Create Table: CREATE TABLE `zip_codes` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `code` int(11) DEFAULT NULL,
  `created_at` datetime DEFAULT NULL,
  `updated_at` datetime DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `code` (`code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
1 row in set (0.00 sec)
mysql> load data local infile '/path/to/zip_code.csv' into table zip_codes fields terminated by ',' (code);
mysql> select count(*) from zip_codes;
+----------+
| count(*) |
+----------+
|   119900 |
+----------+
1 row in set (0.08 sec)

existsルールで1000件ほどのパラメータをバリデーション

  • まずは1000件ほどをワイルドカードで指定
        $params = \App\ZipCode::take(1000)->get()->toArray();

        $bench = new \Ubench;
        $bench->start();
        $validator = \Validator::make($params, [
            '*.code' => 'required|integer|exists:zip_codes,code',
        ]);
        $validator->fails();
        $bench->end();
        \Log::info($bench->getTime());
  • 結果、1000件ほどのバリデーションに2secもかかっていしまうことがわかります
    • existsルールは要素ごとにSQLを発行しているため低速です
    • また、ワイルドカード指定していることも低速な理由の一つです
[2018-03-18 03:16:32] local.INFO: 2.292s
  • ワイルドカード指定でなく、項目ごとのバリデーションを作成します
        $params = \App\ZipCode::take(1000)->get()->toArray();

        $bench = new \Ubench;
        $bench->start();
        $rules = [];
        foreach ($params as $i => $param) {
            $rules[$i . '.code'] = 'required|integer|exists:zip_codes,code';
        }
        $validator = \Validator::make($params, $rules);
        $validator->fails();
        $bench->end();
        \Log::info($bench->getTime());
  • 結果、1sec近くバリデーションにかかる時間を削減できました
    • ワイルドカード指定は都度、要素の特定を行っているため低速です
[2018-03-18 03:20:02] local.INFO: 1.468s

in条件で1000件のバリデーション

        $params = \App\ZipCode::take(1000)->get()->toArray();
        $all_codes = implode(',', array_column($params, 'code'));

        $bench = new \Ubench;
        $bench->start();
        $rules = [];
        foreach ($params as $i => $param) {
            $rules[$i . '.code'] = 'required|integer|in:' . $all_codes;
        }
        $validator = \Validator::make($params, $rules);
        $validator->fails();
        $bench->end();
        \Log::info($bench->getTime());
  • 結果、existsよりもパフォーマンスが低下することがわかります
    • 内部でin_arrayを使っているため、要素が多いほど検索対象が配列後方にあるほど低速です
    • そもそも、すべての郵便番号を指定できてない状態でこれです
[2018-03-18 03:28:40] local.INFO: 6.898s

カスタムバリデータを作ってissetでバリデーション

  • まずはカスタムバリデータクラスを作成
<?php
namespace App\Validator;

class ZipValidator extends \Illuminate\Validation\Validator {

    public $all_codes = [];

    public function validateIsset($attribute, $value, $rule, $messages)
    {
        $property = $this->{$rule[0]};
        return isset($property[$value]);
    }
}
  • DBから取得した結果セットをarray_columnを使って郵便番号をキーにした連想配列に変換
  • 作成した連想配列をValidatorのプロパティにセット
  • セット後にバリデーションを実施
        $params = \App\ZipCode::take(1000)->get()->toArray();
        $all_code = array_column(\App\ZipCode::select('code')->get()->toArray(), 'code', 'code');

        $bench = new \Ubench;
        $bench->start();
        $rules = [];
        foreach ($params as $i => $param) {
            $rules[$i . '.code'] = 'required|integer|isset:all_codes';
        }
        $validator = \Validator::make($params, $rules);
        $validator->all_code = $all_code;
        $validator->fails();
        $bench->end();
        \Log::info($bench->getTime());
  • existsやinルールよりも高速になる
  • ただし、issetに用いる連想配列を作成するコスト次第ではexistsよりも低速になりえます
    • 今回の例では連想配列を作成するために5secほどかかっています
    • 12万件のエンティティ生成が遅い要因と思われる
[2018-03-18 04:16:37] local.INFO: 351ms