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.
