3

I'm trying to search the same text across to fields in my database for a livesearch box.

SELECT DISTINCT u.id, u.username FROM
users AS u, user_invoice AS ui, user_roles AS ur, roles AS r WHERE
u.id = ur.user_id AND
ur.role_id = r.id AND
r.name = 'teacher' AND
(
    ui.user_id = u.id AND
    CAST(ui.invoice AS TEXT) = 'searchterm'
)

This query searches the invoice table and returns results properly and extremely quickly.

SELECT DISTINCT u.id, u.username FROM
users AS u, user_invoice AS ui, user_roles AS ur, roles AS r WHERE
u.id = ur.user_id AND
ur.role_id = r.id AND
r.name = 'teacher' AND
(u.username like '%searchterm%')

This query searches for a matching username and returns extremely quickly as well.

But when I combine the two like this:

SELECT DISTINCT u.id, u.username FROM
users AS u, user_invoice AS ui, user_roles AS ur, roles AS r WHERE
u.id = ur.user_id AND
ur.role_id = r.id AND
r.name = 'teacher' AND
(
    u.username like '%searchterm%' OR
    (
        ui.user_id = u.id AND
        CAST(ui.invoice AS TEXT) = 'searchterm'
    )
)

It returns the proper results but take almost a minute to do so. What am I doing wrong?

EDIT: EXPLAINs of my queries:

First: http://explain.depesz.com/s/PvS

Second: http://explain.depesz.com/s/D5c

Combined: http://explain.depesz.com/s/Dhf


Edited for mistake in copying the cast lines.

8
  • PostgreSQL version? Show explain analyze output for both please; paste to explain.depesz.com and link to it here. Commented Oct 15, 2012 at 23:30
  • You'll probably find out yourself if you rewrite your query into the JOIN-syntax. Commented Oct 15, 2012 at 23:30
  • I must agree with wildplasser - it's so much easier to understand what's going on when using FROM a INNER JOIN b ON (condition) instead of FROM a, b WHERE (condition). Also, how can the 2nd of the three be producing sensible results? You have a cartesian product from the unconstrained join ui in there. Commented Oct 15, 2012 at 23:32
  • @CraigRinger : I only counted the where clause terms and it appeared there was one missing. Been there... Commented Oct 15, 2012 at 23:38
  • Don't know how the second of the 3 is producing sensible results, but it is. What's a cartesian product? Commented Oct 16, 2012 at 0:13

2 Answers 2

1

Here's how I solve this problem in my main app.

I have a main entity that I want users to be able to search for. Call it customer. This entity has associated detail records in a 1:n contact (for phone, email, etc) table.

I define a view, customer_quicksearch, that calculates a quicksearch key - a text field containing the concatenation of contact records for a customer along with some of the customer fields directly.

I've added triggers to customer and contact customer_summary table. The customer trigger adds a record to customer_summary when a row is inserted into customer and delete the row when the customer record is deleted. They update customer_summary by SELECTing an updated quicksearch key from `customer_quicksearch. I could use a SQL function for this instead of a view, but found the view both more useful and faster. With a view it's quicker to calculate the quicksearch keys for all customers, say, after a bulk insert or update.

CREATE VIEW customer_quicksearch AS
SELECT
        customer.id AS customer_id, array_to_string(ARRAY[
                customer.code,
                customer.name,
                string_agg(array_to_string(ARRAY[
                        contact.email::text,contact.altemail::text, contact.mobile_phone, contact.work_phone, contact.home_phone, contact.fax
                ],'|'),'|')
        ], '|') AS quicksearch_key
FROM customer
LEFT OUTER JOIN contact ON (customer.id = contact.customer_id)
GROUP BY customer.id;

and one of the triggers:

CREATE OR REPLACE FUNCTION customer_summary_update_for_contact() RETURNS trigger AS $$
DECLARE
    _customer_id integer;
BEGIN
    -- When a contact is added/removed/changed we have to regenerate the customer search key
    IF tg_op = 'INSERT' OR tg_op = 'UPDATE' THEN
      _customer_id = NEW.customer_id;
    ELSE
      _customer_id = OLD.customer_id;
    END IF;
    UPDATE customer_summary
    SET quicksearch_key = (SELECT quicksearch_key FROM customer_quicksearch WHERE customer_id = _customer_id)
    WHERE customer_id = _customer_id;
    RETURN NULL;
END;
$$
LANGUAGE 'plpgsql'
SET search_path = 'public';

CREATE TRIGGER customer_summary_update_for_contact_trg AFTER INSERT OR UPDATE OR DELETE ON contact
FOR EACH ROW EXECUTE PROCEDURE customer_summary_update_for_contact();

You also need a trigger on customer to handle insert, update and delete of customer, maintaining the customer_summary record for that customer appropriately.

The customer_summary table contains records that include a quicksearch_key that's a pipe-concatenation of fields, like:

'1800MA|1800 MAKE IT BUILDERS|[email protected]|1234 5678|0499 999 999'
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^
[from customer record]        [from 1st contact record]             [from another contact record]

This is searched with a simple LIKE pattern. I could add a text_pattern_ops index on it for improved performance if I was doing prefix searches, but since I'm mostly doing searches with no left or right anchor - LIKE '%search%' - there's no benefit.

Sign up to request clarification or add additional context in comments.

2 Comments

You could use the new trigram index type to make like '%search%' use an index
@a_horse_with_no_name Ooh, good point. It's fast enough over the small-ish data set size that it hasn't proved necessary to worry about much in the way of optimisation, but every little bit helps.
0

No-op (transformed into JOIN syntax) (not an anwer!) :

SELECT DISTINCT u.id, u.username 
FROM
users AS u 
JOIN user_invoice AS ui ON u.username like '%searchterm%'
                        OR ( ui.user_id = u.id AND ui.invoice = CAST('searchterm' AS INTEGER))
JOIN user_roles AS ur ON u.id = ur.user_id
JOIN roles AS r ON ur.role_id = r.id
WHERE r.name = 'teacher'
   ;

The CAST('searchterm' AS INTEGER)) makes no sense to me. Double quotes? parameter?

2 Comments

searchterm can be alphanumeric, while the field invoice is an integer. It was throwing an error without the cast, so I left it in there.
My mistake, I copied the wrong code. I actually CAST(ui.invoice AS TEXT) = 'searchterm'

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.