web-dev-qa-db-ja.com

同じテーブルで複数のJOINを使用してクエリを実行する代わりに?

Postgresql 11データベースがあります。 housesというテーブルがあるとします。何十万ものレコードが必要です。

CREATE TABLE houses (
  pkid serial primary key,
  address varchar(255) NOT NULL,
  rent float NOT NULL
);

現在、私の家にはデータベースに登録したい機能があります。可能な機能のリストはかなり長く(数十)なり、時間の経過とともに進化します。テーブルに長い列のリストを追加したくないのでhousesそして 'ALTER TABLE'で常にテーブルを変更します。これらの機能用に別のテーブルを用意することを考えました:

CREATE TABLE house_features (
   pkid serial primary key,
   house_pkid integer NOT NULL,
   feature_name varchar(255) NOT NULL,
   feature_value varchar(255)
);
CREATE INDEX ON house_features (feature_name, feature_value);
ALTER TABLE house_features ADD CONSTRAINT features_fk FOREIGN KEY (house_pkid) REFERENCES houses (pkid) ON DELETE CASCADE;

平均して、各houseレコードは、house_features テーブル。

これまでのところ、これは単純で効率的なモデルのようです:feature_nameの可能な値を制御して、さまざまな機能を追加できます=)feature_value上位層(アプリケーション層および/またはGUI)。アプリケーションが進化するたびにデータベースを変更する必要はなく、新しいタイプの機能が必要です。

例として、次の機能があるとします。

  • feature_name:「rooftype」と可能なfeature_value:「flat」または「inclined」
  • feature_name: 'wallcolors'と可能なfeature_value: 'white'、 'beige'、 'blue'、 'green'など(15の異なる値)
  • feature_name: 'has_basement'と可能なfeature_value: 'True'または 'False'。
  • feature_name: 'number_of_doors'。可能なfeature_valueは文字列としてコード化された任意の整数(つまり、 '0'、 '1'、 '2'、...)。
  • feature_name: 'floor_surface'と可能なfeature_valueを使用して、文字列としてコード化された任意のフロート(例: '155.2')

明らかに、ブール値、整数、および浮動小数点数を文字列として格納することはあまり効率的ではなく、これも私が注意する必要があることです。各XXXタイプ(文字列、ブール値、浮動小数点数、整数)ごとに個別のhouse_features_XXXテーブルを作成することを考えていました。

しかし、それは私の問題でもありません。

私の問題は:特定の機能を持つ家を検索するにはどうすればよいですか?

例として、地下室、白い壁、傾斜屋根タイプの家を検索するとします。次のようなクエリをアプリケーション層で動的に作成できます。

SELECT sq1.* FROM 
( SELECT house_pkid FROM house_features WHERE feature_name = 'has_basement' AND feature_value = 'True' ) AS sq1
JOIN
( SELECT house_pkid FROM house_features WHERE feature_name = 'wallcolors' AND feature_value = 'white' ) AS sq2
ON sq1.house_pkid = sq2.house_pkid
JOIN
( SELECT house_pkid FROM house_features WHERE feature_name = 'rooftype' AND feature_value = 'inclined' ) AS sq3
ON sq1.house_pkid = sq3.house_pkid
;

しかし、それは特にhouse_featuresに数十の条件があるかもしれないことを考えると、それほど効率的ではないようです。

これを行うより良い方法はありますか?

3
Darth Kangooroo

機能をJSON値に集約してみると、複数の機能の組み合わせを簡単に検索できます。

select h.*, hf.features
from houses
  join (
    select house_id, jsonb_object_agg(feature_name, feature_value) as features
    from house_features
    group by house_id
  ) hf on hf.house_pkid = h.pkid 
where hf.features @> '{"rooftype": "flat", "has_basement", "true", "wallcolors": "white"}';

機能名を繰り返す副選択にWHERE句を追加することで、パフォーマンスを改善できます。例:

where feature_name in ('rooftype', 'has_basement', 'wallcolors')

あるいは

where (feature_name, feature_value) in (('rooftype', 'flat') ('has_basement', 'true'), ('wallcolors', 'white'))

内部のwhereにはすべての機能を備えていない家が含まれるため、外部の条件はまだ必要です。

