check-database-scaling

Analyzes PHP code for database scaling issues. Detects single DB connection for all queries, missing read replica configuration, SELECT queries hitting master, and missing connection pooling.

59 stars

Best use case

check-database-scaling is best used when you need a repeatable AI agent workflow instead of a one-off prompt.

Analyzes PHP code for database scaling issues. Detects single DB connection for all queries, missing read replica configuration, SELECT queries hitting master, and missing connection pooling.

Teams using check-database-scaling should expect a more consistent output, faster repeated execution, less prompt rewriting.

When to use this skill

  • You want a reusable workflow that can be run more than once with consistent structure.

When not to use this skill

  • You only need a quick one-off answer and do not need a reusable workflow.
  • You cannot install or maintain the underlying files, dependencies, or repository context.

Installation

Claude Code / Cursor / Codex

$curl -o ~/.claude/skills/check-database-scaling/SKILL.md --create-dirs "https://raw.githubusercontent.com/dykyi-roman/awesome-claude-code/main/skills/check-database-scaling/SKILL.md"

Manual Installation

  1. Download SKILL.md from GitHub
  2. Place it in .claude/skills/check-database-scaling/SKILL.md inside your project
  3. Restart your AI agent — it will auto-discover the skill

How check-database-scaling Compares

Feature / Agentcheck-database-scalingStandard Approach
Platform SupportNot specifiedLimited / Varies
Context Awareness High Baseline
Installation ComplexityUnknownN/A

Frequently Asked Questions

What does this skill do?

Analyzes PHP code for database scaling issues. Detects single DB connection for all queries, missing read replica configuration, SELECT queries hitting master, and missing connection pooling.

Where can I find the source code?

You can find the source code on GitHub using the link provided at the top of the page.

SKILL.md Source

# Database Scaling Check

Analyze PHP code for database access patterns that prevent horizontal scaling, overload the primary database, and miss read replica offloading opportunities.

## Detection Patterns

### 1. Single DB Connection for All Queries

```php
<?php

declare(strict_types=1);

// BAD: All queries use the same connection
final readonly class DatabaseConnection
{
    private PDO $pdo;

    public function __construct()
    {
        $this->pdo = new PDO(
            'mysql:host=db-primary;dbname=app',
            'root',
            'password',
        );
        // All reads AND writes go through this single connection
    }

    public function query(string $sql): array
    {
        return $this->pdo->query($sql)->fetchAll();
    }
}

// GOOD: Separate read/write connections
final readonly class ReadWriteConnection
{
    public function __construct(
        private PDO $writeConnection,
        private PDO $readConnection,
    ) {}

    public static function fromConfig(DatabaseConfig $config): self
    {
        return new self(
            writeConnection: new PDO($config->writeDsn(), $config->user(), $config->password()),
            readConnection: new PDO($config->readDsn(), $config->user(), $config->password()),
        );
    }

    public function forWrite(): PDO
    {
        return $this->writeConnection;
    }

    public function forRead(): PDO
    {
        return $this->readConnection;
    }
}
```

### 2. Missing Read Replica Configuration

```php
<?php

declare(strict_types=1);

// BAD: Only one database host configured
// .env:
// DATABASE_URL=mysql://root:pass@primary-db:3306/app
// No DB_READ_HOST, no replica DSN

// BAD: Doctrine with single connection
// doctrine.yaml:
// doctrine:
//     dbal:
//         url: '%env(DATABASE_URL)%'

// GOOD: Doctrine with read replica(s)
// doctrine.yaml:
// doctrine:
//     dbal:
//         default_connection: default
//         connections:
//             default:
//                 wrapper_class: Doctrine\DBAL\Connections\PrimaryReadReplicaConnection
//                 primary:
//                     url: '%env(DATABASE_PRIMARY_URL)%'
//                 replica:
//                     replica1:
//                         url: '%env(DATABASE_REPLICA1_URL)%'
//                     replica2:
//                         url: '%env(DATABASE_REPLICA2_URL)%'

// GOOD: Environment with read replica
// .env:
// DATABASE_PRIMARY_URL=mysql://root:pass@primary-db:3306/app
// DATABASE_REPLICA1_URL=mysql://root:pass@replica1-db:3306/app
// DATABASE_REPLICA2_URL=mysql://root:pass@replica2-db:3306/app
```

### 3. SELECT Queries Hitting Primary

