August 1, 2008

How adding another table to JOIN can improve performance ?

Posted by peter

JOINs are expensive and it most typical the fewer tables (for the same database) you join the better performance you will get. As for any rules there are however exceptions :)

The one I’m speaking about comes from the issue with MySQL optimizer stopping using further index key parts as soon as there is a range clause on the previous key part. So if you have INDEX(A,B) and have a where clause A BETWEEN 5 and 10 AND B=6 only the first part (A) of the index will be used which can be seriously affect performance. Of course in this example you can use index (B,A) but there are many similar cases when it is not possible.
[read more...]

April 28, 2008

The MySQL optimizer, the OS cache, and sequential versus random I/O

Posted by Baron Schwartz

In my post on estimating query completion time, I wrote about how I measured the performance on a join between a few tables in a typical star schema data warehousing scenario.

In short, a query that could take several days to run with one join order takes an hour with another, and the optimizer chose the poorer of the two join orders. Why is one join order so much slower than the other, and why did the optimizer not choose the faster one? That's what this post is about.

Let's start with the MySQL query optimizer. The optimizer tries to choose the best join order based on its cost metric; it tries to estimate the cost for a query, then choose the query plan that has the lowest cost. The unit of cost for the MySQL query optimizer is a single random 4k data page read. In general, it's a pretty good metric, but it has one major weakness: the server doesn't know whether a read will be satisfied from the operating system cache, or whether it'll have to go to disk. (This distinction is abstracted away by the storage engine; the optimizer doesn't know how a given storage engine stores its data).

I'll try to omit the details and keep this clean. Let's take a look at the tables.

SQL:
  1. mysql> SHOW TABLE STATUS LIKE 'fact'\G
  2. *************************** 1. row ***************************
  3. Name: fact
  4. Engine: MyISAM
  5. Rows: 147045493
  6. Avg_row_length: 117
  7. Data_length: 17217646764
  8. Index_length: 11993816064
  9.  
  10. mysql> SHOW TABLE STATUS LIKE 'dim1'\G
  11. *************************** 1. row ***************************
  12. Name: dim1
  13. Engine: MyISAM
  14. Rows: 453193
  15. Avg_row_length: 122
  16. Data_length: 55605116
  17. Index_length: 93812736
  18.  
  19. mysql> SHOW TABLE STATUS LIKE 'dim2'\G
  20. *************************** 1. row ***************************
  21. Name: dim2
  22. Engine: MyISAM
  23. Rows: 811
  24. Avg_row_length: 105
  25. Data_length: 85368
  26. Index_length: 154624

It's a big fact table and two fairly small dimension tables, which is normal. Here is the query:

SQL:
  1. SELECT fact.col1, min(fact.col2) AS min_col2
  2. FROM fact, dim1, dim2
  3. WHERE fact.col4 = dim1.col4
  4. AND dim1.col3 <> 'hello world'
  5. AND dim2.col5 = 1
  6. AND fact.dim2_id = dim2.dim2_id
  7. AND fact.col2> some_const
  8. GROUP BY fact.col1

There are indexes on all the columns in all the ways you'd expect: all the dimension columns are indexed on every table, and there's a separate index on every column in the WHERE clause. Here's the query plan initially.

SQL:
  1. *************************** 1. row ***************************
  2. TABLE: dim1
  3. type: range
  4. key_len: 195
  5. rows: 18790
  6. Extra: USING WHERE; USING TEMPORARY; USING filesort
  7. *************************** 2. row ***************************
  8. TABLE: fact
  9. type: ref
  10. key_len: 4
  11. rows: 606
  12. Extra: USING WHERE
  13. *************************** 3. row ***************************
  14. TABLE: dim2
  15. type: eq_ref
  16. key_len: 2
  17. rows: 1
  18. Extra: USING WHERE

This query will run for days and never complete. No one ever let it finish to see how long it would run.

How do I know it will run for days? Here's my train of thought:

  • It's performing index lookups into the fact table, which is big.
  • An index lookup is a random I/O.
  • A modern disk can do about 100 random I/O's per second, as a rule of thumb.
  • If you do the math with the rows column in EXPLAIN, you realize that this equates to about 18790 * 606 = 11386740 I/O operations, assuming that the indexes are fully in memory.
  • When you divide this by 100 I/O operations per second, and then divide that by 86400 seconds in a day, you get about 2.6 days.

Ouch! That's slow.

Now let's look at the alternative: table-scan the fact table, and do index lookups in the two dimension tables. MySQL doesn't want to choose this join order, so we'll force it with STRAIGHT_JOIN:

SQL:
  1. EXPLAIN SELECT STRAIGHT_JOIN  ....
  2. +-------+-----------+-----------+---------------------------------+
  3. | TABLE | type      | rows      | Extra                           |
  4. +-------+-----------+-----------+---------------------------------+
  5. | fact  | ALL       | 147367284 | USING TEMPORARY; USING filesort |
  6. | dim1  | eq_ref    | 1         | USING WHERE                     |
  7. | dim2  | eq_ref    | 1         | USING WHERE                     |
  8. +-------+-----------+-----------+---------------------------------+

As we saw in the previous post, which I linked at the top of this post, we can scan the fact table in less than an hour. And it turns out that joining to the dimension tables doesn't slow the query perceptibly, because these tables are small and they stay in memory, in the OS cache. (They don't get evicted from memory by the cache's LRU policy, because they are frequently used -- once per row in the fact table. The LRU policy evicts old blocks from the fact table instead, which is perfect -- these blocks are used only once and not needed again, so they can be replaced).

The difference between the two queries -- 55 minutes and 2.6 days -- is basically the difference between scanning data sequentially on disk and random disk I/O.

So now you know why one join order is faster than the other. But why didn't the optimizer know this, too? The optimizer does know that random access is slower than sequential access, but it doesn't know that the dimension tables will stay in memory, and this is an important distinction.

Let's put ourselves into the mindset of the optimizer. We'll assume that every join to the dimension tables will go to disk instead of being read from cache. Now the STRAIGHT_JOIN becomes a table scan of about 313 sequential reads (150 million rows / 117 bytes per row / 4096 bytes per read), plus about 150 million random I/Os for the first dimension table, plus 150 million random I/Os for the second dimension table. That's 300 million random I/O operations.

In contrast, the optimizer chose a plan that it thought would cause only 11.3 million random I/O operations.

The optimizer was being smart, given its lack of knowledge about the OS cache. This is why an expert is sometimes needed to provide the missing information. If the MySQL optimizer were right and each of these had to go to disk, our STRAIGHT_JOIN plan would take more than a month to complete! Good thing we know the difference between cache and disk!

April 22, 2008

How to estimate query completion time in MySQL

Posted by Baron Schwartz

Have you ever run a query in MySQL and wondered how long it'll take to complete? Many people have had this experience. It's not a big deal until the query has been running for an hour. Or a day and a half. Just when IS that query going to finish, anyway?

There are actually a few ways to estimate how long it'll take for the query to complete, depending on what the query is. One of the simplest is to estimate how many rows the query needs to examine, measure how fast it's working, and do the math.

As an example, I recently worked on a customer's site where a typical data-warehousing query needed optimization. It was a fact table joined to two dimension tables -- a classic star schema query. The fact table was very large, and after some tuning (I'll write more about that later) I convinced MySQL to perform the query as a table scan of the fact table, then an index lookup in each dimension table in turn.

