Skip to content

feat(compression): support per-column compression for non-cloud - #66169

Open
zwy991114 wants to merge 1 commit into
apache:masterfrom
zwy991114:feature/per-column-compression-codec
Open

feat(compression): support per-column compression for non-cloud#66169
zwy991114 wants to merge 1 commit into
apache:masterfrom
zwy991114:feature/per-column-compression-codec

Conversation

@zwy991114

Copy link
Copy Markdown

What problem does this PR solve?

Issue Number: close #xxx

Related PR: #xxx

Problem Summary:

Doris only supports a single compression codec configured at the table level (via PROPERTIES("compression"=...)), applied uniformly to every column. In real workloads, columns have very different data characteristics — highly redundant text columns (URLs, referers, search phrases) compress far better under a high-level ZSTD/LZ4HC codec, while numeric/low-cardinality columns gain little and pay the CPU cost. There was no way to tune the codec per column.

This PR adds per-column generic compression codec support for non-cloud (OLAP) tables. Users can specify a codec and optional level directly on a column:

CREATE TABLE t (
    k INT,
    url STRING COMPRESSION 'zstd:19',
    tag VARCHAR(64) COMPRESSION 'lz4hc:12'
)
DUPLICATE KEY(k)
DISTRIBUTED BY HASH(k) BUCKETS 1;

Compression ratio test on ClickBench hits:

Loaded the ClickBench hits dataset into two tables with identical schema. One table (baseline) uses the ZSTD level 3 default on every column; the other overrides five heavy text columns with COMPRESSION 'zstd:19'. After forcing full compaction on both, per-column sizes were read from information_schema.column_data_sizes.

Column Baseline(zstd:3) Per-column(zstd:19) Ratio improvement
OriginalURL 5.233 6.401 +22.3%
Referer 3.574 4.396 +23.0%
SearchPhrase 4.015 4.970 +23.8%
Title 4.676 5.690 +21.7%
URL 3.954 4.905 +24.1%

Release note

None

Check List (For Author)

  • Test

    • Regression test
    • Unit Test
    • Manual test (add detailed scripts or steps below)
    • No need to test or manual test. Explain why:
      • This is a refactor/code format and no logic has been changed.
      • Previous test can cover this change.
      • No code files have been changed.
      • Other reason
  • Behavior changed:

    • No.
    • Yes.
  • Does this need documentation?

    • No.
    • Yes.

Check List (For Reviewer who merge this PR)

  • Confirm the release note
  • Confirm test cases
  • Confirm document
  • Add branch pick label

@hello-stephen

Copy link
Copy Markdown
Contributor

Thank you for your contribution to Apache Doris.
Don't know what should be done next? See How to process your PR.