```php
<?php

declare(strict_types=1);

// BAD: Read queries go to primary database
final readonly class ProductRepository
{
    public function __construct(
        private EntityManagerInterface $em,
    ) {}

    public function findAll(): array
    {
        // Uses default (primary) connection for reads
        return $this->em->getRepository(Product::class)->findAll();
    }

    public function findById(ProductId $id): ?Product
    {
        // Every SELECT hits the primary -- wasting write capacity
        return $this->em->find(Product::class, $id->toString());
    }
}

// GOOD: Explicit read connection for queries
final readonly class ProductRepository
{
    public function __construct(
        private EntityManagerInterface $em,
    ) {}

    public function findAll(): array
    {
        $connection = $this->em->getConnection();
        if ($connection instanceof PrimaryReadReplicaConnection) {
            $connection->ensureConnectedToReplica();
        }

        return $this->em->getRepository(Product::class)->findAll();
    }
}

// GOOD: CQRS -- separate read model with read-only connection
final readonly class ProductReadRepository
{
    public function __construct(
        private Connection $readConnection, // Injected read-only connection
    ) {}

    public function findAll(): array
    {
        return $this->readConnection
            ->executeQuery('SELECT id, name, price FROM products WHERE active = 1')
            ->fetchAllAssociative();
    }
}
```

### 4. Missing Connection Pooling

```php
<?php

declare(strict_types=1);

// BAD: New connection per request without pooling
final readonly class LegacyDatabase
{
    public function query(string $sql): array
    {
        // Each PHP-FPM request opens a new connection
        $pdo = new PDO('mysql:host=db;dbname=app', 'root', 'pass');
        $result = $pdo->query($sql)->fetchAll();
        // Connection closed at end of request
        // Under 1000 RPS = 1000 connections to DB
        return $result;
    }
}

// GOOD: Persistent connections + external pooler (PgBouncer/ProxySQL)
// Connection through PgBouncer/ProxySQL
final readonly class PooledDatabase
{
    public function __construct(
        private PDO $pdo, // Injected once, persistent
    ) {}

    // With PDO persistent connections
    public static function createPersistent(string $dsn, string $user, string $pass): self
    {
        return new self(new PDO($dsn, $user, $pass, [
            PDO::ATTR_PERSISTENT => true,
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
        ]));
    }
}

// docker-compose.yml or infrastructure:
// pgbouncer:
//     image: edoburu/pgbouncer
//     environment:
//         DATABASE_URL: postgres://user:pass@postgres:5432/app
//         POOL_MODE: transaction
//         MAX_CLIENT_CONN: 1000
//         DEFAULT_POOL_SIZE: 20
```

### 5. Heavy Queries on Primary

```php
<?php

declare(strict_types=1);

// BAD: Reporting/analytics queries on write database
final readonly class ReportService
{
    public function generateMonthlyReport(DateTimeImmutable $month): Report
    {
        // Heavy aggregation on primary -- blocks writes!
        $data = $this->em->createQuery(
            'SELECT SUM(o.total), COUNT(o) FROM Order o
             WHERE o.createdAt BETWEEN :start AND :end
             GROUP BY o.status'
        )
            ->setParameter('start', $month->modify('first day of this month'))
            ->setParameter('end', $month->modify('last day of this month'))
            ->getResult();

        return new Report($data);
    }
}

// GOOD: Heavy queries on dedicated read replica or analytics database
final readonly class ReportService
{
    public function __construct(
        private Connection $analyticsConnection, // Separate replica for analytics
    ) {}

    public function generateMonthlyReport(DateTimeImmutable $month): Report
    {
        $data = $this->analyticsConnection->executeQuery(
            'SELECT SUM(total) as revenue, COUNT(*) as order_count, status
             FROM orders
             WHERE created_at BETWEEN :start AND :end
             GROUP BY status',
            [
                'start' => $month->modify('first day of this month')->format('Y-m-d'),
                'end' => $month->modify('last day of this month')->format('Y-m-d 23:59:59'),
            ],
        )->fetchAllAssociative();

        return new Report($data);
    }
}
```

## Grep Patterns

```bash
# Single database connection
Grep: "new PDO\(|DriverManager::getConnection" --glob "**/*.php"

# Read replica configuration
Grep: "DB_READ_HOST|DATABASE_REPLICA|replica|PrimaryReadReplicaConnection" --glob "**/*.php"
Grep: "DB_READ_HOST|DATABASE_REPLICA|replica" --glob "**/.env*"

# Connection pooling indicators
Grep: "ATTR_PERSISTENT|pgbouncer|ProxySQL|pool_size" --glob "**/*.php"
Grep: "pgbouncer|proxysql" --glob "**/docker-compose*.yml"

# Heavy queries (GROUP BY, SUM, COUNT, subqueries)
Grep: "GROUP BY|SUM\(|COUNT\(|AVG\(|HAVING" --glob "**/*.php"

# Report/analytics queries
Grep: "class.*Report|generateReport|analytics" --glob "**/*.php"

# Doctrine read/write split
Grep: "ensureConnectedToReplica|ensureConnectedToPrimary" --glob "**/*.php"

# All database connections in config
Grep: "DATABASE_URL|DB_HOST|DB_CONNECTION" --glob "**/.env*"
```

