From e2ffff1e427be1e98a64de1d2c8a837447d47538 Mon Sep 17 00:00:00 2001 From: Denis Ledoux Date: Tue, 6 Sep 2022 15:34:24 +0000 Subject: [PATCH] [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) --- odoo/addons/base/models/ir_filters.py | 4 ++-- odoo/addons/base/populate/__init__.py | 1 + odoo/addons/base/populate/ir_filters.py | 28 +++++++++++++++++++++++++ 3 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 odoo/addons/base/populate/ir_filters.py diff --git a/odoo/addons/base/models/ir_filters.py b/odoo/addons/base/models/ir_filters.py index 4fa5d66b01d..3a3938b3caa 100644 --- a/odoo/addons/base/models/ir_filters.py +++ b/odoo/addons/base/models/ir_filters.py @@ -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 diff --git a/odoo/addons/base/populate/__init__.py b/odoo/addons/base/populate/__init__.py index abcf946fd4f..3a0d4a4430b 100644 --- a/odoo/addons/base/populate/__init__.py +++ b/odoo/addons/base/populate/__init__.py @@ -1,3 +1,4 @@ +from . import ir_filters from . import res_partner from . import res_company from . import res_currency diff --git a/odoo/addons/base/populate/ir_filters.py b/odoo/addons/base/populate/ir_filters.py new file mode 100644 index 00000000000..e714b29528e --- /dev/null +++ b/odoo/addons/base/populate/ir_filters.py @@ -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)), + ]