The Problem
Odoo was snappy when you started, but after a year of data, list views take 10 seconds to load, reports timeout, and the whole system feels sluggish. The culprit is almost always slow database queries — and the fix is usually straightforward once you identify the problem.
Common Symptoms
- List views take 5-30 seconds to load
- Search results slow to appear
- Reports timeout with
504 Gateway Timeout - High CPU on the database server
- Odoo log shows queries taking 1000ms+
- Browser shows spinning wheel for minutes
Step 1: Identify the Slow Queries
Enable Odoo Query Logging
# In odoo.conf:
log_level = debug_sql
# Or command line:
./odoo-bin --log-handler=odoo.sql_db:DEBUG
# This logs ALL SQL queries with execution time.
# Warning: very verbose, use only for debugging.Use PostgreSQL Slow Query Log
# In postgresql.conf:
log_min_duration_statement = 500 # log queries taking > 500ms
log_statement = 'none' # don't log all queries, just slow ones
# Reload: sudo systemctl reload postgresqlCheck Active Queries
# Find currently running slow queries:
SELECT pid, now() - query_start AS duration,
state, query
FROM pg_stat_activity
WHERE state = 'active'
AND now() - query_start > interval '1 second'
ORDER BY query_start;Common Slow Query Patterns and Fixes
1. Missing Indexes on Filtered Fields
The most impactful fix. If you filter or sort by a field that does not have a database index, PostgreSQL does a full table scan.
# Identify missing indexes:
EXPLAIN ANALYZE SELECT id FROM sale_order
WHERE state = 'sale' AND date_order > '2025-01-01';
# Look for "Seq Scan" in the output — that means no index.
# "Index Scan" or "Bitmap Index Scan" = good.Fix:
# Add index in Odoo model:
state = fields.Selection([...], index=True) # adds B-tree index
date_order = fields.Datetime(index=True)
# Or add directly in PostgreSQL:
CREATE INDEX CONCURRENTLY idx_sale_order_state
ON sale_order (state);
CREATE INDEX CONCURRENTLY idx_sale_order_date
ON sale_order (date_order);2. name_search on Large Tables
Many2one fields use name_search which does ILIKE '%term%' — the slowest possible text search pattern.
Fix: Add a trigram index for ILIKE performance:
-- Enable pg_trgm extension:
CREATE EXTENSION IF NOT EXISTS pg_trgm;
-- Add trigram index:
CREATE INDEX CONCURRENTLY idx_partner_name_trgm
ON res_partner USING gin (name gin_trgm_ops);3. Computed Fields Without Store
Non-stored computed fields are computed for every record on every request. If a list view shows 80 records with 5 computed fields, that is 400 computations per page load.
Fix: Add store=True to computed fields used in list views or filters:
# Slow: computed on every access
margin = fields.Float(compute='_compute_margin')
# Fast: computed once, stored in database, indexed for filtering
margin = fields.Float(compute='_compute_margin', store=True)4. Large One2many Fields Loaded in Views
Loading a form view that includes a One2many with thousands of child records is slow.
Fix:
- Add pagination to the One2many:
<field name="line_ids" limit="20"/> - Use a separate list view action instead of embedding
- Add default filters to reduce loaded records
5. Unoptimized Domain Filters
Complex domains with nested OR conditions and cross-model filters generate slow SQL.
# Slow: cross-model filter with OR
domain = ['|',
('partner_id.country_id.name', '=', 'US'),
('partner_id.state_id.name', '=', 'California')
]
# Faster: pre-fetch partner IDs
us_partners = self.env['res.partner'].search([
'|', ('country_id.name', '=', 'US'),
('state_id.name', '=', 'California')
]).ids
domain = [('partner_id', 'in', us_partners)]6. count() on Large Tables
Odoo's list view calls search_count() to show the total record count. On tables with millions of rows, this is slow.
Fix:
# In custom models, override search_count for performance:
@api.model
def search_count(self, domain, limit=None):
# Use estimate for very large counts
if not domain:
self.env.cr.execute(
"SELECT reltuples FROM pg_class WHERE relname = %s",
[self._table]
)
return int(self.env.cr.fetchone()[0])
return super().search_count(domain, limit=limit)PostgreSQL Tuning for Odoo
# Key postgresql.conf settings for Odoo:
shared_buffers = 2GB # 25% of RAM
effective_cache_size = 6GB # 75% of RAM
work_mem = 64MB # per query sort/hash memory
maintenance_work_mem = 512MB # for VACUUM, CREATE INDEX
random_page_cost = 1.1 # for SSD storage (default 4.0)
effective_io_concurrency = 200 # for SSD
max_worker_processes = 8
max_parallel_workers_per_gather = 4Regular Maintenance
# Vacuum and analyze (should run automatically, but verify):
VACUUM ANALYZE;
# Check table bloat:
SELECT schemaname, tablename,
pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) as size
FROM pg_tables
WHERE schemaname = 'public'
ORDER BY pg_total_relation_size(schemaname||'.'||tablename) DESC
LIMIT 20;
# Reindex bloated indexes:
REINDEX TABLE CONCURRENTLY sale_order;