Skip to content

refactor(database): optimize _processForeignKeys() string allocations and loop invariants#10351

Open
gr8man wants to merge 2 commits into
codeigniter4:developfrom
gr8man:perf/optimize-forge-foreignkeys
Open

refactor(database): optimize _processForeignKeys() string allocations and loop invariants#10351
gr8man wants to merge 2 commits into
codeigniter4:developfrom
gr8man:perf/optimize-forge-foreignkeys

Conversation

@gr8man

@gr8man gr8man commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Description
Refactors _processForeignKeys() in Forge.php to optimize performance and reduce memory allocations:

  • Calculates invariant SQL fragments (e.g., driver checks, fkSuffix, prefixSql) once outside the main loop.
  • Caches the escaped table identifier, significantly reducing repeated calls to $this->db->escapeIdentifiers().
  • Builds the SQL constraint string locally in memory using a single sprintf() and string appending before performing a single concatenation to the $sqls array. This saves multiple intermediate array lookup and string allocations per foreign key.

Checklist:

  • Securely signed commits
  • Component(s) with PHPDoc blocks, only if necessary or adds value (without duplication)
  • Unit testing, with >80% coverage
  • User guide updated
  • Conforms to style guide

Refactors _processForeignKeys() to:
- calculate invariant SQL fragments outside the loop
- cache escaped table identifier
- build SQL string locally before concatenating to the array, saving intermediate string allocations

@michalsn michalsn left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR. The refactor appears behaviorally equivalent, but I'm not convinced the small performance gain justifies the added complexity without benchmark results.

The old code already used one sprintf(), and identifier escaping is reduced only for ALTER queries with multiple foreign keys. The common no-FK path also now performs unnecessary setup.

Could you add benchmarks, restore a fast return for no foreign keys, remove the unrelated formatting changes, and translate/remove the Polish comment?

- Restore fast return without foreign keys\n- Remove unrelated formatting changes\n- Remove non-English comment
@gr8man gr8man force-pushed the perf/optimize-forge-foreignkeys branch from 433f70d to d007add Compare June 30, 2026 18:50
@gr8man

gr8man commented Jun 30, 2026

Copy link
Copy Markdown
Contributor Author

php benchmark_fk.php
--- BENCHMARK ---
Current Branch: perf/optimize-forge-foreignkeys
Iterations/Keys: 500,000

[1] Time taken for 500000 calls WITHOUT foreign keys: 0.1973 seconds
[2] Time processing 500000 added foreign keys (1 call): 4.8473 seconds

php benchmark_fk.php
--- BENCHMARK ---
Current Branch: develop
Iterations/Keys: 500,000

[1] Time taken for 500000 calls WITHOUT foreign keys: 0.2577 seconds
[2] Time processing 500000 added foreign keys (1 call): 6.3270 seconds

`<?php

require DIR . '/vendor/autoload.php';
require DIR . '/system/Test/bootstrap.php';

use CodeIgniter\Database\Forge;
use CodeIgniter\Test\Mock\MockConnection;

class MockForge extends Forge
{
public function processForeignKeys(string $table, bool $asQuery = false)
{
return $this->_processForeignKeys($table, $asQuery);
}
}

$count = 500000;

$db = new MockConnection([]);
$forge = new MockForge($db);

$forge->addField([
'id' => ['type' => 'INT', 'constraint' => 9, 'auto_increment' => true],
]);

echo "--- BENCHMARK ---\n";
echo "Current Branch: " . trim(shell_exec('git rev-parse --abbrev-ref HEAD')) . "\n";
echo "Iterations/Keys: " . number_format($count) . "\n\n";

// SCENARIO 1: WITHOUT FOREIGN KEYS (Fast return path)
// We test multiple calls to the method when no foreign keys have been added.
$startNoKeys = microtime(true);
for ($i = 0; $i < $count; $i++) {
$forge->processForeignKeys('my_table', true);
}
$endNoKeys = microtime(true);
$timeNoKeys = $endNoKeys - $startNoKeys;

echo "[1] Time taken for {$count} calls WITHOUT foreign keys: " . number_format($timeNoKeys, 4) . " seconds\n";

// SCENARIO 2: WITH FOREIGN KEYS
// First, we add the specified amount of keys to the configuration
for ($i = 0; $i < $count; $i++) {
$forge->addForeignKey('id', 'table_' . $i, 'id_' . $i);
}

// Then we call the method once, which processes all of them at once
$startWithKeys = microtime(true);
$forge->processForeignKeys('my_table', true);
$endWithKeys = microtime(true);
$timeWithKeys = $endWithKeys - $startWithKeys;

echo "[2] Time processing {$count} added foreign keys (1 call): " . number_format($timeWithKeys, 4) . " seconds\n";
`

@gr8man gr8man requested a review from michalsn June 30, 2026 18:55

@michalsn michalsn left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for addressing the feedback. This looks good to me, although the practical gain is quite small given the number of foreign keys we would normally process.

@michalsn michalsn added the refactor Pull requests that refactor code label Jun 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

refactor Pull requests that refactor code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants