Forum OpenACS Q&A: Using math with CONNECT BY

Collapse
Posted by Jade Rubick on
I have a complex (and perhaps interesting) SQL question for you all:

Let's say I have an INGREDIENT table for recipes, that looks something like this:

INGREDIENT
id               primary key,
name             varchar(200),
carbohydrates_g  number
Ingredients can also be composed of several other ingredients. For example, let's say you have a pancake mix that is composed of flour, eggs, and salt.
MIX
master_ingredient    references INGREDIENT,
component_ingredient references INGREDIENT,
percent              number
Thus, an entry for this pancake mix would be in INGREDIENT:
id 5
name "pancake mix"
carbohydrates 0
The pancake mix has three components, so there are three entries in the MIX table:
master_ingredient      5  
component_ingredient   3 (let's say three is water)
percent               33 (let's say the mix is 33% water)

master_ingredient      5  
component_ingredient   2 (let's say two is flour)
percent               20 (let's say the mix is 20% flour)

master_ingredient      5  
component_ingredient   1 (let's say one is salt)
percent               47 (let's say the mix is 47% salt)
It's not terribly difficult to figure out what the total carbohydrate content for this pancake mix is, but what if a mix contains another mix? It becomes a recursive endeavor.

I could do this with Tcl, and just sum everything up, but I imagine a better solution might be found using SQL, and perhaps using CONNECT BY. I've never used it for summing things up in a tree before, however.

Sql for Smarties has a section that discusses this. In his example, you have parts for a machine. Each of the parts is composed of component parts, and the weight of the part is based on the sum of the weight of the components. He figures it out iteratively, by going through a table and saying component A's weight = sum of all child components. It doesn't really seem very ideal.

Has anybody encountered this issue before, and solved it satisfactorily?

Collapse
Posted by Jade Rubick on
On further reflection, another possibility would be to figure out the carbohydrates whenever the mix is changed. Whenver a mix was changed, you'd have to change the carbohydrate levels of all everything based on it.

Alternatively, if you were using the Sql for Smarties example, then every time you changed a part's weight, you would change the weight of all parts that include that component part. You would then store the weight in each part, so you could query it quickly.

This seems like it would be much more efficient, but perhaps more liable to error. If seems more likely to end up with bad data, so perhaps you'd need to completely regenerate the weights (or carbohydrates) occasionally...

Collapse
Posted by Jonathan Ellis on
with properly defined triggers there's no reason you should end up with bad data.  as long as your number of updates is low compared to reads you don't really have to worry how efficient the actual update computation is.
Collapse
4: triggers firing triggers (response to 1)
Posted by Jade Rubick on
Thanks for your help Jonathan.

Do triggers set off other triggers recursively?

Using the Sql for Smarties example, if I have parts A, B, C, D, and E, and some of the parts are composed of other parts like so:

    A
   / 
  B   C
     / 
    D   E
So part A is composed of part B and C, and part C is composed of D and E.

Let's say I have a trigger on the PARTS table which updates the parent parts' weight.

If I update D's weight and add 5 kilos to it, then part C and A should also go up 5 kilos. Would a trigger on the PARTS table which updated the parent part fire twice? Would it hang if there were loops? The Oracle8: The Complete Reference book doesn't seem to have any information on this that I can find.

Collapse
Posted by Jonathan Ellis on
yes, updates performed by triggers will fire other (including "recursive") triggers just as any other update would.

since you're dealing with a tree structure (no cycles) this means your job is pretty simple.  doing something like

update table set weight = weight + (new.weight - old.weight) where id = new.parent_id

should cover you (allowing reparenting of nodes would complicate things some, but you could check for and handle that if you needed to).  top-level nodes should have parent_id = null so an update there will result in zero rows updated (by the trigger) and no subsequent triggers firing.  similarly for your ingredients/mix tables, your "top level" incredients will not exist as components in the mix table.

Collapse
Posted by Jade Rubick on
Jonathan, thank you again for your help. For posterity, I'm going to write about a problem I ran into here. I searched the bboard and found no mention of this, so I thought I'd write a little bit about it:

I set up two tables, FOO and BAR, as follows:


SQL> create table foo (
  2  id integer primary key,
  3  weight number
  4  );

Table created.

SQL> create table bar (
  2  foo_id references foo,
  3  child_id references foo
  4  );

Table created.

SQL> select * from foo;     

	ID     WEIGHT
---------- ----------
	 1	   35
	 2	   15
	 3	   20
	 4	   10
	 5	   10

SQL> select * from bar;

    FOO_ID   CHILD_ID
---------- ----------
	 1	    2
	 1	    3
	 3	    4
	 3	    5

Thus, the two tables describe a tree structure like this:
    #1=35kilos
     /    
 #2=15k   #3=20k
          /   
       #4=10k #5=10k

The weight (in kilos) is a sum of the children nodes.

If I add 5k to #4, it should also add 5k to #3 and #1. So I write my trigger (I am pretty inexperienced at this):

SQL> create or replace trigger foo_trigger
  2  before update on foo
  3  for each row
  4  begin
  5    update foo set weight = weight + (:new.weight - :old.weight) 
  6    where id = (select foo_id from bar where child_id = :old.id)
  7    ;
  8  end;
  9  /

Trigger created.

SQL> update foo set weight = 20 where id = 4;
update foo set weight = 20 where id = 4
       *
ERROR at line 1:
ORA-04091: table INTRANET.FOO is mutating, trigger/function may not see it
ORA-06512: at "INTRANET.FOO_TRIGGER", line 2
ORA-04088: error during execution of trigger 'INTRANET.FOO_TRIGGER'


Welcome to the world of mutating tables. I found a good resource on this at http://www.cs.purdue.edu/homes/mcclure/cs448/info/trigger.html.

Tomorrow, I'll continue working on this. If anyone has some quick pointers, I would of course appreciate it. The manuals and books don't seem to be helping here.

Collapse
Posted by Yonatan Feldman on
jade, use global temporary tables to temporarily store values and get around the mutating table problem.
Collapse
Posted by Sebastiano Pilla on
There is also a page by Tom Kyte about the mutating table error that's worth reading.
Collapse
Posted by Tom Jackson on

I would start with three tables:

ingredients
 ingredient_id
 recipe_id references recipes default null
 name
 carb_count_per_measure
 measure_id

recipes
 recipe_id
 name
 general_instructions
 expected_yield
 measure_id

recipe_ingredients
 recipe_id ref recipes
 ingredient_id ref ingredients
 quantity
 measure_id
 add_order
 add_instructions

Although an ingredient may be composed of other ingredients, it still will need a recipe. It also isn't obvious that once a recipe is prepared, that all the carbs, fat, etc. that went into making the recipe make it through. For example, water may evaporate or drain away, salt could be washed off, so totalling up the nodes may not represent the final product.

A recipe can be used to calculate the cost of the final product, or a shopping list, but if an ingredient is created by using a recipe, that new ingredient should have it's own carb_count, etc.

Now the connect by problem moves to the calculation of the shopping list, but I believe you would be better served by moving this calculation into a procedure.

Here is an example: Pasta Inc. creates a dried pasta product. Their recipe will include a little water and semolina. Most of the water will evaporate during processing, some of the semolina will be lost during the manufacturing process (shrinkage). The final product will be an ingredient in their retail store's biggest seller: Cooked Pasta. The recipe for this product will consist of quite a lot of water and salt. Most of this will be lost, some of the carbs will dissolve in the salt water that is drained away. The final weight and carb count will depend on the length of cooking the pasta. Cooked pasta will be an ingredient to the final product: Pasta with your favorite sauce. Yumm.