The table structures aren't really important. All you need to know for this post is that the fact table has about 150 million rows and the query was taking over 10 minutes to complete. Actually, it had never completed at all, according to the customer. I wanted to know whether I'd be waiting for another minute, hours, or days.

The answer was simple, because there was nothing else running on the server. That means that SHOW GLOBAL STATUS gave a rough idea of what the query was actually doing. (If there had been a lot of activity on the server, I wouldn't have been able to say with confidence that SHOW GLOBAL STATUS showed what that one query was doing; activity from other queries would have been mixed in there too. It would be great to be able to choose another thread and watch only its status, but MySQL doesn't currently let you do that.)

The solution was to measure how fast the query was scanning rows in the table scan of the fact table. This is shown by the Handler_read_rnd_next status variable. Here's an easy way to watch it (innotop is another handy way):

CODE:
  1. mysqladmin extended -r -i 10 | grep Handler_read_rnd_next
  2. -- ignore the first line of output...
  3. | Handler_read_rnd_next             | 429224      |

So the server was reading roughly 43K rows per second, and there were 150 million rows in the table. A little math later, and you get 3488 seconds to completion, or a little less than an hour. And indeed the query completed in about 55 minutes.

This is the simplest case, and there are more complicated ones to consider, but hopefully this gives you an idea how you can tackle this problem in different situations.

