Classifieds sites live and die by their categories: "For Sale > Electronics > Phones" needs to load fast, show counts, and support breadcrumbs. I've used three approaches over the years. Here's where each one fits.
Adjacency list: simple, but recursive
CREATE TABLE categories (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
parent_id INT UNSIGNED NULL,
name VARCHAR(100) NOT NULL,
slug VARCHAR(100) NOT NULL,
KEY (parent_id)
);
Easy to insert and move. On MySQL 8 you can fetch a subtree with a recursive CTE:
WITH RECURSIVE sub AS (
SELECT id FROM categories WHERE id = ?
UNION ALL
SELECT c.id FROM categories c JOIN sub ON c.parent_id = sub.id
)
SELECT id FROM sub;
On MySQL 5.7 or older you're stuck with loops in PHP or a fixed number of self-joins.
Closure table: fast reads, a bit more bookkeeping
Store every ancestor/descendant pair:
CREATE TABLE category_paths (
ancestor INT UNSIGNED NOT NULL,
descendant INT UNSIGNED NOT NULL,
depth TINYINT UNSIGNED NOT NULL,
PRIMARY KEY (ancestor, descendant),
KEY (descendant)
);
All listings under "Electronics", including subcategories, become one indexed join:
SELECT l.*
FROM listings l
JOIN category_paths p ON p.descendant = l.category_id
WHERE p.ancestor = ? AND l.status = 'active'
ORDER BY l.created_at DESC
LIMIT 30;
Breadcrumbs are just as easy: select ancestors of the current category ordered by depth descending.
Inserting a new category means copying the parent's ancestor rows:
INSERT INTO category_paths (ancestor, descendant, depth)
SELECT ancestor, :new_id, depth + 1 FROM category_paths WHERE descendant = :parent_id
UNION ALL SELECT :new_id, :new_id, 0;
Nested sets: skip it for classifieds
Nested sets are fast to read but painful to write, and admins reorganize categories more often than you'd think. I wouldn't use them here.
Counts without counting
Don't COUNT(*) on every page view. Keep a listing_count column per category, updated by a nightly job or on listing status changes, and roll it up through the closure table.
My pick
Adjacency list for the source of truth, a closure table for reads, both maintained in the same transaction. Category pages stay at one indexed query, and moving a branch is still manageable.
These patterns power the category pages across our network: Classifieds24.net, Classifieds24.info, Classifieds24.org, GlobalClassifieds.info, GlobalClassifieds.org, HirePros.pro, Realtor.pm and LinkMarket.app.
Top comments (0)