正味を決定する pl/pgsql 関数

商品の出荷にあたって、価格からどれだけの値引きを行うかということは、業務上様々な条件があって、かつそれが固定的でなく変動することもある。いろいろな方法を試してみましたが、現在は以下の方法でやっています。

1 正味を決定する条件を羅列するテーブルを用意
  このテーブルに、条件となる項目を列として用意しておいて、その条件のプライオリティをしめす列を付けておく。
2 条件となる各列は、条件が適用されない場合はナルにしておく。
3 条件を網羅して渡す pl/pgsql 関数を作成。条件網羅テーブルをプライオリティ逆順でループして最初に全ての条件を満たした行の適用正味を返す。
4 pl/pgsql 関数にしておくことで、webアプリからでも、C/S系のクライアントアプリからでも常に同じ結果が得られ、正味適用条件が変わった場合、テーブルの内容を変えればそれで済む。

具体的なテーブル例

CREATE TABLE ma_rate_decision_tbl
(
id serial NOT NULL,
created timestamp without time zone NOT NULL DEFAULT now(),
modified timestamp without time zone NOT NULL DEFAULT now(),
"createuser" text NOT NULL DEFAULT "current_user"(),
updateuser text NOT NULL DEFAULT "current_user"(),
voucher_type text,
product_type text,
wholesaler_group text,
wholesaler_code text,
bookstore_group text,
bookstore_code text,
category_id integer,
conditional_type text,
conditional_pattern text,
min_price integer,
max_price integer,
product_code text,
priority integer NOT NULL,
apply_rate numeric NOT NULL,
memo text NOT NULL, -- 説明書き
CONSTRAINT ma_rate_decision_tbl_pkey PRIMARY KEY (id)
);
COMMENT ON TABLE ma_rate_decision_tbl IS '正味を算出する計算用テーブル.この表は、列がNULLであることを積極的に利用している';

それで、正味を取得したい場合利用する関数の例

CREATE OR REPLACE FUNCTION get_apply_rate(IN r_voucher_type text, IN r_wholesaler_code text, IN r_bookstore_code text, IN r_conditional_pattern text, IN r_price integer, IN r_product_code text, OUT o_rate numeric, OUT o_priority integer, OUT o_sd integer)
RETURNS SETOF record AS
$BODY$
DECLARE
r_tbl ma_rate_decision_tbl%ROWTYPE;
matchcnt integer;
querycnt integer;
p_product_type text;
p_category_cnt integer;
p_conditional_type text;
BEGIN
--対照表のレコードでループ
for r_tbl in select * from ma_rate_decision_tbl order by priority DESC,id DESC
LOOP
matchcnt :=0;
querycnt :=0;

-- 第1関門 伝票区分は同じか
if r_tbl.voucher_type is not null then
querycnt := querycnt +1;
end if;
if r_tbl.voucher_type is not null and r_tbl.voucher_type = r_voucher_type then
matchcnt := matchcnt + 1;
end if;


--第2関門
if r_tbl.wholesaler_code is not null then
querycnt := querycnt +1;
end if;
if r_tbl.wholesaler_code is not null and r_tbl.wholesaler_code = r_wholesaler_code then
matchcnt := matchcnt + 1;
end if;



--第3関門 パターン区分が同じか
if r_tbl.conditional_pattern is not null then
querycnt := querycnt +1;
end if;

if r_tbl.conditional_pattern is not null and r_tbl.conditional_pattern = r_conditional_pattern then
matchcnt := matchcnt + 1;
end if;



--第4関門 商品区分は同じか
if r_tbl.product_type is not null then
querycnt := querycnt +1;
RAISE WARNING 'stage 4 start';
end if;

-- r_product_code に2重の意味を与えている商品区分と商品番号そのもの
if r_tbl.product_type is not null and r_tbl.product_type = r_product_code then
matchcnt := matchcnt + 1;
else

select into p_product_type product_type from .ma_publisher_products
where .ma_publisher_products.product_code = r_product_code;

if r_tbl.product_type is not null and r_tbl.product_type = p_product_type then
matchcnt := matchcnt + 1;
end if;
end if;

--第5関門 カテゴリーIDは同じか
if r_tbl.category_id is not null then
querycnt := querycnt +1;
end if;

select into p_category_cnt count(*) from .ma_product_categories
where .ma_product_categories.product_code = r_product_code
and .ma_product_categories.category_id = r_tbl.category_id;

if r_tbl.category_id is not null and p_category_cnt >=1 then
matchcnt := matchcnt + 1;
end if;


--第6関門 conditional_typeは同じか
if r_tbl.conditional_type is not null then
querycnt := querycnt +1;
end if;

select into p_conditional_type conditional_type from .ma_conditionals
where .ma_conditionals.conditional_pattern = r_conditional_pattern;

if r_tbl.conditional_type is not null and r_tbl.conditional_type = p_conditional_type then
matchcnt := matchcnt + 1;
end if;


--第7関門 本体価格の min max 両方
if r_tbl.min_price is not null and r_tbl.max_price is not null then
querycnt := querycnt +1;
end if;

if r_tbl.min_price <= r_price and r_price <= r_tbl.max_price then
matchcnt := matchcnt + 1;
end if;


--第8関門 本体価格の minのみ つまり 円以上
if r_tbl.min_price is not null and r_tbl.max_price is null then
querycnt := querycnt +1;
end if;


if r_tbl.min_price <= r_price then
matchcnt := matchcnt + 1;
end if;


--第9関門 本体価格の maxのみ つまり 円以下
if r_tbl.min_price is null and r_tbl.max_price is not null then
querycnt := querycnt +1;
end if;


if r_price <= r_tbl.max_price then
matchcnt := matchcnt + 1;
end if;


--第10関門 商品コードは同じか
if r_tbl.product_code is not null then
querycnt := querycnt +1;
end if;


if r_tbl.product_code = r_product_code then
matchcnt := matchcnt + 1;
end if;




if matchcnt >= querycnt then
RAISE WARNING ' all ok qc % and mc %',querycnt,matchcnt;
exit;
end if;
end LOOP;




o_rate:=r_tbl.apply_rate;
o_priority :=r_tbl.priority;
o_sd :=r_tbl.id;
RETURN NEXT ;
END$BODY$
LANGUAGE plpgsql VOLATILE
COST 100
ROWS 1000;
ALTER FUNCTION get_apply_rate(text, text, text, text, integer, text) OWNER TO postgres;
COMMENT ON FUNCTION get_apply_rate(text, text, text, text, integer, text) IS '適用する正味を返す';


テーブルにはできるだけナルを残さないのが善とされているみたいですが、この表はナルであることを利用しています。
sql的じゃない感じで、実行効率とかも悪いと思われるが、実務上必要十分なので良し(入力者に手打ちで正味を入力してもらっているよりずっと良し)としています。
もっとエレガントな解決方法あるのかしら?