あかね
税率設計から学ぶ実務DB設計
2026年03月12日
要約を生成中...
はじめに
今月頭はDB設計週間でした。
オリジナルアプリの開発でもDB設計は避けて通れないので、実務でDB設計する際に考えたことを振り返ろうと思います。
背景
私の関わっている開発現場ではまだ立ち上げ期ということもあり、機能単位で設計&実装を進めているので、あったりなかったり、あっても違ったりするfigmaを見て、次ここやります〜!(宣言方式w)でDB設計できてんのかな?あ、まだテーブルないねOKって感じでそこから進めるので、DB設計→API→UIという流れで進めています。
基本全部難しいのですが、最近やったのが特にミスるとやばめの消費税周りでした。
最初デザイン段階では商品の価格を税込で登録してもらうの前提でしたが、
税率変わったら詰む(全商品価格変更してもらう?それ現実的?)
合計に対して課税だから個別で税込にすると端数処理大丈夫そ?
この2点を懸念して税抜価格を登録するべきだということで消費税の設計が始まりました。
アンチパターン
というか破綻する設計ですが、
products
- price
- tax_rate一見、商品が価格と税率を持ってるということでよさそうに見えますよね。
問題になること
税率変更
日本では過去にも消費税が何度も変更されています。
5%
8%
10%
この場合
products.tax_rateを変更すると、過去のデータまで変わってしまいます。
軽減税率
日本には軽減税率がありますよね。
イートインではない食料などです。
商品 | 税率 |
|---|---|
食品 | 8% |
酒 | 10% |
外食 | 10% |
つまり
product → tax_rateではなくて
product → 税区分 → 税率という関係性にならないと正しく計算することができません。
商品 → 税率ではなくこの構造にすることで税率変更や軽減税率に対応することができます。
設計で考慮したこと
次の4つを考えました。
税区分
商品カテゴリごとに税区分が決まります。
STANDARD //標準税率(10%)
REDUCED //軽減税率(8%)税率履歴
基本DB上に履歴は残すのが鉄則です。
過去のデータがわからない設計は必ず困る時がくるはずです。
税率は変更されるため履歴を持つ必要があります。
tax_category | rate | effective_from | effective_until |
|---|---|---|---|
STANDARD | 8 | 2014 | 2019 |
STANDARD | 10 | 2019 | null |
effective_untilがnullなら現行有効と判断します。
税率が改定された時は、データを上書きするのではなく、effective_untilに終了日を追加して、新たなレコードを追加します。
地域
国によって税率が異なる場合があります。
今回はシンプルに日本だけを対象としますが、実際は海外にも対応できる拡張性を考慮しつつYAGNI(必要になった時に追加するという原則)であることを考えて設計しました。
インボイス制度
私は税周りの知識は多少ありますが、追ってなくてあまり知らなかったのでどういう対応しておかないといけないか調査から行いました。
AIに聞く、国税庁のサイトで裏とるという流れで簡単に理解できてほんといい時代だと思いました。
結果気を付けるべきことは、
1回の注文時の税率で端数処理していいのは一回だけ(個別に端数処理禁止)
請求書に税率ごとに表示する必要がある(8%と10%の合算禁止)
この2点です。
つまりDB設計をする上では一つの注文に税別合計額と税率と税額を複数紐付けられるようにしないといけない(1対多になる)ということになります。
設計したテーブル
以下のように設計しました。
erDiagram
AREAS ||--o{ TAX_RATES : has
AREAS ||--o{ CATEGORY_TAX_CLASSIFICATIONS : scoped
PRODUCT_CATEGORIES ||--o{ CATEGORY_TAX_CLASSIFICATIONS : classified
ORDERS ||--o{ ORDER_TAX_LINES : has
// 税率の履歴
TAX_RATES {
bigint id
bigint area_id
enum tax_category
decimal rate
date effective_from
date effective_until
}
// 商品カテゴリーごとの税率管理
CATEGORY_TAX_CLASSIFICATIONS {
bigint id
bigint product_category_id
bigint area_id
enum tax_category
boolean tax_override //お酒のような食品(8%)でありながら10%になる時に優先することを示すフラグ
}
// 1注文・税率ごとの税額
ORDER_TAX_LINES {
bigint id
bigint order_id
enum line_type
enum tax_category
decimal rate
integer taxable_amount
integer tax_amount
}以下のような紐付きをしています。
商品カテゴリ → 税区分
税区分 → 税率履歴
注文 → 税のスナップショット
我ながら綺麗に正規化できていると思います☺️
スナップショット
なぜ注文時税率を保存するかということなのですが、税率が変わるからです。
FKで参照してしまうと元データの変更の影響を受けてしまいますが、注文した時点のデータをあとから見たときに、変わっていたらおかしいですよね?
注文情報なのに1100円だったのが1年後見たら1080円に変わってるなんてことがあったら明らかにおかしいです。
履歴として管理するときはその時の情報をスナップショットとしてそのまま保存します。
実際この設計だと税率が変わったら別のレコードを追加するので参照しても変わることはないのですが、そういう問題ではなく、変わるとおかしいものは履歴として管理です。
まとめ
サラッとだったのですが、この設計には以下の設計の本質みたいなことが含まれています。
正規化→責務分離
履歴管理→時間軸データ
参照整合性→カテゴリ → 税区分
将来拡張→国追加/税率変更
大原則なところでもあるので、これからDB設計をする方にきっと何か参考になる部分があるかなと思います!!!
おわりに
私は結構DB設計するの好きで、DB設計からのタスクをすることも少なくないですがいつも楽しく頭を悩ませていますw
CTOのレビューで新たな設計の原則みたいなことを学ぶこともありこの1年弱で観点がすごく増えました。本当にありがたい環境にいます、、
ちなみに前職でkintoneのアプリを山ほど作ってきたのですが、あれは完全にデータモデリングであり、1アプリ1テーブルで正しく正規化して設計しないと詰むノーコードのSaasなんですよね。。。
DB設計としてはイケてない設計にした結果たくさんのアンチパターンを実装して地雷を山ほど踏んできました。
めんどくさすぎるデータの移行もたくさんしましたし、そこで勘所を掴んでいったような気がします。
また学びの多い設計をしたら共有しようと思います!