March 25, 2008

MySQL 6.0 vs 5.1 in TPC-H queries

Posted by Vadim

Last week I played with queries from TPC-H benchmarks, particularly comparing MySQL 6.0.4-alpha with 5.1. MySQL 6.0 is interesting here, as there is a lot of new changes in optimizer, which should affect execution plan of TPC-H queries. In reality only two queries (from 22) have significantly better execution time (about them in next posts), but I want to write about is queries that execute slower in new MySQL 6.0 version.
[read more...]

October 17, 2007

MySQL Performance - eliminating ORDER BY function

Posted by peter

One of the first rules you would learn about MySQL Performance Optimization is to avoid using functions when comparing constants or order by. Ie use indexed_col=N is good. function(indexed_col)=N is bad because MySQL Typically will be unable to use index on the column even if function is very simple such as arithmetic operation. Same can apply to order by, if you would like that to use the index for sorting. There are however some interesting exception.
[read more...]

October 5, 2007

UNION vs UNION ALL Performance

Posted by peter

When I was comparing performance of UNION vs MySQL 5.0 index merge algorithm Sinisa pointed out I should be using UNION ALL instead of simple UNION in my benchmarks, and he was right. Numbers would be different but it should not change general point of having optimization of moving LIMIT inside of union clause being cool thing.

But So is UNION ALL indeed faster than UNION DISTINCT (the UNION is shortcut for UNION DISTINCT) ?
[read more...]

September 18, 2007

Possible optimization for sort_merge and UNION ORDER BY LIMIT

Posted by peter

Every so often you need to perform sort results retrieved from MySQL when your WHERE clause goes beyound col=const values which would allow MySQL to still use second portion of the index for the order by. Ranges as well as IN lists make this optimization impossible, not even speaking about index merge optimization. Lets look at this example:
[read more...]

August 28, 2007

Redundant index is not always bad

Posted by Vadim

About year ago Peter wrote about redundant indexes and mentioned sometimes it is good to leave two indexes, even one is first part of another. I'm speaking about BTREE indexes, for example, KEY (A), and KEY (A,B). From SQL point of view KEY(A) is not needed, as for queries like WHERE A=5 the index (A,B) also can be used.

But there is case when for performance it would be good to have both
[read more...]

April 10, 2007

COUNT(*) vs COUNT(col)

Posted by peter

Looking at how people are using COUNT(*) and COUNT(col) it looks like most of them think they are synonyms and just using what they happen to like, while there is substantial difference in performance and even query result.

Lets look at the following series of examples:

[read more...]

April 6, 2007

Using delayed JOIN to optimize count(*) and LIMIT queries

Posted by peter

In many Search/Browse applications you would see main (fact) table which contains search fields and dimension tables which contain more information about facts and which need to be joined to get query result.

If you're executing count(*) queries for such result sets MySQL will perform the join even if you use LEFT JOIN so it is not needed which slows down things considerably. In similar way MySQL generates full rows while executing queries with limit before throwing them away which makes queries with high offset values very expensive.

To get better performance you can "Help" MySQL and remove JOIN for count(*) and do JOIN after limiting result set for retrieval queries.

Lets look at following simple example with one dimension table. In real life you will usually have several of these so performance improvements can be even higher.

