File: README
Description
To ensure your test cases call efficient MySQL…
def test_my_case
assert_efficient_sql do
# just wrap them in this block!
end
end
The assertion intercepts and copies out your MySQL SELECT statements, then calls EXPLAIN on each one, and inspects the results for common problems.
The goal is test cases that resist database pessimization, even as you change your data relations, to add new features. If you run your tests after every few changes, you can easily detect which change broke your database‘s indices and relations.
Credits to Jay Pipe for his excellent little survey of ways to detect pessimizations!
Install
piston is the best way to install this assertion:
piston import svn://rubyforge.org/var/svn/efficient-sql/ \
vendor/plugins/efficient-sql
The documented Rails way is the worst:
script/plugin install -x svn://rubyforge.org/var/svn/efficient-sql
The -x option bonds your Subversion repository to rubyforge.org. Each time you svn update your own code, you will also automatically get the latest version of the plugin, whether you need it or not. piston avoids these fun issues. You can update on command, with piston update…
Diagnose
If your SQL is already efficient, use :verbose to diagnose why it‘s efficient. See AssertEfficientSqlTest#test_verbose
It prints a table like this to STDOUT:
query for SELECT * FROM foos WHERE (foos.`id` = 42) LIMIT 1
Foo Load
select_type | key_len | type | id | Extra | possible_keys | table | rows | ref | key
-------------------------------------------------------------------------------------------
SIMPLE | 4 | const | 1 | | PRIMARY | foos | 1 | const | PRIMARY
Reading a table‘s primary key is naturally efficient, so the type field is a healthy const.
See the MySQL document Optimizing Queries with EXPLAIN to learn what those results mean.
Optimize
AssertEfficientSqlTest#test_assert_inefficient_sql shows assert_efficient_sql failing:
Its failure would look like this:
1) Failure:
test_assert_inefficient_sql(AssertEfficientSqlTest)
[./lib/assert_efficient_sql.rb:220:in `analyze_efficiency'
./test/assert_efficient_sql_test.rb:67:in `test_assert_inefficient_sql']:
Pessimistic
query for select * from foos a
full table scan
Foo Load
select_type | key_len | type | id | Extra | possible_keys | table | rows | ref | key
------------------------------------------------------------------------------------
SIMPLE | | ALL | 1 | | | a | 43 | | .
7 tests, 9 assertions, 1 failures, 0 errors
Most queries should not blindly read ALL rows in a database table.
If that query used a WHERE condition that selects fewer records, but if EXPLAIN produced a query type of ALL, this would indicate MySQL might read the entire database table just to apply the WHERE. The best fix, in that situation, is to add an index (a MySQL "KEY") to the table, and put the fields from that WHERE condition into it.
That technique allows MySQL to read the index first, then chop directly thru the table to the records you need.
Pessimize
Sometimes you need an ALL, even while other assert_efficient_sql checks must pass. To positively declare we like ALL, pass in :ALL => true.
See AssertEfficientSqlTest#test_assert_all
Assertions use positive reinforcement; they state conditions we like. They don‘t try to deny conditions we don‘t like. If :ALL => false meant "false to warn about ALL", the assertion would confuse developers. So we use :ALL => true to allow queries to scan all rows.
Options
If assert_efficient_sql (generally) dislikes your arguments, it will print out its default options, each with an explanation.
See AssertEfficientSqlTest#test_help
Nesting
You can also nest the assertion, to provide different options for different blocks. The assertion allows this because your test might also have some other reason to use blocks.
See AssertEfficientSqlTest#test_nest
Using filesort
If your WHERE and ORDER clauses are too complex, MySQL might need to write a file (or worse), just to satisfy a query. assert_efficient_sql detects this pernicious situation:
See AssertEfficientSqlTest#test_prevent_filesorts
If you want to use the assertion without faulting on filesorts, pass the option :Using_filesort => true.
Throttle
One common pessimization is a query that reads thousands of rows just to return a few. assert_efficient_sql counts the rows hit in each phase of an SQL SELECT, and faults if any row count exceeds 1,000.
Adjust this count with :throttle => 42.
See AssertEfficientSqlTest#test_throttle
SHOW SESSION STATUS
assert_efficient_sql calls SHOW SESSION STATUS before and after its sampled block. If you are seeking an advanced pessimization, such as Created_tmp_disk_tables, pass :Created_tmp_disk_tables => 0. The assertion will compare difference in STATUS before and after calling its block. A difference greater than the allowed difference will trigger a fault.
See AssertEfficientSqlTest#test_declare_futile_war_on_Innodb_rows_read