Billy
Billy

Reputation: 788

Why is this MySQL query slow?

I have the following query, all relevant columns are indexed correctly. MySQL version 5.0.8. The query takes forever:

SELECT COUNT(*) FROM `members` `t` WHERE t.member_type NOT IN (1,2)
AND ( SELECT end_date FROM subscriptions s
WHERE s.sub_auth_id = t.member_auth_id AND s.sub_status = 'Completed'
AND s.sub_pkg_id > 0 ORDER BY s.id DESC LIMIT 1 ) < curdate( )

EXPLAIN output:

----+--------------------+-------+-------+-----------------------+---------+---------+------+------+-------------
id  | select_type        | table | type  | possible_keys         | key     | key_len | ref  | rows | Extra
----+--------------------+-------+-------+-----------------------+---------+---------+------+------+-------------
1   | PRIMARY            | t     | ALL   | membership_type       | NULL    | NULL    | NULL | 9610 | Using where
----+--------------------+-------+-------+-----------------------+---------+---------+------+------+-------------
2   | DEPENDENT SUBQUERY | s     | index | subscription_auth_id, | PRIMARY | 4       | NULL |    1 | Using where
    |                    |       |       | subscription_pkg_id,  |         |         |      |      |            
    |                    |       |       | subscription_status   |         |         |      |      |            
----+--------------------+-------+-------+-----------------------+---------+---------+------+------+-------------

Why?

Upvotes: 1

Views: 166

Answers (4)

Billy
Billy

Reputation: 788

I ended up doing it like this:

select count(*) from members t 
JOIN subscriptions s ON s.sub_auth_id = t.member_auth_id
WHERE t.membership_type > 2 AND s.sub_status = 'Completed' AND s.sub_pkg_id > 0 
AND  s.sub_end_date < curdate( ) 
AND s.id = (SELECT MAX(ss.id) FROM subscriptions ss WHERE ss.sub_auth_id =   t.member_auth_id)

I believe that the problem is due to a bug that won't be fixed until MySQL 6.

Upvotes: 0

jimp
jimp

Reputation: 17487

Your query is slow because as written you are considering 9,610 rows and therefore performing 9,610 SELECT subqueries in your WHERE clause. You really should rewrite your query to JOIN the members and subscriptions tables first, to which your WHERE conditions could still apply.

EDIT: Try this.

SELECT COUNT(*)
FROM `members` `t`
JOIN subscriptions s ON (s.sub_auth_id = t.member_auth_id)
WHERE t.member_type NOT IN (1,2)
AND s.sub_status = 'Completed'
AND s.sub_pkg_id > 0
AND end_date < curdate()
ORDER BY s.id DESC LIMIT 1

Upvotes: 2

user1233508
user1233508

Reputation:

Your subselect refers to values in the parent query. This is known as a correlated (dependent) subquery, and such a query has to be executed once for every row in the parent query, which often leads to poor performance. It is often faster to rewrite the query as a JOIN, for example like this

(Note: without a sample schema to test with, it is impossible to say in advance if this will be faster and still correct, you might need to adjust it a little):

SELECT COUNT(*) FROM members t 
LEFT JOIN (
 SELECT sub_auth_id as member_id, max(id) as sid FROM subscriptions
 WHERE sub_status = 'Completed'
 AND sub_pkg_id > 0
 GROUP BY sub_auth_id
 LEFT JOIN (
  SELECT id AS subid, end_date FROM subscriptions
  WHERE sub_status = 'Completed'
  AND sub_pkg_id > 0
 ) sdate ON sid = subid
) sub ON sub.member_id = t.member_auth_id
WHERE t.member_type NOT IN (1,2)
AND sub.end_date < curdate( )

The logic here is:

  1. For each member, find his latest subscription.
  2. For each latest subscription, find its end date.
  3. Join these member-latest_sub_date pair to the members list.
  4. Filter the list.

Upvotes: 2

Andrea
Andrea

Reputation: 214

Caveat: I'm not a MySQL expert, but pretty good in a different SQL flavour (VFP), but I believe you will save some time if:

  1. You count just one field, let's say memberid, instead of *.

  2. Your comparison NOT IN (1,2) is replaced with > 2 (provided that is valid).

  3. The ORDER BY in your subselect is unnecessary, I think. You're trying to get the last completed subscription?

  4. The < curdate() should be inside your subselect's WHERE.

    (SELECT end_date FROM subscriptions s
    WHERE s.end_date < curdate() and s.sub_auth_id = t.member_auth_id AND 
    s.sub_status =  'Completed' AND s.sub_pkg_id > 0 ORDER BY s.id DESC LIMIT 1 )
    
  5. Tune your subselect so as to trim down the set as quickly as possible. The first conditional should be the one least likely to occur.

Upvotes: 0

Related Questions