# Database Odoo của tôi phình nhanh: làm gì bây giờ?

> Một cẩm nang thực dụng cho Odoo 16 đến 19

**Date:** 2026-05-28
**Source:** <https://trobz.com/vi/insights/odoo-db-growing-fast/>

---


Mọi hệ thống Odoo đều phình ra ở đúng một nhúm chỗ giống nhau. Một bộ công cụ nhỏ phủ được phần lớn trong số đó. Còn đúng một khoảng trống, lưu trữ lâu dài theo tuổi và theo từng loại bản ghi, đến nay vẫn chưa có lời giải đóng gói sẵn.

Bài này là một cẩm nang năm bước dành cho trưởng nhóm kỹ thuật Odoo và kỹ sư DevOps đang vận hành hệ thống production từ Odoo 16.0 đến 19.0. Chúng ta sẽ xem các byte thực sự nằm ở đâu, xóa cái gì, vì sao chỉ xóa thôi thì đĩa chưa nhỏ lại, cách đưa file đính kèm ra khỏi database, và khuôn mẫu lưu trữ lâu dài mà chưa module nào phủ.

Đây không phải bài về tinh chỉnh PostgreSQL. Chúng ta không đụng tới `shared_buffers`, `work_mem` hay ngưỡng autovacuum. Chúng ta đụng tới cấu hình Odoo, các module OCA, và một nhóm nhỏ lệnh PostgreSQL thật sự thu hồi được dung lượng đĩa sau khi Odoo xóa dòng.

## Bước 1: Chẩn đoán

Đừng hành động theo cảm tính. Hãy chạy ba truy vấn và một lệnh shell trước khi cài bất cứ thứ gì.

**Các bảng lớn nhất, tách riêng TOAST và index:**

```sql
SELECT c.relname,
       pg_size_pretty(pg_total_relation_size(c.oid)) AS total,
       pg_size_pretty(pg_relation_size(c.oid))       AS heap,
       pg_size_pretty(pg_total_relation_size(c.reltoastrelid)) AS toast,
       pg_size_pretty(pg_indexes_size(c.oid))        AS indexes
FROM pg_class c
WHERE c.relkind = 'r'
ORDER BY pg_total_relation_size(c.oid) DESC
LIMIT 20;
```

<img src="{{< surl-static >}}/images/insights/odoo-db-growing-fast/biggest-tables.png" alt="20 bảng PostgreSQL lớn nhất theo tổng dung lượng, tách riêng heap, toast và index"/>

Cột `toast` thường là chỗ gây bất ngờ. Trên một database Odoo điển hình, `mail_message.body` (phần thân HTML của các thông báo) và `ir_attachment.db_datas` (khi file đính kèm lưu trong database) đẩy hàng gigabyte vào bảng phụ TOAST mà lời gọi `pg_relation_size` cơ bản không cho thấy.

**Cơ cấu nơi lưu file đính kèm:**

```sql
SELECT type, COUNT(*), pg_size_pretty(SUM(file_size))
FROM ir_attachment
GROUP BY type;
```

<img src="{{< surl-static >}}/images/insights/odoo-db-growing-fast/attachment-storage-mix.png" alt="Các dòng ir_attachment nhóm theo type, cho thấy số lượng url và binary cùng tổng dung lượng binary"/>

Truy vấn này cho biết các byte đang nằm trong PostgreSQL (`binary` với `db_datas` có dữ liệu), trong filestore, hay đã ở trên cloud (kiểu `cloud_storage`, có từ Odoo 18.0 trở đi).

**Lượng chatter theo từng model:**

```sql
SELECT model, COUNT(*)
FROM mail_message
GROUP BY model
ORDER BY 2 DESC
LIMIT 20;
```

<img src="{{< surl-static >}}/images/insights/odoo-db-growing-fast/chatter-volume.png" alt="Số dòng mail_message nhóm theo model, với account.move đứng đầu"/>

Phần đầu danh sách đó (thường là `account.move`, `stock.picking`, `sale.order` ở hầu hết hệ thống) cho bạn biết model nào có dấu vết thông báo dày đặc nhất.

**Dung lượng filestore trên đĩa:** `du -sh <data_dir>/filestore/<dbname>`. Kết quả mẫu trên một hệ thống production cỡ vừa:

```text
$ du -sh /var/lib/odoo/filestore/prod
42G     /var/lib/odoo/filestore/prod
```

Hãy so với truy vấn cơ cấu nơi lưu file đính kèm ở trên. Một filestore lớn không phải là phình vô ích. Một filestore lớn cộng với `ir_attachment.db_datas` lớn mới là.

