On rules and relations

March 13, 2008

Having designed and built a number of rules engines over the years, I’ve come to realize that rules and relational databases just don’t get along. I thought I might explore why that is, and, given the shotgun wedding of the two demanded by Oracle’s dominion and the emerging demand for business rule automation, suggest some ways of working around it.

Rules are sentences. Remember the other name for the propositional calculus is the sentential calculus.

So rules are a kind of language. And one of the defining features of language is that its structure is recursive: bigger units are built up by combining similar smaller units, which themselves may be combinations of even smaller similar units, and so on.

The first problem with putting rules in databases is that databases don’t like recursive data. They especially don’t like recursion (think self-join) to an unknown-ahead-of-time or varying depth.

The second problem is that our rules’ literals, the ‘variables’ which are bound at evaluation, usually come from the database. So there is a dependency between our rule structure and the database structure. There is a modeling-level problem here: a rule stored as data has a structural dependency on the level of structure that contains the data.

So storing rules in a relational database makes them difficult to work with, and difficult to maintain. (I confess I have not looked at how other people’s rules engines such as Drools have addressed this problem. I am inspired to do so now, and will add to this thread with what I find out.) [see comment below - Max.]

There are a couple of practical things that will help.

The first is something called disjunctive normal form. This is something well known in the realm of automated theorem proving. In disjunctive normal form, or DNF, a formula is expressed as a disjunction of conjunctions. ‘Not’ is only allowed to bind to literals. So these are valid formulas in DNF, assuming ‘A’, ‘B’, and ‘C’ are literals. I’ll use a v for ‘or’, a ‘^’ for and, and a ‘!’ ‘not’:

  • A
  • !A
  • A ^ B
  • (A ^ B) v C
  • (A ^ !B ^ C) v (!B ^ A)

And so on.

DNF has some interesting and useful features.

The first is that it lends itself to conditional evaluation. You can determine the truth value of the entire formula very efficiently by simply starting at the leftmost literal and evaluating to the right. The first false literal you reach in a conjunction makes the whole conjunction false, so you can skip to the next conjunction. And the first disjunction that is true makes the whole formula true.

The second useful feature of DNF is that it consists of only a few elements in a very regular structure, making it relatively easy to model.

The third useful feature of DNF is the ‘resolution principle’: (p ^ q) v (!p ^ r) –> q v r is a tautology, so we can replace the left side with the right before we evaluate the rule. There are other similar simplifications that can be applied to make evaluation more efficient (these are only two examples):

  • p v ( p ^ q) –> p
  • ( p ^ q) v ( p ^ r) –> p ^ ( q v r )

The fourth useful feature of DNF, and the most suprising, is that it has all the expressive power of the full propositional calculus. Any WFF of the propositional calculus can be mechanically translated into DNF. If we combine this with the substitution strategy we just visited, we come up with a proof strategy:convert to DNF, then substitute until you reach a tautology. In a rules engine we can follow the same path until we run out of substitutions, then we bind our literals to our domain and evalute. (One caution – if you start with something in the inverse form, Conjunctive Normal, and convert it, you can end up with an extremely large – think exponentially large – DNF formula.)

So rules (or just their antecedents if our model is logical antecedent->consequent action) can be stored and evaluated efficiently in DNF. Translating from the full PC can be a challenge. In practice, business users don’t use material implication in business rules, so constraining the UI to DNF is not particularly onerous. UI designers and builders will appreciate the simplicity and regularity of the form.

So lets say you’ve followed my advice, and have a database for storing DNF, with relational expressions (like schema.table.column >= scalar value) as the logical literals. (I will flesh this out sometime with a pseudocode example.)

How do you query a rule from the database, when you don’t know how deeply nested it is?

The answer is to use Oracle’s ability to do hierarchical queries. Hierarchial queries appeared in Oracle 8i (don’t quote me) and have been improved in subsequent versions. Hierarchical queries permit querying n-deep self joins efficiently in a single SELECT, where parent->child rows (and->grandchild rows, etc, to whatever level of nesting is present) are returned as consecutive rows.

Getting our rules out of the database is reduced to executing a hierarchical query, then iterating over the result set to build up the rule from its parts.

