Compacting Hive ACID ORC Tables
How to clean up the delta directories that pile up in Hive ACID (transactional) ORC tables with major compaction, rebalance compaction and Hive INSERT OVERWRITE — including controlling the resulting file count.
A Hive ACID (transactional) ORC table creates a new delta_* directory under the partition for every INSERT, UPDATE or DELETE. On frequently loaded tables a single partition can accumulate hundreds of deltas — that is where small files come from in ACID tables.
For non-ACID tables we rewrote partitions with PySpark through a staging area, but ACID tables must not be merged with Spark directly. This post covers compacting ACID tables with Hive's own features and controlling the resulting file count. The source document is ACID_COMPACTION.md in the DataDynamics/hive-orc-table-compaction repository.
Identifying an ACID table
Table properties
SHOW TBLPROPERTIES dw.sales('transactional'); -- true means ACID
SHOW TBLPROPERTIES dw.sales('transactional_properties');transactional_properties | Meaning |
|---|---|
absent or default | Full ACID (INSERT / UPDATE / DELETE / MERGE) |
insert_only | Insert-only ACID (MM table) |
On HDP 3.x / CDP, managed tables are created as ACID by default, so a table can be ACID even if nobody asked for it.
HDFS layout
If the partition path contains directories like these, it is an ACID table:
/warehouse/tablespace/managed/hive/dw.db/sales/dt=2026-10-01/
├── base_0000123/
│ ├── bucket_00000
│ ├── bucket_00001
│ └── ...
├── delta_0000124_0000124_0000/
│ └── bucket_00000
├── delta_0000125_0000125_0000/
│ └── bucket_00000
└── delete_delta_0000126_0000126_0000/
└── bucket_00000| Directory / file | Meaning |
|---|---|
base_N | Result of a major compaction or INSERT OVERWRITE |
delta_N_M_S | Changes from each INSERT / UPDATE |
delete_delta_N_M_S | Rows deleted by UPDATE / DELETE |
bucket_NNNNN | ACID data file; used even on tables without CLUSTERED BY |
Each repeated INSERT adds one more delta_*:
Why not merge with Spark
- Without the Hive Warehouse Connector (HWC), Spark cannot merge
base/delta/delete_deltaon read. Row counts and data may be wrong, or the read may fail outright. - An
INSERT OVERWRITEfrom Spark can leave Hive's transaction metadata (write IDs) out of sync with the actual files. - The PySpark program from the non-ACID post aborts when
transactional=truefor exactly this reason.
So ACID tables are compacted with Hive's own features.
Minor vs. major compaction
| Type | What it does | Result |
|---|---|---|
| Minor | Merges many deltas into one delta, and many delete_deltas into one delete_delta | delta_124_130 |
| Major | Rewrites base + all deltas + delete_deltas into a new base | base_130 |
For small-file cleanup, use major compaction.
Running a major compaction
Run in beeline:
-- 1. Confirm the table is ACID
SHOW TBLPROPERTIES dw.sales('transactional');
-- 2. Request a major compaction per partition
ALTER TABLE dw.sales
PARTITION (dt='2026-10-01')
COMPACT 'major';
-- 3. Check progress
SHOW COMPACTIONS;ALTER TABLE ... COMPACT only enqueues the request and returns immediately. Append AND WAIT to block until it finishes — handy in batch scripts before moving on to validation.
ALTER TABLE dw.sales
PARTITION (dt='2026-10-01')
COMPACT 'major' AND WAIT;SHOW COMPACTIONS moves through these states:
| State | Meaning |
|---|---|
initiated | Queued |
working | A compactor worker is processing it |
ready for cleaning | New base written; old directories awaiting deletion |
succeeded | Cleaner removed the old directories |
failed | Failed; check Metastore / HiveServer2 logs |
The cleaner deletes old base / delta directories only after every query reading them has finished, so they may linger in HDFS for a while after compaction.
If a compaction is stuck in initiated, check these Hive Metastore settings:
| Setting | Required value |
|---|---|
hive.compactor.initiator.on | true |
hive.compactor.worker.threads | 1 or more |
Controlling file count and size
This is the most commonly misunderstood part of ACID compaction: COMPACT 'major' has no option for the resulting file count or size. After a major compaction, the number of files in base_N is determined as follows:
| Table type | Files after major compaction |
|---|---|
Bucketed ACID (CLUSTERED BY ... INTO N BUCKETS) | Fixed at N (one per bucket) |
| Non-bucketed ACID | Number of distinct bucket IDs in the input (set by the writer task count at initial load) |
ORC settings such as orc.stripe.size only affect stripe size inside a file, not the file count or size. To control the file count directly, use one of two methods:
Method 1: Rebalance compaction (Hive 4.x)
Since Hive 4.0, non-bucketed full ACID tables can be compacted into a specified number of buckets (files):
ALTER TABLE dw.sales
PARTITION (dt='2026-10-01')
COMPACT 'rebalance'
CLUSTERED INTO 8 BUCKETS;- Query-based compaction must be enabled.
- Not available for bucketed or insert-only tables.
- Hive 3.x / HDP 3.x do not have this syntax; on CDP, support depends on the release.
- Check your Hive version with
SELECT version();and confirm the syntax in your distribution's documentation first.
Method 2: Hive INSERT OVERWRITE (recommended, any version)
Rewrite the same partition from Hive (beeline). The output file count equals the reducer count, so you get exact control.
Compute the target file count from the partition size:
Target Files = ceil(Partition Size / Target File Size)
e.g. 4 GB / 512 MB = 4 × 1024 / 512 = 8hdfs dfs -du -s -h /warehouse/tablespace/managed/hive/dw.db/sales/dt=2026-10-01Fixed file count
SET hive.tez.auto.reducer.parallelism=false;
SET mapreduce.job.reduces=8;
INSERT OVERWRITE TABLE dw.sales
PARTITION (dt='2026-10-01')
SELECT
id,
customer_id,
amount
FROM dw.sales
WHERE dt='2026-10-01'
DISTRIBUTE BY pmod(hash(id), 8);| Setting | Role |
|---|---|
hive.tez.auto.reducer.parallelism=false | Stops Tez from automatically shrinking the reducer count |
mapreduce.job.reduces=8 | Reducer count = output file count |
DISTRIBUTE BY pmod(hash(id), 8) | Spreads rows evenly over the 8 reducers |
Do not use
DISTRIBUTE BY rand(). When a task is retried, the same row can land on a different reducer and be duplicated or lost. Use a deterministic, evenly distributed column (e.g. the PK) as the key.
Size-based
SET hive.exec.reducers.bytes.per.reducer=536870912; -- 512 MBThis estimates the reducer count from input size, so output file sizes are approximate. Use the fixed-count method when you need an exact number.
Caveats
- ACID tables support snapshot isolation, so you can overwrite a partition while reading from it — unlike non-ACID tables, which needed a staging area. A new
base_Nis produced and the cleaner removes the old deltas. - The partition holds an exclusive lock while this runs, so only target fully loaded partitions (typically D-1 or older).
- The
SELECTlist must contain every non-partition column in definition order.
Row count validation
Compare counts before and after the compaction or INSERT OVERWRITE. With hive.compute.query.using.stats=true, COUNT(*) may be answered from Metastore statistics instead of the data, so turn it off for validation:
SET hive.compute.query.using.stats=false;
-- Before
SELECT COUNT(*) FROM dw.sales WHERE dt='2026-10-01';
-- Run compaction / INSERT OVERWRITE
-- After
SELECT COUNT(*) FROM dw.sales WHERE dt='2026-10-01';Check the resulting file count in HDFS:
hdfs dfs -ls -R /warehouse/tablespace/managed/hive/dw.db/sales/dt=2026-10-01Automatic compaction thresholds
The Hive Metastore initiator requests compactions automatically based on these settings. They decide when a compaction starts, not the resulting file size.
| Setting | Meaning | Default |
|---|---|---|
hive.compactor.delta.num.threshold | Minor compaction when the number of delta directories exceeds this | 10 |
hive.compactor.delta.pct.threshold | Major compaction when delta size relative to base exceeds this ratio | 0.1 |
hive.compactor.check.interval | How often the initiator looks for candidates | 300s |
To disable automatic compaction for a single table — useful when you manage its file count yourself via INSERT OVERWRITE:
ALTER TABLE dw.sales SET TBLPROPERTIES ('no_auto_compaction'='true');Preventing small files
Changing how you load data is more effective than compacting after the fact:
- Batch small
INSERTs together and load in as few statements as possible. - Avoid repeated
INSERT INTO ... VALUES; each run creates anotherdelta. - Run major compaction regularly on partitions that have finished loading.
Operational flow
Recommended settings
| Item | Recommendation |
|---|---|
| Compaction unit | Hive partition |
| Default method | COMPACT 'major' |
| File count control | Hive INSERT OVERWRITE + fixed reducer count (rebalance compaction on Hive 4.x) |
| Target file size | 256 MB – 512 MB |
| Targets | Fully loaded partitions (D-1 or older) |
| Validation | Before/after COUNT(*) (hive.compute.query.using.stats=false) |
| Direct Spark merge | Never |
Summary
| Non-ACID | ACID | |
|---|---|---|
| Source of small files | Small ORC files from repeated INSERTs | delta_* from repeated INSERT / UPDATE / DELETE |
| Hive compactor | Does not run | Automatic (initiator) or on COMPACT request |
| Recommended method | PySpark + staging + INSERT OVERWRITE | COMPACT 'major' |
| File count control | coalesce to a target size | Hive INSERT OVERWRITE + reducer count, or rebalance (Hive 4.x) |
| Overwrite while reading same partition | No (staging required) | Yes (snapshot isolation) |
| Spark | OK | Never (without HWC) |
For ACID tables, COMPACT 'major' is the default; reach for Hive INSERT OVERWRITE or rebalance compaction only when you also need to pin the file count. Whichever you use, run it only on fully loaded partitions and validate with before/after COUNT(*) with stats-based answers disabled.