framework: Query builder memory leak on large insert
- Laravel Version: 5.7.19
- PHP Version: 7.2.5
- Database Driver & Version: MySQL 5.7.22
When inserting many rows using the query builder, memory usage continually increases. In contrast, executing the same code with the pdo connection it does not.
It seems like some data is getting cached somewhere and Laravel is doing something with that cached data on teardown. When creating large tables with hundreds of thousands of rows this behavior is not desirable.
Steps To Reproduce:
Create test table
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
class CreateTestTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('test_table', function (Blueprint $table) {
$table->increments('id');
$table->string('column0', 50)->nullable();
$table->string('column1', 50)->nullable();
$table->string('column2', 50)->nullable();
$table->string('column3', 50)->nullable();
$table->string('column4', 50)->nullable();
$table->string('column5', 50)->nullable();
$table->string('column6', 50)->nullable();
$table->string('column7', 50)->nullable();
$table->string('column8', 50)->nullable();
$table->string('column9', 50)->nullable();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('test_table');
}
}
Create test command to insert rows
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use DB;
class TestCode extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'test:code';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Easy way to test code.';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return null
*/
public function handle()
{
// Memory consumption insert test
$pdo = DB::getPdo();
$rowValues = [];
$valuesToInsert = [];
$placeHolderString = '(' . implode(', ', array_fill(0, 10, '?')) . ')';
$placeHolders = [];
for ($i = 0; $i < 10; $i++) {
$rowValues['column' . $i] = uniqid() . uniqid();
}
$columns = array_keys($rowValues);
$counter = 0;
while ($counter < 100000) {
foreach ($rowValues as $value) {
$valuesToInsert[] = $value;
}
$placeHolders[] = $placeHolderString;
if ($counter > 0 && $counter % 1000 === 0) {
$query = "INSERT INTO `test_table` (" . implode(', ', $columns) . ") VALUES " . implode(', ', $placeHolders);
// Using query builder, memory consumption continually rises
DB::statement($query, $valuesToInsert);
// Using the pdo connection, memory consumption levels out and stays constant
// $pdo->prepare($query)
// ->execute($valuesToInsert);
$valuesToInsert = [];
$placeHolders = [];
$this->info('Memory usage: ' . memory_get_usage(true) );
}
$counter++;
}
}
}
After executing the test:code command the output in the shell looks like this:
Memory usage: 20971520
Memory usage: 20971520
Memory usage: 20971520
Memory usage: 23068672
Memory usage: 23068672
Memory usage: 23068672
Memory usage: 25165824
Memory usage: 25165824
Memory usage: 25165824
Memory usage: 27262976
Memory usage: 27262976
Memory usage: 27262976
Memory usage: 29360128
Memory usage: 29360128
Memory usage: 29360128
Memory usage: 31457280
Memory usage: 31457280
Memory usage: 31457280
Memory usage: 33554432
Memory usage: 33554432
Memory usage: 33554432
Memory usage: 35651584
...
When bypassing the query builder and using the pdo connection the output looks like this:
Memory usage: 18874368
Memory usage: 18874368
Memory usage: 18874368
Memory usage: 18874368
Memory usage: 18874368
Memory usage: 20971520
Memory usage: 20971520
Memory usage: 20971520
Memory usage: 20971520
Memory usage: 20971520
Memory usage: 20971520
Memory usage: 20971520
Memory usage: 20971520
Memory usage: 20971520
Memory usage: 20971520
Memory usage: 20971520
Memory usage: 20971520
Memory usage: 20971520
Memory usage: 20971520
Memory usage: 20971520
Memory usage: 20971520
Memory usage: 20971520
Memory usage: 20971520
...
FYI The same behavior occurs when using DB::table()->insert()
to insert the data instead of DB::statement()
.
About this issue
- Original URL
- State: closed
- Created 5 years ago
- Comments: 20 (8 by maintainers)
For anyone landing on this page after a Google search, I had the same symptoms in a data import script. It would work when batching 100 rows per insert but would fail when batching 1000 rows per insert. I profiled my code as shown above and found:
I traced through the
Connection
class and noted that even with logging disabled it was still raising aQueryExecuted
event. I updated the code to callDB::connection()->unsetEventDispatcher();
which disabled the events, and since then the code runs without leaking over the full data set (2.64m records).I hope that helps someone.
PHP 7.4.1 MariaDB 10.4.11 Laravel 6.6.0
I encountered this while seeding the database in Laravel 5.8.19 A discussion in Telescope (https://github.com/laravel/telescope/issues/173) hinted at this approach, which seemed to work form me.
DB::connection()->unsetEventDispatcher();
finally solved this for me when inserting 7+ million records (chunked to 25k records per batch) with ~450 mb memory availableAlso forcing GC of current batch e.g.:
Also dont forget
DB::disableQueryLog();
I solved the problem by disabling the listener from package
facade/ignition
, @see here.DB::connection()->unsetEventDispatcher(); Worked for me as well
I have the same error as yours which throwing through the Connection class. Everytime i running my insert script for large amount of rows into database, i always facing the same error at some points. So below i tried to logging the memory usage from both scenario.
The first scenario without unsetEventDispatcher(); ,
at processing number 60 i got FatalError caused by memory exhausted (memory_limit set is at 1GB)
The second scenario with unsetEventDispatcher(); setting at the beginning of the scripts.
as you can see from the stats, max memory usage from the script is not sky-rocketed like the first one and seems like itโs added bit by bit as the real usage of the script. ๐ so i can confirm, by setting the unsetEventDispatcher in the connection, and of course by disabled the queryLog really helps to avoid FatalError which causing by memory leaked.
Thanks @KennedyTedesco for testing it out. After reading your response I thought it might have something to do with telescope and after I added
test:code
to the ignore_commands in the telescope config the memory leak went away.