blob: 92aa01c8177c7ed2464195228bdde64cc2a3f00c [file]
.. Licensed to the Apache Software Foundation (ASF) under one
.. or more contributor license agreements. See the NOTICE file
.. distributed with this work for additional information
.. regarding copyright ownership. The ASF licenses this file
.. to you under the Apache License, Version 2.0 (the
.. "License"); you may not use this file except in compliance
.. with the License. You may obtain a copy of the License at
.. http://www.apache.org/licenses/LICENSE-2.0
.. Unless required by applicable law or agreed to in writing,
.. software distributed under the License is distributed on an
.. "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
.. KIND, either express or implied. See the License for the
.. specific language governing permissions and limitations
.. under the License.
Compaction
==========
Compaction is the process of merging multiple small data files into fewer, larger
files. It is a resource intensive procedure which consumes CPU time and disk IO,
so too frequent compaction may result in slower writes. However, without
compaction, the accumulation of small files degrades query performance. Tuning
compaction is therefore a trade-off between write throughput and read efficiency.
.. note::
- There can only be one job working on the same partition's compaction,
otherwise it will cause conflicts.
- Changelog is produced according to the ``changelog-producer`` table option
(``input``, ``full-compaction`` or ``lookup``): the ``full-compaction`` and
``lookup`` producers emit their changelog files from compaction, and the
files are committed with their snapshot.
- Automatic compaction is disabled when ``write-only`` is set to ``true``,
or when an append-only table uses unaware-bucket mode (``bucket = -1``).
- For a complete list of compaction-related configurations, see the
:ref:`Options API Reference <cpp-api-options>`.
Append-Only Table Compaction
----------------------------
In append-only table, data files are simply appended in sequence order.
Over time, many small files accumulate, which degrades read performance due to the
overhead of opening and scanning numerous files.
Append-only table compaction merges multiple small files into fewer, larger files
to improve read efficiency. The compaction is performed asynchronously and does
not block writes.
.. note::
Automatic append-only table compaction is only available for fixed-bucket
mode (``bucket > 0``). Writers of unaware-bucket tables (``bucket = -1``) do
not compact, and tables with blob columns also skip automatic compaction.
``AppendCompactCoordinator::Run`` explicitly compacts ``bucket = -1``
tables, but rejects data-evolution tables, which include every table with
blob columns, since its rewrite can merge neither their column layers nor
their blob files.
.. _data-evolution-deletion-vectors-compaction:
.. note::
A data-evolution table (``data-evolution.enabled = true``) may enable
``deletion-vectors.enabled``. Paimon C++ never compacts such a table: auto
compaction never runs on it, since data evolution requires ``bucket = -1``,
and ``AppendCompactCoordinator::Run``, the dedicated compaction entry point,
rejects it outright rather than dropping the deletes. This matches the
Paimon Java revision this support was ported from, which ends the compaction
scan as soon as deletion vectors are enabled; later Java releases do compact
such a table, and Paimon C++ has not caught up.
Reading such a table is supported; see
:ref:`data-evolution-deletion-vectors`.
Auto Compaction
~~~~~~~~~~~~~~~
During each flush, the writer triggers a best-effort auto compaction. The
compaction picker scans the file queue ordered by sequence number and selects a
contiguous window of files for merging when the number of candidate files reaches
the ``compaction.min.file-num`` threshold.
Full Compaction
~~~~~~~~~~~~~~~
Full compaction rewrites all eligible files in the bucket. During full
compaction:
- Files whose size is already at or above the compaction file-size threshold (and
have no associated deletion vectors) are skipped to avoid unnecessary rewrites.
- When deletion vectors are enabled, all files are always eligible for
compaction regardless of size, because deletion vectors must be applied.
- When ``compaction.force-rewrite-all-files`` is ``true``, all files are
rewritten unconditionally.
- Without deletion vectors, full compaction only proceeds when the number of
small files exceeds the number of large files and the total file count is at
least 3.
After compaction, if the last output file is still smaller than the compaction
file-size threshold, it is placed back into the compaction queue for future
merging.
The compaction file-size threshold is not a separate option. It is derived from
``target-file-size`` as ``target-file-size / 10 * 7``, that is 70% of the target
size, so that a file already close to the target is not compacted twice.
``target-file-size`` defaults to 256 MiB for append-only tables and 128 MiB for
primary-key tables, which makes the derived threshold about 179 MiB and 89 MiB
respectively. Adjust ``target-file-size`` to move the threshold.
Dictionary Passthrough
~~~~~~~~~~~~~~~~~~~~~~
An append-only compaction rewrite copies rows into the new file without
inspecting any value, so a Parquet column that an input file already stores
dictionary-encoded is forwarded to the writer still encoded instead of being
expanded to one copy of the value per row and re-encoded. This saves the reader
materializing the values and the writer hashing them again; how much that is
worth depends on the column, and low-cardinality ``STRING`` columns benefit most.
Primary-key compaction merges rows and is not covered.
This is **off by default** and is enabled per table by setting
``parquet.read.enable-dictionary-passthrough`` to ``true``. It is opt-in rather
than automatic because of the output file size trade-off described below, which
depends on the data and is not a win on every table. Measure before turning it
on, and compare output file size as well as compaction time.
Note that this is a *read* option, so it applies to every read of the table and
not only to the compaction rewrite. The values are unchanged, but an eligible
column reaches the consumer as an Arrow ``DictionaryArray`` rather than one value
per row, and a consumer that reads columns through its own accessors has to
unwrap it. Only a consumer that forwards batches without inspecting values -
which is what the rewrite does - gains anything from the encoding.
Once enabled, eligibility is decided per input file: a non-nested ``STRING``
column is forwarded when its data pages are dictionary-encoded throughout every
row group of *that* file, so one input file can be read encoded while the next
one is read as ordinary values, and the writer takes both. A high-cardinality
column that started dictionary-encoded and fell back to plain encoding therefore
does not qualify, even though it still carries a dictionary page. ``BINARY`` is
not forwarded although Parquet stores it in the same physical type and
dictionary-encodes it the same way; this option only selects ``STRING`` columns.
A file with a serialized Arrow dictionary schema can independently produce a
``BINARY`` dictionary, which the value accessors also support. The rewrite also
overrides the option back to ``false`` when the table writes a format other than Parquet, when
``parquet.enable.dictionary`` is ``false`` because the writer would only expand
the values again, or when variant/map shredding is configured because those
writers reshape each batch against a fixed physical schema. Setting the option
therefore never makes a rewrite fail; at worst it has no effect.
When the option is vetoed, the rewrite also enforces the veto on every input
batch. A format reader can independently hand over dictionary-encoded columns
because of its own lazy-decoding setting, so the rewrite decodes those columns
before handing them to a writer that cannot accept dictionary arrays.
If a file index is configured on a forwarded column, that column alone is
materialized so the index still sees its values; the other columns stay encoded.
Trade-off
^^^^^^^^^
Passthrough changes what the rewrite costs, not what it produces. The rewritten
data is identical either way, but the output file can be **larger**.
A Parquet column chunk can carry only one dictionary. The writer keeps the first
dictionary a column presents in a row group and falls back to plain encoding for
the rest of that row group as soon as a different one arrives. Compaction merges
several input files, each with its own dictionary, and output row groups are cut
by ``parquet.block.size`` and by the writer's memory limit, which are not aligned
to input file boundaries - a boundary may coincide, but nothing arranges for it.
So a rewrite can keep the first input file's dictionary and write the rest of the
row group plain, where materializing and rebuilding would have hashed the values
into a single dictionary for that row group (up to
``parquet.dictionary.page.size``, past which the writer falls back to plain in
either case). Where that happens the output column also becomes ineligible for
passthrough on the next compaction round, since its data pages are then only
partly dictionary-encoded.
How often it happens, and what it costs on a given table, is what the
measurement above is for.
Passthrough is therefore worth enabling when the reduction in compaction CPU
matters more than output size - for example when the same dictionary recurs
across input files, when the columns are wide enough that materializing them
dominates, or when the output is short-lived. Leave it off otherwise.
Append-Only Table Compaction Options
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. list-table::
:header-rows: 1
:widths: 30 10 10 10 40
* - Option
- Required
- Default
- Type
- Description
* - ``compaction.min.file-num``
- No
- 5
- Integer
- The minimum number of files to trigger an auto compaction for
append-only tables.
Primary Key Table Compaction
----------------------------
Primary key tables use an LSM tree (log-structured merge-tree) for file storage.
When more and more records are written, the number of sorted runs increases.
Because querying an LSM tree requires all sorted runs to be combined, too many
sorted runs will result in poor query performance, or even out of memory.
To limit the number of sorted runs, several sorted runs are merged into one big
sorted run once in a while. Paimon currently adopts a compaction strategy similar
to RocksDB's `universal compaction
<https://github.com/facebook/rocksdb/wiki/Universal-Compaction>`_.
Primary key table compaction solves:
- Reduce Level 0 files to avoid poor query performance.
- Produce deletion vectors for MOW mode.
Full Compaction
~~~~~~~~~~~~~~~
Paimon uses Universal Compaction. By default, when there is too much incremental
data, Full Compaction will be automatically performed. You don't usually have to
worry about it.
Paimon also provides configurations that allow for regular execution of Full
Compaction:
- ``compaction.optimization-interval``: Implying how often to perform an
optimization full compaction. This configuration is used to ensure the query
timeliness of the read-optimized system table.
- ``compaction.total-size-threshold``: Full compaction will be constantly triggered
when total size is smaller than this threshold.
- ``compaction.incremental-size-threshold``: Full compaction will be constantly
triggered when incremental size is bigger than this threshold.
Lookup Compaction
~~~~~~~~~~~~~~~~~
When a primary key table is configured with ``lookup`` changelog producer or
``first-row`` merge engine or has enabled deletion vectors for MOW mode, Paimon
will use a radical compaction strategy to force compacting level 0 files to
higher levels for every compaction trigger.
Paimon also provides configurations to optimize the frequency of this
compaction:
- ``lookup-compact``: compact mode used for lookup compaction. Possible values:
* ``radical``: will use ``ForceUpLevel0Compaction`` strategy to radically
compact new files.
* ``gentle``: will use ``UniversalCompaction`` strategy to gently compact new
files.
- ``lookup-compact.max-interval``: The max interval for a forced L0 lookup
compaction to be triggered in ``gentle`` mode. This option is only valid when
``lookup-compact`` mode is ``gentle``.
By configuring ``lookup-compact`` as ``gentle``, new files in L0 will not be
compacted immediately. This may greatly reduce the overall resource usage at the
expense of worse data freshness in certain cases.
Primary Key Table Compaction Options
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Number of Sorted Runs to Pause Writing
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
When the number of sorted runs is small, Paimon writers will perform compaction
asynchronously in separated threads, so records can be continuously written into
the table. However, to avoid unbounded growth of sorted runs, writers will pause
writing when the number of sorted runs hits the threshold.
.. list-table::
:header-rows: 1
:widths: 30 10 10 10 40
* - Option
- Required
- Default
- Type
- Description
* - ``num-sorted-run.stop-trigger``
- No
- (none)
- Integer
- The number of sorted runs that trigger the stopping of writes. The
default value is ``num-sorted-run.compaction-trigger + 3``.
Write stalls will become less frequent when ``num-sorted-run.stop-trigger``
becomes larger, thus improving writing performance. However, if this value
becomes too large, more memory and CPU time will be needed when querying the
table.
Number of Sorted Runs to Trigger Compaction
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Paimon uses LSM tree which supports a large number of updates. LSM organizes
files in several sorted runs. When querying records from an LSM tree, all sorted
runs must be combined to produce a complete view of all records.
One can easily see that too many sorted runs will result in poor query
performance. To keep the number of sorted runs in a reasonable range, Paimon
writers will automatically perform compactions. The following table property
determines the minimum number of sorted runs to trigger a compaction.
.. list-table::
:header-rows: 1
:widths: 30 10 10 10 40
* - Option
- Required
- Default
- Type
- Description
* - ``num-sorted-run.compaction-trigger``
- No
- 5
- Integer
- The sorted run number to trigger compaction. Includes level 0 files (one
file one sorted run) and high-level runs (one level one sorted run).
Compaction will become less frequent when ``num-sorted-run.compaction-trigger``
becomes larger, thus improving writing performance. However, if this value
becomes too large, more memory and CPU time will be needed when querying the
table. This is a trade-off between writing and query performance.