The query which does a lower() on the two columns can take a _long time_ if you have a big users table. The use of a function means it is unable to use an index (well, at least since there is no index on the column that uses the function as well).

So...we should see what we can do to make this faster.

1. Perhaps we can check if we are on mysql and do a query without the lower in that case.
2. Perhaps we could make it a configurable option (without any admin UI) that defaults to using the "lower" query but can be changed (i.e. via settings.php) to use the non-lower query. This would behave the same for small sites and then sites that really need performance would be able to set the variable.

3. I think we could add an index to the user table, but this would slow down inserts/updates.

Comments

greggles’s picture

More specifically the query is:

    if ($name = db_result(db_query("SELECT name FROM {users} WHERE LOWER(mail) = LOWER('%s')", $form_state['values']['name']))) {
greggles’s picture

Status: Active » Needs review
StatusFileSize
new987 bytes
new1.63 KB

So, if we just get rid of the LOWER() functions this will 1) still work on mysql 2) get to use the index.

Here is the explain statement:

mysql> explain SELECT name FROM users where mail = 'greg@example.com';
+----+-------------+-------+------+---------------+------+---------+-------+------+-------------+
| id | select_type | table | type | possible_keys | key  | key_len | ref   | rows | Extra       |
+----+-------------+-------+------+---------------+------+---------+-------+------+-------------+
|  1 | SIMPLE      | users | ref  | mail          | mail | 195     | const |    1 | Using where | 
+----+-------------+-------+------+---------------+------+---------+-------+------+-------------+
1 row in set (0.13 sec)

mysql> explain SELECT name FROM users where lower(mail) = lower('greg@example.com');
+----+-------------+-------+------+---------------+------+---------+------+---------+-------------+
| id | select_type | table | type | possible_keys | key  | key_len | ref  | rows    | Extra       |
+----+-------------+-------+------+---------------+------+---------+------+---------+-------------+
|  1 | SIMPLE      | users | ALL  | NULL          | NULL | NULL    | NULL | 3527497 | Using where | 
+----+-------------+-------+------+---------------+------+---------+------+---------+-------------+

Note how the first query without the lower gets a possible_key of mail and only has to scan 1 row. The second query has to scan all 3 million+ records :(

The attached patch creates a variable that has no user interface, but it can be used based on the documentation in the new README.txt

dww’s picture

Status: Needs review » Needs work

I appreciate the desire to fix the performance here. However, a few problems:

A) I'd rather we tested the DB type and if we're on mysql default to the right query automatically, instead of documenting how to solve this for the 99% of users that are on a mysql DB already.

B) This seems awkward:

+    if (variable_get('email_registration_alternate_query', FALSE)) {
+      $query = variable_get('email_registration_alternate_query', "SELECT name FROM {users} WHERE lower(mail) = lower('%s')");
+    }
+    else {
+      $query = "SELECT name FROM {users} WHERE lower(mail) = lower('%s')";
+    }

What's wrong with just:

    $query = variable_get('email_registration_alternate_query', email_registration_default_query());

? email_registration_default_query() would have the logic to default to the right thing if we're on mysql...

YK85’s picture

subscribing - i was wondering if there has been any further progress in improving performance?
thank you!

pdrake’s picture

Status: Needs work » Needs review
StatusFileSize
new2.53 KB

Attached is a patch which optimizes two queries in email_registration (including this one) based on the global db_type.

greggles’s picture

StatusFileSize
new823 bytes

Here's a patch based on #3.

greggles’s picture

Also, I'm going roughly with option B from #3. One reason I don't like the database type test is that it's not really accurate to say that "mysql is case insensitive." Mysql's defaults are to be case insensitive but it's possible to make a column case sensitive (as was done to drupal.org usernames for a while). Making it a variable also lets someone optimize the query for their own mongo, sqlite, oracle, sql server, etc.

greggles’s picture

StatusFileSize
new1.78 KB

Well, that was using the wrong query by default. We should default to the version without lower (right?).

pdrake’s picture

Status: Needs review » Needs work