Nếu bạn dùng CLI `odoo-db` của Trobz, ba lệnh phủ được phần lớn những việc trên: `odoo-db stats <db>` cho số bản ghi và dung lượng theo từng bảng kèm mức tăng trưởng theo năm, `odoo-db bloat <db>` cho phần phình có thể thu hồi ở bảng và index (chính xác qua `pgstattuple` khi extension đã cài, kèm tỷ lệ tuple chết, autovacuum bị ì và index không dùng đến), và `odoo-db attachments <db>` cho phân bố nơi lưu file đính kèm cùng danh sách ứng viên dọn dẹp và lưu trữ.

Đến đây bạn đã biết thủ phạm. Thứ tự xử lý: dọn, nén, đưa ra ngoài, lưu trữ lâu dài.

## Bước 2: Dọn

**Hãy bắt đầu bằng thứ Odoo đã cho sẵn.** Cron `ir.attachment._gc_file_store()` có sẵn chạy hằng ngày và xóa các file trong filestore mà dòng `ir.attachment` tương ứng đã bị xóa, cộng thêm các dòng đính kèm trỏ tới bản ghi không còn tồn tại (`res_model` cộng `res_id` mồ côi). Nó không xóa `mail.message` hay các dòng đính kèm theo tuổi. Nếu dung lượng filestore của bạn lớn hơn hẳn tổng `ir_attachment` tương ứng ở bước 1, hãy kiểm tra cron này có đang bật và không bị lỗi, trước khi cài thêm bất cứ thứ gì.

Ngoài `mail.message` và `ir.attachment`, còn bốn bảng nữa phình lên âm thầm trên các hệ thống bận rộn. Hãy đối chiếu từng bảng với top 20 ở bước 1:

- `mail.notification` (mỗi người nhận mỗi tin nhắn một dòng — nhân lên theo số người theo dõi phục vụ kiểm toán)
- `mail.tracking.value` (mỗi lần đổi một trường được theo dõi là một dòng — `account.move` và `stock.picking` chiếm đa số)
- `bus.bus` (hàng đợi thông báo thời gian thực — có cron GC riêng từ 16.0; hãy kiểm tra nó đang bật)
- `ir.logging` (chỉ có dữ liệu khi `log_db` được đặt trong file INI của máy chủ; nếu không thì rỗng)

Module OCA nói ở phần tiếp theo đã tự động lan sang `mail.tracking.value` và `mail.followers`. Các bảng còn lại cần câu DELETE có phạm vi riêng nếu chúng đang phình.

