This is an automated email from the ASF dual-hosted git repository.
morningman pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris-website.git
The following commit(s) were added to refs/heads/master by this push:
new 5879146fe34 [doc](fluss) add Fluss Catalog doc (#4114)
5879146fe34 is described below
commit 5879146fe349d6c5af1e190db732c7d4cdc7848a
Author: Mingyu Chen (Rayner) <[email protected]>
AuthorDate: Thu Sep 3 21:26:05 2026 +0800
[doc](fluss) add Fluss Catalog doc (#4114)
Add the user doc for the Fluss Catalog (experimental, since 5.0.0,
apache/doris#66399) in English and Chinese, and add it to the Data
Catalogs sidebar right after Paimon Catalog.
The page covers catalog setup (`fluss.bootstrap.servers`, pass-through
`fluss.*` client properties, SASL), lake connection settings
(`fluss.lake.paimon.*` and how they map to Doris storage parameters),
the three ways to read a tiered lake table (`tbl`, `tbl$lake`,
`tbl$log`), the Union Read modes and their fallback cases, column type
mapping, partitioned tables, the `EXPLAIN` / Profile fields, query
performance (native reader vs. JNI), limitations and FAQ.
---
docs/lakehouse/catalogs/fluss-catalog.md | 482 +++++++++++++++++++++
.../current/lakehouse/catalogs/fluss-catalog.md | 482 +++++++++++++++++++++
sidebars.ts | 1 +
3 files changed, 965 insertions(+)
diff --git a/docs/lakehouse/catalogs/fluss-catalog.md
b/docs/lakehouse/catalogs/fluss-catalog.md
new file mode 100644
index 00000000000..e8123d42240
--- /dev/null
+++ b/docs/lakehouse/catalogs/fluss-catalog.md
@@ -0,0 +1,482 @@
+---
+{
+ "title": "Fluss Catalog",
+ "language": "en",
+ "description": "Query Apache Fluss tables from Doris: log and primary-key
tables, plus Union Read of Paimon-tiered data and Fluss logs.",
+ "keywords": [
+ "Fluss Catalog",
+ "Apache Fluss",
+ "query Fluss from Doris",
+ "Union Read",
+ "lake and log union read",
+ "tiered lake table",
+ "Fluss Paimon",
+ "Tiering Service",
+ "tbl$lake",
+ "tbl$log",
+ "fluss.union_read.mode",
+ "fluss.bootstrap.servers",
+ "fluss.lake.paimon",
+ "Fluss log table",
+ "Fluss primary-key table",
+ "Fluss type mapping",
+ "streaming storage",
+ "has no readable lake snapshot yet"
+ ]
+}
+---
+
+<!-- Knowledge Type: Capability Definition + Configuration Parameters +
Performance Tuning + Troubleshooting -->
+<!-- Use Case: Real-time queries on Fluss tables / Lake and log Union Read /
Data integration and federated queries -->
+
+Doris reads tables in [Apache Fluss](https://fluss.apache.org/) through a
Fluss Catalog. Fluss is a streaming storage system built for real-time
analytics, and its tiering service can keep writing table data into a Paimon
data lake. Doris can read the live data in Fluss directly. It can also merge
the history already tiered into Paimon with the newer rows still in the Fluss
log and query them as one table. That second way of reading is called Union
Read.
+
+:::note
+- This feature is experimental and has been available since version 5.0.0.
+- Only reads are supported. Writes and database or table management statements
are not.
+- For tiered lake tables, Paimon is the only supported lake format.
+:::
+
+## Use Cases
+
+| Scenario | Description |
+| --- | --- |
+| Real-time queries | Query the latest data in Fluss log tables and
primary-key tables directly. |
+| Union Read | For a table with lake tiering enabled, one query reads the
tiered history in the Paimon lake together with the rows in the Fluss log that
have not been tiered yet. This is the lake and log union read. |
+| Data integration | Read Fluss data into Doris internal tables, or join it
with internal tables and tables from other catalogs in federated queries. |
+| Writing back | Not supported. |
+
+## How It Works
+
+<!-- Knowledge Type: Architecture Principles -->
+
+Fluss tables come in three forms, and each is read differently:
+
+| Table type | How Doris reads it |
+| --- | --- |
+| Log table | Reads the Fluss log. Each bucket becomes one scan range. |
+| Primary-key table | Reads the latest KV snapshot of each bucket, applies the
change log written after the snapshot, and merges by primary key to get the
latest state. |
+| Tiered lake table (`table.datalake.enabled = true`) | Reads the tiered data
in the Paimon lake, applies the Fluss log after the tiering offset, and merges
both in one scan. This is Union Read. |
+
+The lake side is read by the Paimon Connector built into Doris; the Fluss
Catalog has no lake reader of its own. Fluss records a readable Paimon snapshot
for every tiered table, and Doris reads the Paimon data at that snapshot, so
the native ORC/Parquet readers and the file cache all apply.
+
+A Union Read on a tiered primary-key table merges the two sides by primary
key. During planning, the FE attaches the offset range of the bucket's log tail
to each lake split of that bucket. When the BE reads the lake data, it drops
rows that the log tail has since updated or deleted, and then emits the latest
state of the log tail itself once more as a separate scan range. Every row
comes out exactly once, and the result matches reading the whole table from
Fluss alone.
+
+## Configuring Catalog
+
+<!-- Knowledge Type: Configuration Parameters -->
+<!-- Use Case: Creating a Fluss Catalog / Configuring SASL authentication /
Configuring the Paimon lake connection -->
+
+### Syntax
+
+```sql
+CREATE CATALOG [IF NOT EXISTS] catalog_name PROPERTIES (
+ 'type' = 'fluss',
+ 'fluss.bootstrap.servers' = '<bootstrap_servers>',
+ {FlussProperties},
+ {LakeProperties},
+ {CommonProperties}
+);
+```
+
+* `<bootstrap_servers>`
+
+ Required. The bootstrap address of the Fluss cluster, a comma-separated
list of `host:port`. This is normally the CoordinatorServer address, for
example `'fluss.bootstrap.servers' = '10.0.0.1:9123,10.0.0.2:9123'`.
+
+* `{FlussProperties}`
+
+ The FlussProperties section holds the parameters specific to the Fluss
Catalog.
+
+ | Parameter | Required | Default | Description |
+ | --- | --- | --- | --- |
+ | `fluss.union_read.mode` | No | `auto` | Union Read mode for tiered lake
tables. One of `auto`, `required`, or `disabled`. See Union Read Modes. |
+ | `fluss.union_read.max_tail_rows` | No | `2000000` | For a Union Read on
a primary-key table, the maximum number of rows allowed in the log tail of a
single bucket. See Union Read Modes. |
+ | `fluss.union_read.max_total_tail_rows` | No | `20000000` | For a Union
Read on a primary-key table, the maximum total number of rows allowed across
the log tails of all buckets in one scan. Must not be smaller than
`fluss.union_read.max_tail_rows`. |
+ | `enable.mapping.varbinary` | No | `false` | Whether to map the Fluss
`BINARY`/`BYTES` types to Doris `varbinary`. By default they map to `string`. |
+ | `enable.mapping.timestamp_tz` | No | `false` | Whether to map the Fluss
`TIMESTAMP_LTZ` type to Doris `timestamptz`. By default it maps to `datetime`. |
+
+ Apart from the Doris-specific parameters above and `fluss.lake.paimon.*`,
every other property with the `fluss.` prefix is passed to the Fluss client as
is, with the prefix stripped. The FE and the BE share the same set. Doris does
not restrict the parameter names, so any configuration item the Fluss client
understands can be passed this way. For example, if the Fluss cluster has SASL
authentication turned on:
+
+ ```sql
+ 'fluss.client.security.protocol' = 'sasl',
+ 'fluss.client.security.sasl.mechanism' = 'PLAIN',
+ 'fluss.client.security.sasl.username' = '<username>',
+ 'fluss.client.security.sasl.password' = '<password>'
+ ```
+
+ See the [Fluss configuration
reference](https://fluss.apache.org/docs/maintenance/configuration/) and the
[Fluss authentication
guide](https://fluss.apache.org/docs/security/authentication/) for the full
list.
+
+* `{LakeProperties}`
+
+ The LakeProperties section holds the connection settings of the Paimon
lake behind tiered lake tables, with the prefix `fluss.lake.paimon.`. What
follows the prefix is written exactly like `datalake.paimon.*` in the Fluss
cluster's `server.yaml`, so the values can be copied over. See Lake Connection
Settings.
+
+* `{CommonProperties}`
+
+ The CommonProperties section holds the common properties. See the Common
Properties section in [Catalog Overview](../catalog-overview.md).
+
+### Examples
+
+The minimal configuration. It is enough when you only read Fluss's own data,
or when the storage behind the lake needs no credentials:
+
+```sql
+CREATE CATALOG fluss PROPERTIES (
+ 'type' = 'fluss',
+ 'fluss.bootstrap.servers' = '10.0.0.1:9123'
+);
+```
+
+The Paimon lake is on object storage (S3-compatible storage in this example):
+
+```sql
+CREATE CATALOG fluss_lake PROPERTIES (
+ 'type' = 'fluss',
+ 'fluss.bootstrap.servers' = '10.0.0.1:9123',
+ 'fluss.lake.paimon.s3.endpoint' = 'http://minio:9000',
+ 'fluss.lake.paimon.s3.access-key' = '<ak>',
+ 'fluss.lake.paimon.s3.secret-key' = '<sk>'
+);
+```
+
+The Fluss cluster has SASL authentication turned on:
+
+```sql
+CREATE CATALOG fluss_sasl PROPERTIES (
+ 'type' = 'fluss',
+ 'fluss.bootstrap.servers' = '10.0.0.1:9123',
+ 'fluss.client.security.protocol' = 'sasl',
+ 'fluss.client.security.sasl.mechanism' = 'PLAIN',
+ 'fluss.client.security.sasl.username' = '<username>',
+ 'fluss.client.security.sasl.password' = '<password>'
+);
+```
+
+### Deployment Requirements
+
+- The FE uses the Fluss client to read metadata and plan queries, and the BE
uses it to read data. Both must be able to reach the CoordinatorServer and the
TabletServer of the Fluss cluster.
+- The KV snapshots of primary-key tables and the archived log segments live in
the remote storage configured in Fluss (`remote.data.dir`). The BE reads those
files directly, so it must be able to reach that remote storage as well.
+- To read tiered lake tables, both the FE and the BE must be able to reach the
storage that holds the Paimon lake.
+- Lake data is read by the Paimon Connector plugin built into Doris. The
plugin ships with Doris and needs no separate installation.
+
+## Supported Fluss Versions
+
+<!-- Knowledge Type: Version Compatibility -->
+
+| Doris version | Fluss client version |
+| --- | --- |
+| 5.0 | 1.0 |
+
+## Metadata Mapping
+
+<!-- Knowledge Type: Behavior Rules -->
+
+The Fluss metadata hierarchy is Database -> Table, which maps one to one onto
Doris. There is no extra name mapping.
+
+For a table with lake tiering enabled, `SHOW TABLES` lists only the table
itself. `tbl$lake` and `tbl$log` are two ways of reading the same table and do
not appear as separate entries.
+
+`SHOW CREATE TABLE` lists the Fluss table properties. They show whether lake
tiering is enabled (`table.datalake.enabled`) and which lake format is used
(`table.datalake.format`).
+
+## Column Type Mapping
+
+<!-- Knowledge Type: Type Reference -->
+
+| Fluss Type | Doris Type | Comment |
+| --- | --- | --- |
+| BOOLEAN | boolean | |
+| TINYINT | tinyint | |
+| SMALLINT | smallint | |
+| INT | int | |
+| BIGINT | bigint | |
+| FLOAT | float | |
+| DOUBLE | double | |
+| DECIMAL(P, S) | decimal(P, S) | |
+| CHAR(N) | char(N) | Maps to string when N is greater than 255 |
+| STRING | string | |
+| BINARY(N) | string / varbinary(N) | Controlled by
`enable.mapping.varbinary`. The default `false` maps to `string`; `true` maps
to `varbinary` |
+| BYTES | string / varbinary | Same as above |
+| DATE | date | |
+| TIME | UNSUPPORTED | Doris has no type with the same semantics. The other
columns in the table can still be queried; only a query that touches this
column fails |
+| TIMESTAMP(P) | datetime(P) | Maps to datetime(6) when the precision is
greater than 6, which may lose precision |
+| TIMESTAMP_LTZ(P) | datetime(P) / timestamptz(P) | Precision greater than 6
is mapped to 6. Controlled by `enable.mapping.timestamp_tz`. The default
`false` maps to `datetime`; `true` maps to `timestamptz` |
+| ARRAY | array | |
+| MAP | map | |
+| ROW | struct | |
+
+> Note:
+>
+> A `TIMESTAMP_LTZ` column shows `WITH_TIMEZONE` in the Extra column of
`DESCRIBE`. When it is mapped to `datetime`, Doris converts the value to the
current session time zone (`SET time_zone = <tz>`) before returning it.
+>
+> For a table with lake tiering enabled, `tbl` and `tbl$lake` have the same
column types. The mapping above is aligned with the type conversion Fluss
applies when writing to Paimon and with the type mapping of the Paimon Catalog.
+
+## Partitioned Tables
+
+<!-- Knowledge Type: Behavior Rules + Limitations -->
+
+Partition columns are ordinary columns in Doris. `SHOW PARTITIONS` lists
partitions in the form `k1=v1/k2=v2`. Predicates on partition columns are used
for partition pruning, so fewer buckets are read.
+
+Fluss stores partition values only in the partition name, and characters that
are not allowed in a partition name (such as `.` and `:`) are replaced with
`_`. Doris can only read partition columns whose values survive that round trip
unchanged:
+
+- Supported: `CHAR`, `STRING`, `BOOLEAN`, `TINYINT`, `SMALLINT`, `INT`,
`BIGINT`, `DATE`, and `BINARY`/`BYTES` when `enable.mapping.varbinary` is off
(the partition value is the hex text of the bytes).
+- Not supported: `FLOAT`, `DOUBLE`, `TIME`, `TIMESTAMP`, `TIMESTAMP_LTZ`. Such
tables can still be described with `DESCRIBE`, but queries and `SHOW
PARTITIONS` fail, and the error message names the partition column that caused
it.
+
+## Query Operations
+
+<!-- Knowledge Type: Operational Examples + Behavior Rules -->
+<!-- Use Case: Querying Fluss tables / Reading only the lake or only the log /
Controlling the Union Read mode -->
+
+### Basic Query
+
+```sql
+-- Switch to the catalog, then query
+SWITCH fluss;
+USE db;
+SELECT * FROM tbl LIMIT 10;
+
+-- Use the fully qualified name
+SELECT * FROM fluss.db.tbl WHERE dt = '20260101';
+```
+
+A log table returns all log records up to the moment the query was planned.
Buckets in the same partition use offsets taken at the same instant, so one
query sees a consistent view. A primary-key table returns the latest state of
each key, and deleted keys do not appear in the result.
+
+### Tiered Lake Tables and Union Read
+
+For a table with `table.datalake.enabled = true`, Doris offers three ways to
read it. The default reads the lake and the log together, which is Union Read:
+
+```sql
+SELECT * FROM fluss.db.tbl; -- Union Read: lake + log merged into the
full data set (default)
+SELECT * FROM fluss.db.`tbl$lake`; -- Only the tiered data in the Paimon lake
+SELECT * FROM fluss.db.`tbl$log`; -- Only the data in the Fluss log that has
not been tiered yet
+```
+
+`tbl$lake` and `tbl$log` split the data by where it lives. They do not
overlap, and together they make up all of `tbl`. The names describe where the
data is, not how fresh it is: if the tiering service stops, `tbl$log` is still
whatever remains in the log.
+
+**`tbl$lake`**
+
+- The Paimon Connector reads the Paimon table written by the tiering service
directly. Besides the table's own columns, it exposes three system columns that
Fluss adds when writing to the lake: `__bucket`, `__offset`, and `__timestamp`.
+- If the tiering service has not committed any data to the lake yet, the query
fails with `nothing has been tiered`.
+- The time travel and incremental queries of the Paimon Catalog are not
supported.
+
+**`tbl$log`**
+
+- Starts reading from the log offset that corresponds to the lake snapshot and
returns only the data written after that offset.
+- Only log tables are supported. For a primary-key table, the log after the
lake snapshot is a change stream with updates and deletes of keys that already
exist in the lake, so it cannot be returned as a set of independent rows.
Querying `$log` on a primary-key table fails.
+- The table must already have a readable lake snapshot, otherwise the query
fails.
+- Tables without lake tiering have no `$lake` or `$log`.
+
+### Union Read Modes
+
+<!-- Knowledge Type: Configuration Parameters + Behavior Rules -->
+<!-- Use Case: Controlling whether Union Read happens / Finding out why auto
mode fell back to a Fluss-only read -->
+
+Whether a query on the tiered lake table itself (`SELECT * FROM tbl`) does a
Union Read is controlled by the catalog property `fluss.union_read.mode`:
+
+| Value | Behavior |
+| --- | --- |
+| `auto` (default) | Does a Union Read when a readable lake snapshot exists.
Falls back to reading only Fluss when the conditions are not met. |
+| `required` | Union Read is mandatory. Fails when the conditions are not met
instead of falling back. |
+| `disabled` | No Union Read. Reads only Fluss and ignores the data in the
lake. |
+
+The session variable `fluss_union_read_mode` overrides the catalog setting for
a single statement:
+
+```sql
+SET fluss_union_read_mode = 'required'; -- No fallback to a Fluss-only read;
fail instead
+SET fluss_union_read_mode = 'disabled'; -- Read from Fluss only
+SET fluss_union_read_mode = ''; -- Default: follow the catalog setting
+```
+
+The Union Read mode only decides which path the data is read through; it does
not change the result. As long as the log in Fluss is still complete, all three
modes return the same rows. `required` is useful for regression tests and
troubleshooting, because it fails when the read path is not what you expect
instead of quietly taking another path.
+
+**Cases where `auto` falls back to a Fluss-only read**
+
+| Case | Description |
+| --- | --- |
+| No readable lake snapshot | The tiering service has not committed anything
yet, so all data is still in the log. |
+| `key-type` | The primary-key columns of a primary-key table have types that
cannot be compared exactly between the lake side and the log side: `FLOAT`,
`DOUBLE`, `TIMESTAMP`/`TIMESTAMP_LTZ` with precision greater than 6, and
`TIME`. |
+| `partition-type` | A partition column of a primary-key table is not
`STRING`. Lake splits are matched to Fluss partitions by the text form of the
partition value, and only `STRING` guarantees that both sides write it the same
way. |
+| `tail-truncated` | Part of the log after the lake snapshot of a primary-key
table has already been deleted by the Fluss TTL, so the log tail cannot be read
in full. |
+| `tail-too-large` | The log tail of a primary-key table has more rows than
`fluss.union_read.max_tail_rows` or `fluss.union_read.max_total_tail_rows`
allows. |
+
+A primary-key table loses no data when it falls back to a Fluss-only read.
Fluss keeps the full state of a primary-key table; the query cannot use the
columnar files in the lake and runs slower. Log tables are different. A
Fluss-only read returns only what is still kept in the Fluss log, and once the
early log expires under the TTL, the result has fewer rows than a Union Read
would. `disabled` is not recommended for log tables that have been tiered.
+
+**Row limits for the log tail**
+
+For a Union Read on a primary-key table, the BE keeps the log tail of every
bucket in memory: one copy is the set of primary keys used to filter the lake
data, and one copy is the rows the tail itself has to emit. To bound memory
use, `fluss.union_read.max_tail_rows` limits the tail rows of a single bucket
(default 2 million rows), and `fluss.union_read.max_total_tail_rows` limits the
total tail rows across all buckets in one scan (default 20 million rows). Both
limits are checked during [...]
+
+**Checking the read path with EXPLAIN**
+
+The `flussScan` line in the `EXPLAIN` output shows how the query actually
reads the table:
+
+```text
+flussScan: readMode=default, unionRead=yes, lakeSplits=3,
suppressedLakeSplits=1, logRanges=0, pkRanges=0, pkTailRanges=1, mode=auto
+```
+
+| Field | Meaning |
+| --- | --- |
+| `readMode` | `default` reads the whole table; `log` reads `tbl$log`. |
+| `unionRead` | Whether a Union Read was done. |
+| `lakeSplits` | Number of lake splits. |
+| `suppressedLakeSplits` | Number of those lake splits that have to be
filtered by the log tail. Primary-key tables only. |
+| `logRanges` | Number of log scan ranges of a log table. |
+| `pkRanges` | Number of buckets of a primary-key table read entirely from
Fluss (KV snapshot + change log). |
+| `pkTailRanges` | Number of log tail scan ranges in a Union Read on a
primary-key table. |
+| `mode` | The Union Read mode in effect. Carries a `(session)` suffix when it
comes from the session variable. |
+| `degraded` | Appears only when `auto` mode has fallen back to a Fluss-only
read. The value is the reason from the table above. |
+
+### Lake Connection Settings
+
+<!-- Knowledge Type: Configuration Parameters -->
+<!-- Use Case: Paimon lake on object storage that needs credentials /
Overriding the lake settings reported by the Fluss cluster -->
+
+The Fluss cluster writes the `datalake.paimon.*` settings from its
`server.yaml` into the properties of every tiered table, and Doris builds the
Paimon lake connection from them. In most cases the catalog needs no lake
connection settings of its own.
+
+However, before Fluss returns table properties to a client, it removes every
configuration item whose name contains `key`, `secret`, or `password`. When the
Paimon lake sits on object storage that needs credentials, Doris cannot get
them from Fluss, so they must be supplied in the catalog with the
`fluss.lake.paimon.` prefix:
+
+```sql
+CREATE CATALOG fluss PROPERTIES (
+ 'type' = 'fluss',
+ 'fluss.bootstrap.servers' = '10.0.0.1:9123',
+ 'fluss.lake.paimon.s3.endpoint' = 'http://minio:9000',
+ 'fluss.lake.paimon.s3.access-key' = '<ak>',
+ 'fluss.lake.paimon.s3.secret-key' = '<sk>'
+);
+```
+
+After the prefix, use Paimon's own parameter names, the same as what follows
`datalake.paimon.` in the Fluss `server.yaml`. Copy them as they are:
+
+| Fluss server.yaml | Doris catalog property |
+| --- | --- |
+| `datalake.paimon.warehouse: s3://bucket/lake` |
`'fluss.lake.paimon.warehouse' = 's3://bucket/lake'` |
+| `datalake.paimon.metastore: filesystem` | `'fluss.lake.paimon.metastore' =
'filesystem'` |
+| `datalake.paimon.s3.endpoint: http://minio:9000` |
`'fluss.lake.paimon.s3.endpoint' = 'http://minio:9000'` |
+
+Overrides are applied per key, not as a whole. A key set in the catalog
overrides the same key reported by the Fluss cluster, and keys that are not set
still come from the cluster. If you only supply credentials, `warehouse`,
`metastore`, and the rest still come from the cluster.
+
+Storage parameters (credentials, endpoint, region, and addressing style) are
not handed to the Paimon Catalog. They are converted to Doris's own storage
parameter names and applied by the Doris storage layer. The reason is that
these settings serve both the FE (reading manifests) and the BE (reading data
files), and only the storage layer gives both sides the same set. The mapping
is as follows:
+
+| Paimon parameter | Doris storage parameter |
+| --- | --- |
+| `s3.access-key` / `s3.access.key` | `s3.access_key` |
+| `s3.secret-key` / `s3.secret.key` | `s3.secret_key` |
+| `s3.endpoint` | `s3.endpoint` |
+| `s3.region` | `s3.region` |
+| `s3.path-style-access` / `s3.path.style.access` | `use_path_style` |
+| `fs.oss.accessKeyId` | `oss.access_key` |
+| `fs.oss.accessKeySecret` | `oss.secret_key` |
+| `fs.oss.endpoint` | `oss.endpoint` |
+| `fs.obs.access.key` / `fs.obs.access-key` | `obs.access_key` |
+| `fs.obs.secret.key` / `fs.obs.secret-key` | `obs.secret_key` |
+| `fs.obs.endpoint` | `obs.endpoint` |
+
+`fluss.lake.paimon.metastore` accepts `filesystem` (default), `hive`, and
`rest`. Other parameters, such as the Hive Metastore address or the REST
authentication settings, are written the same way as in the [Paimon
Catalog](./paimon-catalog.mdx).
+
+`ALTER CATALOG` can only set properties; it cannot remove them. To withdraw a
`fluss.lake.paimon.*` override, set it to an empty string, and Doris goes back
to the configuration reported by the Fluss cluster:
+
+```sql
+ALTER CATALOG fluss SET PROPERTIES ('fluss.lake.paimon.warehouse' = '');
+```
+
+One Fluss Catalog reads lake tables with a single set of lake settings. After
the Fluss cluster changes `datalake.paimon.*`, run `REFRESH CATALOG` so that
Doris reloads it.
+
+### Query Profile
+
+<!-- Knowledge Type: Performance Diagnostics -->
+
+A single Fluss scan may read the Fluss log, the Paimon lake, and the log tail
that overrides lake data, and it has to filter as well. The profile therefore
reports time and row counts per read path and per range type, which shows where
the time goes:
+
+```text
+TableReader
+├── FlussLogReadTime 8s25ms Total time on the Fluss side (JNI)
+│ ├── FlussPkTailRangeNum 2 Only the range types actually read
in this scan are registered
+│ ├── FlussPkTailRangeReadTime 6s2ms
+│ └── FlussLogRowsReturned 2
+├── FlussLakeReadTime 8s25ms Total time on the Paimon side,
including tail reads and filtering
+│ ├── FlussLakeRangeNum 1
+│ ├── FlussLakeSuppressRangeNum 2
+│ └── FlussLakeRowsReturned 7 Rows after filtering; add
SuppressedRows to get the count before filtering
+├── FlussUnionTailReadTime 5s20ms One round trip to Fluss per bucket
+└── FlussUnionSuppressTime 16.1us Filtering per block, grows with
the number of lake rows
+```
+
+Subtracting the time of each range type from the total of its side gives the
cost of initializing that reader (JNI class loading, or setting up the Paimon
read stack). A query that only reads a log table has no lake or primary-key
counters in its profile.
+
+## Query Performance
+
+<!-- Knowledge Type: Architecture Principles + Performance Tuning -->
+<!-- Use Case: Performance tuning / Checking whether a query uses the native
reader or JNI -->
+
+The BE has two paths for reading Fluss tables, and their costs differ a lot:
+
+- C++ native reader. The BE reads the ORC/Parquet files in the Paimon lake
directly, with the same readers the Paimon Catalog uses. It does not go through
the JVM, and it can use the Doris [file cache](../data-cache.md).
+- JNI reader. The BE calls the Fluss Java SDK through JNI to read Fluss's own
data. Logs, KV snapshots, and change logs can only be read this way. Every
batch has to be converted from Java objects into Doris columnar blocks, which
is much slower than the native reader.
+
+Which path is used depends on the type of the scan range, which corresponds to
the counters on the `flussScan` line of `EXPLAIN`:
+
+| Data read | EXPLAIN counter | Read path |
+| --- | --- | --- |
+| Log of a log table | `logRanges` | JNI |
+| Whole primary-key table read (KV snapshot + change log) | `pkRanges` | JNI |
+| Log tail of a primary-key table in a Union Read | `pkTailRanges` | JNI |
+| Lake splits of a log table, in a Union Read or `tbl$lake` | `lakeSplits` |
C++ native reader |
+| Lake splits of a primary-key table, in a Union Read or `tbl$lake` |
`lakeSplits`, `suppressedLakeSplits` | Decided by the Paimon Catalog rules, see
below. Filtering lake rows by the primary keys in the log tail happens in C++
on the BE |
+
+Lake splits are planned by the Paimon Connector, and whether they use the
native reader follows the same rules as a plain Paimon table:
+
+- A log table is tiered into a Paimon append table. Every split reads files
directly, so all of them use the native reader.
+- A primary-key table is tiered into a Paimon primary-key table. Splits that
are already compacted and need no merging with other files use the native
reader. Files just written and not yet compacted, and splits whose files have
overlapping key ranges and need merge-on-read, use the Paimon JNI reader.
+- The session variable `force_jni_scanner = true` sends every lake split
through JNI. Use it only when troubleshooting.
+
+For a tiered table, Union Read lets the part in the lake (usually the vast
majority of the data) go through the native reader, and only the small amount
of log after the tiering offset goes through JNI. The more timely the tiering
(the smaller `table.datalake.freshness`), the less goes through JNI. That is
why tiered tables use Union Read by default. A few suggestions:
+
+- When analyzing history and freshness does not matter much, query `tbl$lake`
directly. The whole path is then the native reader.
+- `fluss.union_read.mode = disabled` sends the whole table through JNI. It is
not recommended except for troubleshooting.
+- When `auto` mode falls back to a Fluss-only read (`degraded=` appears in
`EXPLAIN`), the whole table also goes through JNI. For primary-key tables,
watch for the `key-type` and `partition-type` cases: they are determined by the
table schema, and only changing the table definition fixes them.
+- A Union Read on a primary-key table reads the primary keys in the log tail
once more for every bucket (`FlussUnionTailReadTime` in the profile). The
longer the tail, the higher the cost.
+
+To confirm which path a query took, compare `lakeSplits` with `logRanges`,
`pkRanges`, and `pkTailRanges` in `EXPLAIN`, and compare `FlussLakeReadTime`
with `FlussLogReadTime` in the profile.
+
+## Limitations
+
+<!-- Knowledge Type: Limitations -->
+
+- Read only. `INSERT`, `UPDATE`, and `DELETE` are not supported, nor are
database and table management statements such as `CREATE TABLE` and `DROP
TABLE`.
+- Paimon is the only supported lake format. For tables whose
`table.datalake.format` is something else, the lake data cannot be read.
+- The Fluss `TIME` type maps to `UNSUPPORTED`, and that column cannot be
queried.
+- Predicates are not pushed down to Fluss or to the Paimon lake. Apart from
partition pruning, Doris applies all filters after reading the data. Column
pruning and sub-column pruning of nested types are both supported.
+- Time travel and incremental queries are not supported, including on
`tbl$lake`.
+- Fluss provides only table-level row counts, without column-level statistics.
+
+## FAQ
+
+<!-- Knowledge Type: Troubleshooting -->
+<!-- Use Case: Missing Paimon plugin / Object storage credentials / Lake
snapshot not ready / Lake configuration changed / Unsupported partition column
type -->
+
+1. Querying a tiered lake table fails with `the paimon connector plugin is not
available`
+
+ Lake data is read by the Paimon Connector plugin. Check that the
`plugins/connector/paimon` directory under the FE deployment directory is
complete.
+
+2. Querying `tbl$lake` or doing a Union Read fails with an error about
accessing object storage
+
+ Fluss does not hand out storage credentials. Configure them in the catalog
with `fluss.lake.paimon.*`. See Lake Connection Settings.
+
+3. Error `has no readable lake snapshot yet`
+
+ The tiering service has not committed any data to the lake yet. In
`required` mode this is an error. Wait for the tiering service to commit, or
switch to `auto`.
+
+4. Error `is already serving lake tables with a different paimon configuration`
+
+ The `datalake.paimon.*` configuration of the Fluss cluster changed after
the catalog was created. Run `REFRESH CATALOG` to reload it.
+
+5. Error `cannot be read: its partition column 'xx' has fluss type
TIMESTAMP(3)`
+
+ Doris cannot read partition columns of that type. See Partitioned Tables.
+
+## Debugging
+
+<!-- Knowledge Type: Debugging Environment -->
+
+The `docker/thirdparties/docker-compose/fluss` directory in the Doris code
base contains the Fluss environment used by the regression tests, with Fluss,
Flink, the Paimon tiering service, and MinIO. Follow the README there to set
one up and verify the feature.
+
+## References
+
+- [Apache Fluss documentation](https://fluss.apache.org/docs/)
+- [Fluss Lakehouse
Storage](https://fluss.apache.org/docs/maintenance/tiered-storage/lakehouse-storage/)
+- [Paimon Catalog](./paimon-catalog.mdx)
diff --git
a/i18n/zh-CN/docusaurus-plugin-content-docs/current/lakehouse/catalogs/fluss-catalog.md
b/i18n/zh-CN/docusaurus-plugin-content-docs/current/lakehouse/catalogs/fluss-catalog.md
new file mode 100644
index 00000000000..7350ac64d37
--- /dev/null
+++
b/i18n/zh-CN/docusaurus-plugin-content-docs/current/lakehouse/catalogs/fluss-catalog.md
@@ -0,0 +1,482 @@
+---
+{
+ "title": "Fluss Catalog",
+ "language": "zh-CN",
+ "description": "如何在 Apache Doris 中查询 Apache Fluss 表:通过 Fluss Catalog
读取日志表、主键表,并对已分层到 Paimon 的表做 Union Read(湖与日志联合读取),含参数配置与类型映射。",
+ "keywords": [
+ "Fluss Catalog",
+ "Apache Fluss",
+ "Doris 查询 Fluss",
+ "Union Read",
+ "湖与日志联合读取",
+ "湖仓分层表",
+ "Fluss Paimon",
+ "Tiering Service",
+ "tbl$lake",
+ "tbl$log",
+ "fluss.union_read.mode",
+ "fluss.bootstrap.servers",
+ "fluss.lake.paimon",
+ "Fluss 日志表",
+ "Fluss 主键表",
+ "Fluss 类型映射",
+ "流存储",
+ "has no readable lake snapshot yet"
+ ]
+}
+---
+
+<!-- 知识类型: 能力定义 + 配置参数 + 性能调优 + 故障排查 -->
+<!-- 适用场景: 实时查询 Fluss 表 / 湖与日志 Union Read / 数据集成与联邦查询 -->
+
+Doris 可以通过 Fluss Catalog 读取 [Apache Fluss](https://fluss.apache.org/)
中的表。Fluss 是面向实时分析的流存储系统,表里的数据可以由分层服务(Tiering Service)持续写入 Paimon 数据湖。Doris
既能直接读 Fluss 里的实时数据,也能把 Paimon 里已分层的历史数据和 Fluss 日志里还没分层的增量数据合并成一张表来查,这种读法称为
Union Read。
+
+:::note
+- 该功能为实验功能,自 5.0.0 版本开始支持。
+- 当前仅支持读取 Fluss 表,不支持写入以及库表管理操作。
+- 湖仓分层表的湖端格式目前仅支持 Paimon。
+:::
+
+## 适用场景
+
+| 场景 | 说明 |
+| --- | --- |
+| 实时查询 | 直接查询 Fluss 日志表和主键表中的最新数据。 |
+| Union Read | 开启了湖仓分层的表,一次查询同时读 Paimon 湖里已分层的历史数据和 Fluss
日志里尚未分层的增量数据,也就是湖与日志的联合读取。 |
+| 数据集成 | 读取 Fluss 数据写入 Doris 内表,或与内表及其他 Catalog 中的表做联邦查询。 |
+| 数据写回 | 暂不支持。 |
+
+## 工作原理
+
+<!-- 知识类型: 架构原理 -->
+
+Fluss 的表有三种形态,读法各不相同:
+
+| 表类型 | 读取方式 |
+| --- | --- |
+| 日志表(Log Table) | 读取 Fluss 的日志,每个 Bucket 生成一个扫描范围。 |
+| 主键表(Primary-Key Table) | 读取每个 Bucket 最新的 KV 快照,再叠加快照之后的变更日志,按主键合并出最新状态。 |
+| 湖仓分层表(`table.datalake.enabled = true`) | 读取 Paimon 湖中已分层的数据,再叠加分层位点之后的 Fluss
日志,在一次扫描中合并,即 Union Read。 |
+
+湖端部分由 Doris 内置的 Paimon Connector 读取,Fluss Catalog 自己不实现。Fluss 会为每张分层表记录一个可读的
Paimon 快照,Doris 按这个快照读 Paimon 数据,原生的 ORC/Parquet 读取器和文件缓存都能直接用上。
+
+对分层的主键表做 Union Read 时,按主键合并两部分数据。FE 规划时给每个 Bucket 的湖端 Split 带上该 Bucket
日志尾部的位点范围;BE 读湖端数据时,丢掉已经被日志尾部更新或删除的行;日志尾部自己的最新状态再作为单独的扫描范围输出一次。每行数据只输出一次,结果和只从
Fluss 读整张表相同。
+
+## 配置 Catalog
+
+<!-- 知识类型: 配置参数 -->
+<!-- 适用场景: 创建 Fluss Catalog / 配置 SASL 认证 / 配置 Paimon 湖连接 -->
+
+### 语法
+
+```sql
+CREATE CATALOG [IF NOT EXISTS] catalog_name PROPERTIES (
+ 'type' = 'fluss',
+ 'fluss.bootstrap.servers' = '<bootstrap_servers>',
+ {FlussProperties},
+ {LakeProperties},
+ {CommonProperties}
+);
+```
+
+* `<bootstrap_servers>`
+
+ 必填。Fluss 集群的引导地址,逗号分隔的 `host:port` 列表,一般填 CoordinatorServer 的地址,如
`'fluss.bootstrap.servers' = '10.0.0.1:9123,10.0.0.2:9123'`。
+
+* `{FlussProperties}`
+
+ FlussProperties 部分用于填写 Fluss Catalog 特有的参数。
+
+ | 参数名 | 必填 | 默认值 | 说明 |
+ | --- | --- | --- | --- |
+ | `fluss.union_read.mode` | 否 | `auto` | 湖仓分层表的 Union Read 模式,可选
`auto`、`required`、`disabled`。详见【Union Read 模式】。 |
+ | `fluss.union_read.max_tail_rows` | 否 | `2000000` | 对主键表做 Union Read 时,单个
Bucket 的日志尾部允许包含的最大行数。详见【Union Read 模式】。 |
+ | `fluss.union_read.max_total_tail_rows` | 否 | `20000000` | 对主键表做 Union
Read 时,一次扫描涉及的所有 Bucket 的日志尾部允许包含的最大总行数。不能小于 `fluss.union_read.max_tail_rows`。 |
+ | `enable.mapping.varbinary` | 否 | `false` | 是否将 Fluss 的 `BINARY`/`BYTES`
类型映射为 Doris 的 `varbinary`。默认映射为 `string`。 |
+ | `enable.mapping.timestamp_tz` | 否 | `false` | 是否将 Fluss 的
`TIMESTAMP_LTZ` 类型映射为 Doris 的 `timestamptz`。默认映射为 `datetime`。 |
+
+ 除了上面这些 Doris 专用参数和 `fluss.lake.paimon.*`,其他 `fluss.` 前缀的属性都会去掉前缀后原样交给
Fluss 客户端,FE 和 BE 用同一套。Doris 不限制参数名,Fluss 客户端认识的配置项都能这样传。比如 Fluss 集群开了 SASL
认证,可以这样填:
+
+ ```sql
+ 'fluss.client.security.protocol' = 'sasl',
+ 'fluss.client.security.sasl.mechanism' = 'PLAIN',
+ 'fluss.client.security.sasl.username' = '<username>',
+ 'fluss.client.security.sasl.password' = '<password>'
+ ```
+
+ 参数列表见 [Fluss
配置文档](https://fluss.apache.org/docs/maintenance/configuration/) 和 [Fluss
认证文档](https://fluss.apache.org/docs/security/authentication/)。
+
+* `{LakeProperties}`
+
+ LakeProperties 部分填写湖仓分层表对应的 Paimon 湖的连接信息,前缀为
`fluss.lake.paimon.`。前缀后面的写法和 Fluss 集群 `server.yaml` 里 `datalake.paimon.*`
的写法一样,可以直接复制过来。详见【湖表连接配置】。
+
+* `{CommonProperties}`
+
+ CommonProperties 部分用于填写通用属性。请参阅[数据目录概述](../catalog-overview.md)中【通用属性】部分。
+
+### 示例
+
+最简配置。只读 Fluss 自身的数据,或者湖所在的存储不需要凭证时,这样就够了:
+
+```sql
+CREATE CATALOG fluss PROPERTIES (
+ 'type' = 'fluss',
+ 'fluss.bootstrap.servers' = '10.0.0.1:9123'
+);
+```
+
+Paimon 湖放在对象存储上(以 S3 兼容存储为例):
+
+```sql
+CREATE CATALOG fluss_lake PROPERTIES (
+ 'type' = 'fluss',
+ 'fluss.bootstrap.servers' = '10.0.0.1:9123',
+ 'fluss.lake.paimon.s3.endpoint' = 'http://minio:9000',
+ 'fluss.lake.paimon.s3.access-key' = '<ak>',
+ 'fluss.lake.paimon.s3.secret-key' = '<sk>'
+);
+```
+
+Fluss 集群开启了 SASL 认证:
+
+```sql
+CREATE CATALOG fluss_sasl PROPERTIES (
+ 'type' = 'fluss',
+ 'fluss.bootstrap.servers' = '10.0.0.1:9123',
+ 'fluss.client.security.protocol' = 'sasl',
+ 'fluss.client.security.sasl.mechanism' = 'PLAIN',
+ 'fluss.client.security.sasl.username' = '<username>',
+ 'fluss.client.security.sasl.password' = '<password>'
+);
+```
+
+### 部署要求
+
+- FE 用 Fluss 客户端读元数据、做查询规划,BE 用它读数据。两者都要能访问 Fluss 集群的 CoordinatorServer 和
TabletServer。
+- 主键表的 KV 快照和已归档的日志段放在 Fluss 配置的远端存储(`remote.data.dir`)里,BE
会直接读这些文件,因此也要能访问这个远端存储。
+- 读湖仓分层表时,FE 和 BE 都要能访问 Paimon 湖所在的存储。
+- 湖端数据由 Doris 内置的 Paimon Connector 插件读取,插件随 Doris 一起发布,不用单独安装。
+
+## 依赖的 Fluss 版本
+
+<!-- 知识类型: 版本兼容 -->
+
+| Doris 版本 | Fluss 客户端版本 |
+| --- | --- |
+| 5.0 | 1.0 |
+
+## 元数据映射
+
+<!-- 知识类型: 行为规则 -->
+
+Fluss 的元数据层级是 Database -> Table,和 Doris 一一对应,没有额外的命名映射。
+
+开启了湖仓分层的表,`SHOW TABLES` 只列出表本身。`tbl$lake` 和 `tbl$log` 是同一张表的两种读法,不会单独出现在列表里。
+
+`SHOW CREATE TABLE` 会列出 Fluss
表的属性,从中能看到表有没有开湖仓分层(`table.datalake.enabled`)和湖格式(`table.datalake.format`)。
+
+## 列类型映射
+
+<!-- 知识类型: 类型参考 -->
+
+| Fluss Type | Doris Type | Comment |
+| --- | --- | --- |
+| BOOLEAN | boolean | |
+| TINYINT | tinyint | |
+| SMALLINT | smallint | |
+| INT | int | |
+| BIGINT | bigint | |
+| FLOAT | float | |
+| DOUBLE | double | |
+| DECIMAL(P, S) | decimal(P, S) | |
+| CHAR(N) | char(N) | N 大于 255 时映射为 string |
+| STRING | string | |
+| BINARY(N) | string / varbinary(N) | 由 `enable.mapping.varbinary` 控制。默认为
`false`,映射为 `string`;为 `true` 时映射为 `varbinary` |
+| BYTES | string / varbinary | 同上 |
+| DATE | date | |
+| TIME | UNSUPPORTED | Doris 没有语义相同的类型。表里其他列照常可以查,只有查到这一列时报错 |
+| TIMESTAMP(P) | datetime(P) | 精度大于 6 时映射为 datetime(6),可能丢失精度 |
+| TIMESTAMP_LTZ(P) | datetime(P) / timestamptz(P) | 精度大于 6 时映射为精度 6。由
`enable.mapping.timestamp_tz` 控制。默认为 `false`,映射为 `datetime`;为 `true` 时映射为
`timestamptz` |
+| ARRAY | array | |
+| MAP | map | |
+| ROW | struct | |
+
+> 注:
+>
+> `TIMESTAMP_LTZ` 列在 `DESCRIBE` 语句的 Extra 列中会显示 `WITH_TIMEZONE`。映射为 `datetime`
时,Doris 会按当前会话时区(`SET time_zone = <tz>`)转换后返回。
+>
+> 开启了湖仓分层的表,`tbl` 和 `tbl$lake` 的列类型一致。上面的映射规则和 Fluss 写入 Paimon 时的类型转换、Paimon
Catalog 的类型映射是对齐的。
+
+## 分区表
+
+<!-- 知识类型: 行为规则 + 使用限制 -->
+
+分区列在 Doris 里就是普通列,`SHOW PARTITIONS` 以 `k1=v1/k2=v2`
的形式列出分区。查询时分区列上的谓词用于分区裁剪,少读一些 Bucket。
+
+Fluss 只在分区名里保存分区值,分区名不允许出现的字符(比如 `.`、`:`)会被替换成 `_`。只有分区值能原样写进分区名的类型,Doris 才能读:
+
+-
支持:`CHAR`、`STRING`、`BOOLEAN`、`TINYINT`、`SMALLINT`、`INT`、`BIGINT`、`DATE`,以及未开启
`enable.mapping.varbinary` 时的 `BINARY`/`BYTES`(分区值为字节的十六进制文本)。
+- 不支持:`FLOAT`、`DOUBLE`、`TIME`、`TIMESTAMP`、`TIMESTAMP_LTZ`。这类表可以
`DESCRIBE`,但查询和 `SHOW PARTITIONS` 会报错,错误信息里会指出是哪个分区列。
+
+## 查询操作
+
+<!-- 知识类型: 操作示例 + 行为规则 -->
+<!-- 适用场景: 查询 Fluss 表 / 只读湖或只读日志 / 控制 Union Read 模式 -->
+
+### 基础查询
+
+```sql
+-- 切换到 Catalog 后查询
+SWITCH fluss;
+USE db;
+SELECT * FROM tbl LIMIT 10;
+
+-- 使用全限定名
+SELECT * FROM fluss.db.tbl WHERE dt = '20260101';
+```
+
+日志表读到查询规划那一刻为止的全部日志,同一分区里的各个 Bucket
用同一时刻的位点,一次查询看到的是一致的视图。主键表返回每个主键的最新状态,删掉的主键不会出现在结果里。
+
+### 湖仓分层表与 Union Read
+
+对于 `table.datalake.enabled = true` 的表,Doris 提供三种读法。默认读法把湖和日志合在一起读,称为 Union
Read:
+
+```sql
+SELECT * FROM fluss.db.tbl; -- Union Read:湖 + 日志合并后的完整数据(默认)
+SELECT * FROM fluss.db.`tbl$lake`; -- 只读 Paimon 湖中已分层的数据
+SELECT * FROM fluss.db.`tbl$log`; -- 只读 Fluss 日志中尚未分层的数据
+```
+
+`tbl$lake` 和 `tbl$log` 按数据所在位置划分,互不重叠,合起来就是 `tbl`
的全部数据。名字说的是数据在哪里,不是数据有多新:分层服务停了,`tbl$log` 仍然是还留在日志里的那部分。
+
+**`tbl$lake`**
+
+- 由 Paimon Connector 直接读分层服务写出的 Paimon 表。除了表本身的列,还多出 Fluss
写湖时附加的三个系统列:`__bucket`、`__offset`、`__timestamp`。
+- 分层服务还没往湖里提交过数据时,查询报错 `nothing has been tiered`。
+- 不支持 Paimon Catalog 的时间旅行和增量查询。
+
+**`tbl$log`**
+
+- 从湖快照对应的日志位点开始读,只返回这个位点之后写入的数据。
+- 只支持日志表。主键表在湖快照之后的日志是一段变更流,里面有对湖中已有主键的更新和删除,没法作为一组独立的行返回,查主键表的 `$log` 会报错。
+- 表必须已经有可读的湖快照,否则报错。
+- 未开启湖仓分层的表没有 `$lake` 和 `$log`。
+
+### Union Read 模式
+
+<!-- 知识类型: 配置参数 + 行为规则 -->
+<!-- 适用场景: 控制是否 Union Read / 排查 auto 模式退回只读 Fluss 的原因 -->
+
+查询湖仓分层表本身(即 `SELECT * FROM tbl`)时是否做 Union Read,由 Catalog 属性
`fluss.union_read.mode` 决定:
+
+| 取值 | 行为 |
+| --- | --- |
+| `auto`(默认) | 有可读的湖快照时做 Union Read;条件不满足时退回为只读 Fluss。 |
+| `required` | 必须 Union Read,条件不满足时报错,不退回。 |
+| `disabled` | 不做 Union Read,只读 Fluss,不看湖里的数据。 |
+
+会话变量 `fluss_union_read_mode` 可以在单条语句范围内覆盖 Catalog 的设置:
+
+```sql
+SET fluss_union_read_mode = 'required'; -- 不允许退回为只读 Fluss,退回时直接报错
+SET fluss_union_read_mode = 'disabled'; -- 只从 Fluss 读取
+SET fluss_union_read_mode = ''; -- 默认值,跟随 Catalog 的设置
+```
+
+Union Read 模式只决定数据从哪条路径读出来,不改变结果:只要 Fluss 里的日志还完整,三种模式返回的行一样。`required`
适合回归测试和排查问题,读取路径不符合预期时直接报错,而不是换条路径把结果读出来。
+
+**`auto` 模式下退回为只读 Fluss 的情形**
+
+| 情形 | 说明 |
+| --- | --- |
+| 没有可读的湖快照 | 分层服务还没提交过数据,此时全部数据都在日志里。 |
+| `key-type` | 主键表的主键列类型在湖端和日志端无法精确比较:`FLOAT`、`DOUBLE`、精度大于 6 的
`TIMESTAMP`/`TIMESTAMP_LTZ`,以及 `TIME`。 |
+| `partition-type` | 主键表的分区列不是 `STRING`。湖端 Split 要靠分区值的文本形式匹配到 Fluss 分区,只有
`STRING` 能保证两边写法一致。 |
+| `tail-truncated` | 主键表湖快照之后的日志已经被 Fluss 按 TTL 删掉了一部分,日志尾部读不全。 |
+| `tail-too-large` | 主键表的日志尾部行数超过了 `fluss.union_read.max_tail_rows` 或
`fluss.union_read.max_total_tail_rows`。 |
+
+主键表退回为只读 Fluss 不丢数据:Fluss 保存着主键表的完整状态,只是用不上湖里的列存文件,读起来慢一些。日志表不同,只读 Fluss 拿到的是
Fluss 日志里还留着的数据,早期日志按 TTL 过期后,结果会比 Union Read 少。已经分层的日志表不建议用 `disabled`。
+
+**日志尾部行数上限**
+
+对主键表做 Union Read 时,BE 要把每个 Bucket
的日志尾部放在内存里:一份是用来过滤湖端数据的主键集合,一份是尾部自己要输出的行。为了限制内存占用,`fluss.union_read.max_tail_rows`
限制单个 Bucket 的尾部行数(默认 200 万行),`fluss.union_read.max_total_tail_rows` 限制一次扫描所有
Bucket 尾部的总行数(默认 2000 万行)。两个上限都在规划阶段按 Fluss 上报的位点检查。正常情况下,日志尾部只是一个
`table.datalake.freshness` 周期内写入的数据,离默认上限很远;碰到上限,多半是分层服务停了或者严重滞后。
+
+**通过 EXPLAIN 确认读取路径**
+
+`EXPLAIN` 输出中的 `flussScan` 行会显示本次查询实际的读取方式:
+
+```text
+flussScan: readMode=default, unionRead=yes, lakeSplits=3,
suppressedLakeSplits=1, logRanges=0, pkRanges=0, pkTailRanges=1, mode=auto
+```
+
+| 字段 | 含义 |
+| --- | --- |
+| `readMode` | `default` 表示读取整张表,`log` 表示读取的是 `tbl$log`。 |
+| `unionRead` | 是否做了 Union Read。 |
+| `lakeSplits` | 湖端 Split 数量。 |
+| `suppressedLakeSplits` | 其中需要按日志尾部过滤的湖端 Split 数量,仅主键表有。 |
+| `logRanges` | 日志表的日志扫描范围数量。 |
+| `pkRanges` | 主键表从 Fluss 整体读取(KV 快照 + 变更日志)的 Bucket 数量。 |
+| `pkTailRanges` | 主键表 Union Read 时,日志尾部的扫描范围数量。 |
+| `mode` | 生效的 Union Read 模式。来自会话变量时带 `(session)` 后缀。 |
+| `degraded` | 只在 `auto` 模式退回为只读 Fluss 时出现,值是上表里的退回原因。 |
+
+### 湖表连接配置
+
+<!-- 知识类型: 配置参数 -->
+<!-- 适用场景: Paimon 湖在需要凭证的对象存储上 / 覆盖 Fluss 集群上报的湖配置 -->
+
+Fluss 集群会把 `server.yaml` 里的 `datalake.paimon.*` 配置写进每张分层表的属性,Doris 据此构造 Paimon
湖的连接信息。多数情况下 Catalog 里不用再配湖的连接方式。
+
+不过 Fluss 在把表属性返回给客户端之前,会删掉所有名字里含 `key`、`secret` 或 `password` 的配置项。Paimon
湖放在需要凭证的对象存储上时,Doris 拿不到凭证,必须在 Catalog 里用 `fluss.lake.paimon.` 前缀补上:
+
+```sql
+CREATE CATALOG fluss PROPERTIES (
+ 'type' = 'fluss',
+ 'fluss.bootstrap.servers' = '10.0.0.1:9123',
+ 'fluss.lake.paimon.s3.endpoint' = 'http://minio:9000',
+ 'fluss.lake.paimon.s3.access-key' = '<ak>',
+ 'fluss.lake.paimon.s3.secret-key' = '<sk>'
+);
+```
+
+前缀后面用 Paimon 自己的参数写法,和 Fluss `server.yaml` 里 `datalake.paimon.` 后面的部分一样,直接复制即可:
+
+| Fluss server.yaml | Doris Catalog 属性 |
+| --- | --- |
+| `datalake.paimon.warehouse: s3://bucket/lake` |
`'fluss.lake.paimon.warehouse' = 's3://bucket/lake'` |
+| `datalake.paimon.metastore: filesystem` | `'fluss.lake.paimon.metastore' =
'filesystem'` |
+| `datalake.paimon.s3.endpoint: http://minio:9000` |
`'fluss.lake.paimon.s3.endpoint' = 'http://minio:9000'` |
+
+覆盖是按 Key 进行的,不是整体替换。Catalog 里指定的 Key 覆盖 Fluss
集群上报的同名配置,没指定的仍然用集群的。只填凭证时,`warehouse`、`metastore` 等仍然来自集群。
+
+其中存储类参数(凭证、Endpoint、Region、寻址方式)不会交给 Paimon Catalog,而是转成 Doris 自己的存储参数名,由
Doris 的存储层统一生效。原因是这些配置要同时给 FE(读 Manifest)和 BE(读数据文件)用,只有走存储层两边才是同一套。对应关系如下:
+
+| Paimon 参数写法 | 对应的 Doris 存储参数 |
+| --- | --- |
+| `s3.access-key` / `s3.access.key` | `s3.access_key` |
+| `s3.secret-key` / `s3.secret.key` | `s3.secret_key` |
+| `s3.endpoint` | `s3.endpoint` |
+| `s3.region` | `s3.region` |
+| `s3.path-style-access` / `s3.path.style.access` | `use_path_style` |
+| `fs.oss.accessKeyId` | `oss.access_key` |
+| `fs.oss.accessKeySecret` | `oss.secret_key` |
+| `fs.oss.endpoint` | `oss.endpoint` |
+| `fs.obs.access.key` / `fs.obs.access-key` | `obs.access_key` |
+| `fs.obs.secret.key` / `fs.obs.secret-key` | `obs.secret_key` |
+| `fs.obs.endpoint` | `obs.endpoint` |
+
+`fluss.lake.paimon.metastore` 支持 `filesystem`(默认)、`hive` 和 `rest`。其余参数,比如 Hive
Metastore 地址、REST 认证信息,按 [Paimon Catalog](./paimon-catalog.mdx) 的方式填写。
+
+`ALTER CATALOG` 只能设置属性,不能删除属性。要撤销某个 `fluss.lake.paimon.*` 覆盖,把它设成空字符串,Doris
会重新使用 Fluss 集群上报的配置:
+
+```sql
+ALTER CATALOG fluss SET PROPERTIES ('fluss.lake.paimon.warehouse' = '');
+```
+
+一个 Fluss Catalog 只按一套湖配置读湖表。Fluss 集群改了 `datalake.paimon.*` 之后,需要执行 `REFRESH
CATALOG` 让 Doris 重新加载。
+
+### 查询 Profile
+
+<!-- 知识类型: 性能诊断 -->
+
+一次 Fluss 扫描可能同时在读 Fluss 日志、Paimon 湖、覆盖湖端数据的日志尾部,还要做过滤,所以 Profile
按读取路径和范围类型分别统计耗时和行数,能看出时间花在哪一侧:
+
+```text
+TableReader
+├── FlussLogReadTime 8s25ms Fluss 侧(JNI)总耗时
+│ ├── FlussPkTailRangeNum 2 只注册本次扫描实际读取的范围类型
+│ ├── FlussPkTailRangeReadTime 6s2ms
+│ └── FlussLogRowsReturned 2
+├── FlussLakeReadTime 8s25ms Paimon 侧总耗时,包含尾部读取与过滤
+│ ├── FlussLakeRangeNum 1
+│ ├── FlussLakeSuppressRangeNum 2
+│ └── FlussLakeRowsReturned 7 过滤后的行数;加上 SuppressedRows 即为过滤前的行数
+├── FlussUnionTailReadTime 5s20ms 每个 Bucket 到 Fluss 的一次往返
+└── FlussUnionSuppressTime 16.1us 按 Block 过滤,随湖端行数增长
+```
+
+某一侧的总耗时减去它下面各范围类型的耗时,就是初始化这个读取器的开销(JNI 类加载,或 Paimon 读取栈的初始化)。只读日志表的查询,Profile
里不会出现湖和主键相关的计数器。
+
+## 查询性能
+
+<!-- 知识类型: 架构原理 + 性能调优 -->
+<!-- 适用场景: 性能调优 / 确认查询走 native reader 还是 JNI -->
+
+BE 读 Fluss 表有两条路径,开销差别很大:
+
+- C++ native reader。BE 直接读 Paimon 湖里的 ORC/Parquet 文件,和 Paimon Catalog
用的是同一套读取器,不经过 JVM,也能用上 Doris 的[文件缓存](../data-cache.md)。
+- JNI reader。BE 通过 JNI 调用 Fluss Java SDK 读 Fluss 自身的数据。日志、KV
快照、变更日志都只能这样读,每一批数据都要从 Java 对象转成 Doris 的列式 Block,比 native reader 慢得多。
+
+走哪条路径由扫描范围的类型决定,对应 `EXPLAIN` 中 `flussScan` 行的各个计数:
+
+| 读取内容 | EXPLAIN 计数 | 读取路径 |
+| --- | --- | --- |
+| 日志表的日志 | `logRanges` | JNI |
+| 主键表整体读取(KV 快照 + 变更日志) | `pkRanges` | JNI |
+| Union Read 中主键表的日志尾部 | `pkTailRanges` | JNI |
+| Union Read 或 `tbl$lake` 中,日志表的湖端 Split | `lakeSplits` | C++ native reader |
+| Union Read 或 `tbl$lake` 中,主键表的湖端 Split | `lakeSplits`、`suppressedLakeSplits`
| 按 Paimon Catalog 的规则决定,见下文。按日志尾部主键过滤湖端行的动作在 BE 的 C++ 里完成 |
+
+湖端 Split 由 Paimon Connector 规划,走不走 native reader 和查普通 Paimon 表时一样:
+
+- 日志表分层出来的是 Paimon Append 表,Split 都能直接读文件,全部走 native reader。
+- 主键表分层出来的是 Paimon 主键表。已经过 compaction、不需要和其他文件合并的 Split 走 native reader;刚写入还没
compaction 的文件,以及多个文件键范围重叠、需要 merge-on-read 的 Split,走 Paimon 的 JNI reader。
+- 会话变量 `force_jni_scanner = true` 会让湖端 Split 全部改走 JNI,只在排查问题时使用。
+
+对已分层的表,Union Read 让湖里的那部分(通常是绝大部分数据)走 native reader,只有分层位点之后的少量日志走
JNI。分层越及时(`table.datalake.freshness` 越小),走 JNI 的部分越少,这也是分层表默认采用 Union Read
的原因。几点建议:
+
+- 分析历史数据、对新鲜度要求不高时,直接查 `tbl$lake`,整条路径都是 native reader。
+- `fluss.union_read.mode = disabled` 会让整张表改走 JNI,除了排查问题不建议使用。
+- `auto` 模式退回为只读 Fluss 时(`EXPLAIN` 里出现 `degraded=`),整张表同样改走了 JNI。主键表要留意
`key-type` 和 `partition-type` 两种情况,它们由表结构决定,得调整建表才能解决。
+- 主键表的 Union Read 要为每个 Bucket 额外读一次日志尾部的主键(Profile 里的
`FlussUnionTailReadTime`),尾部越长开销越大。
+
+要确认一条查询实际走了哪条路径,看 `EXPLAIN` 里 `lakeSplits` 与
`logRanges`、`pkRanges`、`pkTailRanges` 的比例,以及 Profile 里 `FlussLakeReadTime` 与
`FlussLogReadTime` 的耗时。
+
+## 使用限制
+
+<!-- 知识类型: 使用限制 -->
+
+- 只读。不支持 `INSERT`、`UPDATE`、`DELETE`,也不支持 `CREATE TABLE`、`DROP TABLE` 这类库表管理操作。
+- 湖格式只支持 Paimon。`table.datalake.format` 是其他格式的表,读不了湖端数据。
+- Fluss 的 `TIME` 类型映射为 `UNSUPPORTED`,这一列查不了。
+- 谓词不下推到 Fluss 和 Paimon 湖端,除分区裁剪外,过滤都由 Doris 读到数据后再做。列裁剪和嵌套类型的子列裁剪都支持。
+- 不支持时间旅行和增量查询,`tbl$lake` 也不支持。
+- Fluss 只提供表级行数,没有列级统计信息。
+
+## 常见问题
+
+<!-- 知识类型: 故障排查 -->
+<!-- 适用场景: Paimon 插件缺失 / 对象存储凭证 / 湖快照未就绪 / 湖配置变更 / 分区列类型不支持 -->
+
+1. 查询湖仓分层表时报错 `the paimon connector plugin is not available`
+
+ 湖端数据靠 Paimon Connector 插件读取。检查 FE 部署目录下的 `plugins/connector/paimon` 目录是否完整。
+
+2. 查询 `tbl$lake` 或 Union Read 时报错,提示无法访问对象存储
+
+ Fluss 不会下发存储凭证,需要在 Catalog 里用 `fluss.lake.paimon.*` 配上。参见【湖表连接配置】。
+
+3. 报错 `has no readable lake snapshot yet`
+
+ 分层服务还没往湖里提交过数据。`required` 模式下这是错误,可以等分层服务提交,或者改用 `auto`。
+
+4. 报错 `is already serving lake tables with a different paimon configuration`
+
+ Catalog 创建之后,Fluss 集群的 `datalake.paimon.*` 配置变了。执行 `REFRESH CATALOG` 重新加载。
+
+5. 报错 `cannot be read: its partition column 'xx' has fluss type TIMESTAMP(3)`
+
+ 分区列的类型 Doris 读不了。参见【分区表】。
+
+## 功能调试
+
+<!-- 知识类型: 调试环境 -->
+
+Doris 代码库的 `docker/thirdparties/docker-compose/fluss` 目录有一套回归测试用的 Fluss 环境,包含
Fluss、Flink、Paimon 分层服务和 MinIO,可以照着里面的 README 搭一套来验证功能。
+
+## 参考资料
+
+- [Apache Fluss 官方文档](https://fluss.apache.org/docs/)
+- [Fluss Lakehouse
Storage](https://fluss.apache.org/docs/maintenance/tiered-storage/lakehouse-storage/)
+- [Paimon Catalog](./paimon-catalog.mdx)
diff --git a/sidebars.ts b/sidebars.ts
index d61d0187c0a..4d605d60526 100644
--- a/sidebars.ts
+++ b/sidebars.ts
@@ -670,6 +670,7 @@ const sidebars: SidebarsConfig = {
'lakehouse/best-practices/doris-dlf-paimon',
],
},
+ 'lakehouse/catalogs/fluss-catalog',
{
type: 'category',
label: 'Hudi Catalog',
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]