It is possible to make the column case sensitive in MySQL. It is also possible to move that data to an entirely different data store (eg. mongo) or to rename the column to something else, but I can't think of any other modules where the SQL queries are obtained from variables for interoperability with custom data storage configurations. It seems like db_rewrite_sql() is the accepted method for enabling that type of query customization. Setting this to needs work, but I'm not trying to be difficult here, so if you disagree feel free to set it back to needs review.

greggles’s picture

That's fine, I'm not personally affected by this and don't plan to commit it in the near future unless a clear consensus comes.

While you're right that getting the query from a variable is not common it's also not common to use the global db type to flip around.

I'm not really familiar with db_rewrite_sql being used for performance purposes - do you have any examples of that?

pdrake’s picture

I did not intend to indicate that db_rewrite_sql is commonly used as a performance tool, but rather that it is commonly used to allow modification of a query (as might be done to support a custom SQL configuration). $db_type is used in various D6 contrib modules to customize SQL queries for specific databases (cck, devel, date, radioactivity, user_relationships & views are a few I'm familiar with) though I'm not certain how many (if any) of those are for performance reasons.

greggles’s picture

Version: 6.x-1.x-dev » 7.x-1.x-dev
Issue summary: View changes
Status: Needs work » Needs review
StatusFileSize
new2.95 KB

OK, let's revive this. It's now affecting me :)

In #279851: Replace LOWER() with db_select() and LIKE() where possible there was a system created for cross-db compatible case insensitive queries. It's documented at https://www.drupal.org/update/modules/6/7#nomorelower which suggests this change:

Drupal 6:

 $result = db_query_range("SELECT value FROM {profile_values} WHERE fid = %d AND LOWER(value) LIKE LOWER('%s%%')", $field, $string, 0, 10);

Drupal 7:

$result = db_select('profile_field')
    ->fields('profile_field', array('category'))
    ->condition('category', db_like($string) . '%', 'LIKE')
    ->range(0, 10)
    ->execute();

So...let's do that for d7.

Also, there's no sense in running the query if the content of the field is not a valid email address, so I've added a check to see if the field is an email. I also moved around some of the logic of the function to keep some of the other features working (I think I got it right, would love a review because I don't use all the features of the module).

greggles’s picture

I've now tested this locally and it seems to work with email_registration_login_with_username both true and false.

I also confirmed that the query is greatly improved.

Here's a slightly updated version of the patch with some whitespace removed and a comment clarified.

greggles’s picture

I had just copy/pasted the db_like without thinking about it too much, but @yesnoio asked whether that was right, so I tested.

I created a user 1 with the email admin@example.com and uid 3 with adm_n@example.com. Without the db_like, the module can get confused about whether the user that is trying to login is 1 or 3. I don't think this is really a security issue, because the password still has to be guessed, but it's not optimal. With the db_like, the condition for adm_n@example.com becomes "mail LIKE 'adm\_n@example.com'". For funzies, here's some queries showing the different behavior:

mysql> select uid, mail from users where mail LIKE 'adm_n@example.com';
+-----+-------------------+
| uid | mail              |
+-----+-------------------+
|   1 | admin@example.com |
|   4 | adm_n@example.com |
+-----+-------------------+
2 rows in set (0.00 sec)

mysql> select uid, mail from users where mail LIKE 'adm\_n@example.com';
+-----+-------------------+
| uid | mail              |
+-----+-------------------+
|   4 | adm_n@example.com |
+-----+-------------------+
1 row in set (0.01 sec)

mysql>
greggles’s picture

Version: 7.x-1.x-dev » 8.x-1.x-dev
Status: Needs review » Patch (to be ported)

OK, committed that.

  • greggles committed 0af4f85 on
    Issue #551626 by greggles: slow query in...
greggles’s picture

Version: 8.x-1.x-dev » 7.x-1.x-dev
Status: Patch (to be ported) » Fixed

Drupal 8 uses `user_load_by_mail` and honestly I'm not sure why Drupal 7 doesn't do that....Anyway, seems this status should be fixed since there's nothing to port forward.

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.