Please clearly describe your PR:

  1. What problem was fixed (it's best to include specific error reporting information). How it was fixed.
  2. Which behaviors were modified. What was the previous behavior, what is it now, why was it modified, and what possible impacts might there be.
  3. What features were added. Why was this function added?
  4. Which code was refactored and why was this part of the code refactored?
  5. Which functions were optimized and what is the difference before and after the optimization?

@zwy991114

Copy link
Copy Markdown
Author

run buildall

1 similar comment
@csun5285

Copy link
Copy Markdown
Contributor

run buildall

@csun5285

Copy link
Copy Markdown
Contributor

/review

@hello-stephen

Copy link
Copy Markdown
Contributor

Cloud UT Coverage Report

Increment line coverage 🎉

Increment coverage report
Complete coverage report

Category Coverage
Function Coverage 77.62% (1908/2458)
Line Coverage 64.46% (34145/52968)
Region Coverage 64.87% (17564/27076)
Branch Coverage 54.05% (9418/17424)

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes for seven P1 findings and one P2 finding. No additional user-provided review focus was supplied, so I reviewed the whole PR.

Critical checkpoint conclusions:

  • Goal and scope: the PR coherently adds a per-column codec and optional level across SQL, FE catalog/protocol, tablet schema, and both segment writers. The ordinary non-cloud scalar path is largely connected, but cloud/full schema change, compression-only MODIFY, light CCR SQL, complex AGG_STATE storage, codec lifetime/accounting, and test gaps mean the goal is not safely complete.
  • Size and focus: the cross-module scope is necessary for this feature and unrelated production changes were not found.
  • Concurrency and lifecycle: codec pools are mutex-protected, writer ownership keeps the observer pointer valid, and no new race, lock-order, or deadlock issue was found. The new owned-codec lifecycle does create the retained-workspace multiplier and allocation/free tracker mismatch called out inline.
  • Configuration and compatibility: no configuration item is added. Optional thrift/protobuf/footer fields remain readable by compatible readers; old BEs can ignore the write policy during a rolling upgrade and emit the table codec, which is storage-policy drift rather than unreadable data.
  • Parallel paths and conditions: legacy, vertical, row-binlog, schema-change, and compaction writers implement small-segment suppression > explicit override > table default when every physical schema node carries the policy. The cloud protobuf omission and complex AGG_STATE child/auxiliary metas are the exceptions. Compression in general Column.equals() also reaches routing/type guards that should not treat it as a logical change.
  • Tests and results: none of the changed tests observes requested-level application; the regression write is below the compression-suppression threshold, and the suite lacks the required generated result contract. Per the review-only task contract I did not run local builds/tests; this checkout also lacks .worktree_initialized, thirdparty/installed, and protoc. Live CI currently has compile, FE UT, macOS BE UT, style, license, and secret checks passing; BE UT is ERROR, Cloud UT is FAILURE, several regression/performance checks are pending, and the PR title checker is failing.
  • Observability and persistence: SHOW CREATE from the catalog column renders the policy, but reconstructed command SQL omits it. Local edit-log replay retains schema objects; the light CCR binlog executes the incomplete rawSql and loses the clause. Full cloud shadow-tablet protobuf construction separately drops the fields.
  • Data writes and transactions: no visible-version, transaction, delete-bitmap, or atomicity mechanism is changed. Segments remain readable because the actual codec is stored in the footer, but affected paths silently write a different policy from the DDL.
  • Performance: a private reusable native workspace per levelled column/open segment is an unbounded schema-width multiplier and is not included in segment buffer estimation.
  • Other: no security-sensitive behavior, new metric, or dynamic configuration requirement was introduced.

PR metadata also needs cleanup: the title does not match the required bracketed format (for example, [feature](compression) Support per-column compression for non-cloud); Issue Number and Related PR still contain #xxx; and this user-visible feature is marked both Release note: None and Behavior changed: No while documentation is checked without a link.

} catch (Exception e) {
throw new AnalysisException(e.getMessage(), e);
}
if (compressionType != null && !isOlap) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Preserve this setting on the cloud tablet-schema path (or reject it in cloud mode). isOlap is also true for cloud OlapTable, but CloudInternalCatalog.createTabletMetaBuilder() serializes columns through ColumnToProtobuf.toPb(), which never writes the new ColumnPB.compression_type/compression_level. A full cloud ADD/MODIFY job therefore builds its shadow tablet without the override and rewrites historical rows with the table codec; later direct loads can receive the thrift fields and use a different codec. Please update that protobuf producer and add cloud coverage, or explicitly gate cloud DDL if cloud support is out of scope.

&& Objects.equals(realDefaultValue, other.realDefaultValue)
&& clusterKeyId == other.clusterKeyId;
&& clusterKeyId == other.clusterKeyId
&& Objects.equals(compressionType, other.compressionType)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Do not let a compression-only delta become a logical/routing change. With these fields in general Column.equals(), SchemaChangeHandler.checkTable() now rejects ALTER ... MODIFY on range/list partition columns and hash-distribution columns because their full equality changed. For ordinary non-VARCHAR scalars, processModifyColumn() also sets typeChanged=true and falls into a full shadow-column rewrite even though the codec is per-segment metadata. Keep compression in no-op detection, but use routing/type-specific comparisons (or explicitly recognize a compression-only delta) for these guards and for light-schema-change classification; add partition/distribution and non-VARCHAR MODIFY tests.

}
switch (type) {
case segment_v2::CompressionTypePB::ZSTD: {
auto instance = std::make_unique<ZstdBlockCompression>(level);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Avoid a private context pool per levelled column. ScalarColumnWriter owns this instance, and after the first page its ZstdBlockCompression/Lz4HCBlockCompression retains a native context plus reusable buffer until the whole segment clears its column writers. Wide schemas (and multiple open segments) therefore retain one heavyweight workspace per levelled column even though compression is serial; the old singleton pool reused contexts according to actual concurrency, and this memory is not included in the segment-size estimate. Please share/reset pools by codec+level (or release rather than pool per-column contexts) and add a wide-schema tracker test.

break;
}
case segment_v2::CompressionTypePB::LZ4HC: {
auto instance = std::make_unique<Lz4HCBlockCompression>(level);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Balance native context allocation and destruction under the same tracker for these new short-lived instances. Both _acquire_compression_ctx() paths allocate the LZ4/ZSTD native context (and ZSTD later allocates workspace) under the current load/schema-change tracker, while the context/codec destructors switch to block_compression_mem_tracker() before freeing it. MemTrackerLimiter explicitly requires allocation and free to hit the same tracker; owned per-column codecs now repeat this mismatch at every segment teardown. Put creation/lazy allocation under the compression tracker as well, or keep the entire owned lifetime consistently task-local, and test tracker balance.

// into ColumnMetaPB, and the reader decompresses (which does not need the level).
//
// A tiny data_page_size forces many data pages so the codec is invoked once per
// page -- this is what guards the LZ4HC "first page uses the default level"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] This does not guard the claimed first-page level regression: data compressed at the default LZ4HC/ZSTD level decompresses identically, because the decompressor never needs the level. The test also writes compression_level into the in-memory meta itself, so that assertion is tautological, and the regression suite's three-row write is below the 256 KiB suppression threshold and exercises NO_COMPRESSION. Removing the level constructor/reset code would leave all changed tests green. Please add an observable first/reused-page level oracle and inspect an actual compressed segment footer/page.

// under the License.

suite("test_column_compression") {
def tableName = "test_column_compression_tbl"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Please convert this suite to the required Doris regression-test form before relying on it as feature coverage: hardcode the ordinary single-table name, express determined SHOW/ordered SELECT results with qt/order_qt and generate the matching .out via the runner, and remove the final DROP so failed state remains debuggable. The current direct assertions have no generated result artifact and conflict with the repository test contract.

}

/** Called by the parser to attach a COMPRESSION clause. */
public void setCompressionSpec(String spec) throws org.apache.doris.common.AnalysisException {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Include the compression clause in reconstructed column SQL. This setter stores the policy, but ColumnDefinition.toSql(String, boolean) stops after COMMENT, so CREATE/ADD/MODIFY reconstruction omits COMPRESSION 'algo[:level]'. Nereids ALTER passes that incomplete SQL into SchemaChangeHandler; the light schema-change binlog/CCR path executes TableAddOrDropColumnsInfo.rawSql, so an ADD loses the policy on the target, while full ALTER_JOB records also expose incomplete SQL metadata. Please render the clause and add CREATE/ADD/MODIFY toSql() plus light-CCR/binlog coverage.

// the bulk of the bytes live in child/sub-columns that never receive the override, so the
// requested codec would be silently dropped for that data. Reject it instead.
if (compressionType != null
&& (type.isArrayType() || type.isMapType() || type.isStructType()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Treat complex serialized AGG_STATE storage like the complex types rejected here. AggStateType fails all four checks, so DDL accepts AGG_STATE<ARRAY_AGG(INT)> COMPRESSION 'zstd:9' (and MAP_AGG). BE dispatches these states to ARRAY/MAP writers: their item/key/value children have no override and use the table codec, while offset/null metas copy the parent codec but omit its level. Both segment writers therefore store one logical state with inconsistent policy. Reject such AGG_STATE types, or propagate codec and level to every physical child/auxiliary meta, and add ARRAY_AGG/MAP_AGG footer coverage.

@hello-stephen

Copy link
Copy Markdown
Contributor

BE UT Coverage Report

Increment line coverage 97.87% (92/94) 🎉

Increment coverage report
Complete coverage report

Category Coverage
Function Coverage 58.18% (24679/42416)
Line Coverage 42.22% (246541/583967)
Region Coverage 38.10% (195874/514065)
Branch Coverage 39.24% (88499/225519)

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-H: Total hot run time: 29478 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpch-tools
Tpch sf100 test result on commit 99ea75a16520b861ae1f049ac39979b2a433463d, data reload: false

------ Round 1 ----------------------------------
============================================
q1	17687	4149	4163	4149
q2	2061	332	201	201
q3	10832	1458	819	819
q4	4794	473	340	340
q5	8433	845	554	554
q6	334	168	137	137
q7	813	826	610	610
q8	10498	1568	1595	1568
q9	5817	4342	4324	4324
q10	6763	1752	1447	1447
q11	511	351	330	330
q12	764	577	459	459
q13	18065	3391	2783	2783
q14	268	260	252	252
q15	q16	788	784	709	709
q17	1064	1003	1055	1003
q18	6833	5905	5545	5545
q19	1156	1201	1067	1067
q20	801	692	563	563
q21	5462	2576	2324	2324
q22	448	354	294	294
Total cold run time: 104192 ms
Total hot run time: 29478 ms

----- Round 2, with runtime_filter_mode=off -----
============================================
q1	4416	4308	4314	4308
q2	281	320	217	217
q3	4694	4968	4411	4411
q4	2051	2173	1352	1352
q5	4364	4239	4328	4239
q6	232	177	126	126
q7	2258	1895	1593	1593
q8	2494	2155	2155	2155
q9	7765	7624	7701	7624
q10	4758	4643	4196	4196
q11	755	428	393	393
q12	742	754	546	546
q13	3202	3617	2944	2944
q14	304	294	287	287
q15	q16	724	748	654	654
q17	1359	1370	1334	1334
q18	7928	7488	6956	6956
q19	1113	1080	1122	1080
q20	2233	2222	1933	1933
q21	5255	4552	4439	4439
q22	504	463	399	399
Total cold run time: 57432 ms
Total hot run time: 51186 ms

@hello-stephen

Copy link
Copy Markdown
Contributor

BE Regression && UT Coverage Report

Increment line coverage 97.87% (92/94) 🎉

Increment coverage report
Complete coverage report

Category Coverage
Function Coverage 75.41% (31191/41363)
Line Coverage 59.94% (347740/580136)
Region Coverage 56.63% (292121/515856)
Branch Coverage 57.95% (130727/225587)

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-DS: Total hot run time: 177512 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpcds-tools
TPC-DS sf100 test result on commit 99ea75a16520b861ae1f049ac39979b2a433463d, data reload: false

query5	4312	642	477	477
query6	457	222	220	220
query7	4867	588	356	356
query8	330	187	173	173
query9	8754	4034	4029	4029
query10	452	358	299	299
query11	5912	2559	2115	2115
query12	155	107	100	100
query13	1272	598	440	440
query14	6257	5200	4880	4880
query14_1	4249	4232	4225	4225
query15	216	198	181	181
query16	1010	482	481	481
query17	1129	712	591	591
query18	2445	485	365	365
query19	210	187	162	162
query20	116	111	108	108
query21	240	161	131	131
query22	13515	13584	13387	13387
query23	17407	16495	16116	16116
query23_1	16208	16194	16196	16194
query24	7610	1747	1268	1268
query24_1	1292	1255	1301	1255
query25	575	464	388	388
query26	1322	354	213	213
query27	2585	627	399	399
query28	4505	1985	1985	1985
query29	1089	632	509	509
query30	348	276	228	228
query31	1126	1099	970	970
query32	111	66	62	62
query33	524	327	265	265
query34	1211	1167	652	652
query35	780	779	658	658
query36	1042	1032	872	872
query37	161	110	94	94
query38	1870	1724	1688	1688
query39	885	905	845	845
query39_1	832	826	844	826
query40	288	165	148	148
query41	69	61	64	61
query42	94	99	94	94
query43	329	326	279	279
query44	1412	768	757	757
query45	196	189	177	177
query46	1059	1193	748	748
query47	2138	2111	1980	1980
query48	408	417	292	292
query49	579	420	311	311
query50	1152	443	346	346
query51	10660	10687	10723	10687
query52	90	87	78	78
query53	262	283	201	201
query54	297	226	216	216
query55	76	69	67	67
query56	308	285	315	285
query57	1310	1301	1188	1188
query58	290	263	252	252
query59	1609	1695	1479	1479
query60	312	285	268	268
query61	149	149	145	145
query62	535	496	431	431
query63	246	203	199	199
query64	2837	1026	883	883
query65	4731	4662	4602	4602
query66	1827	500	388	388
query67	28780	29209	29185	29185
query68	3112	1549	962	962
query69	408	295	267	267
query70	895	813	830	813
query71	374	332	339	332
query72	2997	2891	2412	2412
query73	831	781	438	438
query74	5058	4918	4690	4690
query75	2532	2502	2132	2132
query76	2369	1171	798	798
query77	339	381	281	281
query78	11945	12070	11294	11294
query79	1448	1183	758	758
query80	1279	549	459	459
query81	541	341	296	296
query82	669	155	119	119
query83	370	322	296	296
query84	287	157	129	129
query85	1000	609	538	538
query86	407	242	236	236
query87	1835	1813	1759	1759
query88	3677	2794	2823	2794
query89	428	367	336	336
query90	1932	199	190	190
query91	203	189	160	160
query92	64	62	57	57
query93	1800	1475	1024	1024
query94	733	357	321	321
query95	785	497	459	459
query96	1058	839	377	377
query97	2618	2645	2516	2516
query98	214	211	197	197
query99	1092	1095	963	963
Total cold run time: 263129 ms
Total hot run time: 177512 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
ClickBench: Total hot run time: 25.77 s
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/clickbench-tools
ClickBench test result on commit 99ea75a16520b861ae1f049ac39979b2a433463d, data reload: false

query1	0.01	0.01	0.00
query2	0.14	0.07	0.08
query3	0.37	0.25	0.25
query4	1.61	0.24	0.26
query5	0.35	0.31	0.32
query6	1.17	0.67	0.67
query7	0.05	0.00	0.00
query8	0.10	0.08	0.07
query9	0.50	0.38	0.40
query10	0.59	0.59	0.59
query11	0.31	0.17	0.18
query12	0.32	0.19	0.18
query13	0.52	0.53	0.52
query14	0.93	0.94	0.93
query15	0.68	0.59	0.60
query16	0.39	0.39	0.39
query17	1.02	0.99	1.01
query18	0.31	0.30	0.31
query19	1.93	1.82	1.86
query20	0.02	0.02	0.02
query21	15.41	0.38	0.32
query22	4.83	0.13	0.14
query23	15.86	0.50	0.30
query24	2.44	0.62	0.44
query25	0.16	0.11	0.09
query26	0.73	0.27	0.21
query27	0.11	0.10	0.10
query28	3.51	0.89	0.55
query29	12.48	4.24	3.37
query30	0.39	0.27	0.27
query31	2.77	0.63	0.32
query32	3.23	0.60	0.47
query33	2.96	3.01	3.03
query34	15.87	4.06	3.34
query35	3.27	3.24	3.26
query36	0.65	0.53	0.52
query37	0.12	0.10	0.09
query38	0.07	0.07	0.07
query39	0.07	0.06	0.06
query40	0.20	0.17	0.17
query41	0.13	0.08	0.08
query42	0.09	0.06	0.06
query43	0.08	0.08	0.07
Total cold run time: 96.75 s
Total hot run time: 25.77 s

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants