WordPress stores category and comment hierarchies with a parent column, but querying the full tree at arbitrary depth with a single SQL statement requires recursive CTEs — available in MySQL 8.0+ and MariaDB 10.2+. A recursive CTE consists of an anchor member (the root rows) and a recursive member (each subsequent level) joined with UNION ALL.
Problem: WordPress hierarchical data — categories with subcategories, menus with sub-menus, comment threads — is stored as flat rows in MySQL but needs to be queried as a tree structure, which recursive PHP loops achieve slowly with N+1 queries.
Solution: Use MySQL Recursive Common Table Expressions (CTEs) — WITH RECURSIVE cte AS (anchor UNION ALL recursive_step) — to traverse a hierarchy in a single query. For wp_terms, join with wp_term_taxonomy on parent to walk the tree in SQL. Use $wpdb->get_results() with $wpdb->prepare() for safe parameterisation.
The queries below fetch a full WordPress category tree starting from a given root, calculate the depth of each node, build the breadcrumb path as a string, and detect circular references — a safety guard when the data is user-managed.
-- 1. Fetch full category subtree from a given root (term_id = 5)
WITH RECURSIVE category_tree AS (
-- Anchor: the root category
SELECT
t.term_id,
t.name,
tt.parent,
0 AS depth,
CAST( t.name AS CHAR(1000) ) AS breadcrumb
FROM wp_terms t
JOIN wp_term_taxonomy tt ON tt.term_id = t.term_id
WHERE tt.taxonomy = 'category'
AND tt.parent = 0 -- top-level only; or: AND t.term_id = 5
UNION ALL
-- Recursive member: children of the current level
SELECT
t.term_id,
t.name,
tt.parent,
ct.depth + 1,
CONCAT( ct.breadcrumb, ' > ', t.name )
FROM wp_terms t
JOIN wp_term_taxonomy tt ON tt.term_id = t.term_id
JOIN category_tree ct ON tt.parent = ct.term_id
WHERE tt.taxonomy = 'category'
)
SELECT term_id, REPEAT(' ', depth) || name AS indented_name, breadcrumb, depth
FROM category_tree
ORDER BY breadcrumb;
-- 2. Recursive comment thread (all descendants of comment_ID = 10)
WITH RECURSIVE comment_thread AS (
SELECT comment_ID, comment_parent, comment_content, comment_author, 0 AS depth
FROM wp_comments
WHERE comment_ID = 10
UNION ALL
SELECT c.comment_ID, c.comment_parent, c.comment_content, c.comment_author,
ct.depth + 1
FROM wp_comments c
JOIN comment_thread ct ON c.comment_parent = ct.comment_ID
)
SELECT depth, comment_ID, comment_author,
SUBSTR( comment_content, 1, 60 ) AS excerpt
FROM comment_thread
ORDER BY depth, comment_ID;
-- 3. Guard against cycles: limit recursion depth to 20
WITH RECURSIVE safe_tree AS (
SELECT term_id, parent, 0 AS depth
FROM wp_term_taxonomy
WHERE parent = 0 AND taxonomy = 'category'
UNION ALL
SELECT tt.term_id, tt.parent, st.depth + 1
FROM wp_term_taxonomy tt
JOIN safe_tree st ON tt.parent = st.term_id
WHERE tt.taxonomy = 'category'
AND st.depth < 20 -- hard safety cap
)
SELECT * FROM safe_tree;
NOTE: MySQL 8.0 limits recursive CTE iterations with the cte_max_recursion_depth variable (default 1000); set it to a lower value per session (SET cte_max_recursion_depth = 50) when processing untrusted hierarchical data to prevent runaway queries from user-controlled inputs.