# N+1 Query Detection & Prevention

Comprehensive N+1 query detection system for the Custom PHP Framework.

## Overview

The N+1 Query Detection System automatically identifies and analyzes N+1 query problems - one of the most common performance killers in database-driven applications. N+1 problems occur when an application executes 1 query to load parent records, then N additional queries to load related data for each parent.

**Performance Impact**: Fixing N+1 problems can improve performance by **10-100x** by reducing hundreds of database queries to just 1-2.

## Architecture

```
┌─────────────────────┐
│  ProfilingConnection│ (Captures all queries)
└──────────┬──────────┘
           │
           v
┌──────────────────────┐
│    QueryLogger       │ (Collects query execution data)
└──────────┬───────────┘
           │
           v
┌──────────────────────┐
│  NPlusOneDetector    │ (Analyzes patterns, detects N+1)
└──────────┬───────────┘
           │
           v
┌───────────────────────┐
│ EagerLoadingAnalyzer  │ (Generates optimization strategies)
└──────────┬────────────┘
           │
           v
┌────────────────────────────┐
│ NPlusOneDetectionService   │ (Facade & Public API)
└────────────────────────────┘
```

## Core Components

### 1. QueryLog Value Object

Represents a single executed database query with full context.

```php
final readonly class QueryLog
{
    public function __construct(
        public string $sql,                   // SQL query
        public array $bindings,               // Bound parameters
        public float $executionTimeMs,        // Execution time in milliseconds
        public string $stackTrace,            // Full stack trace
        public int $rowCount = 0,            // Number of rows returned/affected
        public ?string $callerClass = null,   // Calling class
        public ?string $callerMethod = null,  // Calling method
        public int $callerLine = 0           // Line number
    ) {}

    // Pattern normalization for grouping similar queries
    public function getPattern(): string;

    // Query type detection
    public function isSelect(): bool;
    public function isInsert(): bool;
    public function isUpdate(): bool;
    public function isDelete(): bool;

    // Analysis helpers
    public function getTableName(): ?string;
    public function hasWhereClause(): bool;
    public function hasJoin(): bool;
    public function isSlow(float $thresholdMs = 100.0): bool;
}
```

**Pattern Normalization Example**:
```php
// Original SQL
"SELECT * FROM users WHERE id = 123 AND email = 'test@example.com'"

// Normalized Pattern
"SELECT * FROM users WHERE id = ? AND email = ?"
```

### 2. QueryPattern Value Object

Represents a repeated SQL pattern with N+1 detection logic.

```php
final readonly class QueryPattern
{
    public function __construct(
        public string $pattern,        // Normalized SQL pattern
        public array $queries          // array<QueryLog>
    ) {}

    // N+1 Detection
    public function isPotentialNPlusOne(): bool;
    public function getNPlusOneSeverity(): int;  // 0-10 scale
    public function getSeverityLevel(): string;  // CRITICAL, HIGH, MEDIUM, LOW, INFO

    // Statistics
    public function getExecutionCount(): int;
    public function getTotalExecutionTimeMs(): float;
    public function getAverageExecutionTimeMs(): float;

    // Analysis
    public function getTableName(): ?string;
    public function hasConsistentCaller(): bool;
    public function getCallerLocations(): array;
}
```

**N+1 Detection Criteria**:
- Executed >5 times (configurable)
- Has WHERE clause (filtering by ID)
- No JOIN present (could have been optimized)
- Is SELECT query

**Severity Scoring** (0-10):
- **Execution count**: Up to 4 points
- **Performance impact**: Up to 3 points
- **Consistent caller**: Up to 2 points
- **Total time impact**: Up to 1 point

**Severity Levels**:
- **CRITICAL**: Score >= 8 (immediate action required)
- **HIGH**: Score >= 6 (fix soon)
- **MEDIUM**: Score >= 4 (optimization opportunity)
- **LOW**: Score >= 2 (minor issue)
- **INFO**: Score < 2 (informational)

### 3. NPlusOneDetection Value Object

Detection result with recommendations and performance impact estimates.

```php
final readonly class NPlusOneDetection
{
    public function __construct(
        public QueryPattern $pattern,       // Detected pattern
        public string $recommendation,      // How to fix
        public array $metadata = []        // Additional context
    ) {}

    public function getSeverityLevel(): string;
    public function getSeverityScore(): int;
    public function isCritical(): bool;
    public function getPerformanceImpact(): string;
}
```

**Performance Impact Example**:
```
"Reducing 50 queries to ~2 could save ~96% (500ms → ~20ms)"
```

### 4. EagerLoadingStrategy

Optimization strategy with code examples.

