空き時間に、データベース バックエンドを備えた小さなマルチプレイヤー ゲームを書き始めました。私はプレイヤーのログイン情報をゲーム内の他の情報 (インベントリ、統計、ステータス) から分離しようと考えていましたが、友人がこれを持ち出したのは最善のアイデアではないかもしれません.
すべてを 1 つの表にまとめたほうがよいでしょうか。
空き時間に、データベース バックエンドを備えた小さなマルチプレイヤー ゲームを書き始めました。私はプレイヤーのログイン情報をゲーム内の他の情報 (インベントリ、統計、ステータス) から分離しようと考えていましたが、友人がこれを持ち出したのは最善のアイデアではないかもしれません.
すべてを 1 つの表にまとめたほうがよいでしょうか。
パフォーマンスを無視することから始めて、できるだけ論理的で理解しやすいデータベース設計を作成してください。プレイヤーとゲーム オブジェクト (剣? チェスの駒?) が概念的に別のものである場合は、それらを別のテーブルに配置します。プレーヤーが物を運ぶことができる場合、「プレーヤー」テーブルを参照する「物」テーブルに外部キーを置きます。等々。
次に、何百人ものプレーヤーと何千ものものがあり、プレーヤーがゲーム内を走り回り、データベース検索を必要とすることを行う場合でも、適切なインデックスを追加するだけで、設計は十分に高速になります。
もちろん、何千人もの同時プレイヤーを計画し、それぞれが毎秒自分のものをインベントリするか、データベース検索の他の膨大な負荷 (グラフィック エンジンによってレンダリングされた各フレームの検索?) を計画している場合は、次のことを考慮する必要があります。パフォーマンス。ただし、その場合、テーブルをどのように設計しても、通常のディスク ベースのリレーショナル データベースでは十分な速度が得られません。
リレーショナル データベースはセットで動作します。データベース クエリは、何も (「見つかりません」) またはそれ以上の結果を提供します。リレーショナル データベースは、(リレーションに対して) クエリを実行することによって常に正確に 1 つの結果を提供することを意図していません。
実生活もあると言いたいです ;-) . 1 対 1 の関係は理にかなっている場合があります。つまり、テーブルの列数が重要なレベルに達した場合、このテーブルを分割する方が速い場合があります。しかし、この種の条件は本当にまれです。
テーブルを分割する必要が本当にない場合は、分割しないでください。少なくとも、あなたの場合、すべてのデータを取得するには、2 つのテーブルを選択する必要があると思います。これはおそらく、すべてを 1 つのテーブルに格納するよりも遅くなります。そうすることで、プログラミング ロジックの可読性が少し低下します。
Edith は次のように述べています。とにかく、後で列が正規化される (つまり、他のテーブルに置かれる) かどうかが正確にわからない場合は、「テーブル」-1 対 1-「他のテーブル」ではなく、1 つのテーブルでこれを行う方が簡単です。 "
保持するかどうか
プレイヤーのログイン情報 [別の] ゲーム内の他の情報 (インベントリ、統計、およびステータス)
多くの場合、正規化の問題です。ウィキペディア ( Third Normal Form、Denormalization ) の定義をざっと見てみるとよいでしょう。これらを複数のテーブルに分割するかどうかは、格納されるデータによって異なります。質問を拡大すると、これが具体的になります。保存したいデータは次のとおりです。
これを単一のテーブルまたは複数のテーブルとして保存できます。
単一テーブルの例
プレイヤーがこのゲームの世界で単一のキャラクターを持っている場合、単一のテーブルが適切な場合があります。例えば、
CREATE TABLE players(
id INTEGER NOT NULL,
username VARCHAR2(32) NOT NULL,
password VARCHAR2(32) NOT NULL,
email VARCHAR2(160),
character VARCHAR2(32) NOT NULL,
status VARCHAR2(32),
hitpoints INTEGER,
CONSTRAINT pk_players PRIMARY KEY (uid)
);
これにより、players テーブルに対する単一のクエリで、プレーヤーに関するすべての関連情報を見つけることができます。
複数テーブルの例
ただし、1 人のプレイヤーが複数の異なるキャラクターを使用できるようにする場合は、このデータを 2 つの異なるテーブルに分割する必要があります。たとえば、次のようにします。
-- track information on the player
CREATE TABLE players(
id INTEGER NOT NULL,
username VARCHAR2(32) NOT NULL,
password VARCHAR2(32) NOT NULL,
email VARCHAR2(160),
CONSTRAINT pk_players PRIMARY KEY (id)
);
-- track information on the character
CREATE TABLE characters(
id INTEGER NOT NULL,
player_id INTEGER NOT NULL,
character VARCHAR2(32) NOT NULL,
status VARCHAR2(32),
hitpoints INTEGER,
CONSTRAINT pk_characters PRIMARY KEY (id)
);
-- foreign key reference
ALTER TABLE characters
ADD CONSTRAINT characters__players
FOREIGN KEY (player_id) REFERENCES players (id);
この形式では、プレイヤーとキャラクターの両方の情報を見るときに結合が必要です。ただし、レコードの更新によって不整合が発生する可能性は低くなります。この後者の議論は、通常、複数のテーブルを提唱するときに使用されます。たとえば、1 つのテーブル形式で 2 人のキャラクター (gollum、frodo) を持つプレイヤー (LotRFan) がいる場合、ユーザー名、パスワード、電子メール フィールドが重複します。プレーヤーが自分の電子メール/パスワードを変更したい場合は、両方のレコードを変更する必要があります。1 つのレコードのみが変更された場合、テーブルは不整合になります。ただし、LotRFan が複数のテーブル形式で自分のパスワードを変更したい場合、テーブル内の単一の行が更新されます。
その他の考慮事項
単一のテーブルを使用するか複数のテーブルを使用するかは、基になるデータによって異なります。ここでは説明されていませんが、他の回答で指摘されているのは、最適化では多くの場合、2 つのテーブルが使用され、それらが 1 つのテーブルに結合されるということです。
また、行われる変更の種類を知ることも重要です。たとえば、複数のキャラクターを持つ可能性のあるプレーヤーに 1 つのテーブルを使用することを選択し、プレーヤー テーブルのフィールドをインクリメントすることで、プレーヤーが発行したコマンドの総数を追跡したいとします。これにより、単一テーブルの例で更新される行が増えます。
いずれにせよ、Inventory は多対 1 の関係であるため、おそらく独自のテーブルが必要です (少なくとも外部キーでインデックスが作成されます)。
また、公開されているもの (つまり、他の人があなたを見ることができるもの) とは別に、各ユーザーの個人的なものを持ちたいと思うかもしれません。多くの人があなたの公開属性を見ることになりますが、あなたの個人属性を見るのはあなただけなので、これはより良いキャッシュヒットを提供するかもしれません。
1 対 1 の関係は単なる縦割りです。他には何もありません。なんらかの理由ですべてのデータを同じテーブルに格納しない場合 (たとえば、複数のサーバーにまたがるパーティション分割など) に実行します。
ご覧のとおり、これに関する一般的なコンセンサスは、「場合による」ということです。@Campbell などで言及されているように、パフォーマンス/クエリの最適化は簡単に頭に浮かびます。
しかし、構築しているアプリケーション固有のデータベースの種類については、アプリケーション全体の設計を検討することから始めるのが最善だと思います。アプリケーション固有のデータベース (多くのアプリやレポート ツールで使用するように設計されたデータベースとは対照的に) の場合、「理想的な」データ モデルは、おそらく、アプリケーション コードに実装されているドメイン モデルに最もよく対応するものです。
デザインをMVCと呼ばないかもしれません。使用している言語はわかりませんが、データ アクセスを論理モデルでラップするコードまたはクラスがあると思います。このレベルでのアプリケーション設計の見方は、データベースが理想的にどのように構成されるべきかを示す最良の指標だと思います。
一般に、これは、ドメイン オブジェクトをデータベース テーブルに 1 対 1 でマッピングすることから始めるのが最適であることを意味します。
例で私が何を意味するかを説明するのが最善だと思います:
Playerクラスの観点から考えます。Player.authenticate、Player.logged_in?、Player.status、Player.hi_score、Player.gold_credits。これらすべてが 1 人のユーザーに対するアクションであるか、またはユーザーの単一値の属性を表している限り、論理的なアプリケーション設計の観点からは、単一のテーブルが出発点となります。シングル クラス (プレーヤー)、シングル テーブル (プレーヤー)。
スイス アーミー ナイフのようなPlayerクラスの設計は好きではないかもしれませんが、 User、Inventory、Scoreboard、およびAccountの観点から考えるほうが好きです。User.authenticate、User.logged_in?、User->account.status、Scoreboard.hi_score(User)、User->account.gold_credits。
ドメイン モデルでこれらの概念を分離することで、コードがより明確になり、書きやすくなると思うので、おそらくこれを行っています。この場合、最適な出発点は、ドメイン クラスを個々のテーブル (ユーザー、インベントリ、スコア、アカウントなど) に直接マッピングすることです。
ドメイン オブジェクトをテーブルに 1 対 1 でマッピングすることが理想的で論理的な設計であることを示すために、いくつかの言葉を上に挿入しました。
それが私の好ましい出発点です。複数のクラスを 1 つのテーブルにマッピングし直すなど、すぐに賢くしようとすると、避けたい問題 (特にデッドロックと同時実行性の問題) が発生する傾向があります。
しかし、それは必ずしも話の終わりではありません。並行性/ロックの問題など、物理データ モデルを最適化するための別のアクティビティが必要であることを意味する実用的な考慮事項がありますか? 行サイズが大きくなりすぎていませんか? インデックス作成のオーバーヘッド? クエリのパフォーマンスなど
ただし、パフォーマンスについて考えるときは注意してください。1 つのテーブルの 1 つの行である可能性があるからといって、行全体を取得するために 1 回だけクエリを実行すればよいというわけではありません。マルチユーザー ゲームのシナリオでは、さまざまな時点でさまざまなプレイヤー情報を取得する一連の呼び出しが必要になる可能性があり、プレイヤーは常に非常に動的な「現在の値」を求めていると思います。これは、毎回テーブル内のほんの数列に対して多くの呼び出しを行うことになります (この場合、複数テーブルの設計は単一テーブルの設計よりも高速になる可能性があります)。
それらを分離することをお勧めします。データベース上の理由ではなく、開発上の理由によるものです。
プレーヤーのログイン モジュールをコーディングするときは、ゲーム情報について考えたくありません。そしてその逆。テーブルが異なる場合は、関心の分離を維持する方が簡単です。
要するに、あなたのゲーム情報モジュールがプレイヤーのログイン テーブルをいじくり回していないという事実です。
トーマス・パドロン・マッカーシーの答えに同意します:
何千もの同時ユーザーの場合を解決することは、あなたの問題ではありません。人々にあなたのゲームをプレイしてもらうことが問題です。最もシンプルなデザインから始めましょう - ゲームを完成させ、機能し、プレイ可能で、簡単に拡張できるようにします。ゲームが軌道に乗れば、非正規化します。
インデックスは、データのより狭いビューを含む別のテーブルとして見ることができることにも言及する価値があります。
Blizzard が World of Warcraft に使用したデザインを推測することができました。プレイヤーが進行中のクエストはキャラクターに保存されているようです。例えば:
CREATE TABLE Players (
PlayerID int,
PlayerName varchar(50),
...
Quest1ID int,
Quest2ID int,
Quest3ID int,
...
Quest10ID int,
...
)
最初は最大 10 個のクエストに参加できましたが、後に 25 個に拡張されました。
CREATE TABLE Players (
PlayerID int,
PlayerName varchar(50),
...
Quest1ID int,
Quest2ID int,
Quest3ID int,
...
Quest25ID int,
...
)
これは分割設計とは対照的です。
CREATE TABLE Players (
PlayerID int,
PlayerName varchar(50),
...
)
CREATE TABLE PlayerQuests (
PlayerID int,
QuestID int
)
後者は、概念的にははるかに扱いやすく、任意の数のクエストが可能です。一方、そのすべての情報を取得するには、Players と PlayerQuests に参加する必要があります。
クエリのパフォーマンスが向上するため、この状況ではおそらく 1 つのテーブルに配置することもできます。
次のテーブルを使用するのは、データ ストレージのオーバーヘッドを削減するために正規化を検討する必要がある重複データの要素がある場合のみです。
各レコード タイプを独自のテーブルに保持することをお勧めします。
プレーヤーのログインは、記録の 1 つのタイプです。在庫は別です。ほとんどの情報は共有されないため、テーブルを共有するべきではありません。無関係な情報を分離して、テーブルをシンプルに保ちます。