Is there a faster way to count JSONB tags?
This should be faster and simpler overall:

SELECT *, tag1 + tag2 AS total_tags
FROM  (
   SELECT (data-&gt;&gt;'recruiter_id')::int AS recruiter_id  -- cheaper to group by int
        , count(*) FILTER (WHERE data-&gt;'tags' ? 'tag1') AS tag1
        , count(*) FILTER (WHERE data-&gt;'tags' ? 'tag2') AS tag2
   FROM   people
   WHERE  data-&gt;'tags' ?| ARRAY['tag1','tag2']
   GROUP  BY 1
   ) target;



Assuming that recruiter_id is an integer entity, it's cheaper to group by the integer value than the jsonb object containing an integer value. I also assume, you'd rather get the integer value in the result anyway.
Only count once in the subquery, then add counts for the total sum in the outer SELECT.
Use the aggregate FILTER clause for conditional counts:


Return counts for multiple ranges in a single SELECT statement

If you want shorter syntax, this gets you the same result and performance: 

count(data-&gt;'tags' ? 'tag1' OR NULL) AS tag1



Index and missing statistics for jsonb

Typically, indexes are the deciding factor for performance with big tables. But since your query retrieves 48.493 of 400.000 rows, i.e. > 12 %, the index is not helping this query at all.

Why the poor decision? The query planner has no statistics for values inside a json / jsonb object and has to chose the best query plans based on generic selectivity estimates. It expects to find rows = 550 while the query actually finds ~ 90 x as many (rows=48493). The query plan using bitmap index scans is a poor decision. A sequential scan would be faster (not using the index at all).

The index may still be instrumental for less frequent tags (if you have those), an expression index on data-&gt;'tags' should serve best. Maybe even a jsonb_path_ops index, combined with an adapted query. More:


Index for finding an element in a JSON array


However, for this and other reasons, while working with common tags, a plain Postgres array or a fully normalized schema will beat the performance of your jsonb object by a long shot.

This discussion in the postgresql-performance list is about your problem exactly:


https://www.postgresql.org/message-id/54C738E1.8080405@agliodbs.com