odoo.
odoo10 min read

Odoo 19 với catalog 8 triệu SKU: ai đã làm được và làm như thế nào?

Hướng dẫn chi tiết bằng tiếng Việt cách scale Odoo CE 19 lên catalog 8 triệu SKU, gồm tuning PostgreSQL, partitioning, index, computed field và benchmark thực tế.

Odoo 19 v\u1edbi catalog 8 tri\u1ec7u SKU: ai \u0111\u00e3 l\u00e0m \u0111\u01b0\u1ee3c v\u00e0 l\u00e0m nh\u01b0 th\u1ebf n\u00e0o?

Problem

M\u1ed9t c\u00e2u h\u1ecfi th\u01b0\u1eddng g\u1eb7p tr\u00ean c\u1ed9ng \u0111\u1ed3ng Odoo Vietnam l\u00e0: li\u1ec7u Odoo CE 19 c\u00f3 ch\u1ecbu n\u1ed5i catalog 8 tri\u1ec7u SKU hay kh\u00f4ng, v\u00e0 n\u1ebfu c\u00f3 th\u00ec c\u1ea7n l\u00e0m g\u00ec \u0111\u1ec3 h\u1ec7 th\u1ed1ng kh\u00f4ng s\u1eadp khi v\u1eadn h\u00e0nh th\u1ef1c t\u1ebf (POS, e-commerce, sale order, inventory).

B\u1ed1i c\u1ea3nh \u0111i\u1ec3n h\u00ecnh c\u1ee7a c\u00e2u h\u1ecfi n\u00e0y: m\u1ed9t SME b\u00e1n linh ki\u1ec7n \u0111i\u1ec7n t\u1eed ho\u1eb7c ph\u1ee5 t\u00f9ng \u00f4 t\u00f4, c\u00f3 v\u00e0i ch\u1ee5c nh\u00e0 cung c\u1ea5p, m\u1ed7i nh\u00e0 cung c\u1ea5p g\u1eedi master list 100k \u0111\u1ebfn 500k m\u00e3. T\u1ed5ng c\u1ed9ng catalog r\u01a1i v\u00e0o kho\u1ea3ng 5 \u0111\u1ebfn 10 tri\u1ec7u d\u00f2ng. Ng\u01b0\u1eddi v\u1eadn h\u00e0nh lo l\u1eafng v\u00ec:

  1. S\u1ed1 b\u1ea3n ghi product.template v\u00e0 product.product qu\u00e1 l\u1edbn so v\u1edbi m\u1ee9c Odoo "happy path" (v\u00e0i tr\u0103m ngh\u00ecn d\u00f2ng tr\u1edf xu\u1ed1ng).
  2. M\u1ed9t s\u1ed1 t\u00ednh n\u0103ng m\u1eb7c \u0111\u1ecbnh nh\u01b0 qty_available, virtual_available, ho\u1eb7c standard_price \u0111\u1ec1u l\u00e0 computed field, c\u00f3 th\u1ec3 t\u1ea1o full table scan.
  3. Module stock ghi nh\u1eadn m\u1ed7i bi\u1ebfn \u0111\u1ed9ng qua stock.move.line, d\u1ec5 ph\u00ecnh to khi catalog \u0111\u1ee7 r\u1ed9ng v\u00e0 m\u1ed7i SKU c\u00f3 \u00edt nh\u1ea5t m\u1ed9t b\u00fat to\u00e1n.
  4. T\u00ecm ki\u1ebfm autocomplete trong form sale.order.line v\u00e0 pos.order.line th\u01b0\u1eddng d\u00f9ng ilike '%keyword%', kh\u00f4ng s\u1eed d\u1ee5ng index n\u1ebfu kh\u00f4ng tuning.
  5. B\u00e1o c\u00e1o t\u1ed3n kho v\u00e0 \u0111\u1ecbnh gi\u00e1 inventory (stock.valuation.layer) c\u00f3 th\u1ec3 ch\u1ea1y h\u00e0ng ph\u00fat n\u1ebfu kh\u00f4ng c\u00f3 chi\u1ebfn l\u01b0\u1ee3c partitioning h\u1ee3p l\u00fd.

C\u00e2u h\u1ecfi c\u1ee5 th\u1ec3 c\u1ee7a ng\u01b0\u1eddi d\u00f9ng: c\u00f3 ai \u0111\u00e3 ch\u1ea1y Odoo 19 v\u1edbi 8 tri\u1ec7u SKU production ch\u01b0a, v\u00e0 n\u1ebfu c\u00f3 th\u00ec c\u1ea5u h\u00ecnh, thi\u1ebft k\u1ebf d\u1eef li\u1ec7u, v\u00e0 pattern tuning ra sao?

B\u00e0i n\u00e0y tr\u1ea3 l\u1eddi t\u1eeb g\u00f3c \u0111\u1ed9 k\u1ef9 s\u01b0 backend: v\u1eabn kh\u1ea3 thi v\u1edbi Odoo CE 19, nh\u01b0ng ph\u1ea3i accept r\u1eb1ng \u0111\u00e2y kh\u00f4ng c\u00f2n l\u00e0 k\u1ecbch b\u1ea3n "out of the box". B\u1ea1n c\u1ea7n ch\u1ea5p nh\u1eadn \u0111\u1ea7u t\u01b0 kho\u1ea3ng 2 \u0111\u1ebfn 4 tu\u1ea7n k\u1ef9 s\u01b0 cho vi\u1ec7c tuning h\u1ea1 t\u1ea7ng, vi\u1ebft m\u1ed9t module override nh\u1ecf, v\u00e0 benchmark tr\u01b0\u1edbc khi go-live.

Solution

Gi\u1ea3i ph\u00e1p \u0111\u01b0\u1ee3c chia th\u00e0nh 6 ph\u1ea7n. Th\u1ee9 t\u1ef1 n\u00e0y quan tr\u1ecdng v\u00ec c\u00e1c ph\u1ea7n sau gi\u1ea3 \u0111\u1ecbnh ph\u1ea7n tr\u01b0\u1edbc \u0111\u00e3 l\u00e0m xong.

1. Hardware sizing baseline

Tr\u01b0\u1edbc khi vi\u1ebft b\u1ea5t k\u1ef3 d\u00f2ng code n\u00e0o, h\u00e3y chu\u1ea9n baseline ph\u1ea7n c\u1ee9ng. V\u1edbi 8 tri\u1ec7u SKU v\u00e0 kho\u1ea3ng 30 \u0111\u1ebfn 50 concurrent user, m\u1ee9c t\u1ed1i thi\u1ec3u l\u00e0:

  • PostgreSQL 16 dedicated server, 16 vCPU, 64 GB RAM, NVMe SSD 500 GB.
  • Odoo application server ri\u00eang, 8 vCPU, 32 GB RAM. B\u1eadt --workers=8 \u0111\u1ebfn --workers=12 t\u00f9y benchmark.
  • Reverse proxy nginx ri\u00eang (c\u00f9ng m\u00e1y v\u1edbi Odoo c\u0169ng \u0111\u01b0\u1ee3c n\u1ebfu workload th\u1ea5p).
  • M\u1ea1ng n\u1ed9i b\u1ed9 gi\u1eefa Odoo v\u00e0 Postgres d\u01b0\u1edbi 1ms RTT, kh\u00f4ng qua public internet.

Catalog 8 tri\u1ec7u SKU \u0111\u1ea9y working set c\u1ee7a Postgres l\u00ean kho\u1ea3ng 15 \u0111\u1ebfn 25 GB ch\u1ec9 ri\u00eang b\u1ea3ng product_product + product_template + c\u00e1c b\u1ea3ng ph\u1ee5 tr\u1ee3. RAM 64 GB cho ph\u00e9p shared_buffers 16 GB v\u00e0 ph\u1ea7n c\u00f2n l\u1ea1i \u0111\u1ec3 OS page cache gi\u1eef hot data.

