5

CentOS 6.4 PHP 5.3.3 MySQL 5.1.69 x86_64

mysql_stmt::fetch()

When executing fetch using a prepared statement, PHP yields error: PHP Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 4294967296 bytes).

This occurs when a variable included in a SELECT statement used to create a temporary table is unset, whether or not the variable is otherwise set in the environment before the stored procedure is called. The variable must be set within the stored procedure. When a SELECT statement is used to return data in the temporary table to PHP, and PHP uses mysql_stmt::fetch() to access the data, PHP generates the above fatal error.

MySQL Code:

DELIMITER $$
CREATE PROCEDURE test_sp()
BEGIN

    # uncomment below line, and PHP call to mysqli_stmt::fetch() works
    # SET @status = 1;

    # remove tmp table
    DROP TABLE IF EXISTS tmp_table;
    # CREATE TEMPORARY TABLE
    CREATE TEMPORARY TABLE tmp_table
        SELECT @status AS status;

    SELECT * FROM tmp_table;

END $$
DELIMITER ;

PHP Code:

// obtain MySQL login info
require_once(MYSQLOBJ);

// initialize status
$status = "";


$db = new mysqli(
    DB_HOST,
    DB_USER,
    DB_PASSWORD,
    DB_NAME
    );


$query = "CALL test_sp";

$stmt = $db->prepare($query);

$stmt->execute();

$stmt->bind_result( $status );

$stmt->store_result();

$stmt->fetch(); // PHP FATAL ERROR OCCURS HERE

$stmt->free_result();

$db->close();

print "<p>status = $status</p>\n";
Dharman
  • 30,962
  • 25
  • 85
  • 135
Michael
  • 413
  • 5
  • 13
  • 1
    looks like your php memory limit is 128MB, try increasing it to 512MB – DevZer0 Jul 07 '13 at 05:56
  • Incidentally, whilst the `test_sp` defined above is almost certainly simplified to demonstrate this issue, its behaviour could simply be replaced with something like `$query = 'SELECT @status'` (albeit that wouldn't store the result in a MySQL temporary table, which the sproc does do). – eggyal Jul 07 '13 at 07:15
  • Good idea - I removed store_result() but the error remained. – Michael Jul 07 '13 at 07:33

1 Answers1

6

You will find that this is occurring only when @status is NULL or a string.

The problem is twofold:

  1. Unlike local variables, MySQL user variables support a very limited set of datatypes:

    User variables can be assigned a value from a limited set of data types: integer, decimal, floating-point, binary or nonbinary string, or NULL value.

    The documentation fails to mention that the actual datatypes used are respectively BIGINT, DECIMAL(65,30), DOUBLE, LONGBLOB, LONGTEXT and LONGBLOB. Regarding the last one, the manual does at least explain:

    If you refer to a variable that has not been initialized, it has a value of NULL and a type of string.

    Storage of the first three of these datatypes (i.e. for integer, decimal and floating-point values) require 8, 30 and 8 bytes respectively. The other datatypes (i.e. for string and NULL values) require (up to) 4 gigabytes of storage.

  2. Since you are using a version of PHP prior to v5.4.0, the default MySQL driver is libmysql, with which only column type metadata is available from the server upon data binding—so MySQLi attempts to allocate sufficient memory to hold every possible value (even if the full buffer is not ultimately required); thus NULL- and string-valued user variables, which have a maximum possible size of 4GiB, cause PHP to exceed its default memory limit (of 128MiB since PHP v5.2.0).

Your options include:

  • Overriding the column datatype in the table definition:

    DROP TEMPORARY TABLE IF EXISTS tmp_table;
    CREATE TEMPORARY TABLE tmp_table (
      status VARCHAR(2)
    ) SELECT @status AS status;
    
  • Explicitly casting the user variable to a more specific datatype:

    DROP TEMPORARY TABLE IF EXISTS tmp_table;
    CREATE TEMPORARY TABLE tmp_table
      SELECT CAST(@status AS CHAR(2)) AS status;
    
  • Using local variables, which are declared with an explicit datatype:

    DECLARE status VARCHAR(2) DEFAULT @status;
    DROP TEMPORARY TABLE IF EXISTS tmp_table;
    CREATE TEMPORARY TABLE tmp_table
      SELECT status;
    
  • Working around the issue by calling mysqli_stmt::store_result() before mysqli_stmt::bind_result(), which causes the resultset to be stored in libmysql (outside of PHP's memory limits) and then PHP will only allocate the actual memory required to hold the record upon fetching it:

    $stmt->execute();
    $stmt->store_result();
    $stmt->bind_result( $status );
    $stmt->fetch();
    
  • Raising PHP's memory limit so that it can accomodate the allocation of 4GiB buffers (although one should be aware of the implications on hardware resources from doing so)—for example, to remove the memory constraints entirely (although be aware of potential negative side-effects from doing this, e.g. from genuine memory leaks):

    ini_set('memory_limit', '-1');
    
  • Recompiling PHP, configured to use the native mysqlnd driver (included with PHP since v5.3.0, but not configured as the default until PHP v5.4.0) instead of libmysql:

    ./configure --with-mysqli=mysqlnd
    
  • Upgrading to PHP v5.4.0 or later so that mysqlnd is used by default.

eggyal
  • 122,705
  • 18
  • 212
  • 237
  • I tested with integer (e.g. SET \@status = 1), double/float/decimal (e.g. SET \@status = 1.1), and with string (e.g. SET \@status = "Ok"). It works ok with the integer or double/float/decimal value but not when a string is used. It also fails if a NULL is used (e.g. SET \@status = NULL). However, when the variable is DECLARED, a string (or a NULL) works just fine. For example, DECLARE var_status CHAR(2) DEFAULT "Ok". Or, DECLARE var_status CHAR(2) DEFAULT NULL. Please ignore the backslash in front of the at signs. – Michael Jul 07 '13 at 07:24
  • Thanks for your help on this. Look like it is LONGBLOB or LONGTEXT as you say. I tried out the CAST method, and it works as well. – Michael Jul 07 '13 at 07:41