Phần lớn mức tăng của `mail.message` là những dòng bạn sẽ không bao giờ xem lại: thay đổi trạng thái, thông báo tự động, sự kiện theo dõi. Hãy xóa chúng một cách an toàn bằng module [autovacuum_message_attachment](https://github.com/OCA/server-tools/tree/18.0/autovacuum_message_attachment) của OCA.

Module này mang theo hai cron (`AutoVacuum Mails and Messages` và `AutoVacuum Attachments`) và một bảng quy tắc tại *Configuration, Technical, Email, Message And Attachment Vacuum Rules*. Mỗi quy tắc cho tin nhắn kết hợp một model, một kiểu hoặc phân kiểu tin nhắn, và một thời hạn giữ tính bằng ngày. Mỗi quy tắc cho file đính kèm kết hợp một model, một chuỗi con trong tên, và một thời hạn giữ. Việc xóa tự động lan sang `mail.tracking.value` và `mail.followers`.

Nhịp chạy khuyến nghị: hằng ngày, vào giờ thấp điểm. Hãy bắt đầu với thời hạn giữ dè dặt (180 ngày cho thông báo, 365 ngày cho file đính kèm) rồi siết lại sau vài chu kỳ.

**Một cái bẫy:** quy tắc cho file đính kèm khớp theo chuỗi con trong tên. Một quy tắc với chuỗi `report` cộng model `account.move` thì không sao. Một quy tắc chỉ có chuỗi `report` sẽ đụng vào mọi file đính kèm trong database có chứa từ đó trong tên. Hãy luôn giới hạn kèm theo model.

Với luồng thư đến qua IMAP, module [mail_cleanup](https://github.com/OCA/server-tools/tree/18.0/mail_cleanup) của OCA bổ trợ cho những thứ trên bằng cách đánh dấu, di chuyển hoặc xóa hẳn thư ngay trên máy chủ IMAP, trước khi `fetchmail` nạp chúng vào `mail.message`. Cấu hình nằm trong file INI của máy chủ với các khóa `cleanup_days`, `purge_days` và `cleanup_folder`. Nó chỉ xử lý thư đến, không xử lý chatter nội bộ hay thông báo.

**Ràng buộc phiên bản cần nhớ:** cả hai module đều có trên các nhánh OCA 14.0, 16.0 và 18.0. Chúng không có trên 15.0, 17.0 hay 19.0 tại thời điểm viết bài. Trên các nhánh đó, hoặc bạn tự backport module, hoặc quay về viết script SQL xóa có phạm vi theo `model`, `subtype_id` và `create_date`.

## Bước 3: Nén

Bạn xóa một triệu dòng. Đĩa không nhỏ lại. Đây là chuyện của PostgreSQL, không phải của Odoo.

Trang trong PostgreSQL có kích thước 8 kilobyte. Một tuple không thể trải ra nhiều trang, nên khi một dòng vượt quá khoảng 2 kilobyte, PostgreSQL nén các giá trị lớn của nó và, nếu vẫn còn quá lớn, chuyển chúng ra ngoài vào một bảng phụ TOAST (mỗi bảng cha một bảng phụ). Khi bạn DELETE dòng cha, tuple chết ở cả bảng chính lẫn bảng TOAST chỉ được đánh dấu là chết chứ không được giải phóng. Phần đĩa đã cấp vẫn nằm nguyên cho tới khi có thứ gì đó thu hồi nó.

Ba đường thu hồi, xếp theo chi phí vận hành:

**[pg_repack](https://github.com/reorg/pg_repack) (`pg_repack -t mail_message <db>`)** dựng lại bảng ngay khi đang chạy mà không cần khóa độc quyền. Cần extension `pg_repack` do superuser cài. An toàn cho production. Trên các dịch vụ PostgreSQL có quản lý như AWS RDS hay Aiven, hãy kiểm tra extension có sẵn không trước khi trông cậy vào nó. Một lần chạy mẫu:

```text
$ pg_repack -d odoo_prod -t mail_message
INFO: repacking table "public.mail_message"
NOTICE: Setting up workspaces
NOTICE: Copying tuples
NOTICE: Swapping tables
NOTICE: Dropping the old tables
```

**`VACUUM (FULL, VERBOSE, ANALYZE) mail_message`** ghi lại bảng ngay tại chỗ. Giữ khóa độc quyền trong suốt thời gian chạy. Hãy xem nó như một lần ngừng dịch vụ có kế hoạch, dài tỷ lệ với kích thước bảng: một bảng `mail_message` 50 GB sẽ khóa hàng chục phút. Dùng trên staging, hoặc trong một khung bảo trì đã lên lịch. Kết quả mẫu:

```text
INFO:  vacuuming "public.mail_message"
INFO:  "mail_message": found 0 removable, 412903 nonremovable row versions in 28741 pages
DETAIL:  0 dead row versions cannot be removed yet.
CPU: user: 1.34 s, system: 0.48 s, elapsed: 18.27 s.
INFO:  analyzing "public.mail_message"
VACUUM
```

**`pg_dump -Fc | pg_restore` vào một database mới** ghi từng bảng một cách tuần tự sang database mới. Không còn phần phình nào ở đích. Thời gian ngừng dịch vụ nặng nhất, nhưng đây cũng là cách duy nhất thu hồi được dung lượng trên các dịch vụ có quản lý chặn `VACUUM FULL` hoặc `pg_repack`. Hãy gộp nó vào lần nâng cấp Odoo lớn kế tiếp.

Chọn theo ngân sách ngừng dịch vụ của bạn.

## Bước 4: Đưa ra ngoài

Một `ir.attachment` luôn có một dòng trong database. Phần byte thì có thể nằm ở một trong ba nơi: một cột bytea của PostgreSQL (`db_datas`), thư mục filestore (`store_fname` trỏ tới một file đặt tên theo sha1 dưới `<data_dir>/filestore/<dbname>/`), hoặc kho lưu trữ đám mây bên ngoài.

Lựa chọn này do tham số hệ thống `ir_attachment.location` điều khiển (`db` hoặc `file`, mặc định là `file`), cộng thêm bộ module `cloud_storage` từ Odoo 18.0 trở đi.

```python
@api.model
def _storage(self):
    return self.env['ir.config_parameter'].sudo().get_param('ir_attachment.location', 'file')
```

**Cái bẫy quan trọng:** đổi `ir_attachment.location` chỉ ảnh hưởng tới các lần tải lên *mới*. Các dòng sẵn có vẫn nằm nguyên chỗ cũ. Nếu bạn chuyển từ `db` sang `file` và mong phần phình ở `db_datas` biến mất, sẽ chẳng có gì dịch chuyển cho tới khi bạn chạy thao tác quản trị *Force Storage* (*Settings, Technical, Database Structure, Attachments, Action, Force Storage*) hoặc, trên 18.0 và 19.0 có cloud storage, chạy cron `cloud_storage_migration`. Hãy lên kế hoạch cho việc di chuyển tách bạch với việc đổi tham số.

Quyết định rẽ nhánh theo phiên bản Odoo và backend đích.

**Trên Odoo 18.0 hoặc 19.0, với Azure Blob hoặc Google Cloud Storage:** cài các module gốc [cloud_storage](https://github.com/odoo/odoo/tree/18.0/addons/cloud_storage) cộng với [cloud_storage_azure](https://github.com/odoo/odoo/tree/18.0/addons/cloud_storage_azure) hoặc [cloud_storage_google](https://github.com/odoo/odoo/tree/18.0/addons/cloud_storage_google), và có thể thêm [cloud_storage_migration](https://github.com/odoo/odoo/tree/18.0/addons/cloud_storage_migration) để chuyển các file sẵn có. File đính kèm mới sẽ mang `type='cloud_storage'`, và việc tải xuống được phục vụ bằng một lần chuyển hướng có chữ ký: Odoo ký một URL ngắn hạn, client tải thẳng từ cloud, và Odoo không bao giờ tự truyền các byte đó. Để chuyển các file sẵn có, hãy điền vào hai tham số hệ thống `cloud_storage_migration_all_models` và `cloud_storage_migration_message_models` các model bạn muốn đẩy ra ngoài, rồi kích hoạt cron *Migrate Local Attachment Binaries to Cloud Storage*. Cron duyệt từng file đính kèm một, với ngưỡng tuổi tối thiểu nằm sẵn trong truy vấn (rút gọn từ `cloud_storage_migration/models/ir_attachment.py`):

```python
query = SQL("""
    SELECT ia.id FROM ir_attachment ia
    WHERE ia.id <= %(max_attachment_id)s
      AND ia.type = 'binary'
      AND ia.store_fname IS NOT NULL
      AND ia.create_date < %(create_date)s
      -- plus filters on file_size, url, res_id, res_field,
      -- model scope, and documents.document linkage
    ORDER BY ia.id ASC LIMIT 1;
""",
    create_date=fields.Datetime.now() - timedelta(days=7),
)
```

Ngưỡng bảy ngày đó được gán cứng. Nó là một lan can an toàn, không phải một thời hạn giữ cấu hình được.

**Trên Odoo 16.0 hoặc 17.0, hoặc với bất kỳ backend nào khác ngoài Azure và GCS (S3, MinIO, OVH, Wasabi, SFTP, WebDAV):** cài [fs_storage](https://github.com/OCA/storage/tree/18.0/fs_storage) cộng [fs_attachment](https://github.com/OCA/storage/tree/18.0/fs_attachment) của OCA. Model `fs.storage` bọc thư viện Python `fsspec` và chấp nhận mọi giao thức mà fsspec phơi ra. `fs_attachment` mở rộng `ir.attachment` để định tuyến các byte theo từng model hoặc từng trường qua các backend đó, giữ nguyên tên file gốc (không còn tên sha1 khó hiểu), và tích hợp với X Sendfile để nginx phục vụ thẳng các khối dữ liệu. JSON `Force DB For Default Attachment Rules` giữ các ảnh nhỏ, bundle JavaScript và CSS trong PostgreSQL vì lý do hiệu năng: mặc định giữ lại trong database mọi ảnh dưới 50 kilobyte cùng toàn bộ asset.

**Một cái bẫy cần tránh:** OCA cũng có một [họ module cũ hơn](https://github.com/OCA/storage) (`storage_backend`, `storage_file`, `attachment_s3`) do cùng nhóm tác giả. Trên Odoo 18.0 chúng vẫn cài được, nhưng dự án mới nên dùng `fs_storage` và `fs_attachment`. Họ module cũ chỉ còn ở chế độ sửa lỗi.

**Lưu ý về sao lưu:** cột `db_datas` có `attachment=False` trong định nghĩa trường. Công cụ sao lưu filestore của Odoo không lấy nó. Chỉ `pg_dump` mới lấy. Nếu bạn đang ở chế độ lưu `db` và chiến lược sao lưu của bạn giả định là filestore cộng với một bản dump lược đồ logic không có phần nhị phân, thì bạn không có bản sao lưu nào cho các khối dữ liệu của mình.

## Bước 5: Lưu trữ lâu dài

Đây là chỗ mọi công cụ ở trên đều dừng lại.

Khách hàng Odoo thật sự có những yêu cầu lưu giữ kéo dài nhiều năm. Kế toán giữ hóa đơn theo khung luật định (thường là mười năm), nhưng khoảng thời gian truy cập nóng chỉ là một năm. Nhân sự giữ phiếu lương năm năm, nhưng phần lớn lượt đọc diễn ra trong năm hiện tại. Sản xuất giữ báo cáo sản xuất theo khung quy định, nhưng bộ phận vận hành chỉ đụng tới chúng trong quý hiện tại.

Khuôn mẫu các khách hàng này mong muốn khá đơn giản: sau N ngày hoặc N tháng, phân theo từng loại bản ghi, chuyển file đính kèm từ kho nóng sang một tầng lạnh, theo kiểu bất đồng bộ, có thử lại, và nạp lại theo kiểu lười khi có người mở tài liệu.

Hôm nay chưa module nào phủ chuyện này.

`cloud_storage_migration` gốc là gần nhất, nhưng nó vướng ba thứ. Nó chỉ hỗ trợ Azure và Google (không có S3, không có fsspec). Bộ lọc theo tuổi của nó là một ngưỡng bảy ngày gán cứng nhằm bảo vệ các file mới, chứ không phải một thời hạn giữ cấu hình được theo từng model. Nó chạy trong một worker cron duy nhất, không có cơ chế thử lại kiểu `queue_job`.

`fs_attachment` của OCA thì không có chính sách theo tuổi nào cả. Nó định tuyến file đính kèm mới theo model hoặc theo trường, nhưng việc định tuyến đó là tĩnh.

## Tra cứu nhanh

Toàn bộ ma trận phiên bản ở dạng văn bản, để bạn grep ra đúng ô của mình:

**Chẩn đoán:** các truy vấn SQL trên Odoo 16.0, 17.0, 18.0, 19.0. Không khác nhau về công cụ.

**Dọn trong database:** [autovacuum_message_attachment](https://github.com/OCA/server-tools/tree/18.0/autovacuum_message_attachment) của OCA trên 14.0, 16.0 và 18.0. Không có module trên 15.0, 17.0 hay 19.0; hãy backport hoặc chạy SQL có phạm vi.

**Dọn thư đến qua IMAP:** [mail_cleanup](https://github.com/OCA/server-tools/tree/18.0/mail_cleanup) của OCA trên cùng các nhánh nói trên. Vẫn thiếu ở 15.0, 17.0 và 19.0.

**Nén:** [pg_repack](https://github.com/reorg/pg_repack) hoặc `VACUUM FULL` hoặc dump rồi restore bằng `pg_dump`. Mọi phiên bản Odoo, tùy bạn chọn theo ngân sách ngừng dịch vụ.

**Đưa file đính kèm mới ra Azure hoặc GCS:** module gốc [cloud_storage](https://github.com/odoo/odoo/tree/18.0/addons/cloud_storage) trên 18.0 và 19.0. Không có trên 16.0 hay 17.0.

**Đưa file đính kèm mới ra S3, MinIO, OVH, SFTP, WebDAV và các backend khác:** [fs_storage](https://github.com/OCA/storage/tree/18.0/fs_storage) cộng [fs_attachment](https://github.com/OCA/storage/tree/18.0/fs_attachment) của OCA trên 16.0, 17.0 và 18.0. Bản port 19.0 chưa được công bố tại thời điểm viết bài; hãy kiểm tra [repo storage của OCA](https://github.com/OCA/storage) trước khi trông cậy vào nó.

**Chuyển các file đính kèm sẵn có, một lần, không bộ lọc:** thao tác quản trị `force_storage()` có sẵn trên mọi phiên bản.

**Chuyển các file đính kèm sẵn có sang Azure hoặc GCS, theo từng model với giới hạn theo lô:** cron [cloud_storage_migration](https://github.com/odoo/odoo/tree/18.0/addons/cloud_storage_migration) trên 18.0 và 19.0, kèm ngưỡng bảy ngày mô tả ở trên.

## Kết luận

Hãy đo trước, sửa theo thứ tự (dọn, rồi nén, rồi đưa ra ngoài), sau đó chọn công cụ theo phiên bản Odoo nhân với backend. Nếu bài toán của bạn là lưu trữ lâu dài theo tuổi và theo từng loại bản ghi, hãy liên hệ với chúng tôi: chúng tôi đang làm một module `attachment_archive_policy` chưa công bố nhưng có thể hợp với nhu cầu của bạn.

