Use recursive CTE for child/parent-of clause when the number of ids is too large

The path or MPTT queries are faster than the recursive CTE only when the number
of OR-ed clauses does not require more index scan than the CTE.
This number can be approximate to the depth of the tree.
Log10 is used to estimate the depth of the tree if it has an average of 10
children per node.

Closes #14078
This commit is contained in:
Cédric Krier
2025-06-16 13:48:00 +02:00
parent ea1393bb84
commit b63c28503c
2 changed files with 77 additions and 42 deletions
+9 -2
View File
@@ -1,5 +1,7 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
import math
import warnings
from sql import As, Column, Expression, Literal, Null, Query, With
@@ -255,10 +257,15 @@ class Many2One(Field):
if operator.startswith('not'):
return ~expression
return expression
elif self.left and self.right:
elif (self.left and self.right
and len(ids) <= math.floor(math.log10(
Target.estimated_count() or 1))):
return self.convert_domain_mptt(
(name, operator, ids), tables)
elif self.path:
elif (self.path
and (operator.endswith('parent_of')
or len(ids) <= math.floor(math.log10(
Target.estimated_count() or 1)))):
return self.convert_domain_path(
(name, operator, ids), tables)
else:
+68 -40
View File
@@ -1,6 +1,8 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
from unittest.mock import patch
from sql import Join
from trytond.model.exceptions import DomainValidationError
@@ -348,9 +350,11 @@ class FieldMany2OneTreeTestCase(TestCase):
Many2One = Pool().get(self.model_name)
self.create_tree(Many2One)
result = Many2One.search([
('many2one', 'child_of', [self.root1.id]),
])
with patch.object(Many2One, 'estimated_count') as ec:
ec.return_value = 100
result = Many2One.search([
('many2one', 'child_of', [self.root1.id]),
])
self.assertListEqual(
sorted(result),
@@ -364,9 +368,11 @@ class FieldMany2OneTreeTestCase(TestCase):
Many2One = Pool().get(self.model_name)
self.create_tree(Many2One)
result = Many2One.search([
('many2one', 'not child_of', [self.root1.id]),
])
with patch.object(Many2One, 'estimated_count') as ec:
ec.return_value = 100
result = Many2One.search([
('many2one', 'not child_of', [self.root1.id]),
])
self.assertListEqual(
sorted(result),
@@ -378,9 +384,11 @@ class FieldMany2OneTreeTestCase(TestCase):
Many2One = Pool().get(self.model_name)
self.create_tree(Many2One)
result = Many2One.search([
('many2one', 'child_of', [self.second1.id]),
])
with patch.object(Many2One, 'estimated_count') as ec:
ec.return_value = 100
result = Many2One.search([
('many2one', 'child_of', [self.second1.id]),
])
self.assertListEqual(result, [self.second1])
@@ -406,10 +414,12 @@ class FieldMany2OneTreeTestCase(TestCase):
Many2One = Pool().get(self.model_name)
self.create_tree(Many2One)
result = Many2One.search([
('many2one', 'child_of',
[self.first1.id, self.second1.id]),
])
with patch.object(Many2One, 'estimated_count') as ec:
ec.return_value = 100
result = Many2One.search([
('many2one', 'child_of',
[self.first1.id, self.second1.id]),
])
self.assertListEqual(
sorted(result),
@@ -421,9 +431,11 @@ class FieldMany2OneTreeTestCase(TestCase):
Many2One = Pool().get(self.model_name)
self.create_tree(Many2One)
result = Many2One.search([
('many2one', 'child_of', []),
])
with patch.object(Many2One, 'estimated_count') as ec:
ec.return_value = 100
result = Many2One.search([
('many2one', 'child_of', []),
])
self.assertListEqual(result, [])
@@ -433,9 +445,11 @@ class FieldMany2OneTreeTestCase(TestCase):
Many2One = Pool().get(self.model_name)
self.create_tree(Many2One)
result = Many2One.search([
('many2one', 'not child_of', []),
])
with patch.object(Many2One, 'estimated_count') as ec:
ec.return_value = 100
result = Many2One.search([
('many2one', 'not child_of', []),
])
self.assertListEqual(
sorted(result),
@@ -449,9 +463,11 @@ class FieldMany2OneTreeTestCase(TestCase):
Many2One = Pool().get(self.model_name)
self.create_tree(Many2One)
result = Many2One.search([
('many2one', 'child_of', [None]),
])
with patch.object(Many2One, 'estimated_count') as ec:
ec.return_value = 100
result = Many2One.search([
('many2one', 'child_of', [None]),
])
self.assertListEqual(result, [])
@@ -461,9 +477,11 @@ class FieldMany2OneTreeTestCase(TestCase):
Many2One = Pool().get(self.model_name)
self.create_tree(Many2One)
result = Many2One.search([
('many2one', 'parent_of', [self.root1.id]),
])
with patch.object(Many2One, 'estimated_count') as ec:
ec.return_value = 100
result = Many2One.search([
('many2one', 'parent_of', [self.root1.id]),
])
self.assertListEqual(result, [self.root1])
@@ -473,9 +491,11 @@ class FieldMany2OneTreeTestCase(TestCase):
Many2One = Pool().get(self.model_name)
self.create_tree(Many2One)
result = Many2One.search([
('many2one', 'not parent_of', [self.root1.id]),
])
with patch.object(Many2One, 'estimated_count') as ec:
ec.return_value = 100
result = Many2One.search([
('many2one', 'not parent_of', [self.root1.id]),
])
self.assertListEqual(
sorted(result),
@@ -489,9 +509,11 @@ class FieldMany2OneTreeTestCase(TestCase):
Many2One = Pool().get(self.model_name)
self.create_tree(Many2One)
result = Many2One.search([
('many2one', 'parent_of', [self.second4.id]),
])
with patch.object(Many2One, 'estimated_count') as ec:
ec.return_value = 100
result = Many2One.search([
('many2one', 'parent_of', [self.second4.id]),
])
self.assertListEqual(
sorted(result),
@@ -503,9 +525,11 @@ class FieldMany2OneTreeTestCase(TestCase):
Many2One = Pool().get(self.model_name)
self.create_tree(Many2One)
result = Many2One.search([
('many2one', 'not parent_of', [self.second4.id]),
])
with patch.object(Many2One, 'estimated_count') as ec:
ec.return_value = 100
result = Many2One.search([
('many2one', 'not parent_of', [self.second4.id]),
])
self.assertListEqual(
sorted(result),
@@ -519,9 +543,11 @@ class FieldMany2OneTreeTestCase(TestCase):
Many2One = Pool().get(self.model_name)
self.create_tree(Many2One)
result = Many2One.search([
('many2one', 'parent_of', []),
])
with patch.object(Many2One, 'estimated_count') as ec:
ec.return_value = 100
result = Many2One.search([
('many2one', 'parent_of', []),
])
self.assertListEqual(result, [])
@@ -531,9 +557,11 @@ class FieldMany2OneTreeTestCase(TestCase):
Many2One = Pool().get(self.model_name)
self.create_tree(Many2One)
result = Many2One.search([
('many2one', 'not parent_of', []),
])
with patch.object(Many2One, 'estimated_count') as ec:
ec.return_value = 100
result = Many2One.search([
('many2one', 'not parent_of', []),
])
self.assertListEqual(
sorted(result),