web-dev-qa-db-ja.com

マルチレベルのリレーションシップテーブルを設計/クエリする最良の方法

私の会社では、さまざまな地域でサービスを販売するモバイルアプリを開発します。ユーザーがSearch関数に大きく依存することを期待しています。したがって、データベースの設計が検索を処理するのに十分なものであることを確認する必要があります。

私の主な懸念は、ユーザーが名前や説明でサービスを検索できるだけでなく、サービスの場所でも検索できるようにすることです。

まず、私が設計したものを紹介しましょう。

Solution 1
Table: Service
+------+----------+--------------+--------+----------+----------+
| id   | name     | description  | cityid | address1 | address2 |
+------+----------+--------------+--------+----------+----------+
|     1| Service1 | Description1 |       1| Address1 |          |
|     2| Service2 | Description2 |       2| Address2 |          |
|     3| Service3 | Description3 |       3| Address3 |          |
+------+----------+--------------+--------+----------+----------+

Table: City
+------+-----------+---------+
| id   | name      | stateid |
+------+-----------+---------+
|     1| KL        |       1 |
|     2| Georgetown|       2 |
|     3| JB        |       3 |
+------+-----------+---------+

Table: State
+------+---------------------+-----------+
| id   | name                | countryid |
+------+---------------------+-----------+
|     1| Wilayah Persekutuan |         1 |
|     2| Penang              |         2 |
|     3| Johor               |         3 |
+------+---------------------+-----------+

Table: Country
+------+-----------+
| id   | name      |
+------+-----------+
|     1| Malaysia  |
|     2| Singapore |
+------+-----------+

上記は、いくつかのサンプルデータを含む私のテーブルです。要件は、ユーザーがキーワードを入力してServiceテーブルで検索を開始できるようにすることです。キーワードがサービス名と一致する場合は、Serviceからその結果を返します。そうでない場合は、Cityテーブルでキーワードを検索します。キーワードが特定のCity行と一致する場合は、Serviceでその都市に一致するすべてのcityid行を取得します。行が見つからない場合は、Stateテーブルまで、Countryテーブルの検索を続けます。

だからここに私の解決策の問題があります。ユーザーが国名を入力すると、Countryテーブルから一致結果を取得し、その結果のcountryidを使用してStateテーブル内のすべての関連する州を取得し、それらの結果のstateidsを使用してCityテーブル–そして、最後の結果のcityidを使用して、Serviceテーブル内の関連サービスを検索します。多くの再帰的なIN検索を使用して結果をフィルター処理する必要があるため、これは良い方法ではないと思いますが、これはパフォーマンスには適していません。

その後、私の同僚は別の解決策を考え出しました。この解決策は、検索に関連するすべての参照をServiceテーブルに入れます。次のようなものです。

Solution 2
Table: Service
+------+----------+--------------+--------+----------+-----------+----------+----------+
| id   | name     | description  | cityid | stateid  | countryid | address1 | address2 |
+------+----------+--------------+--------+----------+-----------+----------+----------+
|     1| Service1 | Description1 |      1 |        1 |         1 | Address1 |          |
|     2| Service2 | Description2 |      2 |        2 |         2 | Address2 |          |
|     3| Service3 | Description3 |      3 |        3 |         3 | Address3 |          |
+------+----------+--------------+--------+----------+-----------+----------+----------+

したがって、Countryが検索に一致した場合、結果のcountryidを使用してServiceテーブルで直接検索を実行します。それ以外の場合は、Stateが検索に一致した場合、結果のstateidを使用してServiceなどで検索を実行します。この方法は再帰的な検索が少ないためより効率的ですが、欠点は正規化の慣行に違反していることです。Serviceテーブルには、countryidstateidという冗長な情報があります。そして論理的に言えば、複数のルックアップ/結合を実行する必要がある場合でも、cityidだけでこれら2つのIDを見つけることができます。

それで、私はどの解決策をとるべきですか?それとももっと良い提案がありますか?お知らせ下さい。

4
Wai Hong

私はあなたが述べたテクニックのすべてを展開したくなるでしょう。説明させてください。正規化されたService/City/State/Countryは、OLTP処理に最適です。アプリケーションのその部分のために保持し、実際のデータストアとして扱います。

