AboutBlogContact
DatabasesMarch 14, 2011 4 min read 157Updated: May 18, 2026

MySQL Optimization: How We Handled 100,000 Daily Queries on PHP 5.3

AunimedaAunimeda
📋 Table of Contents

MySQL Optimization: How We Handled 100,000 Daily Queries on PHP 5.3

In early 2011 we were running a regional news portal on a shared hosting stack: Apache 2.2, PHP 5.3, MySQL 5.1 with MyISAM tables. Traffic grew faster than expected and the database became the bottleneck. Page generation times climbed to 4-8 seconds. Here's what we did.


The Problem with MyISAM at Scale

MyISAM uses table-level locking. Every INSERT or UPDATE locks the entire table - all concurrent SELECT queries wait. With 60-70 writes per minute (comments, pageview counters, analytics inserts) and hundreds of concurrent readers, contention was constant.

Profiling with SHOW PROCESSLIST revealed a queue of 15-30 threads waiting on Locked status during peak hours.

-- The query causing most pain: updating pageview counter
UPDATE articles SET views = views + 1 WHERE id = ?;

-- With MyISAM: this locks the entire articles table
-- Every article page load triggered this - pure poison for a news site

Migration to InnoDB

InnoDB uses row-level locking. The same UPDATE above only locks the specific row, not the entire table. For a read-heavy site with frequent counter updates, this is the fundamental fix.

Migration steps:

-- Check current engine
SELECT TABLE_NAME, ENGINE 
FROM information_schema.TABLES 
WHERE TABLE_SCHEMA = 'your_db';

-- Convert table (zero downtime for reads during conversion on small tables)
ALTER TABLE articles ENGINE=InnoDB;

-- For large tables: create new table, copy data, rename
CREATE TABLE articles_new LIKE articles;
ALTER TABLE articles_new ENGINE=InnoDB;
INSERT INTO articles_new SELECT * FROM articles;
RENAME TABLE articles TO articles_old, articles_new TO articles;

Immediate results: Average query wait time dropped from 340ms to 45ms on write-heavy tables. Page generation times fell to 1.2-1.8 seconds.


Adding Memcached for Expensive Queries

Some queries were inherently slow regardless of engine - complex JOINs across 4-5 tables for homepage aggregates, category counts, tag clouds.

<?php
// PHP 5.3 Memcached pattern
$memcache = new Memcache();
$memcache->connect('127.0.0.1', 11211);

function getCategoryArticles($categoryId, $limit = 20) {
    global $memcache, $db;
    
    $cacheKey = "cat_articles_{$categoryId}_{$limit}";
    $cached = $memcache->get($cacheKey);
    
    if ($cached !== false) {
        return $cached;
    }
    
    // The expensive query
    $result = $db->query("
        SELECT a.id, a.title, a.created_at, a.views,
               u.username, c.name as category_name,
               COUNT(cm.id) as comment_count
        FROM articles a
        JOIN users u ON a.author_id = u.id
        JOIN categories c ON a.category_id = c.id
        LEFT JOIN comments cm ON a.id = cm.article_id
        WHERE a.category_id = {$categoryId}
          AND a.published = 1
        GROUP BY a.id
        ORDER BY a.created_at DESC
        LIMIT {$limit}
    ");
    
    $data = $result->fetchAll(PDO::FETCH_ASSOC);
    
    // Cache for 5 minutes
    $memcache->set($cacheKey, $data, 0, 300);
    
    return $data;
}

Cache invalidation - the hard part:

// After publishing a new article, clear affected caches
function invalidateCategoryCache($categoryId) {
    global $memcache;
    // Clear all limit variants
    foreach ([10, 20, 50] as $limit) {
        $memcache->delete("cat_articles_{$categoryId}_{$limit}");
    }
    $memcache->delete("homepage_latest");
    $memcache->delete("category_counts");
}

Index Audit

After fixing the engine and adding caching, we ran EXPLAIN on every slow query in the log:

-- Enable slow query log
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1;  -- Log queries > 1 second

-- Then analyze
EXPLAIN SELECT * FROM articles 
WHERE category_id = 5 AND published = 1 
ORDER BY created_at DESC;

-- Result showed full table scan: type = ALL
-- Fix:
CREATE INDEX idx_articles_category_published 
ON articles(category_id, published, created_at DESC);

-- Re-run EXPLAIN: type = range, rows examined dropped from 45,000 to 23

Results After All Three Changes

Metric Before After
Average page generation 4.2 sec 0.4 sec
DB queries per page 18-24 3-5 (rest cached)
Peak concurrent connections 180-220 40-60
Server load average 8-12 1.2-2.1
Monthly hosting bill $180 (overages) $60 (base plan)

The server went from struggling to having headroom. We scaled to 300,000 daily pageviews on the same hardware.


What We'd Do Differently Today

This was 2011. In 2024, the same problem gets solved with:

  • Redis instead of Memcached (better data structures, persistence, pub/sub)
  • Read replicas for horizontal read scaling
  • Query caching at ORM level (Prisma, Eloquent)
  • Connection pooling (PgBouncer equivalent for MySQL: ProxySQL)

But the fundamentals haven't changed: identify the bottleneck, fix locking, cache expensive reads, index your queries. The tools evolve; the principles don't.


Aunimeda builds backend systems with optimized database architectures - PostgreSQL, Redis, ClickHouse, and more.

Contact us for backend and database engineering. See also: Custom Software Development

Read Also

PostgreSQL EXPLAIN ANALYZE: Reading Query Plans Like a Senior DBAaunimeda
Databases

PostgreSQL EXPLAIN ANALYZE: Reading Query Plans Like a Senior DBA

Stop guessing why your queries are slow. Learn to read PostgreSQL query plans at a level where you can actually fix problems - seq scans, join strategies, row estimate disasters, and the N+1 you didn't know was hiding in your ORM output.

How to Scale MySQL with Read Replicas When Your App Slows Down (2015)aunimeda
Databases

How to Scale MySQL with Read Replicas When Your App Slows Down (2015)

When a single MySQL server handles both reads and writes, reads win and writes stall. Adding a read replica splits the load: writes go to master, reads go to replica. This is the exact setup - my.cnf, replication config, PHP connection routing - we used when our app hit 50k daily users.

The Big Data Hype (2012): How Hadoop and MongoDB Started the Data Revolutionaunimeda
Databases

The Big Data Hype (2012): How Hadoop and MongoDB Started the Data Revolution

In 2012 'Big Data' was on every slide deck. MongoDB was going to replace MySQL. Hadoop was going to process everything. We lived through the hype and learned what was real.

Need IT development for your business?

We build websites, mobile apps and AI solutions. Free consultation.

Get Consultation All articles