これには、(私の目には)各機能の1つの行ではなく、すべての機能を持つ1つの行だけが表示されるという利点もあります。


家の機能を頻繁に削除、追加、変更しない限り、それらをhouseテーブル(features)の単一のJSONB列として保存し、house_featuresテーブルを削除します。代わりになるかもしれません。その場合、列にインデックスを作成して検索を高速化できます。

それで、私はPostgresqlでcrosstab関数を使用することの先導に従いました。これは私が得た場所です:

クロス集計関数を使用すると、家ごとにfeature_name列ごとにfeature_value

SELECT * FROM crosstab (
' SELECT house_pkid, feature_name, feature_value 
  FROM house_features
  WHERE feature_name IN (''rooftype'',''wallcolors'',''has_basement'',''number_of_doors'',''floor_surface'')
  ORDER BY house_pkid, feature_name, feature_value '
,
$$VALUES ('rooftype'), ('wallcolors'), ('has_basement'), ('number_of_doors'), ('floor_surface') $$
) 
AS ct (house_pkid int, "rooftype" varchar, "wallcolors" varchar, "has_basement" varchar, "number_of_doors" varchar, "floor_surface" varchar) ;

このクエリにより、次のような一連のレコードを取得できます。

house_pkid | rooftype | wallcolors | has_basement | number_of_doors | floor_surface 
-------------------------------------------------------------------------------------
    232    | inclined |   beige    |   False      |         2       |       90
    234    | flat     |   white    |   False      |         1       |       70

そして、このレコードのセットに対してSELECTを実行できます。

2つのことに注意してください。

  • WHERE句が必要なのは、feature_nameの他の値もあり、それらが最終的な検索条件に表示されてはならない場合(これは私の場合ですが、オリジナルメッセージ)。
  • house_pkidを除いて、feature_valueはvarcharであるため、他のすべての列はvarcharとして返されます。

さて、これが機能し、遅くない場合は、最適化に関して、私はまだ改善できることに気づきました:

  • 1つ目は、ETLプロセスがデータベースにデータを送るときに、データが大きく変化せず、年に3〜4回だけです。残りの時間、テーブルhouseshouse_featuresのデータは同じままです。そのため、クエリをPosgresql MATERIALIZED VIEWに変換する方がよいと判断しました。このようにして、housesおよびhouse_featuresテーブルを作成するたびに、MATERIALIZED VIEWを再構築(およびクロス集計関数を呼び出す)する必要があります。 ETLを通じて再ロードされます。 2つのETL間で、MATERIALIZED VIEWは、呼び出しごとにクロス集計関数を処理する必要なく、結果にアクセスできます。通常のテーブルのようにMATERIALIZED VIEWにインデックスを追加して、SELECTクエリを高速化することもできます。
  • Crosstab呼び出しはすべてに対してvarchar型の列を返し、house_pkidは保存しますが、トランスタイプすることで、より適切でより効率的なデータ型を得ることができます。文字列「True」または「False」ではなく、ブール;代わりに、文字列「90」がある場合、値90の整数を使用します。
  • 最初のメッセージに記載されているように、列house_features.feature_nameが時間とともに変化する可能性のある値のリストですが、私の場合は、アプリケーションレイヤーの新しいバージョンが配信された場合のみです。 、つまり、ETLもあり、MATERIALIZED VIEWを再構築する場合。したがって、私はmy Python applicative layer(これはETLを実行します)内に、名前と各値のPSQLタイプを含むタプルのリストに基づいてMATERIALIZED VIEWのPSQLコードを作成する関数をコーディングしました。 feature_nameは、私の検索基準の1つです。

これは与える :

from collections import namedtuple
hf_Tuple = namedtuple('house_searchable_features', ['fieldname', 'fieldtype'])
searchablefeatures = [
    hf_Tuple(fieldname='rooftype', fieldtype='varchar'),
    hf_Tuple(fieldname='wallcolors', fieldtype='varchar'),
    hf_Tuple(fieldname='has_basement', fieldtype='boolean'),
    hf_Tuple(fieldname='number_of_doors', fieldtype='integer'),
    hf_Tuple(fieldname='floor_surface', fieldtype='float'),
]