```php
final readonly class EagerLoadingStrategy
{
    public function __construct(
        public string $tableName,            // Affected table
        public string $relationshipType,     // belongsTo, hasMany, belongsToMany
        public string $codeExample,          // Before/after code
        public int $priority,                // 0-200 priority score
        public string $estimatedImprovement, // Performance estimate
        public array $affectedLocations     // Where to apply fix
    ) {}
}
```

**Relationship Type Detection**:
- `WHERE.*_id = ?` → **belongsTo** (many-to-one)
- `JOIN.*_.*ON` → **belongsToMany** (many-to-many)
- Default → **hasMany** (one-to-many)

**Code Example Template**:
```php
// Before (N+1 Problem):
$items = $repository->findAll();
foreach ($items as $item) {
    $related = $item->posts; // Triggers separate query
}

// After (Eager Loading):
$items = $repository->findWithRelations(['*'], ['posts']);
// All posts loaded in 1-2 queries total
```

## NPlusOneDetectionService API

Main facade for all N+1 detection operations.

### Start/Stop Logging

```php
$service->startLogging();  // Enable query logging
$service->stopLogging();   // Disable query logging
```

### Analysis Methods

```php
// Analyze all logged queries
$result = $service->analyze();
/*
Returns:
[
    'detections' => array<NPlusOneDetection>,
    'strategies' => array<EagerLoadingStrategy>,
    'statistics' => [
        'total_queries' => 150,
        'total_patterns' => 12,
        'n_plus_one_patterns' => 3,
        'n_plus_one_queries' => 89,
        'n_plus_one_percentage' => 59.3,
        'time_wasted_percentage' => 78.5
    ]
]
*/

// Generate formatted text report
$report = $service->analyzeAndReport();

// Quick check for N+1 problems
$hasProblems = $service->hasNPlusOneProblems(); // boolean

// Get only critical problems (severity >= 8)
$critical = $service->getCriticalProblems(); // array<NPlusOneDetection>
```

### Profiling Specific Code

```php
// Profile a specific code block
$analysis = $service->profile(function() {
    // Your code here
    $users = $userRepository->findAll();

    foreach ($users as $user) {
        $posts = $user->getPosts(); // Potential N+1
    }

    return $users;
});

/*
Returns analysis + execution info:
[
    'detections' => [...],
    'strategies' => [...],
    'statistics' => [...],
    'execution_time_ms' => 245.67,
    'callback_result' => $users
]
*/
```

### Query Statistics

```php
// Get general query statistics
$stats = $service->getQueryStatistics();
/*
[
    'total_queries' => 150,
    'total_time_ms' => 345.6,
    'average_time_ms' => 2.304,
    'select_count' => 120,
    'insert_count' => 15,
    'update_count' => 10,
    'delete_count' => 5,
    'slow_queries' => 3,
    'unique_patterns' => 25
]
*/

// Get all logged queries
$logs = $service->getQueryLogs(); // array<QueryLog>

// Clear query logs
$service->clearLogs();
```

## Console Command

### Usage

```bash
# Profile specific code
php console.php detect:n-plus-one --profile=UserController::index

# Generate full report
php console.php detect:n-plus-one --report

# Show only critical problems
php console.php detect:n-plus-one --critical-only

# Show usage help
php console.php detect:n-plus-one
```

### Interactive Profiling Workflow

```bash
$ php console.php detect:n-plus-one --profile=UserService::getUsers

Profiling: UserService::getUsers
============================================================

Query logging started. Execute your code now...
Note: This command only starts logging. You need to execute the code separately.
Press Enter when done...

[Execute your code in browser/tests]
[Press Enter]

=== N+1 Query Detection Report ===

Query Statistics:
  Total queries: 52
  Total patterns: 8
  Total time: 234.50ms

N+1 Problems Detected:
  N+1 patterns: 2
  N+1 queries: 45 (86.5% of total)
  Time wasted: 198.30ms (84.5% of total)

Detected Issues:

[1] CRITICAL - posts
    Score: 9/10
    Executions: 40
    Total time: 180.00ms
    Impact: Reducing 40 queries to ~2 could save ~95% (180ms → ~9ms)

    Recommendation:
    N+1 Query detected in table 'posts'

    Location: UserService::getUsers:42

    Recommended fix:
    1. Use eager loading instead of lazy loading
       Example: $repository->findWithRelations($ids, ['posts'])

    2. Or use batch loading:
       Example: $repository->batchLoad($ids)

    3. Or add JOIN to initial query:
       Example: SELECT * FROM parent LEFT JOIN posts ON ...

[2] HIGH - comments
    Score: 7/10
    Executions: 5
    Total time: 18.30ms
    Impact: Reducing 5 queries to ~2 could save ~60% (18.30ms → ~7.32ms)

=== Eager Loading Opportunities ===

Found 2 opportunities:

[1] posts (Priority: 150)
    Relationship Type: hasMany
    Estimated Improvement: Reducing 40 queries to ~2 could save ~95% (180ms → ~9ms)
    Affected Locations:
      - UserService::getUsers:42

    Code Example:
    // Before (N+1 Problem):
    $items = $repository->findAll();
    foreach ($items as $item) {
        $related = $item->posts; // Triggers separate query
    }

    // After (Eager Loading):
    $items = $repository->findWithRelations(['*'], ['posts']);
    // This will load all related posts in one query
```

