1

I have the following tables: in a MySQL 5 database:

  • projects - 1,000 rows
  • tasks (FKed to projects) - 10,000 rows
  • task_tags (FKed to tasks) - 350,000 rows
  • task_comments (FKed to tasks) - 750,000 rows

and want to get a list of projects for which any task has exactly the word urgent in a tag or comment, i.e.

select DISTINCT p.*
from projects p
join tasks t on p.id = t.project_id
left join task_tags tt on t.id = tt.task_id
left join task_comments tc on t.id = tc.task_id
where tt.value = 'urgent' OR tc.text = 'urgent'

Without the DISTINCT, the query executes in well under 0.1 second (and the indices used look sensible). The Visual Execution Plan is (with the tables being projects, tasks, task_tags and task_comments in that order from left to right):

MySQL plan for query without DISTINCT

Adding the DISTINCT makes the query seriously unperformant (on the order of minutes), with the DISTINCT being added on after the last nested loop, and thus I believe requiring MySQL to sort all the resulting rows, and then deduplicate them.

While this is correct, it does not feel like the best possible option--after all, once we know that a project meets the criteria, there is no point checking any other tasks for that project as this isn't e.g. select distinct p.*, t.id - but I don't know how to encourage MySQL to plan this more sensibly because I don't know what plan I want.

I have tried something along the lines of

select distinct project_id
from tasks
where id in (
    select task_id from task_tags where value = 'urgent'
    union
    select task_id from task_comments where text = 'urgent'
)

as an alternative but we are also on the order of minutes for that though I believe it is slightly faster, and I have no other great ideas (except for things not in MySQL 5 like persisted views, but upgrading the DB is not a practical option at this stage).

Any ideas as to what I could try (or even where to investigate next)? The database model is fairly fixed, but certainly I can add indices and happy to look at options.

2
  • Welcome to DBA.SE. It would help if we had the respective DDL of the tables involved, just to on the safe side, and possibly any indexes that may have already been created. Have you tried OPTIMIZE TABLE...? Otherwise a very decent first question. Good luck.
    – John K. N.
    Commented Aug 25, 2021 at 17:36
  • What about the second query, but with union all or as an or Commented Aug 25, 2021 at 19:09

1 Answer 1

1

Let's turn it inside-out so we can see that it is starting at the right places. The Optimizer won't do this work for us.

  1. Start with each table that might say 'urgent'
  2. UNION them. (UNION DISTINCT is slightly slower than UNION ALL, but you might get two duplicate rows. You decide.)
  3. Join to tasks to get the project_id
  4. Finally, reach into projects for the few rows that are needed. (Note how both of your formulations effectively require fetching all of p before figuring out that most of the rows aren't needed.)

Switching from OR to UNION was a good idea, but IN ( SELECT ... ) is not an efficient construct.

SELECT p.*
    FROM (
         SELECT t.project_id
            FROM task_comments tc
            JOIN tasks t  ON t.id = tc.task_id
            WHERE tc.text = 'urgent'  -- see Note
         ) UNION DISTINCT (
         SELECT t.project_id
            FROM task_tags tt
            JOIN tasks t  ON t.id = tt.task_id
            WHERE tt.value = 'urgent'
         ) AS x
    JOIN projects p  ON p.id = x.project_id

That will need

tc:  INDEX(text, task_id)  -- see Note
t:   (I assume you have PRIMARY KEY(id))
tt:  INDEX(value, task_id)
p:   (I assume you have PRIMARY KEY(id))

Note: Perhaps you really want to check for "urgent" anywhere in tc.text? If so, the best way to optimize it is to have

tc:  FULLTEXT(text)

and switch to

WHERE MATCH(tc.text) AGAINST ('+urgent' IN BOOLEAN MODE)
         
4
  • Thanks! That takes it down to about ~2 seconds; not exactly ideal but it's not used that frequently so I guess it'll do. (And in the real case, it is meant to be an equality check, not a fulltext search; I just couldn't come up with a good example so went with that as the example)
    – waiwai933
    Commented Aug 26, 2021 at 13:15
  • @waiwai933 - It might be useful to show the (non-Visual) EXPLAIN for my version with my indexes. (2 seconds seems high; I'll check for a mistake somewhere.) Oh, I assume you don't have thousands of "urgent" projects; if you do, that might be an explanation for "2 seconds".
    – Rick James
    Commented Aug 26, 2021 at 19:16
  • Full explain (both tabular and visual source) at: gist.github.com/waiwaing/f3bd487d7ca500df04d625ec4ac694f6, and about 2k urgent projects, as it were. (FWIW, this is used only in an admin-only interface and we can cache the result, so while tuning it a bit more would definitely be nice, the suggestion you gave of converting the IN to a JOIN is probably sufficient for the use case anyway)
    – waiwai933
    Commented Aug 29, 2021 at 21:43
  • Are there thousands of rows with 'urgent'? Please provide SHOW CREATE TABLE for each table; something looks inconsistent.
    – Rick James
    Commented Aug 30, 2021 at 4:34

Your Answer

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

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