Skip to content

[PERF] ni_service, ni_patient, ni_questionnaire: declare production indexes, stop stray Meeting activities - #177

Merged
piruin merged 4 commits into
16.0from
16.0-ni_service-declare-production-indexes
Oct 23, 2026
Merged

piruin merged 4 commits into
16.0from
16.0-ni_service-declare-production-indexes

Conversation

@piruin

@piruin piruin commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Closes #174

Stray Meeting activities (root cause of mail_activity growth)

When a service event is created from the patient form, the context carries active_model = "ni.patient" and active_id = <patient>. ni.service.event uses _inherits from calendar.event, so calendar.event.default_get turns those keys into res_model/res_id. calendar.event.create then adds a Meeting mail.activity on the patient. That is how mail_activity grew to millions of rows.

ServiceEvent.create() now calls super() with active_model=False, active_id=False. default_* keys from the action still apply, and ni.service.create_event() (default_res_model = "ni.service") is unaffected. Covered by test_create_from_patient_form_adds_no_activity_on_patient, which fails without the fix.

Indexes

Every name matches production exactly. Odoo skips an existing index by name, so -u on production builds nothing.

Index Definition Module How
calendar_attendee_event_id_index calendar_attendee (event_id) ni_service event_id field override, index=True
calendar_event_id_filter_idx calendar_event (id) INCLUDE (active, start, stop, allday, start_date) ni_service raw SQL in init()
ni_service_event_message_main_attachment_id_index btree, WHERE ... IS NOT NULL ni_service index="btree_not_null"
ni_service_event_displayed_image_id_index btree, WHERE ... IS NOT NULL ni_service index="btree_not_null"
calendar_event_message_main_attachment_id_index btree, WHERE ... IS NOT NULL ni_service index="btree_not_null"
ni_patient_message_main_attachment_id_index btree, WHERE ... IS NOT NULL ni_patient index="btree_not_null"
res_partner_message_main_attachment_id_index btree, WHERE ... IS NOT NULL ni_patient index="btree_not_null"
ir_attachment_original_id_index btree, WHERE ... IS NOT NULL ni_patient index="btree_not_null"
survey_user_input_message_main_attachment_id_index btree, WHERE ... IS NOT NULL ni_questionnaire index="btree_not_null"
  • mail_activity_user_id_date_deadline_id_idx and mail_activity_id_res_model_res_id_idx are intentionally not declared. The plan is to drop them on production after the activity cleanup.
  • ir.attachment.original_id comes from web_editor. ni_patient already depends on it through portal, so there's no new dependency.

After deploy (ops, per database using ni_service)

  1. Delete the Meeting activities on ni.patient that are linked to a calendar event. Match on res_model = 'ni.patient', activity type category meeting, and calendar_event_id IS NOT NULL. Delete in batches at night, then run VACUUM (ANALYZE) mail_activity.
  2. After that, drop the two mail_activity_*_idx indexes with DROP INDEX CONCURRENTLY.

Verification

  • A fresh install of ni_service,ni_questionnaire creates all 9 indexes, and each indexdef matches the issue. No mail_activity_*_idx index is created.
  • Running -u ni_patient,ni_service,ni_questionnaire on that database left every index OID unchanged, so nothing was rebuilt or duplicated.
  • Tests for /ni_patient,/ni_service,/ni_questionnaire: 0 failed, 0 errors of 80.
  • Not checked: EXPLAIN of the query shapes. A small local database won't pick these indexes, so this has to be checked on production.

Related: nirun-life/nirun-dop#70 (create_uid indexes in ni_community_care).

Deleting an ir.attachment checks every ON DELETE SET NULL foreign key
that points at it. Without an index on message_main_attachment_id
(ni.patient, res.partner) and ir.attachment.original_id, each check
scans the whole table, which made a single delete take ~141 s on
production.

The btree_not_null field indexes generate the same names as the
indexes already created by hand on production, so -u is a no-op there.

Part of #174
Declare the indexes added by hand on production during query tuning,
so new or rebuilt databases get them too:

- calendar_attendee_event_id_index for attendee reads by event_id
- calendar_event_id_filter_idx, a covering index for the calendar view
  search (~79 s -> ~1.5 s)
- btree_not_null indexes on the attachment foreign keys of
  ni.service.event and calendar.event, used by ir.attachment deletes

Names match production exactly: Odoo skips an index by name, so -u
does not rebuild them on the multi-million-row tables.

Part of #174
Creating a service event from the patient form runs with
active_model = "ni.patient" and active_id = <patient> in the context.
Through _inherits, calendar.event.default_get turns them into
res_model/res_id, and calendar.event.create then adds a Meeting
mail.activity on the patient, assigned to the creator. On production
this grew mail_activity to millions of rows and left users with
thousands of overdue activities, slowing activity searches and the
systray.

Clear active_model/active_id before calling super().create();
default_* keys from the action still apply.

Part of #174
Deleting an ir.attachment checks survey_user_input's
message_main_attachment_id foreign key, which scanned the whole table.
The btree_not_null index matches the name already on production.

Closes #174
@piruin
piruin force-pushed the 16.0-ni_service-declare-production-indexes branch from 2f4c323 to b76e413 Compare October 23, 2026 11:13
@piruin piruin changed the title [PERF] ni_service, ni_patient, ni_questionnaire: declare indexes added on production [PERF] ni_service, ni_patient, ni_questionnaire: declare production indexes, stop stray Meeting activities Oct 23, 2026
@piruin

piruin commented Oct 23, 2026

Copy link
Copy Markdown
Contributor Author

Decision: index message_main_attachment_id per model, not on mail.thread.

Overriding the field once on mail.thread would index every mail-thread table. We keep the per-model overrides for three reasons:

  1. Upgrade stays a no-op on production. Production has these indexes only on the tables listed in [Task] ni_service and related modules: declare production indexes and stop unwanted Meeting activities #174. Overriding mail.thread would make -u ni_patient run a plain, write-blocking CREATE INDEX on every other mail-thread table: Odoo core models, ni.encounter, ni.appointment, ni.medication*, ni.procedure, ni.device*, ni.goal, and the nirun-dop models. None of those has been measured.
  2. Only large tables need it. The ON DELETE SET NULL check on an attachment delete is only slow on tables with millions of rows. Small tables scan cheaply, and an extra index only adds write cost. The list follows what pg_stat_statements showed.
  3. Forward-port. In 17.0, message_main_attachment_id moved from mail.thread to the mail.thread.main.attachment mixin, so an override on mail.thread would break on 17+.

If another table (for example ni_encounter) grows large later, confirm on production first, then add the index on that model under the name production uses.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Task] ni_service and related modules: declare production indexes and stop unwanted Meeting activities

1 participant