[FIX] expression: use sub-select when searching on many2many field

Avoid pathological performance issue caused by injecting ids retrieved with
another query.

Consider a domain like `[('m2m', 'in', ids)]` on a many2many field.  The
current implementation will perform the subquery:

    SELECT m2m_id1 FROM m2m_table WHERE m2m_id2 IN (ids)

and inject its result into the main query as:

    SELECT id FROM ... WHERE id IN (result_ids)

The latter may be very slow if `result_ids` is a huge list of ids.

The fix injects the first query into the main query as:

    SELECT id FROM ... WHERE id IN (
        SELECT m2m_id1 FROM m2m_table WHERE m2m_id2 IN (ids)
    )

As a result, the database will typically JOIN both tables, and avoid generating
the whole list from the subquery.
This commit is contained in:
Raphael Collet 2017-04-05 10:48:39 +02:00
parent 3128e84243
commit 3c2065c89d
1 changed files with 12 additions and 8 deletions

View File

@ -994,15 +994,16 @@ class expression(object):
rel_table, rel_id1, rel_id2 = column._sql_names(model)
#FIXME
if operator == 'child_of':
def _rec_convert(ids):
if comodel == model:
return ids
return select_from_where(cr, rel_id1, rel_table, rel_id2, ids, operator)
ids2 = to_ids(right, comodel, context)
dom = child_of_domain('id', ids2, comodel)
ids2 = comodel.search(cr, uid, dom, context=context)
push(create_substitution_leaf(leaf, ('id', 'in', _rec_convert(ids2)), model))
if comodel == model:
push(create_substitution_leaf(leaf, ('id', 'in', ids2), model))
else:
subquery = 'SELECT "%s" FROM "%s" WHERE "%s" IN %%s' % (rel_id1, rel_table, rel_id2)
# avoid flattening of argument in to_sql()
subquery = cr.mogrify(subquery, [tuple(ids2)])
push(create_substitution_leaf(leaf, ('id', 'inselect', (subquery, [])), internal=True))
else:
call_null_m2m = True
if right is not False:
@ -1024,8 +1025,11 @@ class expression(object):
operator = 'in' # operator changed because ids are directly related to main object
else:
call_null_m2m = False
m2m_op = 'not in' if operator in NEGATIVE_TERM_OPERATORS else 'in'
push(create_substitution_leaf(leaf, ('id', m2m_op, select_from_where(cr, rel_id1, rel_table, rel_id2, res_ids, operator) or [0]), model))
subop = 'not inselect' if operator in NEGATIVE_TERM_OPERATORS else 'inselect'
subquery = 'SELECT "%s" FROM "%s" WHERE "%s" IN %%s' % (rel_id1, rel_table, rel_id2)
# avoid flattening of argument in to_sql()
subquery = cr.mogrify(subquery, [tuple(filter(None, res_ids))])
push(create_substitution_leaf(leaf, ('id', subop, (subquery, [])), internal=True))
if call_null_m2m:
m2m_op = 'in' if operator in NEGATIVE_TERM_OPERATORS else 'not in'