Frage

(Using Postgresql 9.1)

I have a tree structure in the database and I need to sum the node's values. There are two caveats:

  1. Not all nodes have a value.
  2. If a parent node has a value, ignore the child values.

While recursing the tree is easy with the powerful recursive WITH clause, it's enforcing these two caveats that is breaking my code. Here's my setup:

CREATE TABLE node (
  id VARCHAR(1) PRIMARY KEY
);

INSERT INTO node VALUES ('A');
INSERT INTO node VALUES ('B');
INSERT INTO node VALUES ('C');
INSERT INTO node VALUES ('D');
INSERT INTO node VALUES ('E');
INSERT INTO node VALUES ('F');
INSERT INTO node VALUES ('G');

CREATE TABLE node_value (
  id VARCHAR(1) PRIMARY KEY,
  value INTEGER
);

INSERT INTO node_value VALUES ('B', 5);
INSERT INTO node_value VALUES ('D', 2);
INSERT INTO node_value VALUES ('E', 0);
INSERT INTO node_value VALUES ('F', 3);
INSERT INTO node_value VALUES ('G', 4);

CREATE TABLE tree (
  parent VARCHAR(1),
  child VARCHAR(1)
);

INSERT INTO tree VALUES ('A', 'B');
INSERT INTO tree VALUES ('B', 'D');
INSERT INTO tree VALUES ('B', 'E');
INSERT INTO tree VALUES ('A', 'C');
INSERT INTO tree VALUES ('C', 'F');
INSERT INTO tree VALUES ('C', 'G');

This gives me the following tree (nodes and values):

A
|--B(5)
|  |--D(2)
|  |--E(0)
|
|--C
   |--F(3)
   |--G(4)

Given the rules above, here are the expected sum values:

A = (5 + 3 + 4) = 12
B = 5
D = 2
E = 0
C = (3 + 4) = 7
F = 3
G = 4

I have written the following SQL, but I can't integrate the recursive UNION and JOIN logic to enforce rule #1 and #2:

WITH recursive treeSum(root, parent, child, total_value) AS (

  SELECT tree.parent root, tree.parent, tree.child, node_value.value total_value
  FROM tree
  LEFT JOIN node_value ON node_value.id = tree.parent

  UNION

  SELECT treeSum.root, tree.parent, tree.child, node_value.value total_value
  FROM tree
  INNER JOIN treeSum ON treeSum.child = tree.parent
  LEFT JOIN node_value ON node_value.id = tree.parent
)

SELECT root, sum(total_value) FROM treeSum WHERE root = 'A' GROUP BY root

The query returns 10 for root A, but it should be 12. I know the UNION and/or JOIN logic is what's throwing this off. Any help would be appreciated.

EDIT: To clarify, the sum for A is 12, not 14. Given the rules, if a node has a value, grab that value and ignore its children. Because B has a value of 5 we ignore D and E. C has no value, so we grab its children, thus the sum of A = 5(B) + 3(F) + 4(G) = 12. I know it's odd but that's the requirement. Thanks.

EDIT 2: These results will be joined with external datasets so I can't hardcode the root in the WITH clause. For example, I might need something like this:

SELECT root, SUM(total_value) FROM treeSUM GROUP BY root WHERE root = 'A'

This tree is one of many so that means there's multiple roots, specified by calling code--not within the recursive clause itself. Thanks.

EDIT 3: An example of how this will be used in production is the roots will be specified by another table, so I can't hardcode the root into the recursive clause. There might be many roots from many trees.

SELECT id, SUM(COALESCE(value,0)) FROM treeSUM 
INNER JOIN roots_to_select rts ON rts.id = treeSUM.id GROUP BY id

SOLUTION (Cleaned up from koriander's answer below)! The following allows roots to be specified by outside sources (either using roots_to_select or WHERE criteria:

WITH recursive roots_to_select AS (
  SELECT 'A'::varchar as id
),

treeSum(root, id, value) AS (

select     node.id as root, node.id, node_value.value
from       node
inner join roots_to_select rts on (node.id = rts.id)
left join  node_value          on (node.id = node_value.id)

union

select     treeSum.root, node.id, node_value.value 
from       treeSum
inner join tree       on (treeSum.id = tree.parent)
inner join node       on (tree.child = node.id)
left join  node_value on (node.id    = node_value.id)
where      treeSum.value is null
)

select   root, sum(coalesce(value, 0))
from     treeSum
group by root

OUTPUT: 12

War es hilfreich?

Lösung

tested here:

with recursive treeSum(id, value) AS (

select    node.id, node_value.value
from      node
left join node_value on (node.id = node_value.id)
where     node.id = 'A'

union

select     node.id, node_value.value 
from       treeSum
inner join tree       on (treeSum.id = tree.parent)
inner join node       on (tree.child = node.id)
left join  node_value on (node.id = node_value.id)
where      treeSum.value is null
)

select sum(coalesce(value, 0)) from treeSum

Edit 1: to combine the result with other table, you can do:

select id, (select sum(coalesce(value, 0)) from treeSum) as nodesum
from   node
inner join some_table on (...)
where  node.id = 'A'

Edit 2: to support multiple roots based on your Edit 3, you can do (untested):

with recursive treeSum(root, id, value) AS (

select     node.id as root, node.id, node_value.value
from       node
inner join roots_to_select rts on (node.id = rts.id)
left join  node_value          on (node.id = node_value.id)

union

select     treeSum.root, node.id, node_value.value 
from       treeSum
inner join tree       on (treeSum.id = tree.parent)
inner join node       on (tree.child = node.id)
left join  node_value on (node.id    = node_value.id)
where      treeSum.value is null
)

select   root, sum(coalesce(value, 0))
from     treeSum
group by root
Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top