Search code examples
sqlsqlitecountcommon-table-expressionrecursive-query

SQLite recursive CTE to calculate number of items in list (of lists)


Each List can have multiple ListItems (1 --> n). And a list can have a parent list.

      List 1
       |
       |-- List Item 1
       |
       |-- List 2  
       |     |              
       |     |-- Liste Item 3
       |
       |-- List 3

I would like to know how many items there are in List1 + its child lists(List2 + List3 + ... etc), so for List1 the totalNumberOfItems is 6.

The table List looks like this

id name parentId
1 List 1
2 List 2 1
3 List 3 2

And table ListItem looks like this

id name parentId
1 Item 1 1
2 Item 2 1
3 Item 3 2
4 Item 4 1
5 Item 5 2
6 Item 6 3

The column "parentId" in ListItem points to the List that the item belongs to. When I run the SQLite query below...

    WITH RECURSIVE x (id, name, parentId, totalNumberOfItems)
    AS (
    SELECT ID, Name, parentId, 
           (SELECT COUNT(*) FROM listitem WHERE parentId = list.id) 
           as totalNumberOfItems
    FROM List 
    WHERE parentId is NULL
    UNION ALL
    SELECT e.id, e.name, e.parentId, 
          (SELECT COUNT(*) FROM listitem WHERE parentId = e.id) 
          as totalNumberOfItems
    FROM list e INNER JOIN x on e.parentId = x.ID
    )
    SELECT * FROM x

Then I get the following result:

enter image description here

Below is a script that allows to create the Db-Schema and insert the data.

BEGIN TRANSACTION;
CREATE TABLE IF NOT EXISTS "List" (
    "id"    INTEGER NOT NULL,
    "name"  TEXT NOT NULL,
    "parentId"  INTEGER,
    PRIMARY KEY("id" AUTOINCREMENT)
);
CREATE TABLE IF NOT EXISTS "ListItem" (
    "id"    INTEGER NOT NULL,
    "name"  TEXT NOT NULL,
    "parentId"  INTEGER NOT NULL,
    PRIMARY KEY("id" AUTOINCREMENT),
    FOREIGN KEY("parentId") REFERENCES "List"("id")
);
INSERT INTO "List" ("id","name","parentId") VALUES (1,'List1',NULL),
 (2,'List2',1),
 (3,'List3',2);
INSERT INTO "ListItem" ("id","name","parentId") VALUES (1,'Item1',1),
 (2,'Item2',1),
 (3,'Item3',2),
 (4,'Item4',1),
 (5,'Item5',2),
 (6,'Item6',3);
COMMIT;

Solution

  • Use a recursive CTE that returns the ids of the list that you want and all its children below up the lowest level.

    Then aggregate in the table ListItem by using the results of that query:

    WITH cte AS (
      SELECT id FROM List WHERE name = 'List1' 
      UNION ALL
      SELECT l.id
      FROM List l INNER JOIN cte c
      ON l.parentId = c.id
    )
    SELECT COUNT(*) count FROM ListItem WHERE parentId IN cte;
    

    See the demo.