web-dev-qa-db-fra.com

Optimisation CTE Postgres avec json_build_Object imbriqué

J'essaie d'écrire une requête qui renvoie des données de plusieurs tables et l'agrége dans un champ JSON imbriqué. J'ai l'impression que cela jouerait bien sur SQLServer, mais, comme Brent Ozar a écrit Dans ce post, les murs de l'optimiseur Postgres Le CTE requis ensemble. Cela me force à utiliser une instruction WHERE au niveau du premier CTE car elle chargerait tout autrement l'ensemble du jeu de données à chaque fois. Cela et les fonctions de JSON spécifiques que je ne suis pas vraiment utilisée pour me faire me demander si cela pourrait mieux performer.

J'ai essayé d'écrire ceci sans CTE mais je n'étais pas sûr de nier les sous-requêtes.

Y a-t-il des astuces Postgres qui me manque ici? Les indices sont-ils efficaces?

La sortie ressemble à ceci:

[{
    "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"
    ]
}]

Voici le script pour générer le schéma et les données de test

--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.

Voici la commande SQL

Sans l'endroit où la troisième ligne prend environ 15 secondes pour charger. Je crois comprendre que cela charge des milliers d'enregistrements, alors peut-être que cela se passe bien, mais j'aimerais vraiment un deuxième avis.

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

Quoi @ jjanes a écrit À propos de CTES agissant comme une clôture d'optimisation.

Votre requête particulière n'a pas besoin de CTES pour commencer - ni la plupart des autres bruits inclus. Ce que je vois peut être réduit à un SELECT avec deux niveaux de sous-requêtes imbriquées:

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 <> violon ICI

Était plus de deux fois plus rapide dans mon test rapide.

Tout en interrogeant des tables entières, il est également beaucoup plus rapide de agrégée d'abord et rejoindre plus tard . Encore plus que lorsque vous avez plus que 2 ou 3 rangées par agrégat, comme dans votre démo, ce qui peut être excessivement excessivant.

En rapport:

1
Erwin Brandstetter

Vous (ou Brent) sont corrects que les CTES sont des clôtures d'optimisation dans PostgreSQL. Il y a travail actif sur l'élimination de cette limitation, mais je ne suis pas très optimiste que ce travail soit incorporé dans la prochaine version, V12.

J'utilise rarement des CTES sélectionnées uniquement dans le code de production. Si la CTE est uniquement sélectionnée et ne contient aucun paramètre remplaçable, je crée généralement une vue en dehors de celle-ci. Ce que je pense, c'est un meilleur code, ainsi que de s'éloigner du problème de la clôture d'optimisation. En effet, les seuls endroits A peuvent trouver quelques CTES Select-Seuls-Seuls dans mon code de production sont là où j'ai spécifiquement besoin du comportement de clôture d'optimisation, afin d'empêcher que le planificateur de mal optimiser les requêtes sur la base des corrélations que je connaisse, mais le planificateur ne .

1
jjanes