Skip to content

Bug: getCompiledSelect() does not clear $binds, so a reused builder gets slower on every call #10691

Closed

Description

@Guferos

PHP Version

8.5

CodeIgniter4 Version

4.7.3

CodeIgniter4 Installation Method

Composer (using codeigniter4/appstarter)

Which operating systems have you tested for this bug?

Linux

Which server did you use?

cli

Environment

production

Database

MySQL 8.4 (MySQLi driver)

What happened?

BaseBuilder::getCompiledSelect($reset = true) resets the select parts via resetSelect(), but it never clears $this->binds. Only get() does that (// Clear our binds so we don't eat up memory).

When the same builder instance is reused, for example the shared builder returned by Model::builder(), every call adds new binds (queue.1, queue.2, …) and keeps all the old ones. compileFinalQuery() → Query::compileNamedBinds() then escapes every stored bind and adds it to the strtr() map on each call, even though only the newest placeholders appear in the SQL. The SQL stays correct, but the cost of each call grows linearly with the number of earlier calls, so a loop of calls slows down quadratically.

We found this in a long-running codeigniter4/queue worker, which calls getCompiledSelect() on the model's shared builder on every poll. After about 28 hours (about 21k polls) the idle worker was using 20–60% of a CPU core while the queue was empty, and each poll spent about 1 second compiling the query.

Steps to Reproduce

$builder = db_connect()->table('queue_jobs');
$binds   = new ReflectionProperty(\CodeIgniter\Database\BaseBuilder::class, 'binds');

for ($i = 1; $i <= 3; $i++) {
    $builder->where('queue', 'emails')->where('status', 0)->limit(1)->getCompiledSelect();
    echo json_encode(array_keys($binds->getValue($builder))), PHP_EOL;
}

Output:

["queue","status"]
["queue","status","queue.1","status.1"]
["queue","status","queue.1","status.1","queue.2","status.2"]

The cost per call grows with the number of earlier calls on the same builder:

Earlier getCompiledSelect() calls on the builder Time per call
1,000 4.0 ms
10,000 29.5 ms

Expected Output

With $reset = true, getCompiledSelect() should leave the builder in the same state as get() does, including clearing $this->binds. After each call above, only ["queue","status"] should remain, and the time per call should stay flat.

Anything else?

A possible fix in BaseBuilder::getCompiledSelect(): compile the final query first, then reset the select parts and clear the binds when $reset is true:

public function getCompiledSelect(bool $reset = true): string
{
    $select = $this->compileFinalQuery($this->compileSelect());

    if ($reset) {
        $this->resetSelect();
        $this->binds = [];
    }

    return $select;
}

The binds have to be cleared after compileFinalQuery(), because it reads them. getCompiledInsert(), getCompiledUpdate() and getCompiledDelete() may be worth checking for the same pattern.

Activity

  1. added
    bugVerified issues on the current code behavior or pull requests that will fix them
    on Oct 30, 2026
  2. paulbalandan commented on Oct 31, 2026

    @paulbalandan
    Member

    Fixed in #10692

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugVerified issues on the current code behavior or pull requests that will fix them

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions