Problem/Motivation

Having a Condition class that can be overriden by a third party database is important. The Condition class generates a significant amount of SQL string parts. The currently by Drupal core supported databases may not need such a class, but that does not mean that a third party database driver will not need it. It is fantastic that Drupal core supports more then only MySQL but lets not limit it by only adding what is needed for the by core supported database drivers.

Proposed resolution

Making the class Drupal\Core\Database\Query\Condition overridable by a database driver.

Remaining tasks

  • Write patch
  • Review patch

User interface changes

None.

API changes

Yes. Third party database drivers need to add a Condition class to their driver.

Data model changes

None.

The old issue summary

pwolanin just pointed me to #592522: Hooks node_type, taxonomy and user knocks out our database server.

Digest version: The Apache Solr module has some queries that involve UPDATES with large subselects. Now, we support subselects in UPDATE queries just fine... as subselects. Unfortunately, MySQL's handling of subselects is crap, because it reruns the query for each record (in a select), does stupid stuff with temp tables (disk thrashing), etc. That's fine when the subselect gets a dozen records. When it's selecting a few hundred thousand taxonomy terms (as Solr does), MySQL stops off to its room and slams the door.

MySQL has an alternate syntax that uses Joins(!!!) within the Update query that is a lot faster, and the Solr D6 module is using that now. The D7 version, of course, is not, because it's being a good little module and using the DB API properly. UpdateQuery will therefore dutifully run the ANSI-proper but MySQL-horrid version, and the server will fall over. Fail.

So our options are:

1) Tell modules running such queries to check the DB type (that is still possible) and if they're on MySQL use db_query() directly with an Update query that does the MySQL-proprietary version. If not on MySQL, just do the normal db_update(). This would be the least effort for us in core.

2) Override __toString() for UpdateQuery_mysql and try to detect if we're dealing with a subselect that we can optimize, then try to optimize it. This would be the most elegant solution and the best for contrib, but I don't know how feasible it is in practice.

3) Peter suggested adding tagging support to UpdateQuery so that modules could tag them as "needs subselect optimization" and then offer suggestions about how to optimize the query. This would be a sort of extended version of #2, but is a deeper structural change.

I am open to suggestions as to the best route forward.

Comments

pwolanin’s picture

Priority: Normal » Major

Marking this as major - since #1 is clearly discouraged.

Anonymous’s picture

Issue tags: +Needs committer feedback

blurgh, mysql sure has some sharp bits.

i guess we need committer feedback.

i think i'd favour #3, only because #2 sounds hard to get right. at least with #3, any mistakes would be more explicit.

webchick’s picture

Issue tags: -Needs committer feedback

I have no idea why you need committer feedback when this hasn't even been discussed yet. Please call me in when things are deadlocked and there's no way forward. I can't commit when I'm feedbacking.

pwolanin’s picture

The pattern we are trying to match is simple enough that it feels like #2 should be feasible in at least osme cases.

I'd still love to see a query tag/alter for everything, not just selects.

chx’s picture

You can double up your subselects to avoid the problem http://www.xaprb.com/blog/2006/04/30/how-to-optimize-subqueries-and-join...

Crell’s picture

Priority: Major » Normal

It sounds like chx has a viable fix, and I think it's a sufficient edge-case that most modules won't hit it. Also talking to webchick we decided that we're not going to go with the API changing approach (#3), so this isn't a 7.0 blocker.

Worst case there's also workarounds (using db_query()).

Marking back down to normal.

pwolanin’s picture

Where's the fix? I don't see in the article that this works for UPDATE queries, which was the original focus of this issue.

Crell’s picture

Can you try doing a double-subselect as the issue mentions, and see what happens?

pwolanin’s picture

from IRC:

[5:20pm] davidstrauss: pwolanin, I would probably pick one of these strategies, in order (1) double-wrap the subquery, (2) extend the API to handle the special case of checking for existence of something in a foreign table, (3) give up and have Solr use a different query for mysql

So #2 would be an API addition. #1 is less efficient than the JOIN method, but easier to put in core. #3 is easiest, but leaves the rest of contrib and core out in the code.

Crell’s picture