def create_searchablefeatures_query():
    """ Creates the SQL query for re-creating the MATERIALIZED VIEW. """
    query_sourcesql = 'SELECT house_pkid, feature_name, feature_value FROM house_features WHERE feature_name IN ( \n'
    query_sourcesql += ",\n".join(f" \t''{sf.fieldname}'' " for sf in searchablefeatures)
    query_sourcesql += ')\n ORDER BY house_pkid, feature_name, feature_value'

    query_categories = "$$VALUES \n"
    query_categories += ",\n".join(f"\t('{sf.fieldname}')" for sf in searchablefeatures)
    query_categories += "\n$$"

    query_output = ''
    query_output += ",\n".join(f'\t"{sf.fieldname}" varchar' for sf in searchablefeatures)

    query_transtyping = ''
    for sf in searchablefeatures:
        if sf.fieldtype == 'boolean':
            query_transtyping += f',\n\t("{sf.fieldname}" IS NOT NULL AND "{sf.fieldname}" != \'False\')  AS "{sf.fieldname}"'
        Elif sf.fieldtype == 'int' or sf.fieldtype == 'float':
            query_transtyping += f',\n\t"{sf.fieldname}"::{sf.fieldtype}'
        Elif sf.fieldtype == 'varchar':
            query_transtyping += f',\n\t"{sf.fieldname}"'
        else:
            raise ValueError(f"unknown PSQL data type: {sf.fieldname}, {sf.fieldtype}")

    sql_def = f"""
DROP MATERIALIZED VIEW IF EXISTS house_searchablefeatures CASCADE ;
CREATE MATERIALIZED VIEW house_searchablefeatures AS
    SELECT house_pkid {query_transtyping} FROM
    (   SELECT * FROM crosstab( '\n{query_sourcesql}',\n {query_categories} \n)
        AS ct ( house_pkid int, \n{query_output} \n) 
    ) AS b4transtyping ; """

    return sql_def

hf_Tupleでは、fieldtypeはMATERIALIZED VIEWで必要なPostgresqlデータ型であり、Pythonデータ型また、データベースの内容によっては、query_transtypingのロジックを調整する必要がある場合があります。

これは簡単な部分ではなく、いくつかのテストでうまくいくことが確認されますが、堅牢で効率的です。メンテナンスに関しては、リストsearchablefeaturesを更新し、すべてのETLが受け入れられるようになったらクエリを実行するだけです。

関数はPython 3.8。

0
Darth Kangooroo

特に、検索する機能の数が多い場合は、メガクエリステートメントの作成を回避するために、代わりに検索された機能を保持する一時テーブルを作成するを検討し、簡単なINNER JOINを実行します。前述のように、GROUP BYはカウントされます。

これは、Pythonの機能を連結するSELECT ... feature IN ( feat1, feat2, feat3...)で長いクエリを作成するためのreplacementです。

パフォーマンスに関しては、現時点ではテストする時間はありませんが、これははるかに優れているように思えます。

これは、検索する機能の数が任意である各クエリに対して行うことです。

たとえば、ユーザーは白い壁basement傾斜した屋根

CREATE TEMPORARY TABLE search_features ( FEAT_NAME VARCHAR(255), FEAT_VALUE VARCHAR(255));

次に、(おそらくバッチの方が良い)Pythonを介して、検索するパラメータを挿入します。これはonlyであり、ユーザーが選択した機能によって異なります。

INSERT INTO search_features ('has_basement','True');
INSERT INTO search_features ('wallcolors','white');
INSERT INTO search_features ('rooftype','inclined');

...

Python(この場合、FEAT_COUNTは3になります) )、ただし、クエリごとに追加のSELECT COUNT(*)FROM search_featuresを実行できます。

次にクエリを実行します。

SELECT DISTINT house_pkid,count(HF.feature_name)
FROM house_features HF 
     INNER JOIN search_features SF 
     ON SF.FEAT_NAME=HF.feature_name AND SF.FEAT_VALUE=HF.feature_value
GROUP BY house_pkid
HAVING count(HF.feature_name) = %FEAT_COUNT

ボーナスは、機能のリストが変更された場合、何にも触れる必要がないことです。

0
Gnudiff