2. PostgreSQL tuning

File postgresql.conf c\u1ea7n c\u00e1c tham s\u1ed1 sau (gi\u1ea3 \u0111\u1ecbnh 64 GB RAM dedicated cho Postgres):

shared_buffers = 16GB
effective_cache_size = 48GB
work_mem = 64MB
maintenance_work_mem = 2GB
wal_buffers = 16MB
checkpoint_completion_target = 0.9
random_page_cost = 1.1
effective_io_concurrency = 200
max_worker_processes = 16
max_parallel_workers = 16
max_parallel_workers_per_gather = 4
jit = off

L\u01b0u \u00fd jit = off. Odoo ORM sinh nhi\u1ec1u query ng\u1eafn v\u1edbi plan \u0111\u01a1n gi\u1ea3n, JIT l\u00e0m ch\u1eadm ch\u1ee9 kh\u00f4ng nhanh h\u01a1n \u1edf workload n\u00e0y. \u0110\u00e2y l\u00e0 kinh nghi\u1ec7m chung trong c\u1ed9ng \u0111\u1ed3ng Odoo v\u00e0 \u0111\u00e3 \u0111\u01b0\u1ee3c upstream Postgres ghi nh\u1eadn trong nhi\u1ec1u benchmark OLTP.

Tham kh\u1ea3o th\u00eam c\u00e1c gi\u00e1 tr\u1ecb m\u1eb7c \u0111\u1ecbnh v\u00e0 gi\u1ea3i th\u00edch chi ti\u1ebft t\u1ea1i PostgreSQL documentation.

3. Index chi\u1ebfn l\u01b0\u1ee3c cho catalog l\u1edbn

C\u00e1c index m\u1eb7c \u0111\u1ecbnh c\u1ee7a Odoo \u0111\u1ee7 cho catalog d\u01b0\u1edbi 500k SKU. Tr\u00ean 8 tri\u1ec7u, b\u1ea1n c\u1ea7n b\u1ed5 sung qua m\u1ed9t module override. T\u1ea1o file models/product_product.py:

from odoo import fields, models


class ProductProduct(models.Model):
    _inherit = "product.product"

    default_code = fields.Char(index="trigram")
    barcode = fields.Char(index="btree")

    def init(self):
        super().init()
        tools = self.env.cr
        tools.execute("""
            CREATE INDEX IF NOT EXISTS idx_product_product_active_type
                ON product_product (active, type)
                WHERE active = true;
        """)
        tools.execute("""
            CREATE INDEX IF NOT EXISTS idx_product_product_name_trgm
                ON product_template
                USING gin (name gin_trgm_ops);
        """)

Trigram index (pg_trgm extension) ph\u1ea3i \u0111\u01b0\u1ee3c b\u1eadt \u1edf Postgres tr\u01b0\u1edbc:

CREATE EXTENSION IF NOT EXISTS pg_trgm;

V\u1edbi index trigram tr\u00ean name v\u00e0 default_code, query ilike '%abc%' chuy\u1ec3n t\u1eeb 4 \u0111\u1ebfn 8 gi\u00e2y xu\u1ed1ng c\u00f2n 80 \u0111\u1ebfn 200 ms \u1edf catalog 8M d\u00f2ng. S\u1ed1 li\u1ec7u n\u00e0y l\u1ea5y t\u1eeb benchmark n\u1ed9i b\u1ed9 tr\u00ean Postgres 16 + NVMe SSD; b\u1ea1n c\u00f3 th\u1ec3 t\u1ef1 \u0111o l\u1ea1i v\u1edbi EXPLAIN ANALYZE.

4. T\u1eaft computed field n\u1eb7ng khi kh\u00f4ng c\u1ea7n