Given that we're on the cusp of an RC, I'd favor #1. It should still work well enough here, and it means we don't have to change anything in core.

And I find it quite amusing that you said "out in the code". :-)

damien tournoud’s picture

Status: Active » Needs review
StatusFileSize
new2.49 KB

Rough starting point.

chx’s picture

The issue has absolutely nothing to do with updates. Subselects suck on SELECT or UPDATE equally. So while Damien's patch is right it just needs to verify the existence of a subselect and if it's not marked up for prevention then yes probably wrap it up.

Edit: In some cases, the optimizer code can execute the subquery for every row. That's not true. The truth is In every case MySQL runs the subquery for every row. MySQL only has dependent subqueries, no matter what. Anonymous views are, however, not translated into dependent subqueries only because that's simply not possible as the tables need to be resolved before the result set can even exist so independent subqueries are moved to an anonymous view.

Crell’s picture

Here's a crazy idea. Currently, I think DatabaseCondition is one of the only classes that a given DB driver cannot extend and customize. Should they be able to? Would it make more sense to put this double-wrapping into a DatabaseCondition_mysql class?

I'm not sure what the knock-on effect of that would be off hand, but I think it could be done without an external API change. At worst it would only affect drivers, which are quite few.

Someone tell me that's a stupid idea... :-)

pwolanin’s picture

@Crell - putting it into the condition object might be cleaner, since that's logically where it lives.

pwolanin’s picture

StatusFileSize
new2.12 KB

Revised patch fixing the comments and making this apply in all cases where we are running a subquery.

Crell’s picture

StatusFileSize
new7.79 KB

OK, so it's a very good thing I spent time this weekend working on this issue as in the process I stumbled across a bug in the conditional compiler for subqueries. :-) It wasn't preprocessing the query properly, so sub-sub queries were dying completely.

The attached patch does 3 things:

1) Fix the aforementioned bug.

2) Make it possible for drivers to implement their own DatabaseCondition classes, just like they can for everything else.

3) Add a MySQL-specific DatabaseCondition class that preprocesses subqueries in where clauses into wrapped FROM subqueries, as per the article chx linked above.

It should be transparent for any existing well-behaved code, but very-big WHERE subqueries in MySQL should (in theory) just kinda get faster as a result of this patch.

Crell’s picture

StatusFileSize
new7.76 KB

Same thing but with less-wrong indentation.

damien tournoud’s picture

As already mentioned on IRC, I'm rather reluctant to wrap *all* subqueries into an additional SELECT. My reasoning is:

  • For UPDATE and DELETE, this is actually a bug fix: MySQL by default doesn't support referencing the same table in the main query and the subquery (so, for example: DELETE FROM table WHERE id IN (SELECT id FROM table WHERE id > 0) will trigger an error). In that case the functionality is very broken, and it makes sense to fix it.
  • For SELECT, this is just a performance optimization, and as a consequence is very much dependent on MySQL version.

For example, on MariaDB 5.1:

MariaDB [drupalorg]> pager cat > /dev/null
PAGER set to 'cat > /dev/null'

MariaDB [drupalorg]> DELETE FROM users WHERE uid IN (SELECT uid FROM users WHERE uid < 0);
ERROR 1093 (HY000): You can't specify target table 'users' for update in FROM clause

MariaDB [drupalorg]> SELECT uid FROM node;
601506 rows in set (0.41 sec)

MariaDB [drupalorg]> SELECT SQL_NO_CACHE * FROM users WHERE uid IN (SELECT uid FROM node);
86572 rows in set (4.07 sec)

MariaDB [drupalorg]> SELECT SQL_NO_CACHE * FROM users WHERE uid IN (SELECT * FROM (SELECT uid FROM node) _table);
Ctrl-C -- query killed. Continuing normally.
ERROR 1317 (70100): Query execution was interrupted

I had to interrupt the double-wrapper subquery, it ran for more then 20 minutes.

Crell’s picture

Priority: Normal » Major

Well fart.

I have to veto forking the logic by MySQL version. That way lies madness. But I don't know what else to do here. Do we know what versions of MySQL don't suck on WHERE subqueries?

