Fill and query a decision table with defaults and exceptions. This article is based on a post in OTN:
In the course of time a piece of code that was used to take decisions programmatically grew into a mess of multiple nested IF statements. We decided to implement it as a decision table.
Because there are more than 20,000 possible combinations of the attributes, a table that holds each of them is out of question, nobody would be able to maintain it.
Here is a simplified example with clients which each can be in different regions, one attribute that can be Yes or No and one attribute that is derived from many other properties and there is no table that has every possible value of this attribute (I know there should be a table for this instead of hard coding them but that is another story).
The decision table is hierarchical meaning that each column carries more weight than the columns to the right.
The result is either Y (YES) or N (NO)
DROP VIEW ranked_decisions; DROP TABLE decisions; DROP TABLE clients; DROP TABLE regions; CREATE TABLE clients ( client_id INTEGER NOT NULL ,client_name VARCHAR2 (100) ) ; ALTER TABLE clients ADD CONSTRAINT cli_pk PRIMARY KEY ( client_id ) ; CREATE TABLE regions ( region_id INTEGER NOT NULL ,region_name VARCHAR2 (100) ) ; ALTER TABLE regions ADD CONSTRAINT reg_pk PRIMARY KEY ( region_id ) ; CREATE TABLE decisions ( client_id INTEGER NOT NULL ,region_id INTEGER ,dec_yn_1 VARCHAR2 (1 CHAR) ,dec_attr_1 VARCHAR2 (10 CHAR) ,dec_result VARCHAR2 (1 CHAR) ) ; ALTER TABLE decisions ADD CONSTRAINT dec_cli_fk FOREIGN KEY ( client_id ) REFERENCES clients ( client_id ) ; ALTER TABLE decisions ADD CONSTRAINT dec_reg_fk FOREIGN KEY ( region_id ) REFERENCES regions ( region_id ) ; INSERT INTO clients (client_id,client_name) VALUES (1,'A'); INSERT INTO clients (client_id,client_name) VALUES (2,'B'); INSERT INTO clients (client_id,client_name) VALUES (3,'C'); INSERT INTO clients (client_id,client_name) VALUES (4,'D'); INSERT INTO clients (client_id,client_name) VALUES (5,'E'); INSERT INTO regions (region_id,region_name) VALUES (10,'BY'); INSERT INTO regions (region_id,region_name) VALUES (11,'BW'); INSERT INTO regions (region_id,region_name) VALUES (12,'HE'); INSERT INTO regions (region_id,region_name) VALUES (13,'TH'); INSERT INTO regions (region_id,region_name) VALUES (14,'HH'); --Default for each client INSERT INTO decisions (client_id,region_id,dec_yn_1,dec_attr_1,dec_result) VALUES ( 1, NULL, NULL, NULL, 'N'); INSERT INTO decisions (client_id,region_id,dec_yn_1,dec_attr_1,dec_result) VALUES ( 2, NULL, NULL, NULL, 'N'); INSERT INTO decisions (client_id,region_id,dec_yn_1,dec_attr_1,dec_result) VALUES ( 3, NULL, NULL, NULL, 'Y'); INSERT INTO decisions (client_id,region_id,dec_yn_1,dec_attr_1,dec_result) VALUES ( 4, NULL, NULL, NULL, 'N'); INSERT INTO decisions (client_id,region_id,dec_yn_1,dec_attr_1,dec_result) VALUES ( 5, NULL, NULL, NULL, 'Y');
CREATE OR REPLACE VIEW ranked_decisions AS WITH all_combinations AS ( --generate each possible combination of the attributes SELECT cli.client_id ,reg.region_id ,yn.yn_1 ,attr.attr_1 ,dc.dec_result -- Ranking of the results -- If there is a result with dc.client_id <> NULL, a value of 8 is associated -- Accordingly for dc.region_id 4, dc.dec_yn_1 2 and for dc.dec_attr_1 1 -- Thus I can be sure that combinations that have a match get a higher -- value than generated combinations ,ROW_NUMBER () OVER ( PARTITION BY cli.client_id,reg.region_id,yn.yn_1,attr.attr_1 ORDER BY CASE WHEN dc.client_id IS NULL THEN 0 ELSE 8 END + CASE WHEN dc.region_id IS NULL THEN 0 ELSE 4 END + CASE WHEN dc.dec_yn_1 IS NULL THEN 0 ELSE 2 END + CASE WHEN dc.dec_attr_1 IS NULL THEN 0 ELSE 1 END DESC ) r FROM clients cli CROSS JOIN regions reg CROSS JOIN ( SELECT 'Y' yn_1 FROM dual UNION ALL SELECT 'N' yn_1 FROM dual ) yn CROSS JOIN ( SELECT 'LA' attr_1 FROM dual UNION ALL SELECT 'M' attr_1 FROM dual UNION ALL SELECT 'SR' attr_1 FROM dual UNION ALL SELECT 'N' attr_1 FROM dual ) attr JOIN decisions dc ON ( (cli.client_id = dc.client_id OR dc.client_id IS NULL) AND (reg.region_id = dc.region_id OR dc.region_id IS NULL) AND (yn.yn_1 = dc.dec_yn_1 OR dc.dec_yn_1 IS NULL) AND (attr.attr_1 = dc.dec_attr_1 OR dc.dec_attr_1 IS NULL) ) ) SELECT ac.client_id ,ac.region_id ,ac.yn_1 ,ac.attr_1 ,ac.dec_result FROM all_combinations ac WHERE r = 1; SELECT dec_result FROM ranked_decisions WHERE client_id = 4 AND region_id = 14 AND yn_1 = 'N' AND attr_1 = 'LA';
Of course we get a 'N'.
Let's add some exceptions
--Default for region HH is Y for client A and D INSERT INTO decisions (client_id,region_id,dec_yn_1,dec_attr_1,dec_result) VALUES ( 1, 14, NULL, NULL, 'Y'); INSERT INTO decisions (client_id,region_id,dec_yn_1,dec_attr_1,dec_result) VALUES ( 4, 14, NULL, NULL, 'Y');
Now we get an 'Y' for the same query
Other exceptions might be
-- Client A is Y if region is BY INSERT INTO decisions (client_id,region_id,dec_yn_1,dec_attr_1,dec_result) VALUES ( 1, 10, NULL, NULL, 'Y'); --except if attribute 1 is LA INSERT INTO decisions (client_id,region_id,dec_yn_1,dec_attr_1,dec_result) VALUES ( 1, 10, NULL, 'LA', 'N'); -- Client D is Y if YN-attribute is Y INSERT INTO decisions (client_id,region_id,dec_yn_1,dec_attr_1,dec_result) VALUES ( 4, NULL, 'Y', NULL, 'Y');
Our decision Table has the following content
CLIENT_ID REGION_ID D DEC_ATTR_1 D ----------- ----------- - ---------- - 1 N 2 N 3 Y 4 N 5 Y 1 14 Y 4 14 Y 1 10 Y 1 10 LA N 4 Y Y