Addressing the structural dependency problem requires bridging the modeling level gap. One effective approach to managing this is to have the schema name, table name, column name, data type and size part of the record of the literal. Then it is a simple matter to query the data dictionary to determine if the schema->table->column->type->size is valid and raise an exception if a database structure change has broken rules.

This will all be a lot more clear with some explicit examples. I’ll try to add them soon.

6 Responses to “On rules and relations”

  1. Arnoud Says:

    The interesting part is that relational databases are based on the same propositional calculus, or predicate logic. Predicate logic combined with set theory becomes a very powerful base to come up with good database designs. Recall that a tuple is nothing more than a proposition that evaluates to TRUE for the predicate defined through the relation. (http://www.amazon.com/Applied-Mathematics-Database-Professionals-Experts/dp/1590597451/ref=pd_bbs_sr_2?ie=UTF8&s=books&qid=1207856850&sr=8-2)

    As to the hierarchical query approach, the SQL standard now supports the WITH clause which allows for hierarchical processing. Not as clear or easy, but a far wider implementation base. (http://gennick.com/with.html)

  2. Arnoud Says:

    Being new to the rules engine area, I’m somewhat confused by the recursive quality you describe. Can the antecedent contain a reference to its own consequent, either directly or indirectly? I would qualify that as an invalid rule…

    I can see rules chained in an hierarchy of unknown depth which, as you point out, could indeed be stored “recursively” through a self-join in a rule table.

    Thinking out loud:
    Why store all rules in a single table, why not make a table per rule? As a table is a rule: the primary key is the antecedent and the remaining attributes are the consequent.

    That would be a large number of tables, but as we are not going to store any data in the table, other than a single row per rule evaluation, there should be no issue there.

    Say we have the following rule:
    IF age > 65 THEN discount = 20%

    We would have two tables: one to hold the variables, another to define the rule.

    -- Create table
    create table RULE_DATA
    (
    AGE NUMBER(4),
    DISCOUNT NUMBER(7,3)
    );

    -- Create table
    create table SENIOR_DISCOUNT
    (
    AGE NUMBER(4) not null,
    DISCOUNT NUMBER(7,3) default .2
    );
    -- Create/Recreate primary, unique and foreign key constraints
    alter table SENIOR_DISCOUNT
    add constraint SENIOR_DISCOUNT_ANTECEDENT primary key (AGE);
    -- Create/Recreate check constraints
    alter table SENIOR_DISCOUNT
    add constraint SENIOR_AGE
    check (age>65);

    Now we receive data to assess the rules. (I’m purposely not calling this forward or backward chaining, as I have no clue where this is going yet)

    INSERT INTO rule_data VALUES (60,NULL);
    COMMIT;

    Now for assessing the rule:
    MERGE INTO senior_discount trgt
    USING (SELECT age FROM rule_data) src
    ON (src.age = trgt.age)
    WHEN NOT MATCHED THEN
    INSERT (trgt.age) VALUES (src.age)

    ORA-02290: check constraint (DEVELOPMENT.SENIOR_AGE) violated

    This is good, the rule correctly failed, thus no answer.

    Now, let’s change our input:
    DELETE FROM rule_data WHERE 1=1;

    1 row deleted
    COMMIT;

    Commit complete
    INSERT INTO rule_data VALUES (73,NULL);

    1 row inserted
    COMMIT;

    Commit complete
    MERGE INTO senior_discount trgt
    USING (SELECT age FROM rule_data) src
    ON (src.age = trgt.age)
    WHEN NOT MATCHED THEN
    INSERT (trgt.age) VALUES (src.age);

    Done
    COMMIT;

    Commit complete
    SELECT * FROM senior_discount;

    AGE DISCOUNT
    ----- ---------
    73 0.200

    OK, rule succeeded, and result is documented. Now make it available for other rule evaluations:

    UPDATE rule_data rd SET rd.discount = (SELECT sd.discount FROM senior_discount sd);

    1 row updated
    COMMIT;

    Commit complete
    SELECT * FROM rule_data;

    AGE DISCOUNT
    ----- ---------
    73 0.200

    Ready to process the next set of rules.

    My current thoughts are that the MERGE as described above would be executed for all rules available, with all data available. Repeating until the merges do not insert any rows.

    Obviously optimizations are possible, and most likely are required for performance. The core is based on two principles which relational theory is grounded in: predicate logic and set theory.

    Obviously this does not address conflicting rules, and other rule engine intricacies that are beyond my rules engine knowledge.

    I will have to noodle on this some more before it will leave me alone.

  3. Max Says:

    There are a lot of separate threads in your post. Let’s tease them out, treat them (in discrete posts), and then go back to the loom.

    First, we need to understand that there is a discrete domain, a universe, for a body of rules, which contains the literal values that are plugged into the variables – which are bound – in our rules at runtime.

    Rule antecedents, generally written in a logical expression grammar, frequently constrained for facile evaluation, are evaluated and if true trigger actions which can be one of two kinds: actions that change or create new values within the domain, or actions that don’t.

    Either kind can also trigger events outside the rule domain – which is really the point, of course. If our rules engine has no contact with the outside world, why build it. But in a way these actions can be seen as a side effect.

    Some rules engines have flow-of-control constructs as well, which edge into procedural evaluation.

    But that seems orthogonal to the fundamental parallel evaluation model of a rules engine.

    The fundamental approach is more akin to game programming.

    A turn is triggered by a change to our domain.

    In a given turn we (for all our rules in parallel) assign values to variables in our antecedents. Any that are completely filled in are evaluated. If more than one is true, we take the set of true rules and see if the consequents are in conflict. If so, we resolve the conflict, which may consist of one of more of the consequents not firing, or some of the consequents firing in a particular sequence. (Conflict resolution is one of our discrete threads.)

    According to our conflict resolution we fire our antecedents.

    Next turn.

    The familiar Rete algorithm makes this process more efficient by not evaluating a rule in turn n+1 if the rule was evaluated in turn n and no value has changed which might cause the rule to evaluate differently.

    More tomorrow…

  4. Max Says:

    There is an important distinction to draw here. In the propositional calculus there is an operator called ‘material implication’ which is usually read as an ‘if then’ statement. If a then b, formally written as a -> b or a u-on-its-side-open-to-the-left. The left hand side of the expression is called the antecedent, the right hand side the consequent.

    The important thing to note is that this is not a causal statement. a -> b does not mean a causes b to happen. It simply means the entire expression is true unless a is true and b is false. It is another way of writing not a V b – their truth tables are the same.

    As I mentioned above, a rules engine has to act. Values are set, actions are triggered as a consequence of rules evaluating to true.

    So when I talk about the antecedent and consequent at a high level above, material implication, if present, is all in the antecedent. Whatever our consequent is happens if the antecedent is true.

  5. Max Says:

    Arnoud, the mechanism you described above would certainly allow the use of the database engine as a rules engine (assuming some action-producing mechanism – using Dynamic SQL perhaps – were grafted on). Though as you point out, it might not be very efficient. Rete and other similar algorithms speak to the computational challenges around large rule sets.

    But our original issue was around the challenge in storing rules as data. Our goal was to have the rules as data, so that the body of rules was maintainable by users through some sort of application front-ending our database. Since relational databases are predominate today – MySQL, PostGRES, and Derby apps are popping up like dandelions in a tweaker’s front yard – we are looking at the rule to relational db mapping.

    Your solution approach pushes the rules proper into check constraints. So now we would be faced with having our front-end app maintain a body of check constraints in the database. Which would be a language-parsing based task. Not undoable – but not nearly as developer friendly as a database – and it would make it challenging to work with the body of rules as a whole.

  6. Max Says:

    Drools does not store rules in a database. Rules, whose logic is constrained to ANDs and ORs in any pattern – there are no NOTs, which has to be worked around – are persisted either as Drools native rule files, which are text files with a .DRL extension, or as XML.

    The right hand side expression of a Droolish rule is an action to be taken against working memory.

    Here is a sample .DRL rule from the Drools doc:

    rule “Approve if not rejected”
    salience -100
    agenda-group “approval”
    when
    not Rejection()
    p : Policy(approved == false, policyState:status)
    exists Driver(age > 25)
    Process(status == policyState)
    then
    log(“APPROVED: due to no objections.”);
    p.setApproved(true);
    end

    The XML flavor is, as one would expect, much more verbose.

    The XML could be mapped into a relational database in a straightforward fashion. Which moves the rule to relation db mapping issue into the XML to relational DB space. Wherein reside the same core issues – recursive language constructs to relational db mapping – with the special sauce of XPATHs to spice things up.


Leave a Reply