(Either way the rest of the patch should still go in, IMO, even if we don't subclass for MySQL in this case.)

Crell’s picture

StatusFileSize
new6.08 KB

Per discussion with webchick, attached is a patch that just does #1 and #2 above. There should be no net effect from this patch other than a bug fix and allowing drivers to have their own condition classes. We can then debate what if anything to do with the MySQL condition class later, as that's not an RC-sensitive fix but the other two are.

pwolanin’s picture

So this patch doesn't fix the bug?

How can we let a condition know that it's a condition on a update/delete versus a select?

Crell’s picture

#20 sets up extra flexibility to fix the performance issue mentioned in #0. Per #18 it may not be so simple to fix due to conflicting server versions. :-( The extra flexibility and bug fix should go in either way, IMO.

chx’s picture

Status: Needs review » Needs work

The doxygen is totally useless. The rest is probably useful, yes.

Crell’s picture

Status: Needs work » Needs review
StatusFileSize
new6.1 KB

Revised docblock.

Crell’s picture

Someone want to RTBC this? Please?

moshe weitzman’s picture

Status: Needs review » Reviewed & tested by the community

More flexibility is good. Bot is happy, and we know that DBTNG is heavily tested.

webchick’s picture

catch’s picture

Version: 7.x-dev » 8.x-dev
Issue tags: +Needs backport to D7
catch’s picture

catch’s picture

Title: No optimization for very large subselects » dbtng drivers are not able to have their own condition classes
pwolanin’s picture

Waiting on this for resolution to apachesolr issue #592522: Hooks node_type, taxonomy and user knocks out our database server

webchick’s picture

Status: Reviewed & tested by the community » Needs work
Issue tags: +Needs tests

So it looks like the API change part of this would be internal to db_or() and db_and(), so existing modules should be unaffected. Yes?

However, I'm not seeing test coverage for:

a) The bug Crell found in #16.
b) Confirmation that the problem with crazy sub-queries like the one pwolanin wants to do in ApacheSolr is fixed.

I'm not sure if b) is possible, but it seems a) should be. Confirmation either way from pwolanin about this fixing the bug would be good.

Crell’s picture

Ugh. Hell, I don't even remember what I was doing 8 months ago. I've no idea how to write a test for it now.

sun’s picture

Category: bug » task

I'm tempted to classify this as a feature request, but let's call it task in terms of "oversight" for now. Definitely not a bug.

nick_vh’s picture

Just as an update. Because of this missing feature we currently have to write our query in the module as the example below because mysql has a bug that prevents us from using the IN with a subquery.
Could you tell us how the patch above would fix this problem?

function apachesolr_user_update(&$edit, $account, $category) {
  if (isset($edit['name']) && $account->name != $edit['name']) {
    switch (db_driver()) {
      case 'mysql' :
        // UPDATE with inner join for Mysql only
        $query = "UPDATE {apachesolr_search_node} asn
          INNER JOIN {node} n ON asn.nid = n.nid SET asn.changed = :changed
          WHERE n.uid = :uid";
        $result = db_query($query, array(':changed' => REQUEST_TIME,
          ':uid' => $account->uid,
        ));
        break;
      default :
        // The IN condition with a subquery. Works properly in all databases except Mysql
        $nids = db_select('node')
          ->fields('node', array('nid'))
          ->where("uid = :uid", array(':uid' => $account->uid));
        $update = db_update('apachesolr_search_node')
          ->condition('nid', $nids, 'IN')
          ->fields(array('changed' => REQUEST_TIME))
          ->execute();
    }
  }
}
pwolanin’s picture

see #16:

3) Add a MySQL-specific DatabaseCondition class that preprocesses subqueries in where clauses into wrapped FROM subqueries, as per the article chx linked above.

tim.plunkett’s picture

Category: task » feature

Recategorizing.

jhedstrom’s picture

Version: 8.0.x-dev » 8.1.x-dev

Probably needs to wait to at least 8.1.

Version: 8.1.x-dev » 8.2.x-dev

Drupal 8.1.0-beta1 was released on March 2, 2016, which means new developments and disruptive changes should now be targeted against the 8.2.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.2.x-dev » 8.3.x-dev

Drupal 8.2.0-beta1 was released on August 3, 2016, which means new developments and disruptive changes should now be targeted against the 8.3.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

