-- how to model threaded comments (e.g. reddit comments) in SQL with a simple 'ancestors' column -- The comment tree: -- [1] -- / \ -- [2] [4] -- / \ \ -- [3] [7] [6] -- / -- [5] -- This is an improved version of the classic adjacency list in SQL used to model -- trees. By replacing the parent_id with an array of ancestors, one can query -- all nested child elements without the need for recursive queries. This makes it -- more efficient. This works for any database that supports array-like data -- types (Postgres supports arrays natively, while MySQL and Sqlite3 support JSON -- arrays) -- This method is referred to as the 'Path Enumeration' in -- https://stackoverflow.com/a/192462. The stackoverflow answer espouses something -- called a 'Closure Table', but Path Enumeration is much simpler to use. It is -- also mentioned in -- https://www.sqlteam.com/articles/more-trees-hierarchies-in-sql as a better -- alternative to Joe Celko's 'Nested Set' model. -- Postgres -- CREATE TABLE comments ( comment_id INT UNIQUE, ancestors INT[] ); INSERT INTO comments (comment_id, ancestors) VALUES (1, NULL), (2, '{1}'), (4, '{1}'), (3, '{1, 2}'), (7, '{1, 2}'), (6, '{1, 4}'), (5, '{1, 2, 3}'); -- Postgres: Get all ancestors of comment 3, ordered by depth SELECT ancestors FROM comments WHERE comment_id = 3 ; -- Postgres: Get all descendants of comment 1, ordered by depth SELECT comment_id, array_length(ancestors) AS depth FROM comments WHERE 1 = ANY(ancestors) ORDER BY depth ; -- Postgres: Get all siblings of comment 3 SELECT comment_id FROM comments WHERE ancestors = (SELECT c2.ancestors FROM comments AS c2 WHERE c2.comment_id = 3) AND comment_id <> 3 ; -- Postgres: Add comment 8 under comment 7 INSERT INTO comments (comment_id, ancestors) SELECT 8, array_append(ancestors, comment_id) FROM comments WHERE comment_id = 7 ; -- Postgres: Delete comment 2 and all its child comments DELETE FROM comments WHERE comment_id = 2 OR 2 = ANY(ancestors) ; -- MySQL -- CREATE TABLE comments ( comment_id INT UNIQUE, ancestors JSON ); INSERT INTO comments (comment_id, ancestors) VALUES (1, NULL), (2, '[1]'), (4, '[1]'), (3, '[1, 2]'), (7, '[1, 2]'), (6, '[1, 4]'), (5, '[1, 2, 3]') ; -- MySQL: Get all ancestors of comment 3, ordered by depth SELECT ancestors FROM comments WHERE comment_id = 3 ; -- MySQL: Get all descendants of comment 1, ordered by depth SELECT comment_id, json_length(ancestors) AS depth FROM comments WHERE json_contains(ancestors, 1) ORDER BY depth ; -- MySQL: Get all siblings of comment 3 SELECT comment_id FROM comments WHERE ancestors = (SELECT c2.ancestors FROM comments AS c2 WHERE c2.comment_id = 3) AND comment_id <> 3 ; -- MySQL: Add comment 8 under comment 7 INSERT INTO comments (comment_id, ancestors) SELECT 8, json_array_append(ancestors, '$', comment_id) FROM comments WHERE comment_id = 7 ; -- MySQL: Delete comment 2 and all its child comments DELETE FROM comments WHERE comment_id = 2 OR json_contains(ancestors, 2) ; -- Sqlite3 -- CREATE TABLE comments ( comment_id INT UNIQUE, ancestors JSON ); INSERT INTO comments (comment_id, ancestors) VALUES (1, NULL), (2, '[1]'), (4, '[1]'), (3, '[1, 2]'), (7, '[1, 2]'), (6, '[1, 4]'), (5, '[1, 2, 3]') ; -- Sqlite3: Get all ancestors of comment 3, ordered by depth SELECT ancestors FROM comments WHERE comment_id = 3 ; -- Sqlite3: Get all descendants of comment 1, ordered by depth SELECT comment_id, json_array_length(ancestors) AS depth FROM comments WHERE EXISTS(SELECT * FROM json_each(ancestors) WHERE json_each.value = 1) ORDER BY depth ; -- Sqlite3: Get all siblings of comment 3 SELECT comment_id FROM comments WHERE ancestors = (SELECT c2.ancestors FROM comments AS c2 WHERE c2.comment_id = 3) AND comment_id <> 3 ; -- Sqlite3: Add comment 8 under comment 7 INSERT INTO comments (comment_id, ancestors) SELECT 8, json_insert(ancestors, '$[#]', comment_id) FROM comments WHERE comment_id = 7 ; -- Sqlite3: Reparent -- Sqlite3: Delete comment 2 and all its child comments DELETE FROM comments WHERE comment_id = 2 OR EXISTS(SELECT * FROM json_each(ancestors) WHERE json_each.value = 2) ;