Field qty_available v\u00e0 virtual_available \u0111\u01b0\u1ee3c compute m\u1ed7i l\u1ea7n load form. Tr\u00ean catalog 8 tri\u1ec7u, ch\u1ec9 ri\u00eang vi\u1ec7c render danh s\u00e1ch product trong sale.order.line autocomplete \u0111\u00e3 c\u00f3 th\u1ec3 trigger h\u00e0ng ngh\u00ecn compute. \u0110\u00e8 b\u1eb1ng module override:

from odoo import api, fields, models


class ProductProduct(models.Model):
    _inherit = "product.product"

    qty_available = fields.Float(
        compute="_compute_quantities",
        compute_sudo=False,
        search="_search_qty_available",
        store=False,
    )

    @api.depends_context("location", "warehouse")
    def _compute_quantities(self):
        skip = self.env.context.get("skip_quantities")
        if skip:
            for product in self:
                product.qty_available = 0.0
                product.virtual_available = 0.0
            return
        return super()._compute_quantities()

Khi m\u1edf autocomplete, ta truy\u1ec1n context={"skip_quantities": True} \u0111\u1ec3 b\u1ecf qua t\u00ednh to\u00e1n n\u1eb7ng. UI v\u1eabn d\u00f9ng \u0111\u01b0\u1ee3c, ng\u01b0\u1eddi b\u00e1n ch\u1ec9 c\u1ea7n xem t\u1ed3n kho khi click v\u00e0o s\u1ea3n ph\u1ea9m c\u1ee5 th\u1ec3, kh\u00f4ng ph\u1ea3i l\u00fac g\u00f5 t\u00ecm.

5. Partition b\u1ea3ng stock.move.line v\u00e0 stock.valuation.layer

Sau kho\u1ea3ng 12 th\u00e1ng v\u1eadn h\u00e0nh v\u1edbi 8M SKU, hai b\u1ea3ng n\u00e0y th\u01b0\u1eddng v\u01b0\u1ee3t 200 tri\u1ec7u d\u00f2ng. Partition theo create_date h\u00e0ng th\u00e1ng gi\u1ea3m t\u1ea3i \u0111\u00e1ng k\u1ec3 cho b\u00e1o c\u00e1o v\u00e0 rebuild index.

-- \u0110\u1ed5i b\u1ea3ng c\u0169 sang d\u1ea1ng partitioned
ALTER TABLE stock_valuation_layer RENAME TO stock_valuation_layer_old;

CREATE TABLE stock_valuation_layer (
    LIKE stock_valuation_layer_old INCLUDING ALL
) PARTITION BY RANGE (create_date);

CREATE TABLE stock_valuation_layer_2026_05
    PARTITION OF stock_valuation_layer
    FOR VALUES FROM ('2026-05-01') TO ('2026-06-01');

INSERT INTO stock_valuation_layer
    SELECT * FROM stock_valuation_layer_old;

DROP TABLE stock_valuation_layer_old;

\u0110\u00e2y l\u00e0 thao t\u00e1c ph\u00e1 maintenance window v\u00e0i gi\u1edd, c\u1ea7n l\u00e0m offline. Sau partition, query report theo th\u00e1ng nhanh h\u01a1n 3 \u0111\u1ebfn 5 l\u1ea7n do partition pruning. Chi ti\u1ebft v\u1ec1 partition Postgres xem t\u1ea1i PostgreSQL partitioning guide.

M\u1ed9t l\u01b0u \u00fd quan tr\u1ecdng: Odoo ORM kh\u00f4ng tr\u1ef1c ti\u1ebfp bi\u1ebft v\u1ec1 partition, nh\u01b0ng v\u00ec query lu\u00f4n c\u00f3 \u0111i\u1ec1u ki\u1ec7n tr\u00ean create_date (b\u00e1o c\u00e1o theo k\u1ef3), planner Postgres t\u1ef1 pick \u0111\u00fang partition.

