web-dev-qa-db-ja.com

ネストされたjson_build_objectによるPostgres CTE最適化

複数のテーブルからデータを返し、ネストされた1つのJSONフィールドにそれを集約するクエリを記述しようとしています。これはSqlServerで優れたパフォーマンスを発揮すると思いますが、この投稿で Brent Ozarが書きました のように、PostgresオプティマイザはCTEクエリをまとめています。これにより、毎回データセット全体をロードするため、最初のCTEのレベルでWHEREステートメントを使用する必要があります。それと、私が実際に使用していない特定のJSON関数は、これがより良いパフォーマンスを発揮できるかどうか疑問に思います。

CTEなしでこれを書こうとしましたが、サブクエリをネストする方法がわかりませんでした。

私がここで見逃しているpostgresのトリックはありますか?それらのインデックスは効果的ですか?

出力は次のようになります。

[{
    "item_property_id": 1001010,
    "property_name": "aadb480d8716e52da33ed350b00d6cef",
    "values": [
        "1f64450fae03b127cf95f9b06fca4bca",
        "9a6883b8a87a5028bf7dfc27412c2de8"
    ]
},{
    "item_property_id": 501010,
    "property_name": "e870e8d81e16ee46c75493856b4c6b66",
    "values": [
        "a6bed25b407c515bb8a55f2e239066ec",
        "feb10299fd6408e0d37a8761e334c97a"
    ]
},{
    "item_property_id": 1010,
    "property_name": "f2d7b27c50a059d9337c949c13aa3396",
    "values": [
        "56674c1c3d66c832abf87b436a4fd095",
        "ff88fe69f4438a6277c792faaf485368"
    ]
}]

スキーマとテストデータを生成するスクリプトは次のとおりです

--create schema
drop table if exists public.items;
drop table if exists public.items_properties;
drop table if exists public.items_properties_values;
create table public.items(
    item_id integer primary key,
    item_name varchar(250));                      
create table public.items_properties(
    item_property_id serial primary key,
    item_id integer,
    property_name varchar(250));                      
create table public.items_properties_values(
    item_property_value_id serial primary key,
    item_property_id integer,
    property_value varchar(250));
CREATE INDEX items_index
    ON public.items USING btree
    (item_id ASC NULLS LAST,item_name asc nulls last)
    TABLESPACE pg_default; 
CREATE INDEX properties_index
    ON public.items_properties USING btree
    (item_property_id ASC NULLS LAST,item_id asc nulls last,property_name asc nulls last)
    TABLESPACE pg_default;
CREATE INDEX values_index
    ON public.items_properties_values USING btree
    (item_property_value_id ASC NULLS LAST,item_property_id asc nulls last,property_value asc nulls last)
    TABLESPACE pg_default;

--insert dummy data
insert into public.items                        
SELECT generate_series(1,500000),md5(random()::text);

insert into public.items_properties (item_id,property_name)
SELECT item_id,md5(random()::text) from public.items;
insert into public.items_properties (item_id,property_name)
SELECT item_id,md5(random()::text) from public.items;
insert into public.items_properties (item_id,property_name)
SELECT item_id,md5(random()::text) from public.items;


insert into public.items_properties_values (item_property_id,property_value)
select item_property_id,md5(random()::text) from public.items_properties;
insert into public.items_properties_values (item_property_id,property_value)
select item_property_id,md5(random()::text) from public.items_properties;

--Query returned successfully in 22 secs 704 msec.

ここにSQLコマンドがあります

3行目のwhereがなければ、ロードに最大15秒かかります。これは何千ものレコードを読み込んでいるので、おそらく正常に機能していると思いますが、本当にセカンドオピニオンが好きです。

with cte_items as (
    select item_id,item_name from public.items  
    --where item_id between 1000 and 1010
),cte_properties as (
    select ip.item_id,ip.item_property_id,ip.property_name from public.items_properties ip
    inner join cte_items i on i.item_id=ip.item_id
),cte_values as (
    select ipv.item_property_value_id,ipv.item_property_id,ipv.property_value from public.items_properties_values ipv
    inner join cte_properties p on ipv.item_property_id=p.item_property_id
)
select i.item_id,i.item_name,json_agg(json_build_object('item_property_id',prop.item_property_id,'property_name',prop.property_name,'values',prop.values))
from cte_items i
left join (
    select cp.item_id,cp.item_property_id,cp.property_name,json_agg(to_json(cv.property_value)) "values"
    from cte_properties cp
    left join ( select val.item_property_id,val.property_value from cte_values val ) cv on cv.item_property_id=cp.item_property_id
    group by cp.item_id,cp.item_property_id,cp.property_name
) prop
on i.item_id=prop.item_id
group by i.item_id,i.item_name
3
A_V

@ jjanesが書いたもの 最適化フェンスとして機能するCTEについて。

特定のクエリでは、最初にCTEは必要ありません。他のほとんどのノイズも含まれていません。私が見るものは、ネストされたサブクエリの2つのレベルを持つSELECTに削減できます。

SELECT item_id, item_name, js
FROM   items i
LEFT   JOIN (
   SELECT item_id, json_agg(json_build_object('item_property_id',item_property_id,'property_name',property_name,'values',values)) AS js
   FROM   items_properties
   LEFT   JOIN (
      SELECT item_property_id, json_agg(property_value) AS values
      FROM   items_properties_values
      GROUP  BY 1
      ) ipv USING (item_property_id)
   GROUP  BY 1
   ) ip USING (item_id)
ORDER  BY 1, 2;

db <> fiddle ここ

私のクイックテストでは2倍以上高速でした。

テーブル全体をクエリしている間は、最初に集計し、後で結合する方がはるかに高速です。デモのように、集計ごとに2行または3行を超える行がある場合は、さらに単純化される可能性があります。

関連:

1

あなた(またはブレント)は、CTEがPostgreSQLの最適化フェンスであることを理解しています。その制限を取り除くことについて アクティブな作業 があり、 しかし、その作業が次のリリースであるv12に組み込まれることについては、あまり楽観的ではありません。

量産コードで選択のみのCTEを使用することはほとんどありません。 CTEが選択のみで、置き換え可能なパラメーターが含まれていない場合は、通常、CTEからビューを作成します。どちらが良いコードだと思いますし、最適化のフェンスの問題を回避します。実際、本番コードでいくつかの選択のみのCTEを見つけることができる唯一の場所は、プランナーが知っている相関に基づいてクエリを誤って最適化しないようにするために、特に最適化フェンスの動作が必要な場所ですが、プランナーはそうではありません。

1
jjanes