あなたが述べたように、これは検索を複雑にする可能性が高いので、非正規化が有利になるでしょう。あなたの提案とは異なり、検索を支援するためだけに存在する別のテーブルに非正規化します。実際のコンピュータ上の実際のDBMSには、理論的なコンピュータ科学者が想像したいパフォーマンス特性がないことは、明確に認められています。別のテーブルに配置することにより、妥協された物理的な必要性を、好ましい正規化された設計から分離します。

最後に、この非正規化されたテーブルの1つの列にすべての検索用語を組み合わせます。 1つの方法は、サービス名、都市名、州名、国名をスペースで区切り、その列にフルテキストインデックスを配置することです。

もう1つは、idnameおよびsource_tableの3つの列を持つことです。最初の2つは、質問にリストされているデータ値からのものです。 source_tableは、これがどの正規化テーブルからのものかを示しています。質問からのサンプルデータは次のようになります。

  id     name      source_table
  -----  --------  ------------
  1      Service1  Service
  2      Service2  Service
  ...
  1      KL        City  
  3      JB        City
  ...
  2      Penang    State
  ..
  1      Malaysia  Country

クエリは、ユーザーの検索用語をこのテーブルのname列と照合します。

select
  id,
  source_table
from search_table
where name = <user-provided value>

返されたidおよびsource_tableを使用して、正規化されたテーブルから選択する値を決定できます。これらは通常の方法で相互に結合できます。階層には4つのレイヤーがあるため、可能なクエリは4つだけです(サービス、サービス/都市、サービス/都市/州、サービス/都市/州/国)。次のように適切なものを呼び出すのは簡単です。

if source_table = "Service"

  select
    <columns>
  from Service
  where id = <search_table.id>

if source_table = "City"

  select
    <columns>
  from Service
  inner join City
    on Service.cityid = City.id
  where City.id = <search_table.id>

... etc.

名前がこれらのレベル(モナコなど)で重複している可能性があることに注意して、これらのケースをどのように処理するかを決定してください。

名前が一意であることが保証され、代理キーではなく自然キーが使用された場合、プロセスは多少簡略化できます。

しかし、このすべての努力が価値があるかどうかは疑問です。これらのテーブルには何行ありますか?ユーザーの検索値をORを使用して各名前列と比較する、インデックス付きの正規化されたテーブルに対するクエリのパフォーマンスはどのくらい悪いですか?代表的な量のダミーデータを作成し、プロダクションのようなハードウェアで応答時間を測定することにより、開発と多くのメンテナンス作業を節約できます。

2
Michael Green

特定の都市、州、または国の存在によって地理的近接度を測定する代わりに、地理的ポイント(緯度/経度)または地理的ポリゴンのいずれかを使用してサービスを検索してみませんか?

あなたが提案しているスキームはこれらの状況を考慮に入れていません:

  • 一部の国は非常に大きいため、国内にいることは近接していることの大きな尺度ではありません。
  • すべての場所が都市内にあるわけではないため、都市名でサービスを検索できるとは限りません。
  • すべての国が州に類似した政治的区分を持っているわけではないので、あなたの階層は常に成り立つわけではありません。

MySQLには空間拡張機能があります。これらを調べて、サーチャーに「近い」(コンテキストでの意味が何であれ)サービスを検索できるようにすることで、問題をより直接的に解決できるかどうかを確認する必要があります。

2
Joel Brown

City + State + Countryは1つのLocationテーブルにある必要があります。または...冗長性がほとんどない場合は、それらをリンクするテーブルに追加します。

つまり、「正規化しすぎる」というものがあります。過度に正規化すると、検索がより複雑になり、クエリが遅くなる可能性があります。一方、正規化の利点このタイプのデータはごくわずかです。

descriptionには何がありますか?ユーザーが検索する可能性のある単語の束?その場合は、FULLTEXTが最適です。ただし、制限があることに注意してください。

countryCHAR(2)を使用する場合は、CHAR(2) CHARACTER SET asciiにします。デフォルトの文字セットがutf8mb4の場合、CHAR(2)は8バイトを使用します。 asciiを指定すると、2バイトに抑えられます。必要なのはこれだけです。

「最も近い」を検索する必要がある場合、それは別の魚のやかんです。あなたはここで多くの議論を見つけるでしょう: https://stackoverflow.com/questions/tagged/latitude-longitude

0
Rick James