7

I have one Postgres table with more than 20M tuples:

first_name | last_name | email
-------------------------------------------
bat        | man       | batman@wayne.com
arya       | vidal     | foo@email.com
max        | joe       | bar@email.com

To filter the records I'm using:

SELECT *
  FROM people
WHERE (first_name || '' || last_name) ILIKE '%bat%man%' OR 
    first_name ILIKE '%bat%man%'  OR  
    last_name ILIKE '%bat%man%'   OR
    email ILIKE '%bat%man%'
    LIMIT 25 OFFSET 0

Even with indexes the search is taking almost one minute to return results.
There are indexes for (first_name || '' || last_name), first_name, last_name and email.

What can I do to improve the performance of this query?

  • 1
    are you aware of varchar_pattern_ops? – Sahap Asci Oct 8 '15 at 13:03
  • 1
    I presume you are using ordinary B-tree indexes? These won't work for ilike '%something' (with leading wildcard). You'll need to add other indexes. – Colin 't Hart Oct 8 '15 at 13:09
  • 1
    Which version of PostgreSQL are you using? Please edit this (and the fact that you are using B-tree indexes) into your question so we can delete these comments. – Colin 't Hart Oct 8 '15 at 13:33
  • 1
    ...and (as always) your actual table definition that shows actual data types and constraints: CREATE TABLE script or what you get with \d people in psql. – Erwin Brandstetter Oct 8 '15 at 13:41
  • 3
    @SahapAsci: The *_pattern_ops operator classes are useful with left-anchored patterns, but impotent for the demonstrated use case. – Erwin Brandstetter Oct 8 '15 at 15:10
12

For your kind of pattern matching you best use a trigram index. Read this first:

I assume there's a typo in your expression (first_name || '' || last_name), which makes no sense with an empty string, and you really want (first_name || ' ' || last_name) - with a space character.

Assuming that either column can be NULL, you would need NULL-safe concatenation, the simple solution is concat_ws():

But that function is not IMMUTABLE (explanation in the linked answer), so you cannot use it directly in an index expression. You could use an IMMUTABLE function wrapper:

CREATE OR REPLACE FUNCTION f_immutable_concat_ws(s text, t1 text, t2 text)
  RETURNS text AS
$func$
SELECT concat_ws(s, t1, t2)
$func$ LANGUAGE sql IMMUTABLE;

The wrapper can be IMMUTABLE because it only takes text parameters.
Either way, this is more verbose but has less internal overhead and is considerably faster:

CREATE OR REPLACE FUNCTION f_immutable_concat_ws1(s text, t1 text, t2 text)
  RETURNS text AS
$func$
SELECT CASE
         WHEN t1 IS NULL THEN t2
         WHEN t2 IS NULL THEN t1
         ELSE t1 || s || t2
       END
$func$ LANGUAGE sql IMMUTABLE;

Or, with hard-coded space character:

CREATE OR REPLACE FUNCTION f_concat_space(t1 text, t2 text)
  RETURNS text AS
$func$
SELECT CASE
         WHEN t1 IS NULL THEN t2
         WHEN t2 IS NULL THEN t1
         ELSE t1 || ' ' || t2
       END
$func$ LANGUAGE sql IMMUTABLE;

Basing the index on this function, I suggest:

CREATE INDEX people_special_gin_trgm_idx ON people
USING gin (f_concat_space(first_name, last_name) gin_trgm_ops, email gin_trgm_ops);

I added email as second index column for several considerations.

Creating the index will take a while for 20M rows, best not during top load, or maybe use CREATE INDEX CONCURRENTLY .... A GIN index is considerably bigger than a plain btree index and also more expensive to maintain. Be sure to run the latest version of Postgres, there have been major improvements for GIN indexes in recent versions.

Then your slightly adapted and simplified query should be fast and correct:

SELECT *
FROM   people
WHERE  f_concat_space(first_name, last_name) ILIKE '%bat%man%' OR
       email ILIKE '%bat%man%'
LIMIT  25;

You only need the one index for this query.

Basics for pattern matching:

  • 1
    Heh. Fast(ish) and correct. Infix searches are always going to be a bit slow. As for concat_ws, do you know why it's not immutable? I'd suggest checking that before just wrapping it, otherwise you might land up with index contents that don't match the current expression results for the same input, which behaves like index corruption. – Craig Ringer Oct 9 '15 at 4:56
  • 2
    @CraigRinger: I researched concat_ws() not being IMMUTABLE and added an explanation to the answer referenced above. The explanation is rather simple and my wrapper is safe because it only takes text parameters. – Erwin Brandstetter Oct 9 '15 at 5:00
  • Thanks, much appreciated. I only raised it because I really don't want to encourage people to just bypass !immutable flags with wrappers. – Craig Ringer Oct 9 '15 at 5:01
  • @CraigRinger: You are totally right about that. I have seen crimes happening in that dark alley before: stackoverflow.com/a/11007216/939860. I added a note above for why the wrapper is safe. – Erwin Brandstetter Oct 9 '15 at 5:06

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.