建立專案
建立專案。
| 1
 | laravel new eloquent-chunking
 | 
新增資料庫
為求方便,使用 SQLite 資料庫。
安裝套件
由於需要查看記憶體使用量、執行時間以及資料庫查詢,需要安裝 Telescope 套件。
新增模型
新增 Book 模型,使用 -a 參數同時新增遷移檔、工廠和控制器。
| 1
 | php artisan make:model Book -a
 | 
在遷移檔新增欄位。
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 
 | use Illuminate\Database\Migrations\Migration;use Illuminate\Database\Schema\Blueprint;
 use Illuminate\Support\Facades\Schema;
 
 class CreateBooksTable extends Migration
 {
 
 
 
 
 
 public function up()
 {
 Schema::create('books', function (Blueprint $table) {
 $table->bigIncrements('id');
 $table->string('title');
 $table->string('author');
 $table->timestamps();
 });
 }
 
 
 
 
 
 
 public function down()
 {
 Schema::dropIfExists('books');
 }
 }
 
 | 
資料填充
修改 database/factories/BookFactory.php 檔,使工廠產生不重複的資料。
| 12
 3
 4
 5
 6
 7
 8
 9
 
 | use App\Book;use Faker\Generator as Faker;
 
 $factory->define(Book::class, function (Faker $faker) {
 return [
 'title' => sprintf('%s (%u)', $faker->sentence(), rand()),
 'author' => sprintf('%s (%u)', $faker->name(), rand()),
 ];
 });
 
 | 
新增 BooksTableSeeder 資料填充。
| 1
 | php artisan make:seed BooksTableSeeder
 | 
分批插入共 10 萬筆資料。
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 
 | use App\Book;use Illuminate\Database\Seeder;
 
 class BooksTableSeeder extends Seeder
 {
 
 
 
 
 
 public function run()
 {
 for ($i = 1; $i <= 100; $i++) {
 $books = factory(Book::class)->times(1000)->make()->toArray();
 
 Book::insert($books);
 }
 }
 }
 
 | 
在 database/seeds/DatabaseSeeder.php 檔註冊資料填充。
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 
 | use Illuminate\Database\Seeder;
 class DatabaseSeeder extends Seeder
 {
 
 
 
 
 
 public function run()
 {
 $this->call(BooksTableSeeder::class);
 }
 }
 
 | 
執行資料填充。
| 1
 | php artisan migrate --seed
 | 
新增路由
在 routes/api.php 檔新增路由。
| 1
 | Route::get('books', 'BookController@index');
 | 
情境
從 10 萬筆資料中找到一筆已存在的資料:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 
 | $max = 100000;
 
 $existingBook = Book::find($max);
 
 
 $books = Book::where('id', '<=', $max)->get();
 
 
 $isExisting = $books->some(function ($book) use (&$existingBook) {
 return $book->title === $existingBook->title;
 });
 
 dump($isExisting);
 
 | 
結果:
實測 5 次的效能分別如下:
| Duration | Memory usage | queries | 
| 1037 ms | 119.9 MB | 2 | 
| 1040 ms | 117.9 MB | 2 | 
| 1092 ms | 113.9 MB | 2 | 
| 1151 ms | 111.9 MB | 2 | 
| 1084 ms | 103.9 MB | 2 | 
優化
分塊
在處理大型結果集時,為了節省記憶體使用量,可以使用 chunk() 方法來查詢 Eloquent 模型中的「分塊」,將它們提供給指定的閉包處理。
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 
 | $max = 100000;
 
 $existingBook = Book::find($max);
 
 $isExisting = false;
 
 
 Book::where('id', '<=', $max)->chunk($max / 10, function ($books) use (&$existingBook, &$isExisting) {
 // 檢驗是否已存在
 $isExisting = $books->some(function ($book) use (&$existingBook) {
 return $book->title === $existingBook->title;
 });
 
 
 return ! $isExisting;
 });
 
 dump($isExisting);
 
 | 
結果:
實測 5 次的效能分別如下:
| Duration | Memory usage | queries | 
| 1053 ms | 18 MB | 11 | 
| 1112 ms | 18 MB | 11 | 
| 1145 ms | 18 MB | 11 | 
| 1114 ms | 18 MB | 11 | 
| 1120 ms | 18 MB | 11 | 
使用 chunkById() 方法可以更快速地查詢資料庫,但僅限用於主鍵為 AUTO_INCREMENT 的資料表,並且在有 join 的情況下可能不能使用。
游標
使用 cursor() 方法可以藉由游標遍歷資料庫,並且只會執行一次的查詢。處理大量的數據時,可以減少記憶體使用量。
| 12
 3
 4
 5
 6
 7
 8
 9
 
 | $max = 100000;
 $existingBook = Book::find($max);
 
 $isExisting = Book::where('id', '<=', $max)->cursor()->some(function ($book) use (&$existingBook) {
 return $book->title === $existingBook->title;
 });
 
 dump($isExisting);
 
 | 
結果:
實測 5 次的效能分別如下:
| Duration | Memory usage | queries | 
| 2168 ms | 4 MB | 2 | 
| 2166 ms | 4 MB | 2 | 
| 2214 ms | 4 MB | 2 | 
| 2178 ms | 4 MB | 2 | 
| 2165 ms | 4 MB | 2 | 
其他
如果只是要避免重複插入資料而產生錯誤,可以考慮使用 insertOrIgnore() 方法,但是這個方法只會檢查主鍵而已。
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 
 | $max = 100000;
 Book::insertOrIgnore([
 [
 "id" => $max,
 "title" => "New Book",
 "author" => "Memo Chou",
 ],
 ]);
 
 dump(Book::count());
 
 | 
結果:
程式碼