From 05855b6bb6d8a22ceb1f8b25332a146eed912660 Mon Sep 17 00:00:00 2001 From: "Victor Piryns (pivi)" <pivi@odoo.com> Date: Fri, 3 Mar 2023 13:44:52 +0000 Subject: [PATCH] [FIX] project: speed up name_search of tags on a task Description: The pop-up that shows suggestions when adding a tag on a task is really slow (300~500ms per request on prod). Cause: `project.tag` has an override of `_name_search()`, which adds a domain of the form ```python ['|', ('task_ids.project_id', '=', project_id), ('project_ids', 'in', project_id)] ``` which is expensive to compute. Functional requirements: - Suggest tags of the "project" first (those that are on tasks in the same project), then all the rest. - Tag's name should be sorted alphabetically per result set (tags on tasks of project first, sorted alphabetically, then all other tags, sorted alphabetically) Solution: Construct a custom query that makes use of CTE and `UNION ALL` to sort only on a subsets of the tags to speed up. Also `UNION ALL` with a `LIMIT` is lazy evaluated, so we gain on speed of all tags are comming from tags on tasks in the current project. Affected version: - 16.0 - saas-16.1 - master perf-3209468 closes odoo/odoo#114318 Signed-off-by: Xavier Bol (xbo) <xbo@odoo.com> --- addons/project/models/project.py | 66 ++++++++++++++++++++++++++------ 1 file changed, 55 insertions(+), 11 deletions(-) diff --git a/addons/project/models/project.py b/addons/project/models/project.py index 305c2b55fc1c..8962b88ba70b 100644 --- a/addons/project/models/project.py +++ b/addons/project/models/project.py @@ -2702,14 +2702,58 @@ class ProjectTags(models.Model): @api.model def _name_search(self, name='', args=None, operator='ilike', limit=100, name_get_uid=None): - domain = args - if 'project_id' in self.env.context: - domain = self._get_project_tags_domain(domain, self.env.context.get('project_id')) - return super()._name_search(name, domain, operator, limit, name_get_uid) - - @api.model - def name_create(self, name): - existing_tag = self.search([('name', '=ilike', name.strip())], limit=1) - if existing_tag: - return existing_tag.name_get()[0] - return super().name_create(name) + if 'project_id' in self.env.context and operator == 'ilike': + # `args` has the form of the default filter ['!', ['id', 'in', <ids>]] + # passed to exclude already selected tags -> exclude them in our query too + excluded_ids = list(args[1][2]) \ + if args and len(args) == 2 and args[0] == '!' and len(args[1]) == 3 and args[1][:2] == ["id", "in"] \ + else [] + # UNION ALL is lazy evaluated, if the first query has enough results, + # the second is not executed (just planned). + query = """ + WITH query_tags_in_tasks AS ( + SELECT tags.id, COALESCE(tags.name ->> %(lang)s, tags.name ->> 'en_US') AS name, 1 AS sequence + FROM project_tags AS tags + JOIN ( + SELECT project_tags_id + FROM project_tags_project_task_rel AS rel + JOIN project_task AS task + ON task.project_id = %(project_id)s + AND task.id = rel.project_task_id + ORDER BY task.id DESC + LIMIT 1000 -- arbitrary limit to speed up lookup on huge projects (fallback below on global scope) + ) AS tags__tasks_ids + ON tags__tasks_ids.project_tags_id = tags.id + WHERE tags.id != ALL(%(excluded_ids)s) + AND COALESCE(tags.name ->> %(lang)s, tags.name ->> 'en_US') ILIKE %(search_term)s + GROUP BY 1, 2, 3 -- faster than a distinct + LIMIT %(limit)s + ), query_all_tags AS ( + SELECT tags.id, COALESCE(tags.name ->> %(lang)s, tags.name ->> 'en_US') AS name, 2 AS sequence + FROM project_tags AS tags + WHERE tags.id != ALL(%(excluded_ids)s) + AND tags.id NOT IN (SELECT id FROM query_tags_in_tasks) + AND COALESCE(tags.name ->> %(lang)s, tags.name ->> 'en_US') ILIKE %(search_term)s + LIMIT %(limit)s + ) + SELECT id FROM ( + SELECT id, name, sequence + FROM query_tags_in_tasks + UNION ALL + SELECT id, name, sequence + FROM query_all_tags + LIMIT %(limit)s + ) AS tags + ORDER BY sequence, name + """ + params = { + 'project_id': self.env.context.get('project_id'), + 'excluded_ids': excluded_ids, + 'limit': limit, + 'lang': self.env.context.get('lang', 'en_US'), + 'search_term': '%' + name + '%', + } + self.env.cr.execute(query, params) + return [row[0] for row in self.env.cr.fetchall()] + else: + return super()._name_search(name, args, operator, limit, name_get_uid) -- GitLab