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)。アプリケーションが進化するたびにデータベースを変更する必要はなく、新しいタイプの機能が必要です。
例として、次の機能があるとします。
明らかに、ブール値、整数、および浮動小数点数を文字列として格納することはあまり効率的ではなく、これも私が注意する必要があることです。各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に数十の条件があるかもしれないことを考えると、それほど効率的ではないようです。
これを行うより良い方法はありますか?
機能を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つのことに注意してください。
さて、これが機能し、遅くない場合は、最適化に関して、私はまだ改善できることに気づきました:
これは与える :
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。
特に、検索する機能の数が多い場合は、メガクエリステートメントの作成を回避するために、代わりに検索された機能を保持する一時テーブルを作成するを検討し、簡単な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
ボーナスは、機能のリストが変更された場合、何にも触れる必要がないことです。