Skip to content

About

No description, website, or topics provided.

Resources

Stars

2 stars

Watchers

0 watching

Forks

Repository files navigation

PHP Database Fetch Benchmark — 10 Million Rows

This project measures the time and memory required to fetch 10 million rows from a PostgreSQL database using three different PHP approaches, in two modes each: streaming (one row at a time) and fetch-all (entire result set loaded into memory at once).

Architecture

Container Language / Framework Streaming strategy Fetch-all strategy
pdo_benchmark Pure PHP + PDO fetch() — FETCH_ASSOC fetchAll() — FETCH_ASSOC
pdo_benchmark Pure PHP + PDO fetch() — FETCH_OBJ fetchAll() — FETCH_OBJ
pdo_benchmark Pure PHP + PDO — fetchAll() — FETCH_CLASS
symfony_benchmark Symfony 8 + Doctrine ORM toIterable() + detach() getResult()
laravel_benchmark Laravel 11 + Eloquent cursor() — LazyCollection get() — Collection

Infrastructure

  • Database: PostgreSQL 18
  • PHP: 8.5-cli-alpine
  • Table: records — 10,000,000 rows, 7 columns (id, name, email, age, score, active, created_at)

Results

Approach Mode Rows Fetched Time Peak Memory
Pure PDO (FETCH_ASSOC) fetch-all 10,000,000 7.727 seconds 5.54 GB
Pure PDO (FETCH_CLASS) fetch-all 10,000,000 6.282 seconds 3.32 GB
Pure PDO (FETCH_OBJ) fetch-all 10,000,000 9.000 seconds 6.04 GB
Symfony + Doctrine ORM fetch-all 10,000,000 123.336 seconds 16.97 GB
Laravel + Eloquent fetch-all 10,000,000 120.495 seconds 14.54 GB
Pure PDO (FETCH_ASSOC) streaming 10,000,000 4.462 seconds 2 MB
Pure PDO (FETCH_CLASS) streaming 10,000,000 4.919 seconds 2 MB
Pure PDO (FETCH_OBJ) streaming 10,000,000 4.407 seconds 2 MB
Symfony + Doctrine ORM streaming 10,000,000 47.733 seconds 14 MB
Laravel + Eloquent streaming 10,000,000 91.955 seconds 28 MB

Results measured inside Docker containers. Hardware and Docker resource limits affect numbers. Fetch-all benchmarks run with memory_limit=-1; OOM means the OS killed the process before completion.

Key Takeaways

Streaming vs Fetch-all

Streaming approaches keep memory flat regardless of result set size:

  • PDO cursor: ~2 MB constant
  • Doctrine ORM with detach(): ~28 MB (framework bootstrap only)
  • Eloquent with cursor(): ~28 MB (framework bootstrap only)

Fetch-all loads the entire result set into memory before iterating:

  • PDO fetchAll() FETCH_ASSOC requires ~5.5 GB for 10 M rows
  • PDO fetchAll() FETCH_CLASS requires ~3.3 GB (typed objects are more compact than hash maps)
  • Doctrine ORM getResult() is killed by the OS OOM killer before completing
  • Eloquent get() requires ~14.5 GB (full model hydration overhead)

Streaming approach comparison

  • Pure PDO — fastest and lowest memory. No ORM overhead, direct RDBMS cursor.
  • Symfony + Doctrine ORM — toIterable() streams one hydrated entity at a time; the Doctrine UnitOfWork pipeline adds per-row cost. detach() after each row keeps the identity map flat.
  • Laravel + Eloquent — cursor() streams one model at a time but fires attribute casts, accessor resolution, and model events on every single row.

How to Run

Prerequisites

  • Docker + Docker Compose v2

1. Clone / enter the project

cd php-database-fetch

2. Run everything in one command

chmod +x run_benchmarks.sh generate_readme.sh
./run_benchmarks.sh

This will:

  1. Start PostgreSQL
  2. Populate the records table with 10 million rows
  3. Build the three PHP benchmark containers
  4. Run all streaming and fetch-all benchmarks in each container
  5. Regenerate this README with the result table

3. Run individual benchmarks

# PDO — streaming
docker exec pdo_benchmark php /app/fetch.php

# PDO — fetch-all (memory_limit=-1 required for 10 M rows)
docker exec pdo_benchmark php -d memory_limit=-1 /app/fetch_all.php
docker exec pdo_benchmark php -d memory_limit=-1 /app/fetch_class_all.php

# Symfony + Doctrine ORM
docker exec symfony_benchmark php bin/console benchmark:fetch
docker exec symfony_benchmark php -d memory_limit=-1 bin/console benchmark:fetch-all

# Laravel + Eloquent
docker exec laravel_benchmark php artisan benchmark:fetch
docker exec laravel_benchmark php -d memory_limit=-1 artisan benchmark:fetch-all

4. Tear down

docker compose down -v

Project Structure

php-database-fetch/
├── docker-compose.yml
├── run_benchmarks.sh        # One-shot: seed + build + run all benchmarks + README
├── generate_readme.sh       # Parses results and writes README.md
├── setup/
│   ├── Dockerfile
│   ├── populate.sql         # Creates & seeds records table (10M rows)
│   └── entrypoint.sh
├── pdo/
│   ├── Dockerfile           # php:8.5-cli-alpine + pdo_pgsql
│   ├── fetch.php            # Streaming  — FETCH_ASSOC, one array per row
│   ├── fetch_obj.php        # Streaming  — FETCH_OBJ, one stdClass per row
│   ├── fetch_class.php      # Streaming  — FETCH_CLASS, one typed object per row
│   ├── fetch_all.php        # Fetch-all  — FETCH_ASSOC, full result set in memory
│   ├── fetch_obj_all.php    # Fetch-all  — FETCH_OBJ, full stdClass array in memory
│   └── fetch_class_all.php  # Fetch-all  — FETCH_CLASS, full object array in memory
├── symfony/
│   ├── Dockerfile
│   └── src/
│       ├── Entity/
│       │   └── Record.php                  # Doctrine ORM entity
│       └── Command/
│           ├── FetchRecordsCommand.php     # benchmark:fetch     — streaming toIterable()
│           └── FetchAllRecordsCommand.php  # benchmark:fetch-all — getResult() all in memory
└── laravel/
    ├── Dockerfile
    ├── app/Models/
    │   └── Record.php
    └── app/Console/Commands/
        ├── FetchRecordsCommand.php     # benchmark:fetch     — streaming cursor()
        └── FetchAllRecordsCommand.php  # benchmark:fetch-all — get() all in memory

About

No description, website, or topics provided.

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages