1

I'm trying to return the result of 3 tables being joined together for a user to download as CSV, and this is throwing the error:

Allowed memory size of 734003200 bytes exhausted

This is the query being run:

SELECT *
FROM `tblProgram`
JOIN `tblPlots` ON `tblPlots`.`programID`=`tblProgram`.`pkProgramID`
JOIN `tblTrees` ON `tblTrees`.`treePlotID`=`tblPlots`.`id`

The line of code causing the error is this:

$resultsALL=$this->db->query($fullQry);

Where $fullQry is the query shown above. When I comment out that single line, everything runs without the error. So I'm certain its not an infinite loop somewhere I'm missing.

I'm wondering how do I break up the query so that I can get the results without erroring out? The tables only have a relatively small amount of data in them right now and will be even larger eventually, so I don't think raising the memory size is a good option.

I'm using CodeIgniter/php/mysql. I can provide more code if need be...

Thank you for any direction you can advise!

steven7mwesigwa
  • 5,701
  • 3
  • 20
  • 34
xanabobana
  • 63
  • 2
  • 16
  • Which version of CodeIgniter are you using? – steven7mwesigwa Mar 17 '22 at 16:06
  • I don't know codeigniter but my presumption would be that buffered queries are being used. That commits full result set, use unbuffered if possible and it will only run 1 per row.. you may still run out of memory though if storing all results. Also do you need every column? replacing `*` with columns you need will cut memory consumption. – user3783243 Mar 17 '22 at 16:12
  • I'm on CodeIgniter 3.1.11 – xanabobana Mar 17 '22 at 20:17
  • I would like to provide all fields if possible- this is for a user to download available data. I actually have tried just pulling a few fields and still get the out of memory error, though. – xanabobana Mar 17 '22 at 20:18
  • @xanabobana CodeIgniter 3 uses [$this->db->query("YOUR QUERY")->unbuffered_row()](https://codeigniter.com/userguide3/database/results.html?highlight=buffer#CI_DB_result::unbuffered_row) – steven7mwesigwa Mar 17 '22 at 20:31
  • [Allowed memory size of 134217728 bytes exhausted + mysql + php](https://stackoverflow.com/questions/25585999/allowed-memory-size-of-134217728-bytes-exhausted-mysql-php) – steven7mwesigwa Mar 17 '22 at 20:53
  • @steven7mwesigwa I tried using $dbquery->fetch(PDO::FETCH_ASSOC) and unbuffered_row() but still receive the same out of memory error :( there has to be a way to do this! – xanabobana Mar 18 '22 at 16:27
  • @xanabobana Okay. Thanks for the feedback. Please give my second answer a second look. I fixed the "temporary table not persisting" issue. – steven7mwesigwa Mar 18 '22 at 17:47

2 Answers2

0

Use getUnbufferedRow() for processing large result sets.

getUnbufferedRow()

This method returns a single result row without prefetching the whole result in memory as row() does. If your query has more than one row, it returns the current row and moves the internal data pointer ahead.

$query = $db->query("YOUR QUERY");

while ($row = $query->getUnbufferedRow()) {
    echo $row->title;
    echo $row->name;
    echo $row->body;
}

For use with MySQLi you may set MySQLi’s result mode to MYSQLI_USE_RESULT for maximum memory savings. Use of this is not generally recommended but it can be beneficial in some circumstances such as writing large queries to csv. If you change the result mode be aware of the tradeoffs associated with it.

$db->resultMode = MYSQLI_USE_RESULT; // for unbuffered results

$query = $db->query("YOUR QUERY");

$file = new \CodeIgniter\Files\File(WRITEPATH.'data.csv');

$csv = $file->openFile('w');

while ($row = $query->getUnbufferedRow('array'))
{
    $csv->fputcsv($row);
}

$db->resultMode = MYSQLI_STORE_RESULT; // return to default mode

Note:

When using MYSQLI_USE_RESULT all subsequent calls on the same connection will result in error until all records have been fetched or a freeResult() call has been made. The getNumRows() method will only return the number of rows based on the current position of the data pointer. MyISAM tables will remain locked until all the records have been fetched or a freeResult() call has been made.

You can optionally pass ‘object’ (default) or ‘array’ in order to specify the returned value’s type:

$query->getUnbufferedRow();         // object
$query->getUnbufferedRow('object'); // object
$query->getUnbufferedRow('array');  // associative array

freeResult()

It frees the memory associated with the result and deletes the result resource ID. Normally PHP frees its memory automatically at the end of script execution. However, if you are running a lot of queries in a particular script you might want to free the result after each query result has been generated in order to cut down on memory consumption.

$query = $thisdb->query('SELECT title FROM my_table');

foreach ($query->getResult() as $row) {
    echo $row->title;
}

$query->freeResult(); // The $query result object will no longer be available

$query2 = $db->query('SELECT name FROM some_table');

$row = $query2->getRow();
echo $row->name;
$query2->freeResult(); // The $query2 result object will no longer be available
steven7mwesigwa
  • 5,701
  • 3
  • 20
  • 34
0

Based off of: MySQL : retrieve a large select by chunks

You may also try retrieving the data in chunks by using the LIMIT clause.

Since you're using CodeIgniter 3, here is how you can go about it.

You may need to pass a different $orderBy argument#6 to the getChunk(...) method if in case your joined tables have conflicting id column names.

I.e: $this->getChunk(..., ..., ..., 0, 2000, "tblProgram.id");

Solution:

<?php

class Csv_model extends CI_Model
{
    public function __construct()
    {
        parent::__construct();
        $this->load->database();
    }

    public function index()
    {
        $sql = <<< END
SELECT *
FROM `tblProgram`
JOIN `tblPlots` ON `tblPlots`.`programID`=`tblProgram`.`pkProgramID`
JOIN `tblTrees` ON `tblTrees`.`treePlotID`=`tblPlots`.`id`
END;

        $this->getChunk(function (array $chunk) {
            /*
             * Do something with each chunk here;
             * Do something with each chunk here;
             * log_message('error', json_encode($chunk));
             * */
        }, $this->db, $sql);
    }

    /*
     * Processes a raw SQL query result in chunks sending each chunk to the provided callback function.
     * */
    function getChunk(callable $callback, $DBContext, string $rawSQL = "SELECT 1", int $initialRowOffset = 0, int $maxRows = 2000, string $orderBy = "id")
    {
        $DBContext->query('DROP TEMPORARY TABLE IF EXISTS chunkable');
        $DBContext->query("CREATE TEMPORARY TABLE chunkable AS ( $rawSQL ORDER BY `$orderBy` )");

        do {

            $constrainedSQL = sprintf("SELECT * FROM chunkable ORDER BY `$orderBy` LIMIT %d, %d", $initialRowOffset, $maxRows);
            $queryBuilder = $DBContext->query($constrainedSQL);
            $callback($queryBuilder->result_array());
            $initialRowOffset = $initialRowOffset + $maxRows;

        } while ($queryBuilder->num_rows() === $maxRows);
    }

}

steven7mwesigwa
  • 5,701
  • 3
  • 20
  • 34
  • Thank you! I get an error that the table chunkable does not exist on the first SELECT * FROM chunkable. Why isn't the temporary table persisting long enough to run that select on it? – xanabobana Mar 18 '22 at 16:04
  • @xanabobana Sorry for the bug! I didn't run into it until today. Anyways the fix is quite simple. Check my edited answer. I simply changed `->simple_query(...)` to `->query(...)` Let me know if that works for you. Good luck! – steven7mwesigwa Mar 18 '22 at 17:42
  • thanks @steven7mwesigwa. I get the error "Access denied for user 'vmc_writer'@'%' to database 'VMC_nefin'" now when trying to create the table. I tried to grant privileges in phpmyadmin using "GRANT ALL ON *.* TO 'vmc_writer'@'%';GRANT SELECT, INSERT ON *.* TO 'vmc_writer'@'%';" but I get "Access denied for user 'vmc_admin'@'%' (using password: YES)" Does this mean I need to ask the db admin for different privileges? – xanabobana Mar 18 '22 at 20:41
  • @xanabobana Yes. Most specifically `GRANT CREATE TEMPORARY TABLES ON *.* TO 'vmc_writer'@'%';` – steven7mwesigwa Mar 19 '22 at 02:54
  • @xanabobana That command needs to be run by a MySQL/MariaDB user account that has the ability to **grant privileges**. – steven7mwesigwa Mar 19 '22 at 02:57