Saving a penny on large volumes in PostgreSQL

Continuing the topic of recording large streams of data raised in the previous article about partitioning, in this one we will consider methods by which you can reduce the 'physical' size of stored data in PostgreSQL, and their impact on server performance.

We will talk about TOAST settings and data alignment.. On average, these methods will save not too many resources, but without any modification of the application code.

Saving a penny on large volumes in PostgreSQL
However, our experience turned out to be quite productive in this regard, as the storage for almost any monitoring is, by its nature, mostly append-only in terms of the data being written. And if you're curious about how to make the database write to disk instead of 200MB/s half that — please read on.

Little secrets of big data

According to the profile of our service, it regularly receives text packets from logs.And since the IBS complex, whose databases we monitor, is a multi-component product with complex data structures, the queries.

to achieve maximum performance end up being quitelike 'multi-volume' with complex algorithmic logic. Thus, the size of each individual query instance or the resulting execution plan in the incoming log turns out to be, on average, quite large. Let's take a look at the structure of one of the tables where we write 'raw' data — that is, straight from the original log entry: CREATE TABLE rawdata_orig( pack -- PK uuid NOT NULL , recno -- PK smallint NOT NULL , dt -- section key date , data -- the most important text , PRIMARY KEY(pack, recno) );A typical such table (already partitioned, of course, so this is a section template), where the most important part is the text. Sometimes it's quite large.

Remember that the 'physical' size of a single record in PG cannot exceed one page of data, but the 'logical' size is a completely different matter. To write a large value (varchar/text/bytea) into a field, the

TOAST technology is used.

A typical table like this (already partitioned, of course, so this is a section template), where the most important part is the text. Sometimes, it's quite extensive.

Recall that the 'physical' size of a single entry in PG cannot exceed one data page, but the 'logical' size is a different matter. To write a large value in a field (varchar/text/bytea), the TOAST technology:

PostgreSQL uses a fixed page size (usually 8 KB) and does not allow tuples to occupy multiple pages. Therefore, it is not possible to directly store very large field values. To overcome this limitation, large field values are compressed and/or split into several physical rows. This occurs transparently to the user and has minimal impact on most of the server code. This method is known as TOAST …

In fact, for each table with 'potentially large' fields, a paired table with 'slices' is automatically created for each 'large' record in segments of 2KB: TOAST( chunk_id integer , chunk_seq integer , chunk_data bytea , PRIMARY KEY(chunk_id, chunk_seq) );

That is, if we need to write a row with a 'large' value, the actual record will occur

not only in the main table and its PK but also in TOAST and its PK. dataReducing TOAST impact However, most records are not that large,.

and should fit within 8KB.

— How can we save on this?.. Here, the column attribute STORAGE

comes to our aid: EXTENDED allows both compression and separate storage. This

  • is the standard option for most data types compatible with TOAST. First, an attempt is made to compress, then — to store outside the table if the row is still too large. MAIN allows compression but not separate storage. (In fact, separate storage will still be performed for such columns, but only
  • as a last resort , when there is no other way to reduce the row to fit on the page.) In fact, this is exactly what we need for text —to maximize compression, and if it still doesn't fit — to move it to TOAST.

This can be done right 'on the fly' with a single command: ALTER TABLE rawdata_orig ALTER COLUMN data SET STORAGE MAIN;How to assess the effect

Since the data flow changes daily, we cannot compare absolute figures, but relatively, the smaller the proportion

we recorded in TOAST — the better. However, there is a danger — the larger the 'physical' volume of each individual record, the 'wider' the index becomes, as it has to cover a greater number of data pages.

Before changes heap = 37GB (39%) TOAST = 54GB (57%) PK = 4GB ( 4%) After changes

Section heap = 37GB (67%) TOAST = 16GB (29%) PK = 2GB ( 4%):

In fact, we

Section have started writing to TOAST twice as infrequently.:

heap  = 37GB (67%)
TOAST = 16GB (29%)
PK    =  2GB ( 4%)

In fact, we have started writing to TOAST half as often, which relieved not only the disk but also the CPU:

Saving a penny on large volumes in PostgreSQL
Saving a penny on large volumes in PostgreSQL
I would note that we have also started to «read» the disk less, not just «write» — since when inserting a record into a table, we also have to «read» part of the tree for each of the indexes to determine its future position in them.

Who lives well on PostgreSQL 11

After upgrading to PG11, we decided to continue the «tuning» of TOAST and noted that starting from this version, the parameter became available for configuration toast_tuple_target:

The TOAST processing code is triggered only when the value of the row that needs to be stored in the table exceeds the size of TOAST_TUPLE_THRESHOLD bytes (usually 2 KB). The TOAST code will compress and/or move field values outside the table until the row value is less than TOAST_TUPLE_TARGET bytes (a variable size, usually also 2 KB) or further reduction is no longer possible.

We decided that our data are usually either "quite short" or "very long", so we decided to limit ourselves to the minimally possible value:

ALTER TABLE rawplan_orig SET (toast_tuple_target = 128);

Let's see how the new settings affected disk load after readjustment:

Saving a penny on large volumes in PostgreSQL
Not bad! The average disk queue has decreased by about 1.5 times, and the disk's occupancy — by 20%! But maybe this somehow affected the CPU?

Saving a penny on large volumes in PostgreSQL
At least, it definitely did not get worse. Although, it's hard to judge if even such volumes still cannot raise the average CPU load above 5%.

The sum changes when the terms are rearranged!

As is known, a penny saves a ruble, and with our storage volumes of about 10TB/month even a slight optimization can provide a good profit. Therefore, we paid attention to the physical structure of our data — specifically how fields are "arranged" within each record of the tables.

Because due to data alignment it directly affects the resulting volume.:

Many architectures provide for data alignment on machine word boundaries. For example, on a 32-bit x86 system, integers (type integer, occupying 4 bytes) will be aligned on a 4-byte word boundary, as will double-precision floating point numbers (type double precision, 8 bytes). On a 64-bit system, double values will be aligned on an 8-byte word boundary. This is yet another reason for incompatibility.

Due to alignment, the size of a table row depends on the order of field placement. This effect is usually not very noticeable, but in some cases, it can lead to a significant increase in size. For example, if you mix fields of types char(1) and integer, there will typically be 3 unused bytes between them.

Let's start with synthetic models:

SELECT pg_column_size(ROW(
  '0000-0000-0000-0000-0000-0000-0000-0000'::uuid
, 0::smallint
, '2019-01-01'::date
));
-- 48 bytes

SELECT pg_column_size(ROW(
  '2019-01-01'::date
, '0000-0000-0000-0000-0000-0000-0000-0000'::uuid
, 0::smallint
));
-- 46 bytes

Where did the extra bytes come from in the first case? It's simple — the 2-byte smallint is aligned to a 4-byte boundary before the next field, and when it stands last, there is nothing to align and no need to do so.

In theory, everything is fine, and you can rearrange fields however you like. Let's check this with real data from one of the tables, whose daily section occupies about 10-15GB.

Original structure:

CREATE TABLE public.plan_20190220
(
-- Inherited from table plan:  pack uuid NOT NULL,
-- Inherited from table plan:  recno smallint NOT NULL,
-- Inherited from table plan:  host uuid,
-- Inherited from table plan:  ts timestamp with time zone,
-- Inherited from table plan:  exectime numeric(32,3),
-- Inherited from table plan:  duration numeric(32,3),
-- Inherited from table plan:  bufint bigint,
-- Inherited from table plan:  bufmem bigint,
-- Inherited from table plan:  bufdsk bigint,
-- Inherited from table plan:  apn uuid,
-- Inherited from table plan:  ptr uuid,
-- Inherited from table plan:  dt date,
  CONSTRAINT plan_20190220_pkey PRIMARY KEY (pack, recno),
  CONSTRAINT chck_ptr CHECK (ptr IS NOT NULL),
  CONSTRAINT plan_20190220_dt_check CHECK (dt = '2019-02-20'::date)
)
INHERITS (public.plan)

The section after changing the order of columns has exactly the same fields, just in a different order:

CREATE TABLE public.plan_20190221
(
-- Inherited from table plan:  dt date NOT NULL,
-- Inherited from table plan:  ts timestamp with time zone,
-- Inherited from table plan:  pack uuid NOT NULL,
-- Inherited from table plan:  recno smallint NOT NULL,
-- Inherited from table plan:  host uuid,
-- Inherited from table plan:  apn uuid,
-- Inherited from table plan:  ptr uuid,
-- Inherited from table plan:  bufint bigint,
-- Inherited from table plan:  bufmem bigint,
-- Inherited from table plan:  bufdsk bigint,
-- Inherited from table plan:  exectime numeric(32,3),
-- Inherited from table plan:  duration numeric(32,3),
  CONSTRAINT plan_20190221_pkey PRIMARY KEY (pack, recno),
  CONSTRAINT chck_ptr CHECK (ptr IS NOT NULL),
  CONSTRAINT plan_20190221_dt_check CHECK (dt = '2019-02-21'::date)
)
INHERITS (public.plan)

The total section size is determined by the number of 'facts' and depends only on external processes, so we will divide the heap size (pg_relation_size) on the number of records in it — so we will obtain the average size of an actual stored record:

Saving a penny on large volumes in PostgreSQL
Minus 6% of the volume, great!

But of course, it’s not all that rosy — because we can't change the order of fields in the indexes, and therefore 'overall' (pg_total_relation_size)…

Saving a penny on large volumes in PostgreSQL
… still here saved 1.5%, without changing a single line of code. Indeed!

Saving a penny on large volumes in PostgreSQL

I should note that the above arrangement of fields is not necessarily the most optimal. Because some blocks of fields we don't want to 'split' for aesthetic reasons — for instance, a pair (pack, recno), which serves as the PK for this table.

Overall, defining the 'minimum' arrangement of fields is a relatively simple 'brute force' task. Therefore, you may achieve results even better than ours with your own data — give it a try!

Source: habr.com

Buy reliable website hosting with DDoS protection, VPS VDS servers 🔥 Buy reliable website hosting with DDoS protection, VPS VDS servers | ProHoster