Skip to content

Performance Optimization ​

Bob Query Builder is designed with performance in mind. This guide covers the built-in performance features and best practices for optimizing your database operations.

Prepared Statement Caching ​

Bob automatically caches prepared statements to avoid the overhead of repeatedly preparing the same SQL queries.

How It Works ​

When you execute a query, Bob:

  1. Generates an MD5 hash of the SQL statement
  2. Checks if a prepared statement exists for that hash
  3. Reuses the existing statement or creates a new one
  4. Caches the statement for future use
php
use Bob\Database\Connection;

$connection = new Connection($config);

// Enable statement caching (enabled by default)
$connection->enableStatementCaching();

// Set maximum cached statements (default: 100)
$connection->setMaxCachedStatements(200);

// The second execution reuses the prepared statement
for ($i = 0; $i < 1000; $i++) {
    $user = $connection->table('users')
        ->where('id', $i)
        ->first(); // Same prepared statement reused
}

// Clear the statement cache if needed
$connection->clearStatementCache();

Benefits ​

  • Reduced Parse Time: SQL doesn't need to be re-parsed
  • Better Performance: Especially noticeable with repeated queries
  • Memory Efficient: LRU eviction prevents unlimited growth

Query Result Caching ​

Cache query results to avoid hitting the database for frequently accessed data.

Existence Check Caching (New in v2.1.0) ​

Optimize repeated existence checks with the new exists() caching feature:

php
use Bob\Database\Connection;

$connection = new Connection($config);

// Enable query cache for the connection
$connection->enableQueryCache(1000, 3600); // Max 1000 items, 1 hour TTL

// Enable exists caching for specific queries
$builder = $connection->table('users')
    ->enableExistsCache(120) // Cache exists() results for 2 minutes
    ->where('email', 'user@example.com');

// First call - hits database
if ($builder->exists()) {
    echo "User exists!";
}

// Subsequent calls within 2 minutes - uses cache
if ($builder->exists()) { // No database query!
    echo "Still exists (cached)!";
}

// Different query conditions = different cache key
$builder->where('status', 'active'); // Cache key changes
if ($builder->exists()) { // New database query
    echo "Active user exists!";
}

Use Cases:

  • Validation checks in loops
  • Conditional logic based on existence
  • Permission checks
  • Duplicate detection

Basic Usage ​

php
use Bob\Cache\QueryCache;
use Bob\Database\Connection;

// Create cache instance
$cache = new QueryCache();

// Configure connection with cache
$connection = new Connection($config);
$connection->setQueryCache($cache);

// Enable caching for specific queries
$users = $connection->table('users')
    ->where('status', 'active')
    ->cache(300) // Cache for 5 minutes
    ->get();

// Subsequent calls within 5 minutes return cached results

Cache Key Management ​

php
// Manually set cache key
$users = $connection->table('users')
    ->where('status', 'active')
    ->cacheKey('active_users')
    ->cache(600)
    ->get();

// Clear specific cache
$cache->forget('active_users');

// Clear all query cache
$cache->flush();

Cache Tags ​

Group related cache entries:

php
$posts = $connection->table('posts')
    ->where('status', 'published')
    ->cacheTags(['posts', 'content'])
    ->cache(3600)
    ->get();

// Clear all posts cache
$cache->flushTag('posts');

Connection Pooling ​

Manage multiple database connections efficiently with connection pooling.

Setting Up Connection Pool ​

php
use Bob\Database\ConnectionPool;

$pool = new ConnectionPool($config, [
    'min_connections' => 2,
    'max_connections' => 10,
    'max_idle_time' => 300, // 5 minutes
    'acquisition_timeout' => 5 // 5 seconds
]);

// Acquire connection from pool
$connection = $pool->acquire();

// Use the connection
$users = $connection->table('users')->get();

// Release back to pool
$pool->release($connection);

Automatic Connection Management ​