## Automatic Integration with ProfilingConnection

The N+1 Detection System automatically integrates with the framework's `ProfilingConnection` when profiling is enabled.

### Setup in DI Container

```php
// NPlusOneDetectionServiceInitializer handles setup automatically
final readonly class NPlusOneDetectionServiceInitializer
{
    #[Initializer]
    public function __invoke(Container $container): NPlusOneDetectionService
    {
        $queryLogger = new QueryLogger();
        $detector = new NPlusOneDetector();
        $analyzer = new EagerLoadingAnalyzer();

        $service = new NPlusOneDetectionService(
            $queryLogger,
            $detector,
            $analyzer,
            $this->logger
        );

        // Automatic integration
        $this->setupProfilingIntegration($queryLogger);

        return $service;
    }
}
```

### How It Works

1. **ProfilingConnection** captures every query execution
2. **NPlusOneQueryLoggerIntegration** forwards query data to **QueryLogger**
3. **QueryLogger** collects queries (when enabled)
4. **NPlusOneDetectionService** analyzes patterns on demand

## Common N+1 Patterns

### Pattern 1: User Posts (hasMany)

**Problem**:
```php
$users = $userRepository->findAll(); // 1 query

foreach ($users as $user) {
    $posts = $user->getPosts(); // N queries (one per user)
}
```

**Solution**:
```php
$users = $userRepository->findWithRelations(['*'], ['posts']); // 2 queries total

foreach ($users as $user) {
    $posts = $user->getPosts(); // Already loaded
}
```

### Pattern 2: Post Author (belongsTo)

**Problem**:
```php
$posts = $postRepository->findAll(); // 1 query

foreach ($posts as $post) {
    $author = $post->getAuthor(); // N queries
}
```

**Solution**:
```php
$posts = $queryBuilder
    ->select('posts.*', 'users.name as author_name')
    ->leftJoin('users', 'posts.user_id', '=', 'users.id')
    ->get(); // 1 query with JOIN
```

### Pattern 3: Many-to-Many Tags

**Problem**:
```php
$posts = $postRepository->findAll(); // 1 query

foreach ($posts as $post) {
    $tags = $post->getTags(); // N queries via pivot table
}
```

**Solution**:
```php
$posts = $postRepository->findWithRelations(['*'], ['tags']); // 2 queries
// Query 1: SELECT * FROM posts
// Query 2: SELECT * FROM tags JOIN post_tag ...
```

## Configuration

### NPlusOneDetector Configuration

```php
$detector = new NPlusOneDetector(
    minExecutionCount: 5,      // Minimum executions to consider N+1
    minSeverityScore: 4.0      // Minimum severity to include in results
);
```

**Tuning Guidelines**:
- **Low traffic dev**: `minExecutionCount: 3` (catch smaller issues)
- **High traffic prod**: `minExecutionCount: 10` (reduce noise)
- **All issues**: `minSeverityScore: 0.0`
- **Critical only**: `minSeverityScore: 8.0`

## Performance Characteristics

### Detection Overhead

- **Logging overhead**: <1ms per query
- **Pattern analysis**: <10ms for 100 queries
- **Report generation**: <50ms for complex analysis

### Memory Usage

- ~1KB per logged query
- ~100 queries = ~100KB memory

### Typical Performance Gains

| N+1 Severity | Queries Before | Queries After | Performance Improvement |
|--------------|----------------|---------------|-------------------------|
| CRITICAL (50+ queries) | 52 | 2 | 10-50x faster |
| HIGH (20-50 queries) | 25 | 2-3 | 5-10x faster |
| MEDIUM (10-20 queries) | 15 | 2-3 | 3-5x faster |

## Best Practices

### 1. Use Profiling in Development

```php
// Enable N+1 detection in development
if ($environment === 'development') {
    $service->startLogging();

    // Your code

    if ($service->hasNPlusOneProblems()) {
        $report = $service->analyzeAndReport();
        error_log($report);
    }
}
```

### 2. Profile Critical User Flows

```php
// Profile specific user actions
$analysis = $service->profile(function() {
    // Critical user flow
    return $dashboardController->index($request);
});

foreach ($analysis['detections'] as $detection) {
    if ($detection->isCritical()) {
        // Alert development team
        $this->alerting->send("Critical N+1 in dashboard: " . $detection->pattern->getTableName());
    }
}
```

