Excessive table joins and poor performance
| Project: | Taxonomy Filter |
| Version: | 6.x-1.x-dev |
| Component: | Code |
| Category: | bug report |
| Priority: | normal |
| Assigned: | solotandem |
| Status: | active |
Jump to:
The following error occurs on a view that accepts term-ids (TID) as an argument, where I have specified about 10 term's on the argument.
/myview/10,23,25,78,324,5,2,8,20
I think there are two issues here, one is the pure size of the query being sent to the SQL and the other is the performance of that query.
User warning: Too many tables; MySQL can only use 61 tables in a join query: SELECT count( DISTINCT(node.nid)) FROM node node LEFT JOIN term_node term_node ON node.nid = term_node.nid LEFT JOIN term_hierarchy term_hierarchy ON term_node.tid = term_hierarchy.tid LEFT JOIN term_hierarchy term_hierarchy2 ON term_hierarchy.parent = term_hierarchy2.tid LEFT JOIN term_hierarchy term_hierarchy3 ON term_hierarchy2.parent = term_hierarchy3.tid LEFT JOIN term_hierarchy term_hierarchy4 ON term_hierarchy3.parent = term_hierarchy4.tid LEFT JOIN term_hierarchy term_hierarchy5 ON term_hierarchy4.parent = term_hierarchy5.tid LEFT JOIN term_hierarchy term_hierarchy6 ON term_hierarchy5.parent = term_hierarchy6.tid LEFT JOIN term_node term_node2 ON node.nid = term_node2.nid LEFT JOIN term_hierarchy term_hierarchy7 ON term_node2.tid = term_hierarchy7.tid LEFT JOIN term_hierarchy term_hierarchy8 ON term_hierarchy7.parent = term_hierarchy8.tid LEFT JOIN term_hierarchy term_hierarchy9 ON term_hierarchy8.parent = term_hierarchy9.tid LEFT JOIN t in
Looks like the _views_add_taxonomy(...) of views/modules/views_taxonomy.inc function is executing sub-optimally by adding numberofterms * depthofquery to the $views query object.
foreach ($value as $tid) {
// For every term we have to match add the depth chain
$num = $query->add_table('term_node');
$tablename = $query->get_table_name('term_node', $num);
$where = "$tablename.tid = '$tid'";
// for each depth > 0, add the next parent in term_hierarchy to the join
$thnum = $query->add_table('term_hierarchy', false, 1, array('left' => array('table' => $tablename, 'field' => 'tid'), 'right' => array('field' => 'tid')));
$tablename = $query->get_table_name('term_hierarchy', $thnum);
for ($i = 0; $i < $depth; $i++) {
$thnum = $query->add_table('term_hierarchy', false, 1, array('left' => array('table' => $tablename, 'field' => 'parent'), 'right' => array('field' => 'tid')));
$tablename = $query->get_table_name('term_hierarchy', $thnum);
$where .= " OR $tablename.tid = '$tid'";
}
$query->add_where($where);
}the result is that if you have 10 terms specified on the argument and you have 5 levels of deepness specified then that is going to be 50 "LEFT JOINS" in your SQL query.
A similar issue exists with function _views_add_vocabulary($op, $value, $depth, &$query) however that is adding numberofterms * numberofVocabs
So on my celeron 1.5ghz and 1gb of RAM, with term_node containing ~900 terms i get
"mysql> SELECT COUNT(DISTINCT(n.nid)) FROM node n INNER JOIN term_node tn0 ON n.nid = tn0.nid INNER JOIN term_node tn1 ON n.nid = tn1.nid INNER JOIN term_node tn2 ON n.nid = tn2.nid INNER JOIN term_node tn3 ON n.nid = tn3.nid INNER JOIN term_node tn4 ON n.nid = tn4.nid INNER JOIN term_node tn5 ON n.nid = tn5.nid INNER JOIN term_node tn6 ON n.nid = tn6.nid INNER JOIN term_node tn7 ON n.nid = tn7.nid INNER JOIN term_node tn8 ON n.nid = tn8.nid INNER JOIN term_node tn9 ON n.nid = tn9.nid INNER JOIN term_node tn10 ON n.nid = tn10.nid INNER JOIN term_node tn11 ON n.nid = tn11.nid INNER JOIN term_node tn12 ON n.nid = tn12.nid INNER JOIN term_node tn13 ON n.nid = tn13.nid INNER JOIN node_access na ON na.nid = n.nid WHERE (na.grant_view >= 1 AND ((na.gid = 0 AND na.realm = 'all') OR (na.gid = 1 AND na.realm = 'workflow_access') OR (na.gid = 0 AND na.realm = 'workflow_access_owner'))) AND ( n.status = 1 AND n.moderate = 0 AND tn0.tid IN (20,10,11,12,13,15,18,19,67,43,68,42,14,16,17,35,44,36,37,45,50,46,38,39,47,40,53,54,55,56,57,58,59,60,61,62,63,64,65,48,49,69,51,52,21,22,23,24,25,26,27,28,30,31,32,33,34,66,70,71,72) AND tn1.tid IN (13) AND tn2.tid IN (96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,120,119,121,122,123,124,125,126,127,128,129,130,131,132,133,134) AND tn3.tid IN (196) AND tn4.tid IN (104) AND tn5.tid IN (183) AND tn6.tid IN (233) AND tn7.tid IN (136) AND tn8.tid IN (10,11,12,13,15,18,19) AND tn9.tid IN (180,181,182,183,184,185,186,187,188,189,190,191,192,193) AND tn10.tid IN (135,136,137,138,139,140,141,142) AND tn11.tid IN (1) AND tn12.tid IN (95,229,230,231,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,120,119,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,150,151,153,154,155,156,158,159,160,161,162,163,164,165,166,167,168,169,170,172,171,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,232,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,220,221,219) AND tn13.tid IN (164) AND n.type IN ("program"));
+------------------------+
| COUNT(DISTINCT(n.nid)) |
+------------------------+
| 0 |
+------------------------+
1 row in set (1 min 21.50 sec)
"If i strip the query down to something real simple
SELECT COUNT(*) FROM node n INNER JOIN term_node tn0 ON n.nid = tn0.nid INNER JOIN term_node tn1 ON n.nid = tn1.nid INNER JOIN term_node tn2 ON n.nid = tn2.nid INNER JOIN term_node tn3 ON n.nid = tn3.nid INNER JOIN term_node tn4 ON n.nid = tn4.nid INNER JOIN term_node tn5 ON n.nid = tn5.nid INNER JOIN term_node tn6 ON n.nid = tn6.nid INNER JOIN term_node tn7 ON n.nid = tn7.nid INNER JOIN term_node tn8 ON n.nid = tn8.nid INNER JOIN term_node tn9 ON n.nid = tn9.nid INNER JOIN term_node tn10 ON n.nid = tn10.nid INNER JOIN term_node tn11 ON n.nid = tn11.nid INNER JOIN term_node tn12 ON n.nid = tn12.nid INNER JOIN term_node tn13 ON n.nid = tn13.nid INNER JOIN node_access na ON na.nid = n.nid where n.nid IN (106,107,108,109,110,111,112,113);
this takes minutes to tens of minutes to execute on mysql with only 182 nodes in my DB.
I think the sql load gets exponentially heavier with each additional JOIN
SELECT COUNT(*) FROM node n INNER JOIN term_node tn0 ON n.nid = tn0.nid INNER JOIN term_node tn1 ON n.nid = tn1.nid INNER JOIN term_node tn2 ON n.nid = tn2.nid INNER JOIN term_node tn3 ON n.nid = tn3.nid INNER JOIN term_node tn4 ON n.nid = tn4.nid INNER JOIN term_node tn5 ON n.nid = tn5.nid INNER JOIN term_node tn6 ON n.nid = tn6.nid INNER JOIN term_node tn7 ON n.nid = tn7.nid INNER JOIN term_node tn8 ON n.nid = tn8.nid where n.nid IN (106,107,108,109,110,111,112,113);
+----------+
| COUNT(*) |
+----------+
| 0 |
+----------+
1 row in set (7.95 sec)
If i add one extra INNER JOIN from that original query, you can see the blow-out in the load
mysql> SELECT COUNT(*) FROM node n INNER JOIN term_node tn0 ON n.nid = tn0.nid INNER JOIN term_node tn1 ON n.nid = tn1.nid INNER JOIN term_node tn2 ON n.nid = tn2.nid INNER JOIN term_node tn3 ON n.nid = tn3.nid INNER JOIN term_node tn4 ON n.nid = tn4.nid INNER JOIN term_node tn5 ON n.nid = tn5.nid INNER JOIN term_node tn6 ON n.nid = tn6.nid INNER JOIN term_node tn7 ON n.nid = tn7.nid INNER JOIN term_node tn8 ON n.nid = tn8.nid INNER JOIN term_node tn9 ON n.nid = tn9.nid where n.nid IN (106,107,108,109,110,111,112,113);
+----------+
| COUNT(*) |
+----------+
| 0 |
+----------+
1 row in set (1 min 40.94 sec)Found an interesting article here on managing graph type data with mysql ttp://www.artfulsoftware.com/mysqlbook/sampler/mysqled1ch20.html , also i guess this makes using views for matching term-ids with depth on a large set of term_nodes not possible? maybe there is another way todo this ?

#1
Wonder if we could do this by something like
CREATE TABLE lookup
(
id INT NOT NULL,
tid INT,
parent INT,
PRIMARY KEY USING BTREE (id)
) ENGINE = MEMORY;
- CREATE TEMPORARY TABLE tree_results
INSERT INTO tree_results from lookup ?
#2
AAAh hang on, i suspect this is a load issue generated by the http://drupal.org/project/taxonomy_filter module
#3
#4
Ok, reanimating this.
tf_count_nodes_by_terms() consuming all server (mysql) resources with {node} having ~18,000, and {term_node} having 53,782 rows, on a dedicated server with moderate resources. Load averages almost tripled after adding "Context" blocks to taxonomy/term pages, and yes we have many listing pages all rely on taxonomy.
This module is marvellous, but many inner joins ornated with count(distinct) really consumes everything. Loosing page cache is our most feared problem now. By the way, I added "nid_tid", and "tid_nid" indexes manually (blindly) for {term_node} and situation seems to be soothed noticeably, though I am not sure if this is not a placebo.
Tough similar (and very fast), we do not want to use our ApacheSolr's taxo support, as we have customised taxo pages heavily with panels and views, and of course fell in love with Taxo filter that, we solved all contextual drill-down problems "in-style" just by adding few blocks.
Can we hope any performance improvements in the future?
Tx.
Drupal 6.13, min cache 1hr, JS aggregator with Jsmin+, optimized CSS
MySQL (5.1.36) MyISAM. Query_cache, key_buffers, join_buffers etc. are all tuned for best effort.
Centos 5.3 (32bit)
PHP 5.2.10
Xcache opt code, memcached object cache with libmemcached+PECL-memcached.
#5
You have hit on the Achilles heel of the submodules for taxonomy filter with the term depth you describe. I appreciate your thoughts on how to improve the performance and am open to suggestions.
I am curious what kind of performance you would have if you used the base module instead of the context module. I believe the base module avoids the calls to tf_count_nodes_by_terms() and hopefully has better performance. Would you try it simply for the performance metrics and respond here? Would it work for your site? Why or why not?
Also, would you post a link to the site so I could see the context menu in action?
#6
By the way we are not using deep hierarchies but we have quite many terms. You can try this page for example. Site is not in English but is self explanatatory. You can also click to the top menu items with small arrows, which expand and expose list of taxonomy terms. As you might see, we are using taxonomy terms for all navigation purposes, not just tagging.
Site is renewed and nearly 5% of the nodes have properly tagged, and only half of the terms are in place yet, even this can cause heavy loads, especially when SE bots are crawling.
We use heavy caching but as you might agree, it is impossiple to cache every possible combination of pages with a crowded taxonomy. What's more, our site is updated so frequently that few dozen nodes with new tags are introduced daily, that expires most of the cached objects. Also context is absolutely needed because of our site's dynamic and quite complex structure. Just listing all terms with the base is not adequate for us, as 95% of the list will be irrelevant and will yield empty lists.
I am not a real PHP or SQL coder, but I can make some educated guesses anyway.
As I have said, indexes seem to help though I am not sure. On a second thought, at least for the ones not using item counts near terms, COUNT (may be DISTINCT also) can be eliminated and some sort of LIMIT = 1 functionality can be added to queries just to check if the terms are present together or not. I hoped the relevant setting in the admin page would yield this, but tf_count function have not been used this way. First hit is enough for the term to be in the contextual list anyway, but again I am not sure if this might help :/
I dig into SQL documents a bit and hit delayed joins, but people say that at least new versions of MySQL does handle inner joins better than this method, and this is not applicable to all sorts of join queries anyway.
I am rather sure some sort of optimization can be done, but that's only a feeling as I am not up to deep secrets of coding much. I can not make a precise profiling on a live and busy site, but I can say context nearly triples MySQL loads on busy hours, causing long waits though does not crash anything.
Our server will be changed with a more muscled one in a short time, but brute force is not a real solution. As you might share, any possible headroom is invaluable in this business, matter is not just to keep up with the load.
Tx for listening and for your efforts.
#7
You wrote:
Although the name is not 'context,' the default behavior of the base module is to omit a menu item for terms that do not match any nodes. Menu items could be included but this requires the user to check the box:
"Display a menu item for terms that do not match any nodes."
on the admin/settings/taxonomy_filter/%vid/edit page.
Again, I am curious how the base module would work in this situation and if it would help with performance.
#8
Yes, you are absolutely right. In fact base fuctionality is what we have enoghly needed. I am not sure why we explicitly determined to use context. Lack of investigation due to tight schedule I am afraid. "Context" name seems to be more contextual by the way :) and we stuck somehow.
Anyway, base seems to be cheaper on sql (at least not using tf_count), though queries resembles each other but everted. Perhaps not jailing the results till last bit, but using GROUP BY helps also. Now SQL load does not triple but seems to be doubled when TF is enabled on busy hours, but this seems to be quite normal.
~ 25-30% of SQL load seems to be freed with the base, and filtering seems to be quicker, but live server is hard to profile for one aspect and this may be rather subjective, thus should be taken with a grain of salt.
I think that's what we should expect from TF for now. In fact I dig more on query optimization recently, but suspected query seems to be unreducible and minimal enough.
...but in my opinion COUNT(DISTINCT) still can be conditionally supressed according to the relevant setting (in fact I deleted manually :S).
Thank you for your assist, and any milliseconds are welcome :)
#9
Too many JOINs and poor performance:
Problem cause:
tanxonomy_filter.module - row 462
// By replacing %d with multiple %d's we could run this query at one time (from another function above)
$wheres = ' WHERE td.vid IN (%d)';
foreach ($tids as $index => $tid) {
$joins .= ' INNER JOIN {term_node} tn' . $index . ' ON tn.nid = tn' . $index . '.nid';
$wheres .= ' AND tn' . $index . '.tid IN ('. $tid .')';
}
$joins .= " INNER JOIN node n ON tn.nid = n.nid";
$wheres .= ' AND n.status = 1 AND n.moderate = 0';
$group = ' GROUP BY td.tid';
$order = ' ORDER BY vid, weight, name';
$sql = $select . $joins . $wheres . $group . $order;
$sql = db_rewrite_sql($sql);
Example result:
SELECT td.vid, td.tid, td.name, COUNT(DISTINCT(n.nid)) AS count FROM term_data td INNER JOIN term_node tn ON tn.tid = td.tid
INNER JOIN term_node tn0 ON tn.nid = tn0.nid INNER JOIN term_node tn1 ON tn.nid = tn1.nid INNER JOIN node n ON tn.nid = n.nid
INNER JOIN ....INNER JOIN ....INNER JOIN ....INNER JOIN ....INNER JOIN ....INNER JOIN ....INNER JOIN ....
WHERE td.vid IN (1) AND tn0.tid IN (169) AND tn1.tid IN (92) AND tn2.tid IN (.....
AND n.status = 1 AND n.moderate = 0 GROUP BY td.tid ORDER BY vid, weight, name
...and bug fixing:
// By replacing %d with multiple %d's we could run this query at one time (from another function above)
$wheres = ' WHERE td.vid IN (%d)';
$joins .= ' INNER JOIN {term_node} tn' . '0' . ' ON tn.nid = tn' . '0' . '.nid';
$wheres .= ' AND tn' . '0' . '.tid IN (';
foreach ($tids as $index => $tid) {
$wheres .= $tid.',';
}
$joins .= " INNER JOIN node n ON tn.nid = n.nid";
$wheres .= '0) AND n.status = 1 AND n.moderate = 0';
$group = ' GROUP BY td.tid';
$order = ' ORDER BY vid, weight, name';
$sql = $select . $joins . $wheres . $group . $order;
$sql = db_rewrite_sql($sql);
Example result:
SELECT td.vid, td.tid, td.name, COUNT(DISTINCT(n.nid)) AS count FROM term_data td INNER JOIN term_node tn ON tn.tid = td.tid
INNER JOIN term_node tn0 ON tn.nid = tn0.nid INNER JOIN node n ON tn.nid = n.nid
WHERE td.vid IN (1)
AND tn0.tid IN (169, 92, ...,...,..,...,...,0)
AND n.status = 1 AND n.moderate = 0 GROUP BY td.tid ORDER BY vid, weight, name
(Sorry, I can't made patch yet.)
#10
Thanks for posting on this. I think you are on the right track. I ran your code alongside the current code. They produce different results. Would you try the same on your site and see if you get different results?
Would you provide some metrics about the site having the performance issue -- number of vocabularies, number of terms, number of nodes, etc.? How long do the queries take?
Also, you might want to use the dev version.