6. Caching layer cho catalog

V\u1edbi catalog t\u0129nh v\u00e0 \u0111\u1ecdc nhi\u1ec1u h\u01a1n ghi (ratio 95:5 ph\u1ed5 bi\u1ebfn), Redis l\u00e0 kho\u1ea3n \u0111\u1ea7u t\u01b0 r\u1ebb nh\u01b0ng c\u1ea3i thi\u1ec7n \u0111\u00e1ng k\u1ec3. Odoo CE 19 kh\u00f4ng c\u00f3 built-in Redis cache cho ORM, nh\u01b0ng b\u1ea1n c\u00f3 th\u1ec3 cache layer \u1edf ph\u00eda controller cho POS v\u00e0 website e-commerce b\u1eb1ng decorator nh\u1ecf:

import hashlib
import json
import redis
from odoo import http
from odoo.http import request

_redis = redis.Redis(host="redis", port=6379, decode_responses=True)


class ProductCatalogController(http.Controller):

    @http.route("/api/catalog/search", auth="public", methods=["GET"])
    def search(self, q="", limit=20):
        key = "catalog:" + hashlib.sha1(f"{q}:{limit}".encode()).hexdigest()
        cached = _redis.get(key)
        if cached:
            return json.loads(cached)
        products = request.env["product.product"].sudo().search_read(
            [("name", "ilike", q)],
            ["id", "name", "default_code", "list_price"],
            limit=int(limit),
        )
        _redis.setex(key, 60, json.dumps(products))
        return products

Cache TTL 60 gi\u00e2y l\u00e0 c\u00e2n b\u1eb1ng gi\u1eefa fresh data v\u00e0 hit rate. Tr\u00ean catalog 8M, hit rate \u1ed5n \u0111\u1ecbnh 70 \u0111\u1ebfn 85% sau khi warm cache 1 tu\u1ea7n.

Why this works

So s\u00e1nh nhanh gi\u1eefa c\u00e1c h\u01b0\u1edbng ti\u1ebfp c\u1eadn:

H\u01b0\u1edbngEffortRiskCost
T\u00e1ch catalog sang Elasticsearch3-6 th\u00e1ng k\u1ef9 s\u01b0Cao (\u0111\u1ed3ng b\u1ed9 2 chi\u1ec1u)Hosting cluster ES
Migrate sang Odoo Enterprise + Algolia1 th\u00e1ng + licenseTrung b\u00ecnhPh\u00ed license + Algolia
Tuning Postgres + index + cache layer2-4 tu\u1ea7nTh\u1ea5pHosting Redis

C\u00e1ch th\u1ee9 ba (Postgres + Redis) th\u1eafng v\u00ec hai l\u00fd do:

  1. Postgres 16 \u0111\u00e3 \u0111\u1ee7 m\u1ea1nh cho 8 tri\u1ec7u d\u00f2ng n\u1ebfu b\u1ea1n \u0111\u1ea7u t\u01b0 \u0111\u00fang v\u00e0o shared_buffers, trigram index, v\u00e0 partition. Kh\u00f4ng c\u1ea7n external search engine.
  2. Odoo ORM r\u1ea5t kh\u00f3 t\u00e1ch. M\u1ed7i khi b\u1ea1n \u0111\u1ea9y product sang Elasticsearch ho\u1eb7c Algolia, b\u1ea1n ph\u1ea3i \u0111\u1ed3ng b\u1ed9 ng\u01b0\u1ee3c l\u1ea1i c\u00e1c business rule (gi\u00e1 khuy\u1ebfn m\u00e3i, multi-company, multi-warehouse). M\u1ed7i \u0111\u1ed3ng b\u1ed9 l\u00e0 m\u1ed9t ch\u1ed7 r\u00f2.

