From e045e76e35f3d14030a49728f258cd31476ca93d Mon Sep 17 00:00:00 2001 From: Fabien Pinckaers Date: Mon, 4 Apr 2022 12:34:42 +0000 Subject: [PATCH] [IMP] base: reduce new DB size by ordering columns Postgres is aligning columns to 4 or 8 bytes, depending on their type. So, consecutive fixed-length columns of differing size will be padded with empty bytes due to the alignment requirements. Before this patch, columns where created in their definition order. Now, they are ordered based on their size, in order to minimize the padding. As an example, before each row uses 36 bytes (+24b header): attname | typname | typlen -------------+-----------+-------- id | int4 | 4 create_uid | int4 | 4 create_date | timestamp | 8 write_uid | int4 | 4 -> 4 bytes padding write_date | timestamp | 8 active | bool | 1 -> 3 bytes padding After each row uses 32 bytes (4 bytes saved per row): attname | typname | typlen -------------+-----------+-------- id | int4 | 4 create_uid | int4 | 4 write_uid | int4 | 4 active | bool | 1 -> 3 bytes padding create_date | timestamp | 8 write_date | timestamp | 8 This saving scheme applies to all rows in all tables. We save between 4 and 8 bytes per row just on the usual create_uid, create_date, write_uid, write_date. closes odoo/odoo#87896 Signed-off-by: Raphael Collet --- odoo/addons/base/data/base_data.sql | 10 +++++----- odoo/fields.py | 7 ++++++- odoo/models.py | 8 ++++---- odoo/tools/sql.py | 12 ++++++++++++ 4 files changed, 27 insertions(+), 10 deletions(-) diff --git a/odoo/addons/base/data/base_data.sql b/odoo/addons/base/data/base_data.sql index b90c3bbb779..a40dc963ce7 100644 --- a/odoo/addons/base/data/base_data.sql +++ b/odoo/addons/base/data/base_data.sql @@ -14,14 +14,14 @@ CREATE TABLE ir_act_client (primary key(id)) INHERITS (ir_actions); CREATE TABLE res_users ( id serial NOT NULL, - active boolean default True, - login varchar(64) NOT NULL UNIQUE, - password varchar default null, -- No FK references below, will be added later by ORM -- (when the destination rows exist) company_id integer, -- references res_company, partner_id integer, -- references res_partner, + active boolean default True, create_date timestamp without time zone, + login varchar(64) NOT NULL UNIQUE, + password varchar default null, primary key(id) ); @@ -86,11 +86,11 @@ CREATE TABLE ir_model_data ( create_date timestamp without time zone DEFAULT (now() at time zone 'UTC'), write_date timestamp without time zone DEFAULT (now() at time zone 'UTC'), write_uid integer, + res_id integer, noupdate boolean DEFAULT False, name varchar NOT NULL, module varchar NOT NULL, model varchar NOT NULL, - res_id integer, primary key(id) ); @@ -113,9 +113,9 @@ CREATE TABLE res_company ( CREATE TABLE res_partner ( id serial, - name varchar, company_id integer, create_date timestamp without time zone, + name varchar, primary key(id) ); diff --git a/odoo/fields.py b/odoo/fields.py index 45c959ecbc6..3c73428c535 100644 --- a/odoo/fields.py +++ b/odoo/fields.py @@ -21,7 +21,7 @@ import pytz from .tools import ( float_repr, float_round, float_compare, float_is_zero, html_sanitize, human_size, pg_varchar, ustr, OrderedSet, pycompat, sql, date_utils, unique, - image_process, merge_sequences, + image_process, merge_sequences, SQL_ORDER_BY_TYPE, ) from .tools import DEFAULT_SERVER_DATE_FORMAT as DATE_FORMAT from .tools import DEFAULT_SERVER_DATETIME_FORMAT as DATETIME_FORMAT @@ -937,6 +937,11 @@ class Field(MetaField('DummyField', (object,), {})): # Update database schema # + @property + def column_order(self): + """ Prescribed column order in table. """ + return 0 if self.column_type is None else SQL_ORDER_BY_TYPE[self.column_type[0]] + def update_db(self, model, columns): """ Update the database schema to implement this field. diff --git a/odoo/models.py b/odoo/models.py index 2ba025553f5..0c01392b208 100644 --- a/odoo/models.py +++ b/odoo/models.py @@ -2850,9 +2850,9 @@ class BaseModel(metaclass=MetaModel): return field.column_type[1] + (" NOT NULL" if field.required else "") tools.create_model_table(cr, self._table, self._description, [ - (name, make_type(field), field.string) - for name, field in self._fields.items() - if name != 'id' and field.store and field.column_type + (field.name, make_type(field), field.string) + for field in sorted(self._fields.values(), key=lambda f: f.column_order) + if field.name != 'id' and field.store and field.column_type ]) if self._parent_store: @@ -2868,7 +2868,7 @@ class BaseModel(metaclass=MetaModel): columns = tools.table_columns(cr, self._table) fields_to_compute = [] - for field in self._fields.values(): + for field in sorted(self._fields.values(), key=lambda f: f.column_order): if not field.store: continue if field.manual and not update_custom_fields: diff --git a/odoo/tools/sql.py b/odoo/tools/sql.py index 8cbf8c883da..914cdd3bed0 100644 --- a/odoo/tools/sql.py +++ b/odoo/tools/sql.py @@ -6,6 +6,7 @@ import logging import psycopg2 import odoo.sql_db +from collections import defaultdict from contextlib import closing _schema = logging.getLogger('odoo.schema') @@ -50,6 +51,17 @@ def table_kind(cr, tablename): cr.execute(query, (tablename,)) return cr.fetchone()[0] if cr.rowcount else None +# prescribed column order by type size: 4 bytes, 1 byte, 8 bytes, variable size +# (values have been chosen to minimize padding in rows; unknown column types +# are considered variable size and put last) +SQL_ORDER_BY_TYPE = defaultdict(lambda: 6, { + 'int4': 1, # 4 bytes + 'date': 2, # 4 bytes + 'bool': 3, # 1 byte + 'float8': 4, # 8 bytes + 'timestamp': 5, # 8 bytes +}) + def create_model_table(cr, tablename, comment=None, columns=()): """ Create the table for a model. """ colspecs = ['id SERIAL NOT NULL'] + [