## Severity Classification

| Pattern | Severity |
|---------|----------|
| All queries on single primary connection | 🔴 Critical |
| Reporting/analytics on primary database | 🔴 Critical |
| No read replica configuration | 🟠 Major |
| No connection pooling under high load | 🟠 Major |
| SELECT queries not routed to replica | 🟠 Major |
| New PDO connection per query | 🟡 Minor |
| Missing persistent connections | 🟡 Minor |

## Output Format

```markdown
### Database Scaling Issue: [Brief Description]

**Severity:** 🔴/🟠/🟡
**Location:** `file.php:line`
**Type:** [Single Connection|No Replica|SELECT on Primary|No Pooling|Heavy Query]

**Issue:**
[Description of the database scaling problem]

**Impact:**
- Primary database overloaded with reads
- Write latency increases under read load
- Cannot scale reads independently

**Code:**
```php
// Non-scalable database access
```

**Fix:**
```php
// With read/write split and pooling
```
```

## When This Is Acceptable

- **Low-traffic applications** -- Under 100 QPS, a single database connection is sufficient
- **Single-server deployment** -- When horizontal scaling is not a requirement
- **Development/staging** -- Non-production environments don't need read replicas
- **Write-heavy workloads** -- If 90%+ of operations are writes, read replicas add complexity without benefit

### False Positive Indicators
- Application is a CLI tool or batch processor (not serving HTTP traffic)
- Database is already behind a managed proxy (AWS RDS Proxy, Cloud SQL Proxy)
- Read/write split is configured at the ORM level but not visible in code
- PDO is created once in a service container and reused across requests

Related Skills

create-health-check

59
from dykyi-roman/awesome-claude-code

Generates Health Check pattern for PHP 8.4. Creates application-level health endpoints with component checkers (Database, Redis, RabbitMQ), status aggregation, and RFC-compliant JSON response. Includes unit tests.

create-docker-healthcheck

59
from dykyi-roman/awesome-claude-code

Generates Docker health check scripts for PHP services. Creates PHP-FPM, Nginx, and custom endpoint health checks.

check-xxe

59
from dykyi-roman/awesome-claude-code

Analyzes PHP code for XML External Entity vulnerabilities. Detects unsafe XML parsers, missing entity protection, LIBXML flags issues, XSLT attacks.

check-version-consistency

59
from dykyi-roman/awesome-claude-code

Audits version consistency across project files. Checks composer.json, README, CHANGELOG, docs, and configuration files for version number synchronization.

check-type-juggling

59
from dykyi-roman/awesome-claude-code

Detects PHP type juggling vulnerabilities. Identifies loose comparison with user input, in_array without strict mode, switch statement type coercion, and hash comparison bypasses.

check-timeout-strategy

59
from dykyi-roman/awesome-claude-code

Audits timeout configuration across HTTP clients, database connections, queue consumers, cache operations, and external service calls. Detects missing or misconfigured timeouts.

check-test-quality

59
from dykyi-roman/awesome-claude-code

Analyzes PHP test code quality. Checks test structure, assertion quality, test isolation, naming conventions, AAA pattern adherence.

check-ssrf

59
from dykyi-roman/awesome-claude-code

Analyzes PHP code for SSRF vulnerabilities. Detects unvalidated URLs, internal network access, DNS rebinding, cloud metadata access, URL parsing bypass attempts.

check-sql-injection

59
from dykyi-roman/awesome-claude-code

Analyzes PHP code for SQL injection vulnerabilities. Detects query concatenation, ORM misuse, raw queries, dynamic identifiers, prepared statement bypasses.

check-serialization

59
from dykyi-roman/awesome-claude-code

Analyzes PHP code for serialization overhead. Detects inefficient JSON encoding, large object hydration, missing JsonSerializable, circular reference issues.

check-sensitive-data

59
from dykyi-roman/awesome-claude-code

Analyzes PHP code for sensitive data exposure. Detects plaintext secrets, exposed credentials, PII in logs, insecure storage, hardcoded keys.

check-secure-headers

59
from dykyi-roman/awesome-claude-code

Audits HTTP security headers configuration. Checks CSP, X-Frame-Options, HSTS, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, and cache control headers.