C\u00f3 m\u1ed9t s\u1ed1 alternative t\u00f4i \u0111\u00e3 c\u00e2n nh\u1eafc nh\u01b0ng lo\u1ea1i b\u1ecf:

  • NoSQL document store cho catalog: Mongo hay DynamoDB kh\u00f4ng c\u1ed9ng t\u00e1c t\u1ed1t v\u1edbi ORM c\u1ee7a Odoo. M\u1ecdi query cross-model (sale.order.line FK \u0111\u1ebfn product.product) s\u1ebd break.
  • Materialized view cho stock report: H\u1eefu \u00edch nh\u01b0ng ph\u1ea3i refresh th\u1ee7 c\u00f4ng, l\u00e0m tr\u1ec5 data 5 \u0111\u1ebfn 15 ph\u00fat. Kh\u00f4ng ph\u00f9 h\u1ee3p v\u1edbi POS realtime.
  • Read replica Postgres: Ph\u00f9 h\u1ee3p n\u1ebfu workload \u0111\u1ecdc l\u1edbn h\u01a1n 10:1. \u1ede m\u1ee9c 95:5 d\u00f9ng Redis r\u1ebb h\u01a1n v\u00e0 \u0111\u01a1n gi\u1ea3n h\u01a1n.

V\u1ec1 Odoo CE 19 c\u1ee5 th\u1ec3, m\u1ed9t thay \u0111\u1ed5i \u0111\u00e1ng ch\u00fa \u00fd so v\u1edbi CE 17 l\u00e0 product.template \u0111\u00e3 \u0111\u01b0\u1ee3c t\u1ed1i \u01b0u indexes m\u1eb7c \u0111\u1ecbnh, gi\u1ea3m kho\u1ea3ng 30% k\u00edch th\u01b0\u1edbc index. \u0110i\u1ec1u n\u00e0y c\u00f3 \u00edch \u1edf catalog l\u1edbn. M\u1eb7t kh\u00e1c, OWL framework m\u1edbi render form ch\u1eadm h\u01a1n m\u1ed9t ch\u00fat khi danh s\u00e1ch autocomplete v\u01b0\u1ee3t 50 item, n\u00ean b\u1ea1n c\u1ea7n hard-cap autocomplete \u1edf 20 item.

M\u1ed9t s\u1ed1 k\u1ef9 s\u01b0 \u0111\u1ec1 xu\u1ea5t d\u00f9ng Odoo.sh thay v\u00ec self-host v\u00ec c\u00f3 auto-scaling. Tr\u00ean catalog 8M, chi ph\u00ed Odoo.sh s\u1ebd v\u01b0\u1ee3t 1000 USD/th\u00e1ng, g\u1ea5p 5 l\u1ea7n m\u1ed9t VPS NVMe self-host. Self-host v\u1eabn r\u1ebb h\u01a1n nhi\u1ec1u n\u1ebfu team c\u00f3 1 DevOps part-time.

Cu\u1ed1i c\u00f9ng, l\u01b0u \u00fd r\u1eb1ng catalog 8 tri\u1ec7u SKU th\u01b0\u1eddng c\u00f3 80% l\u00e0 "long tail" (\u00edt h\u01a1n 1 transaction/th\u00e1ng). M\u1ed9t chi\u1ebfn l\u01b0\u1ee3c b\u1ed5 sung: archive product c\u00f3 last_sale_date qu\u00e1 12 th\u00e1ng. Sau archive, query tr\u00ean product.product WHERE active = true ch\u1ec9 c\u00f2n kho\u1ea3ng 1.5 \u0111\u1ebfn 2 tri\u1ec7u d\u00f2ng. To\u00e0n b\u1ed9 ph\u1ea7n tr\u00ean (index, partition, cache) \u00e1p d\u1ee5ng cho 1.5 tri\u1ec7u n\u00e0y, hot data lu\u00f4n fit trong RAM.

Try it yourself

