SQLer 生島勘富 のブログ

RDB・SQLの話題を中心に情報発信をしています。

SQLで消費税の処理

こちらで書いた記事のご意見が気になったので少し。

ご意見

gihyo.jp


> 99%は同意するが一つ言わせてほしい。SQLの最大の弱点は時間の扱いに弱いことで、RDBの理論的基盤の数学が時間を考慮してないからしょうがないとはいえ消費税みたいな時間で変化するマスタへの配慮が無さすぎる。

99%は同意するが一つ言わせてほしい。SQLの最大の弱点は時間の扱いに弱いことで、RDBの理論的基盤の数学が時間を考慮してないからしょうがないとはいえ消費税みたいな時間で変化するマスタへの配慮が無さすぎる。 - turanukimaru のブックマーク / はてなブックマーク

とのことですが、Window関数以前は、前後のレコードの処理が考慮されていませんでした。現在は、MySQLでもWindow関数が実装されたので、ほとんどの問題は解決されたと思います。

しかし、消費税率の変更程度であれば、通常はテーブルとデータを整備することで処理が可能です。

テーブル設計における消費税率について

消費税で考慮すべき点は、税率は、3種類(通常、軽減税率、非課税品目)あり、顧客(販売相手)に2種類(課税対象、課税対象外)あることにあります。

税率については、商品のカテゴリーに対して税率種別を付けるべきでしょう。

マスタ類の設計例

f:id:Sikushima:20200609105424p:plain
マスタ類の設計例

トランザクション類の設計例

f:id:Sikushima:20200609105831p:plain
トランザクション類の設計例

消費税率の登録の仕方

f:id:Sikushima:20200609105644p:plain
消費税率の登録の仕方

非課税品目や、消費税がなかった時代のデータまで税率をゼロで登録しておくことがミソです!

SQLで処理するとこうなる

上のようなテーブル設計にして、消費税率を登録しておくと、SQLで処理するときは以下のようになります。

SELECT 
    -- 中略
    , ss.単価 * ss.個数 * tm.税率 * cm.消費税課税区分 AS 消費税額
    -- 中略
FROM 
    売上テーブル sm
    INNER JOIN 売上明細テーブル ss
        ON sm.ID = ss.売上ID
    INNER JOIN 顧客マスタ cm
        ON sm.顧客ID = cm.ID
    INNER JOIN 商品マスタ pm
        ON ss.商品ID = pm.ID
    INNER JOIN 商品分類マスタ pc
        ON pm.商品分類ID = pc.ID
    INNER JOIN 消費税率マスタ tm
        ON sm.売上日 >= tm.適応開始日
        AND sm.売上日 <= tm.適応終了日
        AND pc.消費税区分ID = tm.消費税区分ID
WHERE
    -- 以下略
;

CASE式すら入らないということに注目してください。

ゼロにするときには、ゼロを掛ければ良い

このブログでも、何度か書いていますが、

・ゼロを得たいときにゼロを掛ける
・変化なしを得たいときにゼロを足す、あるいは1を掛ける

などの処理は、自然言語では行われない表現ですが、数式にするときに工夫すべきです。
つまり、仕様を決めるとき、

「顧客が課税対象のときは税率区分の税率を適用し、課税対象外(海外企業など)のときは消費税額をゼロとする」

などという会話がなされます。それをそのままプログラムにすると、とても複雑な処理が必要になります。
消費税課税区分(名前は課税係数の方が良いかも)を0と1にしておけば、

単価 * 個数 * 税率 * 消費税課税区分

という単純な数式で、すべての組合せの処理が可能になります。

トランザクションに税率を入れるか?

税率は通常は導出項目になります。
しかし、税率が変更になる前後で、特別に前の税率で処理して欲しいなどというイレギュラーなことを言い出す可能性があるときには、トランザクションに非正規化しておいた方が良いでしょう。
(売上日を前日にすべきですけどね)