I have a table that looks like this:
CREATE TABLE tracks (id SERIAL, artists JSON); INSERT INTO tracks (id, artists) VALUES (1, '[{"name": "blink-182"}]'); INSERT INTO tracks (id, artists) VALUES (2, '[{"name": "The Dirty Heads"}, {"name": "Louis Richards"}]');
There's several other columns that aren't relevant to this question. There's a reason to have them stored as JSON.
What I'm trying to do is lookup a track that has a specific artist name (exact match).
I'm using this query:
SELECT * FROM tracks WHERE 'ARTIST NAME' IN (SELECT value->>'name' FROM json_array_elements(artists))
for example
SELECT * FROM tracks WHERE 'The Dirty Heads' IN (SELECT value->>'name' FROM json_array_elements(artists))
However, this does a full table scan, and it isn't very fast. I tried creating a GIN index using a function names_as_array(artists)
, and used 'ARTIST NAME' = ANY names_as_array(artists)
, however the index isn't used and the query is actually significantly slower.
To get the index from a JSON object with value with JavaScript, we can use the array findIndex method.
To find the index of an object in an array, by a specific property: Use the map() method to iterate over the array, returning only the value of the relevant property. Call the indexOf() method on the returned from map array. The indexOf method returns the index of the first occurrence of a value in an array.
You can index JSON data as you would any data of the type you use to store it. In addition, you can define a JSON search index, which is useful for both ad hoc structural queries and full-text queries.
jsonb
in Postgres 9.4+The binary JSON data type jsonb
largely improves index options. You can now have a GIN index on a jsonb
array directly:
CREATE TABLE tracks (id serial, artists jsonb); -- ! CREATE INDEX tracks_artists_gin_idx ON tracks USING gin (artists);
No need for a function to convert the array. This would support a query:
SELECT * FROM tracks WHERE artists @> '[{"name": "The Dirty Heads"}]';
@>
being the jsonb
"contains" operator, which can use the GIN index. (Not for json
, only jsonb
!)
Or you use the more specialized, non-default GIN operator class jsonb_path_ops
for the index:
CREATE INDEX tracks_artists_gin_idx ON tracks USING gin (artists jsonb_path_ops); -- !
Same query.
Currently jsonb_path_ops
only supports the @>
operator. But it's typically much smaller and faster. There are more index options, details in the manual.
If the column artists
only holds names as displayed in the example, it would be more efficient to store just the values as JSON text primitives and the redundant key can be the column name.
Note the difference between JSON objects and primitive types:
CREATE TABLE tracks (id serial, artistnames jsonb); INSERT INTO tracks VALUES (2, '["The Dirty Heads", "Louis Richards"]'); CREATE INDEX tracks_artistnames_gin_idx ON tracks USING gin (artistnames);
Query:
SELECT * FROM tracks WHERE artistnames ? 'The Dirty Heads';
?
does not work for object values, just keys and array elements.
Or:
CREATE INDEX tracks_artistnames_gin_idx ON tracks USING gin (artistnames jsonb_path_ops);
Query:
SELECT * FROM tracks WHERE artistnames @> '"The Dirty Heads"'::jsonb;
More efficient if names are highly duplicative.
json
in Postgres 9.3+This should work with an IMMUTABLE
function:
CREATE OR REPLACE FUNCTION json2arr(_j json, _key text) RETURNS text[] LANGUAGE sql IMMUTABLE AS 'SELECT ARRAY(SELECT elem->>_key FROM json_array_elements(_j) elem)';
Create this functional index:
CREATE INDEX tracks_artists_gin_idx ON tracks USING gin (json2arr(artists, 'name'));
And use a query like this. The expression in the WHERE
clause has to match the one in the index:
SELECT * FROM tracks WHERE '{"The Dirty Heads"}'::text[] <@ (json2arr(artists, 'name'));
Updated with feedback in comments. We need to use array operators to support the GIN index.
The "is contained by" operator <@
in this case.
You can declare your function IMMUTABLE
even if json_array_elements()
isn't wasn't.
Most JSON
functions used to be only STABLE
, not IMMUTABLE
. There was a discussion on the hackers list to change that. Most are IMMUTABLE
now. Check with:
SELECT p.proname, p.provolatile FROM pg_proc p JOIN pg_namespace n ON n.oid = p.pronamespace WHERE n.nspname = 'pg_catalog' AND p.proname ~~* '%json%';
Functional indexes only work with IMMUTABLE
functions.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With