web-dev-qa-db-fra.com

Recherche de valeurs imbriquées dans le tableau jsonb avec un opérateur supérieur

Voici la définition du tableau (simplifiée):

CREATE TABLE documents (
    document_id int4 NOT NULL GENERATED BY DEFAULT AS IDENTITY,
    data_block jsonb NULL
);

Exemples de valeurs:

INSERT INTO documents (document_id, data_block)
VALUES
   (878979, 
    '{"COMMONS": {"DATE": {"value": "2017-03-11"}},
     "PAYABLE_INVOICE_LINES": [
         {"AMOUNT": {"value": 52408.53}}, 
         {"AMOUNT": {"value": 654.23}}
     ]}')
 , (977656, 
    '{"COMMONS": {"DATE": {"value": "2018-03-11"}},
     "PAYABLE_INVOICE_LINES": [
         {"AMOUNT": {"value": 555.10}}
     ]}');

Je souhaite rechercher tous les documents où l'un des 'PAYABLE_INVOICE_LINES' les éléments contiennent un 'value' supérieur à 1000,00.

Ma requête est

select *
from documents d
cross join lateral jsonb_array_elements(d.data_block -> 'PAYABLE_INVOICE_LINES') as pil 
where (pil->'AMOUNT'->>'value')::decimal > 1000

Mais, comme je veux limiter à 50 documents, je dois regrouper sur le document_id et limitez le résultat à 50.

Avec des millions de documents, cette requête est très coûteuse - 10 secondes avec 1 million.

J'essaie d'ajouter un index GIN sur le tableau de l'objet jsonb. Mais il semble qu'il ne soit appliqué qu'en utilisant un opérateur jsonb comme @>.

Avez-vous des idées pour avoir de meilleures performances?

3
Ryu

Ceci est généralement difficile à optimiser: aucun opérateur direct ou support d'index pour jsonb pour ce type de test.

EXISTS devrait au moins être plus rapide que ce que vous avez, tout en évitant les lignes en double (où plusieurs éléments du tableau correspondent) et la colonne supplémentaire (redondante) pil dans le résultat:

SELECT *
FROM   documents d
WHERE  EXISTS (
   SELECT FROM jsonb_array_elements(d.data_block -> 'PAYABLE_INVOICE_LINES') pil 
   WHERE (pil->'AMOUNT'->>'value')::decimal > 1000
   );

En relation:

Pour rendre cela plus rapide par ordre de grandeur, extrayez la valeur maximale par ligne et enregistrez-la de manière redondante ou utilisez un IMMUTABLE fonction dans un index d'expression très petit et rapide (mais aussi spécialisé):

CREATE OR REPLACE FUNCTION f_doc_max_amout(jsonb)
  RETURNS numeric AS
$func$
   SELECT max((a->'AMOUNT'->>'value')::numeric)
   FROM   jsonb_array_elements($1) a
$func$ LANGUAGE sql IMMUTABLE;

CREATE INDEX documents_max_amount_idx
ON documents (f_doc_max_amout(data_block -> 'PAYABLE_INVOICE_LINES')); 

Requête (doit correspondre à l'expression d'index):

SELECT *
FROM   documents d
WHERE  f_doc_max_amout(data_block -> 'PAYABLE_INVOICE_LINES') > 1000;

dbfiddle ici

6
Erwin Brandstetter