### 3. Integrate with CI/CD

```bash
# In test suite
php console.php detect:n-plus-one --critical-only

# Fail build if critical N+1 problems detected
if [ $? -ne 0 ]; then
    echo "Critical N+1 problems detected!"
    exit 1
fi
```

### 4. Monitor Production

```php
// Periodic N+1 detection in production (low overhead)
$scheduler->schedule(
    'n-plus-one-check',
    CronSchedule::fromExpression('0 */6 * * *'), // Every 6 hours
    function() use ($service) {
        $service->startLogging();

        // Run sample of typical operations
        $this->runSampleOperations();

        $service->stopLogging();

        $critical = $service->getCriticalProblems();

        if (!empty($critical)) {
            $this->sendAlertToOps($critical);
        }
    }
);
```

### 5. Document Known N+1 Patterns

```php
// Mark intentional N+1 patterns with comments
public function getUserPosts(UserId $userId): array
{
    // INTENTIONAL N+1: Posts loaded on-demand for admin dashboard
    // Trade-off: Reduces memory usage for large datasets
    // Acceptable because: Admin views typically show <10 users

    return $this->postRepository->findByUserId($userId);
}
```

## Troubleshooting

### Issue: No N+1 Problems Detected

**Causes**:
- Logging not enabled (`$service->startLogging()`)
- Too few query executions (< minExecutionCount)
- All queries use JOINs (already optimized)
- Severity threshold too high

**Solution**:
```php
// Check if logging is enabled
if (!$this->queryLogger->isEnabled()) {
    $service->startLogging();
}

// Lower thresholds temporarily
$detector = new NPlusOneDetector(
    minExecutionCount: 3,
    minSeverityScore: 0.0
);
```

### Issue: False Positives

**Causes**:
- Batch loading patterns detected as N+1
- Intentional on-demand loading for performance
- Pagination queries

**Solution**:
```php
// Review severity score - focus on CRITICAL/HIGH only
$critical = $service->getCriticalProblems();

// Or increase thresholds
$detector = new NPlusOneDetector(
    minExecutionCount: 10,  // More conservative
    minSeverityScore: 6.0   // HIGH and above only
);
```

### Issue: Performance Impact of Detection

**Causes**:
- Logging enabled in production
- Very high query volume

**Solution**:
```php
// Only enable in development/staging
if ($environment !== 'production') {
    $service->startLogging();
}

// Or use sampling in production
if (random_int(1, 100) === 1) { // 1% sample
    $service->startLogging();
}
```

## Framework Integration Patterns

### With EntityManager

```php
class UserController
{
    public function __construct(
        private readonly UserRepository $repository,
        private readonly NPlusOneDetectionService $nPlusOneService
    ) {}

    public function index(): ViewResult
    {
        // Enable detection for this request
        $this->nPlusOneService->startLogging();

        $users = $this->repository->findAll();

        // EntityManager queries are automatically logged

        if ($this->nPlusOneService->hasNPlusOneProblems()) {
            $this->logger->warning('N+1 detected in UserController::index');
        }

        return new ViewResult('users/index', ['users' => $users]);
    }
}
```

### With Test Suite

```php
describe('UserService', function () {
    beforeEach(function () {
        $this->nPlusOneService = $this->container->get(NPlusOneDetectionService::class);
        $this->nPlusOneService->startLogging();
    });

    afterEach(function () {
        // Assert no N+1 problems in tests
        $critical = $this->nPlusOneService->getCriticalProblems();
        expect($critical)->toBeEmpty();
    });

    it('loads users without N+1', function () {
        $users = $this->userService->getAllUsersWithPosts();

        // Assertion happens in afterEach
    });
});
```

### With Events

```php
// Listen for N+1 detection
$eventDispatcher->listen(NPlusOneDetectedEvent::class, function($event) {
    $detection = $event->detection;

    if ($detection->isCritical()) {
        $this->metrics->increment('n_plus_one.critical', [
            'table' => $detection->pattern->getTableName()
        ]);
    }
});
```

## Summary

The N+1 Query Detection System provides:

✅ **Automatic Detection** - Identifies N+1 patterns automatically
✅ **Severity Scoring** - Prioritizes issues by impact (0-10 scale)
✅ **Actionable Recommendations** - Provides fix strategies with code examples
✅ **Integration** - Works seamlessly with ProfilingConnection
✅ **Console Interface** - CLI tools for profiling and reporting
✅ **Minimal Overhead** - <1ms per query logging overhead
✅ **Production-Ready** - Suitable for sampling in production environments

**Typical Results**: 10-100x performance improvement by reducing hundreds of queries to 1-2.
