objection.js icon indicating copy to clipboard operation
objection.js copied to clipboard

Query using whereJsonSupersetOf can't take advantage of partial GIN index

Open fiznool opened this issue 4 years ago • 2 comments

I've been following the docs on how to query for a value inside a JSON column and create a partial GIN index on a JSONB column and have hit upon an issue.

Consider the following database schema:

exports.up = knex => {
  return knex.schema
    .createTable('message', table => {
      table.increments('id').primary();
      table.jsonb('content');
    })
    .raw('CREATE INDEX on ?? USING GIN (?? jsonb_path_ops)', [
      'message',
      'content'
    ]);
};

In this case, content is a JSON object which contains a type field, describing the type of message.

The partial GIN index means that we should be able to perform the following query and have it use the index:

const pictureMessages = await MessageModel.query.whereJsonSupersetOf(
  'content',
  { type: 'PICTURE' }
);

According to the linked article from the docs, in order to use the index, the resulting SQL should be:

select "message.*" from "message" where content @> '{"type":"PICTURE"}'::jsonb

However, Objection produces the following query:

select "message.*" from "message" where ( "content"#>'{}' )::jsonb @> '{"type":"PICTURE"}'::jsonb

Sadly, this form of the query doesn't use the index, as can be seen below.

cb=# explain analyse select "message".* from "message" where content @> '{"type":"PICTURE"}'::jsonb;
                                                                 QUERY PLAN
---------------------------------------------------------------------------------------------------------------------------------------------
 Bitmap Heap Scan on message  (cost=8.01..12.02 rows=1 width=503) (actual time=0.124..8.751 rows=853 loops=1)
   Recheck Cond: (content @> '{"type": "PICTURE"}'::jsonb)
   Heap Blocks: exact=55
   ->  Bitmap Index Scan on message_content_index  (cost=0.00..8.01 rows=1 width=0) (actual time=0.084..0.093 rows=853 loops=1)
         Index Cond: (content @> '{"type": "PICTURE"}'::jsonb)
 Planning Time: 0.078 ms
 Execution Time: 16.907 ms
(7 rows)

cb=# explain analyse select "message".* from "message" where ( "content"#>'{}' )::jsonb @> '{"type":"PICTURE"}'::jsonb;
                                                     QUERY PLAN
--------------------------------------------------------------------------------------------------------------------
 Seq Scan on message  (cost=0.00..67.93 rows=1 width=503) (actual time=0.010..11.879 rows=853 loops=1)
   Filter: ((content #> '{}'::text[]) @> '{"type": "PICTURE"}'::jsonb)
   Rows Removed by Filter: 9
 Planning Time: 0.048 ms
 Execution Time: 20.715 ms
(5 rows)

Here's the line of code which wraps the field reference. I guess this is here for an important reason, so if it needs to stay, maybe there could be a way to opt out of this behaviour, and pass through the field reference as-is?

fiznool avatar Feb 22 '21 15:02 fiznool

In the meantime I'm working around this with this custom method on the query builder, but it would be great to learn if this could be done without a workaround!

whereJsonContains(fieldReference, partialJson) {
  return this.whereRaw('?? @> ?', [
    fieldReference,
    JSON.stringify(partialJson),
  ]);
}

fiznool avatar Feb 22 '21 15:02 fiznool

any news about this ?;)

romain130492 avatar Jan 17 '22 05:01 romain130492