graphics.hatenablog.com

テクニカルアーティストの技術を書き殴るためのメモ帳

テクニカルアーティストのためのデータベース入門 (5) 正規化

前回紹介したエンティティとリレーションを、実際にどうやってDBに落とし込むか。何回かにわけてざっくり書いてみる。

完成品はこちら。オレオレフォーマットだから違和感あるかもだけど、まぁ雰囲気だけ伝われば。
f:id:hal1932:20161010165611p:plain

graphics.hatenablog.com

正規化

正規化というのは、ひとことでいうと「無駄のないテーブル設計をする」ことをいう。例えば、複数のテーブル間で重複する要素をまとめたり、変更に強い(データが変更されたときの影響が少ない)テーブルにつくりかえたりする。

正規化を行うときは、まず何より、アセット内に含まれるデータ同士の関係性を考える。ネットで「正規化」をぐぐったときにでてくるナントカ従属みたいのも、基本的には、この関係性を整理するためのルールだと思えばいい。だから、アセットの制作・管理者として十分に合理的だと思える設計ができれば、必要な正規化の要件はそれで十分に満たしてることも多い。まずは、TAとしての勘をフル活用することを考えよう。

もちろん偉大な先人の知恵を無視しても大抵は何のメリットもないので、ある程度慣れてきたらそういうルールも十分に考慮できるようにしたい。

要素のピックアップ

まずは、何も考えずにDBに保存したい項目を抜き出してみる。

f:id:hal1932:20161010145004p:plain

モデルやテクスチャはファイルとして保存することが一般的だと思うので、それぞれが filepath, created_at, last_updated_at という属性を持つこと自体には、特に違和感はないと思う。

先読みと抽象化

DBの教科書を読むとナントカ従属みたいなのがたくさん出てくるけど、ひとまずは、このテーブル構成だとどんな問題が起きるかを考えてみる。

まず、この構成だとファイルが更新されるたびに models や textures の中身が書き換わることになる。ただ、書き換えたいのはモデル自身ではなく、モデルの実体が含まれるファイルのはずだ。「ファイルの更新が成果物に与える影響」と「モデルの更新が成果物に与える影響」は、常に一致しているとは限らない。ついでに、モデルの名前とファイル名が常に一致している必要もない。

また、制作途中には仮のダミーデータをモデルやテクスチャに割り当てて使うことが間々ある。ダミーかどうかを判断するための is_dummy フラグが欲しくなるかもしれない。models に is_dummy を持たせてしまったら「仮モデル」なのか「仮データが挿さってる本番モデル」なのかの区別もつかない。

ダミーといえば、複数のモデルやテクスチャに全部同じダミーが挿さっていることもある。まったく同じ filepath や created_at が models や textures の中に何度も書き込まれるのは、なんか無駄な気がする。

「必要なモデルが確定したけどファイルはまだ作成されていない」というケースがある。仮モデルすらなく、プログラマが自動生成したグレーのキューブが表示されてる状態。その後ファイルが用意されたら、モデルの created_at はどうなるのか。モデルが最初に用意された日付? それともファイルが最初に用意された日付?

モデルやテクスチャの作成担当者はファイルのユーザ属性を参照すればいいから記録する必要ない? じゃあ外注制作でファイル作成者と監修担当者が違う場合はどうしようか?

などなど……。

なぜこういうことが起きるのかというと、モデルやテクスチャというのは、「アセット制作の都合でたまたま .ma や .psd の形式で保存されている」だけであって、それらのファイル自体がモデルやテクスチャだというわけではない*1からだ。こういった、対象の本質に応じた問題の切り分けを行うことを「抽象化」と呼ぶのだけど、実際のところ、このあたりはアセットの制作や管理の経験がモノをいう部分でもある。DBに限らず、ワークフローの整備をゲームプログラマに丸投げすると大抵ロクなことにならない*2のは、そのあたりが原因であることが多い。

本質でない要素の分離

というわけで、files という新しいテーブルを用意して、モデルやテクスチャからは file_id というかたちで参照することにした。

f:id:hal1932:20161010170200p:plain

こうすると、ファイルが更新されたときは files の中身を更新するだけで、 models や textures には更新が発生しない。また、モデルに挿してあるファイルを差し替えるときも、files の中身はそのままで、models.file_id を書き換えるだけで済む。

モデルやテクスチャの担当者として、それぞれに operator を追加した。実ファイルの作成者は、必要であれば files テーブルに記録することにする。ここでは、そのモデルやテクスチャのクオリティに責任を持つ人を、operator に記録しておく。外注するときの監修担当者とか。

また、こうしておくと、モデルやテクスチャのことを一切気にせず、プロジェクトフォルダを走査して見つかったファイルを片っ端から登録しておくことができる。すべてのファイルがモデルやテクスチャに紐付いてるわけではないし、今後アニメやエフェクトを管理する必要が出てきたときは、animations や effects というテーブルに file_id を持たせればいい。

あえて正規化しないケース

扱いやすさのために抽象化しない

さて、単純に「重複した要素を外出しする」という考え方で正規化を行うと、おそらく operator を別テーブルに分けたくなってくると思う。ここには制作担当者の名前が文字列で記録されることを考えると、例えば "hal1932" という文字列が models 内に何度も登場することになる。これは抽象化して id 参照にしよう、と。

f:id:hal1932:20161010181629p:plain

ただし、これがメリットになるかどうかはケースバイケースになる。大抵の場合、担当者を調べるために別テーブルを参照しないといけなくなってクエリが複雑になるし、DB参照のパフォーマンスが落ちる。単純に人の名前を記録しておくだけなら、そのまま突っ込んでしまっても問題にはならない。というか、そのほうが断然扱いやすい

これが例えば、「モデルAは地形チームが管理する」が「モデルBは担当者Cさんが管理する」というふうに担当者の粒度が対象によって変わる場合*3は、別テーブルにせざるを得なかったりもする。

f:id:hal1932:20161011011707j:plain

models

id name operator_id
1 model1 1
2 model2 3

operators

id name group_id
1 地形チーム null
2 背景チーム null
3 hal1932 1

こうすると、model1 は地形チームが管理して、model2 は hal1932 さん個人が管理している。そして hal1932 さんのグループ ID が 1、つまり hal1932 さんは地形チームに所属している。ということは、model1 について問い合わせるときは hal1932 さんに連絡すればいい、というルールを表現*4できる。

制作ルールを直感的に表現するために抽象化しない

また、「モデルは常に実ファイルと 1 対 1 対応する」「モデルの更新日時とファイルの更新日時は常に一致している」というルールがある場合、これらを models に含めてしまうこともある。

f:id:hal1932:20161011012739j:plain

「モデル」という概念が常にファイルの存在を含んでいるのであれば、これはこれで適切なテーブル設計といえる。ファイル情報を別テーブルに切り分けてももちろん構わないが、常に 1 対 1 対応であれば重複の無駄もないし、わざわざ別テーブルを参照しなくても欲しい情報がすべて手に入るのは効率がいい。

このあたりは実際に SQL を書いてみると、正規化しないメリットを実感できると思う。

*1:例えばプログラマからみれば、頂点や画素をデザイナがどう扱っていようが、制作中に適切にプレビューとデバッグができて、十分に最適化された形式でランタイムでロードできさえすれば、その形式が .ma や .psd といった個々のファイルである必要はない。ランタイムでのロード最適化の観点からいえば、.ma や .psd の形式はむしろ非効率だ。

*2:プログラマが悪いのではなく、単純に得意分野の違い。

*3:そもそもそういう運用を見直すべきだ、という話は、場合によってはあるかもしれないが……。

*4:ただしこれはナイーブツリーという有名なアンチパターンなので、それはそれで気をつける必要がある。