SQL:
  1. CREATE TABLE `fact` (
  2.   `i` int(10) UNSIGNED NOT NULL,
  3.   `val` int(10) UNSIGNED NOT NULL,
  4.   KEY `i` (`i`,`val`)
  5. )
  6.  
  7. CREATE TABLE `dim` (
  8.   `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT,
  9.   `pad` varchar(100) NOT NULL,
  10.   PRIMARY KEY  (`id`)
  11. )
  12.  
  13. mysql> SELECT count(*) FROM dim;
  14. +----------+
  15. | count(*) |
  16. +----------+
  17. |    30720 |
  18. +----------+
  19. 1 row IN SET (0.00 sec)
  20.  
  21. mysql> SELECT count(*) FROM fact;
  22. +----------+
  23. | count(*) |
  24. +----------+
  25. 7340032 |
  26. +----------+
  27. 1 row IN SET (0.00 sec)
  28.  
  29. mysql> SELECT count(*) FROM fact WHERE i<10000;
  30. +----------+
  31. | count(*) |
  32. +----------+
  33. |   733444 |
  34. +----------+
  35. 1 row IN SET (0.44 sec)
  36.  
  37.  
  38. mysql> SELECT count(*) FROM fact LEFT JOIN dim ON val=id WHERE i<10000;
  39. +----------+
  40. | count(*) |
  41. +----------+
  42. |   733444 |
  43. +----------+
  44. 1 row IN SET (2.15 sec)
  45.  
  46.  
  47. mysql> SELECT i,pad FROM fact LEFT JOIN dim ON val=id WHERE i<10000 LIMIT 500000,10;
  48. +------+------------------------------------------+
  49. | i    | pad                                      |
  50. +------+------------------------------------------+
  51. | 6811 | 06bfea523be29a6070488ee66e874dffa170de76 |
  52. | 6811 | 3baf40c2d76998270f8954bedda386b5021e0624 |
  53. | 6811 | 35ad5c3a9d0763acc305992327864bed1af34167 |
  54. | 6811 | 81de98a3ef74ddc0fa4f7c95a27e3dbebca8df0d |
  55. | 6811 | 11cde5d0bd8ffe1eda86b39d05a58c525e8fac8f |
  56. | 6811 | 25c474b380388c23b1de730c4255612e1233e14e |
  57. | 6811 | 1d32b5ba28a513097fc88f3efd91155b2697aeec |
  58. | 6811 | bdc9a39cdfafda26fc2f48a48abd3bc5f051a4ea |
  59. | 6811 | d2e6cb9ca5aa9dd2bc3d033de45579a76ccdafdf |
  60. | 6811 | 0130c708083d77377255bd8f5e0daa15fbb24212 |
  61. +------+------------------------------------------+
  62. 10 rows IN SET (3.88 sec)
  63.  
  64. mysql> SELECT i,pad FROM (SELECT i,val FROM fact WHERE i<10000 LIMIT 500000,10) res LEFT JOIN dim ON val=id;
  65. +------+------------------------------------------+
  66. | i    | pad                                      |
  67. +------+------------------------------------------+
  68. | 6811 | 06bfea523be29a6070488ee66e874dffa170de76 |
  69. | 6811 | 3baf40c2d76998270f8954bedda386b5021e0624 |
  70. | 6811 | 35ad5c3a9d0763acc305992327864bed1af34167 |
  71. | 6811 | 81de98a3ef74ddc0fa4f7c95a27e3dbebca8df0d |
  72. | 6811 | 11cde5d0bd8ffe1eda86b39d05a58c525e8fac8f |
  73. | 6811 | 25c474b380388c23b1de730c4255612e1233e14e |
  74. | 6811 | 1d32b5ba28a513097fc88f3efd91155b2697aeec |
  75. | 6811 | bdc9a39cdfafda26fc2f48a48abd3bc5f051a4ea |
  76. | 6811 | d2e6cb9ca5aa9dd2bc3d033de45579a76ccdafdf |
  77. | 6811 | 0130c708083d77377255bd8f5e0daa15fbb24212 |
  78. +------+------------------------------------------+
  79. 10 rows IN SET (0.30 sec)

So as you can see using this trick we get 5 times speed up for count(*) query and 12 times. This is of course for extremely high offset value but it is also for example with only one dimension which fully fits in memory. for IO bound workload performance difference can be much higher than that.

You may also notice one more trick I'm using here - fact table has covered index on which has val column in it this allow to get join query a bit more optimal.

So right now performance gain may be worth the trick, in the future I hope MySQL Optimizer will be improved so it does these transformations automatically.


This page was found by: optimezer mysql perf...