Почему таблица SELECT с 200 000 записей использует слишком много памяти (+ 2 ГБ)? - PullRequest
1 голос
/ 26 октября 2019

Я выбираю данные из VIEW в Oracle, которые в реальном размере составляют около 50 МБ (61 столбец и 263 000 строк). У меня только один столбец с длиной данных 4000, а все остальные - до 100.

Когда я выбираю с помощью Laravel (разбитого на пакеты по 10 000 записей), он занимает около 2,5 ГБ в памяти.

Я провел некоторый поиск и попытался отключить запросы журнала, используя DB :: disableQueryLog, $ connection-> disableQueryLog (), включил вызов gc_collect_cycles после каждого SELECT и сбросить переменную результатов - без какого-либо эффекта.


<?php

use Yajra\Oci8\Connectors\OracleConnector;
use Yajra\Oci8\Oci8Connection;

/**
 * @return Oci8Connection
 * @throws \Exception
 */
private function getOracleConnection()
{
    $config = [
        'driver' => 'oracle',
        'host' => 'host',
        'database' => 'database',
        'port' => 'port',
        'username' => 'user',
        'password' => 'password',
        'charset' => 'charset',
        'schema' => 'schema',
        'options' => [
            \PDO::ATTR_PERSISTENT => true
        ],
    ];
    $connector = new OracleConnector();
    $connection = $connector->connect($config);

    $db = new Oci8Connection($connection, $database);

    return $db;
}

protected function loadSourceSystemData(): Collection
{
    $connection = $this->getOracleConnection();

    DB::disableQueryLog();
    $connection->disableQueryLog();

    $package_size = 10000;

    $return = new Collection();
    $offset = 0;

    while(true) {
        $records = $connection->query()
            ->from('ZVPCP003')
            ->take($package_size)
            ->offset($offset)
            ->get();

        if(empty($records)) break;

        $return = $return->merge($records);
        unset($records);

        gc_collect_cycles();
        $offset += $package_size;
    }

    return $return;
}

Я ожидаю использовать как минимум менее 1 ГБ, это очень много, но это приемлемо.

Обновление: я измерил реальное использование памяти:

Rows: 10000 | Mem: 124 MB  
Rows: 20000 | Mem: 241 MB  
Rows: 30000 | Mem: 357 MB  
Rows: 40000 | Mem: 474 MB  
Rows: 50000 | Mem: 590 MB  
Rows: 60000 | Mem: 707 MB  
Rows: 70000 | Mem: 825 MB  
Rows: 80000 | Mem: 941 MB  
Rows: 90000 | Mem: 1058 MB  
Rows: 100000 | Mem: 1174 MB  
Rows: 110000 | Mem: 1290 MB  
Rows: 120000 | Mem: 1407 MB  
Rows: 130000 | Mem: 1523 MB  
Rows: 140000 | Mem: 1644 MB  
Rows: 150000 | Mem: 1760 MB  
Rows: 160000 | Mem: 1876 MB  
Rows: 170000 | Mem: 1993 MB  
Rows: 180000 | Mem: 2109 MB  
Rows: 190000 | Mem: 2226 MB  
Rows: 200000 | Mem: 2342 MB  
Rows: 210000 | Mem: 2459 MB  
Rows: 220000 | Mem: 2575 MB  
Rows: 230000 | Mem: 2691 MB  
Rows: 240000 | Mem: 2808 MB  
Rows: 250000 | Mem: 2924 MB  
Rows: 260000 | Mem: 3041 MB  
Rows: 263152 | Mem: 3087 MB  

1 Ответ

1 голос
/ 27 октября 2019

Может быть, преобразовав loadSourceSystemData в генератор и затем обработав данные в чанках, таким образом, вы будете загружать не более 10000 строк одновременно, поскольку они не собираются в одну большую коллекцию, их можно автоматически освобождать.

<?php

function loadSourceSystemData(): iterable
{
    $connection = $this->getOracleConnection();
    DB::disableQueryLog();
    $connection->disableQueryLog();
    $package_size = 10000;
    $offset = 0;
    do {
        $records = $connection->query()->from('ZVPCP003')
                ->take($package_size)
                ->offset($offset)
                ->get();
        $offset += $package_size;
        yield collect($records);
    } while(!empty($records));
}

foreach($this->loadSourceSystemData() as $collection) {
    foreach($collection as $row) {
        // Process row here
    }
}

Обновление

Я пытался загрузить данные из CSV-файла, чтобы проверить накладные расходы, и при использовании массивов занимает примерно на 70% больше памяти, чем при использовании объектов.

Для 500000 строк, таких как «P80, массив A142900,2012,6,35» занял около 213 МБ, а массив объектов - 136 МБ.

class Item {
    public $a;
    public $b;
    public $c;
    public $d;
    public $e;
}

if (($handle = fopen("data.csv", "r")) !== FALSE) {
    $row = 0;
    $list = [];
    while (($data = fgetcsv($handle, 1000, ",")) !== FALSE) {
        $item = new Item();
        $item->a = $data[0];
        $item->b = $data[1];
        $item->c = $data[2];
        $item->d = $data[3];
        $item->e = $data[4];
        $list[] = $item;
        //$list[] = $data;
    }
    fclose($handle);
    $m = memory_get_usage(true) / 1000 / 1000;
    echo "Rows ", count($list), " Memory ", $m, "MB \n";
}

Обновление 2

Подробнеепамять может быть сохранена, если данные преобразуются в конкретные типы, например. int, и если некоторые столбцы имеют много повторяющихся значений, то можно использовать кэширование.

class Name {
    public $name;
    public function __construct($name)
    {
        $this->name = $name;
    }
}

class NameCache {
    private $cache = [];
    public function getName(string $name) {

        if (isset($this->cache[$name])) {
            return $this->cache[$name];
        }
        $item = new Name($name);
        $this->cache[$name] = $item;
        return $item;
    }
}

$nameCache = new NameCache();
$item = new Item();
$item->a = $nameCache->getName($data[0]);
$item->b = $nameCache->getName($data[1]);
$item->c = (int)$data[2];
$item->d = (int)$data[3];
$item->e = (int)$data[4];

При этом объем памяти был уменьшен с 136 МБ до 75 МБ.

...