php
// Use connection with automatic release
$result = $pool->using(function($connection) {
    return $connection->table('users')
        ->where('status', 'active')
        ->get();
});

Pool Statistics ​

php
$stats = $pool->getStatistics();
echo "Active connections: " . $stats['active_connections'];
echo "Idle connections: " . $stats['idle_connections'];
echo "Total connections: " . $stats['total_connections'];
echo "Waiting requests: " . $stats['waiting_count'];

Query Profiling ​

Profile your queries to identify performance bottlenecks.

Basic Profiling ​

php
use Bob\Database\QueryProfiler;

$profiler = new QueryProfiler();
$connection->setProfiler($profiler);

// Enable profiling
$profiler->enable();

// Run your queries
$users = $connection->table('users')->get();
$posts = $connection->table('posts')->where('status', 'published')->get();

// Get profiling data
$profile = $profiler->getProfile();
foreach ($profile as $query) {
    echo "Query: " . $query['sql'] . "\n";
    echo "Time: " . $query['time'] . "ms\n";
    echo "Memory: " . $query['memory'] . " bytes\n";
}

Identifying Slow Queries ​

php
// Set slow query threshold (in milliseconds)
$profiler->setSlowQueryThreshold(100);

// Get only slow queries
$slowQueries = $profiler->getSlowQueries();
foreach ($slowQueries as $query) {
    echo "Slow query detected:\n";
    echo $query['sql'] . "\n";
    echo "Execution time: " . $query['time'] . "ms\n";
}

Query Statistics ​

php
$stats = $profiler->getStatistics();
echo "Total queries: " . $stats['total_queries'] . "\n";
echo "Total time: " . $stats['total_time'] . "ms\n";
echo "Average time: " . $stats['average_time'] . "ms\n";
echo "Slowest query: " . $stats['slowest_time'] . "ms\n";
echo "Total memory: " . $stats['total_memory'] . " bytes\n";

Chunking Large Result Sets ​

Process large datasets efficiently without loading everything into memory.

Basic Chunking ​

php
$connection->table('users')->chunk(100, function($users) {
    foreach ($users as $user) {
        // Process 100 users at a time
        $this->processUser($user);
    }
});

Chunking with Conditions ​

php
$connection->table('orders')
    ->where('status', 'pending')
    ->chunk(200, function($orders) {
        foreach ($orders as $order) {
            // Process order
            $this->processOrder($order);
            
            // Update as processed
            $this->connection->table('orders')
                ->where('id', $order->id)
                ->update(['processed' => true]);
        }
    });

Early Termination ​

php
$connection->table('users')->chunk(100, function($users) {
    foreach ($users as $user) {
        if ($this->shouldStop()) {
            return false; // Stop chunking
        }
        $this->processUser($user);
    }
});

Cursor-Based Iteration ​

For even better memory efficiency with large datasets:

php
// Uses PHP generators for minimal memory usage
foreach ($connection->table('logs')->cursor() as $log) {
    // Process one row at a time
    $this->processLog($log);
    
    // Memory usage remains constant regardless of table size
}

Cursor with Conditions ​

php
$cursor = $connection->table('users')
    ->where('created_at', '>', '2024-01-01')
    ->orderBy('id')
    ->cursor();

foreach ($cursor as $user) {
    // Process users one by one
    if ($this->shouldExportUser($user)) {
        $this->exportUser($user);
    }
}

Index Optimization ​

Using Index Hints ​

php
// Force index usage (MySQL)
$users = $connection->table('users')
    ->useIndex('idx_email')
    ->where('email', 'user@example.com')
    ->get();

// Ignore specific index
$users = $connection->table('users')
    ->ignoreIndex('idx_status')
    ->where('status', 'active')
    ->get();

Covering Indexes ​

Use covering indexes to avoid table lookups:

php
// Create covering index
$connection->statement('
    CREATE INDEX idx_users_covering 
    ON users(status, created_at, id, name, email)
');

// Query uses only the index
$users = $connection->table('users')
    ->select('id', 'name', 'email')
    ->where('status', 'active')
    ->where('created_at', '>', '2024-01-01')
    ->get();

Query Optimization Tips ​

1. Select Only Required Columns ​

php
// Good - specific columns
$users = $connection->table('users')
    ->select('id', 'name', 'email')
    ->get();

// Bad - all columns
$users = $connection->table('users')->get();

2. Use Exists Instead of Count ​

php
// Good - for existence check
if ($connection->table('users')->where('email', $email)->exists()) {
    // User exists
}

// Less efficient
if ($connection->table('users')->where('email', $email)->count() > 0) {
    // User exists
}

3. Optimize Pagination ​

php
// Use cursor-based pagination for large datasets
$lastId = 0;
do {
    $users = $connection->table('users')
        ->where('id', '>', $lastId)
        ->orderBy('id')
        ->limit(100)
        ->get();
    
    foreach ($users as $user) {
        $this->processUser($user);
        $lastId = $user->id;
    }
} while (count($users) === 100);

4. Batch Operations ​

php
// Good - single INSERT for multiple rows
$connection->table('logs')->insert([
    ['message' => 'Log 1', 'level' => 'info'],
    ['message' => 'Log 2', 'level' => 'error'],
    ['message' => 'Log 3', 'level' => 'debug'],
]);

// Bad - multiple INSERT queries
foreach ($logs as $log) {
    $connection->table('logs')->insert($log);
}

5. Use Transactions for Bulk Operations ​

php
$connection->beginTransaction();
try {
    for ($i = 0; $i < 1000; $i++) {
        $connection->table('records')->insert([
            'data' => $this->generateData($i)
        ]);
    }
    $connection->commit();
} catch (\Exception $e) {
    $connection->rollBack();
    throw $e;
}

Monitoring and Debugging ​

Query Logging ​

php
// Enable query logging
$connection->enableQueryLog();

// Run queries
$users = $connection->table('users')->get();

// Get query log
$queries = $connection->getQueryLog();
foreach ($queries as $query) {
    echo "SQL: " . $query['query'] . "\n";
    echo "Bindings: " . json_encode($query['bindings']) . "\n";
    echo "Time: " . $query['time'] . "ms\n\n";
}

EXPLAIN Analysis ​

php
// Get query explanation
$explanation = $connection->table('users')
    ->where('status', 'active')
    ->explain();

foreach ($explanation as $row) {
    echo "Type: " . $row->type . "\n";
    echo "Possible Keys: " . $row->possible_keys . "\n";
    echo "Key Used: " . $row->key . "\n";
    echo "Rows Examined: " . $row->rows . "\n";
}

Best Practices Summary ​

  1. Use Prepared Statements: Let Bob cache them automatically
  2. Cache Frequently Accessed Data: Use query result caching
  3. Pool Connections: Use connection pooling for high-traffic applications
  4. Profile in Development: Identify slow queries early
  5. Chunk Large Operations: Process large datasets in chunks
  6. Optimize Indexes: Ensure proper indexes for your queries
  7. Select Specific Columns: Don't fetch data you don't need
  8. Use Transactions: Group related operations
  9. Monitor Production: Enable query logging and profiling in production (carefully)
  10. Regular Maintenance: Analyze and optimize tables regularly

Performance Benchmarks ​

Bob Query Builder has been optimized for minimal overhead:

  • Query Building: < 0.1ms overhead
  • Prepared Statement Caching: 30-50% improvement for repeated queries
  • Connection Pooling: 60-80% reduction in connection overhead
  • Chunking: Constant memory usage regardless of dataset size
  • Result Caching: 100-1000x improvement for cached queries

Remember: The best optimization is often at the database design level. Ensure your tables are properly indexed, normalized (or denormalized where appropriate), and regularly maintained.

Released under the MIT License.