jhedstrom’s picture

Status: Needs work » Postponed (maintainer needs more info)
Issue tags: +Needs issue summary update

Last patch is from 6 years ago, and last real discussion from 5 years ago. Marking postponed since the IS needs updating if this is still relevant for Drupal 8.

daffie’s picture

Issue summary: View changes
Status: Postponed (maintainer needs more info) » Active

Version: 8.3.x-dev » 8.4.x-dev

Drupal 8.3.0-alpha1 will be released the week of January 30, 2017, which means new developments and disruptive changes should now be targeted against the 8.4.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.4.x-dev » 8.5.x-dev

Drupal 8.4.0-alpha1 will be released the week of July 31, 2017, which means new developments and disruptive changes should now be targeted against the 8.5.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.5.x-dev » 8.6.x-dev

Drupal 8.5.0-alpha1 will be released the week of January 17, 2018, which means new developments and disruptive changes should now be targeted against the 8.6.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.6.x-dev » 8.7.x-dev

Drupal 8.6.0-alpha1 will be released the week of July 16, 2018, which means new developments and disruptive changes should now be targeted against the 8.7.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.7.x-dev » 8.8.x-dev

Drupal 8.7.0-alpha1 will be released the week of March 11, 2019, which means new developments and disruptive changes should now be targeted against the 8.8.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.8.x-dev » 8.9.x-dev

Drupal 8.8.0-alpha1 will be released the week of October 14th, 2019, which means new developments and disruptive changes should now be targeted against the 8.9.x-dev branch. (Any changes to 8.9.x will also be committed to 9.0.x in preparation for Drupal 9’s release, but some changes like significant feature additions will be deferred to 9.1.x.). For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

Version: 8.9.x-dev » 9.1.x-dev

Drupal 8.9.0-beta1 was released on March 20, 2020. 8.9.x is the final, long-term support (LTS) minor release of Drupal 8, which means new developments and disruptive changes should now be targeted against the 9.1.x-dev branch. For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

Version: 9.1.x-dev » 9.2.x-dev

Drupal 9.1.0-alpha1 will be released the week of October 19, 2020, which means new developments and disruptive changes should now be targeted for the 9.2.x-dev branch. For more information see the Drupal 9 minor version schedule and the Allowed changes during the Drupal 9 release cycle.

Version: 9.2.x-dev » 9.3.x-dev

Drupal 9.2.0-alpha1 will be released the week of May 3, 2021, which means new developments and disruptive changes should now be targeted for the 9.3.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.3.x-dev » 9.4.x-dev

Drupal 9.3.0-rc1 was released on November 26, 2021, which means new developments and disruptive changes should now be targeted for the 9.4.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.4.x-dev » 9.5.x-dev

Drupal 9.4.0-alpha1 was released on May 6, 2022, which means new developments and disruptive changes should now be targeted for the 9.5.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.5.x-dev » 10.1.x-dev

Drupal 9.5.0-beta2 and Drupal 10.0.0-beta2 were released on September 29, 2022, which means new developments and disruptive changes should now be targeted for the 10.1.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 10.1.x-dev » 11.x-dev

Drupal core is moving towards using a “main” branch. As an interim step, a new 11.x branch has been opened, as Drupal.org infrastructure cannot currently fully support a branch named main. New developments and disruptive changes should now be targeted for the 11.x branch, which currently accepts only minor-version allowed changes. For more information, see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

smustgrave’s picture

Status: Active » Postponed (maintainer needs more info)
Issue tags: +stale-issue-cleanup

Thank you for sharing your idea for improving Drupal.

We are working to decide if this proposal meets the Criteria for evaluating proposed changes. There hasn't been any discussion here for over 8 years which suggests that this has either been implemented or there is no community support. Your thoughts on this will allow a decision to be made.

Since we need more information to move forward with this issue, the status is now Postponed (maintainer needs more info). If we don't receive additional information to help with the issue, it may be closed after three months.

Thanks!

Version: 11.x-dev » main

Drupal core is now using the main branch as the primary development branch. New developments and disruptive changes should now be targeted to the main branch.

Read more in the announcement.

smustgrave’s picture

Wanted to bump 1 more time if still needed?