[IMP] base: faster search on filters by re-ordering unique constraint

The domain used in `get_filters`, used in `get_views` is

```python
[('action_id', 'in', [action_id, False]), ('model_id', '=', model), ('user_id', 'in', [self._uid, False])]
```

Therefore filtering the filters on `action_id`, `model_id` and
`user_id`.

An index on `(action_id, model_id, user_id)` would therefore be welcome
in order to search quicker on filters.

The unique constraint `name_model_uid_unique` almost does it,
but it puts the name in first, and therefore the index is not used
when searching on the above domain.

By moving `name` to the end of the unique constraint,
the index created for this unique constraint becomes
usable for the above domain,
and it makes the SQL statement to search on filters way quicker.

Before:
```sql
EXPLAIN ANALYZE SELECT "ir_filters".id FROM "ir_filters" LEFT JOIN "ir_translation" AS "ir_filters__name" ON ("ir_filters"."id" = "ir_filters__name"."res_id" AND "ir_filters__name"."type" = 'model' AND "ir_filters__name"."name" = 'ir.filters,name' AND "ir_filters__name"."lang" = 'en_US' AND "ir_filters__name"."value" != '') WHERE (((("ir_filters"."active" = true) AND (("ir_filters"."action_id" in (225)) OR "ir_filters"."action_id" IS NULL)) AND ("ir_filters"."model_id" = 'account.move')) AND (("ir_filters"."user_id" in (2)) OR "ir_filters"."user_id" IS NULL)) AND TRUE ORDER BY  "ir_filters"."model_id" ,COALESCE("ir_filters__name"."value", "ir_filters"."name") ,"ir_filters"."id" DESC;
                                                                              QUERY PLAN
----------------------------------------------------------------------------------------------------------------------------------------------------------------------
 Sort  (cost=3500.58..3500.59 rows=1 width=56) (actual time=12.940..12.942 rows=0 loops=1)
   Sort Key: (COALESCE(ir_filters__name.value, (ir_filters.name)::text)), ir_filters.id DESC
   Sort Method: quicksort  Memory: 25kB
   ->  Nested Loop Left Join  (cost=0.00..3500.57 rows=1 width=56) (actual time=12.933..12.934 rows=0 loops=1)
         Join Filter: (ir_filters.id = ir_filters__name.res_id)
         ->  Seq Scan on ir_filters  (cost=0.00..3499.02 rows=1 width=36) (actual time=12.932..12.932 rows=0 loops=1)
               Filter: (active AND ((action_id = 225) OR (action_id IS NULL)) AND ((user_id = 2) OR (user_id IS NULL)) AND ((model_id)::text = 'account.move'::text))
               Rows Removed by Filter: 100001
         ->  Seq Scan on ir_translation ir_filters__name  (cost=0.00..1.54 rows=1 width=36) (never executed)
               Filter: ((value <> ''::text) AND ((type)::text = 'model'::text) AND ((name)::text = 'ir.filters,name'::text) AND ((lang)::text = 'en_US'::text))
 Planning Time: 0.311 ms
 Execution Time: 12.972 ms
```

After:
```sql
EXPLAIN ANALYZE SELECT "ir_filters".id FROM "ir_filters" LEFT JOIN "ir_translation" AS "ir_filters__name" ON ("ir_filters"."id" = "ir_filters__name"."res_id" AND "ir_filters__name"."type" = 'model' AND "ir_filters__name"."name" = 'ir.filters,name' AND "ir_filters__name"."lang" = 'en_US' AND "ir_filters__name"."value" != '') WHERE (((("ir_filters"."active" = true) AND (("ir_filters"."action_id" in (225)) OR "ir_filters"."action_id" IS NULL)) AND ("ir_filters"."model_id" = 'account.move')) AND (("ir_filters"."user_id" in (2)) OR "ir_filters"."user_id" IS NULL)) AND TRUE ORDER BY  "ir_filters"."model_id" ,COALESCE("ir_filters__name"."value", "ir_filters"."name") ,"ir_filters"."id" DESC;
                                                                            QUERY PLAN
------------------------------------------------------------------------------------------------------------------------------------------------------------------
 Sort  (cost=14.44..14.44 rows=1 width=56) (actual time=0.086..0.087 rows=0 loops=1)
   Sort Key: (COALESCE(ir_filters__name.value, (ir_filters.name)::text)), ir_filters.id DESC
   Sort Method: quicksort  Memory: 25kB
   ->  Nested Loop Left Join  (cost=8.86..14.43 rows=1 width=56) (actual time=0.080..0.081 rows=0 loops=1)
         Join Filter: (ir_filters.id = ir_filters__name.res_id)
         ->  Bitmap Heap Scan on ir_filters  (cost=8.86..12.87 rows=1 width=36) (actual time=0.079..0.080 rows=0 loops=1)
               Recheck Cond: ((((model_id)::text = 'account.move'::text) AND (user_id = 2)) OR (((model_id)::text = 'account.move'::text) AND (user_id IS NULL)))
               Filter: (active AND ((action_id = 225) OR (action_id IS NULL)))
               ->  BitmapOr  (cost=8.86..8.86 rows=1 width=0) (actual time=0.078..0.079 rows=0 loops=1)
                     ->  Bitmap Index Scan on ir_filters_name_model_uid_unique  (cost=0.00..4.43 rows=1 width=0) (actual time=0.075..0.075 rows=0 loops=1)
                           Index Cond: (((model_id)::text = 'account.move'::text) AND (user_id = 2))
                     ->  Bitmap Index Scan on ir_filters_name_model_uid_unique  (cost=0.00..4.43 rows=1 width=0) (actual time=0.002..0.002 rows=0 loops=1)
                           Index Cond: (((model_id)::text = 'account.move'::text) AND (user_id IS NULL))
         ->  Seq Scan on ir_translation ir_filters__name  (cost=0.00..1.54 rows=1 width=36) (never executed)
               Filter: ((value <> ''::text) AND ((type)::text = 'model'::text) AND ((name)::text = 'ir.filters,name'::text) AND ((lang)::text = 'en_US'::text))
 Planning Time: 1.000 ms
 Execution Time: 0.121 ms
```

I take the opportunity to add the populate for `ir.filters`,
which helped me for the above analysis.

closes odoo/odoo#99657

Signed-off-by: Denis Ledoux (dle) <dle@odoo.com>
This commit is contained in:
Denis Ledoux
2022-09-07 11:25:52 +02:00
parent 90c035b70e
commit e2ffff1e42
3 changed files with 31 additions and 2 deletions
+2 -2
View File
@@ -144,12 +144,12 @@ class IrFilters(models.Model):
# Partial constraint, complemented by unique index (see below). Still
# useful to keep because it provides a proper error message when a
# violation occurs, as it shares the same prefix as the unique index.
('name_model_uid_unique', 'unique (name, model_id, user_id, action_id)', 'Filter names must be unique'),
('name_model_uid_unique', 'unique (model_id, user_id, action_id, name)', 'Filter names must be unique'),
]
def _auto_init(self):
result = super(IrFilters, self)._auto_init()
# Use unique index to implement unique constraint on the lowercase name (not possible using a constraint)
tools.create_unique_index(self._cr, 'ir_filters_name_model_uid_unique_action_index',
self._table, ['lower(name)', 'model_id', 'COALESCE(user_id,-1)', 'COALESCE(action_id,-1)'])
self._table, ['model_id', 'COALESCE(user_id,-1)', 'COALESCE(action_id,-1)', 'lower(name)'])
return result
+1
View File
@@ -1,3 +1,4 @@
from . import ir_filters
from . import res_partner
from . import res_company
from . import res_currency
+28
View File
@@ -0,0 +1,28 @@
from odoo import models
from odoo.tools import populate
class Filter(models.Model):
_inherit = "ir.filters"
# Based on the sizes of res.users, 10 filters per user.
_populate_sizes = {
'small': 100,
'medium': 10000,
'large': 100000,
}
_populate_dependencies = ['res.users']
def _populate_factories(self):
return [
('name', populate.constant('filter_{counter}')),
('user_id', populate.randomize(self.env.registry.populated_models['res.users'])),
('domain', populate.iterate(["[('id', '=', 1)]", "[('id', '=', 2)]", "[('id', '=', 3)]"])),
('context', populate.iterate(["{{}}", "{{'group_by': ['create_date:month']}}"])),
('sort', populate.iterate(["[]"])),
('model_id', populate.randomize(
list(dict(self._fields['model_id'].get_description(self.env, ['selection'])['selection']).keys())
)),
('is_default', populate.cartesian([True, False], [0.1, 0.9])),
('action_id', populate.randomize(self.env['ir.actions.actions'].search([]).ids)),
]