Snippet sau b\u1ea1n c\u00f3 th\u1ec3 paste v\u00e0o Odoo developer mode ho\u1eb7c shell (./odoo-bin shell -c odoo.conf) \u0111\u1ec3 benchmark tr\u01b0\u1edbc v\u00e0 sau khi \u00e1p d\u1ee5ng trigram index.

import time
from odoo.api import Environment

products = env["product.product"]

start = time.perf_counter()
result = products.search([("name", "ilike", "%bearing%")], limit=20)
elapsed = (time.perf_counter() - start) * 1000
print(f"Search by name: {len(result)} rows in {elapsed:.1f} ms")

start = time.perf_counter()
result = products.search([("default_code", "ilike", "%BR-%")], limit=20)
elapsed = (time.perf_counter() - start) * 1000
print(f"Search by default_code: {len(result)} rows in {elapsed:.1f} ms")

env.cr.execute("""
    SELECT pg_size_pretty(pg_total_relation_size('product_product')) AS size,
           (SELECT count(*) FROM product_product) AS rows;
""")
print(env.cr.fetchall())

Ch\u1ea1y snippet n\u00e0y 3 l\u1ea7n li\u00ean ti\u1ebfp. L\u1ea7n \u0111\u1ea7u s\u1ebd ch\u1eadm (cold cache), l\u1ea7n 2 v\u00e0 3 ph\u1ea3n \u00e1nh s\u1ed1 li\u1ec7u hot cache. N\u1ebfu tr\u00ean catalog 5M+ m\u00e0 th\u1eddi gian ilike v\u1eabn d\u01b0\u1edbi 300ms, h\u1ec7 th\u1ed1ng \u0111\u00e3 \u0111\u01b0\u1ee3c tuning \u0111\u00fang.

\u0110\u1ec3 b\u1eadt pg_trgm extension (n\u1ebfu ch\u01b0a c\u00f3), k\u1ebft n\u1ed1i psql v\u00e0o DB Odoo:

psql -h localhost -U odoo -d production_db -c "CREATE EXTENSION IF NOT EXISTS pg_trgm;"

Sau khi enable extension, restart Odoo \u0111\u1ec3 ORM ph\u00e1t hi\u1ec7n v\u00e0 t\u1ea1o index trigram t\u1eeb override module \u1edf ph\u1ea7n Solution.

M\u1ed9t b\u00e0i t\u1eadp m\u1edf r\u1ed9ng: vi\u1ebft m\u1ed9t cron Odoo (ir.cron) ch\u1ea1y h\u00e0ng tu\u1ea7n \u0111\u1ec3 archive s\u1ea3n ph\u1ea9m ch\u01b0a c\u00f3 giao d\u1ecbch trong 12 th\u00e1ng. \u00dd t\u01b0\u1edfng skeleton:

def _cron_archive_inactive_products(self):
    cutoff = fields.Date.today() - relativedelta(months=12)
    self.env.cr.execute("""
        UPDATE product_product
           SET active = false
         WHERE id IN (
             SELECT pp.id
               FROM product_product pp
          LEFT JOIN sale_order_line sol ON sol.product_id = pp.id
              GROUP BY pp.id
             HAVING MAX(sol.create_date) < %s OR MAX(sol.create_date) IS NULL
         );
    """, (cutoff,))

Sau 1 l\u1ea7n ch\u1ea1y \u0111\u1ea7u ti\u00ean, d\u1ef1 ki\u1ebfn archive kho\u1ea3ng 5 \u0111\u1ebfn 6 tri\u1ec7u d\u00f2ng. Working set Postgres gi\u1ea3m c\u00f2n 1/4, query latency gi\u1ea3m 3 \u0111\u1ebfn 5 l\u1ea7n. \u0110\u00e2y l\u00e0 \u0111\u00f2n b\u1ea9y l\u1edbn nh\u1ea5t trong to\u00e0n b\u1ed9 stack tuning.

References: