From 273b4ac34ab3796f5714683d736b11e40c0a0711 Mon Sep 17 00:00:00 2001 From: Aihua Xu Date: Tue, 19 May 2026 08:42:57 -0700 Subject: [PATCH] Docs: add versioned docs for 1.11.0 --- 1.11.0/README.md | 18 + 1.11.0/docs/api.md | 257 ++++ 1.11.0/docs/assets/images/audit-branch.png | Bin 0 -> 39065 bytes .../assets/images/historical-snapshot-tag.png | Bin 0 -> 17755 bytes .../iceberg-in-place-metadata-migration.png | Bin 0 -> 31816 bytes .../images/iceberg-migrateaction-step1.png | Bin 0 -> 16223 bytes .../images/iceberg-migrateaction-step2.png | Bin 0 -> 29566 bytes .../images/iceberg-migrateaction-step3.png | Bin 0 -> 19704 bytes .../images/iceberg-snapshotaction-step1.png | Bin 0 -> 39814 bytes .../images/iceberg-snapshotaction-step2.png | Bin 0 -> 42512 bytes .../images/partition-spec-evolution.png | Bin 0 -> 224020 bytes 1.11.0/docs/aws.md | 817 ++++++++++++ 1.11.0/docs/branching.md | 205 +++ 1.11.0/docs/catalog-properties.md | 167 +++ 1.11.0/docs/configuration.md | 145 +++ 1.11.0/docs/custom-catalog.md | 270 ++++ 1.11.0/docs/dell.md | 128 ++ 1.11.0/docs/delta-lake-migration.md | 119 ++ 1.11.0/docs/encryption.md | 153 +++ 1.11.0/docs/evolution.md | 100 ++ 1.11.0/docs/fileio.md | 41 + 1.11.0/docs/flink-configuration.md | 201 +++ 1.11.0/docs/flink-connector.md | 153 +++ 1.11.0/docs/flink-ddl.md | 249 ++++ 1.11.0/docs/flink-maintenance.md | 540 ++++++++ 1.11.0/docs/flink-queries.md | 559 +++++++++ 1.11.0/docs/flink-writes.md | 582 +++++++++ 1.11.0/docs/flink.md | 407 ++++++ 1.11.0/docs/hive-migration.md | 55 + 1.11.0/docs/hive.md | 799 ++++++++++++ 1.11.0/docs/index.md | 52 + 1.11.0/docs/java-api-quickstart.md | 296 +++++ 1.11.0/docs/jdbc.md | 69 + 1.11.0/docs/kafka-connect.md | 532 ++++++++ 1.11.0/docs/maintenance.md | 160 +++ 1.11.0/docs/metrics-reporting.md | 164 +++ 1.11.0/docs/nessie.md | 159 +++ 1.11.0/docs/partitioning.md | 94 ++ 1.11.0/docs/performance.md | 55 + 1.11.0/docs/reliability.md | 66 + 1.11.0/docs/schemas.md | 51 + 1.11.0/docs/spark-configuration.md | 283 +++++ 1.11.0/docs/spark-ddl.md | 730 +++++++++++ 1.11.0/docs/spark-getting-started.md | 208 ++++ 1.11.0/docs/spark-procedures.md | 1105 +++++++++++++++++ 1.11.0/docs/spark-queries.md | 622 ++++++++++ 1.11.0/docs/spark-structured-streaming.md | 151 +++ 1.11.0/docs/spark-writes.md | 465 +++++++ 1.11.0/docs/table-migration.md | 74 ++ 1.11.0/docs/view-configuration.md | 40 + 1.11.0/mkdocs.yml | 78 ++ 51 files changed, 11419 insertions(+) create mode 100644 1.11.0/README.md create mode 100644 1.11.0/docs/api.md create mode 100644 1.11.0/docs/assets/images/audit-branch.png create mode 100644 1.11.0/docs/assets/images/historical-snapshot-tag.png create mode 100644 1.11.0/docs/assets/images/iceberg-in-place-metadata-migration.png create mode 100644 1.11.0/docs/assets/images/iceberg-migrateaction-step1.png create mode 100644 1.11.0/docs/assets/images/iceberg-migrateaction-step2.png create mode 100644 1.11.0/docs/assets/images/iceberg-migrateaction-step3.png create mode 100644 1.11.0/docs/assets/images/iceberg-snapshotaction-step1.png create mode 100644 1.11.0/docs/assets/images/iceberg-snapshotaction-step2.png create mode 100644 1.11.0/docs/assets/images/partition-spec-evolution.png create mode 100644 1.11.0/docs/aws.md create mode 100644 1.11.0/docs/branching.md create mode 100644 1.11.0/docs/catalog-properties.md create mode 100644 1.11.0/docs/configuration.md create mode 100644 1.11.0/docs/custom-catalog.md create mode 100644 1.11.0/docs/dell.md create mode 100644 1.11.0/docs/delta-lake-migration.md create mode 100644 1.11.0/docs/encryption.md create mode 100644 1.11.0/docs/evolution.md create mode 100644 1.11.0/docs/fileio.md create mode 100644 1.11.0/docs/flink-configuration.md create mode 100644 1.11.0/docs/flink-connector.md create mode 100644 1.11.0/docs/flink-ddl.md create mode 100644 1.11.0/docs/flink-maintenance.md create mode 100644 1.11.0/docs/flink-queries.md create mode 100644 1.11.0/docs/flink-writes.md create mode 100644 1.11.0/docs/flink.md create mode 100644 1.11.0/docs/hive-migration.md create mode 100644 1.11.0/docs/hive.md create mode 100644 1.11.0/docs/index.md create mode 100644 1.11.0/docs/java-api-quickstart.md create mode 100644 1.11.0/docs/jdbc.md create mode 100644 1.11.0/docs/kafka-connect.md create mode 100644 1.11.0/docs/maintenance.md create mode 100644 1.11.0/docs/metrics-reporting.md create mode 100644 1.11.0/docs/nessie.md create mode 100644 1.11.0/docs/partitioning.md create mode 100644 1.11.0/docs/performance.md create mode 100644 1.11.0/docs/reliability.md create mode 100644 1.11.0/docs/schemas.md create mode 100644 1.11.0/docs/spark-configuration.md create mode 100644 1.11.0/docs/spark-ddl.md create mode 100644 1.11.0/docs/spark-getting-started.md create mode 100644 1.11.0/docs/spark-procedures.md create mode 100644 1.11.0/docs/spark-queries.md create mode 100644 1.11.0/docs/spark-structured-streaming.md create mode 100644 1.11.0/docs/spark-writes.md create mode 100644 1.11.0/docs/table-migration.md create mode 100644 1.11.0/docs/view-configuration.md create mode 100644 1.11.0/mkdocs.yml diff --git a/1.11.0/README.md b/1.11.0/README.md new file mode 100644 index 000000000000..3b7c46ade50a --- /dev/null +++ b/1.11.0/README.md @@ -0,0 +1,18 @@ + + +For information about the documentation, please see [this README.md](../site/README.md) diff --git a/1.11.0/docs/api.md b/1.11.0/docs/api.md new file mode 100644 index 000000000000..9157dabb903c --- /dev/null +++ b/1.11.0/docs/api.md @@ -0,0 +1,257 @@ +--- +title: "Java API" +--- + + +# Iceberg Java API + +## Tables + +The main purpose of the Iceberg API is to manage table metadata, like schema, partition spec, metadata, and data files that store table data. + +Table metadata and operations are accessed through the `Table` interface. This interface will return table information. + +### Table metadata + +The [`Table` interface](../../javadoc/{{ icebergVersion }}/org/apache/iceberg/Table.html) provides access to the table metadata: + +* `schema` returns the current table [schema](schemas.md) +* `spec` returns the current table partition spec +* `properties` returns a map of key-value [properties](configuration.md) +* `currentSnapshot` returns the current table snapshot +* `snapshots` returns all valid snapshots for the table +* `snapshot(id)` returns a specific snapshot by ID +* `location` returns the table's base location + +Tables also provide `refresh` to update the table to the latest version, and expose helpers: + +* `io` returns the `FileIO` used to read and write table files +* `locationProvider` returns a `LocationProvider` used to create paths for data and metadata files + +### Scanning + +#### File level + +Iceberg table scans start by creating a `TableScan` object with `newScan`. + +```java +TableScan scan = table.newScan(); +``` + +To configure a scan, call `filter` and `select` on the `TableScan` to get a new `TableScan` with those changes. + +```java +TableScan filteredScan = scan.filter(Expressions.equal("id", 5)) +``` + +Calls to configuration methods create a new `TableScan` so that each `TableScan` is immutable and won't change unexpectedly if shared across threads. + +When a scan is configured, `planFiles`, `planTasks`, and `schema` are used to return files, tasks, and the read projection. + +```java +TableScan scan = table.newScan() + .filter(Expressions.equal("id", 5)) + .select("id", "data"); + +Schema projection = scan.schema(); +Iterable tasks = scan.planTasks(); +``` + +Use `asOfTime` or `useSnapshot` to configure the table snapshot for time travel queries. + +#### Row level + +Iceberg table scans start by creating a `ScanBuilder` object with `IcebergGenerics.read`. + +```java +ScanBuilder scanBuilder = IcebergGenerics.read(table) +``` + +To configure a scan, call `where` and `select` on the `ScanBuilder` to get a new `ScanBuilder` with those changes. + +```java +scanBuilder.where(Expressions.equal("id", 5)) +``` + +When a scan is configured, call method `build` to execute scan. `build` return `CloseableIterable` + +```java +CloseableIterable result = IcebergGenerics.read(table) + .where(Expressions.lessThan("id", 5)) + .build(); +``` +where `Record` is Iceberg record for iceberg-data module `org.apache.iceberg.data.Record`. + +### Update operations + +`Table` also exposes operations that update the table. These operations use a builder pattern, [`PendingUpdate`](../../javadoc/{{ icebergVersion }}/org/apache/iceberg/PendingUpdate.html), that commits when `PendingUpdate#commit` is called. + +For example, updating the table schema is done by calling `updateSchema`, adding updates to the builder, and finally calling `commit` to commit the pending changes to the table: + +```java +table.updateSchema() + .addColumn("count", Types.LongType.get()) + .commit(); +``` + +Available operations to update a table are: + +* `updateSchema` -- update the table schema +* `updateSpec` -- modify a table's partition spec +* `updateStatistics` -- update statistics files of a table +* `updatePartitionStatistics` -- update statistics for a specific partition in table +* `updateProperties` -- update table properties +* `updateLocation` -- update the table's base location +* `expireSnapshots` -- used to remove old snapshots from table +* `manageSnapshots` -- used to manage table snapshots +* `newAppend` -- used to append data files +* `newFastAppend` -- used to append data files, will not compact metadata +* `newOverwrite` -- used to append data files and remove files that are overwritten +* `newDelete` -- used to delete data files +* `newRewrite` -- used to rewrite data files; will replace existing files with new versions +* `newRowDelta` -- used to remove or replace rows in existing data files +* `newTransaction` -- create a new table-level transaction +* `rewriteManifests` -- rewrite manifest data by clustering files, for faster scan planning +* `replaceSortOrder` -- for replacing table sort order with a newly created order +* `newReplacePartitions` -- used to dynamically overwrite partitions in the table with new data + +### Transactions + +Transactions are used to commit multiple table changes in a single atomic operation. A transaction is used to create individual operations using factory methods, like `newAppend`, just like working with a `Table`. Operations created by a transaction are committed as a group when `commitTransaction` is called. + +For example, deleting and appending a file in the same transaction: +```java +Transaction t = table.newTransaction(); + +// commit operations to the transaction +t.newDelete().deleteFromRowFilter(filter).commit(); +t.newAppend().appendFile(data).commit(); + +// commit all the changes to the table +t.commitTransaction(); +``` + +## Types + +Iceberg data types are located in the [`org.apache.iceberg.types` package](../../javadoc/{{ icebergVersion }}/org/apache/iceberg/types/package-summary.html). + +### Primitives + +Primitive type instances are available from static methods in each type class. Types without parameters use `get`, and types like `decimal` use factory methods: + +```java +Types.IntegerType.get() // int +Types.DoubleType.get() // double +Types.DecimalType.of(9, 2) // decimal(9, 2) +``` + +### Nested types + +Structs, maps, and lists are created using factory methods in type classes. + +Like struct fields, map keys or values and list elements are tracked as nested fields. Nested fields track [field IDs](evolution.md#correctness) and nullability. + +Struct fields are created using `NestedField.optional` or `NestedField.required`. Map value and list element nullability is set in the map and list factory methods. + +```java +// struct<1 id: int, 2 data: optional string> +StructType struct = Struct.of( + Types.NestedField.required(1, "id", Types.IntegerType.get()), + Types.NestedField.optional(2, "data", Types.StringType.get()) + ) +``` +```java +// map<1 key: int, 2 value: optional string> +MapType map = MapType.ofOptional( + 1, 2, + Types.IntegerType.get(), + Types.StringType.get() + ) +``` +```java +// array<1 element: int> +ListType list = ListType.ofRequired(1, IntegerType.get()); +``` + +## Expressions + +Iceberg's expressions are used to configure table scans. To create expressions, use the factory methods in [`Expressions`](../../javadoc/{{ icebergVersion }}/org/apache/iceberg/expressions/Expressions.html). + +Supported predicate expressions are: + +* `isNull` +* `notNull` +* `equal` +* `notEqual` +* `lessThan` +* `lessThanOrEqual` +* `greaterThan` +* `greaterThanOrEqual` +* `in` +* `notIn` +* `startsWith` +* `notStartsWith` + +Supported expression operations are: + +* `and` +* `or` +* `not` + +Constant expressions are: + +* `alwaysTrue` +* `alwaysFalse` + +### Expression binding + +When created, expressions are unbound. Before an expression is used, it will be bound to a data type to find the field ID the expression name represents, and to convert predicate literals. + +For example, before using the expression `lessThan("x", 10)`, Iceberg needs to determine which column `"x"` refers to and convert `10` to that column's data type. + +If the expression could be bound to the type `struct<1 x: long, 2 y: long>` or to `struct<11 x: int, 12 y: int>`. + +### Expression example + +```java +table.newScan() + .filter(Expressions.greaterThanOrEqual("x", 5)) + .filter(Expressions.lessThan("x", 10)) +``` + +## Modules + +Iceberg table support is organized in library modules: + +* `iceberg-common` contains utility classes used in other modules +* `iceberg-api` contains the public Iceberg API, including expressions, types, tables, and operations +* `iceberg-arrow` is an implementation of the Iceberg type system for reading and writing data stored in Iceberg tables using Apache Arrow as the in-memory data format +* `iceberg-aws` contains implementations of the Iceberg API to be used with tables stored on AWS S3 and/or for tables defined using the AWS Glue data catalog +* `iceberg-core` contains implementations of the Iceberg API and support for Avro data files, **this is what processing engines should depend on** +* `iceberg-parquet` is an optional module for working with tables backed by Parquet files +* `iceberg-orc` is an optional module for working with tables backed by ORC files (*experimental*) +* `iceberg-hive-metastore` is an implementation of Iceberg tables backed by the Hive metastore Thrift client + +This project Iceberg also has modules for adding Iceberg support to processing engines and associated tooling: + +* `iceberg-spark` is an implementation of Spark's Datasource V2 API for Iceberg with submodules for each spark versions (use runtime jars for a shaded version) +* `iceberg-flink` is an implementation of Flink's Table and DataStream API for Iceberg (use iceberg-flink-runtime for a shaded version) +* `iceberg-mr` is an implementation of MapReduce and Hive InputFormats and SerDes for Iceberg (use iceberg-hive-runtime for a shaded version for use with Hive) +* `iceberg-nessie` is a module used to integrate Iceberg table metadata history and operations with [Project Nessie](https://projectnessie.org/) +* `iceberg-data` is a client library used to read Iceberg tables from JVM applications +* `iceberg-runtime` generates a shaded runtime jar for Spark to integrate with iceberg tables diff --git a/1.11.0/docs/assets/images/audit-branch.png b/1.11.0/docs/assets/images/audit-branch.png new file mode 100644 index 0000000000000000000000000000000000000000..3d6506a513ca1e0af6f290c3cd4763be710da089 GIT binary patch literal 39065 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

6`Q8;eo;*=koCH<02oVlCihezhy$j-1 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 Hx^Mm)WV8tK literal 0 HcmV?d00001 diff --git a/1.11.0/docs/assets/images/historical-snapshot-tag.png b/1.11.0/docs/assets/images/historical-snapshot-tag.png new file mode 100644 index 0000000000000000000000000000000000000000..6a6be3d5352610e100b2310bf33af3e8cf395b0e GIT binary patch literal 17755 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 ZlVz&!R_;bthm+~5-+!oFuJ|PQe*xlvKWP8} literal 0 HcmV?d00001 diff --git a/1.11.0/docs/assets/images/iceberg-migrateaction-step2.png b/1.11.0/docs/assets/images/iceberg-migrateaction-step2.png new file mode 100644 index 0000000000000000000000000000000000000000..13bb2444b67b3d0b7e36e8fb9bbac936d23d966b GIT binary patch literal 29566 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

75z@gxhEFfsVnDaf+J8BHrq-mjORfI(3 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�G^Ne)Y6Ywfr^+rGDLv&Y+{p0*Thn^@eI7AUS%n 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

(bO$a^0NgfS{f7A)DABSI?m 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 J{>3}!e*-Y+*@6H7 literal 0 HcmV?d00001 diff --git a/1.11.0/docs/assets/images/partition-spec-evolution.png b/1.11.0/docs/assets/images/partition-spec-evolution.png new file mode 100644 index 0000000000000000000000000000000000000000..0bc595f686e12acc58b7ba0872f564221bafd7cc GIT binary patch literal 224020 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%0AWr{RZ0E1|;F!tNMSqzGHLF$UJJG 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 + +# Iceberg AWS Integrations + +Iceberg provides integration with different AWS services through the `iceberg-aws` module. +This section describes how to use Iceberg with AWS. + +## Enabling AWS Integration + +The `iceberg-aws` module is bundled with Spark and Flink engine runtimes for all versions from `0.11.0` onwards. +However, the AWS clients are not bundled so that you can use the same client version as your application. +You will need to provide the AWS v2 SDK because that is what Iceberg depends on. +You can choose to use the [AWS SDK bundle](https://mvnrepository.com/artifact/software.amazon.awssdk/bundle), +or individual AWS client packages (Glue, S3, DynamoDB, KMS, STS) if you would like to have a minimal dependency footprint. + +All the default AWS clients use the [Apache HTTP Client](https://mvnrepository.com/artifact/software.amazon.awssdk/apache-client) +for HTTP connection management. +This dependency is not part of the AWS SDK bundle and needs to be added separately. +To choose a different HTTP client library such as [URL Connection HTTP Client](https://mvnrepository.com/artifact/software.amazon.awssdk/url-connection-client), +see the section [client customization](#aws-client-customization) for more details. + +All the AWS module features can be loaded through custom catalog properties, +you can go to the documentations of each engine to see how to load a custom catalog. +Here are some examples. + +### Spark + +For example, to use AWS features with Spark 3.4 (with scala 2.12) and AWS clients (which is packaged in the `iceberg-aws-bundle`), you can start the Spark SQL shell with: + +```sh +# start Spark SQL client shell +spark-sql --packages org.apache.iceberg:iceberg-spark-runtime-3.4_2.12:{{ icebergVersion }},org.apache.iceberg:iceberg-aws-bundle:{{ icebergVersion }} \ + --conf spark.sql.defaultCatalog=my_catalog \ + --conf spark.sql.catalog.my_catalog=org.apache.iceberg.spark.SparkCatalog \ + --conf spark.sql.catalog.my_catalog.warehouse=s3://my-bucket/my/key/prefix \ + --conf spark.sql.catalog.my_catalog.type=glue \ + --conf spark.sql.catalog.my_catalog.io-impl=org.apache.iceberg.aws.s3.S3FileIO +``` + +As you can see, In the shell command, we use `--packages` to specify the additional `iceberg-aws-bundle` that contains all relevant AWS dependencies. + +### Flink + +To use AWS module with Flink, you can download the necessary dependencies and specify them when starting the Flink SQL client: + +```sh +# download Iceberg dependency +ICEBERG_VERSION={{ icebergVersion }} +MAVEN_URL=https://repo1.maven.org/maven2 +ICEBERG_MAVEN_URL=$MAVEN_URL/org/apache/iceberg + +wget $ICEBERG_MAVEN_URL/iceberg-flink-runtime/$ICEBERG_VERSION/iceberg-flink-runtime-$ICEBERG_VERSION.jar + +wget $ICEBERG_MAVEN_URL/iceberg-aws-bundle/$ICEBERG_VERSION/iceberg-aws-bundle-$ICEBERG_VERSION.jar + +# start Flink SQL client shell +/path/to/bin/sql-client.sh embedded \ + -j iceberg-flink-runtime-$ICEBERG_VERSION.jar \ + -j iceberg-aws-bundle-$ICEBERG_VERSION.jar \ + shell +``` + +With those dependencies, you can create a Flink catalog like the following: + +```sql +CREATE CATALOG my_catalog WITH ( + 'type'='iceberg', + 'warehouse'='s3://my-bucket/my/key/prefix', + 'catalog-type'='glue', + 'io-impl'='org.apache.iceberg.aws.s3.S3FileIO' +); +``` + +You can also specify the catalog configurations in `sql-client-defaults.yaml` to preload it: + +```yaml +catalogs: + - name: my_catalog + type: iceberg + warehouse: s3://my-bucket/my/key/prefix + catalog-type: glue + io-impl: org.apache.iceberg.aws.s3.S3FileIO +``` + +### Hive + +To use AWS module with Hive, you can download the necessary dependencies similar to the Flink example, +and then add them to the Hive classpath or add the jars at runtime in CLI: + +``` +add jar /my/path/to/iceberg-hive-runtime.jar; +add jar /my/path/to/aws/bundle.jar; +``` + +With those dependencies, you can register a Glue catalog and create external tables in Hive at runtime in CLI by: + +```sql +SET iceberg.engine.hive.enabled=true; +SET hive.vectorized.execution.enabled=false; +SET iceberg.catalog.glue.type=glue; +SET iceberg.catalog.glue.warehouse=s3://my-bucket/my/key/prefix; + +-- suppose you have an Iceberg table database_a.table_a created by GlueCatalog +CREATE EXTERNAL TABLE database_a.table_a +STORED BY 'org.apache.iceberg.mr.hive.HiveIcebergStorageHandler' +TBLPROPERTIES ('iceberg.catalog'='glue'); +``` + +You can also preload the catalog by setting the configurations above in `hive-site.xml`. + +## Catalogs + +There are multiple different options that users can choose to build an Iceberg catalog with AWS. + +### Glue Catalog + +Iceberg enables the use of [AWS Glue](https://aws.amazon.com/glue) as the `Catalog` implementation. +When used, an Iceberg namespace is stored as a [Glue Database](https://docs.aws.amazon.com/glue/latest/dg/aws-glue-api-catalog-databases.html), +an Iceberg table is stored as a [Glue Table](https://docs.aws.amazon.com/glue/latest/dg/aws-glue-api-catalog-tables.html), +and every Iceberg table version is stored as a [Glue TableVersion](https://docs.aws.amazon.com/glue/latest/dg/aws-glue-api-catalog-tables.html#aws-glue-api-catalog-tables-TableVersion). +You can start using Glue catalog by specifying the `catalog-impl` as `org.apache.iceberg.aws.glue.GlueCatalog` +or by setting `catalog-type` as `glue`, +just like what is shown in the [enabling AWS integration](#enabling-aws-integration) section above. +More details about loading the catalog can be found in individual engine pages, such as [Spark](spark-configuration.md#loading-a-custom-catalog) and [Flink](flink.md#creating-catalogs-and-using-catalogs). + +#### Glue Catalog ID + +There is a unique Glue metastore in each AWS account and each AWS region. +By default, `GlueCatalog` chooses the Glue metastore to use based on the user's default AWS client credential and region setup. +You can specify the Glue catalog ID through `glue.id` catalog property to point to a Glue catalog in a different AWS account. +The Glue catalog ID is your numeric AWS account ID. +If the Glue catalog is in a different region, you should configure your AWS client to point to the correct region, +see more details in [AWS client customization](#aws-client-customization). + +#### Skip Archive + +AWS Glue has the ability to archive older table versions and a user can roll back the table to any historical version if needed. +By default, the Iceberg Glue Catalog will skip the archival of older table versions. +If a user wishes to archive older table versions, they can set `glue.skip-archive` to false. +Do note for streaming ingestion into Iceberg tables, setting `glue.skip-archive` to false will quickly create a lot of Glue table versions. +For more details, please read [Glue Quotas](https://docs.aws.amazon.com/general/latest/gr/glue.html) and the [UpdateTable API](https://docs.aws.amazon.com/glue/latest/webapi/API_UpdateTable.html). + +#### Skip Name Validation + +Allow user to skip name validation for table name and namespaces. +It is recommended to stick to [Glue best practices](https://docs.aws.amazon.com/athena/latest/ug/glue-best-practices.html) +to make sure operations are Hive compatible. +This is only added for users that have existing conventions using non-standard characters. When database name +and table name validation are skipped, there is no guarantee that downstream systems would all support the names. + +#### Optimistic Locking + +By default, Iceberg uses Glue's optimistic locking for concurrent updates to a table. +With optimistic locking, each table has a version id. +If users retrieve the table metadata, Iceberg records the version id of that table. +Users can update the table as long as the version ID on the server side remains unchanged. +Version mismatch occurs if someone else modified the table before you did, causing an update failure. +Iceberg then refreshes metadata and checks if there is a conflict. +If there is no commit conflict, the operation will be retried. +Optimistic locking guarantees atomic transaction of Iceberg tables in Glue. +It also prevents others from accidentally overwriting your changes. + +!!! info + Please use AWS SDK version >= 2.17.131 to leverage Glue's Optimistic Locking. + If the AWS SDK version is below 2.17.131, only in-memory lock is used. To ensure atomic transaction, you need to set up a [DynamoDb Lock Manager](#dynamodb-lock-manager). + +#### Warehouse Location + +Similar to all other catalog implementations, `warehouse` is a required catalog property to determine the root path of the data warehouse in storage. +By default, Glue only allows a warehouse location in S3 because of the use of `S3FileIO`. +To store data in a different local or cloud store, Glue catalog can switch to use `HadoopFileIO` or any custom FileIO by setting the `io-impl` catalog property. +Details about this feature can be found in the [custom FileIO](custom-catalog.md#custom-file-io-implementation) section. + +#### Table Location + +By default, the root location for a table `my_table` of namespace `my_ns` is at `my-warehouse-location/my-ns.db/my-table`. +This default root location can be changed at both namespace and table level. + +To use a different path prefix for all tables under a namespace, use AWS console or any AWS Glue client SDK you like to update the `locationUri` attribute of the corresponding Glue database. +For example, you can update the `locationUri` of `my_ns` to `s3://my-ns-bucket`, +then any newly created table will have a default root location under the new prefix. +For instance, a new table `my_table_2` will have its root location at `s3://my-ns-bucket/my_table_2`. + +To use a completely different root path for a specific table, set the `location` table property to the desired root path value you want. +For example, in Spark SQL you can do: + +```sql +CREATE TABLE my_catalog.my_ns.my_table ( + id bigint, + data string, + category string) +USING iceberg +OPTIONS ('location'='s3://my-special-table-bucket') +PARTITIONED BY (category); +``` + +For engines like Spark that support the `LOCATION` keyword, the above SQL statement is equivalent to: + +```sql +CREATE TABLE my_catalog.my_ns.my_table ( + id bigint, + data string, + category string) +USING iceberg +LOCATION 's3://my-special-table-bucket' +PARTITIONED BY (category); +``` + +### DynamoDB Catalog + +Iceberg supports using a [DynamoDB](https://aws.amazon.com/dynamodb) table to record and manage database and table information. + +#### Configurations + +The DynamoDB catalog supports the following configurations: + +| Property | Default | Description | +| --------------------------------- | -------------------------------------------------- | ------------------------------------------------------ | +| dynamodb.table-name | iceberg | name of the DynamoDB table used by DynamoDbCatalog | + +#### Internal Table Design + +The DynamoDB table is designed with the following columns: + +| Column | Key | Type | Description | +| ----------------- | --------------- | ----------- |--------------------------------------------------------------------- | +| identifier | partition key | string | table identifier such as `db1.table1`, or string `NAMESPACE` for namespaces | +| namespace | sort key | string | namespace name. A [global secondary index (GSI)](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/GSI.html) is created with namespace as partition key, identifier as sort key, no other projected columns | +| v | | string | row version, used for optimistic locking | +| updated_at | | number | timestamp (millis) of the last update | +| created_at | | number | timestamp (millis) of the table creation | +| p. | | string | Iceberg-defined table properties including `table_type`, `metadata_location` and `previous_metadata_location` or namespace properties + +This design has the following benefits: + +1. it avoids potential [hot partition issue](https://aws.amazon.com/premiumsupport/knowledge-center/dynamodb-table-throttled/) if there are heavy write traffic to the tables within the same namespace because the partition key is at the table level +2. namespace operations are clustered in a single partition to avoid affecting table commit operations +3. a sort key to partition key reverse GSI is used for list table operation, and all other operations are single row ops or single partition query. No full table scan is needed for any operation in the catalog. +4. a string UUID version field `v` is used instead of `updated_at` to avoid 2 processes committing at the same millisecond +5. multi-row transaction is used for `catalog.renameTable` to ensure idempotency +6. properties are flattened as top level columns so that user can add custom GSI on any property field to customize the catalog. For example, users can store owner information as table property `owner`, and search tables by owner by adding a GSI on the `p.owner` column. + +### RDS JDBC Catalog + +Iceberg also supports the JDBC catalog which uses a table in a relational database to manage Iceberg tables. +You can configure to use the JDBC catalog with relational database services like [AWS RDS](https://aws.amazon.com/rds). +Read [the JDBC integration page](jdbc.md#jdbc-catalog) for guides and examples about using the JDBC catalog. +Read [this AWS documentation](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.IAMDBAuth.Connecting.Java.html) for more details about configuring the JDBC catalog with IAM authentication. + +### Which catalog to choose? + +With all the available options, we offer the following guidelines when choosing the right catalog to use for your application: + +1. if your organization has an existing Glue metastore or plans to use the AWS analytics ecosystem including Glue, [Athena](https://aws.amazon.com/athena), [EMR](https://aws.amazon.com/emr), [Redshift](https://aws.amazon.com/redshift) and [LakeFormation](https://aws.amazon.com/lake-formation), Glue catalog provides the easiest integration. +2. if your application requires frequent updates to table or high read and write throughput (e.g. streaming write), Glue and DynamoDB catalogs provide the best performance through optimistic locking. +3. if you would like to enforce access control for tables in a catalog, Glue tables can be managed as an [IAM resource](https://docs.aws.amazon.com/service-authorization/latest/reference/list_awsglue.html), whereas DynamoDB catalog tables can only be managed through [item-level permission](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/specifying-conditions.html) which is much more complicated. +4. if you would like to query tables based on table property information without the need to scan the entire catalog, DynamoDB catalog allows you to build secondary indexes for any arbitrary property field and provides efficient query performance. +5. if you would like to have the benefit of DynamoDB catalog while also connect to Glue, you can enable [DynamoDB stream with Lambda trigger](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Streams.Lambda.Tutorial.html) to asynchronously update your Glue metastore with table information in the DynamoDB catalog. +6. if your organization already maintains an existing relational database in RDS or uses [serverless Aurora](https://aws.amazon.com/rds/aurora/serverless/) to manage tables, the JDBC catalog provides the easiest integration. + +## DynamoDb Lock Manager + +[Amazon DynamoDB](https://aws.amazon.com/dynamodb) can be used by `HadoopCatalog` or `HadoopTables` so that for every commit, +the catalog first obtains a lock using a helper DynamoDB table and then try to safely modify the Iceberg table. +This is necessary for a file system-based catalog to ensure atomic transaction in storages like S3 that do not provide file write mutual exclusion. + +This feature requires the following lock related catalog properties: + +1. Set `lock-impl` as `org.apache.iceberg.aws.dynamodb.DynamoDbLockManager`. +2. Set `lock.table` as the DynamoDB table name you would like to use. If the lock table with the given name does not exist in DynamoDB, a new table is created with billing mode set as [pay-per-request](https://aws.amazon.com/blogs/aws/amazon-dynamodb-on-demand-no-capacity-planning-and-pay-per-request-pricing). + +Other lock related catalog properties can also be used to adjust locking behaviors such as heartbeat interval. +For more details, please refer to [Lock catalog properties](catalog-properties.md#lock-catalog-properties). + +## S3 FileIO + +Iceberg allows users to write data to S3 through `S3FileIO`. +`GlueCatalog` by default uses this `FileIO`, and other catalogs can load this `FileIO` using the `io-impl` catalog property. + +### Progressive Multipart Upload + +`S3FileIO` implements a customized progressive multipart upload algorithm to upload data. +Data files are uploaded by parts in parallel as soon as each part is ready, +and each file part is deleted as soon as its upload process completes. +This provides maximized upload speed and minimized local disk usage during uploads. +Here are the configurations that users can tune related to this feature: + +| Property | Default | Description | +| --------------------------------- | -------------------------------------------------- | ------------------------------------------------------ | +| s3.multipart.num-threads | the available number of processors in the system | number of threads to use for uploading parts to S3 (shared across all output streams) | +| s3.multipart.part-size-bytes | 32MB | the size of a single part for multipart upload requests | +| s3.multipart.threshold | 1.5 | the threshold expressed as a factor times the multipart size at which to switch from uploading using a single put object request to uploading using multipart upload | +| s3.staging-dir | `java.io.tmpdir` property value | the directory to hold temporary files | + +### S3 Server Side Encryption + +`S3FileIO` supports all 3 S3 server side encryption modes: + +* [SSE-S3](https://docs.aws.amazon.com/AmazonS3/latest/dev/UsingServerSideEncryption.html): When you use Server-Side Encryption with Amazon S3-Managed Keys (SSE-S3), each object is encrypted with a unique key. As an additional safeguard, it encrypts the key itself with a master key that it regularly rotates. Amazon S3 server-side encryption uses one of the strongest block ciphers available, 256-bit Advanced Encryption Standard (AES-256), to encrypt your data. +* [SSE-KMS](https://docs.aws.amazon.com/AmazonS3/latest/dev/UsingKMSEncryption.html): Server-Side Encryption with Customer Master Keys (CMKs) Stored in AWS Key Management Service (SSE-KMS) is similar to SSE-S3, but with some additional benefits and charges for using this service. There are separate permissions for the use of a CMK that provides added protection against unauthorized access of your objects in Amazon S3. SSE-KMS also provides you with an audit trail that shows when your CMK was used and by whom. Additionally, you can create and manage customer managed CMKs or use AWS managed CMKs that are unique to you, your service, and your Region. +* [DSSE-KMS](https://docs.aws.amazon.com/AmazonS3/latest/userguide/UsingDSSEncryption.html): Dual-layer Server-Side Encryption with AWS Key Management Service keys (DSSE-KMS) is similar to SSE-KMS, but applies two layers of encryption to objects when they are uploaded to Amazon S3. DSSE-KMS can be used to fulfill compliance standards that require you to apply multilayer encryption to your data and have full control of your encryption keys. +* [SSE-C](https://docs.aws.amazon.com/AmazonS3/latest/dev/ServerSideEncryptionCustomerKeys.html): With Server-Side Encryption with Customer-Provided Keys (SSE-C), you manage the encryption keys and Amazon S3 manages the encryption, as it writes to disks, and decryption when you access your objects. + +To enable server side encryption, use the following configuration properties: + +| Property | Default | Description | +| --------------------------------- | ---------------------------------------- | ------------------------------------------------------ | +| s3.sse.type | `none` | `none`, `s3`, `kms`, `dsse-kms` or `custom` | +| s3.sse.key | `aws/s3` for `kms` and `dsse-kms` types, null otherwise | A KMS Key ID or ARN for `kms` and `dsse-kms` types, or a custom base-64 AES256 symmetric key for `custom` type. | +| s3.sse.md5 | null | If SSE type is `custom`, this value must be set as the base-64 MD5 digest of the symmetric key to ensure integrity. | + +### S3 Access Control List + +`S3FileIO` supports S3 access control list (ACL) for detailed access control. +User can choose the ACL level by setting the `s3.acl` property. +For more details, please read [S3 ACL Documentation](https://docs.aws.amazon.com/AmazonS3/latest/dev/acl-overview.html). + +### Object Store File Layout + +S3 and many other cloud storage services [throttle requests based on object prefix](https://aws.amazon.com/premiumsupport/knowledge-center/s3-request-limit-avoid-throttling/). +Data stored in S3 with a traditional Hive storage layout can face S3 request throttling as objects are stored under the same file path prefix. + +Iceberg by default uses the Hive storage layout but can be switched to use the `ObjectStoreLocationProvider`. +With `ObjectStoreLocationProvider`, a deterministic hash is generated for each stored file, with the hash appended +directly after the `write.data.path`. This ensures files written to S3 are equally distributed across multiple +[prefixes](https://aws.amazon.com/premiumsupport/knowledge-center/s3-object-key-naming-pattern/) in the S3 bucket; +resulting in minimized throttling and maximized throughput for S3-related IO operations. When using `ObjectStoreLocationProvider` +having a shared `write.data.path` across your Iceberg tables will improve performance. + +For more information on how S3 scales API QPS, check out the 2018 re:Invent session on [Best Practices for Amazon S3 and Amazon S3 Glacier](https://youtu.be/rHeTn9pHNKo?t=3219). At [53:39](https://youtu.be/rHeTn9pHNKo?t=3219) it covers how S3 scales/partitions & at [54:50](https://youtu.be/rHeTn9pHNKo?t=3290) it discusses the 30-60 minute wait time before new partitions are created. + +To use the `ObjectStorageLocationProvider` add `'write.object-storage.enabled'=true` in the table's properties. +Below is an example Spark SQL command to create a table using the `ObjectStorageLocationProvider`: +```sql +CREATE TABLE my_catalog.my_ns.my_table ( + id bigint, + data string, + category string) +USING iceberg +OPTIONS ( + 'write.object-storage.enabled'=true, + 'write.data.path'='s3://my-table-data-bucket/my_table') +PARTITIONED BY (category); +``` + +We can then insert a single row into this new table +```SQL +INSERT INTO my_catalog.my_ns.my_table VALUES (1, "Pizza", "orders"); +``` + +Which will write the data to S3 with a 20-bit base2 hash (`01010110100110110010`) appended directly after the `write.object-storage.path`, +ensuring reads to the table are spread evenly across [S3 bucket prefixes](https://docs.aws.amazon.com/AmazonS3/latest/userguide/optimizing-performance.html), and improving performance. +Previously provided base64 hash was updated to base2 in order to provide an improved auto-scaling behavior on S3 General Purpose Buckets. + +As part of this update, we have also divided the entropy into multiple directories in order to improve the efficiency of the +orphan clean up process for Iceberg since directories are used as a mean to divide the work across workers for faster traversal. You +can see from the example below that we divide the hash to create 4-bit directories with a depth of 3 and attach the final part of the hash to +the end. +``` +s3://my-table-data-bucket/my_ns.db/my_table/0101/0110/1001/10110010/category=orders/00000-0-5affc076-96a4-48f2-9cd2-d5efbc9f0c94-00001.parquet +``` + +Note, the path resolution logic for `ObjectStoreLocationProvider` is `write.data.path` then `/data`. + +However, for the older versions up to 0.12.0, the logic is as follows: + +- before 0.12.0, `write.object-storage.path` must be set. +- at 0.12.0, `write.object-storage.path` then `write.folder-storage.path` then `/data`. +- at 2.0.0 `write.object-storage.path` and `write.folder-storage.path` will be removed + +For more details, please refer to the [LocationProvider Configuration](custom-catalog.md#custom-location-provider-implementation) section. + +We have also added a new table property `write.object-storage.partitioned-paths` that if set to false(default=true), this will +omit the partition values from the file path. Iceberg does not need these values in the file path and setting this value to false +can further reduce the key size. In this case, we also append the final 8 bit of entropy directly to the file name. +Inserted key would look like the following with this config set, note that `category=orders` is removed: +``` +s3://my-table-data-bucket/my_ns.db/my_table/1101/0100/1011/00111010-00000-0-5affc076-96a4-48f2-9cd2-d5efbc9f0c94-00001.parquet +``` + +### S3 Retries + +Workloads which encounter S3 throttling should persistently retry, with exponential backoff, to make progress while S3 +automatically scales. We provide the configurations below to adjust S3 retries for this purpose. For workloads that encounter +throttling and fail due to retry exhaustion, we recommend retry count to set 32 in order allow S3 to auto-scale. Note that +workloads with exceptionally high throughput against tables that S3 has not yet scaled, it may be necessary to increase the retry count further. + +| Property | Default | Description | +|----------------------|---------|---------------------------------------------------------------------------------------| +| s3.retry.num-retries | 5 | Number of times to retry S3 operations. Recommended 32 for high-throughput workloads. | +| s3.retry.min-wait-ms | 2s | Minimum wait time to retry a S3 operation. | +| s3.retry.max-wait-ms | 20s | Maximum wait time to retry a S3 read operation. | + +### S3 Strong Consistency + +In November 2020, S3 announced [strong consistency](https://aws.amazon.com/s3/consistency/) for all read operations, and Iceberg is updated to fully leverage this feature. +There is no redundant consistency wait and check which might negatively impact performance during IO operations. + +### Hadoop S3A FileSystem + +!!! important + **S3FileIO is recommended** for S3 use cases rather than the `S3A FileSystem` (`HadoopFileIO`). + +Before `S3FileIO` was introduced, many Iceberg users choose to use `HadoopFileIO` to write data to S3 through the [S3A FileSystem](https://github.com/apache/hadoop/blob/trunk/hadoop-tools/hadoop-aws/src/main/java/org/apache/hadoop/fs/s3a/S3AFileSystem.java). +As introduced in the previous sections, `S3FileIO` adopts the latest AWS clients and S3 features for optimized security and performance. + +`S3FileIO` writes data with `s3://` URI scheme, but it is also compatible with schemes written by the S3A FileSystem. +This means for any table manifests containing `s3a://` or `s3n://` file paths, `S3FileIO` is still able to read them. +This feature allows people to easily switch from S3A to `S3FileIO`. + +If for any reason you have to use S3A, here are the instructions: + +1. To store data using S3A, specify the `warehouse` catalog property to be an S3A path, e.g. `s3a://my-bucket/my-warehouse` +2. For `HiveCatalog`, to also store metadata using S3A, specify the Hadoop config property `hive.metastore.warehouse.dir` to be an S3A path. +3. Add [hadoop-aws](https://mvnrepository.com/artifact/org.apache.hadoop/hadoop-aws) as a runtime dependency of your compute engine. +4. Configure AWS settings based on [hadoop-aws documentation](https://hadoop.apache.org/docs/current/hadoop-aws/tools/hadoop-aws/index.html) (make sure you check the version, S3A configuration varies a lot based on the version you use). + +### S3 Write Checksum Verification + +To ensure integrity of uploaded objects, checksum validations for S3 writes can be turned on by setting catalog property `s3.checksum-enabled` to `true`. +This is turned off by default. + +### S3 Tags + +Custom [tags](https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-tagging.html) can be added to S3 objects while writing and deleting. +For example, to write S3 tags with Spark 3.5, you can start the Spark SQL shell with: +``` +spark-sql --conf spark.sql.catalog.my_catalog=org.apache.iceberg.spark.SparkCatalog \ + --conf spark.sql.catalog.my_catalog.warehouse=s3://my-bucket/my/key/prefix \ + --conf spark.sql.catalog.my_catalog.type=glue \ + --conf spark.sql.catalog.my_catalog.io-impl=org.apache.iceberg.aws.s3.S3FileIO \ + --conf spark.sql.catalog.my_catalog.s3.write.tags.my_key1=my_val1 \ + --conf spark.sql.catalog.my_catalog.s3.write.tags.my_key2=my_val2 +``` +For the above example, the objects in S3 will be saved with tags: `my_key1=my_val1` and `my_key2=my_val2`. Do note that the specified write tags will be saved only while object creation. + +When the catalog property `s3.delete-enabled` is set to `false`, the objects are not hard-deleted from S3. +This is expected to be used in combination with S3 delete tagging, so objects are tagged and removed using [S3 lifecycle policy](https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-lifecycle-mgmt.html). +The property is set to `true` by default. + +With the `s3.delete.tags` config, objects are tagged with the configured key-value pairs before deletion. +Users can configure tag-based object lifecycle policy at bucket level to transition objects to different tiers. +For example, to add S3 delete tags with Spark 3.5, you can start the Spark SQL shell with: + +``` +sh spark-sql --conf spark.sql.catalog.my_catalog=org.apache.iceberg.spark.SparkCatalog \ + --conf spark.sql.catalog.my_catalog.warehouse=s3://iceberg-warehouse/s3-tagging \ + --conf spark.sql.catalog.my_catalog.type=glue \ + --conf spark.sql.catalog.my_catalog.io-impl=org.apache.iceberg.aws.s3.S3FileIO \ + --conf spark.sql.catalog.my_catalog.s3.delete.tags.my_key3=my_val3 \ + --conf spark.sql.catalog.my_catalog.s3.delete-enabled=false +``` + +For the above example, the objects in S3 will be saved with tags: `my_key3=my_val3` before deletion. +Users can also use the catalog property `s3.delete.num-threads` to mention the number of threads to be used for adding delete tags to the S3 objects. + +When the catalog property `s3.write.table-tag-enabled` and `s3.write.namespace-tag-enabled` is set to `true` then the objects in S3 will be saved with tags: `iceberg.table=` and `iceberg.namespace=`. +Users can define access and data retention policy per namespace or table based on these tags. +For example, to write table and namespace name as S3 tags with Spark 3.5, you can start the Spark SQL shell with: +``` +sh spark-sql --conf spark.sql.catalog.my_catalog=org.apache.iceberg.spark.SparkCatalog \ + --conf spark.sql.catalog.my_catalog.warehouse=s3://iceberg-warehouse/s3-tagging \ + --conf spark.sql.catalog.my_catalog.type=glue \ + --conf spark.sql.catalog.my_catalog.io-impl=org.apache.iceberg.aws.s3.S3FileIO \ + --conf spark.sql.catalog.my_catalog.s3.write.table-tag-enabled=true \ + --conf spark.sql.catalog.my_catalog.s3.write.namespace-tag-enabled=true +``` +For more details on tag restrictions, please refer [User-Defined Tag Restrictions](https://docs.aws.amazon.com/AmazonS3/latest/userguide/tagging-managing.html). + +### S3 Access Points + +[Access Points](https://docs.aws.amazon.com/AmazonS3/latest/userguide/using-access-points.html) can be used to perform +S3 operations by specifying a mapping of bucket to access points. This is useful for multi-region access, cross-region access, +disaster recovery, etc. + +For using cross-region access points, we need to additionally set `use-arn-region-enabled` catalog property to +`true` to enable `S3FileIO` to make cross-region calls, it's not required for same / multi-region access points. + +For example, to use S3 access-point with Spark 3.5, you can start the Spark SQL shell with: +``` +spark-sql --conf spark.sql.catalog.my_catalog=org.apache.iceberg.spark.SparkCatalog \ + --conf spark.sql.catalog.my_catalog.warehouse=s3://my-bucket2/my/key/prefix \ + --conf spark.sql.catalog.my_catalog.type=glue \ + --conf spark.sql.catalog.my_catalog.io-impl=org.apache.iceberg.aws.s3.S3FileIO \ + --conf spark.sql.catalog.my_catalog.s3.use-arn-region-enabled=false \ + --conf spark.sql.catalog.my_catalog.s3.access-points.my-bucket1=arn:aws:s3:::accesspoint/ \ + --conf spark.sql.catalog.my_catalog.s3.access-points.my-bucket2=arn:aws:s3:::accesspoint/ +``` +For the above example, the objects in S3 on `my-bucket1` and `my-bucket2` buckets will use `arn:aws:s3:::accesspoint/` +access-point for all S3 operations. + +For more details on using access-points, please refer [Using access points with compatible Amazon S3 operations](https://docs.aws.amazon.com/AmazonS3/latest/userguide/access-points-usage-examples.html), [Sample notebook](https://github.com/aws-samples/quant-research/tree/main) . + +### S3 Access Grants + +[S3 Access Grants](https://aws.amazon.com/s3/features/access-grants/) can be used to grant accesses to S3 data using IAM Principals. +In order to enable S3 Access Grants to work in Iceberg, you can set the `s3.access-grants.enabled` catalog property to `true` after +you add the [S3 Access Grants Plugin jar](https://github.com/aws/aws-s3-accessgrants-plugin-java-v2) to your classpath. A link +to the Maven listing for this plugin can be found [here](https://mvnrepository.com/artifact/software.amazon.s3.accessgrants/aws-s3-accessgrants-java-plugin). + +In addition, we allow the [fallback-to-IAM configuration](https://github.com/aws/aws-s3-accessgrants-plugin-java-v2) which allows +you to fallback to using your IAM role (and its permission sets directly) to access your S3 data in the case the S3 Access Grants +is unable to authorize your S3 call. This can be done using the `s3.access-grants.fallback-to-iam` boolean catalog property. By default, +this property is set to `false`. + +For example, to add the S3 Access Grants Integration with Spark 3.5, you can start the Spark SQL shell with: +``` +spark-sql --conf spark.sql.catalog.my_catalog=org.apache.iceberg.spark.SparkCatalog \ + --conf spark.sql.catalog.my_catalog.warehouse=s3://my-bucket2/my/key/prefix \ + --conf spark.sql.catalog.my_catalog.catalog-impl=org.apache.iceberg.aws.glue.GlueCatalog \ + --conf spark.sql.catalog.my_catalog.io-impl=org.apache.iceberg.aws.s3.S3FileIO \ + --conf spark.sql.catalog.my_catalog.s3.access-grants.enabled=true \ + --conf spark.sql.catalog.my_catalog.s3.access-grants.fallback-to-iam=true +``` + +For more details on using S3 Access Grants, please refer to [Managing access with S3 Access Grants](https://docs.aws.amazon.com/AmazonS3/latest/userguide/access-grants.html). + +### S3 Cross-Region Access + +S3 Cross-Region bucket access can be turned on by setting catalog property `s3.cross-region-access-enabled` to `true`. +This is turned off by default to avoid first S3 API call increased latency. + +For example, to enable S3 Cross-Region bucket access with Spark 3.5, you can start the Spark SQL shell with: +``` +spark-sql --conf spark.sql.catalog.my_catalog=org.apache.iceberg.spark.SparkCatalog \ + --conf spark.sql.catalog.my_catalog.warehouse=s3://my-bucket2/my/key/prefix \ + --conf spark.sql.catalog.my_catalog.type=glue \ + --conf spark.sql.catalog.my_catalog.io-impl=org.apache.iceberg.aws.s3.S3FileIO \ + --conf spark.sql.catalog.my_catalog.s3.cross-region-access-enabled=true +``` + +For more details, please refer to [Cross-Region access for Amazon S3](https://docs.aws.amazon.com/sdk-for-java/latest/developer-guide/s3-cross-region.html). + +### S3 Acceleration + +[S3 Acceleration](https://aws.amazon.com/s3/transfer-acceleration/) can be used to speed up transfers to and from Amazon S3 by as much as 50-500% for long-distance transfer of larger objects. + +To use S3 Acceleration, we need to set `s3.acceleration-enabled` catalog property to `true` to enable `S3FileIO` to make accelerated S3 calls. + +For example, to use S3 Acceleration with Spark 3.5, you can start the Spark SQL shell with: +``` +spark-sql --conf spark.sql.catalog.my_catalog=org.apache.iceberg.spark.SparkCatalog \ + --conf spark.sql.catalog.my_catalog.warehouse=s3://my-bucket2/my/key/prefix \ + --conf spark.sql.catalog.my_catalog.type=glue \ + --conf spark.sql.catalog.my_catalog.io-impl=org.apache.iceberg.aws.s3.S3FileIO \ + --conf spark.sql.catalog.my_catalog.s3.acceleration-enabled=true +``` + +For more details on using S3 Acceleration, please refer to [Configuring fast, secure file transfers using Amazon S3 Transfer Acceleration](https://docs.aws.amazon.com/AmazonS3/latest/userguide/transfer-acceleration.html). + +### S3 Analytics Accelerator + +The [Analytics Accelerator Library for Amazon S3](https://github.com/awslabs/analytics-accelerator-s3) helps you accelerate access to Amazon S3 data from your applications. This open-source solution reduces processing times and compute costs for your data analytics workloads. + +In order to enable S3 Analytics Accelerator Library to work in Iceberg, you can set the `s3.analytics-accelerator.enabled` catalog property to `true`. By default, this property is set to `false`. + +For example, to use S3 Analytics Accelerator with Spark, you can start the Spark SQL shell with: +``` +spark-sql --conf spark.sql.catalog.my_catalog=org.apache.iceberg.spark.SparkCatalog \ + --conf spark.sql.catalog.my_catalog.warehouse=s3://my-bucket2/my/key/prefix \ + --conf spark.sql.catalog.my_catalog.type=glue \ + --conf spark.sql.catalog.my_catalog.io-impl=org.apache.iceberg.aws.s3.S3FileIO \ + --conf spark.sql.catalog.my_catalog.s3.analytics-accelerator.enabled=true +``` + +The Analytics Accelerator Library can work with either the [S3 CRT client](https://docs.aws.amazon.com/sdk-for-java/latest/developer-guide/crt-based-s3-client.html) or the [S3AsyncClient](https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/services/s3/S3AsyncClient.html). The library recommends that you use the S3 CRT client due to its enhanced connection pool management and [higher throughput on downloads](https://aws.amazon.com/blogs/developer/introducing-crt-based-s3-client-and-the-s3-transfer-manager-in-the-aws-sdk-for-java-2-x/). + +#### Client Configuration + +| Property | Default | Description | +|------------------------|---------|--------------------------------------------------------------| +| s3.crt.enabled | `true` | Controls if the S3 Async clients should be created using CRT | +| s3.crt.max-concurrency | `500` | Max concurrency for S3 CRT clients | + +Additional library specific configurations are organized into the following sections: + +#### Logical IO Configuration + +| Property | Default | Description | +|------------------------------------------------------------------------|-----------------------|----------------------------------------------------------------------------| +| s3.analytics-accelerator.logicalio.prefetch.footer.enabled | `true` | Controls whether footer prefetching is enabled | +| s3.analytics-accelerator.logicalio.prefetch.page.index.enabled | `true` | Controls whether page index prefetching is enabled | +| s3.analytics-accelerator.logicalio.prefetch.file.metadata.size | `32KB` | Size of metadata to prefetch for regular files | +| s3.analytics-accelerator.logicalio.prefetch.large.file.metadata.size | `1MB` | Size of metadata to prefetch for large files | +| s3.analytics-accelerator.logicalio.prefetch.file.page.index.size | `1MB` | Size of page index to prefetch for regular files | +| s3.analytics-accelerator.logicalio.prefetch.large.file.page.index.size | `8MB` | Size of page index to prefetch for large files | +| s3.analytics-accelerator.logicalio.large.file.size | `1GB` | Threshold to consider a file as large | +| s3.analytics-accelerator.logicalio.small.objects.prefetching.enabled | `true` | Controls prefetching for small objects | +| s3.analytics-accelerator.logicalio.small.object.size.threshold | `3MB` | Size threshold for small object prefetching | +| s3.analytics-accelerator.logicalio.parquet.metadata.store.size | `45` | Size of the parquet metadata store | +| s3.analytics-accelerator.logicalio.max.column.access.store.size | `15` | Maximum size of column access store | +| s3.analytics-accelerator.logicalio.parquet.format.selector.regex | `^.*.(parquet\|par)$` | Regex pattern to identify parquet files | +| s3.analytics-accelerator.logicalio.prefetching.mode | `ROW_GROUP` | Prefetching mode (valid values: `OFF`, `ALL`, `ROW_GROUP`, `COLUMN_BOUND`) | + +#### Physical IO Configuration + +| Property | Default | Description | +|--------------------------------------------------------------|---------|---------------------------------------------| +| s3.analytics-accelerator.physicalio.metadatastore.capacity | `50` | Capacity of the metadata store | +| s3.analytics-accelerator.physicalio.blocksizebytes | `8MB` | Size of blocks for data transfer | +| s3.analytics-accelerator.physicalio.readaheadbytes | `64KB` | Number of bytes to read ahead | +| s3.analytics-accelerator.physicalio.maxrangesizebytes | `8MB` | Maximum size of range requests | +| s3.analytics-accelerator.physicalio.partsizebytes | `8MB` | Size of individual parts for transfer | +| s3.analytics-accelerator.physicalio.sequentialprefetch.base | `2.0` | Base factor for sequential prefetch sizing | +| s3.analytics-accelerator.physicalio.sequentialprefetch.speed | `1.0` | Speed factor for sequential prefetch growth | + +#### Telemetry Configuration + +| Property | Default | Description | +|------------------------------------------------------------------------|-------------------------------------|--------------------------------------------------------------------------| +| s3.analytics-accelerator.telemetry.level | `STANDARD` | Telemetry detail level (valid values: `CRITICAL`, `STANDARD`, `VERBOSE`) | +| s3.analytics-accelerator.telemetry.std.out.enabled | `false` | Enable stdout telemetry output | +| s3.analytics-accelerator.telemetry.logging.enabled | `true` | Enable logging telemetry output | +| s3.analytics-accelerator.telemetry.aggregations.enabled | `false` | Enable telemetry aggregations | +| s3.analytics-accelerator.telemetry.aggregations.flush.interval.seconds | `-1` | Interval to flush aggregated telemetry | +| s3.analytics-accelerator.telemetry.logging.level | `INFO` | Log level for telemetry | +| s3.analytics-accelerator.telemetry.logging.name | `com.amazon.connector.s3.telemetry` | Logger name for telemetry | +| s3.analytics-accelerator.telemetry.format | `default` | Telemetry output format (valid values: `json`, `default`) | + +#### Object Client Configuration + +| Property | Default | Description | +|------------------------------------------|---------|----------------------------------------------------------------| +| s3.analytics-accelerator.useragentprefix | `null` | Custom prefix to add to the `User-Agent` string in S3 requests | + +### S3 Dual-stack + +[S3 Dual-stack](https://docs.aws.amazon.com/AmazonS3/latest/userguide/dual-stack-endpoints.html) allows a client to access an S3 bucket through a dual-stack endpoint. +When clients request a dual-stack endpoint, the bucket URL resolves to an IPv6 address if possible, otherwise fallback to IPv4. + +To use S3 Dual-stack, we need to set `s3.dualstack-enabled` catalog property to `true` to enable `S3FileIO` to make dual-stack S3 calls. + +For example, to use S3 Dual-stack with Spark 3.5, you can start the Spark SQL shell with: +``` +spark-sql --conf spark.sql.catalog.my_catalog=org.apache.iceberg.spark.SparkCatalog \ + --conf spark.sql.catalog.my_catalog.warehouse=s3://my-bucket2/my/key/prefix \ + --conf spark.sql.catalog.my_catalog.type=glue \ + --conf spark.sql.catalog.my_catalog.io-impl=org.apache.iceberg.aws.s3.S3FileIO \ + --conf spark.sql.catalog.my_catalog.s3.dualstack-enabled=true +``` + +For more details on using S3 Dual-stack, please refer [Using dual-stack endpoints from the AWS CLI and the AWS SDKs](https://docs.aws.amazon.com/AmazonS3/latest/userguide/dual-stack-endpoints.html#dual-stack-endpoints-cli) + +## AWS Client Customization + +Many organizations have customized their way of configuring AWS clients with their own credential provider, access proxy, retry strategy, etc. +Iceberg allows users to plug in their own implementation of `org.apache.iceberg.aws.AwsClientFactory` by setting the `client.factory` catalog property. + +### Cross-Account and Cross-Region Access + +It is a common use case for organizations to have a centralized AWS account for Glue metastore and S3 buckets, and use different AWS accounts and regions for different teams to access those resources. +In this case, a [cross-account IAM role](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use.html) is needed to access those centralized resources. +Iceberg provides an AWS client factory `AssumeRoleAwsClientFactory` to support this common use case. +This also serves as an example for users who would like to implement their own AWS client factory. + +This client factory has the following configurable catalog properties: + +| Property | Default | Description | +| --------------------------------- | ---------------------------------------- | ------------------------------------------------------ | +| client.assume-role.arn | null, requires user input | ARN of the role to assume, e.g. arn:aws:iam::123456789:role/myRoleToAssume | +| client.assume-role.region | null, requires user input | All AWS clients except the STS client will use the given region instead of the default region chain | +| client.assume-role.external-id | null | An optional [external ID](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_create_for-user_externalid.html) | +| client.assume-role.timeout-sec | 1 hour | Timeout of each assume role session. At the end of the timeout, a new set of role session credentials will be fetched through an STS client. | + +By using this client factory, an STS client is initialized with the default credential and region to assume the specified role. +The Glue, S3 and DynamoDB clients are then initialized with the assume-role credential and region to access resources. +Here is an example to start Spark shell with this client factory: + +```shell +spark-sql --packages org.apache.iceberg:iceberg-spark-runtime-3.4_2.12:{{ icebergVersion }},org.apache.iceberg:iceberg-aws-bundle:{{ icebergVersion }} \ + --conf spark.sql.catalog.my_catalog=org.apache.iceberg.spark.SparkCatalog \ + --conf spark.sql.catalog.my_catalog.warehouse=s3://my-bucket/my/key/prefix \ + --conf spark.sql.catalog.my_catalog.type=glue \ + --conf spark.sql.catalog.my_catalog.client.factory=org.apache.iceberg.aws.AssumeRoleAwsClientFactory \ + --conf spark.sql.catalog.my_catalog.client.assume-role.arn=arn:aws:iam::123456789:role/myRoleToAssume \ + --conf spark.sql.catalog.my_catalog.client.assume-role.region=ap-northeast-1 +``` + +### HTTP Client Configurations +AWS clients support two types of HTTP Client, [URL Connection HTTP Client](https://mvnrepository.com/artifact/software.amazon.awssdk/url-connection-client) +and [Apache HTTP Client](https://mvnrepository.com/artifact/software.amazon.awssdk/apache-client). +By default, AWS clients use **Apache** HTTP Client to communicate with the service. +This HTTP client supports various functionalities and customized settings, such as expect-continue handshake and TCP KeepAlive, at the cost of extra dependency and additional startup latency. +In contrast, URL Connection HTTP Client optimizes for minimum dependencies and startup latency but supports less functionality than other implementations. + +For more details of configuration, see sections [URL Connection HTTP Client Configurations](#url-connection-http-client-configurations) and [Apache HTTP Client Configurations](#apache-http-client-configurations). + +Configurations for the HTTP client can be set via catalog properties. Below is an overview of available configurations: + +| Property | Default | Description | +|---------------------------------------------------|---------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| http-client.type | apache | Types of HTTP Client.
`urlconnection`: URL Connection HTTP Client
`apache`: Apache HTTP Client | +| http-client.proxy-endpoint | null | An optional proxy endpoint to use for the HTTP client. | +| http-client.proxy-use-system-property-values | null, enabled by default | An optional `true/false` setting that controls whether proxy configuration is read from Java system properties (`http.proxyHost`, `http.proxyPort`, `http.nonProxyHosts`, etc.). | +| http-client.proxy-use-environment-variable-values | null, enabled by default | An optional `true/false` setting that controls whether proxy configuration is read from environment variables (`HTTP_PROXY`, `HTTPS_PROXY`, `NO_PROXY`, etc.). | + +#### URL Connection HTTP Client Configurations + +URL Connection HTTP Client has the following configurable properties: + +| Property | Default | Description | +|-------------------------------------------------|---------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| http-client.urlconnection.socket-timeout-ms | null | An optional [socket timeout](https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/http/urlconnection/UrlConnectionHttpClient.Builder.html#socketTimeout(java.time.Duration)) in milliseconds | +| http-client.urlconnection.connection-timeout-ms | null | An optional [connection timeout](https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/http/urlconnection/UrlConnectionHttpClient.Builder.html#connectionTimeout(java.time.Duration)) in milliseconds | + +Users can use catalog properties to override the defaults. For example, to configure the socket timeout for URL Connection HTTP Client when starting a spark shell, one can add: +```shell +--conf spark.sql.catalog.my_catalog.http-client.urlconnection.socket-timeout-ms=80 +``` + +#### Apache HTTP Client Configurations + +Apache HTTP Client has the following configurable properties: + +| Property | Default | Description | +|-------------------------------------------------------|---------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| http-client.apache.socket-timeout-ms | null | An optional [socket timeout](https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/http/apache/ApacheHttpClient.Builder.html#socketTimeout(java.time.Duration)) in milliseconds | +| http-client.apache.connection-timeout-ms | null | An optional [connection timeout](https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/http/apache/ApacheHttpClient.Builder.html#connectionTimeout(java.time.Duration)) in milliseconds | +| http-client.apache.connection-acquisition-timeout-ms | null | An optional [connection acquisition timeout](https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/http/apache/ApacheHttpClient.Builder.html#connectionAcquisitionTimeout(java.time.Duration)) in milliseconds | +| http-client.apache.connection-max-idle-time-ms | null | An optional [connection max idle timeout](https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/http/apache/ApacheHttpClient.Builder.html#connectionMaxIdleTime(java.time.Duration)) in milliseconds | +| http-client.apache.connection-time-to-live-ms | null | An optional [connection time to live](https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/http/apache/ApacheHttpClient.Builder.html#connectionTimeToLive(java.time.Duration)) in milliseconds | +| http-client.apache.expect-continue-enabled | null, disabled by default | An optional `true/false` setting that controls whether [expect continue](https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/http/apache/ApacheHttpClient.Builder.html#expectContinueEnabled(java.lang.Boolean)) is enabled | +| http-client.apache.max-connections | null | An optional [max connections](https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/http/apache/ApacheHttpClient.Builder.html#maxConnections(java.lang.Integer)) in integer | +| http-client.apache.tcp-keep-alive-enabled | null, disabled by default | An optional `true/false` setting that controls whether [tcp keep alive](https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/http/apache/ApacheHttpClient.Builder.html#tcpKeepAlive(java.lang.Boolean)) is enabled | +| http-client.apache.use-idle-connection-reaper-enabled | null, enabled by default | An optional `true/false` setting that controls whether [use idle connection reaper](https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/http/apache/ApacheHttpClient.Builder.html#useIdleConnectionReaper(java.lang.Boolean)) is used | + +Users can use catalog properties to override the defaults. For example, to configure the max connections for Apache HTTP Client when starting a spark shell, one can add: +```shell +--conf spark.sql.catalog.my_catalog.http-client.apache.max-connections=5 +``` + +## Run Iceberg on AWS + +### Amazon Athena + +[Amazon Athena](https://aws.amazon.com/athena/) provides a serverless query engine that could be used to perform read, write, update and optimization tasks against Iceberg tables. +More details could be found [here](https://docs.aws.amazon.com/athena/latest/ug/querying-iceberg.html). + +### Amazon EMR + +[Amazon EMR](https://aws.amazon.com/emr/) can provision clusters with [Spark](https://docs.aws.amazon.com/emr/latest/ReleaseGuide/emr-spark.html) (EMR 6 for Spark 3, EMR 5 for Spark 2), +[Hive](https://docs.aws.amazon.com/emr/latest/ReleaseGuide/emr-hive.html), [Flink](https://docs.aws.amazon.com/emr/latest/ReleaseGuide/emr-flink.html), +[Trino](https://docs.aws.amazon.com/emr/latest/ReleaseGuide/emr-presto.html) that can run Iceberg. + +Starting with EMR version 6.5.0, EMR clusters can be configured to have the necessary Apache Iceberg dependencies installed without requiring bootstrap actions. +Please refer to the [official documentation](https://docs.aws.amazon.com/emr/latest/ReleaseGuide/emr-iceberg-use-cluster.html) on how to create a cluster with Iceberg installed. + +For versions before 6.5.0, you can use a [bootstrap action](https://docs.aws.amazon.com/emr/latest/ManagementGuide/emr-plan-bootstrap.html) similar to the following to pre-install all necessary dependencies: +```sh +#!/bin/bash + +ICEBERG_VERSION={{ icebergVersion }} +MAVEN_URL=https://repo1.maven.org/maven2 +ICEBERG_MAVEN_URL=$MAVEN_URL/org/apache/iceberg +# NOTE: this is just an example shared class path between Spark and Flink, +# please choose a proper class path for production. +LIB_PATH=/usr/share/aws/aws-java-sdk/ + + +ICEBERG_PACKAGES=( + "iceberg-spark-runtime-3.5_2.12" + "iceberg-flink-runtime" + "iceberg-aws-bundle" +) + +install_dependencies () { + install_path=$1 + download_url=$2 + version=$3 + shift + pkgs=("$@") + for pkg in "${pkgs[@]}"; do + sudo wget -P $install_path $download_url/$pkg/$version/$pkg-$version.jar + done +} + +install_dependencies $LIB_PATH $ICEBERG_MAVEN_URL $ICEBERG_VERSION "${ICEBERG_PACKAGES[@]}" +``` + +### AWS Glue + +[AWS Glue](https://aws.amazon.com/glue/) provides a serverless data integration service +that could be used to perform read, write and update tasks against Iceberg tables. +More details could be found [here](https://docs.aws.amazon.com/glue/latest/dg/aws-glue-programming-etl-format-iceberg.html). + +### AWS EKS + +[AWS Elastic Kubernetes Service (EKS)](https://aws.amazon.com/eks/) can be used to start any Spark, Flink, Hive, Presto or Trino clusters to work with Iceberg. + +### Amazon Kinesis + +[Amazon Kinesis Data Analytics](https://aws.amazon.com/about-aws/whats-new/2019/11/you-can-now-run-fully-managed-apache-flink-applications-with-apache-kafka/) provides a platform +to run fully managed Apache Flink applications. You can include Iceberg in your application Jar and run it in the platform. + +### AWS Redshift +[AWS Redshift Spectrum or Redshift Serverless](https://docs.aws.amazon.com/redshift/latest/dg/querying-iceberg.html) supports querying Apache Iceberg tables cataloged in the AWS Glue Data Catalog. + +### Amazon Data Firehose +You can use [Firehose](https://docs.aws.amazon.com/firehose/latest/dev/apache-iceberg-destination.html) to directly deliver streaming data to Apache Iceberg Tables in Amazon S3. With this feature, you can route records from a single stream into different Apache Iceberg Tables, and automatically apply insert, update, and delete operations to records in the Apache Iceberg Tables. This feature requires using the AWS Glue Data Catalog. diff --git a/1.11.0/docs/branching.md b/1.11.0/docs/branching.md new file mode 100644 index 000000000000..77c16316d474 --- /dev/null +++ b/1.11.0/docs/branching.md @@ -0,0 +1,205 @@ +--- +title: "Branching and Tagging" +--- + + + +# Branching and Tagging + +## Overview + +Iceberg table metadata maintains a snapshot log, which represents the changes applied to a table. +Snapshots are fundamental in Iceberg as they are the basis for reader isolation and time travel queries. +For controlling metadata size and storage costs, Iceberg provides snapshot lifecycle management procedures such as [`expire_snapshots`](spark-procedures.md#expire-snapshots) for removing unused snapshots and no longer necessary data files based on table snapshot retention properties. + +**For more sophisticated snapshot lifecycle management, Iceberg supports branches and tags which are named references to snapshots with their own independent lifecycles. This lifecycle is controlled by branch and tag level retention policies.** +Branches are independent lineages of snapshots and point to the head of the lineage. +Branches and tags have a maximum reference age property which control when the reference to the snapshot itself should be expired. +Branches have retention properties which define the minimum number of snapshots to retain on a branch as well as the maximum age of individual snapshots to retain on the branch. +These properties are used when the expireSnapshots procedure is run. +For details on the algorithm for expireSnapshots, refer to the [spec](../../spec.md#snapshot-retention-policy). + +## Use Cases + +Branching and tagging can be used for handling GDPR requirements and retaining important historical snapshots for auditing. +Branches can also be used as part of data engineering workflows, for enabling experimental branches for testing and validating new jobs. +See below for some examples of how branching and tagging can facilitate these use cases. + +### Historical Tags + +Tags can be used for retaining important historical snapshots for auditing purposes. + +![Historical Tags](assets/images/historical-snapshot-tag.png) + +The above diagram demonstrates retaining important historical snapshot with the following retention policy, defined +via Spark SQL. + +1. Retain 1 snapshot per week for 1 month. This can be achieved by tagging the weekly snapshot and setting the tag retention to be a month. +snapshots will be kept, and the branch reference itself will be retained for 1 week. +```sql +-- Create a tag for the first end of week snapshot. Retain the snapshot for a week +ALTER TABLE prod.db.table CREATE TAG `EOW-01` AS OF VERSION 7 RETAIN 7 DAYS; +``` + +2. Retain 1 snapshot per month for 6 months. This can be achieved by tagging the monthly snapshot and setting the tag retention to be 6 months. +```sql +-- Create a tag for the first end of month snapshot. Retain the snapshot for 6 months +ALTER TABLE prod.db.table CREATE TAG `EOM-01` AS OF VERSION 30 RETAIN 180 DAYS; +``` + +3. Retain 1 snapshot per year forever. This can be achieved by tagging the annual snapshot. The default retention for branches and tags is forever. +```sql +-- Create a tag for the end of the year and retain it forever. +ALTER TABLE prod.db.table CREATE TAG `EOY-2023` AS OF VERSION 365; +``` + +4. Create a temporary "test-branch" which is retained for 7 days and the latest 2 snapshots on the branch are retained. +```sql +-- Create a branch "test-branch" which will be retained for 7 days along with the latest 2 snapshots +ALTER TABLE prod.db.table CREATE BRANCH `test-branch` RETAIN 7 DAYS WITH SNAPSHOT RETENTION 2 SNAPSHOTS; +``` + +### Audit Branch + +![Audit Branch](assets/images/audit-branch.png) + +The above diagram shows an example of using an audit branch for validating a write workflow. + +1. First ensure `write.wap.enabled` is set. +```sql +ALTER TABLE db.table SET TBLPROPERTIES ( + 'write.wap.enabled'='true' +); +``` +2. Create `audit-branch` starting from snapshot 3, which will be written to and retained for 1 week. +```sql +ALTER TABLE db.table CREATE BRANCH `audit-branch` AS OF VERSION 3 RETAIN 7 DAYS; +``` +3. Writes are performed on a separate `audit-branch` independent from the main table history. +```sql +-- WAP Branch write +SET spark.wap.branch = audit-branch +INSERT INTO prod.db.table VALUES (3, 'c'); +``` +4. A validation workflow can validate (e.g. data quality) the state of `audit-branch`. +5. After validation, the main branch can be `fastForward` to the head of `audit-branch` to update the main table state. +```sql +CALL catalog_name.system.fast_forward('prod.db.table', 'main', 'audit-branch'); +``` +6. The branch reference will be removed when `expireSnapshots` is run 1 week later. + +## Usage + +Creating, querying and writing to branches and tags are supported in the Iceberg Java library, and in Spark and Flink engine integrations. + +- [Iceberg Java Library](java-api-quickstart.md#branching-and-tagging) +- [Spark DDLs](spark-ddl.md#branching-and-tagging-ddl) +- [Spark Reads](spark-queries.md#time-travel) +- [Spark Branch Writes](spark-writes.md#writing-to-branches) +- [Flink Reads](flink-queries.md#reading-branches-and-tags-with-SQL) +- [Flink Branch Writes](flink-writes.md#branch-writes) + +## Schema selection with branches and tags + +It is important to understand that the schema tracked for a table is valid across all branches. +When working with branches, the table's schema is used as that's the schema being validated when writing data to a branch. +On the other hands, querying a tag uses the snapshot's schema, which is the schema id that snapshot pointed to when the snapshot was created. + +The below examples show which schema is being used when working with branches. + +Create a table and insert some data: + +```sql +CREATE TABLE db.table (id bigint, data string, col float); +INSERT INTO db.table VALUES (1, 'a', 1.0), (2, 'b', 2.0), (3, 'c', 3.0); +SELECT * FROM db.table; +1 a 1.0 +2 b 2.0 +3 c 3.0 +``` + +Create a branch `test_branch` that points to the current snapshot and read data from the branch: + +```sql +ALTER TABLE db.table CREATE BRANCH test_branch; + +SELECT * FROM db.table.branch_test_branch; +1 a 1.0 +2 b 2.0 +3 c 3.0 +``` + +Modify the table's schema by dropping the `col` column and adding a new column named `new_col`: + +```sql +ALTER TABLE db.table DROP COLUMN col; + +ALTER TABLE db.table ADD COLUMN new_col date; + +INSERT INTO db.table VALUES (4, 'd', date('2024-04-04')), (5, 'e', date('2024-05-05')); + +SELECT * FROM db.table; +1 a NULL +2 b NULL +3 c NULL +4 d 2024-04-04 +5 e 2024-05-05 +``` + +Querying the head of the branch using one of the below statements will return data using the **table's schema**: + +```sql +SELECT * FROM db.table.branch_test_branch; +1 a NULL +2 b NULL +3 c NULL + +SELECT * FROM db.table VERSION AS OF 'test_branch'; +1 a NULL +2 b NULL +3 c NULL +``` + +Performing a time travel query using the snapshot id uses the **snapshot's schema**: + +```sql + +SELECT * FROM db.table.refs; +test_branch BRANCH 8109744798576441359 NULL NULL NULL +main BRANCH 6910357365743665710 NULL NULL NULL + + +SELECT * FROM db.table VERSION AS OF 8109744798576441359; +1 a 1.0 +2 b 2.0 +3 c 3.0 +``` + +When writing to the branch, the **table's schema** is used for validation: + +```sql + +INSERT INTO db.table.branch_test_branch VALUES (6, 'e', date('2024-06-06')), (7, 'g', date('2024-07-07')); + +SELECT * FROM db.table.branch_test_branch; +6 e 2024-06-06 +7 g 2024-07-07 +1 a NULL +2 b NULL +3 c NULL +``` diff --git a/1.11.0/docs/catalog-properties.md b/1.11.0/docs/catalog-properties.md new file mode 100644 index 000000000000..5afae0b98ae2 --- /dev/null +++ b/1.11.0/docs/catalog-properties.md @@ -0,0 +1,167 @@ +--- +title: "Catalog properties" +--- + + +# Catalog properties + +## Common properties + +Iceberg catalogs support using catalog properties to configure catalog behaviors. Here is a list of commonly used catalog properties: + +| Property | Default | Description | +| --------------------------------- | ------------------ | ------------------------------------------------------ | +| catalog-impl | null | a custom `Catalog` implementation to use by an engine | +| io-impl | null | a custom `FileIO` implementation to use in a catalog | +| warehouse | null | the root path of the data warehouse | +| uri | null | a URI string, such as Hive metastore URI | +| clients | 2 | client pool size | +| cache-enabled | true | Whether to cache catalog entries | +| cache.expiration-interval-ms | 30000 | How long catalog entries are locally cached, in milliseconds; 0 disables caching, negative values disable expiration | +| metrics-reporter-impl | org.apache.iceberg.metrics.LoggingMetricsReporter | Custom `MetricsReporter` implementation to use in a catalog. See the [Metrics reporting](metrics-reporting.md) section for additional details | +| unique-table-location | false | Whether to use a unique location for new tables | +| encryption.kms-impl | null | a custom `KeyManagementClient` implementation to use in a catalog for interactions with KMS (key management service). See the [Encryption](encryption.md) document for additional details | + +`HadoopCatalog` and `HiveCatalog` can access the properties in their constructors. +Any other custom catalog can access the properties by implementing `Catalog.initialize(catalogName, catalogProperties)`. +The properties can be manually constructed or passed in from a compute engine like Spark or Flink. +Spark uses its session properties as catalog properties, see more details in the [Spark configuration](spark-configuration.md#catalog-configuration) section. +Flink passes in catalog properties through `CREATE CATALOG` statement, see more details in the [Flink](flink.md#adding-catalogs) section. + +## REST catalog properties + +The following properties configure the behavior of the REST catalog client. + +| Property | Default | Description | +|---------------------------------------|-------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `snapshot-loading-mode` | `ALL` | Controls how snapshots are loaded from the REST server. Supported values: `ALL` (load all snapshots), `REFS` (load only referenced snapshots). | +| `rest-metrics-reporting-enabled` | `true` | Whether to enable metrics reporting to the REST server. | +| `view-endpoints-supported` | `false` | For backwards compatibility with older REST servers. Set to `true` if the server supports view endpoints but doesn't send the `endpoints` field in the ConfigResponse. | +| `rest-page-size` | null | The page size to use when listing namespaces, tables, or other paginated resources. | +| `namespace-separator` | `%1F` | The separator character used for namespace levels when communicating with the REST server. | +| `scan-planning-mode` | `CLIENT` | Controls where scan planning is performed. Supported values: `CLIENT` (client-side planning), `SERVER` (server-side planning). Can be overridden per-table by the server in LoadTableResponse. | + +### Table cache properties + +The following properties configure the table cache used for freshness-aware table loading. Note, this cache is different from the one that can be configured at catalog level in general. + +| Property | Default | Description | +|------------------------------------------|-------------------|----------------------------------------------------------------------------------------| +| `rest-table-cache.expire-after-write-ms` | `300000` (5 min) | Time in milliseconds after which cached table entries expire. | +| `rest-table-cache.max-entries` | `100` | Maximum number of table entries to cache. | + +### Auth properties + +The following catalog properties configure authentication for the REST catalog. +They support Basic, OAuth2, SigV4, and Google authentication. + +#### REST auth properties + +| Property | Default | Description | +|--------------------------------------|------------------|-------------------------------------------------------------------------------------------------------------------| +| `rest.auth.type` | `none` | Authentication mechanism for REST catalog access. Supported values: `none`, `basic`, `oauth2`, `sigv4`, `google`. | +| `rest.auth.basic.username` | null | Username for Basic authentication. Required if `rest.auth.type` = `basic`. | +| `rest.auth.basic.password` | null | Password for Basic authentication. Required if `rest.auth.type` = `basic`. | +| `rest.auth.sigv4.delegate-auth-type` | `oauth2` | Auth type to delegate to after `sigv4` signing. | + +#### OAuth2 auth properties +Required and optional properties to include while using `oauth2` authentication + +| Property | Default | Description | +|-------------------------|-------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `token` | null | A Bearer token to interact with the server. Either `token` or `credential` is required. | +| `credential` | null | Credential string in the form of `client_id:client_secret` to exchange for a token in the OAuth2 client credentials flow. Either `token` or `credential` is required. | +| `oauth2-server-uri` | `v1/oauth/tokens` | OAuth2 token endpoint URI. Required if the REST catalog is not the OAuth2 authentication server. | +| `token-expires-in-ms` | 3600000 (1 hour) | Time in milliseconds after which a bearer token is considered expired. Used to decide when to refresh or re-exchange a token. | +| `token-refresh-enabled` | true | Determines whether tokens are automatically refreshed when expiration details are available. | +| `token-exchange-enabled`| true | Determines whether to use the token exchange flow to acquire new tokens. Disabling this will allow fallback to the client credential flow. | +| `scope` | `catalog` | Additional scope for `oauth2`. | +| `audience` | null | Optional param to specify token `audience` | +| `resource` | null | Optional param to specify `resource` | + +#### Google auth properties +Required and optional properties to include while using `google` authentication + +| Property | Default | Description | +|----------------------------|--------------------------------------------------|--------------------------------------------------| +| `gcp.auth.credentials-path`| Application Default Credentials (ADC) | Path to a service account JSON key file. | +| `gcp.auth.credentials-json` | Application Default Credentials (ADC) | JSON string of a service account credential. | +| `gcp.auth.scopes` | `https://www.googleapis.com/auth/cloud-platform` | Comma-separated list of OAuth scopes to request. | + +## Lock catalog properties + +Here are the catalog properties related to locking. They are used by some catalog implementations to control the locking behavior during commits. + +| Property | Default | Description | +| --------------------------------- | ------------------ | ------------------------------------------------------ | +| lock-impl | null | a custom implementation of the lock manager, the actual interface depends on the catalog used | +| lock.table | null | an auxiliary table for locking, such as in [AWS DynamoDB lock manager](aws.md#dynamodb-lock-manager) | +| lock.acquire-interval-ms | 5000 (5 s) | the interval to wait between each attempt to acquire a lock | +| lock.acquire-timeout-ms | 180000 (3 min) | the maximum time to try acquiring a lock | +| lock.heartbeat-interval-ms | 3000 (3 s) | the interval to wait between each heartbeat after acquiring a lock | +| lock.heartbeat-timeout-ms | 15000 (15 s) | the maximum time without a heartbeat to consider a lock expired | + +## Hadoop configuration + +### HadoopTables Lock Configuration + +When using `HadoopTables` (tables without a catalog), lock properties from the [Lock catalog properties](#lock-catalog-properties) section can be configured by prefixing them with `iceberg.tables.hadoop.`. This ensures atomic commits on file systems like S3 that lack native write mutual exclusion. + +!!! info + To use DynamoDB as a lock manager with `HadoopTables`, set `iceberg.tables.hadoop.lock-impl` to `org.apache.iceberg.aws.dynamodb.DynamoDbLockManager` and `iceberg.tables.hadoop.lock.table` to your DynamoDB table name. See [DynamoDB Lock Manager](aws.md#dynamodb-lock-manager) for more details. + +### Hive Metastore Configuration + +The following properties from the Hadoop configuration are used by the Hive Metastore connector. +The HMS table locking is a 2-step process: + +1. Lock Creation: Create lock in HMS and queue for acquisition +2. Lock Check: Check if lock successfully acquired + +| Property | Default | Description | +|-------------------------------------------|-----------------|------------------------------------------------------------------------------| +| iceberg.hive.client-pool-size | 5 | The size of the Hive client pool when tracking tables in HMS | +| iceberg.hive.lock-creation-timeout-ms | 180000 (3 min) | Maximum time in milliseconds to create a lock in the HMS | +| iceberg.hive.lock-creation-min-wait-ms | 50 | Minimum time in milliseconds between retries of creating the lock in the HMS | +| iceberg.hive.lock-creation-max-wait-ms | 5000 | Maximum time in milliseconds between retries of creating the lock in the HMS | +| iceberg.hive.lock-timeout-ms | 180000 (3 min) | Maximum time in milliseconds to acquire a lock | +| iceberg.hive.lock-check-min-wait-ms | 50 | Minimum time in milliseconds between checking the acquisition of the lock | +| iceberg.hive.lock-check-max-wait-ms | 5000 | Maximum time in milliseconds between checking the acquisition of the lock | +| iceberg.hive.lock-heartbeat-interval-ms | 240000 (4 min) | The heartbeat interval for the HMS locks. | +| iceberg.hive.metadata-refresh-max-retries | 2 | Maximum number of retries when the metadata file is missing | +| iceberg.hive.table-level-lock-evict-ms | 600000 (10 min) | The timeout for the JVM table lock is | +| iceberg.engine.hive.lock-enabled | true | Use HMS locks to ensure atomicity of commits | + +Note: `iceberg.hive.lock-check-max-wait-ms` and `iceberg.hive.lock-heartbeat-interval-ms` should be less than the [transaction timeout](https://cwiki.apache.org/confluence/display/Hive/Configuration+Properties#ConfigurationProperties-hive.txn.timeout) +of the Hive Metastore (`hive.txn.timeout` or `metastore.txn.timeout` in the newer versions). Otherwise, the heartbeats on the lock (which happens during the lock checks) would end up expiring in the +Hive Metastore before the lock is retried from Iceberg. + +Warn: Setting `iceberg.engine.hive.lock-enabled`=`false` will cause HiveCatalog to commit to tables without using Hive locks. +This should only be set to `false` if all following conditions are met: + +- [HIVE-26882](https://issues.apache.org/jira/browse/HIVE-26882) +is available on the Hive Metastore server +- [HIVE-28121](https://issues.apache.org/jira/browse/HIVE-28121) +is available on the Hive Metastore server, if it is backed by MySQL or MariaDB +- All other HiveCatalogs committing to tables that this HiveCatalog commits to are also on Iceberg 1.3 or later +- All other HiveCatalogs committing to tables that this HiveCatalog commits to have also disabled Hive locks on commit. + +**Failing to ensure these conditions risks corrupting the table.** + +Even with `iceberg.engine.hive.lock-enabled` set to `false`, a HiveCatalog can still use locks for individual tables by setting the table property `engine.hive.lock-enabled`=`true`. +This is useful in the case where other HiveCatalogs cannot be upgraded and set to commit without using Hive locks. diff --git a/1.11.0/docs/configuration.md b/1.11.0/docs/configuration.md new file mode 100644 index 000000000000..17bf1f8ac0a1 --- /dev/null +++ b/1.11.0/docs/configuration.md @@ -0,0 +1,145 @@ +--- +title: "Configuration" +--- + + +# Configuration + +## Table properties + +Iceberg tables support table properties to configure table behavior, like the default split size for readers. + +### Read properties + +| Property | Default | Description | +| --------------------------------- | ------------------ | ------------------------------------------------------ | +| read.split.target-size | 134217728 (128 MB) | Target size when combining data input splits | +| read.split.metadata-target-size | 33554432 (32 MB) | Target size when combining metadata input splits | +| read.split.planning-lookback | 10 | Number of bins to consider when combining input splits | +| read.split.open-file-cost | 4194304 (4 MB) | The estimated cost to open a file, used as a minimum weight when combining splits. | +| read.parquet.vectorization.enabled| true | Controls whether Parquet vectorized reads are used | +| read.parquet.vectorization.batch-size| 5000 | The batch size for parquet vectorized reads | +| read.orc.vectorization.enabled | false | Controls whether orc vectorized reads are used | +| read.orc.vectorization.batch-size | 5000 | The batch size for orc vectorized reads | + +### Write properties + +| Property | Default | Description | +|-----------------------------------------------------|-----------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| write.format.default | parquet | Default file format for the table; parquet, avro, or orc | +| write.delete.format.default | data file format | Default delete file format for the table; parquet, avro, or orc | +| write.parquet.row-group-size-bytes | 134217728 (128 MB) | Parquet row group size | +| write.parquet.page-size-bytes | 1048576 (1 MB) | Parquet page size | +| write.parquet.page-version | v1 | Parquet data page version: v1 (DataPage V1) or v2 (DataPage V2) | +| write.parquet.page-row-limit | 20000 | Parquet page row limit | +| write.parquet.dict-size-bytes | 2097152 (2 MB) | Parquet dictionary page size | +| write.parquet.compression-codec | zstd | Parquet compression codec: zstd, brotli, lz4, gzip, snappy, uncompressed | +| write.parquet.compression-level | null | Parquet compression level | +| write.parquet.shred-variants | false | When true, variant columns are written with shredded Parquet encoding for improved query performance | +| write.parquet.variant-inference-buffer-size | 100 | Number of rows to buffer for schema inference when variant shredding is enabled | +| write.parquet.bloom-filter-enabled.column.col1 | (not set) | Hint to parquet to write a bloom filter for the column: 'col1' | +| write.parquet.bloom-filter-max-bytes | 1048576 (1 MB) | The maximum number of bytes for a bloom filter bitset | +| write.parquet.bloom-filter-fpp.column.col1 | 0.01 | The false positive probability for a bloom filter applied to 'col1' (must > 0.0 and < 1.0) | +| write.parquet.bloom-filter-ndv.column.col1 | (not set) | The expected number of distinct values for a bloom filter applied to 'col1' (must > 0) | +| write.parquet.stats-enabled.column.col1 | (not set) | Controls whether to collect parquet column statistics for column 'col1' | +| write.avro.compression-codec | gzip | Avro compression codec: gzip(deflate with 9 level), zstd, snappy, uncompressed | +| write.avro.compression-level | null | Avro compression level | +| write.orc.stripe-size-bytes | 67108864 (64 MB) | Define the default ORC stripe size, in bytes | +| write.orc.block-size-bytes | 268435456 (256 MB) | Define the default file system block size for ORC files | +| write.orc.compression-codec | zlib | ORC compression codec: zstd, lz4, lzo, zlib, snappy, none | +| write.orc.compression-strategy | speed | ORC compression strategy: speed, compression | +| write.orc.bloom.filter.columns | (not set) | Comma separated list of column names for which a Bloom filter must be created | +| write.orc.bloom.filter.fpp | 0.05 | False positive probability for Bloom filter (must > 0.0 and < 1.0) | +| write.location-provider.impl | null | Optional custom implementation for LocationProvider | +| write.metadata.compression-codec | none | Metadata compression codec; none or gzip | +| write.metadata.metrics.max-inferred-column-defaults | 100 | Defines the maximum number of columns for which metrics are collected. Columns are included with a pre-order traversal of the schema: top level fields first; then all elements of the first nested struct; then the next nested struct and so on. | +| write.metadata.metrics.default | truncate(16) | Default metrics mode for all columns in the table; none, counts, truncate(length), or full | +| write.metadata.metrics.column.col1 | (not set) | Metrics mode for column 'col1' to allow per-column tuning; none, counts, truncate(length), or full | +| write.target-file-size-bytes | 536870912 (512 MB) | Controls the size of files generated to target about this many bytes | +| write.delete.target-file-size-bytes | 67108864 (64 MB) | Controls the size of delete files generated to target about this many bytes | +| write.distribution-mode | not set, see engines for specific defaults, for example [Spark Writes](spark-writes.md#writing-distribution-modes) | Defines distribution of write data: __none__: don't shuffle rows; __hash__: hash distribute by partition key ; __range__: range distribute by partition key or sort key if table has an SortOrder | +| write.delete.distribution-mode | (not set) | Defines distribution of write delete data | +| write.update.distribution-mode | (not set) | Defines distribution of write update data | +| write.merge.distribution-mode | (not set) | Defines distribution of write merge data | +| write.wap.enabled | false | Enables write-audit-publish writes | +| write.summary.partition-limit | 0 | Includes partition-level summary stats in snapshot summaries if the changed partition count is less than this limit | +| write.metadata.delete-after-commit.enabled | false | Controls whether to delete the oldest **tracked** version metadata files after each table commit. See the [Remove old metadata files](maintenance.md#remove-old-metadata-files) section for additional details | +| write.metadata.previous-versions-max | 100 | The max number of previous version metadata files to track | +| write.spark.fanout.enabled | false | Enables the fanout writer in Spark that does not require data to be clustered; uses more memory | +| write.object-storage.enabled | false | Enables the object storage location provider that adds a hash component to file paths | +| write.object-storage.partitioned-paths | true | Includes the partition values in the file path | +| write.data.path | table location + /data | Base location for data files | +| write.metadata.path | table location + /metadata | Base location for metadata files | +| write.delete.mode | copy-on-write | Mode used for delete commands: copy-on-write or merge-on-read (v2 and above) | +| write.delete.isolation-level | serializable | Isolation level for delete commands: serializable or snapshot | +| write.update.mode | copy-on-write | Mode used for update commands: copy-on-write or merge-on-read (v2 and above) | +| write.update.isolation-level | serializable | Isolation level for update commands: serializable or snapshot | +| write.merge.mode | copy-on-write | Mode used for merge commands: copy-on-write or merge-on-read (v2 and above) | +| write.merge.isolation-level | serializable | Isolation level for merge commands: serializable or snapshot | +| write.delete.granularity | partition | Controls the granularity of generated delete files: partition or file | + +### Encryption properties + +| Property | Default | Description | +| --------------------------------- | ------------------ | ------------------------------------------------------------------------------------- | +| encryption.key-id | (not set) | ID of the master key of the table | +| encryption.data-key-length | 16 (bytes) | Length of keys used for encryption of table files. Valid values are 16, 24, 32 bytes | + +See the [Encryption](encryption.md) document for additional details. + +### Table behavior properties + +| Property | Default | Description | +| ---------------------------------- | ---------------- | ------------------------------------------------------------- | +| commit.retry.num-retries | 4 | Number of times to retry a commit before failing | +| commit.retry.min-wait-ms | 100 | Minimum time in milliseconds to wait before retrying a commit | +| commit.retry.max-wait-ms | 60000 (1 min) | Maximum time in milliseconds to wait before retrying a commit | +| commit.retry.total-timeout-ms | 1800000 (30 min) | Total retry timeout period in milliseconds for a commit | +| commit.status-check.num-retries | 3 | Number of times to check whether a commit succeeded after a connection is lost before failing due to an unknown commit state | +| commit.status-check.min-wait-ms | 1000 (1s) | Minimum time in milliseconds to wait before retrying a status-check | +| commit.status-check.max-wait-ms | 60000 (1 min) | Maximum time in milliseconds to wait before retrying a status-check | +| commit.status-check.total-timeout-ms| 1800000 (30 min) | Total timeout period in which the commit status-check must succeed, in milliseconds | +| commit.manifest.target-size-bytes | 8388608 (8 MB) | Target size when merging manifest files | +| commit.manifest.min-count-to-merge | 100 | Minimum number of manifests to accumulate before merging | +| commit.manifest-merge.enabled | true | Controls whether to automatically merge manifests on writes | +| history.expire.max-snapshot-age-ms | 432000000 (5 days) | Default max age of snapshots to keep on the table and all of its branches while expiring snapshots | +| history.expire.min-snapshots-to-keep | 1 | Default min number of snapshots to keep on the table and all of its branches while expiring snapshots | +| history.expire.max-ref-age-ms | `Long.MAX_VALUE` (forever) | For snapshot references except the `main` branch, default max age of snapshot references to keep while expiring snapshots. The `main` branch never expires. | +| gc.enabled | true | Allows garbage collection operations such as expiring snapshots and removing orphan files | + +### Reserved table properties +Reserved table properties are only used to control behaviors when creating or updating a table. +The value of these properties are not persisted as a part of the table metadata. + +| Property | Default | Description | +| -------------- | -------- |--------------------------------------------------------------------------------------------------------------------------------------| +| format-version | 2 | Table's format version as defined in the [Spec](../../spec.md#format-versioning). Defaults to 2 since version 1.4.0. | + +### Informational properties + +Informational properties can be set to provide additional context about a table. They can be useful for documentation, discovery, and integration with external tools. They do not affect read/write behavior or query semantics. + +| Property | Default | Description | +| -------- | ---------- | ------------------------------------------------------------------------------------------------------------------- | +| comment | (not set) | A table-level description that documents the business meaning and usage context. | + +### Compatibility flags + +| Property | Default | Description | +| --------------------------------------------- | -------- | ------------------------------------------------------------- | +| compatibility.snapshot-id-inheritance.enabled | false | Enables committing snapshots without explicit snapshot IDs (always true if the format version is > 1) | diff --git a/1.11.0/docs/custom-catalog.md b/1.11.0/docs/custom-catalog.md new file mode 100644 index 000000000000..d30a629401aa --- /dev/null +++ b/1.11.0/docs/custom-catalog.md @@ -0,0 +1,270 @@ +--- +title: "Java Custom Catalog" +--- + + +# Custom Catalog + +It's possible to read an iceberg table either from an hdfs path or from a hive table. It's also possible to use a custom metastore in place of hive. The steps to do that are as follows. + +- [Custom TableOperations](#custom-table-operations-implementation) +- [Custom Catalog](#custom-catalog-implementation) +- [Custom FileIO](#custom-file-io-implementation) +- [Custom LocationProvider](#custom-location-provider-implementation) +- [Custom IcebergSource](#custom-icebergsource) + +Note: To work with encrypted tables, custom catalogs must address a number of security [requirements](encryption.md#catalog-security-requirements). + +### Custom table operations implementation +Extend `BaseMetastoreTableOperations` to provide implementation on how to read and write metadata + +Example: +```java +class CustomTableOperations extends BaseMetastoreTableOperations { + private String dbName; + private String tableName; + private Configuration conf; + private FileIO fileIO; + + protected CustomTableOperations(Configuration conf, String dbName, String tableName) { + this.conf = conf; + this.dbName = dbName; + this.tableName = tableName; + } + + // The doRefresh method should provide implementation on how to get the metadata location + @Override + public void doRefresh() { + + // Example custom service which returns the metadata location given a dbName and tableName + String metadataLocation = CustomService.getMetadataForTable(conf, dbName, tableName); + + // When updating from a metadata file location, call the helper method + refreshFromMetadataLocation(metadataLocation); + + } + + // The doCommit method should provide implementation on how to update with metadata location atomically + @Override + public void doCommit(TableMetadata base, TableMetadata metadata) { + String oldMetadataLocation = base.location(); + + // Write new metadata using helper method + String newMetadataLocation = writeNewMetadata(metadata, currentVersion() + 1); + + // Example custom service which updates the metadata location for the given db and table atomically + CustomService.updateMetadataLocation(dbName, tableName, oldMetadataLocation, newMetadataLocation); + + } + + // The io method provides a FileIO which is used to read and write the table metadata files + @Override + public FileIO io() { + if (fileIO == null) { + fileIO = new HadoopFileIO(conf); + } + return fileIO; + } +} +``` + +A `TableOperations` instance is usually obtained by calling `Catalog.newTableOps(TableIdentifier)`. +See the next section about implementing and loading a custom catalog. + +### Custom catalog implementation +Extend `BaseMetastoreCatalog` to provide default warehouse locations and instantiate `CustomTableOperations` + +Example: +```java +public class CustomCatalog extends BaseMetastoreCatalog { + + private Configuration configuration; + + // must have a no-arg constructor to be dynamically loaded + // initialize(String name, Map properties) will be called to complete initialization + public CustomCatalog() { + } + + public CustomCatalog(Configuration configuration) { + this.configuration = configuration; + } + + @Override + protected TableOperations newTableOps(TableIdentifier tableIdentifier) { + String dbName = tableIdentifier.namespace().level(0); + String tableName = tableIdentifier.name(); + // instantiate the CustomTableOperations + return new CustomTableOperations(configuration, dbName, tableName); + } + + @Override + protected String defaultWarehouseLocation(TableIdentifier tableIdentifier) { + + // Can choose to use any other configuration name + String tableLocation = configuration.get("custom.iceberg.warehouse.location"); + + // Can be an s3 or hdfs path + if (tableLocation == null) { + throw new RuntimeException("custom.iceberg.warehouse.location configuration not set!"); + } + + return String.format( + "%s/%s.db/%s", tableLocation, + tableIdentifier.namespace().levels()[0], + tableIdentifier.name()); + } + + @Override + public boolean dropTable(TableIdentifier identifier, boolean purge) { + // Example service to delete table + CustomService.deleteTable(identifier.namespace().level(0), identifier.name()); + } + + @Override + public void renameTable(TableIdentifier from, TableIdentifier to) { + Preconditions.checkArgument(from.namespace().level(0).equals(to.namespace().level(0)), + "Cannot move table between databases"); + // Example service to rename table + CustomService.renameTable(from.namespace().level(0), from.name(), to.name()); + } + + // implement this method to read catalog name and properties during initialization + public void initialize(String name, Map properties) { + } +} +``` + +Catalog implementations can be dynamically loaded in most compute engines. +For Spark and Flink, you can specify the `catalog-impl` catalog property to load it. +Read the [Configuration](catalog-properties.md) section for more details. +For MapReduce, implement `org.apache.iceberg.mr.CatalogLoader` and set Hadoop property `iceberg.mr.catalog.loader.class` to load it. +If your catalog must read Hadoop configuration to access certain environment properties, make your catalog implement `org.apache.hadoop.conf.Configurable`. + +### Custom file IO implementation + +Extend `FileIO` and provide implementation to read and write data files + +Example: +```java +public class CustomFileIO implements FileIO { + + // must have a no-arg constructor to be dynamically loaded + // initialize(Map properties) will be called to complete initialization + public CustomFileIO() { + } + + @Override + public InputFile newInputFile(String s) { + // you also need to implement the InputFile interface for a custom input file + return new CustomInputFile(s); + } + + @Override + public OutputFile newOutputFile(String s) { + // you also need to implement the OutputFile interface for a custom output file + return new CustomOutputFile(s); + } + + @Override + public void deleteFile(String path) { + Path toDelete = new Path(path); + FileSystem fs = Util.getFs(toDelete); + try { + fs.delete(toDelete, false /* not recursive */); + } catch (IOException e) { + throw new RuntimeIOException(e, "Failed to delete file: %s", path); + } + } + + // implement this method to read catalog properties during initialization + public void initialize(Map properties) { + } +} +``` + +If you are already implementing your own catalog, you can implement `TableOperations.io()` to use your custom `FileIO`. +In addition, custom `FileIO` implementations can also be dynamically loaded in `HadoopCatalog` and `HiveCatalog` by specifying the `io-impl` catalog property. +Read the [Configuration](catalog-properties.md) section for more details. +If your `FileIO` must read Hadoop configuration to access certain environment properties, make your `FileIO` implement `org.apache.hadoop.conf.Configurable`. + +### Custom location provider implementation + +Extend `LocationProvider` and provide implementation to determine the file path to write data + +Example: +```java +public class CustomLocationProvider implements LocationProvider { + + private String tableLocation; + + // must have a 2-arg constructor like this, or a no-arg constructor + public CustomLocationProvider(String tableLocation, Map properties) { + this.tableLocation = tableLocation; + } + + @Override + public String newDataLocation(String filename) { + // can use any custom method to generate a file path given a file name + return String.format("%s/%s/%s", tableLocation, UUID.randomUUID().toString(), filename); + } + + @Override + public String newDataLocation(PartitionSpec spec, StructLike partitionData, String filename) { + // can use any custom method to generate a file path given a partition info and file name + return newDataLocation(filename); + } +} +``` + +If you are already implementing your own catalog, you can override `TableOperations.locationProvider()` to use your custom default `LocationProvider`. +To use a different custom location provider for a specific table, specify the implementation when creating the table using table property `write.location-provider.impl` + +Example: +```sql +CREATE TABLE hive.default.my_table ( + id bigint, + data string, + category string) +USING iceberg +OPTIONS ( + 'write.location-provider.impl'='com.my.CustomLocationProvider' +) +PARTITIONED BY (category); +``` + +### Custom IcebergSource +Extend `IcebergSource` and provide implementation to read from `CustomCatalog` + +Example: +```java +public class CustomIcebergSource extends IcebergSource { + + @Override + protected Table findTable(DataSourceOptions options, Configuration conf) { + Optional path = options.get("path"); + Preconditions.checkArgument(path.isPresent(), "Cannot open table: path is not set"); + + // Read table from CustomCatalog + CustomCatalog catalog = new CustomCatalog(conf); + TableIdentifier tableIdentifier = TableIdentifier.parse(path.get()); + return catalog.loadTable(tableIdentifier); + } +} +``` + +Register the `CustomIcebergSource` by updating `META-INF/services/org.apache.spark.sql.sources.DataSourceRegister` with its fully qualified name diff --git a/1.11.0/docs/dell.md b/1.11.0/docs/dell.md new file mode 100644 index 000000000000..b20bd492028e --- /dev/null +++ b/1.11.0/docs/dell.md @@ -0,0 +1,128 @@ +--- +title: "Dell" +--- + + +# Iceberg Dell Integration + +## Dell ECS Integration + +Iceberg can be used with Dell's Enterprise Object Storage (ECS) by using the ECS catalog since 0.15.0. + +See [Dell ECS](https://www.dell.com/en-us/dt/storage/ecs/index.htm) for more information on Dell ECS. + +### Parameters + +When using Dell ECS with Iceberg, these configuration parameters are required: + +| Name | Description | +| ------------------------ | --------------------------------- | +| ecs.s3.endpoint | ECS S3 service endpoint | +| ecs.s3.access-key-id | ECS Username | +| ecs.s3.secret-access-key | S3 Secret Key | +| warehouse | The location of data and metadata | + +The warehouse should use the following formats: + +| Example | Description | +| -------------------------- | --------------------------------------------------------------- | +| ecs://bucket-a | Use the whole bucket as the data | +| ecs://bucket-a/ | Use the whole bucket as the data. The last `/` is ignored. | +| ecs://bucket-a/namespace-a | Use a prefix to access the data only in this specific namespace | + +The Iceberg `runtime` jar supports different versions of Spark and Flink. You should pick the correct version. + +Even though the [Dell ECS client](https://github.com/EMCECS/ecs-object-client-java) jar is backward compatible, Dell EMC still recommends using the latest version of the client. + +### Spark + +To use the Dell ECS catalog with Spark 3.5.0, you should create a Spark session like: + +```bash +ICEBERG_VERSION=1.4.2 +SPARK_VERSION=3.5_2.12 +ECS_CLIENT_VERSION=3.3.2 + +DEPENDENCIES="org.apache.iceberg:iceberg-spark-runtime-${SPARK_VERSION}:${ICEBERG_VERSION},\ +org.apache.iceberg:iceberg-dell:${ICEBERG_VERSION},\ +com.emc.ecs:object-client-bundle:${ECS_CLIENT_VERSION}" + +spark-sql --packages ${DEPENDENCIES} \ + --conf spark.sql.extensions=org.apache.iceberg.spark.extensions.IcebergSparkSessionExtensions \ + --conf spark.sql.catalog.my_catalog=org.apache.iceberg.spark.SparkCatalog \ + --conf spark.sql.catalog.my_catalog.warehouse=ecs://bucket-a/namespace-a \ + --conf spark.sql.catalog.my_catalog.catalog-impl=org.apache.iceberg.dell.ecs.EcsCatalog \ + --conf spark.sql.catalog.my_catalog.ecs.s3.endpoint=http://10.x.x.x:9020 \ + --conf spark.sql.catalog.my_catalog.ecs.s3.access-key-id= \ + --conf spark.sql.catalog.my_catalog.ecs.s3.secret-access-key= +``` + +Then, use `my_catalog` to access the data in ECS. You can use `SHOW NAMESPACES IN my_catalog` and `SHOW TABLES IN my_catalog` to fetch the namespaces and tables of the catalog. + +The related problems of catalog usage: + +1. The `SparkSession.catalog` won't access the 3rd-party catalog of Spark in both Python and Scala, so please use DDL SQL to list all tables and namespaces. + +### Flink + +Use the Dell ECS catalog with Flink, you first must create a Flink environment. + +```bash +# HADOOP_HOME is your hadoop root directory after unpack the binary package. +export HADOOP_CLASSPATH=`$HADOOP_HOME/bin/hadoop classpath` + +# download Iceberg dependency +MAVEN_URL=https://repo1.maven.org/maven2 +ICEBERG_VERSION=0.15.0 +FLINK_VERSION=1.14 +wget ${MAVEN_URL}/org/apache/iceberg/iceberg-flink-runtime-${FLINK_VERSION}/${ICEBERG_VERSION}/iceberg-flink-runtime-${FLINK_VERSION}-${ICEBERG_VERSION}.jar +wget ${MAVEN_URL}/org/apache/iceberg/iceberg-dell/${ICEBERG_VERSION}/iceberg-dell-${ICEBERG_VERSION}.jar + +# download ECS object client +ECS_CLIENT_VERSION=3.3.2 +wget ${MAVEN_URL}/com/emc/ecs/object-client-bundle/${ECS_CLIENT_VERSION}/object-client-bundle-${ECS_CLIENT_VERSION}.jar + +# open the SQL client. +/path/to/bin/sql-client.sh embedded \ + -j iceberg-flink-runtime-${FLINK_VERSION}-${ICEBERG_VERSION}.jar \ + -j iceberg-dell-${ICEBERG_VERSION}.jar \ + -j object-client-bundle-${ECS_CLIENT_VERSION}.jar \ + shell +``` + +Then, use Flink SQL to create a catalog named `my_catalog`: + +```SQL +CREATE CATALOG my_catalog WITH ( + 'type'='iceberg', + 'warehouse' = 'ecs://bucket-a/namespace-a', + 'catalog-impl'='org.apache.iceberg.dell.ecs.EcsCatalog', + 'ecs.s3.endpoint' = 'http://10.x.x.x:9020', + 'ecs.s3.access-key-id' = '', + 'ecs.s3.secret-access-key' = ''); +``` + +Then, you can run `USE CATALOG my_catalog`, `SHOW DATABASES`, and `SHOW TABLES` to fetch the namespaces and tables of the catalog. + +### Limitations + +When you use the catalog with Dell ECS only, you should care about these limitations: + +1. `RENAME` statements are supported without other protections. When you try to rename a table, you need to guarantee all commits are finished in the original table. +2. `RENAME` statements only rename the table without moving any data files. This can lead to a table's data being stored in a path outside of the configured warehouse path. +3. The CAS operations used by table commits are based on the checksum of the object. There is a very small probability of a checksum conflict. diff --git a/1.11.0/docs/delta-lake-migration.md b/1.11.0/docs/delta-lake-migration.md new file mode 100644 index 000000000000..2164014dbd5f --- /dev/null +++ b/1.11.0/docs/delta-lake-migration.md @@ -0,0 +1,119 @@ +--- +title: "Delta Lake Migration" +--- + + +# Delta Lake Table Migration +Delta Lake is a table format that supports Parquet file format and provides time travel and versioning features. When migrating data from Delta Lake to Iceberg, +it is common to migrate all snapshots to maintain the history of the data. + +Currently, Iceberg supports the Snapshot Table action for migrating from Delta Lake to Iceberg tables. +Since Delta Lake tables maintain transactions, all available transactions will be committed to the new Iceberg table as transactions in order. +For Delta Lake tables, any additional data files added after the initial migration will be included in their corresponding transactions and subsequently added to the new Iceberg table using the Add Transaction action. +The Add Transaction action, a variant of the Add File action, is still under development. + +## Enabling Migration from Delta Lake to Iceberg +The `iceberg-delta-lake` module is not bundled with Spark and Flink engine runtimes. To enable migration from delta lake features, the minimum required dependencies are: + +- [iceberg-delta-lake](https://repo1.maven.org/maven2/org/apache/iceberg/iceberg-delta-lake/1.2.1/iceberg-delta-lake-1.2.1.jar) +- [delta-standalone-0.6.0](https://repo1.maven.org/maven2/io/delta/delta-standalone_2.13/0.6.0/delta-standalone_2.13-0.6.0.jar) +- [delta-storage-2.2.0](https://repo1.maven.org/maven2/io/delta/delta-storage/2.2.0/delta-storage-2.2.0.jar) + +### Compatibilities +The module is built and tested with `Delta Standalone:0.6.0` and supports Delta Lake tables with the following protocol version: + +* `minReaderVersion`: 1 +* `minWriterVersion`: 2 + +Please refer to [Delta Lake Table Protocol Versioning](https://docs.delta.io/latest/versioning.html) for more details about Delta Lake protocol versions. + +### API +The `iceberg-delta-lake` module provides an interface named `DeltaLakeToIcebergMigrationActionsProvider`, which contains actions that helps converting from Delta Lake to Iceberg. +The supported actions are: + +* `snapshotDeltaLakeTable`: snapshot an existing Delta Lake table to an Iceberg table + +### Default Implementation +The `iceberg-delta-lake` module also provides a default implementation of the interface which can be accessed by +```java +DeltaLakeToIcebergMigrationActionsProvider defaultActions = DeltaLakeToIcebergMigrationActionsProvider.defaultActions() +``` + +## Snapshot Delta Lake Table to Iceberg +The action `snapshotDeltaLakeTable` reads the Delta Lake table's transactions and converts them to a new Iceberg table with the same schema and partitioning in one iceberg transaction. +The original Delta Lake table remains unchanged. + +The newly created table can be changed or written to without affecting the source table, but the snapshot uses the original table's data files. +Existing data files are added to the Iceberg table's metadata and can be read using a name-to-id mapping created from the original table schema. + +When inserts or overwrites run on the snapshot, new files are placed in the snapshot table's location. The location is default to be the same as that +of the source Delta Lake Table. Users can also specify a different location for the snapshot table. + +!!! info + Because tables created by `snapshotDeltaLakeTable` are not the sole owners of their data files, they are prohibited from + actions like `expire_snapshots` which would physically delete data files. Iceberg deletes, which only effect metadata, + are still allowed. In addition, any operations which affect the original data files will disrupt the Snapshot's + integrity. DELETE statements executed against the original Delta Lake table will remove original data files and the + `snapshotDeltaLakeTable` table will no longer be able to access them. + +#### Usage +| Required Input | Configured By | Description | +|------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------| +| Source Table Location | Argument [`sourceTableLocation`](../../javadoc/latest/org/apache/iceberg/delta/DeltaLakeToIcebergMigrationActionsProvider.html#snapshotDeltaLakeTable(java.lang.String)) | The location of the source Delta Lake table | +| New Iceberg Table Identifier | Configuration API [`as`](../../javadoc/latest/org/apache/iceberg/delta/SnapshotDeltaLakeTable.html#as(org.apache.iceberg.catalog.TableIdentifier)) | The identifier specifies the namespace and table name for the new iceberg table | +| Iceberg Catalog | Configuration API [`icebergCatalog`](../../javadoc/latest/org/apache/iceberg/delta/SnapshotDeltaLakeTable.html#icebergCatalog(org.apache.iceberg.catalog.Catalog)) | The catalog used to create the new iceberg table | +| Hadoop Configuration | Configuration API [`deltaLakeConfiguration`](../../javadoc/latest/org/apache/iceberg/delta/SnapshotDeltaLakeTable.html#deltaLakeConfiguration(org.apache.hadoop.conf.Configuration)) | The Hadoop Configuration used to read the source Delta Lake table. | + +For detailed usage and other optional configurations, please refer to the [SnapshotDeltaLakeTable API](../../javadoc/latest/org/apache/iceberg/delta/SnapshotDeltaLakeTable.html) + +#### Output +| Output Name | Type | Description | +| ------------|------|-------------| +| `imported_files_count` | long | Number of files added to the new table | + +#### Added Table Properties +The following table properties are added to the Iceberg table to be created by default: + +| Property Name | Value | Description | +|-------------------------------|-------------------------------------------|--------------------------------------------------------------------| +| `snapshot_source` | `delta` | Indicates that the table is snapshot from a delta lake table | +| `original_location` | location of the delta lake table | The absolute path to the location of the original delta lake table | +| `schema.name-mapping.default` | JSON name mapping derived from the schema | The name mapping string used to read Delta Lake table's data files | + +#### Examples +```java +import org.apache.iceberg.catalog.TableIdentifier; +import org.apache.iceberg.catalog.Catalog; +import org.apache.hadoop.conf.Configuration; +import org.apache.iceberg.delta.DeltaLakeToIcebergMigrationActionsProvider; + +String sourceDeltaLakeTableLocation = "s3://my-bucket/delta-table"; +String destTableLocation = "s3://my-bucket/iceberg-table"; +TableIdentifier destTableIdentifier = TableIdentifier.of("my_db", "my_table"); +Catalog icebergCatalog = ...; // Iceberg Catalog fetched from engines like Spark or created via CatalogUtil.loadCatalog +Configuration hadoopConf = ...; // Hadoop Configuration fetched from engines like Spark and have proper file system configuration to access the Delta Lake table. + +DeltaLakeToIcebergMigrationActionsProvider.defaultActions() + .snapshotDeltaLakeTable(sourceDeltaLakeTableLocation) + .as(destTableIdentifier) + .icebergCatalog(icebergCatalog) + .tableLocation(destTableLocation) + .deltaLakeConfiguration(hadoopConf) + .tableProperty("my_property", "my_value") + .execute(); +``` diff --git a/1.11.0/docs/encryption.md b/1.11.0/docs/encryption.md new file mode 100644 index 000000000000..cbdce85e760e --- /dev/null +++ b/1.11.0/docs/encryption.md @@ -0,0 +1,153 @@ +--- +title: "Encryption" +--- + + +# Encryption + +Iceberg table encryption protects confidentiality and integrity of table data in an untrusted storage. The `data`, `delete`, `manifest` and `manifest list` files are encrypted and tamper-proofed before being sent to the storage backend. + +The `metadata.json` file does not contain data or stats, and is therefore not encrypted. + +Currently, encryption is supported in the Hive and REST catalogs for tables with Parquet and Avro data formats. + +Two parameters are required to activate encryption of a table: + +1. Catalog property that specifies the KMS ("key management service"). It can be either `encryption.kms-type` for pre-defined KMS clients (`aws`, `azure` or `gcp`) or `encryption.kms-impl` with the client class path for custom KMS clients. +2. Table property `encryption.key-id`, that specifies the ID of a master key used to encrypt and decrypt the table. Master keys are stored and managed in the KMS. + +For more details on table encryption, see the "Appendix: Internals Overview" [subsection](#appendix-internals-overview). + +## Example + +```sh +spark-sql --packages org.apache.iceberg:iceberg-spark-runtime-{{ sparkVersionMajor }}:{{ icebergVersion }}\ + --conf spark.sql.extensions=org.apache.iceberg.spark.extensions.IcebergSparkSessionExtensions \ + --conf spark.sql.catalog.spark_catalog=org.apache.iceberg.spark.SparkSessionCatalog \ + --conf spark.sql.catalog.spark_catalog.type=hive \ + --conf spark.sql.catalog.local=org.apache.iceberg.spark.SparkCatalog \ + --conf spark.sql.catalog.local.type=hive \ + --conf spark.sql.catalog.local.encryption.kms-type=aws +``` + +```sql +CREATE TABLE local.db.table (id bigint, data string) USING iceberg +TBLPROPERTIES ('encryption.key-id'=''); +``` + +Inserted data will be automatically encrypted, + +```sql +INSERT INTO local.db.table VALUES (1, 'a'), (2, 'b'), (3, 'c'); +``` + +To verify encryption, the contents of data, manifest and manifest list files can be dumped in the command line with + +```sh +hexdump -C | more +``` + +The Parquet files must start with the "PARE" magic string (PARquet Encrypted footer mode), and manifest/list files must start with "AGS1" magic string (Aes Gcm Stream version 1). + +Queried data will be automatically decrypted, + +```sql +SELECT * FROM local.db.table; +``` + +## Catalog security requirements + +1. Catalogs must ensure the `encryption.key-id` property is not modified or removed during table lifetime. + +2. To function properly, Iceberg table encryption requires the catalog implementations not to retrieve the metadata directly from metadata.json files, if these files are kept unprotected in a storage vulnerable to tampering: + + - Catalogs may keep the metadata in a trusted independent object store. + - Catalogs may work with metadata.json files in a tamper-proof storage. + - Catalogs may use checksum techniques to verify integrity of metadata.json files in a storage vulnerable to tampering + (the checksums must be kept in a separate trusted storage). + +## Key Management Clients + +Currently, Iceberg has clients for the AWS, GCP and Azure KMS systems. A custom client can be built for other key management systems by implementing the `org.apache.iceberg.encryption.KeyManagementClient` interface. + +This interface has the following main methods, + +```java + /** + * Initialize the KMS client with given properties. + * + * @param properties kms client properties (taken from catalog properties) + */ + void initialize(Map properties); + + /** + * Wrap a secret key, using a wrapping/master key which is stored in KMS and referenced by an ID. + * Wrapping means encryption of the secret key with the master key, and adding optional + * KMS-specific metadata that allows the KMS to decrypt the secret key in an unwrapping call. + * + * @param key a secret key being wrapped + * @param wrappingKeyId a key ID that represents a wrapping key stored in KMS + * @return wrapped key material + */ + ByteBuffer wrapKey(ByteBuffer key, String wrappingKeyId); + + /** + * Unwrap a secret key, using a wrapping/master key which is stored in KMS and referenced by an + * ID. + * + * @param wrappedKey wrapped key material (encrypted key and optional KMS metadata, returned by + * the wrapKey method) + * @param wrappingKeyId a key ID that represents a wrapping key stored in KMS + * @return raw key bytes + */ + ByteBuffer unwrapKey(ByteBuffer wrappedKey, String wrappingKeyId); +``` + +## Appendix: Internals Overview + +The standard Iceberg encryption manager generates an encryption key and a unique file ID ("AAD prefix") +for each data and delete file. The generation is performed in the worker nodes, by using a secure random +number generator. For Parquet data files, these parameters are passed to the native Parquet Modular +Encryption [mechanism](https://parquet.apache.org/docs/file-format/data-pages/encryption). For Avro data files, +these parameters are passed to the AES GCM Stream encryption [mechanism](../../gcm-stream-spec.md). + +The parent manifest file stores the encryption key and AAD prefix for each data and delete file in the +`key_metadata` [field](../../spec.md#data-file-fields). For Avro data tables, the data file length +is also added to the `key_metadata`. +The manifest file is encrypted by the AES GCM Stream encryption mechanism, using an encryption key and an +AAD prefix generated by the standard encryption manager. The generation is performed in the driver nodes, +by using a secure random number generator. + +The parent manifest list file stores the encryption key, AAD prefix and file length for each manifest file +in the `key_metadata` [field](../../spec.md#manifest-lists). The manifest list file is encrypted by +the AES GCM Stream encryption mechanism, +using an encryption key and an AAD prefix generated by the standard encryption manager. + +The manifest list encryption key, AAD prefix and file length are packed in a key metadata object. This object +is serialized and encrypted with a "key encryption key" (KEK), using the KEK creation timestamp as the AES +GCM AAD. A KEK and its unique KEK_ID are generated by using a secure random number generator. For each +snapshot, the KEK_ID of the encryption key that encrypts the manifest list key metadata is kept in the +`key-id` field in the table metadata snapshot [structure](../../spec.md#snapshots). The encrypted +manifest list key metadata is kept in the `encryption-keys` list in the table metadata +[structure](../../spec.md#table-metadata-fields). + +The KEK is encrypted by the table master key via the KMS client. The result is kept in the `encryption-keys` +list in the table metadata structure. The KEK is re-used for a period allowed by the NIST SP 800-57 +specification. Then, it is rotated - a new KEK and KEK_ID are generated for encryption of new manifest list +key metadata objects. The new KEK is encrypted by the table master key and stored in the `encryption-keys` +list in the table metadata structure. The previous KEKs are retained for the existing table snapshots. diff --git a/1.11.0/docs/evolution.md b/1.11.0/docs/evolution.md new file mode 100644 index 000000000000..437234ec3756 --- /dev/null +++ b/1.11.0/docs/evolution.md @@ -0,0 +1,100 @@ +--- +title: Evolution +--- + + +# Evolution + +Iceberg supports **in-place table evolution**. You can [evolve a table schema](#schema-evolution) just like SQL -- even in nested structures -- or [change partition layout](#partition-evolution) when data volume changes. Iceberg does not require costly distractions, like rewriting table data or migrating to a new table. + +For example, Hive table partitioning cannot change so moving from a daily partition layout to an hourly partition layout requires a new table. And because queries are dependent on partitions, queries must be rewritten for the new table. In some cases, even changes as simple as renaming a column are either not supported, or can cause [data correctness](#correctness) problems. + +## Schema evolution + +Iceberg supports the following schema evolution changes: + +* **Add** -- add a new column to the table or to a nested struct +* **Drop** -- remove an existing column from the table or a nested struct +* **Rename** -- rename an existing column or field in a nested struct +* **Update** -- widen the type of a column, struct field, map key, map value, or list element +* **Reorder** -- change the order of columns or fields in a nested struct + +Iceberg schema updates are **metadata changes**, so no data files need to be rewritten to perform the update. + +Note that map keys do not support adding or dropping struct fields that would change equality. + +### Correctness + +Iceberg guarantees that **schema evolution changes are independent and free of side-effects**, without rewriting files: + +1. Added columns never read existing values from another column. +2. Dropping a column or field does not change the values in any other column. +3. Updating a column or field does not change values in any other column. +4. Changing the order of columns or fields in a struct does not change the values associated with a column or field name. + +Iceberg uses unique IDs to track each column in a table. When you add a column, it is assigned a new ID so existing data is never used by mistake. + +* Formats that track columns by name can inadvertently un-delete a column if a name is reused, which violates #1. +* Formats that track columns by position cannot delete columns without changing the names that are used for each column, which violates #2. + +## Partition evolution + +Iceberg table partitioning can be updated in an existing table because queries do not reference partition values directly. + +When you evolve a partition spec, the old data written with an earlier spec remains unchanged. New data is written using the new spec in a new layout. Metadata for each of the partition versions is kept separately. Because of this, when you start writing queries, you get split planning. This is where each partition layout plans files separately using the filter it derives for that specific partition layout. Here's a visual representation of a contrived example: + +![Partition evolution diagram](assets/images/partition-spec-evolution.png) +*The data for 2008 is partitioned by month. Starting from 2009 the table is updated so that the data is instead partitioned by day. Both partitioning layouts are able to coexist in the same table.* + +Iceberg uses [hidden partitioning](partitioning.md), so you don't *need* to write queries for a specific partition layout to be fast. Instead, you can write queries that select the data you need, and Iceberg automatically prunes out files that don't contain matching data. + +Partition evolution is a metadata operation and does not eagerly rewrite files. + +Iceberg's Java table API provides `updateSpec` API to update partition spec. +For example, the following code could be used to update the partition spec to add a new partition field that places `id` column values into 8 buckets and remove an existing partition field `category`: + +```java +Table sampleTable = ...; +sampleTable.updateSpec() + .addField(bucket("id", 8)) + .removeField("category") + .commit(); +``` + +Spark supports updating partition spec through its `ALTER TABLE` SQL statement, see more details in [Spark SQL](spark-ddl.md#alter-table-add-partition-field). + +## Sort order evolution + +Similar to partition spec, Iceberg sort order can also be updated in an existing table. +When you evolve a sort order, the old data written with an earlier order remains unchanged. +Engines can always choose to write data in the latest sort order or unsorted when sorting is prohibitively expensive. + +Iceberg's Java table API provides `replaceSortOrder` API to update sort order. +For example, the following code could be used to create a new sort order +with `id` column sorted in ascending order with nulls last, +and `category` column sorted in descending order with nulls first: + +```java +Table sampleTable = ...; +sampleTable.replaceSortOrder() + .asc("id", NullOrder.NULLS_LAST) + .dec("category", NullOrder.NULL_FIRST) + .commit(); +``` + +Spark supports updating sort order through its `ALTER TABLE` SQL statement, see more details in [Spark SQL](spark-ddl.md#alter-table-write-ordered-by). diff --git a/1.11.0/docs/fileio.md b/1.11.0/docs/fileio.md new file mode 100644 index 000000000000..6c7a779193c2 --- /dev/null +++ b/1.11.0/docs/fileio.md @@ -0,0 +1,41 @@ +--- +title: "FileIO" +--- + + +# Iceberg FileIO + +## Overview + +Iceberg comes with a flexible abstraction around reading and writing data and metadata files. The FileIO interface allows the Iceberg library to communicate with the underlying storage layer. FileIO is used for all metadata operations during the job planning and commit stages. + +## Iceberg Files + +The metadata for an Iceberg table tracks the absolute path for data files which allows greater abstraction over the physical layout. Additionally, changes to table state are performed by writing new metadata files and never involve renaming files. This allows a much smaller set of requirements for file operations. The essential functionality for a FileIO implementation is that it can read files, write files, and seek to any position within a stream. + +## Usage in Processing Engines + +The responsibility of reading and writing data files lies with the processing engines and happens during task execution. However, after data files are written, processing engines use FileIO to write new Iceberg metadata files that capture the new state of the table. + +Different FileIO implementations are used depending on the type of storage. Iceberg comes with a set of FileIO implementations for popular storage providers. + +- Amazon S3 +- Google Cloud Storage +- Object Service Storage (including https) +- Dell Enterprise Cloud Storage +- Hadoop (adapts any Hadoop FileSystem implementation) diff --git a/1.11.0/docs/flink-configuration.md b/1.11.0/docs/flink-configuration.md new file mode 100644 index 000000000000..f30b42288896 --- /dev/null +++ b/1.11.0/docs/flink-configuration.md @@ -0,0 +1,201 @@ +--- +title: "Flink Configuration" +--- + + +# Flink Configuration + +## Catalog Configuration + +A catalog is created and named by executing the following query (replace `` with your catalog name and +``=`` with catalog implementation config): + +```sql +CREATE CATALOG WITH ( + 'type'='iceberg', + ``=`` +); +``` + +The following properties can be set globally and are not limited to a specific catalog implementation: + +| Property | Required | Values | Description | +| ---------------------------- |----------| -------------------------- |----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| type | ✔️ | iceberg | Must be `iceberg`. | +| catalog-type | | `hive`, `hadoop`, `rest`, `glue`, `jdbc` or `nessie` | The underlying Iceberg catalog implementation, `HiveCatalog`, `HadoopCatalog`, `RESTCatalog`, `GlueCatalog`, `JdbcCatalog`, `NessieCatalog` or left unset if using a custom catalog implementation via catalog-impl| +| catalog-impl | | | The fully-qualified class name of a custom catalog implementation. Must be set if `catalog-type` is unset. | +| property-version | | | Version number to describe the property version. This property can be used for backwards compatibility in case the property format changes. The current property version is `1`. | +| cache-enabled | | `true` or `false` | Whether to enable catalog cache, default value is `true`. | +| cache.expiration-interval-ms | | | How long catalog entries are locally cached, in milliseconds; negative values like `-1` will disable expiration, value 0 is not allowed to set. default value is `-1`. | + +The following properties can be set if using the Hive catalog: + +| Property | Required | Values | Description | +| --------------- |----------| ------ |--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| uri | ✔️ | | The Hive metastore's thrift URI. | +| clients | | | The Hive metastore client pool size, default value is 2. | +| warehouse | | | The Hive warehouse location, users should specify this path if neither set the `hive-conf-dir` to specify a location containing a `hive-site.xml` configuration file nor add a correct `hive-site.xml` to classpath. | +| hive-conf-dir | | | Path to a directory containing a `hive-site.xml` configuration file which will be used to provide custom Hive configuration values. The value of `hive.metastore.warehouse.dir` from `/hive-site.xml` (or hive configure file from classpath) will be overwritten with the `warehouse` value if setting both `hive-conf-dir` and `warehouse` when creating iceberg catalog. | +| hadoop-conf-dir | | | Path to a directory containing `core-site.xml` and `hdfs-site.xml` configuration files which will be used to provide custom Hadoop configuration values. | + +The following properties can be set if using the Hadoop catalog: + +| Property | Required | Values | Description | +| --------- |-------------| ------ | ---------------------------------------------------------- | +| warehouse | ✔️ | | The HDFS directory to store metadata files and data files. | + +The following properties can be set if using the REST catalog: + +| Property | Required | Values | Description | +| ---------- |----------| ------ |-----------------------------------------------------------------------------| +| uri | ✔️ | | The URL to the REST Catalog. | +| credential | | | A credential to exchange for a token in the OAuth2 client credentials flow. | +| token | | | A token which will be used to interact with the server. | + +## Runtime configuration + +### Read options + +Flink read options are passed when configuring the Flink IcebergSource: + +``` +IcebergSource.forRowData() + .tableLoader(TableLoader.fromCatalog(...)) + .assignerFactory(new SimpleSplitAssignerFactory()) + .streaming(true) + .streamingStartingStrategy(StreamingStartingStrategy.INCREMENTAL_FROM_SNAPSHOT_ID) + .startSnapshotId(3821550127947089987L) + .monitorInterval(Duration.ofMillis(10L)) // or .set("monitor-interval", "10s") \ set(FlinkReadOptions.MONITOR_INTERVAL, "10s") + .build() +``` + +For Flink SQL, read options can be passed in via SQL hints like this: + +``` +SELECT * FROM tableName /*+ OPTIONS('monitor-interval'='10s') */ +... +``` + +Options can be passed in via Flink configuration, which will be applied to current session. Note that not all options support this mode. + +``` +env.getConfig() + .getConfiguration() + .set(FlinkReadOptions.SPLIT_FILE_OPEN_COST_OPTION, 1000L); +... +``` + +`Read option` has the highest priority, followed by `Flink configuration` and then `Table property`. + +| Read option | Flink configuration | Table property | Default | Description | +|-------------------------------|-------------------------------------------------|------------------------------|----------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| snapshot-id | N/A | N/A | null | For time travel in batch mode. Read data from the specified snapshot-id. | +| case-sensitive | connector.iceberg.case-sensitive | N/A | false | If true, match column name in a case sensitive way. | +| as-of-timestamp | N/A | N/A | null | For time travel in batch mode. Read data from the most recent snapshot as of the given time in milliseconds. | +| starting-strategy | connector.iceberg.starting-strategy | N/A | INCREMENTAL_FROM_LATEST_SNAPSHOT | Starting strategy for streaming execution. TABLE_SCAN_THEN_INCREMENTAL: Do a regular table scan then switch to the incremental mode. The incremental mode starts from the current snapshot exclusive. INCREMENTAL_FROM_LATEST_SNAPSHOT: Start incremental mode from the latest snapshot inclusive. If it is an empty table, all future append snapshots should be discovered. INCREMENTAL_FROM_LATEST_SNAPSHOT_EXCLUSIVE: Start incremental mode from the latest snapshot exclusive. If it is an empty table, all future append snapshots should be discovered. INCREMENTAL_FROM_EARLIEST_SNAPSHOT: Start incremental mode from the earliest snapshot inclusive. If it is an empty table, all future append snapshots should be discovered. INCREMENTAL_FROM_SNAPSHOT_ID: Start incremental mode from a snapshot with a specific id inclusive. INCREMENTAL_FROM_SNAPSHOT_TIMESTAMP: Start incremental mode from a snapshot with a specific timestamp inclusive. If the timestamp is between two snapshots, it should start from the snapshot after the timestamp. Just for FIP27 Source. | +| start-snapshot-timestamp | N/A | N/A | null | Start to read data from the most recent snapshot as of the given time in milliseconds. | +| start-snapshot-id | N/A | N/A | null | Start to read data from the specified snapshot-id. | +| end-snapshot-id | N/A | N/A | The latest snapshot id | Specifies the end snapshot. | +| branch | N/A | N/A | main | Specifies the branch to read from in batch mode | +| tag | N/A | N/A | null | Specifies the tag to read from in batch mode | +| start-tag | N/A | N/A | null | Specifies the starting tag to read from for incremental reads | +| end-tag | N/A | N/A | null | Specifies the ending tag to to read from for incremental reads | +| split-size | connector.iceberg.split-size | read.split.target-size | 128 MB | Target size when combining input splits. | +| split-lookback | connector.iceberg.split-file-open-cost | read.split.planning-lookback | 10 | Number of bins to consider when combining input splits. | +| split-file-open-cost | connector.iceberg.split-file-open-cost | read.split.open-file-cost | 4MB | The estimated cost to open a file, used as a minimum weight when combining splits. | +| streaming | connector.iceberg.streaming | N/A | false | Sets whether the current task runs in streaming or batch mode. | +| monitor-interval | connector.iceberg.monitor-interval | N/A | 60s | Monitor interval to discover splits from new snapshots. Applicable only for streaming read. | +| include-column-stats | connector.iceberg.include-column-stats | N/A | false | Create a new scan from this that loads the column stats with each data file. Column stats include: value count, null value count, lower bounds, and upper bounds. | +| max-planning-snapshot-count | connector.iceberg.max-planning-snapshot-count | N/A | Integer.MAX_VALUE | Max number of snapshots limited per split enumeration. Applicable only to streaming read. | +| limit | connector.iceberg.limit | N/A | -1 | Limited output number of rows. | +| max-allowed-planning-failures | connector.iceberg.max-allowed-planning-failures | N/A | 3 | Max allowed consecutive failures for scan planning before failing the job. Set to -1 for never failing the job for scan planning failure. | +| watermark-column | connector.iceberg.watermark-column | N/A | null | Specifies the watermark column to use for watermark generation. If this option is present, the `splitAssignerFactory` will be overridden with `OrderedSplitAssignerFactory`. | +| watermark-column-time-unit | connector.iceberg.watermark-column-time-unit | N/A | TimeUnit.MICROSECONDS | Specifies the watermark time unit to use for watermark generation. The possible values are DAYS, HOURS, MINUTES, SECONDS, MILLISECONDS, MICROSECONDS, NANOSECONDS. | + +### Write options + +Flink write options are passed when configuring the FlinkSink, like this: + +``` +FlinkSink.Builder builder = FlinkSink.forRow(dataStream, SimpleDataUtil.FLINK_SCHEMA) + .table(table) + .tableLoader(tableLoader) + .set("write-format", "orc") + .set(FlinkWriteOptions.OVERWRITE_MODE, "true"); +``` + +For Flink SQL, write options can be passed in via SQL hints like this: + +``` +INSERT INTO tableName /*+ OPTIONS('upsert-enabled'='true') */ +... +``` + +| Flink option | Default | Description | +|-----------------------------------------|--------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------| +| write-format | Table write.format.default | File format to use for this write operation; parquet, avro, or orc | +| target-file-size-bytes | As per table property | Overrides this table's write.target-file-size-bytes | +| upsert-enabled | Table write.upsert.enabled | Overrides this table's write.upsert.enabled | +| overwrite-enabled | false | Overwrite the table's data, overwrite mode shouldn't be enable when configuring to use UPSERT data stream. | +| distribution-mode | Table write.distribution-mode | Overrides this table's write.distribution-mode. RANGE distribution is in experimental status. | +| range-distribution-statistics-type | Auto | Range distribution data statistics collection type: Map, Sketch, Auto. See details [here](#range-distribution-statistics-type). | +| range-distribution-sort-key-base-weight | 0.0 (double) | Base weight for every sort key relative to target traffic weight per writer task. See details [here](#range-distribution-sort-key-base-weight). | +| compression-codec | Table write.(fileformat).compression-codec | Overrides this table's compression codec for this write | +| compression-level | Table write.(fileformat).compression-level | Overrides this table's compression level for Parquet and Avro tables for this write | +| compression-strategy | Table write.orc.compression-strategy | Overrides this table's compression strategy for ORC tables for this write | +| write-parallelism | Upstream operator parallelism | Overrides the writer parallelism | +| uid-suffix | As per table property | Overrides the uid suffix used in the underlying IcebergSink for this table | + +#### Range distribution statistics type + +Config value is a enum type: `Map`, `Sketch`, `Auto`. +

+ +#### Range distribution sort key base weight + +`range-distribution-sort-key-base-weight`: `0.0`. + +If sort order contains partition columns, each sort key would map to one partition and data +file. This relative weight can avoid placing too many small files for sort keys with low +traffic. It is a double value that defines the minimal weight for each sort key. `0.02` means +each key has a base weight of `2%` of the targeted traffic weight per writer task. + +E.g. the sink Iceberg table is partitioned daily by event time. Assume the data stream +contains events from now up to 180 days ago. With event time, traffic weight distribution +across different days typically has a long tail pattern. Current day contains the most +traffic. The older days (long tail) contain less and less traffic. Assume writer parallelism +is `10`. The total weight across all 180 days is `10,000`. Target traffic weight per writer +task would be `1,000`. Assume the weight sum for the oldest 150 days is `1,000`. Normally, +the range partitioner would put all the oldest 150 days in one writer task. That writer task +would write to 150 small files (one per day). Keeping 150 open files can potentially consume +large amount of memory. Flushing and uploading 150 files (however small) at checkpoint time +can also be potentially slow. If this config is set to `0.02`. It means every sort key has a +base weight of `2%` of targeted weight of `1,000` for every write task. It would essentially +avoid placing more than `50` data files (one per day) on one writer task no matter how small +they are. + +This is only applicable to [`StatisticsType.Map`](../../javadoc/{{ icebergVersion }}/org/apache/iceberg/flink/sink/shuffle/StatisticsType.html#Map) for low-cardinality scenario. For [`StatisticsType.Sketch`](../../javadoc/{{ icebergVersion }}/org/apache/iceberg/flink/sink/shuffle/StatisticsType.html#Sketch) high-cardinality sort columns, they are usually not used as +partition columns. Otherwise, too many partitions and small files may be generated during +write. Sketch range partitioner simply splits high-cardinality keys into ordered ranges. diff --git a/1.11.0/docs/flink-connector.md b/1.11.0/docs/flink-connector.md new file mode 100644 index 000000000000..2ab2a27e8d03 --- /dev/null +++ b/1.11.0/docs/flink-connector.md @@ -0,0 +1,153 @@ +--- +title: "Flink Connector" +--- + + +# Flink Connector +Apache Flink supports creating Iceberg table directly without creating the explicit Flink catalog in Flink SQL. That means we can just create an iceberg table by specifying `'connector'='iceberg'` table option in Flink SQL which is similar to usage in the Flink official [document](https://nightlies.apache.org/flink/flink-docs-release-{{ flinkVersionMajor }}/docs/connectors/table/overview/). + +In Flink, the SQL `CREATE TABLE test (..) WITH ('connector'='iceberg', ...)` will create a Flink table in current Flink catalog (use [GenericInMemoryCatalog](https://ci.apache.org/projects/flink/flink-docs-release-{{ flinkVersionMajor }}/docs/dev/table/catalogs/#genericinmemorycatalog) by default), +which is just mapping to the underlying iceberg table instead of maintaining iceberg table directly in current Flink catalog. + +To create the table in Flink SQL by using SQL syntax `CREATE TABLE test (..) WITH ('connector'='iceberg', ...)`, Flink iceberg connector allows setting the catalog properties through table properties. The valid property values are described on the [Flink Configuration](flink-configuration.md#catalog-configuration) page in detail. + +## Table managed in Hive catalog. + +Before executing the following SQL, please make sure you've configured the Flink SQL client correctly according to the [quick start documentation](flink.md). + +The following SQL will create a Flink table in the current Flink catalog, which maps to the iceberg table `default_database.flink_table` managed in iceberg catalog. + +```sql +CREATE TABLE flink_table ( + id BIGINT, + data STRING +) WITH ( + 'connector'='iceberg', + 'catalog-name'='hive_prod', + 'uri'='thrift://localhost:9083', + 'warehouse'='hdfs://nn:8020/path/to/warehouse' +); +``` + +If you want to create a Flink table mapping to a different iceberg table managed in Hive catalog (such as `hive_db.hive_iceberg_table` in Hive), then you can create Flink table as following: + +```sql +CREATE TABLE flink_table ( + id BIGINT, + data STRING +) WITH ( + 'connector'='iceberg', + 'catalog-name'='hive_prod', + 'catalog-database'='hive_db', + 'catalog-table'='hive_iceberg_table', + 'uri'='thrift://localhost:9083', + 'warehouse'='hdfs://nn:8020/path/to/warehouse' +); +``` + +!!! info + The underlying catalog database (`hive_db` in the above example) will be created automatically if it does not exist when writing records into the Flink table. + +## Table managed in hadoop catalog + +The following SQL will create a Flink table in current Flink catalog, which maps to the iceberg table `default_database.flink_table` managed in hadoop catalog. + +```sql +CREATE TABLE flink_table ( + id BIGINT, + data STRING +) WITH ( + 'connector'='iceberg', + 'catalog-name'='hadoop_prod', + 'catalog-type'='hadoop', + 'warehouse'='hdfs://nn:8020/path/to/warehouse' +); +``` + +## Table managed in REST catalog + +The following SQL will create a Flink table in current Flink catalog, which maps to the iceberg table `default_database.flink_table` managed in REST catalog + +```sql +CREATE TABLE flink_table ( + id BIGINT, + data STRING +) WITH ( + 'connector'='iceberg', + 'catalog-name'='rest_prod', + 'catalog-type'='rest', + 'uri'='https://localhost/' + 'credential'='xxxx' -- Optional + 'token'='xxxx' -- Optional + 'scope'='xxxx' -- Optional + ... +); +``` + +## Table managed in custom catalog + +The following SQL will create a Flink table in current Flink catalog, which maps to the iceberg table `default_database.flink_table` managed in +a custom catalog of type `com.my.custom.CatalogImpl`. + +```sql +CREATE TABLE flink_table ( + id BIGINT, + data STRING +) WITH ( + 'connector'='iceberg', + 'catalog-name'='custom_prod', + 'catalog-impl'='com.my.custom.CatalogImpl', + -- More table properties for the customized catalog + 'my-additional-catalog-config'='my-value', + ... +); +``` + +Please check sections under the Integrations tab for all custom catalogs. + +## A complete example. + +Take the Hive catalog as an example: + +```sql +CREATE TABLE flink_table ( + id BIGINT, + data STRING +) WITH ( + 'connector'='iceberg', + 'catalog-name'='hive_prod', + 'uri'='thrift://localhost:9083', + 'warehouse'='file:///path/to/warehouse' +); + +INSERT INTO flink_table VALUES (1, 'AAA'), (2, 'BBB'), (3, 'CCC'); + +SET execution.result-mode=tableau; +SELECT * FROM flink_table; + ++----+------+ +| id | data | ++----+------+ +| 1 | AAA | +| 2 | BBB | +| 3 | CCC | ++----+------+ +3 rows in set +``` + +For more details, please refer to the Iceberg [Flink documentation](flink.md). diff --git a/1.11.0/docs/flink-ddl.md b/1.11.0/docs/flink-ddl.md new file mode 100644 index 000000000000..0a9b26712235 --- /dev/null +++ b/1.11.0/docs/flink-ddl.md @@ -0,0 +1,249 @@ +--- +title: "Flink DDL" +--- + + +## DDL commands + +### `CREATE Catalog` + +#### Hive catalog + +This creates an Iceberg catalog named `hive_catalog` that can be configured using `'catalog-type'='hive'`, which loads tables from Hive metastore: + +```sql +CREATE CATALOG hive_catalog WITH ( + 'type'='iceberg', + 'catalog-type'='hive', + 'uri'='thrift://localhost:9083', + 'clients'='5', + 'property-version'='1', + 'warehouse'='hdfs://nn:8020/warehouse/path' +); +``` + +The following properties can be set if using the Hive catalog: + +* `uri`: The Hive metastore's thrift URI. (Required) +* `clients`: The Hive metastore client pool size, default value is 2. (Optional) +* `warehouse`: The Hive warehouse location, users should specify this path if neither set the `hive-conf-dir` to specify a location containing a `hive-site.xml` configuration file nor add a correct `hive-site.xml` to classpath. +* `hive-conf-dir`: Path to a directory containing a `hive-site.xml` configuration file which will be used to provide custom Hive configuration values. The value of `hive.metastore.warehouse.dir` from `/hive-site.xml` (or hive configure file from classpath) will be overwritten with the `warehouse` value if setting both `hive-conf-dir` and `warehouse` when creating iceberg catalog. +* `hadoop-conf-dir`: Path to a directory containing `core-site.xml` and `hdfs-site.xml` configuration files which will be used to provide custom Hadoop configuration values. + +!!! warning "Hive Catalog Limitation" + The Hive Metastore (HMS) validates schema changes by comparing column types **positionally** + (`hive.metastore.disallow.incompatible.col.type.changes`, default `true`). When using a Hive catalog, + schema evolution operations that change column positions — such as dropping a non-last column or + reordering columns — may fail regardless of which engine performs the change (Spark, Flink Java API, etc.). + + To work around this, disable the HMS schema compatibility check by setting + `hive.metastore.disallow.incompatible.col.type.changes=false`: + + - **Remote HMS:** Set this property in the HMS server's `hive-site.xml`. + - **Embedded HMS:** Add the equivalent property to the Hive catalog configuration. + + **Trade-off:** After disabling this check, the Hive engine may no longer be able to read the table + correctly due to the schema mismatch in the Hive Metastore. Iceberg-aware engines (Spark, Flink, + Trino, etc.) will continue to work correctly, as they read schema from Iceberg metadata rather + than the Hive Metastore. + +#### Hadoop catalog + +Iceberg also supports a directory-based catalog in HDFS that can be configured using `'catalog-type'='hadoop'`: + +```sql +CREATE CATALOG hadoop_catalog WITH ( + 'type'='iceberg', + 'catalog-type'='hadoop', + 'warehouse'='hdfs://nn:8020/warehouse/path', + 'property-version'='1' +); +``` + +The following properties can be set if using the Hadoop catalog: + +* `warehouse`: The HDFS directory to store metadata files and data files. (Required) + +Execute the sql command `USE CATALOG hadoop_catalog` to set the current catalog. + +#### REST catalog + +This creates an iceberg catalog named `rest_catalog` that can be configured using `'catalog-type'='rest'`, which loads tables from a REST catalog: + +```sql +CREATE CATALOG rest_catalog WITH ( + 'type'='iceberg', + 'catalog-type'='rest', + 'uri'='https://localhost/' +); +``` + +The following properties can be set if using the REST catalog: + +* `uri`: The URL to the REST Catalog (Required) +* `credential`: A credential to exchange for a token in the OAuth2 client credentials flow (Optional) +* `token`: A token which will be used to interact with the server (Optional) + +#### Custom catalog + +Flink also supports loading a custom Iceberg `Catalog` implementation by specifying the `catalog-impl` property: + +```sql +CREATE CATALOG my_catalog WITH ( + 'type'='iceberg', + 'catalog-impl'='com.my.custom.CatalogImpl', + 'my-additional-catalog-config'='my-value' +); +``` + +#### Create through YAML config + +Catalogs can be registered in `sql-client-defaults.yaml` before starting the SQL client. + +```yaml +catalogs: + - name: my_catalog + type: iceberg + catalog-type: hadoop + warehouse: hdfs://nn:8020/warehouse/path +``` + +#### Create through SQL Files + +The Flink SQL Client supports the `-i` startup option to execute an initialization SQL file to set up environment when starting up the SQL Client. + +```sql +-- define available catalogs +CREATE CATALOG hive_catalog WITH ( + 'type'='iceberg', + 'catalog-type'='hive', + 'uri'='thrift://localhost:9083', + 'warehouse'='hdfs://nn:8020/warehouse/path' +); + +USE CATALOG hive_catalog; +``` + +Using `-i ` option to initialize SQL Client session: + +```bash +/path/to/bin/sql-client.sh -i /path/to/init.sql +``` + +### `CREATE DATABASE` + +By default, Iceberg will use the `default` database in Flink. Using the following example to create a separate database in order to avoid creating tables under the `default` database: + +```sql +CREATE DATABASE iceberg_db; +USE iceberg_db; +``` + +### `CREATE TABLE` + +```sql +CREATE TABLE `hive_catalog`.`default`.`sample` ( + id BIGINT COMMENT 'unique id', + data STRING NOT NULL +) WITH ('format-version'='2'); +``` + +Table create commands support the commonly used [Flink create clauses](https://nightlies.apache.org/flink/flink-docs-release-{{ flinkVersionMajor }}/docs/dev/table/sql/create/) including: + +* `PARTITION BY (column1, column2, ...)` to configure partitioning, Flink does not yet support hidden partitioning. +* `COMMENT 'table document'` to set a table description. +* `WITH ('key'='value', ...)` to set [table configuration](configuration.md) which will be stored in Iceberg table properties. + +To specify the table location, use `WITH ('location'='fully-qualified-uri')`: + +```sql +CREATE TABLE `hive_catalog`.`default`.`sample` ( + id BIGINT COMMENT 'unique id', + data STRING NOT NULL +) WITH ( + 'format-version'='2', + 'location'='hdfs//nn:8020/custom-path' +); +``` + +Currently, it does not support computed column and watermark definition etc. + +#### `PRIMARY KEY` + +Primary key constraint can be declared for a column or a set of columns, which must be unique and do not contain null. +It's required for [`UPSERT` mode](flink-writes.md/#upsert). + +```sql +CREATE TABLE `hive_catalog`.`default`.`sample` ( + id BIGINT COMMENT 'unique id', + data STRING NOT NULL, + PRIMARY KEY(`id`) NOT ENFORCED +) WITH ('format-version'='2'); +``` + +#### `PARTITIONED BY` + +To create a partition table, use `PARTITIONED BY`: + +```sql +CREATE TABLE `hive_catalog`.`default`.`sample` ( + id BIGINT COMMENT 'unique id', + data STRING NOT NULL +) +PARTITIONED BY (data) +WITH ('format-version'='2'); +``` + +Iceberg supports hidden partitioning but Flink doesn't support partitioning by a function on columns. There is no way to support hidden partitions in the Flink DDL. + +### `CREATE TABLE LIKE` + +To create a table with the same schema, partitioning, and table properties as another table, use `CREATE TABLE LIKE`. + +```sql +CREATE TABLE `hive_catalog`.`default`.`sample` ( + id BIGINT COMMENT 'unique id', + data STRING +); + +CREATE TABLE `hive_catalog`.`default`.`sample_like` LIKE `hive_catalog`.`default`.`sample`; +``` + +For more details, refer to the [Flink `CREATE TABLE` documentation](https://nightlies.apache.org/flink/flink-docs-release-{{ flinkVersionMajor }}/docs/dev/table/sql/create/). + +### `ALTER TABLE` + +Iceberg only support altering table properties: + +```sql +ALTER TABLE `hive_catalog`.`default`.`sample` SET ('write.format.default'='avro'); +``` + +### `ALTER TABLE .. RENAME TO` + +```sql +ALTER TABLE `hive_catalog`.`default`.`sample` RENAME TO `hive_catalog`.`default`.`new_sample`; +``` + +### `DROP TABLE` + +To delete a table, run: + +```sql +DROP TABLE `hive_catalog`.`default`.`sample`; +``` diff --git a/1.11.0/docs/flink-maintenance.md b/1.11.0/docs/flink-maintenance.md new file mode 100644 index 000000000000..bdf26adbd1c9 --- /dev/null +++ b/1.11.0/docs/flink-maintenance.md @@ -0,0 +1,540 @@ +--- +title: "Flink TableMaintenance" +--- + + +## Flink Table Maintenance BatchMode + +### Rewrite files action + +Iceberg provides API to rewrite small files into large files by submitting Flink batch jobs. The behavior of this Flink action is the same as Spark's [rewriteDataFiles](maintenance.md#compact-data-files). + +```java +import org.apache.iceberg.flink.actions.Actions; + +TableLoader tableLoader = TableLoader.fromCatalog( + CatalogLoader.hive("my_catalog", configuration, properties), + TableIdentifier.of("database", "table") +); + +Table table = tableLoader.loadTable(); +RewriteDataFilesActionResult result = Actions.forTable(table) + .rewriteDataFiles() + .execute(); +``` + +For more details of the rewrite files action, please refer to [RewriteDataFilesAction](../../javadoc/{{ icebergVersion }}/org/apache/iceberg/flink/actions/RewriteDataFilesAction.html) + +## Flink Table Maintenance StreamingMode + +### Overview + +In **Apache Iceberg** deployments within **Flink streaming environments**, implementing automated table maintenance operations—including `snapshot expiration`, `small file compaction`, and `orphan file cleanup`—is critical for optimal query performance and storage efficiency. + +Traditionally, these maintenance operations were exclusively accessible through **Iceberg Spark Actions**, necessitating the deployment and management of dedicated Spark clusters. This dependency on **Spark infrastructure** solely for table optimization introduces significant **architectural complexity** and **operational overhead**. + +The `TableMaintenance` API in **Apache Iceberg** empowers **Flink jobs** to execute maintenance tasks **natively**, either embedded within existing streaming pipelines or deployed as standalone Flink jobs. This eliminates dependencies on external systems, thereby **streamlining architecture**, **reducing operational costs**, and **enhancing automation capabilities**. + +### Supported Features (Flink) + +#### ExpireSnapshots +Removes old snapshots and their files. Internally uses `cleanExpiredFiles(true)` when committing, so expired metadata/files are cleaned up automatically. + +```java +.add(ExpireSnapshots.builder() + .maxSnapshotAge(Duration.ofDays(7)) + .retainLast(10) + .deleteBatchSize(1000)) +``` + +#### RewriteDataFiles +Compacts small files to optimize file sizes. Supports partial progress commits and limiting maximum rewritten bytes per run. + +```java +.add(RewriteDataFiles.builder() + .targetFileSizeBytes(256 * 1024 * 1024) + .minFileSizeBytes(32 * 1024 * 1024) + .partialProgressEnabled(true) + .partialProgressMaxCommits(5)) +``` + +#### DeleteOrphanFiles +Used to remove files which are not referenced in any metadata files of an Iceberg table and can thus be considered "orphaned".The table location is checked for such files. + +```java +.add(DeleteOrphanFiles.builder() + .minAge(Duration.ofDays(3)) + .deleteBatchSize(1000)) +``` + +### Lock Management + +The `TriggerLockFactory` is essential for coordinating maintenance tasks. It prevents concurrent maintenance operations on the same table, which could lead to conflicts or data corruption. This locking mechanism is necessary even for a single job, as multiple instances of the same task could otherwise conflict. + +#### Why Locks Are Needed +- **Concurrent Access**: Multiple Flink jobs may attempt maintenance simultaneously +- **Data Consistency**: Ensures only one maintenance operation runs per table at a time +- **Resource Management**: Prevents resource conflicts and scheduling issues +- **Avoid Duplicate Work**: Even when only a single compaction job is scheduled, multiple instances could attempt the same operation, leading to redundant work and wasted resources. + +#### Supported Lock Types + +##### JDBC Lock Factory +Uses a database table to manage distributed locks: + +```java +Map jdbcProps = new HashMap<>(); +jdbcProps.put("jdbc.user", "flink"); +jdbcProps.put("jdbc.password", "flinkpw"); +jdbcProps.put("flink-maintenance.lock.jdbc.init-lock-tables", "true"); // Auto-create lock table if it doesn't exist + +TriggerLockFactory lockFactory = new JdbcLockFactory( + "jdbc:postgresql://localhost:5432/iceberg", // JDBC URL + "catalog.db.table", // Lock ID (unique identifier) + jdbcProps // JDBC connection properties +); +``` + +##### ZooKeeper Lock Factory +Uses Apache ZooKeeper for distributed locks: + +```java +TriggerLockFactory lockFactory = new ZkLockFactory( + "localhost:2181", // ZooKeeper connection string + "catalog.db.table", // Lock ID (unique identifier) + 60000, // sessionTimeoutMs + 15000, // connectionTimeoutMs + 3000, // baseSleepTimeMs + 3 // maxRetries +); +``` + +#### Flink-maintained lock + +Maintain the lock within Flink itself. This does not require configuring external systems. The only prerequisite is that there are no parallel table maintenance jobs for a given table. + +### Quick Start + +The following example demonstrates the implementation of automated maintenance for an Iceberg table within a Flink environment. + +```java +StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); + +TableLoader tableLoader = TableLoader.fromCatalog( + CatalogLoader.hive("my_catalog", configuration, properties), + TableIdentifier.of("database", "table") +); + +Map jdbcProps = new HashMap<>(); +jdbcProps.put("jdbc.user", "flink"); +jdbcProps.put("jdbc.password", "flinkpw"); + +// JdbcLockFactory Example +TriggerLockFactory lockFactory = new JdbcLockFactory( + "jdbc:postgresql://localhost:5432/iceberg", // JDBC URL + "catalog.db.table", // Lock ID (unique identifier) + jdbcProps // JDBC connection properties +); + +// Option 1: With external lock factory (plan to deprecate this Option since 1.12) +TableMaintenance.forTable(env, tableLoader, lockFactory) +// Option 2: With Flink-managed lock (no external lock required) +TableMaintenance.forTable(env, tableLoader) + .uidSuffix("my-maintenance-job") + .rateLimit(Duration.ofMinutes(10)) + .lockCheckDelay(Duration.ofSeconds(10)) + .add(ExpireSnapshots.builder() + .scheduleOnCommitCount(10) + .maxSnapshotAge(Duration.ofMinutes(10)) + .retainLast(5) + .deleteBatchSize(5) + .parallelism(8)) + .add(RewriteDataFiles.builder() + .scheduleOnDataFileCount(10) + .targetFileSizeBytes(128 * 1024 * 1024) + .partialProgressEnabled(true) + .partialProgressMaxCommits(10)) + .append(); + +env.execute("Table Maintenance Job"); +``` + +### Configuration Options + +#### TableMaintenance Builder + +| Method | Description | Default | +|--------|-------------|---------| +| `uidSuffix(String)` | Unique identifier suffix for the job | Random UUID | +| `rateLimit(Duration)` | Minimum interval between task executions | 60 seconds | +| `lockCheckDelay(Duration)` | Delay for checking lock availability | 30 seconds | +| `parallelism(int)` | Default parallelism for maintenance tasks | System default | +| `maxReadBack(int)` | Max snapshots to check during initialization | 100 | + +#### Maintenance Task Common Options + +| Method | Description | Default Value | Type | +|--------|-------------|---------------|------| +| `scheduleOnCommitCount(int)` | Trigger after N commits | No automatic scheduling | int | +| `scheduleOnDataFileCount(int)` | Trigger after N data files | No automatic scheduling | int | +| `scheduleOnDataFileSize(long)` | Trigger after total data file size (bytes) | No automatic scheduling | long | +| `scheduleOnPosDeleteFileCount(int)` | Trigger after N positional delete files | No automatic scheduling | int | +| `scheduleOnPosDeleteRecordCount(long)` | Trigger after N positional delete records | No automatic scheduling | long | +| `scheduleOnEqDeleteFileCount(int)` | Trigger after N equality delete files | No automatic scheduling | int | +| `scheduleOnEqDeleteRecordCount(long)` | Trigger after N equality delete records | No automatic scheduling | long | +| `scheduleOnInterval(Duration)` | Trigger after time interval | No automatic scheduling | Duration | + +#### ExpireSnapshots Configuration + +| Method | Description | Default Value | Type | +|--------|-------------|---------------|------| +| `maxSnapshotAge(Duration)` | Maximum age of snapshots to retain | 5 days | Duration | +| `retainLast(int)` | Minimum number of snapshots to retain | 1 | int | +| `deleteBatchSize(int)` | Number of files to delete in each batch | 1000 | int | +| `planningWorkerPoolSize(int)` | Number of worker threads for planning snapshot expiration | Shared worker pool | int | +| `cleanExpiredMetadata(boolean)` | Remove expired metadata files when expiring snapshots | true | boolean | + +#### RewriteDataFiles Configuration + +| Method | Description | Default Value | Type | +|--------|-------------|---------------|------| +| `targetFileSizeBytes(long)` | Target size for rewritten files | Table property or 512MB | long | +| `minFileSizeBytes(long)` | Minimum size of files eligible for compaction | 75% of target file size | long | +| `maxFileSizeBytes(long)` | Maximum size of files eligible for compaction | 180% of target file size | long | +| `minInputFiles(int)` | Minimum number of files to trigger rewrite | 5 | int | +| `deleteFileThreshold(int)` | Minimum delete-file count per data file to force rewrite | Integer.MAX_VALUE | int | +| `rewriteAll(boolean)` | Rewrite all data files regardless of thresholds | false | boolean | +| `maxFileGroupSizeBytes(long)` | Maximum total size of a file group | 107374182400 (100GB) | long | +| `maxFilesToRewrite(int)` | If this option is not specified, all eligible files will be rewritten | null | int | +| `partialProgressEnabled(boolean)` | Enable partial progress commits | false | boolean | +| `partialProgressMaxCommits(int)` | Maximum commits allowed for partial progress when partialProgressEnabled is true | 10 | int | +| `maxRewriteBytes(long)` | Maximum bytes to rewrite per execution | Long.MAX_VALUE | long | +| `filter(Expression)` | Filter expression for selecting files to rewrite | Expressions.alwaysTrue() | Expression | +| `maxFileGroupInputFiles(long)` | Maximum allowed number of input files within a file group | Long.MAX_VALUE | long | + +#### DeleteOrphanFiles Configuration + +| Method | Description | Default Value | Type | +|------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------|--------------------| +| `location(string)` | The location to start the recursive listing of the candidate files for removal. | Table's location | String | +| `usePrefixListing(boolean)` | When true, use prefix-based file listing via the SupportsPrefixOperations interface. The Table FileIO implementation must support SupportsPrefixOperations when this flag is enabled.(Note: Setting it to False will use a recursive method to obtain file information. If the underlying storage is object storage, it will repeatedly call the API to get the path.) | True | boolean | +| `prefixMismatchMode(PrefixMismatchMode)` | Action behavior when location prefixes (schemes/authorities) mismatch:
  • ERROR - throw an exception.
  • IGNORE - no action.
  • DELETE - delete files.
| ERROR | PrefixMismatchMode | +| `equalSchemes(Map)` | Mapping of file system schemes to be considered equal. Key is a comma-separated list of schemes and value is a scheme | "s3n"=>"s3","s3a"=>"s3" | Map | +| `equalAuthorities(Map)` | Mapping of file system authorities to be considered equal. Key is a comma-separated list of authorities and value is an authority. | Empty map | Map | +| `minAge(Duration)` | Remove orphan files created before this timestamp | 3 days ago | Duration | +| `planningWorkerPoolSize(int)` | Number of worker threads for planning snapshot expiration | Shared worker pool | int | + +### Complete Example + +```java +public class TableMaintenanceJob { + public static void main(String[] args) throws Exception { + StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); + env.enableCheckpointing(60000); // Enable checkpointing + + // Configure table loader + TableLoader tableLoader = TableLoader.fromCatalog( + CatalogLoader.hive("my_catalog", configuration), + TableIdentifier.of("database", "table") + ); + + // Set up JDBC lock factory + Map jdbcProps = new HashMap<>(); + jdbcProps.put("jdbc.user", "flink"); + jdbcProps.put("jdbc.password", "flinkpw"); + jdbcProps.put("flink-maintenance.lock.jdbc.init-lock-tables", "true"); + + TriggerLockFactory lockFactory = new JdbcLockFactory( + "jdbc:postgresql://localhost:5432/iceberg", + "catalog.db.table", + jdbcProps + ); + + // Set up maintenance with comprehensive configuration + TableMaintenance.forTable(env, tableLoader, lockFactory) + .uidSuffix("production-maintenance") + .rateLimit(Duration.ofMinutes(15)) + .lockCheckDelay(Duration.ofSeconds(30)) + .parallelism(4) + + // Daily snapshot cleanup + .add(ExpireSnapshots.builder() + .maxSnapshotAge(Duration.ofDays(7)) + .retainLast(10)) + + // Continuous file optimization + .add(RewriteDataFiles.builder() + .targetFileSizeBytes(256 * 1024 * 1024) + .minFileSizeBytes(32 * 1024 * 1024) + .scheduleOnDataFileCount(20) + .partialProgressEnabled(true) + .partialProgressMaxCommits(5) + .maxRewriteBytes(2L * 1024 * 1024 * 1024) + .parallelism(6)) + + // Delete orphans files created more than five days ago + .add(DeleteOrphanFiles.builder() + .minAge(Duration.ofDays(5))) + + .append(); + + env.execute("Iceberg Table Maintenance"); + } +} +``` + +### IcebergSink with Post-Commit Integration + +Apache Iceberg Sink V2 for Flink allows automatic execution of maintenance tasks after data is committed to the table, using the addPostCommitTopology(...) method. + +#### DataStream API + +##### Builder + +```java +IcebergSink.forRowData(dataStream) + .table(table) + .tableLoader(tableLoader) + .rewriteDataFiles(Map.of( + RewriteDataFilesConfig.MAX_BYTES, "1073741824")) + .expireSnapshots(Map.of( + ExpireSnapshotsConfig.RETAIN_LAST, "5", + ExpireSnapshotsConfig.MAX_SNAPSHOT_AGE_SECONDS, "604800")) + .deleteOrphanFiles(Map.of( + DeleteOrphanFilesConfig.MIN_AGE_SECONDS, "259200")) + .append(); +``` + +##### Config + +All maintenance tasks are configured through string properties: + +```java +Map flinkConf = new HashMap<>(); + +// Enable maintenance tasks +flinkConf.put("flink-maintenance.rewrite.enabled", "true"); +flinkConf.put("flink-maintenance.expire-snapshots.enabled", "true"); +flinkConf.put("flink-maintenance.delete-orphan-files.enabled", "true"); + +// Configure rewrite data files +flinkConf.put("flink-maintenance.rewrite.max-bytes", "1073741824"); + +// Configure expire snapshots +flinkConf.put("flink-maintenance.expire-snapshots.retain-last", "5"); +flinkConf.put("flink-maintenance.expire-snapshots.max-snapshot-age-seconds", "604800"); + +// Configure delete orphan files +flinkConf.put("flink-maintenance.delete-orphan-files.min-age-seconds", "259200"); + +// Configure JDBC lock settings (deprecated, lock configuration is no longer required for a single Flink job) +flinkConf.put("flink-maintenance.lock.type", "jdbc"); +flinkConf.put("flink-maintenance.lock.jdbc.uri", "jdbc:postgresql://localhost:5432/iceberg"); +flinkConf.put("flink-maintenance.lock.lock-id", "catalog.db.table"); + +IcebergSink.forRowData(dataStream) + .table(table) + .tableLoader(tableLoader) + .setAll(flinkConf) + .append(); +``` + +#### SQL Examples + +You can enable maintenance and configure locks using SQL before executing writes: + +```sql +-- Enable Iceberg V2 Sink and maintenance tasks +SET 'table.exec.iceberg.use.v2.sink' = 'true'; +SET 'flink-maintenance.rewrite.enabled' = 'true'; +SET 'flink-maintenance.expire-snapshots.enabled' = 'true'; +SET 'flink-maintenance.delete-orphan-files.enabled' = 'true'; + +-- Configure rewrite data files +SET 'flink-maintenance.rewrite.max-bytes' = '1073741824'; + +-- Configure expire snapshots +SET 'flink-maintenance.expire-snapshots.retain-last' = '5'; + +-- Configure delete orphan files +SET 'flink-maintenance.delete-orphan-files.min-age-seconds' = '259200'; + +-- Configure maintenance lock (JDBC) +SET 'flink-maintenance.lock.type' = 'jdbc'; +SET 'flink-maintenance.lock.lock-id' = 'catalog.db.table'; +SET 'flink-maintenance.lock.jdbc.uri' = 'jdbc:postgresql://localhost:5432/iceberg'; +SET 'flink-maintenance.lock.jdbc.init-lock-tables' = 'true'; + +-- Now run writes; maintenance will be scheduled post-commit +INSERT INTO db.tbl SELECT ...; +``` + +Or specify options in table DDL: + +```sql +CREATE TABLE db.tbl ( + ... +) WITH ( + 'connector' = 'iceberg', + 'catalog-name' = 'my_catalog', + 'catalog-database' = 'db', + 'catalog-table' = 'tbl', + 'flink-maintenance.rewrite.enabled' = 'true', + 'flink-maintenance.expire-snapshots.enabled' = 'true', + 'flink-maintenance.delete-orphan-files.enabled' = 'true', + + 'flink-maintenance.rewrite.max-bytes' = '1073741824', + 'flink-maintenance.expire-snapshots.retain-last' = '5', + 'flink-maintenance.delete-orphan-files.min-age-seconds' = '259200', + + 'flink-maintenance.lock.type' = 'jdbc', + 'flink-maintenance.lock.lock-id' = 'catalog.db.table', + 'flink-maintenance.lock.jdbc.uri' = 'jdbc:postgresql://localhost:5432/iceberg', + 'flink-maintenance.lock.jdbc.init-lock-tables' = 'true' +); +``` + +### IcebergSink Maintenance Configuration (SQL) + +These keys are used in SQL (SET or table WITH options) or via `IcebergSink.Builder.set()` / `setAll()`. + +#### Enable Flags + +| Key | Description | Default | +|-----|-------------|---------| +| `flink-maintenance.rewrite.enabled` | Enable compaction (rewrite data files) | `false` | +| `flink-maintenance.expire-snapshots.enabled` | Enable expire snapshots | `false` | +| `flink-maintenance.delete-orphan-files.enabled` | Enable delete orphan files | `false` | + +#### Rewrite Data Files Configuration + +| Key | Description | Default | +|-----|-------------|---------| +| `flink-maintenance.rewrite.schedule.commit-count` | Trigger after N commits | `10` | +| `flink-maintenance.rewrite.schedule.data-file-count` | Trigger after N data files | `1000` | +| `flink-maintenance.rewrite.schedule.data-file-size` | Trigger after total data file size (bytes) | `107374182400` (100GB) | +| `flink-maintenance.rewrite.schedule.interval-second` | Trigger after time interval (seconds) | `600` | +| `flink-maintenance.rewrite.max-bytes` | Maximum bytes to rewrite per execution | `Long.MAX_VALUE` | +| `flink-maintenance.rewrite.partial-progress.enabled` | Enable partial progress commits | `false` | +| `flink-maintenance.rewrite.partial-progress.max-commits` | Maximum commits for partial progress | `10` | + +#### Expire Snapshots Configuration + +| Key | Description | Default | +|-----|-------------|---------| +| `flink-maintenance.expire-snapshots.schedule.commit-count` | Trigger after N commits | `10` | +| `flink-maintenance.expire-snapshots.schedule.interval-second` | Trigger after time interval (seconds) | `3600` (1 hour) | +| `flink-maintenance.expire-snapshots.max-snapshot-age-seconds` | Maximum age of snapshots to retain (seconds) | Not set | +| `flink-maintenance.expire-snapshots.retain-last` | Minimum number of snapshots to retain | Not set | +| `flink-maintenance.expire-snapshots.delete-batch-size` | Batch size for deleting expired files | `1000` | +| `flink-maintenance.expire-snapshots.clean-expired-metadata` | Remove expired metadata (partition specs, schemas) | `true` | +| `flink-maintenance.expire-snapshots.planning-worker-pool-size` | Worker pool size for planning | Shared pool | + +#### Delete Orphan Files Configuration + +| Key | Description | Default | +|-----|-------------|---------| +| `flink-maintenance.delete-orphan-files.schedule.interval-second` | Trigger after time interval (seconds) | `3600` (1 hour) | +| `flink-maintenance.delete-orphan-files.min-age-seconds` | Minimum age of files to consider for deletion (seconds) | `259200` (3 days) | +| `flink-maintenance.delete-orphan-files.delete-batch-size` | Batch size for deleting orphan files | `1000` | +| `flink-maintenance.delete-orphan-files.location` | Location to start recursive listing | Table location | +| `flink-maintenance.delete-orphan-files.use-prefix-listing` | Use prefix listing for file discovery | `true` | +| `flink-maintenance.delete-orphan-files.planning-worker-pool-size` | Worker pool size for planning | Shared pool | +| `flink-maintenance.delete-orphan-files.equal-schemes` | Equivalent schemes (format: `s3n=s3,s3a=s3`) | `s3n=s3,s3a=s3` | +| `flink-maintenance.delete-orphan-files.equal-authorities` | Equivalent authorities (format: `auth1=auth2`) | Not set | +| `flink-maintenance.delete-orphan-files.prefix-mismatch-mode` | Behavior on prefix mismatch: `ERROR`, `IGNORE`, `DELETE` | `ERROR` | + +### Lock Configuration (SQL) + +These keys are used in SQL (SET or table WITH options) and are applicable when writing with maintenance enabled. + +- JDBC + +| Key | Description | Default | +|-----|-------------|---------| +| `flink-maintenance.lock.type` | Set to `jdbc` | | +| `flink-maintenance.lock.lock-id` | Unique lock ID per table | | +| `flink-maintenance.lock.jdbc.uri` | JDBC URI | | +| `flink-maintenance.lock.jdbc.init-lock-tables` | Auto-create lock table | `false` | + +- ZooKeeper + +| Key | Description | Default | +|-----|-------------|---------| +| `flink-maintenance.lock.type` | Set to `zookeeper` | | +| `flink-maintenance.lock.lock-id` | Unique lock ID per table | | +| `flink-maintenance.lock.zookeeper.uri` | ZK connection URI | | +| `flink-maintenance.lock.zookeeper.session-timeout-ms` | Session timeout (ms) | `60000` | +| `flink-maintenance.lock.zookeeper.connection-timeout-ms` | Connection timeout (ms) | `15000` | +| `flink-maintenance.lock.zookeeper.max-retries` | Max retries | `3` | +| `flink-maintenance.lock.zookeeper.base-sleep-ms` | Base sleep between retries (ms) | `3000` | +| `flink-maintenance.lock.zookeeper.max-sleep-ms` | Maximum sleep time (ms) between retries. Caps the exponential backoff delay. | `10000` | +| `flink-maintenance.lock.zookeeper.retry-policy` | Retry policy name for ZooKeeper client. Supported values include: ONE_TIME, N_TIME, BOUNDED_EXPONENTIAL_BACKOFF, UNTIL_ELAPSED, EXPONENTIAL_BACKOFF. | `EXPONENTIAL_BACKOFF` | + +- COORDINATOR LOCK + +| Key | Description | Default | +|-----|----------------------|---------| +| `flink-maintenance.lock.type` | Set to `` or not set | | + +### Best Practices + +#### Resource Management +- Use dedicated slot sharing groups for maintenance tasks +- Set appropriate parallelism based on cluster resources +- Enable checkpointing for fault tolerance + +#### Scheduling Strategy +- Avoid too frequent executions with `rateLimit` +- Use `scheduleOnCommitCount` for write-heavy tables +- Use `scheduleOnDataFileCount` for fine-grained control + +#### Performance Tuning +- Adjust `deleteBatchSize` based on storage performance +- Enable `partialProgressEnabled` for large rewrite operations +- Set reasonable `maxRewriteBytes` limits +- Setting an appropriate `maxFileGroupSizeBytes` can break down large FileGroups into smaller ones, thereby increasing the speed of parallel processing + +### Troubleshooting + +#### OutOfMemoryError during file deletion +**Scenario:** This can occur when the maintenance task attempts to delete a very large number of files in a single batch, especially in tables with long retention histories or after bulk deletions. +**Cause:** Each file deletion involves metadata and object store operations, which together can consume significant memory. Large batches magnify this effect and may exhaust the JVM heap. +**Recommendation:** Reduce the batch size to limit memory usage during deletion. +```java +.deleteBatchSize(500) // Example: 500 files per batch +``` + +#### Lock conflicts +**Scenario:** In multi-job or high-availability environments, two or more Flink jobs may attempt maintenance on the same table simultaneously. +**Cause:** Concurrent jobs compete for the same distributed lock, causing retries and possible delays. +**Recommendation:** Increase lock check delay and rate limit so that failed attempts back off and reduce contention. +```java +.lockCheckDelay(Duration.ofMinutes(1)) // Wait longer before re-checking lock +.rateLimit(Duration.ofMinutes(10)) // Reduce frequency of task execution +``` + +#### Slow rewrite operations +**Scenario:** Large tables with many small files can require rewriting terabytes of data in a single run, which may overwhelm available resources. +**Cause:** Without limits, rewrite tasks attempt to process all eligible files at once, leading to long execution times and possible job failures. +**Recommendation:** Enable partial progress so that rewritten files can be committed in smaller batches, and cap the maximum data rewritten in each execution. +```java +.partialProgressEnabled(true) // Commit progress incrementally +.partialProgressMaxCommits(3) // Allow up to 3 commits per run +.maxRewriteBytes(1L * 1024 * 1024 * 1024) // Limit to ~1GB per run +``` diff --git a/1.11.0/docs/flink-queries.md b/1.11.0/docs/flink-queries.md new file mode 100644 index 000000000000..8518f2f40339 --- /dev/null +++ b/1.11.0/docs/flink-queries.md @@ -0,0 +1,559 @@ +--- +title: "Flink Queries" +--- + + +# Flink Queries + +Iceberg support streaming and batch read With [Apache Flink](https://flink.apache.org/)'s DataStream API and Table API. + +## Reading with SQL + +Iceberg support both streaming and batch read in Flink. Execute the following sql command to switch execution mode from `streaming` to `batch`, and vice versa: + +```sql +-- Execute the flink job in streaming mode for current session context +SET execution.runtime-mode = streaming; + +-- Execute the flink job in batch mode for current session context +SET execution.runtime-mode = batch; +``` + +### Flink batch read + +Submit a Flink __batch__ job using the following sentences: + +```sql +-- Execute the flink job in batch mode for current session context +SET execution.runtime-mode = batch; +SELECT * FROM sample; +``` + +### Flink streaming read + +Iceberg supports processing incremental data in Flink streaming jobs which starts from a historical snapshot-id: + +```sql +-- Submit the flink job in streaming mode for current session. +SET execution.runtime-mode = streaming; + +-- Enable this switch because streaming read SQL will provide few job options in flink SQL hint options. +SET table.dynamic-table-options.enabled=true; + +-- Read all the records from the iceberg current snapshot, and then read incremental data starting from that snapshot. +SELECT * FROM sample /*+ OPTIONS('streaming'='true', 'monitor-interval'='1s')*/ ; + +-- Read all incremental data starting from the snapshot-id '3821550127947089987' (records from this snapshot will be excluded). +SELECT * FROM sample /*+ OPTIONS('streaming'='true', 'monitor-interval'='1s', 'start-snapshot-id'='3821550127947089987')*/ ; +``` + +There are some options that could be set in Flink SQL hint options for streaming job, see [read options](#read-options) for details. + +### FLIP-27 source for SQL + +Here is the SQL setting to opt in or out of the +[FLIP-27 source](https://cwiki.apache.org/confluence/display/FLINK/FLIP-27%3A+Refactor+Source+Interface). + +```sql +-- Opt out the FLIP-27 source. +-- Default is false for Flink 1.19 and below, and true for Flink 1.20 and above. +SET table.exec.iceberg.use-flip27-source = false; +``` + +All other SQL settings and options documented above are applicable to the FLIP-27 source. + +### Reading branches and tags with SQL +Branch and tags can be read via SQL by specifying options. For more details +refer to [Flink Configuration](flink-configuration.md#read-options) + +```sql +--- Read from branch b1 +SELECT * FROM table /*+ OPTIONS('branch'='b1') */ ; + +--- Read from tag t1 +SELECT * FROM table /*+ OPTIONS('tag'='t1') */; + +--- Incremental scan from tag t1 to tag t2 +SELECT * FROM table /*+ OPTIONS('streaming'='true', 'monitor-interval'='1s', 'start-tag'='t1', 'end-tag'='t2') */; +``` + +## Reading with DataStream + +Iceberg support streaming or batch read in Java API now. + +### Batch Read + +This example will read all records from iceberg table and then print to the stdout console in flink batch job: + +```java +StreamExecutionEnvironment env = StreamExecutionEnvironment.createLocalEnvironment(); +TableLoader tableLoader = TableLoader.fromHadoopTable("hdfs://nn:8020/warehouse/path"); +DataStream batch = FlinkSource.forRowData() + .env(env) + .tableLoader(tableLoader) + .streaming(false) + .build(); + +// Print all records to stdout. +batch.print(); + +// Submit and execute this batch read job. +env.execute("Test Iceberg Batch Read"); +``` + +### Streaming read + +This example will read incremental records which start from snapshot-id '3821550127947089987' and print to stdout console in flink streaming job: + +```java +StreamExecutionEnvironment env = StreamExecutionEnvironment.createLocalEnvironment(); +TableLoader tableLoader = TableLoader.fromHadoopTable("hdfs://nn:8020/warehouse/path"); +DataStream stream = FlinkSource.forRowData() + .env(env) + .tableLoader(tableLoader) + .streaming(true) + .startSnapshotId(3821550127947089987L) + .build(); + +// Print all records to stdout. +stream.print(); + +// Submit and execute this streaming read job. +env.execute("Test Iceberg Streaming Read"); +``` + +There are other options that can be set, please see the [FlinkSource#Builder](../../javadoc/{{ icebergVersion }}/org/apache/iceberg/flink/source/FlinkSource.html). + +## Reading with DataStream (FLIP-27 source) + +[FLIP-27 source interface](https://cwiki.apache.org/confluence/display/FLINK/FLIP-27%3A+Refactor+Source+Interface) +was introduced in Flink 1.12. It aims to solve several shortcomings of the old `SourceFunction` +streaming source interface. It also unifies the source interfaces for both batch and streaming executions. +Most source connectors (like Kafka, file) in Flink repo have migrated to the FLIP-27 interface. +Flink is planning to deprecate the old `SourceFunction` interface in the near future. + +A FLIP-27 based Flink `IcebergSource` is added in `iceberg-flink` module. The FLIP-27 `IcebergSource` is currently an experimental feature. + +### Batch Read + +This example will read all records from iceberg table and then print to the stdout console in flink batch job: + +```java +StreamExecutionEnvironment env = StreamExecutionEnvironment.createLocalEnvironment(); +TableLoader tableLoader = TableLoader.fromHadoopTable("hdfs://nn:8020/warehouse/path"); + +IcebergSource source = IcebergSource.forRowData() + .tableLoader(tableLoader) + .assignerFactory(new SimpleSplitAssignerFactory()) + .build(); + +DataStream batch = env.fromSource( + source, + WatermarkStrategy.noWatermarks(), + "My Iceberg Source", + TypeInformation.of(RowData.class)); + +// Print all records to stdout. +batch.print(); + +// Submit and execute this batch read job. +env.execute("Test Iceberg Batch Read"); +``` + +### Streaming read + +This example will start the streaming read from the latest table snapshot (inclusive). +Every 60s, it polls Iceberg table to discover new append-only snapshots. +CDC read is not supported yet. + +```java +StreamExecutionEnvironment env = StreamExecutionEnvironment.createLocalEnvironment(); +TableLoader tableLoader = TableLoader.fromHadoopTable("hdfs://nn:8020/warehouse/path"); + +IcebergSource source = IcebergSource.forRowData() + .tableLoader(tableLoader) + .assignerFactory(new SimpleSplitAssignerFactory()) + .streaming(true) + .streamingStartingStrategy(StreamingStartingStrategy.INCREMENTAL_FROM_LATEST_SNAPSHOT) + .monitorInterval(Duration.ofSeconds(60)) + .build(); + +DataStream stream = env.fromSource( + source, + WatermarkStrategy.noWatermarks(), + "My Iceberg Source", + TypeInformation.of(RowData.class)); + +// Print all records to stdout. +stream.print(); + +// Submit and execute this streaming read job. +env.execute("Test Iceberg Streaming Read"); +``` + +There are other options that could be set by Java API, please see the +[IcebergSource#Builder](../../javadoc/{{ icebergVersion }}/org/apache/iceberg/flink/source/IcebergSource.html). + +### Reading branches and tags with DataStream +Branches and tags can also be read via the DataStream API + +```java +StreamExecutionEnvironment env = StreamExecutionEnvironment.createLocalEnvironment(); +TableLoader tableLoader = TableLoader.fromHadoopTable("hdfs://nn:8020/warehouse/path"); +// Read from branch +DataStream batch = FlinkSource.forRowData() + .env(env) + .tableLoader(tableLoader) + .branch("test-branch") + .streaming(false) + .build(); + +// Read from tag +DataStream batch = FlinkSource.forRowData() + .env(env) + .tableLoader(tableLoader) + .tag("test-tag") + .streaming(false) + .build(); + +// Streaming read from start-tag +DataStream batch = FlinkSource.forRowData() + .env(env) + .tableLoader(tableLoader) + .streaming(true) + .startTag("test-tag") + .build(); +``` + +### Read as Avro GenericRecord + +FLIP-27 Iceberg source provides `AvroGenericRecordReaderFunction` that converts +Flink `RowData` Avro `GenericRecord`. You can use the convert to read from +Iceberg table as Avro GenericRecord DataStream. + +Please make sure `flink-avro` jar is included in the classpath. +Also `iceberg-flink-runtime` shaded bundle jar can't be used +because the runtime jar shades the avro package. +Please use non-shaded `iceberg-flink` jar instead. + +```java +TableLoader tableLoader = ...; +Table table; +try (TableLoader loader = tableLoader) { + loader.open(); + table = loader.loadTable(); +} + +AvroGenericRecordReaderFunction readerFunction = AvroGenericRecordReaderFunction.fromTable(table); + +IcebergSource source = + IcebergSource.builder() + .tableLoader(tableLoader) + .readerFunction(readerFunction) + .assignerFactory(new SimpleSplitAssignerFactory()) + ... + .build(); + +DataStream stream = env.fromSource(source, WatermarkStrategy.noWatermarks(), + "Iceberg Source as Avro GenericRecord", new GenericRecordAvroTypeInfo(avroSchema)); +``` + +### Emitting watermarks +Emitting watermarks from the source itself could be beneficial for several purposes, like harnessing the +[Flink Watermark Alignment](https://nightlies.apache.org/flink/flink-docs-release-{{ flinkVersionMajor }}/docs/dev/datastream/event-time/generating_watermarks/#watermark-alignment), +or prevent triggering [windows](https://nightlies.apache.org/flink/flink-docs-release-{{ flinkVersionMajor }}/docs/dev/datastream/operators/windows/) +too early when reading multiple data files concurrently. + +Enable watermark generation for an `IcebergSource` by setting the `watermarkColumn`. +The supported column types are `timestamp`, `timestamptz` and `long`. +Iceberg `timestamp` or `timestamptz` inherently contains the time precision. So there is no need +to specify the time unit. But `long` type column doesn't contain time unit information. Use +`watermarkTimeUnit` to configure the conversion for long columns. + +The watermarks are generated based on column metrics stored for data files and emitted once per split. +If multiple smaller files with different time ranges are combined into a single split, it can increase +the out-of-orderliness and extra data buffering in the Flink state. The main purpose of watermark alignment +is to reduce out-of-orderliness and excess data buffering in the Flink state. Hence it is recommended to +set `read.split.open-file-cost` to a very large value to prevent combining multiple smaller files into a +single split. The negative impact (of not combining small files into a single split) is on read throughput, +especially if there are many small files. In typical stateful processing jobs, source read throughput is not +the bottleneck. Hence this is probably a reasonable tradeoff. + +This feature requires column-level min-max stats. Make sure stats are generated for the watermark column +during write phase. By default, the column metrics are collected for the first 100 columns of the table. +If watermark column doesn't have stats enabled by default, use +[write properties](configuration.md#write-properties) starting with `write.metadata.metrics` when needed. + +The following example could be useful if watermarks are used for windowing. The source reads Iceberg data files +in order, using a timestamp column and emits watermarks: +```java +StreamExecutionEnvironment env = StreamExecutionEnvironment.createLocalEnvironment(); +TableLoader tableLoader = TableLoader.fromHadoopTable("hdfs://nn:8020/warehouse/path"); + +DataStream stream = + env.fromSource( + IcebergSource.forRowData() + .tableLoader(tableLoader) + // Watermark using timestamp column + .watermarkColumn("timestamp_column") + .build(), + // Watermarks are generated by the source, no need to generate it manually + WatermarkStrategy.noWatermarks() + // Extract event timestamp from records + .withTimestampAssigner((record, eventTime) -> record.getTimestamp(pos, precision).getMillisecond()), + SOURCE_NAME, + TypeInformation.of(RowData.class)); +``` + +Example for reading Iceberg table using a long event column for watermark alignment: +```java +StreamExecutionEnvironment env = StreamExecutionEnvironment.createLocalEnvironment(); +TableLoader tableLoader = TableLoader.fromHadoopTable("hdfs://nn:8020/warehouse/path"); + +DataStream stream = + env.fromSource( + IcebergSource source = IcebergSource.forRowData() + .tableLoader(tableLoader) + // Disable combining multiple files to a single split + .set(FlinkReadOptions.SPLIT_FILE_OPEN_COST, String.valueOf(TableProperties.SPLIT_SIZE_DEFAULT)) + // Watermark using long column + .watermarkColumn("long_column") + .watermarkTimeUnit(TimeUnit.MILLI_SCALE) + .build(), + // Watermarks are generated by the source, no need to generate it manually + WatermarkStrategy.noWatermarks() + .withWatermarkAlignment(watermarkGroup, maxAllowedWatermarkDrift), + SOURCE_NAME, + TypeInformation.of(RowData.class)); +``` + +## Options + +### Read options + +Flink read options are passed when configuring the Flink IcebergSource: + +``` +IcebergSource.forRowData() + .tableLoader(TableLoader.fromCatalog(...)) + .assignerFactory(new SimpleSplitAssignerFactory()) + .streaming(true) + .streamingStartingStrategy(StreamingStartingStrategy.INCREMENTAL_FROM_LATEST_SNAPSHOT) + .startSnapshotId(3821550127947089987L) + .monitorInterval(Duration.ofMillis(10L)) // or .set("monitor-interval", "10s") \ set(FlinkReadOptions.MONITOR_INTERVAL, "10s") + .build() +``` + +For Flink SQL, read options can be passed in via SQL hints like this: + +``` +SELECT * FROM tableName /*+ OPTIONS('monitor-interval'='10s') */ +... +``` + +Options can be passed in via Flink configuration, which will be applied to current session. Note that not all options support this mode. + +``` +env.getConfig() + .getConfiguration() + .set(FlinkReadOptions.SPLIT_FILE_OPEN_COST_OPTION, 1000L); +... +``` + +Check out all the options here: [read-options](flink-configuration.md#read-options) + +## Inspecting tables + +To inspect a table's history, snapshots, and other metadata, Iceberg supports metadata tables. + +Metadata tables are identified by adding the metadata table name after the original table name. For example, history for `db.table` is read using `db.table$history`. + +### History + +To show table history: + +```sql +SELECT * FROM prod.db.table$history; +``` + +| made_current_at | snapshot_id | parent_id | is_current_ancestor | +| ----------------------- | ------------------- | ------------------- | ------------------- | +| 2019-02-08 03:29:51.215 | 5781947118336215154 | NULL | true | +| 2019-02-08 03:47:55.948 | 5179299526185056830 | 5781947118336215154 | true | +| 2019-02-09 16:24:30.13 | 296410040247533544 | 5179299526185056830 | false | +| 2019-02-09 16:32:47.336 | 2999875608062437330 | 5179299526185056830 | true | +| 2019-02-09 19:42:03.919 | 8924558786060583479 | 2999875608062437330 | true | +| 2019-02-09 19:49:16.343 | 6536733823181975045 | 8924558786060583479 | true | + +!!! info + **This shows a commit that was rolled back.** In this example, snapshot 296410040247533544 and 2999875608062437330 have the same parent snapshot 5179299526185056830. Snapshot 296410040247533544 was rolled back and is *not* an ancestor of the current table state. + +### Metadata Log Entries + +To show table metadata log entries: + +```sql +SELECT * from prod.db.table$metadata_log_entries; +``` + +| timestamp | file | latest_snapshot_id | latest_schema_id | latest_sequence_number | +| ----------------------- | ------------------------------------------------------------ | ------------------ | ---------------- | ---------------------- | +| 2022-07-28 10:43:52.93 | s3://.../table/metadata/00000-9441e604-b3c2-498a-a45a-6320e8ab9006.metadata.json | null | null | null | +| 2022-07-28 10:43:57.487 | s3://.../table/metadata/00001-f30823df-b745-4a0a-b293-7532e0c99986.metadata.json | 170260833677645300 | 0 | 1 | +| 2022-07-28 10:43:58.25 | s3://.../table/metadata/00002-2cc2837a-02dc-4687-acc1-b4d86ea486f4.metadata.json | 958906493976709774 | 0 | 2 | + +### Snapshots + +To show the valid snapshots for a table: + +```sql +SELECT * FROM prod.db.table$snapshots; +``` + +| committed_at | snapshot_id | parent_id | operation | manifest_list | summary | +| ----------------------- | -------------- | --------- | --------- | -------------------------------------------------- | ------------------------------------------------------------ | +| 2019-02-08 03:29:51.215 | 57897183625154 | null | append | s3://.../table/metadata/snap-57897183625154-1.avro | { added-records -> 2478404, total-records -> 2478404, added-data-files -> 438, total-data-files -> 438, flink.job-id -> 2e274eecb503d85369fb390e8956c813 } | + +You can also join snapshots to table history. For example, this query will show table history, with the application ID that wrote each snapshot: + +```sql +select + h.made_current_at, + s.operation, + h.snapshot_id, + h.is_current_ancestor, + s.summary['flink.job-id'] +from prod.db.table$history h +join prod.db.table$snapshots s + on h.snapshot_id = s.snapshot_id +order by made_current_at; +``` + +| made_current_at | operation | snapshot_id | is_current_ancestor | summary[flink.job-id] | +| ----------------------- | --------- | -------------- | ------------------- | -------------------------------- | +| 2019-02-08 03:29:51.215 | append | 57897183625154 | true | 2e274eecb503d85369fb390e8956c813 | + +### Files + +To show a table's current data files: + +```sql +SELECT * FROM prod.db.table$files; +``` + +| content | file_path | file_format | spec_id | partition | record_count | file_size_in_bytes | column_sizes | value_counts | null_value_counts | nan_value_counts | lower_bounds | upper_bounds | key_metadata | split_offsets | equality_ids | sort_order_id | +| ------- | ------------------------------------------------------------ | ----------- | ------- | ---------------- | ------------ | ------------------ | ------------------ | ---------------- | ----------------- | ---------------- | --------------- | --------------- | ------------ | ------------- | ------------ | ------------- | +| 0 | s3:/.../table/data/00000-3-8d6d60e8-d427-4809-bcf0-f5d45a4aad96.parquet | PARQUET | 0 | {1999-01-01, 01} | 1 | 597 | [1 -> 90, 2 -> 62] | [1 -> 1, 2 -> 1] | [1 -> 0, 2 -> 0] | [] | [1 -> , 2 -> c] | [1 -> , 2 -> c] | null | [4] | null | null | +| 0 | s3:/.../table/data/00001-4-8d6d60e8-d427-4809-bcf0-f5d45a4aad96.parquet | PARQUET | 0 | {1999-01-01, 02} | 1 | 597 | [1 -> 90, 2 -> 62] | [1 -> 1, 2 -> 1] | [1 -> 0, 2 -> 0] | [] | [1 -> , 2 -> b] | [1 -> , 2 -> b] | null | [4] | null | null | +| 0 | s3:/.../table/data/00002-5-8d6d60e8-d427-4809-bcf0-f5d45a4aad96.parquet | PARQUET | 0 | {1999-01-01, 03} | 1 | 597 | [1 -> 90, 2 -> 62] | [1 -> 1, 2 -> 1] | [1 -> 0, 2 -> 0] | [] | [1 -> , 2 -> a] | [1 -> , 2 -> a] | null | [4] | null | null | + +### Manifests + +To show a table's current file manifests: + +```sql +SELECT * FROM prod.db.table$manifests; +``` + +| path | length | partition_spec_id | added_snapshot_id | added_data_files_count | existing_data_files_count | deleted_data_files_count | partition_summaries | +| ------------------------------------------------------------ | ------ | ----------------- | ------------------- | ---------------------- | ------------------------- | ------------------------ | ------------------------------------ | +| s3://.../table/metadata/45b5290b-ee61-4788-b324-b1e2735c0e10-m0.avro | 4479 | 0 | 6668963634911763636 | 8 | 0 | 0 | [[false,null,2019-05-13,2019-05-15]] | + +Note: + +1. Fields within `partition_summaries` column of the manifests table correspond to `field_summary` structs within [manifest list](../../spec.md#manifest-lists), with the following order: + - `contains_null` + - `contains_nan` + - `lower_bound` + - `upper_bound` +2. `contains_nan` could return null, which indicates that this information is not available from the file's metadata. + This usually occurs when reading from V1 table, where `contains_nan` is not populated. + +### Partitions + +To show a table's current partitions: + +```sql +SELECT * FROM prod.db.table$partitions; +``` + +| partition | spec_id | record_count | file_count | total_data_file_size_in_bytes | position_delete_record_count | position_delete_file_count | equality_delete_record_count | equality_delete_file_count | last_updated_at(μs) | last_updated_snapshot_id | +| -------------- |---------|---------------|------------|--------------------------|------------------------------|----------------------------|------------------------------|----------------------------|---------------------|--------------------------| +| {20211001, 11} | 0 | 1 | 1 | 100 | 2 | 1 | 0 | 0 | 1633086034192000 | 9205185327307503337 | +| {20211002, 11} | 0 | 4 | 3 | 500 | 1 | 1 | 0 | 0 | 1633172537358000 | 867027598972211003 | +| {20211001, 10} | 0 | 7 | 4 | 700 | 0 | 0 | 0 | 0 | 1633082598716000 | 3280122546965981531 | +| {20211002, 10} | 0 | 3 | 2 | 400 | 0 | 0 | 1 | 1 | 1633169159489000 | 6941468797545315876 | + +Note: +For unpartitioned tables, the partitions table will not contain the partition and spec_id fields. + +### All Metadata Tables + +These tables are unions of the metadata tables specific to the current snapshot, and return metadata across all snapshots. + +!!! danger + The "all" metadata tables may produce more than one row per data file or manifest file because metadata files may be part of more than one table snapshot. + +#### All Data Files + +To show all of the table's data files and each file's metadata: + +```sql +SELECT * FROM prod.db.table$all_data_files; +``` + +| content | file_path | file_format | partition | record_count | file_size_in_bytes | column_sizes | value_counts | null_value_counts | nan_value_counts | lower_bounds | upper_bounds | key_metadata | split_offsets | equality_ids | sort_order_id | +| ------- | ------------------------------------------------------------ | ----------- | ---------- | ------------ | ------------------ | ------------------ | ------------------ | ----------------- | ---------------- | ----------------------- | ----------------------- | ------------ | ------------- | ------------ | ------------- | +| 0 | s3://.../dt=20210102/00000-0-756e2512-49ae-45bb-aae3-c0ca475e7879-00001.parquet | PARQUET | {20210102} | 14 | 2444 | {1 -> 94, 2 -> 17} | {1 -> 14, 2 -> 14} | {1 -> 0, 2 -> 0} | {} | {1 -> 1, 2 -> 20210102} | {1 -> 2, 2 -> 20210102} | null | [4] | null | 0 | +| 0 | s3://.../dt=20210103/00000-0-26222098-032f-472b-8ea5-651a55b21210-00001.parquet | PARQUET | {20210103} | 14 | 2444 | {1 -> 94, 2 -> 17} | {1 -> 14, 2 -> 14} | {1 -> 0, 2 -> 0} | {} | {1 -> 1, 2 -> 20210103} | {1 -> 3, 2 -> 20210103} | null | [4] | null | 0 | +| 0 | s3://.../dt=20210104/00000-0-a3bb1927-88eb-4f1c-bc6e-19076b0d952e-00001.parquet | PARQUET | {20210104} | 14 | 2444 | {1 -> 94, 2 -> 17} | {1 -> 14, 2 -> 14} | {1 -> 0, 2 -> 0} | {} | {1 -> 1, 2 -> 20210104} | {1 -> 3, 2 -> 20210104} | null | [4] | null | 0 | + +#### All Manifests + +To show all of the table's manifest files: + +```sql +SELECT * FROM prod.db.table$all_manifests; +``` + +| path | length | partition_spec_id | added_snapshot_id | added_data_files_count | existing_data_files_count | deleted_data_files_count | partition_summaries | +| ------------------------------------------------------------ | ------ | ----------------- | ------------------- | ---------------------- | ------------------------- | ------------------------ | ------------------------------------ | +| s3://.../metadata/a85f78c5-3222-4b37-b7e4-faf944425d48-m0.avro | 6376 | 0 | 6272782676904868561 | 2 | 0 | 0 | [{false, false, 20210101, 20210101}] | + +Note: + +1. Fields within `partition_summaries` column of the manifests table correspond to `field_summary` structs within [manifest list](../../spec.md#manifest-lists), with the following order: + - `contains_null` + - `contains_nan` + - `lower_bound` + - `upper_bound` +2. `contains_nan` could return null, which indicates that this information is not available from the file's metadata. + This usually occurs when reading from V1 table, where `contains_nan` is not populated. + +### References + +To show a table's known snapshot references: + +```sql +SELECT * FROM prod.db.table$refs; +``` + +| name | type | snapshot_id | max_reference_age_in_ms | min_snapshots_to_keep | max_snapshot_age_in_ms | +| ------- | ------ | ------------------- | ----------------------- | --------------------- | ---------------------- | +| main | BRANCH | 4686954189838128572 | 10 | 20 | 30 | +| testTag | TAG | 4686954189838128572 | 10 | null | null | diff --git a/1.11.0/docs/flink-writes.md b/1.11.0/docs/flink-writes.md new file mode 100644 index 000000000000..03795b5beed0 --- /dev/null +++ b/1.11.0/docs/flink-writes.md @@ -0,0 +1,582 @@ +--- +title: "Flink Writes" +--- + +# Flink Writes + +Iceberg support batch and streaming writes with [Apache Flink](https://flink.apache.org/)'s DataStream API and Table API. + +The Flink Iceberg sink guarantees exactly-once semantics. + +## Writing with SQL + +Iceberg support both `INSERT INTO` and `INSERT OVERWRITE`. + +### `INSERT INTO` + +To append new data to a table with a Flink streaming job, use `INSERT INTO`: + +```sql +INSERT INTO `hive_catalog`.`default`.`sample` VALUES (1, 'a'); +INSERT INTO `hive_catalog`.`default`.`sample` SELECT id, data from other_kafka_table; +``` + +### `INSERT OVERWRITE` + +To replace data in the table with the result of a query, use `INSERT OVERWRITE` in batch job (flink streaming job does not support `INSERT OVERWRITE`). Overwrites are atomic operations for Iceberg tables. + +Partitions that have rows produced by the SELECT query will be replaced, for example: + +```sql +INSERT OVERWRITE sample VALUES (1, 'a'); +``` + +Iceberg also support overwriting given partitions by the `select` values: + +```sql +INSERT OVERWRITE `hive_catalog`.`default`.`sample` PARTITION(data='a') SELECT 6; +``` + +For a partitioned iceberg table, when all the partition columns are set a value in `PARTITION` clause, it is inserting into a static partition, otherwise if partial partition columns (prefix part of all partition columns) are set a value in `PARTITION` clause, it is writing the query result into a dynamic partition. +For an unpartitioned iceberg table, its data will be completely overwritten by `INSERT OVERWRITE`. + +### `UPSERT` + +Iceberg supports `UPSERT` based on the primary key when writing data into v2 table format. There are two ways to enable upsert. + +1. Enable the `UPSERT` mode as table-level property `write.upsert.enabled`. Here is an example SQL statement to set the table property when creating a table. It would be applied for all write paths to this table (batch or streaming) unless overwritten by write options as described later. + + ```sql + CREATE TABLE `hive_catalog`.`default`.`sample` ( + `id` INT COMMENT 'unique id', + `data` STRING NOT NULL, + PRIMARY KEY(`id`) NOT ENFORCED + ) with ('format-version'='2', 'write.upsert.enabled'='true'); + ``` + +2. Enabling `UPSERT` mode using `upsert-enabled` in the [write options](#write-options) provides more flexibility than a table level config. Note that you still need to use v2 table format and specify the [primary key](flink-ddl.md/#primary-key) or [identifier fields](../../spec.md#identifier-field-ids) when creating the table. + + ```sql + INSERT INTO tableName /*+ OPTIONS('upsert-enabled'='true') */ + ... + ``` + +!!! info + OVERWRITE and UPSERT modes are mutually exclusive and cannot be enabled at the same time. When using UPSERT mode with a partitioned table, source columns of corresponding partition fields must be included in the equality fields. For example, if the partition field is `days(ts)`, then `ts` must be part of the equality fields. + +## Writing with DataStream + +Iceberg support writing to iceberg table from different DataStream input. + +### Appending data + +Flink supports writing `DataStream` and `DataStream` to the sink iceberg table natively. + +```java +StreamExecutionEnvironment env = ...; + +DataStream input = ... ; +Configuration hadoopConf = new Configuration(); +TableLoader tableLoader = TableLoader.fromHadoopTable("hdfs://nn:8020/warehouse/path", hadoopConf); + +FlinkSink.forRowData(input) + .tableLoader(tableLoader) + .append(); + +env.execute("Test Iceberg DataStream"); +``` + +### Overwrite data + +Set the `overwrite` flag in FlinkSink builder to overwrite the data in existing iceberg tables: + +```java +StreamExecutionEnvironment env = ...; + +DataStream input = ... ; +Configuration hadoopConf = new Configuration(); +TableLoader tableLoader = TableLoader.fromHadoopTable("hdfs://nn:8020/warehouse/path", hadoopConf); + +FlinkSink.forRowData(input) + .tableLoader(tableLoader) + .overwrite(true) + .append(); + +env.execute("Test Iceberg DataStream"); +``` + +### Upsert data + +Set the `upsert` flag in FlinkSink builder to upsert the data in existing iceberg table. The table must use v2 table format and have a primary key. + +```java +StreamExecutionEnvironment env = ...; + +DataStream input = ... ; +Configuration hadoopConf = new Configuration(); +TableLoader tableLoader = TableLoader.fromHadoopTable("hdfs://nn:8020/warehouse/path", hadoopConf); + +FlinkSink.forRowData(input) + .tableLoader(tableLoader) + .upsert(true) + .append(); + +env.execute("Test Iceberg DataStream"); +``` + +!!! info + OVERWRITE and UPSERT modes are mutually exclusive and cannot be enabled at the same time. When using UPSERT mode with a partitioned table, source columns of corresponding partition fields must be included in the equality fields. For example, if the partition field is `days(ts)`, then `ts` must be part of the equality fields. + +### Write with Avro GenericRecord + +Flink Iceberg sink provides `AvroGenericRecordToRowDataMapper` that converts +Avro `GenericRecord` to Flink `RowData`. You can use the mapper to write +Avro GenericRecord DataStream to Iceberg. + +Please make sure `flink-avro` jar is included in the classpath. +Also `iceberg-flink-runtime` shaded bundle jar can't be used +because the runtime jar shades the avro package. +Please use non-shaded `iceberg-flink` jar instead. + +```java +DataStream dataStream = ...; + +Schema icebergSchema = table.schema(); + + +// The Avro schema converted from Iceberg schema can't be used +// due to precision difference between how Iceberg schema (micro) +// and Flink AvroToRowDataConverters (milli) deal with time type. +// Instead, use the Avro schema defined directly. +// See AvroGenericRecordToRowDataMapper Javadoc for more details. +org.apache.avro.Schema avroSchema = AvroSchemaUtil.convert(icebergSchema, table.name()); + +GenericRecordAvroTypeInfo avroTypeInfo = new GenericRecordAvroTypeInfo(avroSchema); +RowType rowType = FlinkSchemaUtil.convert(icebergSchema); + +FlinkSink.builderFor( + dataStream, + AvroGenericRecordToRowDataMapper.forAvroSchema(avroSchema), + FlinkCompatibilityUtil.toTypeInfo(rowType)) + .table(table) + .tableLoader(tableLoader) + .append(); +``` + +### Branch Writes +Writing to branches in Iceberg tables is also supported via the `toBranch` API in `FlinkSink` +For more information on branches please refer to [branches](branching.md). +```java +FlinkSink.forRowData(input) + .tableLoader(tableLoader) + .toBranch("audit-branch") + .append(); +``` + +### Metrics + +The following Flink metrics are provided by the Flink Iceberg sink. + +Parallel writer metrics are added under the sub group of `IcebergStreamWriter`. +They should have the following key-value tags. + +* table: full table name (like iceberg.my_db.my_table) +* subtask_index: writer subtask index starting from 0 + + Metric name | Metric type | Description | +| ------------------------- |------------|-----------------------------------------------------------------------------------------------------| +| lastFlushDurationMs | Gauge | The duration (in milli) that writer subtasks take to flush and upload the files during checkpoint. | +| flushedDataFiles | Counter | Number of data files flushed and uploaded. | +| flushedDeleteFiles | Counter | Number of delete files flushed and uploaded. | +| flushedReferencedDataFiles| Counter | Number of data files referenced by the flushed delete files. | +| dataFilesSizeHistogram | Histogram | Histogram distribution of data file sizes (in bytes). | +| deleteFilesSizeHistogram | Histogram | Histogram distribution of delete file sizes (in bytes). | + +The `Histogram` metrics above require `org.apache.flink:flink-metrics-dropwizard` on the classpath, +which is not shipped by Flink by default. Please add this artifact to your classpath to see histogram metrics. +If not present, histogram metrics will be missing. All other metric types will continue to get published. + +Committer metrics are added under the sub group of `IcebergFilesCommitter`. +They should have the following key-value tags. + +* table: full table name (like iceberg.my_db.my_table) + + Metric name | Metric type | Description | +|---------------------------------|--------|----------------------------------------------------------------------------| +| lastCheckpointDurationMs | Gauge | The duration (in milli) that the committer operator checkpoints its state. | +| lastCommitDurationMs | Gauge | The duration (in milli) that the Iceberg table commit takes. | +| committedDataFilesCount | Counter | Number of data files committed. | +| committedDataFilesRecordCount | Counter | Number of records contained in the committed data files. | +| committedDataFilesByteCount | Counter | Number of bytes contained in the committed data files. | +| committedDeleteFilesCount | Counter | Number of delete files committed. | +| committedDeleteFilesRecordCount | Counter | Number of records contained in the committed delete files. | +| committedDeleteFilesByteCount | Counter | Number of bytes contained in the committed delete files. | +| elapsedSecondsSinceLastSuccessfulCommit| Gauge | Elapsed time (in seconds) since last successful Iceberg commit. | + +`elapsedSecondsSinceLastSuccessfulCommit` is an ideal alerting metric +to detect failed or missing Iceberg commits. + +* Iceberg commit happened after successful Flink checkpoint in the `notifyCheckpointComplete` callback. + It could happen that Iceberg commits failed (for whatever reason), while Flink checkpoints succeeding. +* It could also happen that `notifyCheckpointComplete` wasn't triggered (for whatever bug). + As a result, there won't be any Iceberg commits attempted. + +If the checkpoint interval (and expected Iceberg commit interval) is 5 minutes, set up alert with rule like `elapsedSecondsSinceLastSuccessfulCommit > 60 minutes` to detect failed or missing Iceberg commits in the past hour. + +## Options + +### Write options + +Flink write options are passed when configuring the FlinkSink, like this: + +```java +FlinkSink.Builder builder = FlinkSink.forRow(dataStream, SimpleDataUtil.FLINK_SCHEMA) + .table(table) + .tableLoader(tableLoader) + .set("write-format", "orc") + .set(FlinkWriteOptions.OVERWRITE_MODE, "true"); +``` + +For Flink SQL, write options can be passed in via SQL hints like this: + +```sql +INSERT INTO tableName /*+ OPTIONS('upsert-enabled'='true') */ +... +``` + +Check out all the options here: [write-options](flink-configuration.md#write-options) + +## Distribution mode + +Flink streaming writer supports both `HASH` and `RANGE` distribution mode. +You can enable it via `FlinkSink#Builder#distributionMode(DistributionMode)` +or via [write-options](flink-configuration.md#write-options). + +### Hash distribution + +HASH distribution shuffles data by partition key (partitioned table) or +equality fields (non-partitioned table). It simply leverages Flink's +`DataStream#keyBy` to distribute the data. + +HASH distribution has a few limitations. +
    +
  • It doesn't handle skewed data well. E.g. some partitions have a lot more data than others. +
  • It can result in unbalanced traffic distribution if cardinality of the partition key or +equality fields is low as demonstrated by [PR 4228](https://github.com/apache/iceberg/pull/4228). +
  • Writer parallelism is limited to the cardinality of the hash key. +If the cardinality is 10, only at most 10 writer tasks would get the traffic. +Having higher writer parallelism (even if traffic volume requires) won't help. +
+ +### Range distribution (experimental) + +RANGE distribution shuffles data by partition key or sort order via a custom range partitioner. +Range distribution collects traffic statistics to guide the range partitioner to +evenly distribute traffic to writer tasks. + +Range distribution only shuffle the data via range partitioner. Rows are *not* sorted within +a data file, which Flink streaming writer doesn't support yet. + +#### Use cases + +RANGE distribution can be applied to an Iceberg table that either is partitioned or +has SortOrder defined. For a partitioned table without SortOrder, partition columns +are used as sort order. If SortOrder is explicitly defined for the table, it is used by +the range partitioner. + +Range distribution can handle skewed data. E.g. +
    +
  • Table is partitioned by event time. Typically, recent hours have more data, +while the long-tail hours have less and less data. +
  • Table is partitioned by country code, where some countries (like US) have +a lot more traffic and smaller countries have a lot less data +
  • Table is partitioned by event type, where some types have a lot more data than others. +
+ +Range distribution can also cluster data on non-partition columns. +E.g., table is partitioned hourly on ingestion time. Queries often include +predicate on a non-partition column like `device_id` or `country_code`. +Range partition would improve the query performance by clustering on the non-partition column +when table `SortOrder` is defined with the non-partition column. + +#### Traffic statistics + +Statistics are collected by every shuffle operator subtask and aggregated by the coordinator +for every checkpoint cycle. Aggregated statistics are broadcast to all subtasks and +applied to the range partitioner in the next checkpoint. So it may take up to two checkpoint +cycles to detect traffic distribution change and apply the new statistics to range partitioner. + +Range distribution can work with low cardinality (like `country_code`) +or high cardinality (like `device_id`) scenarios. +
    +
  • For low cardinality scenario (like hundreds or thousands), +HashMap is used to track traffic distribution for every key. +If a new sort key value shows up, range partitioner would just +round-robin it to the writer tasks before traffic distribution has been learned +about the new key. +
  • For high cardinality scenario (like millions or billions), +uniform random sampling (reservoir sampling) is used to compute range bounds +that split the sort key space evenly. +It keeps the memory footprint and network exchange low. +Reservoir sampling work well if key distribution is relatively even. +If a single hot key has unbalanced large share of the traffic, +range split by uniform sampling probably won't work very well. +
+ +#### Usage + +Here is how to enable range distribution in Java. There are two optional advanced configs. Default should +work well for most cases. See [write-options](flink-configuration.md#write-options) for details. +```java +FlinkSink.forRowData(input) + ... + .distributionMode(DistributionMode.RANGE) + .rangeDistributionStatisticsType(StatisticsType.Auto) + .rangeDistributionSortKeyBaseWeight(0.0d) + .append(); +``` + +### Overhead + +Data shuffling (hash or range) has computational overhead of serialization/deserialization +and network I/O. Expect some increase of CPU utilization. + +Range distribution also collect and aggregate data distribution statistics. +That would also incur some CPU overhead. Memory overhead is typically +small if using default statistics type of `Auto`. Don't use `Map` statistics +type if key cardinality is high. That could result in significant memory footprint +and large network exchange for statistics aggregation. + +## Notes + +Flink streaming write jobs rely on snapshot summary to keep the last committed checkpoint ID, and +store uncommitted data as temporary files. Therefore, [expiring snapshots](maintenance.md#expire-snapshots) +and [deleting orphan files](maintenance.md#delete-orphan-files) could possibly corrupt +the state of the Flink job. To avoid that, make sure to keep the last snapshot created by the Flink +job (which can be identified by the `flink.job-id` property in the summary), and only delete +orphan files that are old enough. + +## Sink V2 based implementation + +At the time when the current default, `FlinkSink` implementation was created, Flink Sink's interface had some +limitations that were not acceptable for the Iceberg tables purpose. Due to these limitations, `FlinkSink` is based +on a custom chain of `StreamOperator`s terminated by `DiscardingSink`. + +In the 1.15 version of Flink [SinkV2 interface](https://cwiki.apache.org/confluence/display/FLINK/FLIP-191%3A+Extend+unified+Sink+interface+to+support+small+file+compaction) +was introduced. This interface is used in the new `IcebergSink` implementation which is available in the `iceberg-flink` module. +The new implementation is a base for further work on features such as [table maintenance](maintenance.md). +The SinkV2 based implementation is currently an experimental feature so use it with caution. + +### Writing with SQL + +To turn on SinkV2 based implementation in SQL, set this configuration option: +```sql +SET table.exec.iceberg.use-v2-sink = true; +``` + +### Writing with DataStream + +To use SinkV2 based implementation, replace `FlinkSink` with `IcebergSink` in the provided snippets. +!!! warning + There are some slight differences between these implementations: + + - The `RANGE` distribution mode is not yet available for the `IcebergSink` + - When using `IcebergSink` use `uidSuffix` instead of the `uidPrefix` + +## Flink Dynamic Iceberg Sink + +The Flink Dynamic Iceberg Sink (Dynamic Sink) allows: + +1. **Writing to any number of tables** + A single sink can dynamically route records to multiple Iceberg tables. + +2. **Dynamic table creation and updates** + Tables are created and updated based on user-defined routing logic. + +3. **Dynamic schema and partition evolution** + Table schemas and partition specs update during streaming execution. + +All configurations are controlled through the `DynamicRecord` class, eliminating the need for Flink job restarts when requirements change. + +```java + + DynamicIcebergSink.forInput(dataStream) + .generator((inputRecord, out) -> out.collect( + new DynamicRecord( + TableIdentifier.of("db", "table"), + "branch", + SCHEMA, + (RowData) inputRecord, + PartitionSpec.unpartitioned(), + DistributionMode.HASH, + 2))) + .catalogLoader(CatalogLoader.hive("hive", new Configuration(), Map.of())) + .writeParallelism(10) + .immediateTableUpdate(true) + .append(); +``` + +### Configuration Example + +```java +DynamicIcebergSink.Builder builder = DynamicIcebergSink.forInput(inputStream); + +// Set common properties +builder + .set("write.parquet.compression-codec", "gzip"); + +// Set Dynamic Sink specific options +builder + .writeParallelism(4) + .uidPrefix("dynamic-sink") + .cacheMaxSize(500) + .cacheRefreshMs(5000); + +// Add generator and append sink +builder.generator(new CustomRecordGenerator()); +builder.append(); +``` + +### Dynamic Routing Configuration + +Dynamic table routing can be customized by implementing the `DynamicRecordGenerator` interface: + +```java +public class CustomRecordGenerator implements DynamicRecordGenerator { + @Override + public DynamicRecord generate(RowData row) { + DynamicRecord record = new DynamicRecord(); + // Set table name based on business logic + TableIdentifier tableIdentifier = TableIdentifier.of(database, tableName); + record.setTableIdentifier(tableIdentifier); + record.setData(row); + // Set the maximum number of parallel writers for a given table/branch/schema/spec + record.writeParallelism(2); + return record; + } +} + +// Set custom record generator when building the sink +DynamicIcebergSink.Builder builder = DynamicIcebergSink.forInput(inputStream); +builder.generator(new CustomRecordGenerator()); +// ... other config ... +builder.append(); +``` +The user should provide a converter which converts the input record to a DynamicRecord. +We need the following information (DynamicRecord) for every record: + +| Property | Description | +|--------------------|-------------------------------------------------------------------------------------------| +| `TableIdentifier` | The target table to which the record will be written. | +| `Branch` | The target branch for writing the record (optional). | +| `Schema` | The schema of the record. | +| `Spec` | The expected partitioning specification for the record. | +| `RowData` | The actual row data to be written. | +| `DistributionMode` | The distribution mode for writing the record (NONE, HASH or `null`). When `null`, the record won't be shuffled at all. | +| `Parallelism` | The maximum number of parallel writers for a given table/branch/schema/spec (WriteTarget). | +| `UpsertMode` | Overrides this table's write.upsert.enabled (optional). | +| `EqualityFields` | The equality fields for the table(optional). | + +### Schema Evolution + +The dynamic sink tries to match the schema provided in `DynamicRecord` with the existing table schemas. +- If there is a direct match with one of the existing table schemas, that table schema will be used for writing to the table. +- If there is no direct match, DynamicSink tries to adapt the provided schema such that it matches one of table schemas. For example, if there is an additional optional column in the table schema, a null value will be added to the RowData provided via DynamicRecord. +- Otherwise, we evolve the table schema to match the input schema, within the constraints described below. + +The dynamic sink maintains an LRU cache for both table metadata and incoming schemas, with eviction based on size and time constraints. When a DynamicRecord contains a schema that is incompatible with the current table schema, a schema update is triggered. This update can occur either immediately or via a centralized executor, depending on the `immediateTableUpdate` configuration. While centralized updates reduce load on the Catalog, they may introduce backpressure on the sink. + +#### Supported schema updates + +- Adding new columns +- Widening existing column types (e.g., Integer → Long, Float → Double) +- Making required columns optional +- Dropping columns (disabled by default) + +Dropping columns is disabled by default to prevent issues with late or out-of-order data, as removed fields cannot be easily restored without data loss. + +You can opt-in to allow dropping columns (see the configuration options below). Once a column has been dropped, it is +technically still possible to write data to that column because Iceberg maintains all past table schemas. However, +regular queries won't be able to reference the column. If the field was to re-appear as part of a new schema, an +entirely new column would be added, which apart from the name, has nothing in common with the old column, i.e. queries +for the new column will never return data of the old column. + +##### Unsupported schema updates + +- Renaming columns + +Renaming is unsupported because schema comparison is name-based, and renames would require additional metadata or hints to resolve. + +### Caching + +There are two distinct caches involved: the table metadata cache and the input schema cache. + +- The table metadata cache holds metadata such as schema definitions and partition specs to reduce repeated Catalog lookups. Its size is governed by the `cacheMaxSize` setting. +- The input schema cache stores incoming schemas per table along with their compatibility resolution results. Its size is controlled by `inputSchemasPerTableCacheMaxSize`. + +To improve cache hit rates and performance, reuse the same DynamicRecord.schema instance if the record schema is unchanged. + +### Dynamic Sink Configuration + +The Dynamic Iceberg Flink Sink is configured using the Builder pattern. Here are the key configuration methods: + +| Method | Description | +|------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `overwrite(boolean enabled)` | Enable overwrite mode | +| `writeParallelism(int parallelism)` | Set writer parallelism | +| `uidPrefix(String prefix)` | Set operator UID prefix | +| `snapshotProperties(Map properties)` | Set snapshot metadata properties | +| `toBranch(String branch)` | Write to a specific branch | +| `cacheMaxSize(int maxSize)` | Set cache size for table metadata | +| `cacheRefreshMs(long refreshMs)` | Set cache refresh interval | +| `inputSchemasPerTableCacheMaxSize(int size)` | Set max input schemas to cache per table | +| `immediateTableUpdate(boolean enabled)` | Controls whether table metadata (schema/partition spec) updates immediately (default: false) | +| `set(String property, String value)` | Set any Iceberg write property (e.g., `"write.format"`, `"write.upsert.enabled"`).Check out all the options here: [write-options](flink-configuration.md#write-options) | +| `setAll(Map properties)` | Set multiple properties at once | +| `tableCreator(TableCreator creator)` | When DynamicIcebergSink creates new Iceberg tables, allows overriding how tables are created - setting custom table properties and location based on the table name. | +| `dropUnusedColumns(boolean enabled)` | When enabled, drops all columns from the current table schema which are not contained in the input schema (see the caveats above on dropping columns). | + +### Distribution Modes + +The `DistributionMode` set on each `DynamicRecord` controls how that record is routed from the processor to the writer: + +| Mode | Behavior | +|--------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `NONE` | Records are distributed across writer subtasks in a round-robin fashion (or by equality fields if set). | +| `HASH` | Records are distributed by partition key (partitioned tables) or equality fields (unpartitioned tables). Ensures that records for the same partition are handled by the same writer subtask. | +| `null` | Forward mode: bypasses distribution entirely and sends records directly via a forward edge (see below). | + +#### Forward Mode + +Using the `DynamicRecord` constructor overload without `distributionMode` parameter bypasses distribution entirely. This is designed for high-throughput pipelines where every partition already has a large volume of data and the serialization and network shuffle cost is prohibitive. Records are sent directly from the processor to the writer using a forward edge, enabling Flink operator chaining. Table metadata updates are always performed immediately inside the processor (regardless of `immediateTableUpdate` setting), because a dedicated table-update operator was deliberately omitted to avoid introducing extra data shuffles. + +Forward and regular records can be mixed in the same pipeline. The processor routes records to two separate sink outputs: + +- **Shuffle sink**: receives shuffling records. These go through the normal distribution topology (hash/round-robin) before reaching the writer. +- **Forward sink**: receives records without a `distributionMode`. These skip distribution entirely and flow via a forward edge from the processor, allowing Flink operator chaining. Suited for high-throughput tables where avoiding shuffle overhead is critical. The sink's `writeParallelism` config does not apply to this path. + +!!! warning + +1. In the forward path, schema changes are always applied immediately because records must pass straight through via the forward edge. For the intended high-volume use case, this can cause many conflicting commits to the Iceberg catalog and temporarily delay data processing. Consider either updating the schema externally before publishing records with the new schema, or planning for a temporary disruption in throughput when a new schema is introduced from upstream. +2. Because the forward path skips distribution entirely, users are responsible for distributing the data correctly in the upstream before the records reach the dynamic Iceberg sink. Otherwise, writes could be unbalanced. + +### Notes + +- **Range distribution mode**: Currently, the dynamic sink does not support the `RANGE` distribution mode, if set, it will fall back to `HASH`. +- **Property Precedence Note**: When conflicts occur between table properties and sink properties, the sink properties will override the table properties configuration. +- **Table Format Version upgrade**: Dynamic sink does not support upgrading a table with dynamic records. The job should not be running while the V2 to V3 upgrade is in progress. diff --git a/1.11.0/docs/flink.md b/1.11.0/docs/flink.md new file mode 100644 index 000000000000..5a6788621140 --- /dev/null +++ b/1.11.0/docs/flink.md @@ -0,0 +1,407 @@ +--- +title: "Getting Started" +--- + + +!!!tip + For an overview of using Iceberg with Flink, see the [Flink Quickstart](/flink-quickstart) + +Apache Iceberg supports both [Apache Flink](https://flink.apache.org/)'s DataStream API and Table API. See the [Multi-Engine Support](../../multi-engine-support.md#apache-flink) page for the integration of Apache Flink. + +| Feature support | Flink | Notes | +| -------------------------------------------------------- |-------|----------------------------------------------------------------------------------------| +| [SQL create catalog](flink-ddl.md#create-catalog) | ✔️ | | +| [SQL create database](flink-ddl.md#create-database) | ✔️ | | +| [SQL create table](flink-ddl.md#create-table) | ✔️ | | +| [SQL create table like](flink-ddl.md#create-table-like) | ✔️ | | +| [SQL alter table](flink-ddl.md#alter-table) | ✔️ | Only support altering table properties, column and partition changes are not supported | +| [SQL drop_table](flink-ddl.md#drop-table) | ✔️ | | +| [SQL select](flink-queries.md#reading-with-sql) | ✔️ | Support both streaming and batch mode | +| [SQL insert into](flink-writes.md#insert-into) | ✔️ ️ | Support both streaming and batch mode | +| [SQL insert overwrite](flink-writes.md#insert-overwrite) | ✔️ ️ | | +| [DataStream read](flink-queries.md#reading-with-datastream) | ✔️ ️ | | +| [DataStream append](flink-writes.md#appending-data) | ✔️ ️ | | +| [DataStream overwrite](flink-writes.md#overwrite-data) | ✔️ ️ | | +| [Metadata tables](flink-queries.md#inspecting-tables) | ✔️ | | +| [Rewrite files action](flink-maintenance.md#rewrite-files-action) | ✔️ ️ | | + +## Preparation when using Flink SQL Client + +To create Iceberg table in Flink, it is recommended to use [Flink SQL Client](https://ci.apache.org/projects/flink/flink-docs-release-{{ flinkVersionMajor }}/dev/table/sqlClient.html) as it's easier for users to understand the concepts. + +Download Flink from the [Apache download page](https://flink.apache.org/downloads.html). Iceberg uses Scala 2.12 when compiling the Apache `iceberg-flink-runtime` jar, so it's recommended to use Flink {{ flinkVersionMajor }} bundled with Scala 2.12. + +```bash +FLINK_VERSION={{ flinkVersion }} +SCALA_VERSION=2.12 +APACHE_FLINK_URL=https://archive.apache.org/dist/flink/ +wget ${APACHE_FLINK_URL}/flink-${FLINK_VERSION}/flink-${FLINK_VERSION}-bin-scala_${SCALA_VERSION}.tgz +tar xzvf flink-${FLINK_VERSION}-bin-scala_${SCALA_VERSION}.tgz +``` + +Start a standalone Flink cluster within Hadoop environment: + +```bash +# HADOOP_HOME is your hadoop root directory after unpack the binary package. +APACHE_HADOOP_URL=https://archive.apache.org/dist/hadoop/ +HADOOP_VERSION=2.8.5 +wget ${APACHE_HADOOP_URL}/common/hadoop-${HADOOP_VERSION}/hadoop-${HADOOP_VERSION}.tar.gz +tar xzvf hadoop-${HADOOP_VERSION}.tar.gz +HADOOP_HOME=`pwd`/hadoop-${HADOOP_VERSION} + +export HADOOP_CLASSPATH=`$HADOOP_HOME/bin/hadoop classpath` + +# Start the flink standalone cluster +cd flink-${FLINK_VERSION}/ +./bin/start-cluster.sh +``` + +Start the Flink SQL client. There is a separate `flink-runtime` module in the Iceberg project to generate a bundled jar, which could be loaded by Flink SQL client directly. To build the `flink-runtime` bundled jar manually, build the `iceberg` project, and it will generate the jar under `/flink-runtime/build/libs`. Or download the `flink-runtime` jar from the [Apache repository](https://repo.maven.apache.org/maven2/org/apache/iceberg/iceberg-flink-runtime-{{ flinkVersionMajor }}/{{ icebergVersion }}/). + +```bash +# HADOOP_HOME is your hadoop root directory after unpack the binary package. +export HADOOP_CLASSPATH=`$HADOOP_HOME/bin/hadoop classpath` + +# Below works for Flink 1.15 or earlier +./bin/sql-client.sh embedded -j /iceberg-flink-runtime-{{ flinkVersionMajor }}-{{ icebergVersion }}.jar shell + +# Flink 1.16+ has a regression in loading external jars via -j. See FLINK-30035 for details. +# put iceberg-flink-runtime-{{ flinkVersionMajor }}-{{ icebergVersion }}.jar in flink/lib dir +./bin/sql-client.sh embedded shell +``` + +By default, Iceberg ships with Hadoop jars for Hadoop catalog. To use Hive catalog, load the Hive jars when opening the Flink SQL client. Fortunately, Flink has provided a [bundled hive jar](https://repo.maven.apache.org/maven2/org/apache/flink/flink-sql-connector-hive-2.3.9_2.12/{{ flinkVersion }}/flink-sql-connector-hive-2.3.9_2.12-{{ flinkVersion }}.jar) for the SQL client. An example on how to download the dependencies and get started: + +```bash +# HADOOP_HOME is your hadoop root directory after unpack the binary package. +export HADOOP_CLASSPATH=`$HADOOP_HOME/bin/hadoop classpath` + +ICEBERG_VERSION={{ icebergVersion }} +MAVEN_URL=https://repo1.maven.org/maven2 +ICEBERG_MAVEN_URL=${MAVEN_URL}/org/apache/iceberg +ICEBERG_PACKAGE=iceberg-flink-runtime +FLINK_VERSION_MAJOR={{ flinkVersionMajor }} +wget ${ICEBERG_MAVEN_URL}/${ICEBERG_PACKAGE}-${FLINK_VERSION_MAJOR}/${ICEBERG_VERSION}/${ICEBERG_PACKAGE}-${FLINK_VERSION_MAJOR}-${ICEBERG_VERSION}.jar -P lib/ + +HIVE_VERSION=2.3.9 +SCALA_VERSION=2.12 +FLINK_VERSION={{ flinkVersion }} +FLINK_CONNECTOR_URL=${MAVEN_URL}/org/apache/flink +FLINK_CONNECTOR_PACKAGE=flink-sql-connector-hive +wget ${FLINK_CONNECTOR_URL}/${FLINK_CONNECTOR_PACKAGE}-${HIVE_VERSION}_${SCALA_VERSION}/${FLINK_VERSION}/${FLINK_CONNECTOR_PACKAGE}-${HIVE_VERSION}_${SCALA_VERSION}-${FLINK_VERSION}.jar + +./bin/sql-client.sh embedded shell +``` + +## Flink's Python API + +!!! info + PyFlink 1.6.1 has a known issue on macOS with Apple Silicon. See [FLINK-28786](https://issues.apache.org/jira/browse/FLINK-28786). + +Install the Apache Flink dependency using `pip`: + +```bash +pip install apache-flink=={{ flinkVersion }} +``` + +Provide a `file://` path to the `iceberg-flink-runtime` jar, which can be obtained by building the project and looking at `/flink-runtime/build/libs`, or downloading it from the [Apache official repository](https://repo.maven.apache.org/maven2/org/apache/iceberg/iceberg-flink-runtime/). Third-party jars can be added to `pyflink` via: + +- `env.add_jars("file:///my/jar/path/connector.jar")` +- `table_env.get_config().get_configuration().set_string("pipeline.jars", "file:///my/jar/path/connector.jar")` + +This is also mentioned in the official [docs](https://ci.apache.org/projects/flink/flink-docs-release-{{ flinkVersionMajor }}/docs/dev/python/dependency_management/). The example below uses `env.add_jars(..)`: + +```python +import os + +from pyflink.datastream import StreamExecutionEnvironment + +env = StreamExecutionEnvironment.get_execution_environment() +iceberg_flink_runtime_jar = os.path.join(os.getcwd(), "iceberg-flink-runtime-{{ flinkVersionMajor }}-{{ icebergVersion }}.jar") + +env.add_jars("file://{}".format(iceberg_flink_runtime_jar)) +``` + +Next, create a `StreamTableEnvironment` and execute Flink SQL statements. The below example shows how to create a custom catalog via the Python Table API: + +```python +from pyflink.table import StreamTableEnvironment +table_env = StreamTableEnvironment.create(env) +table_env.execute_sql(""" +CREATE CATALOG my_catalog WITH ( + 'type'='iceberg', + 'catalog-impl'='com.my.custom.CatalogImpl', + 'my-additional-catalog-config'='my-value' +) +""") +``` + +Run a query: + +```python +(table_env + .sql_query("SELECT PULocationID, DOLocationID, passenger_count FROM my_catalog.nyc.taxis LIMIT 5") + .execute() + .print()) +``` + +``` ++----+----------------------+----------------------+--------------------------------+ +| op | PULocationID | DOLocationID | passenger_count | ++----+----------------------+----------------------+--------------------------------+ +| +I | 249 | 48 | 1.0 | +| +I | 132 | 233 | 1.0 | +| +I | 164 | 107 | 1.0 | +| +I | 90 | 229 | 1.0 | +| +I | 137 | 249 | 1.0 | ++----+----------------------+----------------------+--------------------------------+ +5 rows in set +``` + +For more details, please refer to the [Python Table API](https://ci.apache.org/projects/flink/flink-docs-release-{{ flinkVersionMajor }}/docs/dev/python/table/intro_to_table_api/). + +## Adding catalogs + +Flink supports creating catalogs using Flink SQL. + +### Catalog Configuration + +A catalog is created and named by executing the following query (replace `` with your catalog name and +`'' = ''` with catalog implementation config): + +```sql +CREATE CATALOG WITH ( + 'type'='iceberg', + '' = '' +); +``` + +The following properties can be set globally and are not limited to a specific catalog implementation: + +* `type`: Must be `iceberg`. (required) +* `catalog-type`: `hive`, `hadoop`, `rest`, `glue`, `jdbc` or `nessie` for built-in catalogs, or left unset for custom catalog implementations using catalog-impl. (Optional) +* `catalog-impl`: The fully-qualified class name of a custom catalog implementation. Must be set if `catalog-type` is unset. (Optional) +* `property-version`: Version number to describe the property version. This property can be used for backwards compatibility in case the property format changes. The current property version is `1`. (Optional) +* `cache-enabled`: Whether to enable catalog cache, default value is `true`. (Optional) +* `cache.expiration-interval-ms`: How long catalog entries are locally cached, in milliseconds; negative values like `-1` will disable expiration, value 0 is not allowed to set. default value is `-1`. (Optional) + +### Hive catalog + +This creates an Iceberg catalog named `hive_catalog` that can be configured using `'catalog-type'='hive'`, which loads tables from Hive metastore: + +```sql +CREATE CATALOG hive_catalog WITH ( + 'type'='iceberg', + 'catalog-type'='hive', + 'uri'='thrift://localhost:9083', + 'clients'='5', + 'property-version'='1', + 'warehouse'='hdfs://nn:8020/warehouse/path' +); +``` + +### REST catalog + +This creates an iceberg catalog named `rest_catalog` that can be configured using `'catalog-type'='rest'`, which loads tables from a REST catalog: + +```sql +CREATE CATALOG rest_catalog WITH ( + 'type'='iceberg', + 'catalog-type'='rest', + 'uri'='https://localhost/' +); +``` + +## Creating a table + +```sql +CREATE TABLE `hive_catalog`.`default`.`sample` ( + id BIGINT COMMENT 'unique id', + data STRING +); +``` + +## Writing + +To append new data to a table with a Flink streaming job, use `INSERT INTO`: + +```sql +INSERT INTO `hive_catalog`.`default`.`sample` VALUES (1, 'a'); +INSERT INTO `hive_catalog`.`default`.`sample` SELECT id, data from other_kafka_table; +``` + +To replace data in the table with the result of a query, use `INSERT OVERWRITE` in a batch job (Flink streaming jobs do not support `INSERT OVERWRITE`). Overwrites are atomic operations for Iceberg tables. + +Partitions that have rows produced by the SELECT query will be replaced, for example: + +```sql +INSERT OVERWRITE `hive_catalog`.`default`.`sample` VALUES (1, 'a'); +``` + +Iceberg also supports overwriting given partitions by the `SELECT` values: + +```sql +INSERT OVERWRITE `hive_catalog`.`default`.`sample` PARTITION(data='a') SELECT 6; +``` + +Flink supports writing `DataStream` and `DataStream` to the sink iceberg table natively. + +```java +StreamExecutionEnvironment env = ...; + +DataStream input = ... ; +Configuration hadoopConf = new Configuration(); +TableLoader tableLoader = TableLoader.fromHadoopTable("hdfs://nn:8020/warehouse/path", hadoopConf); + +FlinkSink.forRowData(input) + .tableLoader(tableLoader) + .append(); + +env.execute("Test Iceberg DataStream"); +``` + +### Branch Writes +Writing to branches in Iceberg tables is also supported via the `toBranch` API in `FlinkSink`. + +For more information on branches please refer to [branches](branching.md). + +```java +FlinkSink.forRowData(input) + .tableLoader(tableLoader) + .toBranch("audit-branch") + .append(); +``` + +## Reading + +Submit a Flink __batch__ job using the following statements: + +```sql +-- Execute the flink job in batch mode for current session context +SET execution.runtime-mode = batch; +SELECT * FROM `hive_catalog`.`default`.`sample`; +``` + +Iceberg supports processing incremental data in Flink __streaming__ jobs that start from a historical snapshot ID: + +```sql +-- Submit the flink job in streaming mode for current session. +SET execution.runtime-mode = streaming; + +-- Enable this switch because streaming read SQL will provide few job options in flink SQL hint options. +SET table.dynamic-table-options.enabled=true; + +-- Read all the records from the iceberg current snapshot, and then read incremental data starting from that snapshot. +SELECT * FROM `hive_catalog`.`default`.`sample` /*+ OPTIONS('streaming'='true', 'monitor-interval'='1s')*/ ; + +-- Read all incremental data starting from the snapshot-id '3821550127947089987' (records from this snapshot will be excluded). +SELECT * FROM `hive_catalog`.`default`.`sample` /*+ OPTIONS('streaming'='true', 'monitor-interval'='1s', 'start-snapshot-id'='3821550127947089987')*/ ; +``` + +SQL is also the recommended way to inspect tables. To view all of the snapshots in a table, use the snapshots metadata table: + +```sql +SELECT * FROM `hive_catalog`.`default`.`sample$snapshots`; +``` + +Iceberg supports streaming or batch reads in the Java API: + +``` +DataStream batch = FlinkSource.forRowData() + .env(env) + .tableLoader(tableLoader) + .streaming(false) + .build(); +``` + +## Type conversion + +Iceberg's integration for Flink automatically converts between Flink and Iceberg types. When writing to a table with types that are not supported by Flink, like UUID, Iceberg will accept and convert values from the Flink type. + +### Flink to Iceberg + +Flink types are converted to Iceberg types according to the following table: + +| Flink | Iceberg | Notes | +| ------------------- | -------------------------- | ------------- | +| boolean | boolean | | +| tinyint | integer | | +| smallint | integer | | +| integer | integer | | +| bigint | long | | +| float | float | | +| double | double | | +| char | string | | +| varchar | string | | +| string | string | | +| binary | binary | | +| varbinary | fixed | | +| decimal | decimal | | +| date | date | | +| time | time | | +| timestamp | timestamp without timezone | | +| timestamp_ltz | timestamp with timezone | | +| array | list | | +| map | map | | +| multiset | map | | +| row | struct | | +| raw | | Not supported | +| interval | | Not supported | +| structured | | Not supported | +| timestamp with zone | | Not supported | +| distinct | | Not supported | +| null | | Not supported | +| symbol | | Not supported | +| logical | | Not supported | + +### Iceberg to Flink + +Iceberg types are converted to Flink types according to the following table: + +| Iceberg | Flink | Notes | +| -------------------------- | --------------------- | ------------- | +| boolean | boolean | | +| struct | row | | +| list | array | | +| map | map | | +| integer | integer | | +| long | bigint | | +| float | float | | +| double | double | | +| date | date | | +| time | time | | +| timestamp without timezone | timestamp(6) | | +| timestamp with timezone | timestamp_ltz(6) | | +| string | varchar(2147483647) | | +| uuid | binary(16) | | +| fixed(N) | binary(N) | | +| binary | varbinary(2147483647) | | +| decimal(P, S) | decimal(P, S) | | +| nanosecond timestamp | timestamp(9) | | +| nanosecond timestamp with timezone | timestamp_ltz(9) | | +| unknown | null | | +| variant | | Not supported | +| geometry | | Not supported | +| geography | | Not supported | + +## Future improvements + +There are some features that are not yet supported in the current Flink Iceberg integration: + +* Creation of Iceberg table with hidden partitioning. [Discussion](http://mail-archives.apache.org/mod_mbox/flink-dev/202008.mbox/%3cCABi+2jQCo3MsOa4+ywaxV5J-Z8TGKNZDX-pQLYB-dG+dVUMiMw@mail.gmail.com%3e) in flink mail list. +* Creation of Iceberg table with computed column. diff --git a/1.11.0/docs/hive-migration.md b/1.11.0/docs/hive-migration.md new file mode 100644 index 000000000000..fa0a6e8a7f4d --- /dev/null +++ b/1.11.0/docs/hive-migration.md @@ -0,0 +1,55 @@ +--- +title: "Hive Migration" +--- + + +# Hive Table Migration +Apache Hive supports ORC, Parquet, and Avro file formats that could be migrated to Iceberg. +When migrating data to an Iceberg table, which provides versioning and transactional updates, only the most recent data files need to be migrated. + +Iceberg supports all three migration actions: Snapshot Table, Migrate Table, and Add Files for migrating from Hive tables to Iceberg tables. Since Hive tables do not maintain snapshots, +the migration process essentially involves creating a new Iceberg table with the existing schema and committing all data files across all partitions to the new Iceberg table. +After the initial migration, any new data files are added to the new Iceberg table using the Add Files action. + +## Enabling Migration from Hive to Iceberg +The Hive table migration actions are supported by the Spark Integration module via Spark Procedures. +The procedures are bundled in the Spark runtime jar, which is available in the [Iceberg Release Downloads](../../releases.md#downloads). + +## Snapshot Hive Table to Iceberg +To snapshot a Hive table, users can run the following Spark SQL: +```sql +CALL catalog_name.system.snapshot('db.source', 'db.dest') +``` +See [Spark Procedure: snapshot](spark-procedures.md#snapshot) for more details. + +## Migrate Hive Table To Iceberg +To migrate a Hive table to Iceberg, users can run the following Spark SQL: +```sql +CALL catalog_name.system.migrate('db.sample') +``` +See [Spark Procedure: migrate](spark-procedures.md#migrate) for more details. + +## Add Files From Hive Table to Iceberg +To add data files from a Hive table to a given Iceberg table, users can run the following Spark SQL: +```sql +CALL spark_catalog.system.add_files( +table => 'db.tbl', +source_table => 'db.src_tbl' +) +``` +See [Spark Procedure: add_files](spark-procedures.md#add_files) for more details. diff --git a/1.11.0/docs/hive.md b/1.11.0/docs/hive.md new file mode 100644 index 000000000000..4829acfe208b --- /dev/null +++ b/1.11.0/docs/hive.md @@ -0,0 +1,799 @@ +--- +title: "Hive" +--- + + +# Hive + +Iceberg supports reading and writing Iceberg tables through [Hive](https://hive.apache.org) by using +a [StorageHandler](https://cwiki.apache.org/confluence/display/Hive/StorageHandlers). + +## Feature support + +Hive supports the following features with Hive version 4.0.0 and above: + +* Creating an Iceberg table. +* Creating an Iceberg identity-partitioned table. +* Creating an Iceberg table with any partition spec, including the various transforms supported by Iceberg. +* Creating a table from an existing table (CTAS table). +* Dropping a table. +* Altering a table while keeping Iceberg and Hive schemas in sync. +* Altering the partition schema (updating columns). +* Altering the partition schema by specifying partition transforms. +* Truncating a table / partition, dropping a partition. +* Migrating tables in Avro, Parquet, or ORC (Non-ACID) format to Iceberg. +* Reading an Iceberg table. +* Reading the schema of a table. +* Querying Iceberg metadata tables. +* Time travel applications. +* Inserting into a table / partition (INSERT INTO). +* Inserting data overwriting existing data (INSERT OVERWRITE) in a table / partition. +* Copy-on-write support for delete, update and merge queries, CRUD support for Iceberg V1 tables. +* Altering a table with expiring snapshots. +* Create a table like an existing table (CTLT table). +* Support adding parquet compression type via Table properties [Compression types](https://spark.apache.org/docs/2.4.3/sql-data-sources-parquet.html#configuration). +* Altering a table metadata location. +* Supporting table rollback. +* Honors sort orders on existing tables when writing a table [Sort orders specification](../../spec.md#sort-orders). +* Creating, writing to and dropping an Iceberg branch / tag. +* Allowing expire snapshots by Snapshot ID, by time range, by retention of last N snapshots and using table properties. +* Set current snapshot using snapshot ID for an Iceberg table. +* Support for renaming an Iceberg table. +* Altering a table to convert to an Iceberg table. +* Fast forwarding, cherry-picking commit to an Iceberg branch. +* Creating a branch from an Iceberg tag. +* Set current snapshot using branch/tag for an Iceberg table. +* Delete orphan files for an Iceberg table. +* Allow full table compaction of Iceberg tables. +* Support of showing partition information for Iceberg tables (SHOW PARTITIONS). + +!!! warning + DML operations work only with Tez execution engine. + +## Enabling Iceberg support in Hive + +Starting from 1.8.0 Iceberg doesn't release Hive runtime connector. For Hive query engine integration (specifically +with Hive 2.x and 3.x) use Hive runtime connector coming with Iceberg 1.6.1, or use Hive 4.0.0 or later +which is released with embedded Iceberg integration. + +### Hive 4.1.x, 4.2.x + +Hive 4.1.x and 4.2.x come with Iceberg 1.9.1 included. + +### Hive 4.0.x + +Hive 4.0.x comes with Iceberg 1.4.3 included. + +#### Enabling support + +If the Iceberg storage handler is not in Hive's classpath, then Hive cannot load or update the metadata for an Iceberg +table when the storage handler is set. To avoid the appearance of broken tables in Hive, Iceberg will not add the +storage handler to a table unless Hive support is enabled. The storage handler is kept in sync (added or removed) every +time Hive engine support for the table is updated, i.e. turned on or off in the table properties. There are two ways to +enable Hive support: globally in Hadoop Configuration and per-table using a table property. + +##### Hadoop configuration + +To enable Hive support globally for an application, set `iceberg.engine.hive.enabled=true` in its Hadoop configuration. +For example, setting this in the `hive-site.xml` loaded by Spark will enable the storage handler for all tables created +by Spark. + +##### Table property configuration + +Alternatively, the property `engine.hive.enabled` can be set to `true` and added to the table properties when creating +the Iceberg table. Here is an example of doing it programmatically: + +```java +Catalog catalog=...; + Map tableProperties=Maps.newHashMap(); + tableProperties.put(TableProperties.ENGINE_HIVE_ENABLED,"true"); // engine.hive.enabled=true + catalog.createTable(tableId,schema,spec,tableProperties); +``` + +The table level configuration overrides the global Hadoop configuration. + +## Catalog Management + +### Global Hive catalog + +HiveCatalog integration supports Hive 2.3.10 or 3.1.3 or later. + +From the Hive engine's perspective, there is only one global data catalog that is defined in the Hadoop configuration in +the runtime environment. In contrast, Iceberg supports multiple different data catalog types such as Hive, Hadoop, AWS +Glue, or custom catalog implementations. Iceberg also allows loading a table directly based on its path in the file +system. Those tables do not belong to any catalog. Users might want to read these cross-catalog and path-based tables +through the Hive engine for use cases like join. + +To support this, a table in the Hive metastore can represent three different ways of loading an Iceberg table, depending +on the table's `iceberg.catalog` property: + +1. The table will be loaded using a `HiveCatalog` that corresponds to the metastore configured in the Hive environment + if no `iceberg.catalog` is set +2. The table will be loaded using a custom catalog if `iceberg.catalog` is set to a catalog name (see below) +3. The table can be loaded directly using the table's root location if `iceberg.catalog` is set + to `location_based_table` + +For cases 2 and 3 above, users can create an overlay of an Iceberg table in the Hive metastore, so that different table +types can work together in the same Hive environment. See [CREATE EXTERNAL TABLE](#create-external-table-overlaying-an-existing-iceberg-table) +and [CREATE TABLE](#create-table) for more details. + +### Custom Iceberg catalogs + +To globally register different catalogs, set the following Hadoop configurations: + +| Config Key | Description | +| --------------------------------------------- | ------------------------------------------------------ | +| iceberg.catalog..type | type of catalog: `hive`, `hadoop`, or left unset if using a custom catalog | +| iceberg.catalog..catalog-impl | catalog implementation, must not be null if type is empty | +| iceberg.catalog.. | any config key and value pairs for the catalog | + +Here are some examples using Hive CLI: + +Register a `HiveCatalog` called `another_hive`: + +``` +SET iceberg.catalog.another_hive.type=hive; +SET iceberg.catalog.another_hive.uri=thrift://example.com:9083; +SET iceberg.catalog.another_hive.clients=10; +SET iceberg.catalog.another_hive.warehouse=hdfs://example.com:8020/warehouse; +``` + +Register a `HadoopCatalog` called `hadoop`: + +``` +SET iceberg.catalog.hadoop.type=hadoop; +SET iceberg.catalog.hadoop.warehouse=hdfs://example.com:8020/warehouse; +``` + +Register an AWS `GlueCatalog` called `glue`: + +``` +SET iceberg.catalog.glue.type=glue; +SET iceberg.catalog.glue.warehouse=s3://my-bucket/my/key/prefix; +SET iceberg.catalog.glue.lock.table=myGlueLockTable; +``` + +## DDL Commands + +### CREATE TABLE + +#### Non partitioned tables + +The Hive `CREATE EXTERNAL TABLE` command creates an Iceberg table when you specify the storage handler as follows: + +```sql +CREATE EXTERNAL TABLE x (i int) STORED BY ICEBERG; +``` + +If you want to create external tables using CREATE TABLE, configure the MetaStoreMetadataTransformer on the cluster, +and `CREATE TABLE` commands are transformed to create external tables. For example: + +```sql +CREATE TABLE x (i int) STORED BY ICEBERG; +``` + +You can specify the default file format (Avro, Parquet, ORC) at the time of the table creation. +The default is Parquet: + +```sql +CREATE TABLE x (i int) STORED BY ICEBERG STORED AS ORC; +``` + +#### Partitioned tables +You can create Iceberg partitioned tables using a command familiar to those who create non-Iceberg tables: + +```sql +CREATE TABLE x (i int) PARTITIONED BY (j int) STORED BY ICEBERG; +``` + +!!! info + The resulting table does not create partitions in HMS, but instead, converts partition data into Iceberg identity partitions. + +Use the DESCRIBE command to get information about the Iceberg identity partitions: + +```sql +DESCRIBE x; +``` +The result is: + +| col_name | data_type | comment +| ---------------------------------- | -------------- | ------- +| i | int | +| j | int | +| | NULL | NULL +| # Partition Transform Information | NULL | NULL +| # col_name | transform_type | NULL +| j | IDENTITY | NULL + +You can create Iceberg partitions using the following Iceberg partition specification syntax +(supported only from Hive 4.0.0): + +```sql +CREATE TABLE x (i int, ts timestamp) PARTITIONED BY SPEC (month(ts), bucket(2, i)) STORED BY ICEBERG; +DESCRIBE x; +``` +The result is: + +| col_name | data_type | comment +| ---------------------------------- | -------------- | ------- +| i | int | +| ts | timestamp | +| | NULL | NULL +| # Partition Transform Information | NULL | NULL +| # col_name | transform_type | NULL +| ts | MONTH | NULL +| i | BUCKET\[2\] | NULL + +The supported transformations for Hive are the same as for Spark: + +* years(ts): partition by year +* months(ts): partition by month +* days(ts) or date(ts): equivalent to dateint partitioning +* hours(ts) or date_hour(ts): equivalent to dateint and hour partitioning +* bucket(N, col): partition by hashed value mod N buckets +* truncate(L, col): partition by value truncated to L + - Strings are truncated to the given length + - Integers and longs truncate to bins: truncate(10, i) produces partitions 0, 10, 20, 30, + +!!! info + The resulting table does not create partitions in HMS, but instead, converts partition data into Iceberg partitions. + +### CREATE TABLE AS SELECT + +`CREATE TABLE AS SELECT` operation resembles the native Hive operation with a single important difference. +The Iceberg table and the corresponding Hive table are created at the beginning of the query execution. +The data is inserted / committed when the query finishes. So for a transient period the table already exists but contains no data. + +```sql +CREATE TABLE target PARTITIONED BY SPEC (year(year_field), identity_field) STORED BY ICEBERG AS + SELECT * FROM source; +``` + +### CREATE TABLE LIKE TABLE + +```sql +CREATE TABLE target LIKE source STORED BY ICEBERG; +``` + +### CREATE EXTERNAL TABLE overlaying an existing Iceberg table + +The `CREATE EXTERNAL TABLE` command is used to overlay a Hive table "on top of" an existing Iceberg table. Iceberg +tables are created using either a [`Catalog`](../../javadoc/{{ icebergVersion }}/org/apache/iceberg/catalog/Catalog.html), or an implementation of the [`Tables`](../../javadoc/{{ icebergVersion }}/org/apache/iceberg/Tables.html) interface, and Hive needs to be configured accordingly to operate on these different types of table. + +#### Hive catalog tables + +As described before, tables created by the `HiveCatalog` with Hive engine feature enabled are directly visible by the +Hive engine, so there is no need to create an overlay. + +#### Custom catalog tables + +For a table in a registered catalog, specify the catalog name in the statement using table property `iceberg.catalog`. +For example, the SQL below creates an overlay for a table in a `hadoop` type catalog named `hadoop_cat`: + +```sql +SET +iceberg.catalog.hadoop_cat.type=hadoop; +SET +iceberg.catalog.hadoop_cat.warehouse=hdfs://example.com:8020/hadoop_cat; + +CREATE +EXTERNAL TABLE database_a.table_a +STORED BY 'org.apache.iceberg.mr.hive.HiveIcebergStorageHandler' +TBLPROPERTIES ('iceberg.catalog'='hadoop_cat'); +``` + +When `iceberg.catalog` is missing from both table properties and the global Hadoop configuration, `HiveCatalog` will be +used as default. + +#### Path-based Hadoop tables + +Iceberg tables created using `HadoopTables` are stored entirely in a directory in a filesystem like HDFS. These tables +are considered to have no catalog. To indicate that, set `iceberg.catalog` property to `location_based_table`. For +example: + +```sql +CREATE +EXTERNAL TABLE table_a +STORED BY 'org.apache.iceberg.mr.hive.HiveIcebergStorageHandler' +LOCATION 'hdfs://some_bucket/some_path/table_a' +TBLPROPERTIES ('iceberg.catalog'='location_based_table'); +``` + +#### CREATE TABLE overlaying an existing Iceberg table + +You can also create a new table that is managed by a custom catalog. For example, the following code creates a table in +a custom Hadoop catalog: + +```sql +SET +iceberg.catalog.hadoop_cat.type=hadoop; +SET +iceberg.catalog.hadoop_cat.warehouse=hdfs://example.com:8020/hadoop_cat; + +CREATE TABLE database_a.table_a +( + id bigint, + name string +) PARTITIONED BY ( + dept string +) STORED BY 'org.apache.iceberg.mr.hive.HiveIcebergStorageHandler' +TBLPROPERTIES ('iceberg.catalog'='hadoop_cat'); +``` + +!!! danger + If the table to create already exists in the custom catalog, this will create a managed overlay + table. This means technically you can omit the `EXTERNAL` keyword when creating an overlay table. However, this is **not + recommended** because creating managed overlay tables could pose a risk to the shared data files in case of accidental + drop table commands from the Hive side, which would unintentionally remove all the data in the table. + +### ALTER TABLE +#### Table properties +For HiveCatalog tables the Iceberg table properties and the Hive table properties stored in HMS are kept in sync. + +!!! info + IMPORTANT: This feature is not available for other Catalog implementations. + +```sql +ALTER TABLE t SET TBLPROPERTIES('...'='...'); +``` + +#### Schema evolution +The Hive table schema is kept in sync with the Iceberg table. If an outside source (Impala/Spark/Java API/etc) +changes the schema, the Hive table immediately reflects the changes. You alter the table schema using Hive commands: + +* Rename a table +```sql +ALTER TABLE orders RENAME TO renamed_orders; +``` + +* Add a column +```sql +ALTER TABLE orders ADD COLUMNS (nickname string); +``` +* Rename a column +```sql +ALTER TABLE orders CHANGE COLUMN item fruit string; +``` +* Reorder columns +```sql +ALTER TABLE orders CHANGE COLUMN quantity quantity int AFTER price; +``` +* Change a column type - only if the Iceberg defined the column type change as safe +```sql +ALTER TABLE orders CHANGE COLUMN price price long; +``` +* Drop column by using REPLACE COLUMN to remove the old column +```sql +ALTER TABLE orders REPLACE COLUMNS (remaining string); +``` +!!! info + Note, that dropping columns is only thing REPLACE COLUMNS can be used for + i.e. if columns are specified out-of-order an error will be thrown signalling this limitation. + +#### Partition evolution +You change the partitioning schema using the following commands: + +* Change the partitioning schema to new identity partitions: +```sql +ALTER TABLE default.customers SET PARTITION SPEC (last_name); +``` +* Alternatively, provide a partition specification: +```sql +ALTER TABLE order SET PARTITION SPEC (month(ts)); +``` + +#### Table migration +You can migrate Avro / Parquet / ORC external tables to Iceberg tables using the following command: +```sql +ALTER TABLE t SET TBLPROPERTIES ('storage_handler'='org.apache.iceberg.mr.hive.HiveIcebergStorageHandler'); +``` +During the migration the data files are not changed, only the appropriate Iceberg metadata files are created. +After the migration, handle the table as a normal Iceberg table. + +#### Drop partitions +You can drop partitions based on a single / multiple partition specification using the following commands: +```sql +ALTER TABLE orders DROP PARTITION (buy_date == '2023-01-01', market_price > 1000), PARTITION (buy_date == '2024-01-01', market_price <= 2000); +``` +The partition specification supports only identity-partition columns. Transform columns in partition specification are not supported. + +#### Branches and tags + +`ALTER TABLE ... CREATE BRANCH` + +Branches can be created via the CREATE BRANCH statement with the following options: + +* Create a branch using default properties. +* Create a branch at a specific snapshot ID. +* Create a branch using system time. +* Create a branch with a specified number of snapshot retentions. +* Create a branch using specific tag. + +```sql +-- CREATE branch1 with default properties. +ALTER TABLE test CREATE BRANCH branch1; + +-- CREATE branch1 at a specific snapshot ID. +ALTER TABLE test CREATE BRANCH branch1 FOR SYSTEM_VERSION AS OF 3369973735913135680; + +-- CREATE branch1 using system time. +ALTER TABLE test CREATE BRANCH branch1 FOR SYSTEM_TIME AS OF '2023-09-16 09:46:38.939 Etc/UTC'; + +-- CREATE branch1 with a specified number of snapshot retentions. +ALTER TABLE test CREATE BRANCH branch1 FOR SYSTEM_VERSION AS OF 3369973735913135680 WITH SNAPSHOT RETENTION 5 SNAPSHOTS; + +-- CREATE branch1 using a specific tag. +ALTER TABLE test CREATE BRANCH branch1 FOR TAG AS OF tag1; +``` + +`ALTER TABLE ... CREATE TAG` + +Tags can be created via the CREATE TAG statement with the following options: + +* Create a tag using default properties. +* Create a tag at a specific snapshot ID. +* Create a tag using system time. + +```sql +-- CREATE tag1 with default properties. +ALTER TABLE test CREATE TAG tag1; + +-- CREATE tag1 at a specific snapshot ID. +ALTER TABLE test CREATE TAG tag1 FOR SYSTEM_VERSION AS OF 3369973735913135680; + +-- CREATE tag1 using system time. +ALTER TABLE test CREATE TAG tag1 FOR SYSTEM_TIME AS OF '2023-09-16 09:46:38.939 Etc/UTC'; +``` + +`ALTER TABLE ... DROP BRANCH` + +Branches can be dropped via the DROP BRANCH statement with the following options: + +* Do not fail if the branch does not exist with IF EXISTS + +```sql +-- DROP branch1 +ALTER TABLE test DROP BRANCH branch1; + +-- DROP branch1 IF EXISTS +ALTER TABLE test DROP BRANCH IF EXISTS branch1; +``` + +`ALTER TABLE ... DROP TAG` + +Tags can be dropped via the DROP TAG statement with the following options: + +* Do not fail if the tag does not exist with IF EXISTS + +```sql +-- DROP tag1 +ALTER TABLE test DROP TAG tag1; + +-- DROP tag1 IF EXISTS +ALTER TABLE test DROP TAG IF EXISTS tag1; +``` + +`ALTER TABLE ... EXECUTE FAST-FORWARD` + +An iceberg branch which is an ancestor of another branch can be fast-forwarded to the state of the other branch. + +```sql +-- This fast-forwards the branch1 to the state of main branch of the Iceberg table. +ALTER table test EXECUTE FAST-FORWARD 'branch1' 'main'; + +-- This fast-forwards the branch1 to the state of branch2. +ALTER table test EXECUTE FAST-FORWARD 'branch1' 'branch2'; +``` + +#### `ALTER TABLE ... EXECUTE CHERRY-PICK` + +Cherry-pick of a snapshot requires the ID of the snapshot. Cherry-pick of snapshots as of now is supported only on the main branch of an Iceberg table. + +```sql + ALTER table test EXECUTE CHERRY-PICK 8602659039622823857; +``` + +### TRUNCATE TABLE +The following command truncates the Iceberg table: +```sql +TRUNCATE TABLE t; +``` + +#### TRUNCATE TABLE ... PARTITION +The following command truncates the partition in an Iceberg table: +```sql +TRUNCATE TABLE orders PARTITION (customer_id = 1, first_name = 'John'); +``` +The partition specification supports only identity-partition columns. Transform columns in partition specification are not supported. + +### DROP TABLE + +Tables can be dropped using the `DROP TABLE` command: + +```sql +DROP TABLE [IF EXISTS] table_name [PURGE]; +``` + +### METADATA LOCATION + +The metadata location (snapshot location) only can be changed if the new path contains the exact same metadata json. +It can be done only after migrating the table to Iceberg, the two operation cannot be done in one step. + +```sql +ALTER TABLE t set TBLPROPERTIES ('metadata_location'='/hivemetadata/00003-a1ada2b8-fc86-4b5b-8c91-400b6b46d0f2.metadata.json'); +``` + +## DML Commands + +### SELECT +Select statements work the same on Iceberg tables in Hive. You will see the Iceberg benefits over Hive in compilation and execution: + +* **No file system listings** - especially important on blob stores, like S3 +* **No partition listing from** the Metastore +* **Advanced partition filtering** - the partition keys are not needed in the queries when they could be calculated +* Could handle **higher number of partitions** than normal Hive tables + +Here are the features highlights for Iceberg Hive read support: + +1. **Predicate pushdown**: Pushdown of the Hive SQL `WHERE` clause has been implemented so that these filters are used at the Iceberg `TableScan` level as well as by the Parquet and ORC Readers. +2. **Column projection**: Columns from the Hive SQL `SELECT` clause are projected down to the Iceberg readers to reduce the number of columns read. +3. **Hive query engines**: With Hive 4.x, the Tez query execution engine is supported. + +Some of the advanced / little used optimizations are not yet implemented for Iceberg tables, so you should check your individual queries. +Also currently the statistics stored in the MetaStore are used for query planning. This is something we are planning to improve in the future. + +Hive 4 supports select operations on branches which also work similar to the table level select operations. However, the branch must be provided as follows - +```sql +-- Branches should be specified as ..branch_ +SELECT * FROM default.test.branch_branch1; +``` + +### INSERT INTO + +Hive supports the standard single-table INSERT INTO operation: + +```sql +INSERT INTO table_a +VALUES ('a', 1); +INSERT INTO table_a +SELECT...; +``` + +Multi-table insert is also supported, but it will not be atomic. Commits occur one table at a time. +Partial changes will be visible during the commit process and failures can leave partial changes committed. +Changes within a single table will remain atomic. + +Insert-into operations on branches also work similar to the table level select operations. However, the branch must be provided as follows - +```sql +-- Branches should be specified as ..branch_ +INSERT INTO default.test.branch_branch1 +VALUES ('a', 1); +INSERT INTO default.test.branch_branch1 +SELECT...; +``` + +Here is an example of inserting into multiple tables at once in Hive SQL: + +```sql +FROM customers + INSERT INTO target1 SELECT customer_id, first_name + INSERT INTO target2 SELECT last_name, customer_id; +``` + +#### INSERT INTO ... PARTITION + +Hive 4 supports partition-level INSERT INTO operation: + +```sql +INSERT INTO table_a PARTITION (customer_id = 1, first_name = 'John') +VALUES (1,2); +INSERT INTO table_a PARTITION (customer_id = 1, first_name = 'John') +SELECT...; +``` +The partition specification supports only identity-partition columns. Transform columns in partition specification are not supported. + +### INSERT OVERWRITE +INSERT OVERWRITE can replace data in the table with the result of a query. Overwrites are atomic operations for Iceberg tables. +For nonpartitioned tables the content of the table is always removed. For partitioned tables the partitions +that have rows produced by the SELECT query will be replaced. +```sql +INSERT OVERWRITE TABLE target SELECT * FROM source; +``` + +#### INSERT OVERWRITE ... PARTITION + +Hive 4 supports partition-level INSERT OVERWRITE operation: + +```sql +INSERT OVERWRITE TABLE target PARTITION (customer_id = 1, first_name = 'John') SELECT * FROM source; +``` +The partition specification supports only identity-partition columns. Transform columns in partition specification are not supported. + +### DELETE FROM + +Hive 4 supports DELETE FROM queries to remove data from tables. + +Delete queries accept a filter to match rows to delete. + +```sql +DELETE FROM target WHERE id > 1 AND id < 10; + +DELETE FROM target WHERE id IN (SELECT id FROM source); + +DELETE FROM target WHERE id IN (SELECT min(customer_id) FROM source); +``` +If the delete filter matches entire partitions of the table, Iceberg will perform a metadata-only delete. If the filter matches individual rows of a table, then Iceberg will rewrite only the affected data files. + +### UPDATE + +Hive 4 supports UPDATE queries which accept a filter to match rows to update. + +```sql +UPDATE target SET first_name = 'Raj' WHERE id > 1 AND id < 10; + +UPDATE target SET first_name = 'Raj' WHERE id IN (SELECT id FROM source); + +UPDATE target SET first_name = 'Raj' WHERE id IN (SELECT min(customer_id) FROM source); +``` +For more complex row-level updates based on incoming data, see the section on MERGE INTO. + +### MERGE INTO + +Hive 4 added support for MERGE INTO queries that can express row-level updates. + +MERGE INTO updates a table, called the target table, using a set of updates from another query, called the source. The update for a row in the target table is found using the ON clause that is like a join condition. + +```sql +MERGE INTO target AS t -- a target table +USING source s -- the source updates +ON t.id = s.id -- condition to find updates for target rows +WHEN ... -- updates +``` + +Updates to rows in the target table are listed using WHEN MATCHED ... THEN .... Multiple MATCHED clauses can be added with conditions that determine when each match should be applied. The first matching expression is used. +```sql +WHEN MATCHED AND s.op = 'delete' THEN DELETE +WHEN MATCHED AND t.count IS NULL AND s.op = 'increment' THEN UPDATE SET t.count = 0 +WHEN MATCHED AND s.op = 'increment' THEN UPDATE SET t.count = t.count + 1 +``` + +Source rows (updates) that do not match can be inserted: +```sql +WHEN NOT MATCHED THEN INSERT VALUES (s.a, s.b, s.c) +``` +Only one record in the source data can update any given row of the target table, or else an error will be thrown. + +### QUERYING METADATA TABLES +Hive supports querying of the Iceberg Metadata tables. The tables could be used as normal +Hive tables, so it is possible to use projections / joins / filters / etc. +To reference a metadata table the full name of the table should be used, like: +... + +Currently the following metadata tables are available in Hive: + +* all_data_files +* all_delete_files +* all_entries +* all_files +* all_manifests +* data_files +* delete_files +* entries +* files +* history +* manifests +* metadata_log_entries +* partitions +* refs +* snapshots + +```sql +SELECT * FROM default.table_a.files; +``` + +### TIMETRAVEL +Hive supports snapshot id based and time base timetravel queries. +For these views it is possible to use projections / joins / filters / etc. +The function is available with the following syntax: +```sql +SELECT * FROM table_a FOR SYSTEM_TIME AS OF '2021-08-09 10:35:57'; +SELECT * FROM table_a FOR SYSTEM_VERSION AS OF 1234567; +``` + +You can expire snapshots of an Iceberg table using an ALTER TABLE query from Hive. You should periodically expire snapshots to delete data files that is no longer needed, and reduce the size of table metadata. + +Each write to an Iceberg table from Hive creates a new snapshot, or version, of a table. Snapshots can be used for time-travel queries, or the table can be rolled back to any valid snapshot. Snapshots accumulate until they are expired by the expire_snapshots operation. +Enter a query to expire snapshots having the following timestamp: `2021-12-09 05:39:18.689000000` +```sql +ALTER TABLE test_table EXECUTE expire_snapshots('2021-12-09 05:39:18.689000000'); +``` + +### `DELETE ORPHAN-FILES` + +Used to remove files which are not referenced in any metadata files of an Iceberg table and can thus be considered "orphaned". +The function is available with the following syntax: +```sql +ALTER TABLE table_a EXECUTE DELETE ORPHAN-FILES; +ALTER TABLE table_a EXECUTE DELETE ORPHAN-FILES OLDER THAN ('2021-12-09 05:39:18.689000000'); +``` + +### Type compatibility + +Hive and Iceberg support different set of types. Iceberg can perform type conversion automatically, but not for all +combinations, so you may want to understand the type conversion in Iceberg in prior to design the types of columns in +your tables. You can enable auto-conversion through Hadoop configuration (not enabled by default): + +| Config key | Default | Description | +| -----------------------------------------| --------------------------- | --------------------------------------------------- | +| iceberg.mr.schema.auto.conversion | false | if Hive should perform type auto-conversion | + +### Hive type to Iceberg type + +This type conversion table describes how Hive types are converted to the Iceberg types. The conversion applies on both +creating Iceberg table and writing to Iceberg table via Hive. + +| Hive | Iceberg | Notes | +|------------------|-------------------------|-------| +| boolean | boolean | | +| short | integer | auto-conversion | +| byte | integer | auto-conversion | +| integer | integer | | +| long | long | | +| float | float | | +| double | double | | +| date | date | | +| timestamp | timestamp without timezone | | +| timestamplocaltz | timestamp with timezone | Hive 3 only | +| interval_year_month | | not supported | +| interval_day_time | | not supported | +| char | string | auto-conversion | +| varchar | string | auto-conversion | +| string | string | | +| binary | binary | | +| decimal | decimal | | +| struct | struct | | +| list | list | | +| map | map | | +| union | | not supported | + +### Table rollback + +Rolling back iceberg table's data to the state at an older table snapshot. + +Rollback to the last snapshot before a specific timestamp + +```sql +ALTER TABLE ice_t EXECUTE ROLLBACK('2022-05-12 00:00:00') +``` + +Rollback to a specific snapshot ID +```sql +ALTER TABLE ice_t EXECUTE ROLLBACK(1111); +``` + +### Compaction + +Hive 4 supports full table compaction of Iceberg tables using the following commands: +```sql +-- Using the ALTER TABLE ... COMPACT syntax +ALTER TABLE t COMPACT 'major'; + +-- Using the OPTIMIZE TABLE ... REWRITE DATA syntax +OPTIMIZE TABLE t REWRITE DATA; +``` +Both these syntax have the same effect of performing full table compaction on an Iceberg table. diff --git a/1.11.0/docs/index.md b/1.11.0/docs/index.md new file mode 100644 index 000000000000..19dc954b51b3 --- /dev/null +++ b/1.11.0/docs/index.md @@ -0,0 +1,52 @@ +--- +title: "Introduction" +--- + + +# Documentation + +**Apache Iceberg is an open table format for huge analytic datasets.** Iceberg adds tables to compute engines including Spark, Trino, PrestoDB, Flink, Hive and Impala using a high-performance table format that works just like a SQL table. + +### User experience + +Iceberg avoids unpleasant surprises. Schema evolution works and won't inadvertently un-delete data. Users don't need to know about partitioning to get fast queries. + +* [Schema evolution](evolution.md#schema-evolution) supports add, drop, update, or rename, and has [no side-effects](evolution.md#correctness) +* [Hidden partitioning](partitioning.md) prevents user mistakes that cause silently incorrect results or extremely slow queries +* [Partition layout evolution](evolution.md#partition-evolution) can update the layout of a table as data volume or query patterns change +* [Time travel](spark-queries.md#time-travel) enables reproducible queries that use exactly the same table snapshot, or lets users easily examine changes +* Version rollback allows users to quickly correct problems by resetting tables to a good state + +### Reliability and performance + +Iceberg was built for huge tables. Iceberg is used in production where a single table can contain tens of petabytes of data and even these huge tables can be read without a distributed SQL engine. + +* [Scan planning is fast](performance.md#scan-planning) -- a distributed SQL engine isn't needed to read a table or find files +* [Advanced filtering](performance.md#data-filtering) -- data files are pruned with partition and column-level stats, using table metadata + +Iceberg was designed to solve correctness problems in eventually-consistent cloud object stores. + +* [Works with any cloud store](reliability.md) and reduces NN congestion when in HDFS, by avoiding listing and renames +* [Serializable isolation](reliability.md) -- table changes are atomic and readers never see partial or uncommitted changes +* [Multiple concurrent writers](reliability.md#concurrent-write-operations) use optimistic concurrency and will retry to ensure that compatible updates succeed, even when writes conflict + +### Open standard + +Iceberg has been designed and developed to be an open community standard with a [specification](../../spec.md) to ensure compatibility across languages and implementations. + +[Apache Iceberg is open source](../../community.md), and is developed at the [Apache Software Foundation](https://www.apache.org/). diff --git a/1.11.0/docs/java-api-quickstart.md b/1.11.0/docs/java-api-quickstart.md new file mode 100644 index 000000000000..74b40feed86d --- /dev/null +++ b/1.11.0/docs/java-api-quickstart.md @@ -0,0 +1,296 @@ +--- +title: "Java Quickstart" +--- + + +# Java API Quickstart + +## Create a table + +Tables are created using either a [`Catalog`](../../javadoc/{{ icebergVersion }}/org/apache/iceberg/catalog/Catalog.html) or an implementation of the [`Tables`](../../javadoc/{{ icebergVersion }}/org/apache/iceberg/Tables.html) interface. + +### Using a Hive catalog + +The Hive catalog connects to a Hive metastore to keep track of Iceberg tables. +You can initialize a Hive catalog with a name and some properties. +(see: [Catalog properties](catalog-properties.md)) + +```java +import java.util.HashMap; +import java.util.Map; + +import org.apache.iceberg.hive.HiveCatalog; + +HiveCatalog catalog = new HiveCatalog(); +catalog.setConf(spark.sparkContext().hadoopConfiguration()); // Optionally use Spark's Hadoop configuration + +Map properties = new HashMap(); +properties.put("warehouse", "..."); +properties.put("uri", "..."); + +catalog.initialize("hive", properties); +``` + +`HiveCatalog` implements the `Catalog` interface, which defines methods for working with tables, like `createTable`, `loadTable`, `renameTable`, and `dropTable`. +To create a table, pass an `Identifier` and a `Schema` along with other initial metadata: + +```java +import org.apache.iceberg.Table; +import org.apache.iceberg.catalog.TableIdentifier; + +TableIdentifier name = TableIdentifier.of("logging", "logs"); +Table table = catalog.createTable(name, schema, spec); + +// or to load an existing table, use the following line +Table table = catalog.loadTable(name); +``` + +The table's [schema](#create-a-schema) and [partition spec](#create-a-partition-spec) are created below. + +### Using a Hadoop catalog + +A Hadoop catalog doesn't need to connect to a Hive MetaStore, but can only be used with HDFS or similar file systems that support atomic rename. Concurrent writes with a Hadoop catalog are not safe with a local FS or S3. To create a Hadoop catalog: + +```java +import org.apache.hadoop.conf.Configuration; +import org.apache.iceberg.hadoop.HadoopCatalog; + +Configuration conf = new Configuration(); +String warehousePath = "hdfs://host:8020/warehouse_path"; +HadoopCatalog catalog = new HadoopCatalog(conf, warehousePath); +``` + +Like the Hive catalog, `HadoopCatalog` implements `Catalog`, so it also has methods for working with tables, like `createTable`, `loadTable`, and `dropTable`. + +This example creates a table with Hadoop catalog: + +```java +import org.apache.iceberg.Table; +import org.apache.iceberg.catalog.TableIdentifier; + +TableIdentifier name = TableIdentifier.of("logging", "logs"); +Table table = catalog.createTable(name, schema, spec); + +// or to load an existing table, use the following line +Table table = catalog.loadTable(name); +``` + +The table's [schema](#create-a-schema) and [partition spec](#create-a-partition-spec) are created below. + +### Tables in Spark + +Spark can work with table by name using `HiveCatalog`. + +```java +// spark.sql.catalog.hive_prod = org.apache.iceberg.spark.SparkCatalog +// spark.sql.catalog.hive_prod.type = hive +spark.table("logging.logs"); +``` + +Spark can also load table created by `HadoopCatalog` by path. +```java +spark.read.format("iceberg").load("hdfs://host:8020/warehouse_path/logging/logs"); +``` + +## Schemas + +### Create a schema + +This example creates a schema for a `logs` table: + +```java +import org.apache.iceberg.Schema; +import org.apache.iceberg.types.Types; + +Schema schema = new Schema( + Types.NestedField.required(1, "level", Types.StringType.get()), + Types.NestedField.required(2, "event_time", Types.TimestampType.withZone()), + Types.NestedField.required(3, "message", Types.StringType.get()), + Types.NestedField.optional(4, "call_stack", Types.ListType.ofRequired(5, Types.StringType.get())) + ); +``` + +When using the Iceberg API directly, type IDs are required. Conversions from other schema formats, like Spark, Avro, and Parquet will automatically assign new IDs. + +When a table is created, all IDs in the schema are re-assigned to ensure uniqueness. + +### Convert a schema from Avro + +To create an Iceberg schema from an existing Avro schema, use converters in `AvroSchemaUtil`: + +```java +import org.apache.avro.Schema; +import org.apache.avro.Schema.Parser; +import org.apache.iceberg.avro.AvroSchemaUtil; + +Schema avroSchema = new Parser().parse("{\"type\": \"record\" , ... }"); +Schema icebergSchema = AvroSchemaUtil.toIceberg(avroSchema); +``` + +### Convert a schema from Spark + +To create an Iceberg schema from an existing table, use converters in `SparkSchemaUtil`: + +```java +import org.apache.iceberg.spark.SparkSchemaUtil; + +Schema schema = SparkSchemaUtil.schemaForTable(sparkSession, tableName); +``` + +## Partitioning + +### Create a partition spec + +Partition specs describe how Iceberg should group records into data files. Partition specs are created for a table's schema using a builder. + +This example creates a partition spec for the `logs` table that partitions records by the hour of the log event's timestamp and by log level: + +```java +import org.apache.iceberg.PartitionSpec; + +PartitionSpec spec = PartitionSpec.builderFor(schema) + .hour("event_time") + .identity("level") + .build(); +``` + +For more information on the different partition transforms that Iceberg offers, visit [this page](../../spec.md#partitioning). + +## Branching and Tagging + +### Creating branches and tags + +New branches and tags can be created via the Java library's ManageSnapshots API. + +```java + +/* Create a branch test-branch which is retained for 1 week, and the latest 2 snapshots on test-branch will always be retained. +Snapshots on test-branch which are created within the last hour will also be retained. */ + +String branch = "test-branch"; +table.manageSnapshots() + .createBranch(branch, 3) + .setMinSnapshotsToKeep(branch, 2) + .setMaxSnapshotAgeMs(branch, 3600000) + .setMaxRefAgeMs(branch, 604800000) + .commit(); + +// Create a tag historical-tag at snapshot 10 which is retained for a day +String tag = "historical-tag" +table.manageSnapshots() + .createTag(tag, 10) + .setMaxRefAgeMs(tag, 86400000) + .commit(); +``` + +### Committing to branches + +Writing to a branch can be performed by specifying `toBranch` in the operation. For the full list refer to [UpdateOperations](api.md#update-operations). +```java +// Append FILE_A to branch test-branch +String branch = "test-branch"; + +table.newAppend() + .appendFile(FILE_A) + .toBranch(branch) + .commit(); + + +// Perform row level updates on "test-branch" +table.newRowDelta() + .addRows(DATA_FILE) + .addDeletes(DELETES) + .toBranch(branch) + .commit(); + + +// Perform a rewrite operation replacing SMALL_FILE_1 and SMALL_FILE_2 on "test-branch" with compactedFile. +table.newRewrite() + .rewriteFiles(ImmutableSet.of(SMALL_FILE_1, SMALL_FILE_2), ImmutableSet.of(compactedFile)) + .toBranch(branch) + .commit(); + +``` + +### Reading from branches and tags +Reading from a branch or tag can be done as usual via the Table Scan API, by passing in a branch or tag in the `useRef` API. When a branch is passed in, the snapshot that's used is the head of the branch. Note that currently reading from a branch and specifying an `asOfSnapshotId` in the scan is not supported. + +```java +// Read from the head snapshot of test-branch +TableScan branchRead = table.newScan().useRef("test-branch"); + +// Read from the snapshot referenced by audit-tag +TableScan tagRead = table.newScan().useRef("audit-tag"); +``` + +### Replacing and fast forwarding branches and tags + +The snapshots which existing branches and tags point to can be updated via the `replace` APIs. The fast forward operation is similar to git fast-forwarding. Fast forward can be used to advance a target branch to the head of a source branch or a tag when the target branch is an ancestor of the source. For both fast forward and replace, retention properties of the target branch are maintained by default. + +```java + +// Update "test-branch" to point to snapshot 4 +table.manageSnapshots() + .replaceBranch(branch, 4) + .commit() + +String tag = "audit-tag"; +// Replace "audit-tag" to point to snapshot 3 and update its retention +table.manageSnapshots() + .replaceBranch(tag, 4) + .setMaxRefAgeMs(1000) + .commit() + + +``` + +### Updating retention properties + +Retention properties for branches and tags can be updated as well. +Use the setMaxRefAgeMs for updating the retention property of the branch or tag itself. Branch snapshot retention properties can be updated via the `setMinSnapshotsToKeep` and `setMaxSnapshotAgeMs` APIs. + +```java +String branch = "test-branch"; +// Update retention properties for test-branch +table.manageSnapshots() + .setMinSnapshotsToKeep(branch, 10) + .setMaxSnapshotAgeMs(branch, 7200000) + .setMaxRefAgeMs(branch, 604800000) + .commit(); + +// Update retention properties for test-tag +table.manageSnapshots() + .setMaxRefAgeMs("test-tag", 604800000) + .commit(); +``` + +### Removing branches and tags + +Branches and tags can be removed via the `removeBranch` and `removeTag` APIs respectively + +```java +// Remove test-branch +table.manageSnapshots() + .removeBranch("test-branch") + .commit() + +// Remove test-tag +table.manageSnapshots() + .removeTag("test-tag") + .commit() +``` diff --git a/1.11.0/docs/jdbc.md b/1.11.0/docs/jdbc.md new file mode 100644 index 000000000000..b1717096a768 --- /dev/null +++ b/1.11.0/docs/jdbc.md @@ -0,0 +1,69 @@ +--- +title: "JDBC" +--- + + +# Iceberg JDBC Integration + +## JDBC Catalog + +Iceberg supports using a table in a relational database to manage Iceberg tables through JDBC. +The database that JDBC connects to must support atomic transaction to allow the JDBC catalog implementation to +properly support atomic Iceberg table commits and read serializable isolation. + +### Configurations + +Because each database and database service provider might require different configurations, +the JDBC catalog allows arbitrary configurations through: + +| Property | Default | Description | +| -------------------- | --------------------------------- | ------------------------------------------------------ | +| uri | | the JDBC connection string | +| jdbc. | | any key value pairs to configure the JDBC connection | + +### Examples + +#### Spark + +You can start a Spark session with a MySQL JDBC connection using the following configurations: + +```shell +spark-sql --packages org.apache.iceberg:iceberg-spark-runtime-3.5_2.12:{{ icebergVersion }} \ + --conf spark.sql.catalog.my_catalog=org.apache.iceberg.spark.SparkCatalog \ + --conf spark.sql.catalog.my_catalog.warehouse=s3://my-bucket/my/key/prefix \ + --conf spark.sql.catalog.my_catalog.type=jdbc \ + --conf spark.sql.catalog.my_catalog.uri=jdbc:mysql://test.1234567890.us-west-2.rds.amazonaws.com:3306/default \ + --conf spark.sql.catalog.my_catalog.jdbc.verifyServerCertificate=true \ + --conf spark.sql.catalog.my_catalog.jdbc.useSSL=true \ + --conf spark.sql.catalog.my_catalog.jdbc.user=admin \ + --conf spark.sql.catalog.my_catalog.jdbc.password=pass +``` + +#### Java API + +```java +Class.forName("com.mysql.cj.jdbc.Driver"); // ensure JDBC driver is at runtime classpath +Map properties = new HashMap<>(); +properties.put(CatalogProperties.CATALOG_IMPL, JdbcCatalog.class.getName()); +properties.put(CatalogProperties.URI, "jdbc:mysql://localhost:3306/test"); +properties.put(JdbcCatalog.PROPERTY_PREFIX + "user", "admin"); +properties.put(JdbcCatalog.PROPERTY_PREFIX + "password", "pass"); +properties.put(CatalogProperties.WAREHOUSE_LOCATION, "s3://warehouse/path"); +Configuration hadoopConf = new Configuration(); // configs if you use HadoopFileIO +JdbcCatalog catalog = CatalogUtil.buildIcebergCatalog("test_jdbc_catalog", properties, hadoopConf); +``` diff --git a/1.11.0/docs/kafka-connect.md b/1.11.0/docs/kafka-connect.md new file mode 100644 index 000000000000..9c7d3d83f2e4 --- /dev/null +++ b/1.11.0/docs/kafka-connect.md @@ -0,0 +1,532 @@ +--- +title: "Kafka Connect" +--- + + +# Kafka Connect + +[Kafka Connect](https://kafka.apache.org/documentation/#connect) is a popular framework for moving data +in and out of Apache Kafka via connectors. There are many different connectors available, such as the S3 sink +for writing data from Kafka to S3 and Debezium source connectors for writing change data capture records from relational +databases to Kafka. + +It has a straightforward, decentralized, distributed architecture. A cluster consists of a number of worker processes, +and a connector runs tasks on these processes to perform the work. Connector deployment is configuration driven, so +generally no code needs to be written to run a connector. + +## Apache Iceberg Sink Connector + +The Apache Iceberg Sink Connector for Kafka Connect is a sink connector for writing data from Kafka into Iceberg tables. + +## Features + +* Commit coordination for centralized Iceberg commits +* Exactly-once delivery semantics +* Multi-table fan-out +* Automatic table creation and schema evolution +* Field name mapping via Iceberg’s column mapping functionality + +## Installation + +The connector zip archive is created as part of the Iceberg build. You can run the build via: +```bash +./gradlew -x test -x integrationTest clean build +``` +The zip archive will be found under `./kafka-connect/kafka-connect-runtime/build/distributions`. There is +one distribution that bundles the Hive Metastore client and related dependencies, and one that does not. +Copy the distribution archive into the Kafka Connect plugins directory on all nodes. + +## Requirements + +The sink relies on [KIP-447](https://cwiki.apache.org/confluence/display/KAFKA/KIP-447%3A+Producer+scalability+for+exactly+once+semantics) +for exactly-once semantics. This requires Kafka 2.5 or later. + +## Configuration + +| Property | Description | +|--------------------------------------------|------------------------------------------------------------------------------------------------------------------| +| iceberg.tables | Comma-separated list of destination tables | +| iceberg.tables.dynamic-enabled | Set to `true` to route to a table specified in `routeField` instead of using `routeRegex`, default is `false` | +| iceberg.tables.route-field | For multi-table fan-out, the name of the field used to route records to tables | +| iceberg.tables.default-commit-branch | Default branch for commits, main is used if not specified | +| iceberg.tables.default-id-columns | Default comma-separated list of columns that identify a row in tables (primary key) | +| iceberg.tables.default-partition-by | Default comma-separated list of partition field names to use when creating tables | +| iceberg.tables.auto-create-enabled | Set to `true` to automatically create destination tables, default is `false` | +| iceberg.tables.evolve-schema-enabled | Set to `true` to add any missing record fields to the table schema, default is `false` | +| iceberg.tables.schema-force-optional | Set to `true` to set columns as optional during table create and evolution, default is `false` to respect schema | +| iceberg.tables.schema-case-insensitive | Set to `true` to look up table columns by case-insensitive name, default is `false` for case-sensitive | +| iceberg.tables.auto-create-props.* | Properties set on new tables during auto-create | +| iceberg.tables.write-props.* | Properties passed through to Iceberg writer initialization, these take precedence | +| iceberg.table.<_table-name_\>.commit-branch | Table-specific branch for commits, use `iceberg.tables.default-commit-branch` if not specified | +| iceberg.table.<_table-name_\>.id-columns | Comma-separated list of columns that identify a row in the table (primary key) | +| iceberg.table.<_table-name_\>.partition-by | Comma-separated list of partition fields to use when creating the table | +| iceberg.table.<_table-name_\>.route-regex | The regex used to match a record's `routeField` to a table | +| iceberg.control.topic | Name of the control topic, default is `control-iceberg` | +| iceberg.control.group-id-prefix | Prefix for the control consumer group, default is `cg-control` | +| iceberg.control.commit.interval-ms | Commit interval in msec, default is 300,000 (5 min) | +| iceberg.control.commit.timeout-ms | Commit timeout interval in msec, default is 30,000 (30 sec) | +| iceberg.control.commit.threads | Number of threads to use for commits, default is (`cores * 2`) | +| iceberg.coordinator.transactional.prefix | Prefix for the transactional id to use for the coordinator producer, default is to use no/empty prefix | +| iceberg.catalog | Name of the catalog, default is `iceberg` | +| iceberg.catalog.* | Properties passed through to Iceberg catalog initialization | +| iceberg.hadoop-conf-dir | If specified, Hadoop config files in this directory will be loaded | +| iceberg.hadoop.* | Properties passed through to the Hadoop configuration | +| iceberg.kafka.* | Properties passed through to control topic Kafka client initialization | + +If `iceberg.tables.dynamic-enabled` is `false` (the default) then you must specify `iceberg.tables`. If +`iceberg.tables.dynamic-enabled` is `true` then you must specify `iceberg.tables.route-field` which will +contain the name of the table. + +### Kafka configuration + +By default the connector will attempt to use Kafka client config from the worker properties for connecting to +the control topic. If that config cannot be read for some reason, Kafka client settings +can be set explicitly using `iceberg.kafka.*` properties. + +#### Message format + +Messages should be converted to a struct or map using the appropriate Kafka Connect converter. + +### Catalog configuration + +The `iceberg.catalog.*` properties are required for connecting to the Iceberg catalog. The core catalog +types are included in the default distribution, including REST, Glue, DynamoDB, Hadoop, Nessie, +JDBC, Hive and BigQuery Metastore. JDBC drivers are not included in the default distribution, so you will need to include +those if needed. When using a Hive catalog, you can use the distribution that includes the Hive metastore client, +otherwise you will need to include that yourself. + +To set the catalog type, you can set `iceberg.catalog.type` to `rest`, `hive`, or `hadoop`. For other +catalog types, you need to instead set `iceberg.catalog.catalog-impl` to the name of the catalog class. + +#### REST example + +``` +"iceberg.catalog.type": "rest", +"iceberg.catalog.uri": "https://catalog-service", +"iceberg.catalog.credential": "", +"iceberg.catalog.warehouse": "", +``` + +#### Hive example + +NOTE: Use the distribution that includes the HMS client (or include the HMS client yourself). Use `S3FileIO` when +using S3 for storage and `GCSFileIO` when using GCS (the default is `HadoopFileIO` with `HiveCatalog`). +``` +"iceberg.catalog.type": "hive", +"iceberg.catalog.uri": "thrift://hive:9083", +"iceberg.catalog.io-impl": "org.apache.iceberg.aws.s3.S3FileIO", +"iceberg.catalog.warehouse": "s3a://bucket/warehouse", +"iceberg.catalog.client.region": "us-east-1", +"iceberg.catalog.s3.access-key-id": "", +"iceberg.catalog.s3.secret-access-key": "", +``` + +#### Glue example + +``` +"iceberg.catalog.catalog-impl": "org.apache.iceberg.aws.glue.GlueCatalog", +"iceberg.catalog.warehouse": "s3a://bucket/warehouse", +"iceberg.catalog.io-impl": "org.apache.iceberg.aws.s3.S3FileIO", +``` + +#### Nessie example + +``` +"iceberg.catalog.catalog-impl": "org.apache.iceberg.nessie.NessieCatalog", +"iceberg.catalog.uri": "http://localhost:19120/api/v2", +"iceberg.catalog.ref": "main", +"iceberg.catalog.warehouse": "s3a://bucket/warehouse", +"iceberg.catalog.io-impl": "org.apache.iceberg.aws.s3.S3FileIO", +``` + +#### BigQuery Metastore example + +``` +"iceberg.catalog.catalog-impl": "org.apache.iceberg.gcp.bigquery.BigQueryMetastoreCatalog", +"iceberg.catalog.gcp.bigquery.project-id": "my-project", +"iceberg.catalog.gcp.bigquery.location": "us-east1", +"iceberg.catalog.warehouse": "gs://bucket/warehouse", +"iceberg.catalog.io-impl": "org.apache.iceberg.gcp.gcs.GCSFileIO", +"iceberg.tables.auto-create-props.bq_connection": "projects/my-project/locations/us-east1/connections/my-connection", +``` + +#### Notes + +Depending on your setup, you may need to also set `iceberg.catalog.s3.endpoint`, `iceberg.catalog.s3.staging-dir`, +or `iceberg.catalog.s3.path-style-access`. See the [Iceberg docs](https://iceberg.apache.org/docs/latest/) for +full details on configuring catalogs. + +### Azure ADLS configuration example + +When using ADLS, Azure requires the passing of AZURE_CLIENT_ID, AZURE_TENANT_ID, and AZURE_CLIENT_SECRET for its Java SDK. +If you're running Kafka Connect in a container, be sure to inject those values as environment variables. See the +[Azure Identity Client library for Java](https://learn.microsoft.com/en-us/java/api/overview/azure/identity-readme?view=azure-java-stable) for more information. + +An example of these would be: +``` +AZURE_CLIENT_ID=e564f687-7b89-4b48-80b8-111111111111 +AZURE_TENANT_ID=95f2f365-f5b7-44b1-88a1-111111111111 +AZURE_CLIENT_SECRET="XXX" +``` +Where the CLIENT_ID is the Application ID of a registered application under +[App Registrations](https://portal.azure.com/#view/Microsoft_AAD_RegisteredApps/ApplicationsListBlade), the TENANT_ID is +from your [Azure Tenant Properties](https://portal.azure.com/#view/Microsoft_AAD_IAM/TenantProperties.ReactView), and +the CLIENT_SECRET is created within the "Certificates & Secrets" section, under "Manage" after choosing your specific +App Registration. You might have to choose "Client secrets" in the middle panel and the "+" in front of "New client secret" +to generate one. Be sure to set this variable to the Value and not the Id. + +It's also important that the App Registration is granted the Role Assignment "Storage Blob Data Contributor" in your +Storage Account's Access Control (IAM), or it won't be able to write new files there. + +Then, within the Connector's configuration, you'll want to include the following: + +``` +"iceberg.catalog.type": "rest", +"iceberg.catalog.uri": "https://catalog:8181", +"iceberg.catalog.warehouse": "abfss://storage-container-name@storageaccount.dfs.core.windows.net/warehouse", +"iceberg.catalog.io-impl": "org.apache.iceberg.azure.adlsv2.ADLSFileIO", +"iceberg.catalog.include-credentials": "true" +``` + +Where `storage-container-name` is the container name within your Azure Storage Account, `/warehouse` is the location +within that container where your Apache Iceberg files will be written by default (or if iceberg.tables.auto-create-enabled=true), +and the `include-credentials` parameter passes along the Azure Java client credentials along. This will configure the +Iceberg Sink connector to connect to the REST catalog implementation at `iceberg.catalog.uri` to obtain the required +Connection String for the ADLSv2 client + +### Google GCS configuration example + +By default, Application Default Credentials (ADC) will be used to connect to GCS. Details on how ADC works can +be found in the [Google Cloud documentation](https://cloud.google.com/docs/authentication/application-default-credentials). + +``` +"iceberg.catalog.type": "rest", +"iceberg.catalog.uri": "https://catalog:8181", +"iceberg.catalog.warehouse": "gs://bucket-name/warehouse", +"iceberg.catalog.io-impl": "org.apache.iceberg.gcp.gcs.GCSFileIO" +``` + +### Hadoop configuration + +When using HDFS or Hive, the sink will initialize the Hadoop configuration. First, config files +from the classpath are loaded. Next, if `iceberg.hadoop-conf-dir` is specified, config files +are loaded from that location. Finally, any `iceberg.hadoop.*` properties from the sink config are +applied. When merging these, the order of precedence is sink config > config dir > classpath. + +## Examples + +### Initial setup + +#### Source topic + +This assumes the source topic already exists and is named `events`. + +#### Control topic + +If your Kafka cluster has `auto.create.topics.enable` set to `true` (the default), then the control topic will be +automatically created. If not, then you will need to create the topic first. The default topic name is `control-iceberg`: +```bash +bin/kafka-topics.sh \ + --command-config command-config.props \ + --bootstrap-server ${CONNECT_BOOTSTRAP_SERVERS} \ + --create \ + --topic control-iceberg \ + --partitions 1 +``` +*NOTE: Clusters running on Confluent Cloud have `auto.create.topics.enable` set to `false` by default.* + +#### Iceberg catalog configuration + +Configuration properties with the prefix `iceberg.catalog.` will be passed to Iceberg catalog initialization. +See the [Iceberg docs](https://iceberg.apache.org/docs/latest/) for details on how to configure +a particular catalog. + +### Single destination table + +This example writes all incoming records to a single table. + +#### Create the destination table + +```sql +CREATE TABLE default.events ( + id STRING, + type STRING, + ts TIMESTAMP, + payload STRING) +PARTITIONED BY (hours(ts)) +``` + +#### Connector config + +This example config connects to a Iceberg REST catalog. +```json +{ + "name": "events-sink", + "config": { + "connector.class": "org.apache.iceberg.connect.IcebergSinkConnector", + "tasks.max": "2", + "topics": "events", + "iceberg.tables": "default.events", + "iceberg.catalog.type": "rest", + "iceberg.catalog.uri": "https://localhost", + "iceberg.catalog.credential": "", + "iceberg.catalog.warehouse": "" + } +} +``` + +### Multi-table fan-out, static routing + +This example writes records with `type` set to `list` to the table `default.events_list`, and +writes records with `type` set to `create` to the table `default.events_create`. Other records +will be skipped. + +#### Create two destination tables + +```sql +CREATE TABLE default.events_list ( + id STRING, + type STRING, + ts TIMESTAMP, + payload STRING) +PARTITIONED BY (hours(ts)); + +CREATE TABLE default.events_create ( + id STRING, + type STRING, + ts TIMESTAMP, + payload STRING) +PARTITIONED BY (hours(ts)); +``` + +#### Connector config + +```json +{ + "name": "events-sink", + "config": { + "connector.class": "org.apache.iceberg.connect.IcebergSinkConnector", + "tasks.max": "2", + "topics": "events", + "iceberg.tables": "default.events_list,default.events_create", + "iceberg.tables.route-field": "type", + "iceberg.table.default.events_list.route-regex": "list", + "iceberg.table.default.events_create.route-regex": "create", + "iceberg.catalog.type": "rest", + "iceberg.catalog.uri": "https://localhost", + "iceberg.catalog.credential": "", + "iceberg.catalog.warehouse": "" + } +} +``` + +### Multi-table fan-out, dynamic routing + +This example writes to tables with names from the value in the `db_table` field. If a table with +the name does not exist, then the record will be skipped. For example, if the record's `db_table` +field is set to `default.events_list`, then the record is written to the `default.events_list` table. + +#### Create two destination tables + +See above for creating two tables. + +#### Connector config + +```json +{ + "name": "events-sink", + "config": { + "connector.class": "org.apache.iceberg.connect.IcebergSinkConnector", + "tasks.max": "2", + "topics": "events", + "iceberg.tables.dynamic-enabled": "true", + "iceberg.tables.route-field": "db_table", + "iceberg.catalog.type": "rest", + "iceberg.catalog.uri": "https://localhost", + "iceberg.catalog.credential": "", + "iceberg.catalog.warehouse": "" + } +} +``` + +## SMTs for the Apache Iceberg Sink Connector + +This project contains some SMTs that could be useful when transforming Kafka data for use by +the Iceberg sink connector. + +### CopyValue +_(Experimental)_ + +The `CopyValue` SMT copies a value from one field to a new field. + +#### Configuration + +| Property | Description | +|------------------|-------------------| +| source.field | Source field name | +| target.field | Target field name | + +#### Example + +``` +"transforms": "copyId", +"transforms.copyId.type": "org.apache.iceberg.connect.transforms.CopyValue", +"transforms.copyId.source.field": "id", +"transforms.copyId.target.field": "id_copy", +``` + +### DmsTransform +_(Experimental)_ + +The `DmsTransform` SMT transforms an AWS DMS formatted message for use by the sink's CDC feature. +It will promote the `data` element fields to top level and add the following metadata fields: +`_cdc.op`, `_cdc.ts`, and `_cdc.source`. + +##### Configuration + +The SMT currently has no configuration. + +### DebeziumTransform +_(Experimental)_ + +The `DebeziumTransform` SMT transforms a Debezium formatted message for use by the sink's CDC feature. +It will promote the `before` or `after` element fields to top level and add the following metadata fields: +`_cdc.op`, `_cdc.ts`, `_cdc.offset`, `_cdc.source`, `_cdc.target`, and `_cdc.key`. + +##### Configuration + +| Property | Description | +|---------------------|-----------------------------------------------------------------------------------| +| cdc.target.pattern | Pattern to use for setting the CDC target field value, default is `{db}.{table}` | + +### JsonToMapTransform +_(Experimental)_ + +The `JsonToMapTransform` SMT parses Strings as Json object payloads to infer schemas. The iceberg-kafka-connect +connector for schema-less data (e.g. the Map produced by the Kafka supplied JsonConverter) is to convert Maps into Iceberg +Structs. This is fine when the JSON is well-structured, but when you have JSON objects with dynamically +changing keys, it will lead to an explosion of columns in the Iceberg table due to schema evolutions. + +This SMT is useful in situations where the JSON is not well-structured, in order to get data into Iceberg where +it can be further processed by query engines into a more manageable form. It will convert nested objects to +Maps and include Map type in the Schema. The connector will respect the Schema and create Iceberg tables with Iceberg +Map (String) columns for the JSON objects. + +Note: + +- You must use the `stringConverter` as the `value.converter` setting for your connector, not `jsonConverter` + - It expects JSON objects (`{...}`) in those strings. +- Message keys, tombstones, and headers are not transformed and are passed along as-is by the SMT + +##### Configuration + +| Property | Description (default value) | +|----------------------|------------------------------------------| +| json.root | (false) Boolean value to start at root | + +The `transforms.IDENTIFIER_HERE.json.root` is meant for the most inconsistent data. It will construct a Struct with a single field +called `payload` with a Schema of `Map`. + +If `transforms.IDENTIFIER_HERE.json.root` is false (the default), it will construct a Struct with inferred schemas for primitive and +array fields. Nested objects become fields of type `Map`. + +Keys with empty arrays and empty objects are filtered out from the final schema. Arrays will be typed unless the +json arrays have mixed types in which case they are converted to arrays of strings. + +Example json: + +```json +{ + "key": 1, + "array": [1,"two",3], + "empty_obj": {}, + "nested_obj": {"some_key": ["one", "two"]} +} +``` + +Will become the following if `json.root` is true: + +``` +SinkRecord.schema: + "payload" : (Optional) Map + +Sinkrecord.value (Struct): + "payload" : Map( + "key" : "1", + "array" : "[1,"two",3]" + "empty_obj": "{}" + "nested_obj": "{"some_key":["one","two"]}" + ) +``` + +Will become the following if `json.root` is false + +``` +SinkRecord.schema: + "key": (Optional) Int32, + "array": (Optional) Array, + "nested_object": (Optional) Map + +SinkRecord.value (Struct): + "key" 1, + "array" ["1", "two", "3"] + "nested_object" Map ("some_key" : "["one", "two"]") +``` + +### KafkaMetadataTransform +_(Experimental)_ + +The `KafkaMetadata` injects `topic`, `partition`, `offset`, `timestamp` which are properties are the Kafka message. + +#### Configuration + +| Property | Description (default value) | +|----------------|-----------------------------------------------------------------------------------| +| field_name | (_kafka_metadata) prefix for fields | +| nested | (false) if true, nests data on a struct else adds to top level as prefixed fields | +| external_field | (none) appends a constant `key,value` to the metadata (e.g. cluster name) | + +If `nested` is on: + +`_kafka_metadata.topic`, `_kafka_metadata.partition`, `_kafka_metadata.offset`, `_kafka_metadata.timestamp` + +If `nested` is off: +`_kafka_metadata_topic`, `_kafka_metadata_partition`, `_kafka_metadata_offset`, `_kafka_metadata_timestamp` + +### MongoDebeziumTransform +_(Experimental)_ + +The `MongoDebeziumTransform` SMT transforms a Mongo Debezium formatted message with `before`/`after` BSON +strings into `before`/`after` typed Structs that the `DebeziumTransform` SMT expects. + +It does not (yet) support renaming columns if mongodb column is not supported by your underlying +catalog type. + +#### Configuration + +| Property | Description | +|---------------------|--------------------------------------------------| +| array_handling_mode | `array` or `document` to set array handling mode | + +Value array (the default) will encode arrays as the array datatype. It is user’s responsibility to ensure that +all elements for a given array instance are of the same type. This option is a restricting one but offers +easy processing of arrays by downstream clients. + +Value document will convert the array into a struct of structs in the similar way as done by BSON serialization. +The main struct contains fields named _0, _1, _2 etc. where the name represents the index of the element in the array. +Every element is then passed as the value for the given field. diff --git a/1.11.0/docs/maintenance.md b/1.11.0/docs/maintenance.md new file mode 100644 index 000000000000..8bc09b8ee6d6 --- /dev/null +++ b/1.11.0/docs/maintenance.md @@ -0,0 +1,160 @@ +--- +title: Maintenance +--- + + +# Maintenance + +!!! info + Maintenance operations require the `Table` instance. Please refer [Java API quickstart](java-api-quickstart.md#create-a-table) page to refer how to load an existing table. + +## Recommended Maintenance + +### Expire Snapshots + +Each write to an Iceberg table creates a new _snapshot_, or version, of a table. Snapshots can be used for time-travel queries, or the table can be rolled back to any valid snapshot. + +Snapshots accumulate until they are expired by the [`expireSnapshots`](../../javadoc/{{ icebergVersion }}/org/apache/iceberg/Table.html#expireSnapshots--) operation. Regularly expiring snapshots is recommended to delete data files that are no longer needed, and to keep the size of table metadata small. + +This example expires snapshots that are older than 1 day: + +```java +Table table = ... +long tsToExpire = System.currentTimeMillis() - (1000 * 60 * 60 * 24); // 1 day +table.expireSnapshots() + .expireOlderThan(tsToExpire) + .commit(); +``` + +See the [`ExpireSnapshots` Javadoc](../../javadoc/{{ icebergVersion }}/org/apache/iceberg/ExpireSnapshots.html) to see more configuration options. + +There is also a Spark action that can run table expiration in parallel for large tables: + +```java +Table table = ... +SparkActions + .get() + .expireSnapshots(table) + .expireOlderThan(tsToExpire) + .execute(); +``` + +Expiring old snapshots removes them from metadata, so they are no longer available for time travel queries. + +!!! info + Data files are not deleted until they are no longer referenced by a snapshot that may be used for time travel or rollback. + Regularly expiring snapshots deletes unused data files. + +### Remove old metadata files + +Iceberg keeps track of table metadata using JSON files. Each change to a table produces a new metadata file to provide atomicity. + +Old metadata files are kept for history by default. Tables with frequent commits, like those written by streaming jobs, may need to regularly clean metadata files. + +Each metadata file tracks the older metadata files in the `metadata-log` field. The number of metadata files being tracked is defined by `write.metadata.previous-versions-max`. + +To automatically delete older metadata files, set `write.metadata.delete-after-commit.enabled=true` in table properties. This will keep some metadata files as tracked (up to `write.metadata.previous-versions-max`), and will delete the oldest metadata file every time a new one is created. +Note that this will only delete metadata files that are **tracked** in the metadata log and will not delete orphaned metadata files. + +Untracked metadata files are also deleted as part of [orphan file deletion](#delete-orphan-files). + +| Property | Default | Description | +|------------------------------------------------------|------------|--------------------------------------------------------------------------------------------------| +| write.metadata.delete-after-commit.enabled | false | Controls whether to delete the oldest **tracked** version metadata files after each table commit | +| write.metadata.previous-versions-max | 100 | The max number of previous version metadata files to track | + +Examples: + +* With `write.metadata.delete-after-commit.enabled=false` and `write.metadata.previous-versions-max=10`, after 100 commits, one will have 10 tracked metadata files and 90 orphaned metadata files. These 90 orphaned metadata files cannot be deleted by setting `write.metadata.delete-after-commit.enabled=true` because they are already untracked. They can only be cleaned with an orphan file deletion procedure. +* With `write.metadata.delete-after-commit.enabled=true` and `write.metadata.previous-versions-max=20`, after 21 commits, one will have 20 tracked metadata files, with the oldest metadata file being deleted by the writer after committing. With each additional commit, the oldest metadata file will be deleted. + +See [table write properties](configuration.md#write-properties) for more details. + +### Delete orphan files + +In Spark and other distributed processing engines, task or job failures can leave files that are not referenced by table metadata, and in some cases normal snapshot expiration may not be able to determine a file is no longer needed and delete it. + +To clean up these "orphan" files under a table location, use the `deleteOrphanFiles` action. + +```java +Table table = ... +SparkActions + .get() + .deleteOrphanFiles(table) + .execute(); +``` + +See the [DeleteOrphanFiles Javadoc](../../javadoc/{{ icebergVersion }}/org/apache/iceberg/actions/DeleteOrphanFiles.html) to see more configuration options. + +This action may take a long time to finish if you have lots of files in data and metadata directories. It is recommended to execute this periodically, but you may not need to execute this often. + +!!! info + It is dangerous to remove orphan files with a retention interval shorter than the time expected for any write to complete because it + might corrupt the table if in-progress files are considered orphaned and are deleted. The default interval is 3 days. + +!!! info + Iceberg uses the string representations of paths when determining which files need to be removed. On some file systems, + the path can change over time, but it still represents the same file. For example, if you change authorities for an HDFS cluster, + none of the old path urls used during creation will match those that appear in a current listing. *This will lead to data loss when + RemoveOrphanFiles is run*. Please be sure the entries in your MetadataTables match those listed by the Hadoop + FileSystem API to avoid unintentional deletion. + +## Optional Maintenance + +Some tables require additional maintenance. For example, streaming queries may produce small data files that should be [compacted into larger files](#compact-data-files). And some tables can benefit from [rewriting manifest files](#rewrite-manifests) to make locating data for queries much faster. + +### Compact data files + +Iceberg tracks each data file in a table. More data files leads to more metadata stored in manifest files, and small data files causes an unnecessary amount of metadata and less efficient queries from file open costs. + +Iceberg can compact data files in parallel using Spark with the `rewriteDataFiles` action. This will combine small files into larger files to reduce metadata overhead and runtime file open cost. + +```java +Table table = ... +SparkActions + .get() + .rewriteDataFiles(table) + .filter(Expressions.equal("date", "2020-08-18")) + .option("target-file-size-bytes", Long.toString(500 * 1024 * 1024)) // 500 MB + .execute(); +``` + +The `files` metadata table is useful for inspecting data file sizes and determining when to compact partitions. + +See the [`RewriteDataFiles` Javadoc](../../javadoc/{{ icebergVersion }}/org/apache/iceberg/actions/RewriteDataFiles.html) to see more configuration options. + +### Rewrite manifests + +Iceberg uses metadata in its manifest list and manifest files to speed up query planning and to prune unnecessary data files. The metadata tree functions as an index over a table's data. + +Manifests in the metadata tree are automatically compacted in the order they are added, which makes queries faster when the write pattern aligns with read filters. For example, writing hourly-partitioned data as it arrives is aligned with time range query filters. + +When a table's write pattern doesn't align with the query pattern, metadata can be rewritten to re-group data files into manifests using `rewriteManifests` or the `rewriteManifests` action (for parallel rewrites using Spark). + +This example rewrites small manifests and groups data files by the first partition field. + +```java +Table table = ... +SparkActions + .get() + .rewriteManifests(table) + .rewriteIf(file -> file.length() < 10 * 1024 * 1024) // 10 MB + .execute(); +``` + +See the [`RewriteManifests` Javadoc](../../javadoc/{{ icebergVersion }}/org/apache/iceberg/actions/RewriteManifests.html) to see more configuration options. diff --git a/1.11.0/docs/metrics-reporting.md b/1.11.0/docs/metrics-reporting.md new file mode 100644 index 000000000000..4ca452b0d503 --- /dev/null +++ b/1.11.0/docs/metrics-reporting.md @@ -0,0 +1,164 @@ +--- +title: "Metrics Reporting" +--- + + +# Metrics Reporting + +As of 1.1.0 Iceberg supports the [`MetricsReporter`](https://github.com/apache/iceberg/blob/main/api/src/main/java/org/apache/iceberg/metrics/MetricsReporter.java) and the [`MetricsReport`](https://github.com/apache/iceberg/blob/main/api/src/main/java/org/apache/iceberg/metrics/MetricsReport.java) APIs. These two APIs allow expressing different metrics reports while supporting a pluggable way of reporting these reports. + +## Type of Reports + +### ScanReport +A [`ScanReport`](https://github.com/apache/iceberg/blob/main/core/src/main/java/org/apache/iceberg/metrics/ScanReport.java) carries metrics being collected during scan planning against a given table. Amongst some general information about the involved table, such as the snapshot id or the table name, it includes metrics like: + +* total scan planning duration +* number of data/delete files included in the result +* number of data/delete manifests scanned/skipped +* number of data/delete files scanned/skipped +* number of equality/positional delete files scanned + +### CommitReport +A [`CommitReport`](https://github.com/apache/iceberg/blob/main/core/src/main/java/org/apache/iceberg/metrics/CommitReport.java) carries metrics being collected after committing changes to a table (aka producing a snapshot). Amongst some general information about the involved table, such as the snapshot id or the table name, it includes metrics like: + +* total duration +* number of attempts required for the commit to succeed +* number of added/removed data/delete files +* number of added/removed equality/positional delete files +* number of added/removed equality/positional deletes + +## Available Metrics Reporters + +### [`LoggingMetricsReporter`](https://github.com/apache/iceberg/blob/main/api/src/main/java/org/apache/iceberg/metrics/LoggingMetricsReporter.java) + +This is the default metrics reporter when nothing else is configured and its purpose is to log results to the log file. Example output would look as shown below: + +``` +INFO org.apache.iceberg.metrics.LoggingMetricsReporter - Received metrics report: +ScanReport{ + tableName=scan-planning-with-eq-and-pos-delete-files, + snapshotId=2, + filter=ref(name="data") == "(hash-27fa7cc0)", + schemaId=0, + projectedFieldIds=[1, 2], + projectedFieldNames=[id, data], + scanMetrics=ScanMetricsResult{ + totalPlanningDuration=TimerResult{timeUnit=NANOSECONDS, totalDuration=PT0.026569404S, count=1}, + resultDataFiles=CounterResult{unit=COUNT, value=1}, + resultDeleteFiles=CounterResult{unit=COUNT, value=2}, + totalDataManifests=CounterResult{unit=COUNT, value=1}, + totalDeleteManifests=CounterResult{unit=COUNT, value=1}, + scannedDataManifests=CounterResult{unit=COUNT, value=1}, + skippedDataManifests=CounterResult{unit=COUNT, value=0}, + totalFileSizeInBytes=CounterResult{unit=BYTES, value=10}, + totalDeleteFileSizeInBytes=CounterResult{unit=BYTES, value=20}, + skippedDataFiles=CounterResult{unit=COUNT, value=0}, + skippedDeleteFiles=CounterResult{unit=COUNT, value=0}, + scannedDeleteManifests=CounterResult{unit=COUNT, value=1}, + skippedDeleteManifests=CounterResult{unit=COUNT, value=0}, + indexedDeleteFiles=CounterResult{unit=COUNT, value=2}, + equalityDeleteFiles=CounterResult{unit=COUNT, value=1}, + positionalDeleteFiles=CounterResult{unit=COUNT, value=1}}, + metadata={ + iceberg-version=Apache Iceberg 1.4.0-SNAPSHOT (commit 4868d2823004c8c256a50ea7c25cff94314cc135)}} +``` + +``` +INFO org.apache.iceberg.metrics.LoggingMetricsReporter - Received metrics report: +CommitReport{ + tableName=scan-planning-with-eq-and-pos-delete-files, + snapshotId=1, + sequenceNumber=1, + operation=append, + commitMetrics=CommitMetricsResult{ + totalDuration=TimerResult{timeUnit=NANOSECONDS, totalDuration=PT0.098429626S, count=1}, + attempts=CounterResult{unit=COUNT, value=1}, + addedDataFiles=CounterResult{unit=COUNT, value=1}, + removedDataFiles=null, + totalDataFiles=CounterResult{unit=COUNT, value=1}, + addedDeleteFiles=null, + addedEqualityDeleteFiles=null, + addedPositionalDeleteFiles=null, + removedDeleteFiles=null, + removedEqualityDeleteFiles=null, + removedPositionalDeleteFiles=null, + totalDeleteFiles=CounterResult{unit=COUNT, value=0}, + addedRecords=CounterResult{unit=COUNT, value=1}, + removedRecords=null, + totalRecords=CounterResult{unit=COUNT, value=1}, + addedFilesSizeInBytes=CounterResult{unit=BYTES, value=10}, + removedFilesSizeInBytes=null, + totalFilesSizeInBytes=CounterResult{unit=BYTES, value=10}, + addedPositionalDeletes=null, + removedPositionalDeletes=null, + totalPositionalDeletes=CounterResult{unit=COUNT, value=0}, + addedEqualityDeletes=null, + removedEqualityDeletes=null, + totalEqualityDeletes=CounterResult{unit=COUNT, value=0}}, + metadata={ + iceberg-version=Apache Iceberg 1.4.0-SNAPSHOT (commit 4868d2823004c8c256a50ea7c25cff94314cc135)}} +``` + +### [`RESTMetricsReporter`](https://github.com/apache/iceberg/blob/main/core/src/main/java/org/apache/iceberg/rest/RESTMetricsReporter.java) + +This is the default when using the [`RESTCatalog`](https://github.com/apache/iceberg/blob/main/core/src/main/java/org/apache/iceberg/rest/RESTCatalog.java) and its purpose is to send metrics to a REST server at the `/v1/{prefix}/namespaces/{namespace}/tables/{table}/metrics` endpoint as defined in the [REST OpenAPI spec](https://github.com/apache/iceberg/blob/main/open-api/rest-catalog-open-api.yaml). + +Sending metrics via REST can be controlled with the `rest-metrics-reporting-enabled` (defaults to `true`) property. + +## Implementing a custom Metrics Reporter + +Implementing the [`MetricsReporter`](https://github.com/apache/iceberg/blob/main/api/src/main/java/org/apache/iceberg/metrics/MetricsReporter.java) API gives full flexibility in dealing with incoming [`MetricsReport`](https://github.com/apache/iceberg/blob/main/api/src/main/java/org/apache/iceberg/metrics/MetricsReport.java) instances. For example, it would be possible to send results to a Prometheus endpoint or any other observability framework/system. + +Below is a short example illustrating an `InMemoryMetricsReporter` that stores reports in a list and makes them available: +```java +public class InMemoryMetricsReporter implements MetricsReporter { + + private List metricsReports = Lists.newArrayList(); + + @Override + public void report(MetricsReport report) { + metricsReports.add(report); + } + + public List reports() { + return metricsReports; + } +} +``` + +## Registering a custom Metrics Reporter + +### Via Catalog Configuration + +The [catalog property](catalog-properties.md) `metrics-reporter-impl` allows registering a given [`MetricsReporter`](https://github.com/apache/iceberg/blob/main/api/src/main/java/org/apache/iceberg/metrics/MetricsReporter.java) by specifying its fully-qualified class name, e.g. `metrics-reporter-impl=org.apache.iceberg.metrics.InMemoryMetricsReporter`. + +### Via the Java API during Scan planning + +Independently of the [`MetricsReporter`](https://github.com/apache/iceberg/blob/main/api/src/main/java/org/apache/iceberg/metrics/MetricsReporter.java) being registered at the catalog level via the `metrics-reporter-impl` property, it is also possible to supply additional reporters during scan planning as shown below: + +```java +TableScan tableScan = + table + .newScan() + .metricsReporter(customReporterOne) + .metricsReporter(customReporterTwo); + +try (CloseableIterable fileScanTasks = tableScan.planFiles()) { + // ... +} +``` diff --git a/1.11.0/docs/nessie.md b/1.11.0/docs/nessie.md new file mode 100644 index 000000000000..ee212c6aa859 --- /dev/null +++ b/1.11.0/docs/nessie.md @@ -0,0 +1,159 @@ +--- +title: "Nessie" +--- + + +# Iceberg Nessie Integration + +Iceberg provides integration with Nessie through the `iceberg-nessie` module. +This section describes how to use Iceberg with Nessie. Nessie provides several key features on top of Iceberg: + +* multi-table transactions +* git-like operations (eg branches, tags, commits) +* hive-like metastore capabilities + +See [Project Nessie](https://projectnessie.org) for more information on Nessie. Nessie requires a server to run, see +[Getting Started](https://projectnessie.org/try/) to start a Nessie server. + +## Enabling Nessie Catalog + +The `iceberg-nessie` module is bundled with Spark and Flink runtimes for all versions from `0.11.0`. To get started +with Nessie (with spark-3.5) and Iceberg simply add the Iceberg runtime to your process. Eg: `spark-sql --packages +org.apache.iceberg:iceberg-spark-runtime-3.5_2.12:{{ icebergVersion }}`. + +## Spark SQL Extensions + +Nessie SQL extensions can be used to manage the Nessie repo as shown below. +Example for Spark 3.5 with scala 2.12: + +``` +bin/spark-sql + --packages "org.apache.iceberg:iceberg-spark-runtime-3.5_2.12:{{ icebergVersion }},org.projectnessie.nessie-integrations:nessie-spark-extensions-3.5_2.12:{{ nessieVersion }}" + --conf spark.sql.extensions="org.apache.iceberg.spark.extensions.IcebergSparkSessionExtensions,org.projectnessie.spark.extensions.NessieSparkSessionExtensions" + --conf +``` +Please refer [Nessie SQL extension document](https://projectnessie.org/tools/sql/) to learn more about it. + +## Nessie Catalog + +One major feature introduced in release `0.11.0` is the ability to easily interact with a [Custom Catalog](custom-catalog.md) from Spark and Flink. See [Spark Configuration](spark-configuration.md#catalog-configuration) + and [Flink Configuration](flink.md#custom-catalog) for instructions for adding a custom catalog to Iceberg. + +To use the Nessie Catalog the following properties are required: + +* `warehouse`. Like most other catalogs the warehouse property is a file path to where this catalog should store tables. +* `uri`. This is the Nessie server base uri. Eg `http://localhost:19120/api/v2`. +* `ref` (optional). This is the Nessie branch or tag you want to work in. + +To run directly in Java this looks like: + +``` java +Map options = new HashMap<>(); +options.put("warehouse", "/path/to/warehouse"); +options.put("ref", "main"); +options.put("uri", "https://localhost:19120/api/v2"); +Catalog nessieCatalog = CatalogUtil.loadCatalog("org.apache.iceberg.nessie.NessieCatalog", "nessie", options, hadoopConfig); +``` + +and in Spark: + +``` java +conf.set("spark.sql.catalog.nessie.warehouse", "/path/to/warehouse"); +conf.set("spark.sql.catalog.nessie.uri", "http://localhost:19120/api/v2") +conf.set("spark.sql.catalog.nessie.ref", "main") +conf.set("spark.sql.catalog.nessie.type", "nessie") +conf.set("spark.sql.catalog.nessie", "org.apache.iceberg.spark.SparkCatalog") +conf.set("spark.sql.extensions", "org.apache.iceberg.spark.extensions.IcebergSparkSessionExtensions,org.projectnessie.spark.extensions.NessieSparkSessionExtensions") +``` +This is how it looks in Flink via the Python API (additional details can be found [here](flink.md#preparation-when-using-flinks-python-api)): +```python +import os +from pyflink.datastream import StreamExecutionEnvironment +from pyflink.table import StreamTableEnvironment + +env = StreamExecutionEnvironment.get_execution_environment() +iceberg_flink_runtime_jar = os.path.join(os.getcwd(), "iceberg-flink-runtime-{{ icebergVersion }}.jar") +env.add_jars("file://{}".format(iceberg_flink_runtime_jar)) +table_env = StreamTableEnvironment.create(env) + +table_env.execute_sql("CREATE CATALOG nessie_catalog WITH (" + "'type'='iceberg', " + "'type'='nessie', " + "'uri'='http://localhost:19120/api/v2', " + "'ref'='main', " + "'warehouse'='/path/to/warehouse')") +``` + +There is nothing special above about the `nessie` name. A spark catalog can have any name, the important parts are the +settings for the `type` or `catalog-impl` and the required config to start Nessie correctly. +Once you have a Nessie catalog you have access to your entire Nessie repo. You can then perform create/delete/merge +operations on branches and perform commits on branches. Each Iceberg table in a Nessie Catalog is identified by an +arbitrary length namespace and table name (eg `data.base.name.table`). These namespaces must be explicitly created +as mentioned [here](https://projectnessie.org/blog/namespace-enforcement/). +Any transaction on a Nessie enabled Iceberg table is a single commit in Nessie. Nessie commits +can encompass an arbitrary number of actions on an arbitrary number of tables, however in Iceberg this will be limited +to the set of single table transactions currently available. + +Further operations such as merges, viewing the commit log or diffs are performed by direct interaction with the +`NessieClient` in java or by using the python client or cli. See [Nessie CLI](https://projectnessie.org/tools/cli/) for +more details on the CLI and [Spark Guide](https://projectnessie.org/tools/iceberg/spark/) for a more complete description of +Nessie functionality. + +## Nessie and Iceberg + +For most cases Nessie acts just like any other Catalog for Iceberg: providing a logical organization of a set of tables +and providing atomicity to transactions. However, using Nessie opens up other interesting possibilities. When using Nessie with +Iceberg every Iceberg transaction becomes a Nessie commit. This history can be listed, merged or cherry-picked across branches. + +### Loosely coupled transactions + +By creating a branch and performing a set of operations on that branch you can approximate a multi-table transaction. +A sequence of commits can be performed on the newly created branch and then merged back into the main branch atomically. +This gives the appearance of a series of connected changes being exposed to the main branch simultaneously. While downstream +consumers will see multiple transactions appear at once this isn't a true multi-table transaction on the database. It is +effectively a fast-forward merge of multiple commits (in git language) and each operation from the branch is its own distinct +transaction and commit. This is different from a real multi-table transaction where all changes would be in the same commit. +This does allow multiple applications to take part in modifying a branch and for this distributed set of transactions to be +exposed to the downstream users simultaneously. + +### Experimentation + +Changes to a table can be tested in a branch before merging back into main. This is particularly useful when performing +large changes like schema evolution or partition evolution. A partition evolution could be performed in a branch and you +would be able to test out the change (eg performance benchmarks) before merging it. This provides great flexibility in +performing on-line table modifications and testing without interrupting downstream use cases. If the changes are +incorrect or not performant the branch can be dropped without being merged. + +### Further use cases + +Please see the [Nessie Documentation](https://projectnessie.org/features/) for further descriptions of +Nessie features. + +!!! danger + Regular table maintenance in Iceberg is complicated when using nessie. Please consult + [Management Services](https://projectnessie.org/features/management/) before performing any + [table maintenance](maintenance.md). + +## Example + +Please have a look at the [Nessie Demos repo](https://github.com/projectnessie/nessie-demos) +for different examples of Nessie and Iceberg in action together. + +## Future Improvements + +* Iceberg multi-table transactions. Changes to multiple Iceberg tables in the same transaction, isolation levels etc diff --git a/1.11.0/docs/partitioning.md b/1.11.0/docs/partitioning.md new file mode 100644 index 000000000000..c83587c06738 --- /dev/null +++ b/1.11.0/docs/partitioning.md @@ -0,0 +1,94 @@ +--- +title: Partitioning +--- + + +# Partitioning + +## What is partitioning? + +Partitioning is a way to make queries faster by grouping similar rows together when writing. + +For example, queries for log entries from a `logs` table would usually include a time range, like this query for logs between 10 and 12 AM: + +```sql +SELECT level, message FROM logs +WHERE event_time BETWEEN '2018-12-01 10:00:00' AND '2018-12-01 12:00:00'; +``` + +Configuring the `logs` table to partition by the date of `event_time` will group log events into files with the same event date. Iceberg keeps track of that date and will use it to skip files for other dates that don't have useful data. + +Iceberg can partition timestamps by year, month, day, and hour granularity. It can also use a categorical column, like `level` in this logs example, to store rows together and speed up queries. + +## What does Iceberg do differently? + +Other tables formats like Hive support partitioning, but Iceberg supports *hidden partitioning*. + +* Iceberg handles the tedious and error-prone task of producing partition values for rows in a table. +* Iceberg avoids reading unnecessary partitions automatically. Consumers don't need to know how the table is partitioned and add extra filters to their queries. +* Iceberg partition layouts can evolve as needed. + +### Partitioning in Hive + +To demonstrate the difference, consider how Hive would handle a `logs` table. + +In Hive, partitions are explicit and appear as a column, so the `logs` table would have a column called `event_date`. When writing, an insert needs to supply the data for the `event_date` column: + +```sql +INSERT INTO logs PARTITION (event_date) + SELECT level, message, event_time, format_time(event_time, 'YYYY-MM-dd') + FROM unstructured_log_source; +``` + +Similarly, queries that search through the `logs` table must have an `event_date` filter in addition to an `event_time` filter. + +```sql +SELECT level, count(1) as count FROM logs +WHERE event_time BETWEEN '2018-12-01 10:00:00' AND '2018-12-01 12:00:00' + AND event_date = '2018-12-01'; +``` + +If the `event_date` filter were missing, Hive would scan through every file in the table because it doesn't know that the `event_time` column is related to the `event_date` column. + +### Problems with Hive partitioning + +Hive must be given partition values. In the logs example, it doesn't know the relationship between `event_time` and `event_date`. + +This leads to several problems: + +* Hive can't validate partition values -- it is up to the writer to produce the correct value + - Using the wrong format, `2018-12-01` instead of `20181201`, produces silently incorrect results, not query failures + - Using the wrong source column, like `processing_time`, or time zone also causes incorrect results, not failures +* It is up to the user to write queries correctly + - Using the wrong format also leads to silently incorrect results + - Users that don't understand a table's physical layout get needlessly slow queries -- Hive can't translate filters automatically +* Working queries are tied to the table's partitioning scheme, so partitioning configuration cannot be changed without breaking queries + +### Iceberg's hidden partitioning + +Iceberg produces partition values by taking a column value and optionally transforming it. Iceberg is responsible for converting `event_time` into `event_date`, and keeps track of the relationship. + +Table partitioning is configured using these relationships. The `logs` table would be partitioned by `day(event_time)` and `level`. + +Because Iceberg doesn't require user-maintained partition columns, it can hide partitioning. Partition values are produced correctly every time and always used to speed up queries, when possible. Producers and consumers wouldn't even see `event_date`. + +Most importantly, queries no longer depend on a table's physical layout. With a separation between physical and logical, Iceberg tables can evolve partition schemes over time as data volume changes. Misconfigured tables can be fixed without an expensive migration. + +For details about all the supported hidden partition transformations, see the [Partition Transforms](../../spec.md#partition-transforms) section. + +For details about updating a table's partition spec, see the [partition evolution](evolution.md#partition-evolution) section. diff --git a/1.11.0/docs/performance.md b/1.11.0/docs/performance.md new file mode 100644 index 000000000000..cbe870347a1d --- /dev/null +++ b/1.11.0/docs/performance.md @@ -0,0 +1,55 @@ +--- +title: Performance +--- + + +# Performance + +* Iceberg is designed for huge tables and is used in production where a *single table* can contain tens of petabytes of data. +* Even multi-petabyte tables can be read from a single node, without needing a distributed SQL engine to sift through table metadata. + +## Scan planning + +Scan planning is the process of finding the files in a table that are needed for a query. + +Planning in an Iceberg table fits on a single node because Iceberg's metadata can be used to prune *metadata* files that aren't needed, in addition to filtering *data* files that don't contain matching data. + +Fast scan planning from a single node enables: + +* Lower latency SQL queries -- by eliminating a distributed scan to plan a distributed scan +* Access from any client -- stand-alone processes can read data directly from Iceberg tables + +### Metadata filtering + +Iceberg uses two levels of metadata to track the files in a snapshot. + +* **Manifest files** store a list of data files, along each data file's partition data and column-level stats +* A **manifest list** stores the snapshot's list of manifests, along with the range of values for each partition field + +For fast scan planning, Iceberg first filters manifests using the partition value ranges in the manifest list. Then, it reads each manifest to get data files. With this scheme, the manifest list acts as an index over the manifest files, making it possible to plan without reading all manifests. + +In addition to partition value ranges, a manifest list also stores the number of files added or deleted in a manifest to speed up operations like snapshot expiration. + +### Data filtering + +Manifest files include a tuple of partition data and column-level stats for each data file. + +During planning, query predicates are automatically converted to predicates on the partition data and applied first to filter data files. Next, column-level value counts, null counts, lower bounds, and upper bounds are used to eliminate files that cannot match the query predicate. + +By using upper and lower bounds to filter data files at planning time, Iceberg uses clustered data to eliminate splits without running tasks. In some cases, this is a [10x performance improvement](https://conferences.oreilly.com/strata/strata-ny-2018/cdn.oreillystatic.com/en/assets/1/event/278/Introducing%20Iceberg_%20Tables%20designed%20for%20object%20stores%20Presentation.pdf +). diff --git a/1.11.0/docs/reliability.md b/1.11.0/docs/reliability.md new file mode 100644 index 000000000000..606d2cab6ad1 --- /dev/null +++ b/1.11.0/docs/reliability.md @@ -0,0 +1,66 @@ +--- +title: Reliability +--- + + +# Reliability + +Iceberg was designed to solve correctness problems that affect Hive tables running in S3. + +Hive tables track data files using both a central metastore for partitions and a file system for individual files. This makes atomic changes to a table's contents impossible, and eventually consistent stores like S3 may return incorrect results due to the use of listing files to reconstruct the state of a table. It also requires job planning to make many slow listing calls: O(n) with the number of partitions. + +Iceberg tracks the complete list of data files in each [snapshot](../../terms.md#snapshot) using a persistent tree structure. Every write or delete produces a new snapshot that reuses as much of the previous snapshot's metadata tree as possible to avoid high write volumes. + +Valid snapshots in an Iceberg table are stored in the table metadata file, along with a reference to the current snapshot. Commits replace the path of the current table metadata file using an atomic operation. This ensures that all updates to table data and metadata are atomic, and is the basis for [serializable isolation](https://en.wikipedia.org/wiki/Isolation_(database_systems)#Serializable). + +This results in improved reliability guarantees: + +* **Serializable isolation**: All table changes occur in a linear history of atomic table updates +* **Reliable reads**: Readers always use a consistent snapshot of the table without holding a lock +* **Version history and rollback**: Table snapshots are kept as history and tables can roll back if a job produces bad data +* **Safe file-level operations**. By supporting atomic changes, Iceberg enables new use cases, like safely compacting small files and safely appending late data to tables + +This design also has performance benefits: + +* **O(1) RPCs to plan**: Instead of listing O(n) directories in a table to plan a job, reading a snapshot requires O(1) RPC calls +* **Distributed planning**: File pruning and predicate push-down is distributed to jobs, removing the metastore as a bottleneck +* **Finer granularity partitioning**: Distributed planning and O(1) RPC calls remove the current barriers to finer-grained partitioning + +## Concurrent write operations + +Iceberg supports multiple concurrent writes using optimistic concurrency. + +Each writer assumes that no other writers are operating and writes out new table metadata for an operation. Then, the writer attempts to commit by atomically swapping the new table metadata file for the existing metadata file. + +If the atomic swap fails because another writer has committed, the failed writer retries by writing a new metadata tree based on the new current table state. + +### Cost of retries + +Writers avoid expensive retry operations by structuring changes so that work can be reused across retries. + +For example, appends usually create a new manifest file for the appended data files, which can be added to the table without rewriting the manifest on every attempt. + +### Retry validation + +Commits are structured as assumptions and actions. After a conflict, a writer checks that the assumptions are met by the current table state. If the assumptions are met, then it is safe to re-apply the actions and commit. + +For example, a compaction might rewrite `file_a.avro` and `file_b.avro` as `merged.parquet`. This is safe to commit as long as the table still contains both `file_a.avro` and `file_b.avro`. If either file was deleted by a conflicting commit, then the operation must fail. Otherwise, it is safe to remove the source files and add the merged file. + +## Compatibility + +By avoiding file listing and rename operations, Iceberg tables are compatible with any object store. No consistent listing is required. diff --git a/1.11.0/docs/schemas.md b/1.11.0/docs/schemas.md new file mode 100644 index 000000000000..7448e858da67 --- /dev/null +++ b/1.11.0/docs/schemas.md @@ -0,0 +1,51 @@ +--- +title: Schemas +--- + + +# Schemas + +Iceberg tables support the following types: + +| Type | Description | Notes | +| --------------------- | ------------------------------------------------------------------------ | ----------------------------------------------- | +| **`boolean`** | True or false | | +| **`int`** | 32-bit signed integers | Can promote to `long` | +| **`long`** | 64-bit signed integers | | +| **`float`** | [32-bit IEEE 754](https://en.wikipedia.org/wiki/IEEE_754) floating point | Can promote to `double` | +| **`double`** | [64-bit IEEE 754](https://en.wikipedia.org/wiki/IEEE_754) floating point | | +| **`decimal(P,S)`** | Fixed-point decimal; precision P, scale S | Scale is fixed and precision must be 38 or less | +| **`date`** | Calendar date without timezone or time | | +| **`time`** | Time of day without date, timezone | Stored as microseconds | +| **`timestamp`** | Timestamp without timezone | Stored as microseconds | +| **`timestamptz`** | Timestamp with timezone | Stored as microseconds | +| **`timestamp_ns`** | Timestamp without timezone, nanosecond precision | Stored as nanoseconds; added in v3 | +| **`timestamptz_ns`** | Timestamp with timezone, nanosecond precision | Stored as nanoseconds; added in v3 | +| **`string`** | Arbitrary-length character sequences | Encoded with UTF-8 | +| **`uuid`** | Universally unique identifiers | | +| **`fixed(L)`** | Fixed-length byte array of length L | | +| **`binary`** | Arbitrary-length byte array | | +| **`variant`** | Semi-structured data (JSON-like) | Added in v3 | +| **`geometry(C)`** | Geospatial features with CRS parameter | Linear edge-interpolation; added in v3 | +| **`geography(C, A)`** | Geospatial features with CRS and edge algorithm | Non-linear edge-interpolation; added in v3 | +| **`unknown`** | Placeholder type for undetermined columns | Must be optional; added in v3 | +| **`struct<...>`** | A record with named fields of any data type | | +| **`list`** | A list with elements of any data type | | +| **`map`** | A map with keys and values of any data type | | + +Iceberg tracks each field in a table schema using an ID that is never reused in a table. See [correctness guarantees](evolution.md#correctness) for more information. diff --git a/1.11.0/docs/spark-configuration.md b/1.11.0/docs/spark-configuration.md new file mode 100644 index 000000000000..5972aafc3d39 --- /dev/null +++ b/1.11.0/docs/spark-configuration.md @@ -0,0 +1,283 @@ +--- +title: "Configuration" +--- + + +# Spark Configuration + +## Catalogs + +Spark adds an API to plug in table catalogs that are used to load, create, and manage Iceberg tables. Spark catalogs are configured by setting Spark properties under `spark.sql.catalog`. + +This creates an Iceberg catalog named `hive_prod` that loads tables from a Hive metastore: + +```plain +spark.sql.catalog.hive_prod = org.apache.iceberg.spark.SparkCatalog +spark.sql.catalog.hive_prod.type = hive +spark.sql.catalog.hive_prod.uri = thrift://metastore-host:port +# omit uri to use the same URI as Spark: hive.metastore.uris in hive-site.xml +``` + +Below is an example for a REST catalog named `rest_prod` that loads tables from REST URL `http://localhost:8080`: + +```plain +spark.sql.catalog.rest_prod = org.apache.iceberg.spark.SparkCatalog +spark.sql.catalog.rest_prod.type = rest +spark.sql.catalog.rest_prod.uri = http://localhost:8080 +``` + +Iceberg also supports a directory-based catalog in HDFS that can be configured using `type=hadoop`: + +```plain +spark.sql.catalog.hadoop_prod = org.apache.iceberg.spark.SparkCatalog +spark.sql.catalog.hadoop_prod.type = hadoop +spark.sql.catalog.hadoop_prod.warehouse = hdfs://nn:8020/warehouse/path +``` + +!!! info + The Hive-based catalog only loads Iceberg tables. To load non-Iceberg tables in the same Hive metastore, use a [session catalog](#replacing-the-session-catalog). + +### Catalog configuration + +A catalog is created and named by adding a property `spark.sql.catalog.(catalog-name)` with an implementation class for its value. + +Iceberg supplies two implementations: + +* `org.apache.iceberg.spark.SparkCatalog` supports a Hive Metastore or a Hadoop warehouse as a catalog +* `org.apache.iceberg.spark.SparkSessionCatalog` adds support for Iceberg tables to Spark's built-in catalog, and delegates to the built-in catalog for non-Iceberg tables + +Both catalogs are configured using properties nested under the catalog name. Common configuration properties for Hive and Hadoop are: + +| Property | Values | Description | +| -------------------------------------------------- |----------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| spark.sql.catalog._catalog-name_.type | `hive`, `hadoop`, `rest`, `glue`, `jdbc` or `nessie` | The underlying Iceberg catalog implementation, `HiveCatalog`, `HadoopCatalog`, `RESTCatalog`, `GlueCatalog`, `JdbcCatalog`, `NessieCatalog` or left unset if using a custom catalog | +| spark.sql.catalog._catalog-name_.catalog-impl | | The custom Iceberg catalog implementation. If `type` is null, `catalog-impl` must not be null. | +| spark.sql.catalog._catalog-name_.io-impl | | The custom FileIO implementation. | +| spark.sql.catalog._catalog-name_.metrics-reporter-impl | | The custom MetricsReporter implementation. | +| spark.sql.catalog._catalog-name_.default-namespace | default | The default current namespace for the catalog | +| spark.sql.catalog._catalog-name_.uri | thrift://host:port | Hive metastore URL for hive typed catalog, REST URL for REST typed catalog | +| spark.sql.catalog._catalog-name_.warehouse | hdfs://nn:8020/warehouse/path | Base path for the warehouse directory | +| spark.sql.catalog._catalog-name_.cache-enabled | `true` or `false` | Whether to enable catalog cache, default value is `true` | +| spark.sql.catalog._catalog-name_.cache.expiration-interval-ms | `30000` (30 seconds) | Duration after which cached catalog entries are expired; Only effective if `cache-enabled` is `true`. `-1` disables cache expiration and `0` disables caching entirely, irrespective of `cache-enabled`. Default is `30000` (30 seconds) | +| spark.sql.catalog._catalog-name_.table-default._propertyKey_ | | Default Iceberg table property value for property key _propertyKey_, which will be set on tables created by this catalog if not overridden | +| spark.sql.catalog._catalog-name_.table-override._propertyKey_ | | Enforced Iceberg table property value for property key _propertyKey_, which cannot be overridden on table creation by user | +| spark.sql.catalog._catalog-name_.view-default._propertyKey_ | | Default Iceberg view property value for property key _propertyKey_, which will be set on views created by this catalog if not overridden | +| spark.sql.catalog._catalog-name_.view-override._propertyKey_ | | Enforced Iceberg view property value for property key _propertyKey_, which cannot be overridden on view creation by user | +| spark.sql.catalog._catalog-name_.use-nullable-query-schema | `true` or `false` | Whether to preserve fields' nullability when creating the table using CTAS and RTAS. If set to `true`, all fields will be marked as nullable. If set to `false`, fields' nullability will be preserved. The default value is `true`. Available in Spark 3.5 and above. | + +Additional properties can be found in common [catalog configuration](catalog-properties.md). + +### Using catalogs + +Catalog names are used in SQL queries to identify a table. In the examples above, `hive_prod` and `hadoop_prod` can be used to prefix database and table names that will be loaded from those catalogs. + +```sql +SELECT * FROM hive_prod.db.table; -- load db.table from catalog hive_prod +``` + +Spark 3 keeps track of the current catalog and namespace, which can be omitted from table names. + +```sql +USE hive_prod.db; +SELECT * FROM table; -- load db.table from catalog hive_prod +``` + +To see the current catalog and namespace, run `SHOW CURRENT NAMESPACE`. + +### Replacing the session catalog + +To add Iceberg table support to Spark's built-in catalog, configure `spark_catalog` to use Iceberg's `SparkSessionCatalog`. + +```plain +spark.sql.catalog.spark_catalog = org.apache.iceberg.spark.SparkSessionCatalog +spark.sql.catalog.spark_catalog.type = hive +``` + +Spark's built-in catalog supports existing v1 and v2 tables tracked in a Hive Metastore. This configures Spark to use Iceberg's `SparkSessionCatalog` as a wrapper around that session catalog. When a table is not an Iceberg table, the built-in catalog will be used to load it instead. + +This configuration can use same Hive Metastore for both Iceberg and non-Iceberg tables. + +`SparkSessionCatalog` is useful when you want `spark_catalog` to work with both Iceberg and non-Iceberg +tables in the same metastore. + +!!! note + Spark before 4.2.0 does not support `V2Function` in the session catalog. See + [SPARK-54760](https://issues.apache.org/jira/browse/SPARK-54760) ([apache/spark#53531](https://github.com/apache/spark/pull/53531)) for details. As a result, + catalog-scoped SQL functions such as `system.bucket`, `system.days`, and `system.iceberg_version` + are not available through `spark_catalog`. To work around this limitation, configure a separate + Iceberg catalog with `org.apache.iceberg.spark.SparkCatalog` and call them through that catalog. + +### Using catalog specific Hadoop configuration values + +Similar to configuring Hadoop properties by using `spark.hadoop.*`, it's possible to set per-catalog Hadoop configuration values when using Spark by adding the property for the catalog with the prefix `spark.sql.catalog.(catalog-name).hadoop.*`. These properties will take precedence over values configured globally using `spark.hadoop.*` and will only affect Iceberg tables. + +```plain +spark.sql.catalog.hadoop_prod.hadoop.fs.s3a.endpoint = http://aws-local:9000 +``` + +### Loading a custom catalog + +Spark supports loading a custom Iceberg `Catalog` implementation by specifying the `catalog-impl` property. Here is an example: + +```plain +spark.sql.catalog.custom_prod = org.apache.iceberg.spark.SparkCatalog +spark.sql.catalog.custom_prod.catalog-impl = com.my.custom.CatalogImpl +spark.sql.catalog.custom_prod.my-additional-catalog-config = my-value +``` + +## SQL Extensions + +Iceberg 0.11.0 and later add an extension module to Spark to add new SQL commands, like `CALL` for stored procedures or `ALTER TABLE ... WRITE ORDERED BY`. + +Using those SQL commands requires adding Iceberg extensions to your Spark environment using the following Spark property: + +| Spark extensions property | Iceberg extensions implementation | +|---------------------------|---------------------------------------------------------------------| +| `spark.sql.extensions` | `org.apache.iceberg.spark.extensions.IcebergSparkSessionExtensions` | + +## Runtime configuration + +### Precedence of Configuration Settings +Iceberg allows configurations to be specified at different levels. The effective configuration for a read or write operation is determined based on the following order of precedence: + +1. DataSource API Read/Write Options – Explicitly passed to `.option(...)` in a read/write operation. + +2. Spark Session Configuration - Set globally in Spark via `spark.conf.set(...)`, `spark-defaults.conf`, or `--conf` in spark-submit. + +3. Table Properties – Defined on the Iceberg table via `ALTER TABLE SET TBLPROPERTIES`. + +4. Default Value. + +If a setting is not defined at a higher level, the next level is used as fallback. This allows flexibility while enabling global defaults when needed. + +### Spark SQL Options + +Iceberg supports setting various global behaviors using Spark SQL configuration options. These can be set via `spark.conf`, `SparkSession` settings, or Spark submit arguments. +For example: + +```scala +// disabling vectorization +val spark = SparkSession.builder() + .appName("IcebergExample") + .master("local[*]") + .config("spark.sql.catalog.my_catalog", "org.apache.iceberg.spark.SparkCatalog") + .config("spark.sql.extensions", "org.apache.iceberg.spark.extensions.IcebergSparkSessionExtensions") + .config("spark.sql.iceberg.vectorization.enabled", "false") + .getOrCreate() +``` + +| Spark option | Default | Description | +|--------------------------------------------------------|----------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------| +| spark.sql.iceberg.vectorization.enabled | Table default | Enables vectorized reads of data files | +| spark.sql.iceberg.check-nullability | true | Validate that the write schema's nullability matches the table's nullability | +| spark.sql.iceberg.check-ordering | true | Validates the write schema column order matches the table schema order | +| spark.sql.iceberg.planning.preserve-data-grouping | false | When true, co-locate scan tasks for the same partition in the same read split, used in Storage Partitioned Joins | +| spark.sql.iceberg.aggregate-push-down.enabled | true | Enables pushdown of aggregate functions (MAX, MIN, COUNT) | +| spark.sql.iceberg.distribution-mode | See [Spark Writes](spark-writes.md#writing-distribution-modes) | Controls distribution strategy during writes | +| spark.wap.id | null | [Write-Audit-Publish](branching.md#audit-branch) snapshot staging ID | +| spark.wap.branch | null | WAP branch name for snapshot commit | +| spark.sql.iceberg.shred-variants | Table default | When true, variant columns are written with shredded Parquet encoding for improved query performance | +| spark.sql.iceberg.variant-inference-buffer-size | Table default | Number of rows to buffer for schema inference when variant shredding is enabled | +| spark.sql.iceberg.compression-codec | Table default | Write compression codec (e.g., `zstd`, `snappy`) | +| spark.sql.iceberg.compression-level | Table default | Compression level for Parquet/Avro | +| spark.sql.iceberg.compression-strategy | Table default | Compression strategy for ORC | +| spark.sql.iceberg.data-planning-mode | AUTO | Scan planning mode for data files (`AUTO`, `LOCAL`, `DISTRIBUTED`) | +| spark.sql.iceberg.delete-planning-mode | AUTO | Scan planning mode for delete files (`AUTO`, `LOCAL`, `DISTRIBUTED`) | +| spark.sql.iceberg.advisory-partition-size | Table default | Advisory size (bytes) used for writing to the Table when Spark's Adaptive Query Execution is enabled. Used to size output files | +| spark.sql.iceberg.locality.enabled | false | Report locality information for Spark task placement on executors | +| spark.sql.iceberg.executor-cache.enabled | true | Enables cache for executor-side (currently used to cache Delete Files) | +| spark.sql.iceberg.executor-cache.timeout | 10 | Timeout in minutes for executor cache entries | +| spark.sql.iceberg.executor-cache.max-entry-size | 67108864 (64MB) | Max size per cache entry (bytes) | +| spark.sql.iceberg.executor-cache.max-total-size | 134217728 (128MB) | Max total executor cache size (bytes) | +| spark.sql.iceberg.executor-cache.locality.enabled | false | Enables locality-aware executor cache usage | +| spark.sql.iceberg.merge-schema | false | Enables modifying the table schema to match the write schema. Only adds columns missing columns | +| spark.sql.iceberg.report-column-stats | true | Report Puffin Table Statistics if available to Spark's Cost Based Optimizer. CBO must be enabled for this to be effective | +| spark.sql.iceberg.async-micro-batch-planning-enabled | false | Enables asynchronous microbatch planning to reduce planning latency by pre-fetching file scan tasks | + +### Read options + +Spark read options are passed when configuring the DataFrameReader, like this: + +```scala +// time travel +spark.read + .option("snapshot-id", 10963874102873L) + .table("catalog.db.table") +``` + +| Spark option | Default | Description | +| --------------- | --------------------- |-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| snapshot-id | (latest) | Snapshot ID of the table snapshot to read | +| as-of-timestamp | (latest) | A timestamp in milliseconds; the snapshot used will be the snapshot current at this time. | +| split-size | As per table property | Overrides this table's read.split.target-size and read.split.metadata-target-size | +| lookback | As per table property | Overrides this table's read.split.planning-lookback | +| file-open-cost | As per table property | Overrides this table's read.split.open-file-cost | +| vectorization-enabled | As per table property | Overrides this table's read.parquet.vectorization.enabled | +| batch-size | As per table property | Overrides this table's read.parquet.vectorization.batch-size | +| stream-from-timestamp | (none) | A timestamp in milliseconds to stream from; if before the oldest known ancestor snapshot, the oldest will be used | +| streaming-max-files-per-micro-batch | INT_MAX | Maximum number of files per microbatch | +| streaming-max-rows-per-micro-batch | INT_MAX | "Soft maximum" number of rows per microbatch; always includes all rows in next unprocessed file, excludes additional files if their inclusion would exceed the soft max limit | +| async-micro-batch-planning-enabled | false | Enables asynchronous microbatch planning to reduce planning latency by pre-fetching file scan tasks | +| streaming-snapshot-polling-interval-ms | 30000 | Overrides the polling time for async planner to refresh and detect new snapshots. Only affects when async-micro-batch-planning-enabled is set | +| async-queue-preload-file-limit | 100 | Overrides the number of files loaded to background queue initially. Tune to prevent queue starvation. Only affects when async-micro-batch-planning-enabled is set | +| async-queue-preload-row-limit | 100000 | Overrides the number of rows loaded to background queue initially. Tune to prevent queue starvation. Only affects when async-micro-batch-planning-enabled is set | + +### Write options + +Spark write options are passed when configuring the DataFrameWriterV2, like this: + +```scala +// write with Avro instead of Parquet +df.writeTo("catalog.db.table") + .option("write-format", "avro") + .option("snapshot-property.key", "value") + .append() +``` + +| Spark option | Default | Description | +| ---------------------- | -------------------------- | ------------------------------------------------------------ | +| write-format | Table write.format.default | File format to use for this write operation; parquet, avro, or orc | +| target-file-size-bytes | As per table property | Overrides this table's write.target-file-size-bytes | +| check-nullability | true | Sets the nullable check on fields | +| snapshot-property._custom-key_ | null | Adds an entry with custom-key and corresponding value in the snapshot summary (the `snapshot-property.` prefix is only required for DSv2) | +| fanout-enabled | false | Overrides this table's write.spark.fanout.enabled | +| check-ordering | true | Checks if input schema and table schema are same | +| isolation-level | null | Desired isolation level for Dataframe overwrite operations. `null` => no checks (for idempotent writes), `serializable` => check for concurrent inserts or deletes in destination partitions, `snapshot` => checks for concurrent deletes in destination partitions. | +| validate-from-snapshot-id | null | If isolation level is set, id of base snapshot from which to check concurrent write conflicts into a table. Should be the snapshot before any reads from the table. Can be obtained via [Table API](api.md#table-metadata) or [Snapshots table](spark-queries.md#snapshots). If null, the table's oldest known snapshot is used. | +| compression-codec | Table write.(fileformat).compression-codec | Overrides this table's compression codec for this write | +| compression-level | Table write.(fileformat).compression-level | Overrides this table's compression level for Parquet and Avro tables for this write | +| compression-strategy | Table write.orc.compression-strategy | Overrides this table's compression strategy for ORC tables for this write | +| distribution-mode | See [Spark Writes](spark-writes.md#writing-distribution-modes) for defaults | Override this table's distribution mode for this write | +| delete-granularity | file | Override this table's delete granularity for this write | +| shred-variants | false | Overrides this table's write.parquet.shred-variants for this write | +| variant-inference-buffer-size | 100 | Overrides this table's write.parquet.variant-inference-buffer-size for this write | + +CommitMetadata provides an interface to add custom metadata to a snapshot summary during a SQL execution, which can be beneficial for purposes such as auditing or change tracking. If properties start with `snapshot-property.`, then that prefix will be removed from each property. Here is an example: + +```java +import org.apache.iceberg.spark.CommitMetadata; + +Map properties = Maps.newHashMap(); +properties.put("property_key", "property_value"); +CommitMetadata.withCommitProperties(properties, + () -> { + spark.sql("DELETE FROM " + tableName + " where id = 1"); + return 0; + }, + RuntimeException.class); +``` diff --git a/1.11.0/docs/spark-ddl.md b/1.11.0/docs/spark-ddl.md new file mode 100644 index 000000000000..9fa6c0e7d3c7 --- /dev/null +++ b/1.11.0/docs/spark-ddl.md @@ -0,0 +1,730 @@ +--- +title: "DDL" +--- + + +# Spark DDL + +To use Iceberg in Spark, first configure [Spark catalogs](spark-configuration.md). Iceberg uses Apache Spark's DataSourceV2 API for data source and catalog implementations. + +## `CREATE TABLE` + +Spark 3 can create tables in any Iceberg catalog with the clause `USING iceberg`: + +```sql +CREATE TABLE prod.db.sample ( + id bigint NOT NULL COMMENT 'unique id', + data string) +USING iceberg; +``` + +Iceberg will convert the column type in Spark to corresponding Iceberg type. Please check the section of [type compatibility on creating table](spark-getting-started.md#spark-type-to-iceberg-type) for details. + +Table create commands, including CTAS and RTAS, support the full range of Spark create clauses, including: + +* `PARTITIONED BY (partition-expressions)` to configure partitioning +* `LOCATION '(fully-qualified-uri)'` to set the table location +* `COMMENT 'table documentation'` to set a table description +* `TBLPROPERTIES ('key'='value', ...)` to set [table configuration](configuration.md) + +Create commands may also set the default format with the `USING` clause. This is only supported for `SparkCatalog` because Spark handles the `USING` clause differently for the built-in catalog. + +`CREATE TABLE ... LIKE ...` syntax is not supported. + +### `PARTITIONED BY` + +To create a partitioned table, use `PARTITIONED BY`: + +```sql +CREATE TABLE prod.db.sample ( + id bigint, + data string, + category string) +USING iceberg +PARTITIONED BY (category); +``` + +The `PARTITIONED BY` clause supports transform expressions to create [hidden partitions](partitioning.md). + +```sql +CREATE TABLE prod.db.sample ( + id bigint, + data string, + category string, + ts timestamp) +USING iceberg +PARTITIONED BY (bucket(16, id), days(ts), category); +``` + +Supported transformations are: + +* `year(ts)`: partition by year +* `month(ts)`: partition by month +* `day(ts)` or `date(ts)`: equivalent to dateint partitioning +* `hour(ts)` or `date_hour(ts)`: equivalent to dateint and hour partitioning +* `bucket(N, col)`: partition by hashed value mod N buckets +* `truncate(L, col)`: partition by value truncated to L + * Strings are truncated to the given length + * Integers and longs truncate to bins: `truncate(10, i)` produces partitions 0, 10, 20, 30, ... + +Note: Old syntax of `years(ts)`, `months(ts)`, `days(ts)` and `hours(ts)` are also supported for compatibility. + +The same transforms are also available as Spark SQL functions under the `system` namespace. See +[Spark SQL functions](spark-queries.md#spark-sql-functions). + +## `CREATE TABLE ... AS SELECT` + +Iceberg supports CTAS as an atomic operation when using a [`SparkCatalog`](spark-configuration.md#catalog-configuration). CTAS is supported, but is not atomic when using [`SparkSessionCatalog`](spark-configuration.md#replacing-the-session-catalog). + +```sql +CREATE TABLE prod.db.sample +USING iceberg +AS SELECT ... +``` + +The newly created table won't inherit the partition spec and table properties from the source table in SELECT, you can use PARTITIONED BY and TBLPROPERTIES in CTAS to declare partition spec and table properties for the new table. + +```sql +CREATE TABLE prod.db.sample +USING iceberg +PARTITIONED BY (part) +TBLPROPERTIES ('key'='value') +AS SELECT ... +``` + +## `REPLACE TABLE ... AS SELECT` + +Iceberg supports RTAS as an atomic operation when using a [`SparkCatalog`](spark-configuration.md#catalog-configuration). RTAS is supported, but is not atomic when using [`SparkSessionCatalog`](spark-configuration.md#replacing-the-session-catalog). + +Atomic table replacement creates a new snapshot with the results of the `SELECT` query, but keeps table history. + +```sql +REPLACE TABLE prod.db.sample +USING iceberg +AS SELECT ... +``` +```sql +REPLACE TABLE prod.db.sample +USING iceberg +PARTITIONED BY (part) +TBLPROPERTIES ('key'='value') +AS SELECT ... +``` +```sql +CREATE OR REPLACE TABLE prod.db.sample +USING iceberg +AS SELECT ... +``` + +The schema and partition spec will be replaced if changed. To avoid modifying the table's schema and partitioning, use `INSERT OVERWRITE` instead of `REPLACE TABLE`. +The new table properties in the `REPLACE TABLE` command will be merged with any existing table properties. The existing table properties will be updated if changed else they are preserved. + +## `DROP TABLE` + +The drop table behavior changed in 0.14. + +Prior to 0.14, running `DROP TABLE` would remove the table from the catalog and delete the table contents as well. + +From 0.14 onwards, `DROP TABLE` would only remove the table from the catalog. +In order to delete the table contents `DROP TABLE PURGE` should be used. + +### `DROP TABLE` + +To drop the table from the catalog, run: + +```sql +DROP TABLE prod.db.sample; +``` + +### `DROP TABLE PURGE` + +To drop the table from the catalog and delete the table's contents, run: + +```sql +DROP TABLE prod.db.sample PURGE; +``` + +## `ALTER TABLE` + +Iceberg has full `ALTER TABLE` support in Spark 3, including: + +* Renaming a table +* Setting or removing table properties +* Adding, deleting, and renaming columns +* Adding, deleting, and renaming nested fields +* Reordering top-level columns and nested struct fields +* Widening the type of `int`, `float`, and `decimal` fields +* Making required columns optional + +In addition, [SQL extensions](spark-configuration.md#sql-extensions) can be used to add support for partition evolution and setting a table's write order + +!!! warning "Hive Catalog Limitation" + The Hive Metastore (HMS) validates schema changes by comparing column types **positionally** + (`hive.metastore.disallow.incompatible.col.type.changes`, default `true`). Any schema evolution + operation that shifts column positions will fail when using a Hive catalog. Affected operations + include: + + - `ADD COLUMN` with `FIRST` or `AFTER` clauses + - `ALTER COLUMN` with `FIRST` or `AFTER` clauses (reordering) + - `DROP COLUMN` on a non-last column + + To work around this, disable the HMS schema compatibility check by setting + `hive.metastore.disallow.incompatible.col.type.changes=false`: + + - **Remote HMS:** Set this property in the HMS server's `hive-site.xml`. + - **Embedded HMS:** Pass `--conf spark.hadoop.hive.metastore.disallow.incompatible.col.type.changes=false` when starting Spark. + + **Trade-off:** After disabling this check, the Hive engine may no longer be able to read the table + correctly due to the schema mismatch in the Hive Metastore. Iceberg-aware engines (Spark, Flink, + Trino, etc.) will continue to work correctly, as they read schema from Iceberg metadata rather + than HMS. + +### `ALTER TABLE ... RENAME TO` + +```sql +ALTER TABLE prod.db.sample RENAME TO prod.db.new_name; +``` + +### `ALTER TABLE ... SET TBLPROPERTIES` + +```sql +ALTER TABLE prod.db.sample SET TBLPROPERTIES ( + 'read.split.target-size'='268435456' +); +``` + +Iceberg uses table properties to control table behavior. For a list of available properties, see [Table configuration](configuration.md). + +`UNSET` is used to remove properties: + +```sql +ALTER TABLE prod.db.sample UNSET TBLPROPERTIES ('read.split.target-size'); +``` + +`SET TBLPROPERTIES` can also be used to set the table comment (description): + +```sql +ALTER TABLE prod.db.sample SET TBLPROPERTIES ( + 'comment' = 'A table comment.' +); +``` + +### `ALTER TABLE ... ADD COLUMN` + +To add a column to Iceberg, use the `ADD COLUMNS` clause with `ALTER TABLE`: + +```sql +ALTER TABLE prod.db.sample +ADD COLUMNS ( + new_column string comment 'new_column docs' +); +``` + +Multiple columns can be added at the same time, separated by commas. + +Nested columns should be identified using the full column name: + +```sql +-- create a struct column +ALTER TABLE prod.db.sample +ADD COLUMN point struct; + +-- add a field to the struct +ALTER TABLE prod.db.sample +ADD COLUMN point.z double; +``` + +```sql +-- create a nested array column of struct +ALTER TABLE prod.db.sample +ADD COLUMN points array>; + +-- add a field to the struct within an array. Using keyword 'element' to access the array's element column. +ALTER TABLE prod.db.sample +ADD COLUMN points.element.z double; +``` + +```sql +-- create a map column of struct key and struct value +ALTER TABLE prod.db.sample +ADD COLUMN points map, struct>; + +-- add a field to the value struct in a map. Using keyword 'value' to access the map's value column. +ALTER TABLE prod.db.sample +ADD COLUMN points.value.b int; +``` + +Note: Altering a map 'key' column by adding columns is not allowed. Only map values can be updated. + +Add columns in any position by adding `FIRST` or `AFTER` clauses: + +```sql +ALTER TABLE prod.db.sample +ADD COLUMN new_column bigint AFTER other_column; +``` + +```sql +ALTER TABLE prod.db.sample +ADD COLUMN nested.new_column bigint FIRST; +``` + +!!! warning "Hive Catalog Limitation" + When using a Hive catalog, adding a column with `FIRST` or `AFTER` may fail due to HMS positional + schema validation. See the warning above for details + and workaround. + +### `ALTER TABLE ... RENAME COLUMN` + +Iceberg allows any field to be renamed. To rename a field, use `RENAME COLUMN`: + +```sql +ALTER TABLE prod.db.sample RENAME COLUMN data TO payload; +ALTER TABLE prod.db.sample RENAME COLUMN location.lat TO latitude; +``` + +Note that nested rename commands only rename the leaf field. The above command renames `location.lat` to `location.latitude` + +### `ALTER TABLE ... ALTER COLUMN` + +Alter column is used to widen types, make a field optional, set comments, and reorder fields. + +Iceberg allows updating column types if the update is safe. Safe updates are: + +* `int` to `bigint` +* `float` to `double` +* `decimal(P,S)` to `decimal(P2,S)` when P2 > P (scale cannot change) + +```sql +ALTER TABLE prod.db.sample ALTER COLUMN measurement TYPE double; +``` + +To add or remove columns from a struct, use `ADD COLUMN` or `DROP COLUMN` with a nested column name. + +Column comments can also be updated using `ALTER COLUMN`: + +```sql +ALTER TABLE prod.db.sample ALTER COLUMN measurement TYPE double COMMENT 'unit is bytes per second'; +ALTER TABLE prod.db.sample ALTER COLUMN measurement COMMENT 'unit is kilobytes per second'; +``` + +Iceberg allows reordering top-level columns or columns in a struct using `FIRST` and `AFTER` clauses: + +```sql +ALTER TABLE prod.db.sample ALTER COLUMN col FIRST; +``` +```sql +ALTER TABLE prod.db.sample ALTER COLUMN nested.col AFTER other_col; +``` + +!!! warning "Hive Catalog Limitation" + When using a Hive catalog, reordering columns may fail due to HMS positional schema validation. + See the Hive Catalog Limitation note above for details and workaround. + +Nullability for a non-nullable column can be changed using `DROP NOT NULL`: + +```sql +ALTER TABLE prod.db.sample ALTER COLUMN id DROP NOT NULL; +``` + +!!! info + It is not possible to change a nullable column to a non-nullable column with `SET NOT NULL` because Iceberg doesn't know whether there is existing data with null values. + +!!! info + `ALTER COLUMN` is not used to update `struct` types. Use `ADD COLUMN` and `DROP COLUMN` to add or remove struct fields. + +### `ALTER TABLE ... DROP COLUMN` + +To drop columns, use `ALTER TABLE ... DROP COLUMN`: + +```sql +ALTER TABLE prod.db.sample DROP COLUMN id; +ALTER TABLE prod.db.sample DROP COLUMN point.z; +``` + +!!! warning "Hive Catalog Limitation" + When using a Hive catalog, dropping a non-last column may fail due to HMS positional schema + validation. See the earlier Hive Catalog Limitation warning above for details and + workaround. + +## `ALTER TABLE` SQL extensions + +These commands are available in Spark 3 when using Iceberg [SQL extensions](spark-configuration.md#sql-extensions). + +### `ALTER TABLE ... ADD PARTITION FIELD` + +Iceberg supports adding new partition fields to a spec using `ADD PARTITION FIELD`: + +```sql +ALTER TABLE prod.db.sample ADD PARTITION FIELD catalog; -- identity transform +``` + +[Partition transforms](#partitioned-by) are also supported: + +```sql +ALTER TABLE prod.db.sample ADD PARTITION FIELD bucket(16, id); +ALTER TABLE prod.db.sample ADD PARTITION FIELD truncate(4, data); +ALTER TABLE prod.db.sample ADD PARTITION FIELD year(ts); +-- use optional AS keyword to specify a custom name for the partition field +ALTER TABLE prod.db.sample ADD PARTITION FIELD bucket(16, id) AS shard; +``` + +Adding a partition field is a metadata operation and does not change any of the existing table data. New data will be written with the new partitioning, but existing data will remain in the old partition layout. Old data files will have null values for the new partition fields in metadata tables. + +Dynamic partition overwrite behavior will change when the table's partitioning changes because dynamic overwrite replaces partitions implicitly. To overwrite explicitly, use the new `DataFrameWriterV2` API. + +!!! note + To migrate from daily to hourly partitioning with transforms, it is not necessary to drop the daily partition field. Keeping the field ensures existing metadata table queries continue to work. + +!!! danger + **Dynamic partition overwrite behavior will change** when partitioning changes + For example, if you partition by days and move to partitioning by hours, overwrites will overwrite hourly partitions but not days anymore. + +### `ALTER TABLE ... DROP PARTITION FIELD` + +Partition fields can be removed using `DROP PARTITION FIELD`: + +```sql +ALTER TABLE prod.db.sample DROP PARTITION FIELD catalog; +ALTER TABLE prod.db.sample DROP PARTITION FIELD bucket(16, id); +ALTER TABLE prod.db.sample DROP PARTITION FIELD truncate(4, data); +ALTER TABLE prod.db.sample DROP PARTITION FIELD year(ts); +ALTER TABLE prod.db.sample DROP PARTITION FIELD shard; +``` + +Note that although the partition is removed, the column will still exist in the table schema. + +Dropping a partition field is a metadata operation and does not change any of the existing table data. New data will be written with the new partitioning, but existing data will remain in the old partition layout. + +!!! danger + **Dynamic partition overwrite behavior will change** when partitioning changes + For example, if you partition by days and move to partitioning by hours, overwrites will overwrite hourly partitions but not days anymore. + +!!! danger + Be careful when dropping a partition field because it will change the schema of metadata tables, like `files`, and may cause metadata queries to fail or produce different results. + +### `ALTER TABLE ... REPLACE PARTITION FIELD` + +A partition field can be replaced by a new partition field in a single metadata update by using `REPLACE PARTITION FIELD`: + +```sql +ALTER TABLE prod.db.sample REPLACE PARTITION FIELD ts_day WITH day(ts); +-- use optional AS keyword to specify a custom name for the new partition field +ALTER TABLE prod.db.sample REPLACE PARTITION FIELD ts_day WITH day(ts) AS day_of_ts; +``` + +### `ALTER TABLE ... WRITE ORDERED BY` + +Iceberg tables can be configured with a sort order that is used to automatically sort data that is written to the table in some engines. For example, `MERGE INTO` in Spark will use the table ordering. + +To set the write order for a table, use `WRITE ORDERED BY`: + +```sql +ALTER TABLE prod.db.sample WRITE ORDERED BY category, id +-- use optional ASC/DESC keyword to specify sort order of each field (default ASC) +ALTER TABLE prod.db.sample WRITE ORDERED BY category ASC, id DESC +-- use optional NULLS FIRST/NULLS LAST keyword to specify null order of each field (default FIRST) +ALTER TABLE prod.db.sample WRITE ORDERED BY category ASC NULLS LAST, id DESC NULLS FIRST +``` + +!!! info + Table write order does not guarantee data order for queries. It only affects how data is written to the table. + +`WRITE ORDERED BY` sets a global ordering where rows are ordered across tasks, like using `ORDER BY` in an `INSERT` command: + +```sql +INSERT INTO prod.db.sample +SELECT id, data, category, ts FROM another_table +ORDER BY ts, category +``` + +To order within each task, not across tasks, use `LOCALLY ORDERED BY`: + +```sql +ALTER TABLE prod.db.sample WRITE LOCALLY ORDERED BY category, id +``` + +To unset the sort order of the table, use `UNORDERED`: + +```sql +ALTER TABLE prod.db.sample WRITE UNORDERED +``` + +### `ALTER TABLE ... WRITE DISTRIBUTED BY PARTITION` + +`WRITE DISTRIBUTED BY PARTITION` will request that each partition is handled by one writer, the default implementation is hash distribution. + +```sql +ALTER TABLE prod.db.sample WRITE DISTRIBUTED BY PARTITION +``` + +`DISTRIBUTED BY PARTITION` and `LOCALLY ORDERED BY` may be used together, to distribute by partition and locally order rows within each task. + +```sql +ALTER TABLE prod.db.sample WRITE DISTRIBUTED BY PARTITION LOCALLY ORDERED BY category, id +``` + +### `ALTER TABLE ... SET IDENTIFIER FIELDS` + +Iceberg supports setting [identifier fields](https://iceberg.apache.org/spec/#identifier-field-ids) to a spec using `SET IDENTIFIER FIELDS`: +Spark table can support Flink SQL upsert operation if the table has identifier fields. + +```sql +ALTER TABLE prod.db.sample SET IDENTIFIER FIELDS id +-- single column +ALTER TABLE prod.db.sample SET IDENTIFIER FIELDS id, data +-- multiple columns +``` + +Identifier fields must be `NOT NULL` columns when they are created or added. +The later `ALTER` statement will overwrite the previous setting. + +### `ALTER TABLE ... DROP IDENTIFIER FIELDS` + +Identifier fields can be removed using `DROP IDENTIFIER FIELDS`: + +```sql +ALTER TABLE prod.db.sample DROP IDENTIFIER FIELDS id +-- single column +ALTER TABLE prod.db.sample DROP IDENTIFIER FIELDS id, data +-- multiple columns +``` + +Note that although the identifier is removed, the column will still exist in the table schema. + +### Branching and Tagging DDL + +#### `ALTER TABLE ... CREATE BRANCH` + +Branches can be created via the `CREATE BRANCH` statement with the following options: + +* Do not fail if the branch already exists with `IF NOT EXISTS` +* Update the branch if it already exists with `CREATE OR REPLACE` +* Create a branch at a specific snapshot +* Create a branch with a specified retention period + +```sql +-- CREATE audit-branch at current snapshot with default retention. +ALTER TABLE prod.db.sample CREATE BRANCH `audit-branch` + +-- CREATE audit-branch at current snapshot with default retention if it doesn't exist. +ALTER TABLE prod.db.sample CREATE BRANCH IF NOT EXISTS `audit-branch` + +-- CREATE audit-branch at current snapshot with default retention or REPLACE it if it already exists. +ALTER TABLE prod.db.sample CREATE OR REPLACE BRANCH `audit-branch` + +-- CREATE audit-branch at snapshot 1234 with default retention. +ALTER TABLE prod.db.sample CREATE BRANCH `audit-branch` +AS OF VERSION 1234 + +-- CREATE audit-branch at snapshot 1234, retain audit-branch for 30 days, and retain the latest 30 days. The latest 3 snapshot snapshots, and 2 days worth of snapshots. +ALTER TABLE prod.db.sample CREATE BRANCH `audit-branch` +AS OF VERSION 1234 RETAIN 30 DAYS +WITH SNAPSHOT RETENTION 3 SNAPSHOTS 2 DAYS +``` + +#### `ALTER TABLE ... CREATE TAG` + +Tags can be created via the `CREATE TAG` statement with the following options: + +* Do not fail if the tag already exists with `IF NOT EXISTS` +* Update the tag if it already exists with `CREATE OR REPLACE` +* Create a tag at a specific snapshot +* Create a tag with a specified retention period + +```sql +-- CREATE historical-tag at current snapshot with default retention. +ALTER TABLE prod.db.sample CREATE TAG `historical-tag` + +-- CREATE historical-tag at current snapshot with default retention if it doesn't exist. +ALTER TABLE prod.db.sample CREATE TAG IF NOT EXISTS `historical-tag` + +-- CREATE historical-tag at current snapshot with default retention or REPLACE it if it already exists. +ALTER TABLE prod.db.sample CREATE OR REPLACE TAG `historical-tag` + +-- CREATE historical-tag at snapshot 1234 with default retention. +ALTER TABLE prod.db.sample CREATE TAG `historical-tag` AS OF VERSION 1234 + +-- CREATE historical-tag at snapshot 1234 and retain it for 1 year. +ALTER TABLE prod.db.sample CREATE TAG `historical-tag` +AS OF VERSION 1234 RETAIN 365 DAYS +``` + +#### `ALTER TABLE ... REPLACE BRANCH` + +The snapshot which a branch references can be updated via +the `REPLACE BRANCH` sql. Retention can also be updated in this statement. + +```sql +-- REPLACE audit-branch to reference snapshot 4567 and update the retention to 60 days. +ALTER TABLE prod.db.sample REPLACE BRANCH `audit-branch` +AS OF VERSION 4567 RETAIN 60 DAYS +``` + +#### `ALTER TABLE ... REPLACE TAG` + +The snapshot which a tag references can be updated via +the `REPLACE TAG` sql. Retention can also be updated in this statement. + +```sql +-- REPLACE historical-tag to reference snapshot 4567 and update the retention to 60 days. +ALTER TABLE prod.db.sample REPLACE TAG `historical-tag` +AS OF VERSION 4567 RETAIN 60 DAYS +``` + +#### `ALTER TABLE ... DROP BRANCH` + +Branches can be removed via the `DROP BRANCH` sql + +```sql +ALTER TABLE prod.db.sample DROP BRANCH `audit-branch` +``` + +#### `ALTER TABLE ... DROP TAG` + +Tags can be removed via the `DROP TAG` sql + +```sql +ALTER TABLE prod.db.sample DROP TAG `historical-tag` +``` + +### Iceberg views in Spark + +Iceberg views are a [common representation](../../view-spec.md) of a SQL view that aim to be interpreted across multiple query engines. +This section covers how to create and manage views in Spark using Spark 3.4 and above (earlier versions of Spark are not supported). + +!!! note + + All the SQL examples in this section follow the official Spark SQL syntax: + + * [CREATE VIEW](https://spark.apache.org/docs/latest/sql-ref-syntax-ddl-create-view.html#create-view) + * [ALTER VIEW](https://spark.apache.org/docs/latest/sql-ref-syntax-ddl-alter-view.html) + * [DROP VIEW](https://spark.apache.org/docs/latest/sql-ref-syntax-ddl-drop-view.html) + * [SHOW VIEWS](https://spark.apache.org/docs/latest/sql-ref-syntax-aux-show-views.html) + * [SHOW TBLPROPERTIES](https://spark.apache.org/docs/latest/sql-ref-syntax-aux-show-tblproperties.html) + * [SHOW CREATE TABLE](https://spark.apache.org/docs/latest/sql-ref-syntax-aux-show-create-table.html) + +#### Creating a view + +Create a simple view without any comments or properties: +```sql +CREATE VIEW AS SELECT * FROM +``` + +Using `IF NOT EXISTS` prevents the SQL statement from failing in case the view already exists: +```sql +CREATE VIEW IF NOT EXISTS AS SELECT * FROM +``` + +Create a view with a comment, including aliased and commented columns that are different from the source table: +```sql +CREATE VIEW (ID COMMENT 'Unique ID', ZIP COMMENT 'Zipcode') + COMMENT 'View Comment' + AS SELECT id, zip FROM +``` + +#### Creating a view with properties + +Create a view with properties using `TBLPROPERTIES`: +```sql +CREATE VIEW + TBLPROPERTIES ('key1' = 'val1', 'key2' = 'val2') + AS SELECT * FROM +``` + +Display view properties: +```sql +SHOW TBLPROPERTIES +``` + +#### Creating a view with location + +To specify the view metadata location, use `TBLPROPERTIES ('location'='fully-qualified-uri')`: + +```sql +CREATE VIEW + TBLPROPERTIES ('location' = '/path/to/custom-location') +AS SELECT * FROM +``` + +The view metadata is stored in the specified location with `/metadata` appended, such as `/path/to/custom-location/metadata`. + +#### Dropping a view + +Drop an existing view: +```sql +DROP VIEW +``` + +Using `IF EXISTS` prevents the SQL statement from failing if the view does not exist: +```sql +DROP VIEW IF EXISTS +``` + +#### Replacing a view + +Update a view's schema, its properties, or the underlying SQL statement using `CREATE OR REPLACE`: +```sql +CREATE OR REPLACE VIEW (updated_id COMMENT 'updated ID') + TBLPROPERTIES ('key1' = 'new_val1') + AS SELECT id FROM +``` + +#### Setting and removing view properties + +Set the properties of an existing view using `ALTER VIEW ... SET TBLPROPERTIES`: +```sql +ALTER VIEW SET TBLPROPERTIES ('key1' = 'val1', 'key2' = 'val2') +``` + +Remove the properties from an existing view using `ALTER VIEW ... UNSET TBLPROPERTIES`: +```sql +ALTER VIEW UNSET TBLPROPERTIES ('key1', 'key2') +``` + +#### Showing available views + +List all views in the currently set namespace (via `USE `): +```sql +SHOW VIEWS +``` + +List all available views in the defined catalog and/or namespace using one of the below variations: +```sql +SHOW VIEWS IN +``` +```sql +SHOW VIEWS IN +``` +```sql +SHOW VIEWS IN . +``` + +#### Showing the CREATE statement of a view + +Show the CREATE statement of a view: +```sql +SHOW CREATE TABLE +``` + +#### Displaying view details + +Display additional view details using `DESCRIBE`: + +```sql +DESCRIBE [EXTENDED] +``` diff --git a/1.11.0/docs/spark-getting-started.md b/1.11.0/docs/spark-getting-started.md new file mode 100644 index 000000000000..821bfd022c20 --- /dev/null +++ b/1.11.0/docs/spark-getting-started.md @@ -0,0 +1,208 @@ +--- +title: "Getting Started" +--- + + +# Getting Started + +The latest version of Iceberg is [{{ icebergVersion }}](../../releases.md). + +Spark is currently the most feature-rich compute engine for Iceberg operations. +We recommend you to get started with Spark to understand Iceberg concepts and features with examples. +You can also view documentations of using Iceberg with other compute engine under the [Multi-Engine Support](../../multi-engine-support.md) page. + +## Using Iceberg in Spark + +To use Iceberg in a Spark shell, use the `--packages` option: + +```sh +spark-shell --packages org.apache.iceberg:iceberg-spark-runtime-{{ sparkVersionMajor }}:{{ icebergVersion }} +``` + +!!! info + + If you want to include Iceberg in your Spark installation, add the [`iceberg-spark-runtime-{{ sparkVersionMajor }}` Jar](https://search.maven.org/remotecontent?filepath=org/apache/iceberg/iceberg-spark-runtime-{{ sparkVersionMajor }}/{{ icebergVersion }}/iceberg-spark-runtime-{{ sparkVersionMajor }}-{{ icebergVersion }}.jar) to Spark's `jars` folder. + +### Adding catalogs + +Iceberg comes with [catalogs](spark-configuration.md#catalogs) that enable SQL commands to manage tables and load them by name. Catalogs are configured using properties under `spark.sql.catalog.(catalog_name)`. + +This command creates a path-based catalog named `local` for tables under `$PWD/warehouse` and adds support for Iceberg tables to Spark's built-in catalog: + +```sh +spark-sql --packages org.apache.iceberg:iceberg-spark-runtime-{{ sparkVersionMajor }}:{{ icebergVersion }}\ + --conf spark.sql.extensions=org.apache.iceberg.spark.extensions.IcebergSparkSessionExtensions \ + --conf spark.sql.catalog.spark_catalog=org.apache.iceberg.spark.SparkSessionCatalog \ + --conf spark.sql.catalog.spark_catalog.type=hive \ + --conf spark.sql.catalog.local=org.apache.iceberg.spark.SparkCatalog \ + --conf spark.sql.catalog.local.type=hadoop \ + --conf spark.sql.catalog.local.warehouse=$PWD/warehouse +``` + +### Creating a table + +To create your first Iceberg table in Spark, use the `spark-sql` shell or `spark.sql(...)` to run a [`CREATE TABLE`](spark-ddl.md#create-table) command: + +```sql +-- local is the path-based catalog defined above +CREATE TABLE local.db.table (id bigint, data string) USING iceberg; +CREATE TABLE source (id bigint, data string) USING parquet; +CREATE TABLE updates (id bigint, data string) USING parquet; +``` + +Iceberg catalogs support the full range of SQL DDL commands, including: + +* [`CREATE TABLE ... PARTITIONED BY`](spark-ddl.md#create-table) +* [`CREATE TABLE ... AS SELECT`](spark-ddl.md#create-table-as-select) +* [`ALTER TABLE`](spark-ddl.md#alter-table) +* [`DROP TABLE`](spark-ddl.md#drop-table) + +### Writing + +Once your table is created, insert data using [`INSERT INTO`](spark-writes.md#insert-into): + +```sql +INSERT INTO local.db.table VALUES (1, 'a'), (2, 'b'), (3, 'c'); +INSERT INTO source VALUES (10, 'd'), (11, 'ee'); +INSERT INTO updates VALUES (1, 'x'), (2, 'x'), (4, 'z'); +INSERT INTO local.db.table SELECT id, data FROM source WHERE length(data) = 1; +``` + +Iceberg also adds row-level SQL updates to Spark, [`MERGE INTO`](spark-writes.md#merge-into) and [`DELETE FROM`](spark-writes.md#delete-from): + +```sql +MERGE INTO local.db.table t USING (SELECT * FROM updates) u ON t.id = u.id +WHEN MATCHED THEN UPDATE SET t.data = u.data +WHEN NOT MATCHED THEN INSERT *; +``` + +Iceberg supports writing DataFrames using the new [v2 DataFrame write API](spark-writes.md#writing-with-dataframes): + +```scala +spark.table("source").select("id", "data") + .writeTo("local.db.table").append() +``` + +The old `write` API is supported, but _not_ recommended. + +### Reading + +To read with SQL, use the Iceberg table's name in a `SELECT` query: + +```sql +SELECT count(1) as count, data +FROM local.db.table +GROUP BY data; +``` + +SQL is also the recommended way to [inspect tables](spark-queries.md#inspecting-tables). To view all snapshots in a table, use the `snapshots` metadata table: +```sql +SELECT * FROM local.db.table.snapshots; +``` +``` ++-------------------------+----------------+-----------+-----------+----------------------------------------------------+-----+ +| committed_at | snapshot_id | parent_id | operation | manifest_list | ... | ++-------------------------+----------------+-----------+-----------+----------------------------------------------------+-----+ +| 2019-02-08 03:29:51.215 | 57897183625154 | null | append | s3://.../table/metadata/snap-57897183625154-1.avro | ... | +| | | | | | ... | +| | | | | | ... | +| ... | ... | ... | ... | ... | ... | ++-------------------------+----------------+-----------+-----------+----------------------------------------------------+-----+ +``` + +[DataFrame reads](spark-queries.md#querying-with-dataframes) are supported and can now reference tables by name using `spark.table`: + +```scala +val df = spark.table("local.db.table") +df.count() +``` + +### Type compatibility + +Spark and Iceberg support different set of types. Iceberg does the type conversion automatically, but not for all combinations, +so you may want to understand the type conversion in Iceberg in prior to design the types of columns in your tables. + +#### Spark type to Iceberg type + +This type conversion table describes how Spark types are converted to the Iceberg types. The conversion applies on both creating Iceberg table and writing to Iceberg table via Spark. + +| Spark | Iceberg | Notes | +|-----------------|----------------------------|-------| +| boolean | boolean | | +| short | integer | | +| byte | integer | | +| integer | integer | | +| long | long | | +| float | float | | +| double | double | | +| date | date | | +| timestamp | timestamp with timezone | | +| timestamp_ntz | timestamp without timezone | | +| char | string | | +| varchar | string | | +| string | string | | +| binary | binary | | +| decimal | decimal | | +| struct | struct | | +| array | list | | +| map | map | | + +!!! info + The table is based on representing conversion during creating table. In fact, broader supports are applied on write. Here're some points on write: + + * Iceberg numeric types (`integer`, `long`, `float`, `double`, `decimal`) support promotion during writes. e.g. You can write Spark types `short`, `byte`, `integer`, `long` to Iceberg type `long`. + * You can write to Iceberg `fixed` type using Spark `binary` type. Note that assertion on the length will be performed. + +#### Iceberg type to Spark type + +This type conversion table describes how Iceberg types are converted to the Spark types. The conversion applies on reading from Iceberg table via Spark. + +| Iceberg | Spark | Note | +|----------------------------|-------------------------|---------------| +| boolean | boolean | | +| integer | integer | | +| long | long | | +| float | float | | +| double | double | | +| date | date | | +| time | | Not supported | +| timestamp with timezone | timestamp | | +| timestamp without timezone | timestamp_ntz | | +| string | string | | +| uuid | string | | +| fixed | binary | | +| binary | binary | | +| decimal | decimal | | +| struct | struct | | +| list | array | | +| map | map | | +| nanosecond timestamp | | Not supported | +| nanosecond timestamp with timezone | | Not supported | +| unknown | null | Spark 4.0+ | +| variant | variant | Spark 4.0+ | +| geometry | | Not supported | +| geography | | Not supported | + +### Next steps + +Next, you can learn more about Iceberg tables in Spark: + +* [DDL commands](spark-ddl.md): `CREATE`, `ALTER`, and `DROP` +* [Querying data](spark-queries.md): `SELECT` queries and metadata tables +* [Writing data](spark-writes.md): `INSERT INTO` and `MERGE INTO` +* [Maintaining tables](spark-procedures.md) with stored procedures diff --git a/1.11.0/docs/spark-procedures.md b/1.11.0/docs/spark-procedures.md new file mode 100644 index 000000000000..8e594caa12d4 --- /dev/null +++ b/1.11.0/docs/spark-procedures.md @@ -0,0 +1,1105 @@ +--- +title: "Procedures" +--- + + +# Spark Procedures + +To use Iceberg in Spark, first configure [Spark catalogs](spark-configuration.md). +For Spark 3.x, stored procedures are only available when using [Iceberg SQL extensions](spark-configuration.md#sql-extensions) in Spark. +For Spark 4.0, stored procedures are supported natively without requiring the Iceberg SQL extensions. However, note that they are __case-sensitive__ in Spark 4.0. + +## Usage + +Procedures can be used from any configured Iceberg catalog with `CALL`. All procedures are in the namespace `system`. + +`CALL` supports passing arguments by name (recommended) or by position. Mixing position and named arguments is not supported. + +### Named arguments + +All procedure arguments are named. When passing arguments by name, arguments can be in any order and any optional argument can be omitted. + +```sql +CALL catalog_name.system.procedure_name(arg_name_2 => arg_2, arg_name_1 => arg_1); +``` + +### Positional arguments + +When passing arguments by position, only the ending arguments may be omitted if they are optional. + +```sql +CALL catalog_name.system.procedure_name(arg_1, arg_2, ... arg_n); +``` + +## Snapshot management + +### `rollback_to_snapshot` + +Roll back a table to a specific snapshot ID. + +To roll back to a specific time, use [`rollback_to_timestamp`](#rollback_to_timestamp). + +!!! info + This procedure invalidates all cached Spark plans that reference the affected table. + +#### Usage + +| Argument Name | Required? | Type | Description | +|---------------|-----------|------|-------------| +| `table` | ✔️ | string | Name of the table to update | +| `snapshot_id` | ✔️ | long | Snapshot ID to rollback to | + +#### Output + +| Output Name | Type | Description | +| ------------|------|-------------| +| `previous_snapshot_id` | long | The current snapshot ID before the rollback | +| `current_snapshot_id` | long | The new current snapshot ID | + +#### Example + +Roll back table `db.sample` to snapshot ID `1`: + +```sql +CALL catalog_name.system.rollback_to_snapshot('db.sample', 1); +``` + +### `rollback_to_timestamp` + +Roll back a table to the snapshot that was current at some time. + +!!! info + This procedure invalidates all cached Spark plans that reference the affected table. + +#### Usage + +| Argument Name | Required? | Type | Description | +|---------------|-----------|------|-------------| +| `table` | ✔️ | string | Name of the table to update | +| `timestamp` | ✔️ | timestamp | A timestamp to rollback to | + +#### Output + +| Output Name | Type | Description | +| ------------|------|-------------| +| `previous_snapshot_id` | long | The current snapshot ID before the rollback | +| `current_snapshot_id` | long | The new current snapshot ID | + +#### Example + +Roll back `db.sample` to a specific day and time. +```sql +CALL catalog_name.system.rollback_to_timestamp('db.sample', TIMESTAMP '2021-06-30 00:00:00.000'); +``` + +### `set_current_snapshot` + +Sets the current snapshot ID for a table. + +Unlike rollback, the snapshot is not required to be an ancestor of the current table state. + +!!! info + This procedure invalidates all cached Spark plans that reference the affected table. + +#### Usage + +| Argument Name | Required? | Type | Description | +|---------------|-----------|------|-------------| +| `table` | ✔️ | string | Name of the table to update | +| `snapshot_id` | | long | Snapshot ID to set as current | +| `ref` | | string | Snapshot Reference (branch or tag) to set as current | + +Either `snapshot_id` or `ref` must be provided but not both. + +#### Output + +| Output Name | Type | Description | +| ------------|------|-------------| +| `previous_snapshot_id` | long | The current snapshot ID before the rollback | +| `current_snapshot_id` | long | The new current snapshot ID | + +#### Example + +Set the current snapshot for `db.sample` to 1: +```sql +CALL catalog_name.system.set_current_snapshot('db.sample', 1); +``` + +Set the current snapshot for `db.sample` to tag `s1`: +```sql +CALL catalog_name.system.set_current_snapshot(table => 'db.sample', ref => 's1'); +``` + +### `cherrypick_snapshot` + +Cherry-picks changes from a snapshot into the current table state. + +Cherry-picking creates a new snapshot from an existing snapshot without altering or removing the original. + +Only append and dynamic overwrite snapshots can be cherry-picked. + +!!! info + This procedure invalidates all cached Spark plans that reference the affected table. + +#### Usage + +| Argument Name | Required? | Type | Description | +|---------------|-----------|------|-------------| +| `table` | ✔️ | string | Name of the table to update | +| `snapshot_id` | ✔️ | long | The snapshot ID to cherry-pick | + +#### Output + +| Output Name | Type | Description | +| ------------|------|-------------| +| `source_snapshot_id` | long | The table's current snapshot before the cherry-pick | +| `current_snapshot_id` | long | The snapshot ID created by applying the cherry-pick | + +#### Examples + +Cherry-pick snapshot 1 +```sql +CALL catalog_name.system.cherrypick_snapshot('my_table', 1); +``` + +Cherry-pick snapshot 1 with named args +```sql +CALL catalog_name.system.cherrypick_snapshot(snapshot_id => 1, table => 'my_table' ); +``` +### `publish_changes` + +Publish changes from a staged WAP ID into the current table state. + +publish_changes creates a new snapshot from an existing snapshot without altering or removing the original. + +Only append and dynamic overwrite snapshots can be successfully published. + +The `publish_changes` procedure will fail if there are multiple snapshots in the table with the provided `wap_id`. + +!!! info + This procedure invalidates all cached Spark plans that reference the affected table. + +#### Usage + +| Argument Name | Required? | Type | Description | +|---------------|-----------|--------|-------------| +| `table` | ✔️ | string | Name of the table to update | +| `wap_id` | ✔️ | string | The wap_id to be published from stage to prod | + +#### Output + +| Output Name | Type | Description | +| ------------|------|-------------| +| `source_snapshot_id` | long | The table's current snapshot before publishing the change | +| `current_snapshot_id` | long | The snapshot ID created by applying the change | + +#### Examples + +publish_changes with WAP ID 'wap_id_1' +```sql +CALL catalog_name.system.publish_changes('my_table', 'wap_id_1'); +``` + +publish_changes with named args +```sql +CALL catalog_name.system.publish_changes(wap_id => 'wap_id_2', table => 'my_table'); +``` + +### `fast_forward` + +Fast-forward the current snapshot of one branch to the latest snapshot of another. + +#### Usage + +| Argument Name | Required? | Type | Description | +|---------------|-----------|------|-------------| +| `table` | ✔️ | string | Name of the table to update | +| `branch` | ✔️ | string | Name of the branch to fast-forward | +| `to` | ✔️ | string | | Name of the branch to be fast-forwarded to | + +#### Output + +| Output Name | Type | Description | +| ------------|------|-------------| +| `branch_updated` | string | Name of the branch that has been fast-forwarded | +| `previous_ref` | long | The snapshot ID before applying fast-forward | +| `updated_ref` | long | The current snapshot ID after applying fast-forward | + +#### Examples + +Fast-forward the main branch to the head of `audit-branch` +```sql +CALL catalog_name.system.fast_forward('my_table', 'main', 'audit-branch'); +``` + +## Metadata management + +Many [maintenance actions](maintenance.md) can be performed using Iceberg stored procedures. + +### `expire_snapshots` + +Each write/update/delete/upsert/compaction in Iceberg produces a new snapshot while keeping the old data and metadata +around for snapshot isolation and time travel. The `expire_snapshots` procedure can be used to remove older snapshots +and their files which are no longer needed. + +This procedure will remove old snapshots and data files which are uniquely required by those old snapshots. This means +the `expire_snapshots` procedure will never remove files which are still required by a non-expired snapshot. + +#### Usage + +| Argument Name | Required? | Type | Description | +|---------------|-----------|------|-------------| +| `table` | ✔️ | string | Name of the table to update | +| `older_than` | ️ | timestamp | Timestamp before which snapshots will be removed (Default: 5 days ago) | +| `retain_last` | | int | Number of ancestor snapshots to preserve regardless of `older_than` (defaults to 1) | +| `max_concurrent_deletes` | | int | Size of the thread pool used for delete file actions (by default, no thread pool is used) | +| `stream_results` | | boolean | When true, deletion files will be sent to Spark driver by RDD partition (by default, all the files will be sent to Spark driver). This option is recommended to set to `true` to prevent Spark driver OOM from large file size | +| `snapshot_ids` | | array of long | Array of snapshot IDs to expire. | +| `clean_expired_metadata` | | boolean | When true, cleans up metadata such as partition specs and schemas that are no longer referenced by snapshots. | + +If `older_than` and `retain_last` are omitted, the table's [expiration properties](configuration.md#table-behavior-properties) will be used. +Snapshots that are still referenced by branches or tags won't be removed. By default, branches and tags never expire, but their retention policy can be changed with the table property `history.expire.max-ref-age-ms`. The `main` branch never expires. + +#### Output + +| Output Name | Type | Description | +| ------------|------|-------------| +| `deleted_data_files_count` | long | Number of data files deleted by this operation | +| `deleted_position_delete_files_count` | long | Number of position delete files deleted by this operation | +| `deleted_equality_delete_files_count` | long | Number of equality delete files deleted by this operation | +| `deleted_manifest_files_count` | long | Number of manifest files deleted by this operation | +| `deleted_manifest_lists_count` | long | Number of manifest list files deleted by this operation | +| `deleted_statistics_files_count` | long | Number of statistics files deleted by this operation | + +#### Examples + +Remove snapshots older than specific day and time, but retain the last 100 snapshots: + +```sql +CALL hive_prod.system.expire_snapshots('db.sample', TIMESTAMP '2021-06-30 00:00:00.000', 100); +``` + +Remove snapshots with snapshot ID `123` (note that this snapshot ID should not be the current snapshot): + +```sql +CALL hive_prod.system.expire_snapshots(table => 'db.sample', snapshot_ids => ARRAY(123)); +``` + +### `remove_orphan_files` + +Used to remove files which are not referenced in any metadata files of an Iceberg table and can thus be considered "orphaned". + +#### Usage + +| Argument Name | Required? | Type | Description | +|---------------|-----------|------|-------------| +| `table` | ✔️ | string | Name of the table to clean | +| `older_than` | ️ | timestamp | Remove orphan files created before this timestamp (Defaults to 3 days ago) | +| `location` | | string | Directory to look for files in (defaults to the table's location) | +| `dry_run` | | boolean | When true, don't actually remove files (defaults to false) | +| `max_concurrent_deletes` | | int | Size of the thread pool used for delete file actions (by default, no thread pool is used) | +| `stream_results` | | boolean | When true, orphan files will be sent to Spark driver by RDD partition (by default, all the files will be sent to Spark driver). This option is recommended to set to `true` to prevent Spark driver OOM from large file size. When enabled, the output will contain a sample of up to 20,000 file paths | +| `file_list_view` | | string | Dataset to look for files in (skipping the directory listing) | +| `equal_schemes` | | map | Mapping of file system schemes to be considered equal. Key is a comma-separated list of schemes and value is a scheme (defaults to `map('s3a,s3n','s3')`). | +| `equal_authorities` | | map | Mapping of file system authorities to be considered equal. Key is a comma-separated list of authorities and value is an authority. | +| `prefix_mismatch_mode` | | string | Action behavior when location prefixes (schemes/authorities) mismatch:
  • ERROR - throw an exception. (default)
  • IGNORE - no action.
  • DELETE - delete files.
| +| `prefix_listing` | | boolean | When true, use prefix-based file listing via the `SupportsPrefixOperations` interface. The Table FileIO implementation must support `SupportsPrefixOperations` when this flag is enabled (defaults to false) | + +#### Output + +| Output Name | Type | Description | +| ------------|------|-------------| +| `orphan_file_location` | String | The path to each file determined to be an orphan by this command | + +#### Examples + +List all the files that are candidates for removal by performing a dry run of the `remove_orphan_files` command on this table without actually removing them: +```sql +CALL catalog_name.system.remove_orphan_files(table => 'db.sample', dry_run => true); +``` + +Remove any files in the `tablelocation/data` folder which are not known to the table `db.sample`. +```sql +CALL catalog_name.system.remove_orphan_files(table => 'db.sample', location => 'tablelocation/data'); +``` + +Remove any files in the `files_view` view which are not known to the table `db.sample`. +```java +Dataset compareToFileList = + spark + .createDataFrame(allFiles, FilePathLastModifiedRecord.class) + .withColumnRenamed("filePath", "file_path") + .withColumnRenamed("lastModified", "last_modified"); +String fileListViewName = "files_view"; +compareToFileList.createOrReplaceTempView(fileListViewName); +``` +```sql +CALL catalog_name.system.remove_orphan_files(table => 'db.sample', file_list_view => 'files_view'); +``` + +When a file matches references in metadata files except for location prefix (scheme/authority), an error is thrown by default. +The error can be ignored and the file will be skipped by setting `prefix_mismatch_mode` to `IGNORE`. +```sql +CALL catalog_name.system.remove_orphan_files(table => 'db.sample', prefix_mismatch_mode => 'IGNORE'); +``` + +The file can still be deleted by setting `prefix_mismatch_mode` to `DELETE`. +```sql +CALL catalog_name.system.remove_orphan_files(table => 'db.sample', prefix_mismatch_mode => 'DELETE'); +``` + +The file can also be deleted by considering the mismatched prefixes equal. +```sql +CALL catalog_name.system.remove_orphan_files(table => 'db.sample', equal_schemes => map('file', 'file1')); +``` + +```sql +CALL catalog_name.system.remove_orphan_files(table => 'db.sample', equal_authorities => map('ns1', 'ns2')); +``` + +List all the files that are candidates for removal using prefix listing. +```sql +CALL catalog_name.system.remove_orphan_files(table => 'db.sample', prefix_listing => true); +``` + +### `rewrite_data_files` + +Iceberg tracks each data file in a table. More data files leads to more metadata stored in manifest files, and small data files causes an unnecessary amount of metadata and less efficient queries from file open costs. + +Iceberg can compact data files in parallel using Spark with the `rewriteDataFiles` action. This will combine small files into larger files to reduce metadata overhead and runtime file open cost. + +#### Usage + +| Argument Name | Required? | Type | Description | +|---------------|-----------|------|-------------| +| `table` | ✔️ | string | Name of the table to update | +| `strategy` | | string | Name of the strategy - binpack or sort. Defaults to binpack strategy | +| `sort_order` | | string | For Zorder use a comma separated list of columns within zorder(). Example: zorder(c1,c2,c3).
Else, Comma separated sort orders in the format (ColumnName SortDirection NullOrder).
Where SortDirection can be ASC or DESC. NullOrder can be NULLS FIRST or NULLS LAST.
Defaults to the table's sort order | +| `options` | ️ | map | Options to be used for actions| +| `where` | ️ | string | predicate as a string used for filtering the files. Note that all files that may contain data matching the filter will be selected for rewriting| + +#### Options + +##### General Options +| Name | Default Value | Description | +|------|---------------|-------------| +| `max-concurrent-file-group-rewrites` | 5 | Maximum number of file groups to be simultaneously rewritten | +| `partial-progress.enabled` | false | Enable committing groups of files prior to the entire rewrite completing | +| `partial-progress.max-commits` | 10 | Maximum amount of commits that this rewrite is allowed to produce if partial progress is enabled | +| `partial-progress.max-failed-commits` | value of `partial-progress.max-commits` | Maximum amount of failed commits allowed before job failure, if partial progress is enabled | +| `use-starting-sequence-number` | true | Use the sequence number of the snapshot at compaction start time instead of that of the newly produced snapshot | +| `rewrite-job-order` | none | Force the rewrite job order based on the value.
  • If rewrite-job-order=bytes-asc, then rewrite the smallest job groups first.
  • If rewrite-job-order=bytes-desc, then rewrite the largest job groups first.
  • If rewrite-job-order=files-asc, then rewrite the job groups with the least files first.
  • If rewrite-job-order=files-desc, then rewrite the job groups with the most files first.
  • If rewrite-job-order=none, then rewrite job groups in the order they were planned (no specific ordering).
| +| `target-file-size-bytes` | 536870912 (512 MB, default value of `write.target-file-size-bytes` from [table properties](configuration.md#write-properties)) | Target output file size | +| `min-file-size-bytes` | 75% of target file size | Files under this threshold will be considered for rewriting regardless of any other criteria | +| `max-file-size-bytes` | 180% of target file size | Files with sizes above this threshold will be considered for rewriting regardless of any other criteria | +| `min-input-files` | 5 | Any file group with this number of files or more will be rewritten regardless of other criteria (the file group should have at least two files) | +| `rewrite-all` | false | Force rewriting of all provided files overriding other options | +| `max-file-group-size-bytes` | 107374182400 (100GB) | Largest amount of data that should be rewritten in a single file group. The entire rewrite operation is broken down into pieces based on partitioning and within partitions based on size into file-groups. This helps with breaking down the rewriting of very large partitions which may not be rewritable otherwise due to the resource constraints of the cluster. | +| `delete-file-threshold` | 2147483647 | Minimum number of deletes that needs to be associated with a data file for it to be considered for rewriting | +| `delete-ratio-threshold` | 0.3 | Minimum deletion ratio that needs to be associated with a data file for it to be considered for rewriting | +| `output-spec-id` | current partition spec id | Identifier of the output partition spec. Data will be reorganized during the rewrite to align with the output partitioning. | +| `remove-dangling-deletes` | false | Remove dangling position and equality deletes after rewriting. A delete file is considered dangling if it does not apply to any live data files. Enabling this will generate an additional commit for the removal. | +| `max-files-to-rewrite` | null | This option sets an upper limit on the number of eligible files that will be rewritten. If this option is not specified, all eligible files will be rewritten. | + +!!! info + Dangling delete files are removed based solely on data sequence numbers. This action does not apply to global + equality deletes or invalid equality deletes if their delete conditions do not match any data files, + nor to position delete files containing position deletes no longer matching any live data files. + +##### Options for sort strategy + +| Name | Default Value | Description | +|------|---------------|-------------| +| `compression-factor` | 1.0 | The number of shuffle partitions and consequently the number of output files created by the Spark sort is based on the size of the input data files used in this file rewriter. Due to compression, the disk file sizes may not accurately represent the size of files in the output. This parameter lets the user adjust the file size used for estimating actual output data size. A factor greater than 1.0 would generate more files than we would expect based on the on-disk file size. A value less than 1.0 would create fewer files than we would expect based on the on-disk size. | +| `shuffle-partitions-per-file` | 1 | Number of shuffle partitions to use for each output file. Iceberg will use a custom coalesce operation to stitch these sorted partitions back together into a single sorted file. | + +##### Options for sort strategy with zorder sort_order + +| Name | Default Value | Description | +|------|---------------|-------------| +| `var-length-contribution` | 8 | Number of bytes considered from an input column of a type with variable length (String, Binary) | +| `max-output-size` | 2147483647 | Amount of bytes interleaved in the ZOrder algorithm | + +#### Output + +| Output Name | Type | Description | +| ------------|------|-------------| +| `rewritten_data_files_count` | int | Number of data which were re-written by this command | +| `added_data_files_count` | int | Number of new data files which were written by this command | +| `rewritten_bytes_count` | long | Number of bytes which were written by this command | +| `failed_data_files_count` | int | Number of data files that failed to be rewritten when `partial-progress.enabled` is true | +| `removed_delete_files_count` | int | Number of delete files removed by this command | + +#### Examples + +Rewrite the data files in table `db.sample` using the default rewrite algorithm of bin-packing to combine small files +and also split large files according to the default write size of the table. +```sql +CALL catalog_name.system.rewrite_data_files('db.sample'); +``` + +Rewrite the data files in table `db.sample` by sorting all the data on id and name +using the same defaults as bin-pack to determine which files to rewrite. +```sql +CALL catalog_name.system.rewrite_data_files(table => 'db.sample', strategy => 'sort', sort_order => 'id DESC NULLS LAST,name ASC NULLS FIRST'); +``` + +Rewrite the data files in table `db.sample` by zOrdering on column c1 and c2. +Using the same defaults as bin-pack to determine which files to rewrite. +```sql +CALL catalog_name.system.rewrite_data_files(table => 'db.sample', strategy => 'sort', sort_order => 'zorder(c1,c2)'); +``` + +Rewrite the data files in table `db.sample` using bin-pack strategy in any partition where at least two files need rewriting, and then remove any dangling delete files. +```sql +CALL catalog_name.system.rewrite_data_files(table => 'db.sample', options => map('min-input-files', '2', 'remove-dangling-deletes', 'true')); +``` + +Rewrite the data files in table `db.sample` and select the files that may contain data matching the filter (id = 3 and name = "foo") to be rewritten. +```sql +CALL catalog_name.system.rewrite_data_files(table => 'db.sample', where => 'id = 3 and name = "foo"'); +``` + +### `rewrite_manifests` + +Rewrite manifests for a table to optimize scan planning. + +Data files in manifests are sorted by fields in the partition spec. This procedure runs in parallel using a Spark job. + +!!! info + This procedure invalidates all cached Spark plans that reference the affected table. + +#### Usage + +| Argument Name | Required? | Type | Description | +|---------------|-----------|------|---------------------------------------------------------------| +| `table` | ✔️ | string | Name of the table to update | +| `use_caching` | ️ | boolean | Use Spark caching during operation (defaults to false). Enabling caching can increase memory footprint on executors. | +| `spec_id` | ️ | int | Spec id of the manifests to rewrite (defaults to current spec id) | +| `sort_by` | ️ | array | List of partition transform names to cluster manifests by. Choosing frequently queried partition transforms can reduce planning time by skipping unnecessary manifests. If not set, manifests will be sorted by all partition transforms in spec order. | + +#### Output + +| Output Name | Type | Description | +| ------------|------|-------------| +| `rewritten_manifests_count` | int | Number of manifests which were re-written by this command | +| `added_manifests_count` | int | Number of new manifest files which were written by this command | + +#### Examples + +Rewrite the manifests in table `db.sample` and align manifest files with table partitioning. +```sql +CALL catalog_name.system.rewrite_manifests('db.sample'); +``` + +Rewrite the manifests on the partition spec `1` in table `db.sample`. +```sql +CALL catalog_name.system.rewrite_manifests(table => 'db.sample', spec_id => 1); +``` + +Rewrite the manifests in table `db.sample` and cluster manifest entries by partition field `category`. +This can improve scan planning performance when queries frequently filter on `category`. +```sql +CALL catalog_name.system.rewrite_manifests(table => 'db.sample', sort_by => array('category')); +``` + +### `rewrite_position_delete_files` + +Iceberg can rewrite position delete files, which serves two purposes: + +* Minor Compaction: Compact small position delete files into larger ones. This reduces the size of metadata stored in manifest files and overhead of opening small delete files. +* Remove Dangling Deletes: Filter out position delete records that refer to data files that are no longer live. After rewrite_data_files, position delete records pointing to the rewritten data files are not always marked for removal, and can remain tracked by the table's live snapshot metadata. This is known as the 'dangling delete' problem. + +#### Usage + +| Argument Name | Required? | Type | Description | +|---------------|-----------|------|----------------------------------| +| `table` | ✔️ | string | Name of the table to update | +| `options` | ️ | map | Options to be used for procedure | +| `where` | ️ | string | predicate as a string used for filtering the files. | + +Dangling deletes are always filtered out during rewriting. + +#### Options + +| Name | Default Value | Description | +|------|---------------|-------------| +| `max-concurrent-file-group-rewrites` | 5 | Maximum number of file groups to be simultaneously rewritten | +| `partial-progress.enabled` | false | Enable committing groups of files prior to the entire rewrite completing | +| `partial-progress.max-commits` | 10 | Maximum amount of commits that this rewrite is allowed to produce if partial progress is enabled | +| `rewrite-job-order` | none | Force the rewrite job order based on the value.
  • If rewrite-job-order=bytes-asc, then rewrite the smallest job groups first.
  • If rewrite-job-order=bytes-desc, then rewrite the largest job groups first.
  • If rewrite-job-order=files-asc, then rewrite the job groups with the least files first.
  • If rewrite-job-order=files-desc, then rewrite the job groups with the most files first.
  • If rewrite-job-order=none, then rewrite job groups in the order they were planned (no specific ordering).
| +| `target-file-size-bytes` | 67108864 (64MB, default value of `write.delete.target-file-size-bytes` from [table properties](configuration.md#write-properties)) | Target output file size | +| `min-file-size-bytes` | 75% of target file size | Files under this threshold will be considered for rewriting regardless of any other criteria | +| `max-file-size-bytes` | 180% of target file size | Files with sizes above this threshold will be considered for rewriting regardless of any other criteria | +| `min-input-files` | 5 | Any file group exceeding this number of files will be rewritten regardless of other criteria | +| `rewrite-all` | false | Force rewriting of all provided files overriding other options | +| `max-file-group-size-bytes` | 107374182400 (100GB) | Largest amount of data that should be rewritten in a single file group. The entire rewrite operation is broken down into pieces based on partitioning and within partitions based on size into file-groups. This helps with breaking down the rewriting of very large partitions which may not be rewritable otherwise due to the resource constraints of the cluster. | +| `max-files-to-rewrite` | null | This option sets an upper limit on the number of eligible files that will be rewritten. If this option is not specified, all eligible files will be rewritten. | + +#### Output + +| Output Name | Type | Description | +|--------------------------------|------|----------------------------------------------------------------------------| +| `rewritten_delete_files_count` | int | Number of delete files which were removed by this command | +| `added_delete_files_count` | int | Number of delete files which were added by this command | +| `rewritten_bytes_count` | long | Count of bytes across delete files which were removed by this command | +| `added_bytes_count` | long | Count of bytes across all new delete files which were added by this command | + +#### Examples + +Rewrite position delete files in table `db.sample`. This selects position delete files that fit default rewrite criteria, and writes new files of target size `target-file-size-bytes`. Dangling deletes are removed from rewritten delete files. +```sql +CALL catalog_name.system.rewrite_position_delete_files('db.sample'); +``` + +Rewrite all position delete files in table `db.sample`, writing new files `target-file-size-bytes`. Dangling deletes are removed from rewritten delete files. +```sql +CALL catalog_name.system.rewrite_position_delete_files(table => 'db.sample', options => map('rewrite-all', 'true')); +``` + +Rewrite position delete files in table `db.sample`. This selects position delete files in partitions where 2 or more position delete files need to be rewritten based on size criteria. Dangling deletes are removed from rewritten delete files. +```sql +CALL catalog_name.system.rewrite_position_delete_files(table => 'db.sample', options => map('min-input-files','2')); +``` + +## Table migration + +The `snapshot` and `migrate` procedures help test and migrate existing Hive or Spark tables to Iceberg. + +### `snapshot` + +Create a light-weight temporary copy of a table for testing, without changing the source table. + +The newly created table can be changed or written to without affecting the source table, but the snapshot uses the original table's data files. + +When inserts or overwrites run on the snapshot, new files are placed in the snapshot table's location rather than the original table location. + +When finished testing a snapshot table, clean it up by running `DROP TABLE`. + +!!! info + Because tables created by `snapshot` are not the sole owners of their data files, they are prohibited from + actions like `expire_snapshots` which would physically delete data files. Iceberg deletes, which only effect metadata, + are still allowed. In addition, any operations which affect the original data files will disrupt the Snapshot's + integrity. DELETE statements executed against the original Hive table will remove original data files and the + `snapshot` table will no longer be able to access them. + +See [`migrate`](#migrate) to replace an existing table with an Iceberg table. + +#### Usage + +| Argument Name | Required? | Type | Description | +|---------------|-----------|------|-------------| +| `source_table`| ✔️ | string | Name of the table to snapshot | +| `table` | ✔️ | string | Name of the new Iceberg table to create | +| `location` | | string | Table location for the new table (delegated to the catalog by default) | +| `properties` | ️ | map | Properties to add to the newly created table | +| `parallelism` | | int | Number of threads to use for file reading (defaults to 1) | + +#### Output + +| Output Name | Type | Description | +| ------------|------|-------------| +| `imported_files_count` | long | Number of files added to the new table | + +#### Examples + +Make an isolated Iceberg table which references table `db.sample` named `db.snap` at the +catalog's default location for `db.snap`. +```sql +CALL catalog_name.system.snapshot('db.sample', 'db.snap'); +``` + +Migrate an isolated Iceberg table which references table `db.sample` named `db.snap` at +a manually specified location `/tmp/temptable/`. +```sql +CALL catalog_name.system.snapshot('db.sample', 'db.snap', '/tmp/temptable/'); +``` + +### `migrate` + +Replace a table with an Iceberg table, loaded with the source's data files. + +Table schema, partitioning, properties, and location will be copied from the source table. + +Migrate will fail if any table partition uses an unsupported format. Supported formats are Avro, Parquet, and ORC. +Migrate will also fail if the table is bucketed, as the bucketing will not be preserved. +Existing data files are added to the Iceberg table's metadata and can be read using a name-to-id mapping created from the original table schema. + +To leave the original table intact while testing, use [`snapshot`](#snapshot) to create new temporary table that shares source data files and schema. + +By default, the original table is retained with the name `table_BACKUP_`. + +#### Usage + +| Argument Name | Required? | Type | Description | +|---------------|-----------|------|-------------| +| `table` | ✔️ | string | Name of the table to migrate | +| `properties` | ️ | map | Properties for the new Iceberg table | +| `drop_backup` | | boolean | When true, the original table will not be retained as backup (defaults to false) | +| `backup_table_name` | | string | Name of the table that will be retained as backup (defaults to `table_BACKUP_`) | +| `parallelism` | | int | Number of threads to use for file reading (defaults to 1) | + +#### Output + +| Output Name | Type | Description | +| ------------|------|-------------| +| `migrated_files_count` | long | Number of files appended to the Iceberg table | + +#### Examples + +Migrate the table `db.sample` in Spark's default catalog to an Iceberg table and add a property 'foo' set to 'bar': + +```sql +CALL catalog_name.system.migrate('spark_catalog.db.sample', map('foo', 'bar')); +``` + +Migrate `db.sample` in the current catalog to an Iceberg table without adding any additional properties: +```sql +CALL catalog_name.system.migrate('db.sample'); +``` + +### `add_files` + +Attempts to directly add files from a Hive or file based table into a given Iceberg table. Unlike migrate or +snapshot, `add_files` can import files from a specific partition or partitions and does not create a new Iceberg table. +This command will create metadata for the new files and will not move them. This procedure will not analyze the schema +of the files to determine if they actually match the schema of the Iceberg table. Upon completion, the Iceberg table +will then treat these files as if they are part of the set of files owned by Iceberg. This means any subsequent +`expire_snapshot` calls will be able to physically delete the added files. This method should not be used if +`migrate` or `snapshot` are possible. + +!!! warning + Keep in mind the `add_files` procedure will fetch the Parquet metadata from each file being added just once. If you're using tiered storage, (such as [Amazon S3 Intelligent-Tiering storage class](https://aws.amazon.com/s3/storage-classes/intelligent-tiering/)), the underlying, file will be retrieved from the archive, and will remain on a higher tier for a set period of time. + +#### Usage + +| Argument Name | Required? | Type | Description | +|-------------------------|-----------|---------------------|-----------------------------------------------------------------------------------------------------| +| `table` | ✔️ | string | Table which will have files added to | +| `source_table` | ✔️ | string | Table where files should come from, paths are also possible in the form of \`file_format\`.\`path\` | +| `partition_filter` | ️ | map | A map of partitions in the source table to import from | +| `check_duplicate_files` | ️ | boolean | Whether to prevent files existing in the table from being added (defaults to true) | +| `parallelism` | | int | Number of threads to use for file reading (defaults to 1) | + +Warning : Schema is not validated, adding files with different schema to the Iceberg table will cause issues. + +Warning : Files added by this method can be physically deleted by Iceberg operations + +#### Output + +| Output Name | Type | Description | +|---------------------------|------|---------------------------------------------------| +| `added_files_count` | long | The number of files added by this command | +| `changed_partition_count` | long | The number of partitioned changed by this command (if known) | + +!!! warning + changed_partition_count will be NULL when table property `compatibility.snapshot-id-inheritance.enabled` is set to true or if the table format version is > 1. + +#### Examples + +Add the files from table `db.src_table`, a Hive or Spark table registered in the session Catalog, to Iceberg table +`db.tbl`. Only add files that exist within partitions where `part_col_1` is equal to `A`. +```sql +CALL spark_catalog.system.add_files( + table => 'db.tbl', + source_table => 'db.src_tbl', + partition_filter => map('part_col_1', 'A') +); +``` + +Add files from a `parquet` file based table at location `path/to/table` to the Iceberg table `db.tbl`. Add all +files regardless of what partition they belong to. +```sql +CALL spark_catalog.system.add_files( + table => 'db.tbl', + source_table => '`parquet`.`path/to/table`' +); +``` + +### `register_table` + +Creates a catalog entry for a metadata.json file which already exists but does not have a corresponding catalog identifier. + +#### Usage + +| Argument Name | Required? | Type | Description | +|---------------|-----------|------|-------------| +| `table` | ✔️ | string | Table which is to be registered | +| `metadata_file`| ✔️ | string | Metadata file which is to be registered as a new catalog identifier | + +!!! warning + Having the same metadata.json registered in more than one catalog can lead to missing updates, loss of data, and table corruption. + Only use this procedure when the table is no longer registered in an existing catalog, or you are moving a table between catalogs. + +#### Output + +| Output Name | Type | Description | +| ------------|------|-------------| +| `current_snapshot_id` | long | The current snapshot ID of the newly registered Iceberg table | +| `total_records_count` | long | Total records count of the newly registered Iceberg table | +| `total_data_files_count` | long | Total data files count of the newly registered Iceberg table | + +#### Examples + +Register a new table as `db.tbl` to `spark_catalog` pointing to metadata.json file `path/to/metadata/file.json`. +```sql +CALL spark_catalog.system.register_table( + table => 'db.tbl', + metadata_file => 'path/to/metadata/file.json' +); +``` + +## Metadata information + +### `ancestors_of` + +Report the live snapshot IDs of parents of a specified snapshot + +#### Usage + +| Argument Name | Required? | Type | Description | +|---------------|-----------|------|-------------| +| `table` | ✔️ | string | Name of the table to report live snapshot IDs | +| `snapshot_id` | ️ | long | Use a specified snapshot to get the live snapshot IDs of parents | + +> tip : Using snapshot_id +> +> Given snapshots history with roll back to B and addition of C' -> D' +> ```shell +> A -> B - > C -> D +> \ -> C' -> (D') +> ``` +> Not specifying the snapshot ID would return A -> B -> C' -> D', while providing the snapshot ID of +> D as an argument would return A-> B -> C -> D + +#### Output + +| Output Name | Type | Description | +| ------------|------|-------------| +| `snapshot_id` | long | the ancestor snapshot id | +| `timestamp` | long | snapshot creation time | + +#### Examples + +Get all the snapshot ancestors of current snapshots(default) +```sql +CALL spark_catalog.system.ancestors_of('db.tbl'); +``` + +Get all the snapshot ancestors by a particular snapshot +```sql +CALL spark_catalog.system.ancestors_of('db.tbl', 1); +CALL spark_catalog.system.ancestors_of(snapshot_id => 1, table => 'db.tbl'); +``` + +## Change Data Capture + +### `create_changelog_view` + +Creates a view that contains the changes from a given table. + +#### Usage + +| Argument Name | Required? | Type | Description | +|----------------------|-----------|---------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `table` | ✔️ | string | Name of the source table for the changelog | +| `changelog_view` | | string | Name of the view to create | +| `options` | | map | A map of Spark read options to use | +| `net_changes` | | boolean | Whether to output net changes (see below for more information). Defaults to false. It must be false when `compute_updates` is true. | +| `compute_updates` | | boolean | Whether to compute pre/post update images (see below for more information). Defaults to true if `identifer_columns` are provided; otherwise, defaults to false. | +| `identifier_columns` | | array | The list of identifier columns to compute updates. If the argument `compute_updates` is set to true and `identifier_columns` are not provided, the table’s current identifier fields will be used. | + +Here is a list of commonly used Spark read options: + +* `start-snapshot-id`: the exclusive start snapshot ID. If not provided, it reads from the table’s first snapshot inclusively. +* `end-snapshot-id`: the inclusive end snapshot id, default to table's current snapshot. +* `start-timestamp`: the exclusive start timestamp. If not provided, it reads from the table’s first snapshot inclusively. +* `end-timestamp`: the inclusive end timestamp, default to table's current snapshot. + +#### Output +| Output Name | Type | Description | +| ------------|------|----------------------------------------| +| `changelog_view` | string | The name of the created changelog view | + +#### Examples + +Create a changelog view `tbl_changes` based on the changes that happened between snapshot `1` (exclusive) and `2` (inclusive). +```sql +CALL spark_catalog.system.create_changelog_view( + table => 'db.tbl', + options => map('start-snapshot-id','1','end-snapshot-id', '2') +); +``` + +Create a changelog view `my_changelog_view` based on the changes that happened between timestamp `1678335750489` (exclusive) and `1678992105265` (inclusive). +```sql +CALL spark_catalog.system.create_changelog_view( + table => 'db.tbl', + options => map('start-timestamp','1678335750489','end-timestamp', '1678992105265'), + changelog_view => 'my_changelog_view' +); +``` + +Create a changelog view that computes updates based on the identifier columns `id` and `name`. +```sql +CALL spark_catalog.system.create_changelog_view( + table => 'db.tbl', + options => map('start-snapshot-id','1','end-snapshot-id', '2'), + identifier_columns => array('id', 'name') +); +``` + +Once the changelog view is created, you can query the view to see the changes that happened between the snapshots. +```sql +SELECT * FROM tbl_changes; +``` +```sql +SELECT * FROM tbl_changes where _change_type = 'INSERT' AND id = 3 ORDER BY _change_ordinal; +``` +Please note that the changelog view includes Change Data Capture(CDC) metadata columns +that provide additional information about the changes being tracked. These columns are: + +- `_change_type`: the type of change. It has one of the following values: `INSERT`, `DELETE`, `UPDATE_BEFORE`, or `UPDATE_AFTER`. +- `_change_ordinal`: the order of changes +- `_commit_snapshot_id`: the snapshot ID where the change occurred + +Here is an example of corresponding results. It shows that the first snapshot inserted 2 records, and the +second snapshot deleted 1 record. + +| id | name |_change_type | _change_ordinal | _commit_snapshot_id | +|---|--------|---|---|---| +|1 | Alice |INSERT |0 |5390529835796506035| +|2 | Bob |INSERT |0 |5390529835796506035| +|1 | Alice |DELETE |1 |8764748981452218370| + +#### Net Changes + +The procedure can remove intermediate changes across multiple snapshots, and only outputs the net changes. Here is an example to create a changelog view that computes net changes. + +```sql +CALL spark_catalog.system.create_changelog_view( + table => 'db.tbl', + options => map('end-snapshot-id', '87647489814522183702'), + net_changes => true +); +``` + +With the net changes, the above changelog view only contains the following row since Alice was inserted in the first snapshot and deleted in the second snapshot. + +| id | name |_change_type | _change_ordinal | _commit_snapshot_id | +|---|--------|---|---|---| +|2 | Bob |INSERT |0 |5390529835796506035| + +#### Carry-over Rows + +The procedure removes the carry-over rows by default. Carry-over rows are the result of row-level operations(`MERGE`, `UPDATE` and `DELETE`) +when using copy-on-write. For example, given a file which contains row1 `(id=1, name='Alice')` and row2 `(id=2, name='Bob')`. +A copy-on-write delete of row2 would require erasing this file and preserving row1 in a new file. The changelog table +reports this as the following pair of rows, despite it not being an actual change to the table. + +| id | name | _change_type | +|-----|-------|--------------| +| 1 | Alice | DELETE | +| 1 | Alice | INSERT | + +To see carry-over rows, query `SparkChangelogTable` as follows: +```sql +SELECT * FROM spark_catalog.db.tbl.changes; +``` + +#### Pre/Post Update Images + +The procedure computes the pre/post update images if configured. Pre/post update images are converted from a +pair of a delete row and an insert row. Identifier columns are used for determining whether an insert and a delete record +refer to the same row. If the two records share the same values for the identity columns they are considered to be before +and after states of the same row. You can either set identifier fields in the table schema or input them as the procedure parameters. + +The following example shows pre/post update images computation with an identifier column(`id`), where a row deletion +and an insertion with the same `id` are treated as a single update operation. Specifically, suppose we have the following pair of rows: + +| id | name | _change_type | +|-----|--------|--------------| +| 3 | Robert | DELETE | +| 3 | Dan | INSERT | + +In this case, the procedure marks the row before the update as an `UPDATE_BEFORE` image and the row after the update +as an `UPDATE_AFTER` image, resulting in the following pre/post update images: + +| id | name | _change_type | +|-----|--------|--------------| +| 3 | Robert | UPDATE_BEFORE| +| 3 | Dan | UPDATE_AFTER | + +## Table Statistics + +### `compute_table_stats` + +This procedure calculates the [Number of Distinct Values (NDV) statistics](../../puffin-spec.md#apache-datasketches-theta-v1-blob-type) for a specific table. +By default, statistics are computed for all columns using the table's current snapshot. +The procedure can be optionally configured to compute statistics for a specific snapshot and/or a subset of columns. + +| Argument Name | Required? | Type | Description | +|---------------|-----------|---------------|-------------------------------------| +| `table` | ✔️ | string | Name of the table | +| `snapshot_id` | | string | Id of the snapshot to collect stats | +| `columns` | | array | Columns to collect stats | + +#### Output + +| Output Name | Type | Description | +|-------------------|--------|-------------------------------------------------| +| `statistics_file` | string | Path to stats file created from by this command | + +#### Examples + +Collect statistics of the latest snapshot of table `my_table` +```sql +CALL catalog_name.system.compute_table_stats('my_table'); +``` + +Collect statistics of the snapshot with id `snap1` of table `my_table` +```sql +CALL catalog_name.system.compute_table_stats(table => 'my_table', snapshot_id => 'snap1' ); +``` + +Collect statistics of the snapshot with id `snap1` of table `my_table` for columns `col1` and `col2` +```sql +CALL catalog_name.system.compute_table_stats(table => 'my_table', snapshot_id => 'snap1', columns => array('col1', 'col2')); +``` + +## Partition Statistics + +### `compute_partition_stats` + +This procedure computes the [partition stats](../../spec.md#partition-statistics) incrementally from the last snapshot that has a `PartitionStatisticsFile` +until the given snapshot (uses current snapshot if not specified) and writes the combined result into a `PartitionStatisticsFile`. +It performs a full compute if the previous partition statistics file does not exist. It also registers the +`PartitionStatisticsFile` to the table metadata. + +| Argument Name | Required? | Type | Description | +|---------------|-----------|---------------|--------------------------------------------------------------------------------| +| `table` | ✔️ | string | Name of the table | +| `snapshot_id` | | string | Id of the snapshot to compute partition stats. Defaults to current snapshot id | + +#### Output + +| Output Name | Type | Description | +|-------------------|--------|----------------------------------------------------------| +| `partition_statistics_file` | string | Path to the partition stats file created from by command | + +#### Examples + +Collect partition statistics of the latest snapshot of table `my_table` +```sql +CALL catalog_name.system.compute_partition_stats('my_table'); +``` + +Collect partition statistics of the snapshot with id `snap1` of table `my_table` +```sql +CALL catalog_name.system.compute_partition_stats(table => 'my_table', snapshot_id => 'snap1'); +``` + +## Table Replication + +The `rewrite_table_path` procedure prepares an Iceberg table for copying to another location. + +### `rewrite_table_path` + +Stages a copy of the Iceberg table's metadata files where every absolute path source prefix is replaced by the specified target prefix. +This can be the starting point to fully or incrementally copy an Iceberg table to a new location. + +!!! info + This procedure only stages rewritten metadata files and prepares a list of files to copy. The actual file copy is not included in this procedure. + +| Argument Name | Required? | default | Type | Description | +|--------------------|-----------|------------------------------------------------|--------|------------------------------------------------------------------------| +| `table` | ✔️ | | string | Name of the table | +| `source_prefix` | ✔️ | | string | The existing prefix to be replaced | +| `target_prefix` | ✔️ | | string | The replacement prefix for `source_prefix` | +| `start_version` | | first metadata.json in table's metadata log | string | The name or path of the chronologically first metadata.json to rewrite | +| `end_version` | | latest metadata.json in table's metadata log | string | The name or path of the chronologically last metadata.json to rewrite | +| `staging_location` | | new directory under table's metadata directory | string | The output location for newly rewritten metadata files | +| `create_file_list` | | true | boolean | Whether to generate a file list containing the paths of rewritten metadata | + +#### Modes of operation + +* Full Rewrite: A full rewrite will rewrite all reachable metadata files (this includes metadata.json, manifest lists, manifests, and position delete files), and will return all reachable files in the `file_list_location`. This is the default mode of operation for this procedure. + +* Incremental Rewrite: Optionally, `start_version` and `end_version` can be provided to limit the scope to an incremental rewrite. An incremental rewrite will only rewrite metadata files added between `start_version` and `end_version`, and will only return files added in this range in the `file_list_location`. + +#### Output + +| Output Name | Type | Description | +|----------------------|--------|-------------------------------------------------------------------| +| `latest_version` | string | Name of the latest metadata file rewritten by this procedure | +| `file_list_location` | string | Path to a CSV file containing a mapping of source to target paths | +| `rewritten_manifest_file_paths_count` | int | Number of manifest files with rewritten paths | +| `rewritten_delete_file_paths_count` | int | Number of delete files with rewritten paths | + +##### File List +The file contains the copy plan for all files added to the table between `start_version` and `end_version`. + +For each file, it specifies: + +* Source Path: The original file path in the table, or the staging location if the file has been rewritten + +* Target Path: The path with the replacement prefix + +The following example shows a copy plan for three files: + +```csv +sourcepath/datafile1.parquet,targetpath/datafile1.parquet +sourcepath/datafile2.parquet,targetpath/datafile2.parquet +stagingpath/manifest.avro,targetpath/manifest.avro +``` + +#### Examples + +This example fully rewrites metadata paths of `my_table` from source location in HDFS to a target location in S3. +It will produce a new set of metadata in the default staging location under the table's metadata directory. + +```sql +CALL catalog_name.system.rewrite_table_path( + table => 'db.my_table', + source_prefix => 'hdfs://nn:8020/path/to/source_table', + target_prefix => 's3a://bucket/prefix/db.db/my_table' +); +``` + +This example incrementally rewrites metadata paths of `my_table` between metadata versions `v2.metadata.json` and `v20.metadata.json`, +with new metadata files written to an explicit staging location. + +```sql +CALL catalog_name.system.rewrite_table_path( + table => 'db.my_table', + source_prefix => 's3a://bucketOne/prefix/db.db/my_table', + target_prefix => 's3a://bucketTwo/prefix/db.db/my_table', + start_version => 'v2.metadata.json', + end_version => 'v20.metadata.json', + staging_location => 's3a://bucketStaging/my_table' +); +``` + +Once the rewrite completes, third-party tools ( +eg. [Distcp](https://hadoop.apache.org/docs/current/hadoop-distcp/DistCp.html)) can copy the newly created +metadata files and data files to the target location. + +Lastly, the [register_table](#register_table) procedure can be used to register the copied table in the target location with a catalog. + +!!! warning + Iceberg tables with partition statistics files are not currently supported for path rewrite. diff --git a/1.11.0/docs/spark-queries.md b/1.11.0/docs/spark-queries.md new file mode 100644 index 000000000000..91f1759f568c --- /dev/null +++ b/1.11.0/docs/spark-queries.md @@ -0,0 +1,622 @@ +--- +title: "Queries" +--- + + +# Spark Queries + +To use Iceberg in Spark, first configure [Spark catalogs](spark-configuration.md). Iceberg uses Apache Spark's DataSourceV2 API for data source and catalog implementations. + +## Querying with SQL + +In Spark, tables use identifiers that include a [catalog name](spark-configuration.md#using-catalogs). + +```sql +SELECT * FROM prod.db.table; -- catalog: prod, namespace: db, table: table +``` + +Metadata tables, like `history` and `snapshots`, can use the Iceberg table name as a namespace. + +For example, to read from the `files` metadata table for `prod.db.table`: + +```sql +SELECT * FROM prod.db.table.files; +``` + +|content|file_path |file_format|spec_id|partition|record_count|file_size_in_bytes|column_sizes |value_counts |null_value_counts|nan_value_counts|lower_bounds |upper_bounds |key_metadata|split_offsets|equality_ids|sort_order_id| +| -- | -- | -- | -- | -- | -- | -- | -- | -- | -- | -- | -- | -- | -- | -- | -- | -- | +| 0 | s3:/.../table/data/00000-3-8d6d60e8-d427-4809-bcf0-f5d45a4aad96.parquet | PARQUET | 0 | {1999-01-01, 01} | 1 | 597 | [1 -> 90, 2 -> 62] | [1 -> 1, 2 -> 1] | [1 -> 0, 2 -> 0] | [] | [1 -> , 2 -> c] | [1 -> , 2 -> c] | null | [4] | null | null | +| 0 | s3:/.../table/data/00001-4-8d6d60e8-d427-4809-bcf0-f5d45a4aad96.parquet | PARQUET | 0 | {1999-01-01, 02} | 1 | 597 | [1 -> 90, 2 -> 62] | [1 -> 1, 2 -> 1] | [1 -> 0, 2 -> 0] | [] | [1 -> , 2 -> b] | [1 -> , 2 -> b] | null | [4] | null | null | +| 0 | s3:/.../table/data/00002-5-8d6d60e8-d427-4809-bcf0-f5d45a4aad96.parquet | PARQUET | 0 | {1999-01-01, 03} | 1 | 597 | [1 -> 90, 2 -> 62] | [1 -> 1, 2 -> 1] | [1 -> 0, 2 -> 0] | [] | [1 -> , 2 -> a] | [1 -> , 2 -> a] | null | [4] | null | null | + +### Spark SQL functions + +Iceberg adds SQL functions to each Iceberg catalog for inspecting transform results in queries and for +writing filters that match Iceberg partition transforms. These functions are available only through an +[Iceberg catalog](spark-configuration.md#catalog-configuration); they are not registered in Spark's +built-in catalog. + +!!! note + Spark before 4.2.0 does not support `V2Function` in the session catalog. + Queries such as `SELECT spark_catalog.system.bucket(16, id)` fail even when + `spark_catalog` is configured with `org.apache.iceberg.spark.SparkSessionCatalog`. + See [SPARK-54760](https://issues.apache.org/jira/browse/SPARK-54760) ([apache/spark#53531](https://github.com/apache/spark/pull/53531)) for details. + To use Iceberg SQL functions, call them through a catalog configured with + `org.apache.iceberg.spark.SparkCatalog`. + +Use the `system` namespace when calling these functions: + +```sql +SELECT system.iceberg_version(); + +SELECT system.bucket(16, id), system.days(ts) +FROM prod.db.table; +``` + +When you want to be explicit about the catalog, qualify the function with the catalog name: + +```sql +SELECT prod.system.bucket(16, id) +FROM prod.db.table; +``` + +!!! info + `PARTITIONED BY` clauses use singular transform expressions such as `year(ts)` and `month(ts)`. + The SQL functions use `system.years(ts)` and `system.months(ts)`. + +| Function | Supported input types | Return type | Example | +| --- | --- | --- | --- | +| `system.iceberg_version()` | none | `string` | `SELECT system.iceberg_version();` | +| `system.bucket(numBuckets, col)` | `date`, `tinyint`, `smallint`, `int`, `bigint`, `timestamp`, `timestamp_ntz`, `decimal`, `string`, `binary` | `int` | `SELECT system.bucket(16, id) FROM prod.db.table;` | +| `system.years(col)` | `date`, `timestamp`, `timestamp_ntz` | `int` | `SELECT system.years(ts) FROM prod.db.table;` | +| `system.months(col)` | `date`, `timestamp`, `timestamp_ntz` | `int` | `SELECT system.months(ts) FROM prod.db.table;` | +| `system.days(col)` | `date`, `timestamp`, `timestamp_ntz` | `date` | `SELECT * FROM prod.db.table WHERE system.days(ts) = date('2025-03-01');` | +| `system.hours(col)` | `timestamp`, `timestamp_ntz` | `int` | `SELECT system.hours(ts) FROM prod.db.table;` | +| `system.truncate(width, col)` | `tinyint`, `smallint`, `int`, `bigint`, `decimal`, `string`, `binary` | same type as `col` | `SELECT system.truncate(4, data) FROM prod.db.table;` | + +All transform functions return `NULL` for `NULL` inputs. + +`system.years`, `system.months`, `system.days`, and `system.hours` return Iceberg transform values +rather than extracted calendar fields. For example, `system.years` returns years since 1970-01-01, +`system.months` returns months since 1970-01, and `system.hours` returns hours since +1970-01-01T00:00. `system.days` returns a `date` value representing the date part of the input +(for `date` inputs it returns the same value unchanged; for timestamps it discards the time component). + +For numeric inputs, `system.truncate(width, col)` rounds down to the nearest multiple of `width`. +For `string` and `binary` inputs, it keeps the first `width` characters or bytes. + +These functions are especially useful when you want to inspect how Iceberg transforms values or +when writing filters for queries and row-level operations that align with partition transforms. + +### Time travel Queries with SQL +Spark supports time travel in SQL queries using `TIMESTAMP AS OF` or `VERSION AS OF` clauses. +The `VERSION AS OF` clause can contain a long snapshot ID or a string branch or tag name. + +!!! info + Note: If the name of a branch or tag is the same as a snapshot ID, then the snapshot which is selected for time travel is the snapshot + with the given snapshot ID. For example, consider the case where there is a tag named '1' and it references snapshot with ID 2. + If the version travel clause is `VERSION AS OF '1'`, time travel will be done to the snapshot with ID 1. + If this is not desired, rename the tag or branch with a well-defined prefix such as 'snapshot-1'. + +```sql +-- time travel to October 26, 1986 at 01:21:00 +SELECT * FROM prod.db.table TIMESTAMP AS OF '1986-10-26 01:21:00'; + +-- time travel to snapshot with id 10963874102873L +SELECT * FROM prod.db.table VERSION AS OF 10963874102873; + +-- time travel to the head snapshot of audit-branch +SELECT * FROM prod.db.table VERSION AS OF 'audit-branch'; + +-- time travel to the snapshot referenced by the tag historical-snapshot +SELECT * FROM prod.db.table VERSION AS OF 'historical-snapshot'; +``` + +In addition, `FOR SYSTEM_TIME AS OF` and `FOR SYSTEM_VERSION AS OF` clauses are also supported: + +```sql +SELECT * FROM prod.db.table FOR SYSTEM_TIME AS OF '1986-10-26 01:21:00'; +SELECT * FROM prod.db.table FOR SYSTEM_VERSION AS OF 10963874102873; +SELECT * FROM prod.db.table FOR SYSTEM_VERSION AS OF 'audit-branch'; +SELECT * FROM prod.db.table FOR SYSTEM_VERSION AS OF 'historical-snapshot'; +``` + +Timestamps may also be supplied as a Unix timestamp, in seconds: + +```sql +-- timestamp in seconds +SELECT * FROM prod.db.table TIMESTAMP AS OF 499162860; +SELECT * FROM prod.db.table FOR SYSTEM_TIME AS OF 499162860; +``` + +The branch or tag may also be specified using a similar syntax to metadata tables, with `branch_` or `tag_`: + +```sql +SELECT * FROM prod.db.table.`branch_audit-branch`; +SELECT * FROM prod.db.table.`tag_historical-snapshot`; +``` + +(Identifiers with "-" are not valid, and so must be escaped using back quotes.) + +Note that the identifier with branch or tag may not be used in combination with `VERSION AS OF`. + +#### Schema selection in time travel queries + +The different time travel queries mentioned in the previous section can use either the snapshot's schema or the table's schema: + +```sql +-- time travel to October 26, 1986 at 01:21:00 -> uses the snapshot's schema +SELECT * FROM prod.db.table TIMESTAMP AS OF '1986-10-26 01:21:00'; + +-- time travel to snapshot with id 10963874102873L -> uses the snapshot's schema +SELECT * FROM prod.db.table VERSION AS OF 10963874102873; + +-- time travel to the head of audit-branch -> uses the table's schema +SELECT * FROM prod.db.table VERSION AS OF 'audit-branch'; +SELECT * FROM prod.db.table.`branch_audit-branch`; + +-- time travel to the snapshot referenced by the tag historical-snapshot -> uses the snapshot's schema +SELECT * FROM prod.db.table VERSION AS OF 'historical-snapshot'; +SELECT * FROM prod.db.table.`tag_historical-snapshot`; +``` + +For example, consider a table that evolves its schema over time, and see how each type of time travel query selects its schema: + +```sql +-- snapshot S1: initial schema (id, status) +CREATE TABLE prod.db.orders ( + id BIGINT, + status STRING +) USING iceberg; + +INSERT INTO prod.db.orders VALUES (1, 'NEW'), (2, 'PAID'); + +-- record snapshot S1's snapshot_id and committed_at timestamp +-- e.g. snapshot_id = 101, committed_at = '2025-01-01 10:00:00' + +-- snapshot S2: add a new column "total" and write new data +ALTER TABLE prod.db.orders ADD COLUMN total DOUBLE; + +INSERT INTO prod.db.orders VALUES (3, 'PAID', 100.0); + +-- now S2 is the current snapshot with schema (id, status, total) +``` + +Time travel queries that select a specific snapshot or timestamp use the +snapshot's schema: + +```sql +-- uses the snapshot schema of S1: columns (id, status) +SELECT * FROM prod.db.orders VERSION AS OF 101; + +SELECT * FROM prod.db.orders TIMESTAMP AS OF '2025-01-01 10:00:00'; +``` + +In both queries above, the result only has `id` and `status`. The `total` +column does not exist in the S1 schema and is not visible, even though the +current table schema includes `total`. + +Now create a branch and a tag that both reference S1: + +```sql +-- branch "audit_branch" points to snapshot S1 +ALTER TABLE prod.db.orders CREATE BRANCH audit_branch AS OF VERSION 101; + +-- tag "first_load" also points to snapshot S1 +ALTER TABLE prod.db.orders CREATE TAG first_load AS OF VERSION 101; +``` + +When you query a branch, Spark uses the table's current schema: + +```sql +-- uses the table schema: columns (id, status, total) +SELECT * FROM prod.db.orders VERSION AS OF 'audit_branch'; + +-- equivalent identifier form +SELECT * FROM prod.db.orders.`branch_audit_branch`; +``` + +In these queries, the result has columns `(id, status, total)`. For the rows +from S1, `total` is returned as `NULL` because that column did not exist when +those rows were written. + +When you query a tag, Spark uses the snapshot's schema referenced by the tag: + +```sql +-- uses the snapshot schema of S1: columns (id, status) +SELECT * FROM prod.db.orders VERSION AS OF 'first_load'; + +-- equivalent identifier form +SELECT * FROM prod.db.orders.`tag_first_load`; +``` + +These queries only return `id` and `status`, because tags are bound to a +specific snapshot and use that snapshot's schema, even if the table's current +schema has evolved. + +## Querying with DataFrames + +To load a table as a DataFrame, use `table`: + +```scala +val df = spark.table("prod.db.table") +``` + +### Catalogs with DataFrameReader + +Paths and table names can be loaded with Spark's `DataFrameReader` interface. How tables are loaded depends on how +the identifier is specified. When using `spark.read.format("iceberg").load(table)` or `spark.table(table)` the `table` +variable can take a number of forms as listed below: + +* `file:///path/to/table`: loads a HadoopTable at given path +* `tablename`: loads `currentCatalog.currentNamespace.tablename` +* `catalog.tablename`: loads `tablename` from the specified catalog. +* `namespace.tablename`: loads `namespace.tablename` from current catalog +* `catalog.namespace.tablename`: loads `namespace.tablename` from the specified catalog. +* `namespace1.namespace2.tablename`: loads `namespace1.namespace2.tablename` from current catalog + +The above list is in order of priority. For example: a matching catalog will take priority over any namespace resolution. + +### Time travel Queries with DataFrame + +To select a specific table snapshot or the snapshot at some time in the DataFrame API, Iceberg supports four Spark read options: + +* `snapshot-id` selects a specific table snapshot +* `as-of-timestamp` selects the current snapshot at a timestamp, in milliseconds +* `branch` selects the head snapshot of the specified branch. Note that currently branch cannot be combined with as-of-timestamp. +* `tag` selects the snapshot associated with the specified tag. Tags cannot be combined with `as-of-timestamp`. + +```scala +// time travel to October 26, 1986 at 01:21:00 +spark.read + .option("as-of-timestamp", "499162860000") + .format("iceberg") + .load("path/to/table") +``` + +```scala +// time travel to snapshot with ID 10963874102873L +spark.read + .option("snapshot-id", 10963874102873L) + .format("iceberg") + .load("path/to/table") +``` + +```scala +// time travel to tag historical-snapshot +spark.read + .option(SparkReadOptions.TAG, "historical-snapshot") + .format("iceberg") + .load("path/to/table") +``` + +```scala +// time travel to the head snapshot of audit-branch +spark.read + .option(SparkReadOptions.BRANCH, "audit-branch") + .format("iceberg") + .load("path/to/table") +``` + +### Incremental read + +To read appended data incrementally, use: + +* `start-snapshot-id` Start snapshot ID used in incremental scans (exclusive). +* `end-snapshot-id` End snapshot ID used in incremental scans (inclusive). This is optional. Omitting it will default to the current snapshot. + +```scala +// get the data added after start-snapshot-id (10963874102873L) until end-snapshot-id (63874143573109L) +spark.read + .format("iceberg") + .option("start-snapshot-id", "10963874102873") + .option("end-snapshot-id", "63874143573109") + .load("path/to/table") +``` + +!!! info + Currently gets only the data from `append` operation. Cannot support `replace`, `overwrite`, `delete` operations. + Incremental read works with both V1 and V2 format-version. + Incremental read is not supported by Spark's SQL syntax. + +## Inspecting tables + +To inspect a table's history, snapshots, and other metadata, Iceberg supports metadata tables. + +Metadata tables are identified by adding the metadata table name after the original table name. For example, history for `db.table` is read using `db.table.history`. + +### History + +To show table history: + +```sql +SELECT * FROM prod.db.table.history; +``` + +| made_current_at | snapshot_id | parent_id | is_current_ancestor | +| -- | -- | -- | -- | +| 2019-02-08 03:29:51.215 | 5781947118336215154 | NULL | true | +| 2019-02-08 03:47:55.948 | 5179299526185056830 | 5781947118336215154 | true | +| 2019-02-09 16:24:30.13 | 296410040247533544 | 5179299526185056830 | false | +| 2019-02-09 16:32:47.336 | 2999875608062437330 | 5179299526185056830 | true | +| 2019-02-09 19:42:03.919 | 8924558786060583479 | 2999875608062437330 | true | +| 2019-02-09 19:49:16.343 | 6536733823181975045 | 8924558786060583479 | true | + +!!! info + **This shows a commit that was rolled back.** The example has two snapshots with the same parent, and one is *not* an ancestor of the current table state. + +### Metadata Log Entries + +To show table metadata log entries: + +```sql +SELECT * from prod.db.table.metadata_log_entries; +``` + +| timestamp | file | latest_snapshot_id | latest_schema_id | latest_sequence_number | +| -- | -- | -- | -- | -- | +| 2022-07-28 10:43:52.93 | s3://.../table/metadata/00000-9441e604-b3c2-498a-a45a-6320e8ab9006.metadata.json | null | null | null | +| 2022-07-28 10:43:57.487 | s3://.../table/metadata/00001-f30823df-b745-4a0a-b293-7532e0c99986.metadata.json | 170260833677645300 | 0 | 1 | +| 2022-07-28 10:43:58.25 | s3://.../table/metadata/00002-2cc2837a-02dc-4687-acc1-b4d86ea486f4.metadata.json | 958906493976709774 | 0 | 2 | + +### Snapshots + +To show the valid snapshots for a table: + +```sql +SELECT * FROM prod.db.table.snapshots; +``` + +| committed_at | snapshot_id | parent_id | operation | manifest_list | summary | +| -- | -- | -- | -- | -- | -- | +| 2019-02-08 03:29:51.215 | 57897183625154 | null | append | s3://.../table/metadata/snap-57897183625154-1.avro | { added-records -> 2478404, total-records -> 2478404, added-data-files -> 438, total-data-files -> 438, spark.app.id -> application_1520379288616_155055 } | + +You can also join snapshots to table history. For example, this query will show table history, with the application ID that wrote each snapshot: + +```sql +select + h.made_current_at, + s.operation, + h.snapshot_id, + h.is_current_ancestor, + s.summary['spark.app.id'] +from prod.db.table.history h +join prod.db.table.snapshots s + on h.snapshot_id = s.snapshot_id +order by made_current_at; +``` + +| made_current_at | operation | snapshot_id | is_current_ancestor | summary[spark.app.id] | +| -- | -- | -- | -- | -- | +| 2019-02-08 03:29:51.215 | append | 57897183625154 | true | application_1520379288616_155055 | +| 2019-02-09 16:24:30.13 | delete | 29641004024753 | false | application_1520379288616_151109 | +| 2019-02-09 16:32:47.336 | append | 57897183625154 | true | application_1520379288616_155055 | +| 2019-02-08 03:47:55.948 | overwrite | 51792995261850 | true | application_1520379288616_152431 | + +### Entries + +To show all the table's current manifest entries for both data and delete files. + +```sql +SELECT * FROM prod.db.table.entries; +``` + +| status | snapshot_id | sequence_number | file_sequence_number | data_file | readable_metrics | +| -- | -- | -- | -- | -- | -- | +| 2 | 57897183625154 | 0 | 0 | {"content":0,"file_path":"s3:/.../table/data/00047-25-833044d0-127b-415c-b874-038a4f978c29-00612.parquet","file_format":"PARQUET","spec_id":0,"record_count":15,"file_size_in_bytes":473,"column_sizes":{1:103},"value_counts":{1:15},"null_value_counts":{1:0},"nan_value_counts":{},"lower_bounds":{1:},"upper_bounds":{1:},"key_metadata":null,"split_offsets":[4],"equality_ids":null,"sort_order_id":0} | {"c1":{"column_size":103,"value_count":15,"null_value_count":0,"nan_value_count":null,"lower_bound":1,"upper_bound":3}} | + +Note: + +1. The columns in the `entries` table correspond to the [manifest entry fields](../../spec.md#manifest-entry-fields): + - `status`: Used to track additions and deletions + - `snapshot_id`: The ID of the snapshot in which the file was added or removed + - `sequence_number`: Used for ordering changes across snapshots + - `file_sequence_number`: Indicates when the file was added + - `data_file`: A struct containing metadata about the data file, see the [data file fields](../../spec.md#data-file-fields) +2. The `readable_metrics` column provides a human-readable map of extended column-level metrics derived from the `data_file` column, making it easier to inspect and debug file-level statistics. + +### Files + +To show a table's current files: + +```sql +SELECT * FROM prod.db.table.files; +``` + +| content | file_path | file_format | spec_id | record_count | file_size_in_bytes | column_sizes | value_counts | null_value_counts | nan_value_counts | lower_bounds | upper_bounds | key_metadata | split_offsets | equality_ids | sort_order_id | readable_metrics | +| -- | -- | -- | -- | -- | -- | -- | -- | -- | -- | -- | -- | -- | -- | -- | -- | -- | +| 0 | s3:/.../table/data/00042-3-a9aa8b24-20bc-4d56-93b0-6b7675782bb5-00001.parquet | PARQUET | 0 | 1 | 652 | {1:52,2:48} | {1:1,2:1} | {1:0,2:0} | {} | {1:,2:d} | {1:,2:d} | NULL | [4] | NULL | 0 | {"data":{"column_size":48,"value_count":1,"null_value_count":0,"nan_value_count":null,"lower_bound":"d","upper_bound":"d"},"id":{"column_size":52,"value_count":1,"null_value_count":0,"nan_value_count":null,"lower_bound":1,"upper_bound":1}} | +| 0 | s3:/.../table/data/00000-0-f9709213-22ca-4196-8733-5cb15d2afeb9-00001.parquet | PARQUET | 0 | 1 | 643 | {1:46,2:48} | {1:1,2:1} | {1:0,2:0} | {} | {1:,2:a} | {1:,2:a} | NULL | [4] | NULL | 0 | {"data":{"column_size":48,"value_count":1,"null_value_count":0,"nan_value_count":null,"lower_bound":"a","upper_bound":"a"},"id":{"column_size":46,"value_count":1,"null_value_count":0,"nan_value_count":null,"lower_bound":1,"upper_bound":1}} | +| 0 | s3:/.../table/data/00001-1-f9709213-22ca-4196-8733-5cb15d2afeb9-00001.parquet | PARQUET | 0 | 2 | 644 | {1:49,2:51} | {1:2,2:2} | {1:0,2:0} | {} | {1:,2:b} | {1:,2:c} | NULL | [4] | NULL | 0 | {"data":{"column_size":51,"value_count":2,"null_value_count":0,"nan_value_count":null,"lower_bound":"b","upper_bound":"c"},"id":{"column_size":49,"value_count":2,"null_value_count":0,"nan_value_count":null,"lower_bound":2,"upper_bound":3}} | +| 1 | s3:/.../table/data/00081-4-a9aa8b24-20bc-4d56-93b0-6b7675782bb5-00001-deletes.parquet | PARQUET | 0 | 1 | 1560 | {2147483545:46,2147483546:152} | {2147483545:1,2147483546:1} | {2147483545:0,2147483546:0} | {} | {2147483545:,2147483546:s3:/.../table/data/00000-0-f9709213-22ca-4196-8733-5cb15d2afeb9-00001.parquet} | {2147483545:,2147483546:s3:/.../table/data/00000-0-f9709213-22ca-4196-8733-5cb15d2afeb9-00001.parquet} | NULL | [4] | NULL | NULL | {"data":{"column_size":null,"value_count":null,"null_value_count":null,"nan_value_count":null,"lower_bound":null,"upper_bound":null},"id":{"column_size":null,"value_count":null,"null_value_count":null,"nan_value_count":null,"lower_bound":null,"upper_bound":null}} | +| 2 | s3:/.../table/data/00047-25-833044d0-127b-415c-b874-038a4f978c29-00612.parquet | PARQUET | 0 | 126506 | 28613985 | {100:135377,101:11314} | {100:126506,101:126506} | {100:105434,101:11} | {} | {100:0,101:17} | {100:404455227527,101:23} | NULL | NULL | [1] | 0 | {"id":{"column_size":135377,"value_count":126506,"null_value_count":105434,"nan_value_count":null,"lower_bound":0,"upper_bound":404455227527},"data":{"column_size":11314,"value_count":126506,"null_value_count": 11,"nan_value_count":null,"lower_bound":17,"upper_bound":23}} | + +!!! info + Content refers to type of content stored by the data file: + + - 0 - Data + - 1 - Position Deletes + - 2 - Equality Deletes + +To show only data files or delete files, query `prod.db.table.data_files` and `prod.db.table.delete_files` respectively. +To show all files, data files and delete files across all tracked snapshots, query `prod.db.table.all_files`, `prod.db.table.all_data_files` and `prod.db.table.all_delete_files` respectively. + +### Manifests + +To show a table's current file manifests: + +```sql +SELECT * FROM prod.db.table.manifests; +``` + +| content | path | length | partition_spec_id | added_snapshot_id | added_data_files_count | existing_data_files_count | deleted_data_files_count | added_delete_files_count | existing_delete_files_count | deleted_delete_files_count | partition_summaries | +| -- | -- | -- | -- | -- | -- | -- | -- | -- | -- | -- | -- | +| 0 | s3://.../table/metadata/45b5290b-ee61-4788-b324-b1e2735c0e10-m0.avro | 4479 | 0 | 6668963634911763636 | 8 | 0 | 0 | 0 | 0 | 0 | [[false,null,2019-05-13,2019-05-15]] | + +Note: + +1. Fields within `partition_summaries` column of the manifests table correspond to `field_summary` structs within [manifest list](../../spec.md#manifest-lists), with the following order: + - `contains_null` + - `contains_nan` + - `lower_bound` + - `upper_bound` +2. `contains_nan` could return null, which indicates that this information is not available from the file's metadata. + This usually occurs when reading from V1 table, where `contains_nan` is not populated. + +### Partitions + +To show a table's current partitions: + +```sql +SELECT * FROM prod.db.table.partitions; +``` + +| partition | spec_id | record_count | file_count | total_data_file_size_in_bytes | position_delete_record_count | position_delete_file_count | equality_delete_record_count | equality_delete_file_count | last_updated_at(μs) | last_updated_snapshot_id | +| -------------- |---------|---------------|------------|--------------------------|------------------------------|----------------------------|------------------------------|----------------------------|---------------------|--------------------------| +| {20211001, 11} | 0 | 1 | 1 | 100 | 2 | 1 | 0 | 0 | 1633086034192000 | 9205185327307503337 | +| {20211002, 11} | 0 | 4 | 3 | 500 | 1 | 1 | 0 | 0 | 1633172537358000 | 867027598972211003 | +| {20211001, 10} | 0 | 7 | 4 | 700 | 0 | 0 | 0 | 0 | 1633082598716000 | 3280122546965981531 | +| {20211002, 10} | 0 | 3 | 2 | 400 | 0 | 0 | 1 | 1 | 1633169159489000 | 6941468797545315876 | + +Note: + +1. For unpartitioned tables, the partitions table will not contain the partition and spec_id fields. + +2. The partitions metadata table shows partitions with data files or delete files in the current snapshot. However, delete files are not applied, and so in some cases partitions may be shown even though all their data rows are marked deleted by delete files. +### Positional Delete Files + +To show all positional delete files from the current snapshot of table: + +```sql +SELECT * from prod.db.table.position_deletes; +``` + +| file_path | pos | row | partition | spec_id | delete_file_path | +| -- | -- | -- | -- | -- | -- | +| s3:/.../table/data/00042-3-a9aa8b24-20bc-4d56-93b0-6b7675782bb5-00001.parquet | 1 | 0 | {20211001, 11} | 0 | s3:/.../table/data/00191-1933-25e9f2f3-d863-4a69-a5e1-f9aeeebe60bb-00001-deletes.parquet | + +### All Metadata Tables + +These tables are unions of the metadata tables specific to the current snapshot, and return metadata across all snapshots. + +!!! danger + The "all" metadata tables may produce more than one row per data file or manifest file because metadata files may be part of more than one table snapshot. + +#### All Data Files + +To show all of the table's data files and each file's metadata: + +```sql +SELECT * FROM prod.db.table.all_data_files; +``` + +| content | file_path | file_format | spec_id | partition | record_count | file_size_in_bytes | column_sizes| value_counts | null_value_counts | nan_value_counts| lower_bounds| upper_bounds|key_metadata|split_offsets|equality_ids|sort_order_id|readable_metrics| +| -- | -- | -- | -- | -- | -- | -- | -- | -- | -- | -- | -- | -- | -- | -- | -- | -- | -- | +| 0|s3://.../dt=20210102/00000-0-756e2512-49ae-45bb-aae3-c0ca475e7879-00001.parquet| PARQUET|0|{20210102}| 14| 2444|{1 -> 94, 2 -> 17}|{1 -> 14, 2 -> 14}| {1 -> 0, 2 -> 0}| {}|{1 -> 1, 2 -> 20210102}|{1 -> 2, 2 -> 20210102}| null| [4]| null| 0| {"id":{"column_size":94,"value_count":14,"null_value_count":0,"nan_value_count":null,"lower_bound":1,"upper_bound":2},"data":{"column_size":17,"value_count":14,"null_value_count": 0,"nan_value_count":null,"lower_bound":20210102,"upper_bound":20210102}} | +| 0|s3://.../dt=20210103/00000-0-26222098-032f-472b-8ea5-651a55b21210-00001.parquet| PARQUET|0|{20210103}| 14| 2444|{1 -> 94, 2 -> 17}|{1 -> 14, 2 -> 14}| {1 -> 0, 2 -> 0}| {}|{1 -> 1, 2 -> 20210103}|{1 -> 3, 2 -> 20210103}| null| [4]| null| 0| {"id":{"column_size":94,"value_count":14,"null_value_count":0,"nan_value_count":null,"lower_bound":1,"upper_bound":3},"data":{"column_size":17,"value_count":14,"null_value_count": 0,"nan_value_count":null,"lower_bound":20210103,"upper_bound":20210103}} | +| 0|s3://.../dt=20210104/00000-0-a3bb1927-88eb-4f1c-bc6e-19076b0d952e-00001.parquet| PARQUET|0|{20210104}| 14| 2444|{1 -> 94, 2 -> 17}|{1 -> 14, 2 -> 14}| {1 -> 0, 2 -> 0}| {}|{1 -> 1, 2 -> 20210104}|{1 -> 3, 2 -> 20210104}| null| [4]| null| 0| {"id":{"column_size":94,"value_count":14,"null_value_count":0,"nan_value_count":null,"lower_bound":1,"upper_bound":3},"data":{"column_size":17,"value_count":14,"null_value_count": 0,"nan_value_count":null,"lower_bound":20210104,"upper_bound":20210104}} | + +#### All Delete Files + +To show the table's delete files and each file's metadata from all the snapshots: + +```sql +SELECT * FROM prod.db.table.all_delete_files; +``` + +| content | file_path | file_format | spec_id | partition | record_count | file_size_in_bytes | column_sizes | value_counts | null_value_counts | nan_value_counts | lower_bounds | upper_bounds | key_metadata | split_offsets | equality_ids | sort_order_id | readable_metrics | +| -- | -- | -- | -- | -- | -- | -- | -- | -- | -- | -- | -- | -- | -- | -- | -- | -- | -- | +| 1 | s3:/.../table/data/00081-4-a9aa8b24-20bc-4d56-93b0-6b7675782bb5-00001-deletes.parquet | PARQUET | 0 | {20210102} | 1 | 1560 | {2147483545:46,2147483546:152} | {2147483545:1,2147483546:1} | {2147483545:0,2147483546:0} | {} | {2147483545:,2147483546:s3:/.../table/data/00000-0-f9709213-22ca-4196-8733-5cb15d2afeb9-00001.parquet} | {2147483545:,2147483546:s3:/.../table/data/00000-0-f9709213-22ca-4196-8733-5cb15d2afeb9-00001.parquet} | NULL | [4] | NULL | NULL | {"data":{"column_size":null,"value_count":null,"null_value_count":null,"nan_value_count":null,"lower_bound":null,"upper_bound":null},"id":{"column_size":null,"value_count":null,"null_value_count":null,"nan_value_count":null,"lower_bound":null,"upper_bound":null}} | +| 2 | s3:/.../table/data/00047-25-833044d0-127b-415c-b874-038a4f978c29-00612.parquet | PARQUET | 0 | {20210103} | 126506 | 28613985 | {100:135377,101:11314} | {100:126506,101:126506} | {100:105434,101:11} | {} | {100:0,101:17} | {100:404455227527,101:23} | NULL | NULL | [1] | 0 | {"id":{"column_size":135377,"value_count":126506,"null_value_count":105434,"nan_value_count":null,"lower_bound":0,"upper_bound":404455227527},"data":{"column_size":11314,"value_count":126506,"null_value_count": 11,"nan_value_count":null,"lower_bound":17,"upper_bound":23}} | + +#### All Entries + +To show the table's manifest entries from all the snapshots for both data and delete files: + +```sql +SELECT * FROM prod.db.table.all_entries; +``` + +| status | snapshot_id | sequence_number | file_sequence_number | data_file | readable_metrics | +| -- | -- | -- | -- | -- | -- | +| 2 | 57897183625154 | 0 | 0 | {"content":0,"file_path":"s3:/.../table/data/00047-25-833044d0-127b-415c-b874-038a4f978c29-00612.parquet","file_format":"PARQUET","spec_id":0,"record_count":15,"file_size_in_bytes":473,"column_sizes":{1:103},"value_counts":{1:15},"null_value_counts":{1:0},"nan_value_counts":{},"lower_bounds":{1:},"upper_bounds":{1:},"key_metadata":null,"split_offsets":[4],"equality_ids":null,"sort_order_id":0} | {"c1":{"column_size":103,"value_count":15,"null_value_count":0,"nan_value_count":null,"lower_bound":1,"upper_bound":3}} | + +#### All Manifests + +To show all of the table's manifest files: + +```sql +SELECT * FROM prod.db.table.all_manifests; +``` + +| content | path | length | partition_spec_id | added_snapshot_id | added_data_files_count | existing_data_files_count | deleted_data_files_count | added_delete_files_count | existing_delete_files_count | deleted_delete_files_count | partition_summaries| reference_snapshot_id | +| -- | -- | -- | -- | -- | -- | -- | -- | -- | -- | -- | -- | -- | +|0| s3://.../metadata/a85f78c5-3222-4b37-b7e4-faf944425d48-m0.avro | 6376 | 0 | 6272782676904868561 | 2 | 0 | 0 | 0 | 0 | 0 |[{false, false, 20210101, 20210101}]| 57897183625154 | + +Note: + +1. Fields within `partition_summaries` column of the manifests table correspond to `field_summary` structs within [manifest list](../../spec.md#manifest-lists), with the following order: + - `contains_null` + - `contains_nan` + - `lower_bound` + - `upper_bound` +2. `contains_nan` could return null, which indicates that this information is not available from the file's metadata. + This usually occurs when reading from V1 table, where `contains_nan` is not populated. + +### References + +To show a table's known snapshot references: + +```sql +SELECT * FROM prod.db.table.refs; +``` + +| name | type | snapshot_id | max_reference_age_in_ms | min_snapshots_to_keep | max_snapshot_age_in_ms | +| -- | -- | -- | -- | -- | -- | +| main | BRANCH | 4686954189838128572 | 10 | 20 | 30 | +| testTag | TAG | 4686954189838128572 | 10 | null | null | + +### Inspecting with DataFrames + +Metadata tables can be loaded using the DataFrameReader API: + +```scala +// named metastore table +spark.read.format("iceberg").load("db.table.files") +// Hadoop path table +spark.read.format("iceberg").load("hdfs://nn:8020/path/to/table#files") +``` + +### Time Travel with Metadata Tables + +To inspect a tables's metadata with the time travel feature: + +```sql +-- get the table's file manifests at timestamp Sep 20, 2021 08:00:00 +SELECT * FROM prod.db.table.manifests TIMESTAMP AS OF '2021-09-20 08:00:00'; + +-- get the table's partitions with snapshot id 10963874102873L +SELECT * FROM prod.db.table.partitions VERSION AS OF 10963874102873; +``` + +Metadata tables can also be inspected with time travel using the DataFrameReader API: + +```scala +// load the table's file metadata at snapshot-id 10963874102873 as DataFrame +spark.read.format("iceberg").option("snapshot-id", 10963874102873L).load("db.table.files") +``` diff --git a/1.11.0/docs/spark-structured-streaming.md b/1.11.0/docs/spark-structured-streaming.md new file mode 100644 index 000000000000..3313f8150b73 --- /dev/null +++ b/1.11.0/docs/spark-structured-streaming.md @@ -0,0 +1,151 @@ +--- +title: "Structured Streaming" +--- + + +# Spark Structured Streaming + +Iceberg uses Apache Spark's DataSourceV2 API for data source and catalog implementations. Spark DSv2 is an evolving API with different levels of support in Spark versions. + +## Streaming Reads + +Iceberg supports processing incremental data in spark structured streaming jobs which starts from a historical timestamp: + +```scala +val df = spark.readStream + .format("iceberg") + .option("stream-from-timestamp", Long.toString(streamStartTimestamp)) + .load("database.table_name") +``` + +!!! warning + Iceberg only supports reading data from append snapshots. Overwrite snapshots cannot be processed and will cause an exception by default. Overwrites may be ignored by setting `streaming-skip-overwrite-snapshots=true`. Similarly, delete snapshots will cause an exception by default, and deletes may be ignored by setting `streaming-skip-delete-snapshots=true`. + +### Limit input rate +To control the size of micro-batches in the DataFrame API, Iceberg supports two read options: + +* `streaming-max-files-per-micro-batch` Maximum number of files to be processed in every micro-batch. +* `streaming-max-rows-per-micro-batch` A "soft max" on the number of rows to be processed in every micro-batch. A batch will always include all the rows in the next unprocessed data file but additional files will not be included if doing so would exceed the soft max limit. + +If both options are set, the micro-batch size will be limited by whichever option is reached first. + +```scala +// Read a hard limit of 1 file per micro-batch +val df = spark.readStream + .format("iceberg") + .option("streaming-max-files-per-micro-batch", "1") + .load("database.table_name") +``` + +```scala +// Read files until the number of included rows >= 1000 per micro-batch +val df = spark.readStream + .format("iceberg") + .option("streaming-max-rows-per-micro-batch", "1000") + .load("database.table_name") +``` + +!!! info + Note: In addition to limiting micro-batch sizes on queries that use the default trigger (i.e. `Trigger.ProcessingTime`), rate limiting options can be applied to queries that use `Trigger.AvailableNow` to split one-time processing of all available source data into multiple micro-batches for better query scalability. Rate limiting options will be ignored when using the deprecated `Trigger.Once` trigger. + +### Asynchronous Micro-Batch Planning + +Users can enable asynchronous micro-batch planning by setting `async-micro-batch-planning-enabled` to true. With this option enabled, Iceberg will start processing the current micro-batch while planning the next micro-batches in parallel. +This can help improve query throughput by reducing idle time between micro-batches. Users should weigh the tradeoffs, which include higher memory usage and increased snapshot detection latency. + +Users can also set additional options to control the behavior of asynchronous micro-batch planning, found in the [spark configuration](spark-configuration.md#read-options). + +## Streaming Writes + +To write values from streaming query to Iceberg table, use `DataStreamWriter`: + +```scala +data.writeStream + .format("iceberg") + .outputMode("append") + .trigger(Trigger.ProcessingTime(1, TimeUnit.MINUTES)) + .option("checkpointLocation", checkpointPath) + .toTable("database.table_name") +``` + +In the case of the directory-based Hadoop catalog: + +```scala +data.writeStream + .format("iceberg") + .outputMode("append") + .trigger(Trigger.ProcessingTime(1, TimeUnit.MINUTES)) + .option("path", "hdfs://nn:8020/path/to/table") + .option("checkpointLocation", checkpointPath) + .start() +``` + +Iceberg supports `append` and `complete` output modes: + +* `append`: appends the rows of every micro-batch to the table +* `complete`: replaces the table contents every micro-batch + +Prior to starting the streaming query, ensure you created the table. Refer to the [SQL create table](spark-ddl.md#create-table) documentation to learn how to create the Iceberg table. + +Iceberg doesn't support experimental [continuous processing](https://spark.apache.org/docs/latest/structured-streaming-programming-guide.html#continuous-processing), as it doesn't provide the interface to "commit" the output. + +### Partitioned table + +Iceberg requires sorting data by partition per task prior to writing the data. In Spark tasks are split by Spark partition +against partitioned table. For batch queries you're encouraged to do explicit sort to fulfill the requirement +(see [here](spark-writes.md#writing-distribution-modes)), but the approach would bring additional latency as +repartition and sort are considered as heavy operations for streaming workload. To avoid additional latency, you can +enable fanout writer to eliminate the requirement. + +```scala +data.writeStream + .format("iceberg") + .outputMode("append") + .trigger(Trigger.ProcessingTime(1, TimeUnit.MINUTES)) + .option("fanout-enabled", "true") + .option("checkpointLocation", checkpointPath) + .toTable("database.table_name") +``` + +Fanout writer opens the files per partition value and doesn't close these files till the write task finishes. Avoid using the fanout writer for batch writing, as explicit sort against output rows is cheap for batch workloads. + +## Maintenance for streaming tables + +Streaming writes can create new table versions quickly, creating lots of table metadata to track those versions. +Maintaining metadata by tuning the rate of commits, expiring old snapshots, and automatically cleaning up metadata files +is highly recommended. + +### Tune the rate of commits + +Having a high rate of commits produces data files, manifests, and snapshots which leads to additional maintenance. It is recommended to have a trigger interval of 1 minute at the minimum and increase the interval if needed. + +The triggers section in [Structured Streaming Programming Guide](https://spark.apache.org/docs/latest/structured-streaming-programming-guide.html#triggers) +documents how to configure the interval. + +### Expire old snapshots + +Each batch written to a table produces a new snapshot. Iceberg tracks snapshots in table metadata until they are expired. Snapshots accumulate quickly with frequent commits, so it is highly recommended that tables written by streaming queries are [regularly maintained](maintenance.md#expire-snapshots). [Snapshot expiration](spark-procedures.md#expire_snapshots) is the procedure of removing the metadata and any data files that are no longer needed. By default, the procedure will expire the snapshots older than five days. + +### Compacting data files + +The amount of data written from a streaming process is typically small, which can cause the table metadata to track lots of small files. [Compacting small files into larger files](maintenance.md#compact-data-files) reduces the metadata needed by the table, and increases query efficiency. Iceberg and Spark [comes with the `rewrite_data_files` procedure](spark-procedures.md#rewrite_data_files). + +### Rewrite manifests + +To optimize write latency on a streaming workload, Iceberg can write the new snapshot with a "fast" append that does not automatically compact manifests. +This could lead lots of small manifest files. Iceberg can [rewrite the number of manifest files to improve query performance](maintenance.md#rewrite-manifests). Iceberg and Spark [come with the `rewrite_manifests` procedure](spark-procedures.md#rewrite_manifests). diff --git a/1.11.0/docs/spark-writes.md b/1.11.0/docs/spark-writes.md new file mode 100644 index 000000000000..31a0bbc21155 --- /dev/null +++ b/1.11.0/docs/spark-writes.md @@ -0,0 +1,465 @@ +--- +title: "Writes" +--- + + +# Spark Writes + +To use Iceberg in Spark, first configure [Spark catalogs](spark-configuration.md). + +Some plans are only available when using [Iceberg SQL extensions](spark-configuration.md#sql-extensions). + +Iceberg uses Apache Spark's DataSourceV2 API for data source and catalog implementations. Spark DSv2 is an evolving API with different levels of support in Spark versions: + +| Feature support | Spark | Notes | +|--------------------------------------------------|---------|-----------------------------------------------------------------------------| +| [SQL insert into](#insert-into) | ✔️ | ⚠ Requires `spark.sql.storeAssignmentPolicy=ANSI` (default since Spark 3.0) | +| [SQL merge into](#merge-into) | ✔️ | ⚠ Requires Iceberg Spark extensions | +| [SQL insert overwrite](#insert-overwrite) | ✔️ | ⚠ Requires `spark.sql.storeAssignmentPolicy=ANSI` (default since Spark 3.0) | +| [SQL delete from](#delete-from) | ✔️ | ⚠ Row-level delete requires Iceberg Spark extensions | +| [SQL update](#update) | ✔️ | ⚠ Requires Iceberg Spark extensions | +| [DataFrame append](#appending-data) | ✔️ | | +| [DataFrame overwrite](#overwriting-data) | ✔️ | | +| [DataFrame CTAS and RTAS](#creating-tables) | ✔️ | ⚠ Requires DSv2 API | +| [DataFrame merge into](#merging-data) | ✔️ | ⚠ Requires DSv2 API (Spark 4.0 and later) | + +## Writing with SQL + +Spark supports SQL `INSERT INTO`, `MERGE INTO`, and `INSERT OVERWRITE`, as well as the new `DataFrameWriterV2` API. + +### `INSERT INTO` + +To append new data to a table, use `INSERT INTO`. + +```sql +INSERT INTO prod.db.table VALUES (1, 'a'), (2, 'b') +``` +```sql +INSERT INTO prod.db.table SELECT ... +``` + +### `MERGE INTO` + +Spark supports `MERGE INTO` queries that can express row-level updates. + +Iceberg supports `MERGE INTO` by rewriting data files that contain rows that need to be updated in an `overwrite` commit. + +**`MERGE INTO` is recommended instead of `INSERT OVERWRITE`** because Iceberg can replace only the affected data files, and because the data overwritten by a dynamic overwrite may change if the table's partitioning changes. + +#### `MERGE INTO` syntax + +`MERGE INTO` updates a table, called the _target_ table, using a set of updates from another query, called the _source_. The update for a row in the target table is found using the `ON` clause that is like a join condition. + +```sql +MERGE INTO prod.db.target t -- a target table +USING (SELECT ...) s -- the source updates +ON t.id = s.id -- condition to find updates for target rows +WHEN ... -- updates +``` + +Updates to rows in the target table are listed using `WHEN MATCHED ... THEN ...`. Multiple `MATCHED` clauses can be added with conditions that determine when each match should be applied. The first matching expression is used. + +```sql +WHEN MATCHED AND s.op = 'delete' THEN DELETE +WHEN MATCHED AND t.count IS NULL AND s.op = 'increment' THEN UPDATE SET t.count = 0 +WHEN MATCHED AND s.op = 'increment' THEN UPDATE SET t.count = t.count + 1 +``` + +Source rows (updates) that do not match can be inserted: + +```sql +WHEN NOT MATCHED THEN INSERT * +``` + +Inserts also support additional conditions: + +```sql +WHEN NOT MATCHED AND s.event_time > still_valid_threshold THEN INSERT (id, count) VALUES (s.id, 1) +``` + +Only one record in the source data can update any given row of the target table, or else an error will be thrown. + +Spark 3.5 added support for `WHEN NOT MATCHED BY SOURCE ... THEN ...` to update or delete rows that are not present in the source data: + +```sql +WHEN NOT MATCHED BY SOURCE THEN UPDATE SET status = 'invalid' +``` + +#### Snapshot summary + +After a `MERGE INTO` commit, the [snapshot summary](../../spec.md#optional-snapshot-summary-fields) may include the following fields. Each value is the string form of a non-negative count. A field is omitted when the value is unknown (e.g., not reported by Spark). + +!!! info + Only available in Spark 4.1 and higher. + +| Field | Description | +|-----------------------------------------------------------------------|-----------------------------------------------------------------------------| +| **`spark.merge-into.num-target-rows-copied`** | Number of target rows copied unmodified because they did not match any action | +| **`spark.merge-into.num-target-rows-deleted`** | Number of target rows deleted | +| **`spark.merge-into.num-target-rows-updated`** | Number of target rows updated | +| **`spark.merge-into.num-target-rows-inserted`** | Number of target rows inserted | +| **`spark.merge-into.num-target-rows-matched-updated`** | Number of target rows updated by a MATCHED clause | +| **`spark.merge-into.num-target-rows-matched-deleted`** | Number of target rows deleted by a MATCHED clause | +| **`spark.merge-into.num-target-rows-not-matched-by-source-updated`** | Number of target rows updated by a NOT MATCHED BY SOURCE clause | +| **`spark.merge-into.num-target-rows-not-matched-by-source-deleted`** | Number of target rows deleted by a NOT MATCHED BY SOURCE clause | + +### `INSERT OVERWRITE` + +`INSERT OVERWRITE` can replace data in the table with the result of a query. Overwrites are atomic operations for Iceberg tables. + +The partitions that will be replaced by `INSERT OVERWRITE` depends on Spark's partition overwrite mode and the partitioning of a table. `MERGE INTO` can rewrite only affected data files and has more easily understood behavior, so it is recommended instead of `INSERT OVERWRITE`. + +#### Overwrite behavior + +Spark's default overwrite mode is **static**, but **dynamic overwrite mode is recommended when writing to Iceberg tables.** Static overwrite mode determines which partitions to overwrite in a table by converting the `PARTITION` clause to a filter, but the `PARTITION` clause can only reference table columns. + +Dynamic overwrite mode is configured by setting `spark.sql.sources.partitionOverwriteMode=dynamic`. + +To demonstrate the behavior of dynamic and static overwrites, consider a `logs` table defined by the following DDL: + +```sql +CREATE TABLE prod.my_app.logs ( + uuid string NOT NULL, + level string NOT NULL, + ts timestamp NOT NULL, + message string) +USING iceberg +PARTITIONED BY (level, hours(ts)) +``` + +#### Dynamic overwrite + +When Spark's overwrite mode is dynamic, partitions that have rows produced by the `SELECT` query will be replaced. + +For example, this query removes duplicate log events from the example `logs` table. + +```sql +INSERT OVERWRITE prod.my_app.logs +SELECT uuid, first(level), first(ts), first(message) +FROM prod.my_app.logs +WHERE cast(ts as date) = '2020-07-01' +GROUP BY uuid +``` + +In dynamic mode, this will replace any partition with rows in the `SELECT` result. Because the date of all rows is restricted to 1 July, only hours of that day will be replaced. + +#### Static overwrite + +When Spark's overwrite mode is static, the `PARTITION` clause is converted to a filter that is used to delete from the table. If the `PARTITION` clause is omitted, all partitions will be replaced. + +Because there is no `PARTITION` clause in the query above, it will drop all existing rows in the table when run in static mode, but will only write the logs from 1 July. + +To overwrite just the partitions that were loaded, add a `PARTITION` clause that aligns with the `SELECT` query filter: + +```sql +INSERT OVERWRITE prod.my_app.logs +PARTITION (level = 'INFO') +SELECT uuid, first(level), first(ts), first(message) +FROM prod.my_app.logs +WHERE level = 'INFO' +GROUP BY uuid +``` + +Note that this mode cannot replace hourly partitions like the dynamic example query because the `PARTITION` clause can only reference table columns, not hidden partitions. + +### `DELETE FROM` + +Spark supports `DELETE FROM` queries to remove data from tables. + +Delete queries accept a filter to match rows to delete. + +```sql +DELETE FROM prod.db.table +WHERE ts >= '2020-05-01 00:00:00' and ts < '2020-06-01 00:00:00' + +DELETE FROM prod.db.all_events +WHERE session_time < (SELECT min(session_time) FROM prod.db.good_events) + +DELETE FROM prod.db.orders AS t1 +WHERE EXISTS (SELECT oid FROM prod.db.returned_orders WHERE t1.oid = oid) +``` + +If the delete filter matches entire partitions of the table, Iceberg will perform a metadata-only delete. If the filter matches individual rows of a table, then Iceberg will rewrite only the affected data files. + +### `UPDATE` + +Update queries accept a filter to match rows to update. + +```sql +UPDATE prod.db.table +SET c1 = 'update_c1', c2 = 'update_c2' +WHERE ts >= '2020-05-01 00:00:00' and ts < '2020-06-01 00:00:00' + +UPDATE prod.db.all_events +SET session_time = 0, ignored = true +WHERE session_time < (SELECT min(session_time) FROM prod.db.good_events) + +UPDATE prod.db.orders AS t1 +SET order_status = 'returned' +WHERE EXISTS (SELECT oid FROM prod.db.returned_orders WHERE t1.oid = oid) +``` + +For more complex row-level updates based on incoming data, see the section on `MERGE INTO`. + +## Writing to Branches + +The branch must exist before performing write. Operations do **not** create the branch if it does not exist. +A branch can be created using [Spark DDL](spark-ddl.md#branching-and-tagging-ddl). + +!!! info + Note: When writing to a branch, the current schema of the table will be used for validation. + +### Via SQL + +Branch writes can be performed by providing a branch identifier, `branch_yourBranch` in the operation. + +Branch writes can also be performed as part of a write-audit-publish (WAP) workflow by specifying the `spark.wap.branch` config. +Note WAP branch and branch identifier cannot both be specified. + +```sql +-- INSERT (1,' a') (2, 'b') into the audit branch. +INSERT INTO prod.db.table.branch_audit VALUES (1, 'a'), (2, 'b'); + +-- MERGE INTO audit branch +MERGE INTO prod.db.table.branch_audit t +USING (SELECT ...) s +ON t.id = s.id +WHEN ... + +-- UPDATE audit branch +UPDATE prod.db.table.branch_audit AS t1 +SET val = 'c' + +-- DELETE FROM audit branch +DELETE FROM prod.db.table.branch_audit WHERE id = 2; + +-- WAP Branch write +SET spark.wap.branch = audit-branch +INSERT INTO prod.db.table VALUES (3, 'c'); +``` + +### Via DataFrames + +Branch writes via DataFrames can be performed by providing a branch identifier, `branch_yourBranch` in the operation. + +```scala +// To insert into `audit` branch +val data: DataFrame = ... +data.writeTo("prod.db.table.branch_audit").append() +``` + +```scala +// To overwrite `audit` branch +val data: DataFrame = ... +data.writeTo("prod.db.table.branch_audit").overwritePartitions() +``` + +## Writing with DataFrames + +Spark introduced the new `DataFrameWriterV2` API for writing to tables using data frames. The v2 API is recommended for several reasons: + +* CTAS, RTAS, and overwrite by filter are supported +* All operations consistently write columns to a table by name +* Hidden partition expressions are supported in `partitionedBy` +* Overwrite behavior is explicit, either dynamic or by a user-supplied filter +* The behavior of each operation corresponds to SQL statements + - `df.writeTo(t).create()` is equivalent to `CREATE TABLE AS SELECT` + - `df.writeTo(t).replace()` is equivalent to `REPLACE TABLE AS SELECT` + - `df.writeTo(t).append()` is equivalent to `INSERT INTO` + - `df.writeTo(t).overwritePartitions()` is equivalent to dynamic `INSERT OVERWRITE` + +The v1 DataFrame `write` API is still supported, but is not recommended. + +!!! danger + When writing with the v1 DataFrame API in Spark, use `saveAsTable` or `insertInto` to load tables with a catalog. + Using `format("iceberg")` loads an isolated table reference that will not automatically refresh tables used by queries. + +### Appending data + +To append a dataframe to an Iceberg table, use `append`: + +```scala +val data: DataFrame = ... +data.writeTo("prod.db.table").append() +``` + +### Overwriting data + +To overwrite partitions dynamically, use `overwritePartitions()`: + +```scala +val data: DataFrame = ... +data.writeTo("prod.db.table").overwritePartitions() +``` + +To explicitly overwrite partitions, use `overwrite` to supply a filter: + +```scala +data.writeTo("prod.db.table").overwrite($"level" === "INFO") +``` + +### Creating tables + +To run a CTAS or RTAS, use `create`, `replace`, or `createOrReplace` operations: + +```scala +val data: DataFrame = ... +data.writeTo("prod.db.table").create() +``` + +If you have replaced the default Spark catalog (`spark_catalog`) with Iceberg's `SparkSessionCatalog`, do: + +```scala +val data: DataFrame = ... +data.writeTo("db.table").using("iceberg").create() +``` + +Create and replace operations support table configuration methods, like `partitionedBy` and `tableProperty`: + +```scala +data.writeTo("prod.db.table") + .tableProperty("write.format.default", "orc") + .partitionedBy($"level", days($"ts")) + .createOrReplace() +``` + +The Iceberg table location can also be specified by the `location` table property: + +```scala +data.writeTo("prod.db.table") + .tableProperty("location", "/path/to/location") + .createOrReplace() +``` + +### Merging data + +Spark 4.0 added support for performing a MERGE INTO query using the `DataFrameWriterV2` API. + +A MERGE INTO query updates a _target_ table using a set of updates from the _source_, which in this case, is a `DataFrame`: + +```scala +val source: DataFrame = ... // e.g., read from a table, "source" +source.mergeInto("target", $"source.id" === $"target.id") // second argument is the ON condition + .whenMatched($"target.id" === 1) // argument is the additional condition + .updateAll() // UPDATE SET * + .whenMatched($"target.id" === 2) + .delete() + .whenNotMatched() + .insertAll() // INSERT * + .whenNotMatchedBySource($"target.id" === 3) + .update(Map("status" -> lit("invalid"))) // set column name(s) to expression(s) + .merge() +``` + +### Schema Merge + +While inserting or updating Iceberg is capable of resolving schema mismatch at runtime. If configured, Iceberg will perform an automatic schema evolution as follows: + +* A new column is present in the source but not in the target table. + + The new column is added to the target table. Column values are set to `NULL` in all the rows already present in the table + +* A column is present in the target but not in the source. + + The target column value is set to `NULL` when inserting or left unchanged when updating the row. + +The target table must be configured to accept any schema change by setting the property `write.spark.accept-any-schema` to `true`. + +```sql +ALTER TABLE prod.db.sample SET TBLPROPERTIES ( + 'write.spark.accept-any-schema'='true' +) +``` +The writer must enable the `mergeSchema` option. + +```scala +data.writeTo("prod.db.sample").option("mergeSchema","true").append() +``` +## Writing Distribution Modes + +Iceberg's default Spark writers require that the data in each spark task is clustered by partition values. This +distribution is required to minimize the number of file handles that are held open while writing. By default, starting +in Iceberg 1.2.0, Iceberg also requests that Spark pre-sort data to be written to fit this distribution. The +request to Spark is done through the table property `write.distribution-mode` with the value `hash`. Spark doesn't respect +distribution mode in CTAS/RTAS before 3.5.0. + +Let's go through writing the data against below sample table: + +```sql +CREATE TABLE prod.db.sample ( + id bigint, + data string, + category string, + ts timestamp) +USING iceberg +PARTITIONED BY (days(ts), category) +``` + +To write data to the sample table, data needs to be sorted by `days(ts), category` but this is taken care +of automatically by the default `hash` distribution. Previously this would have required manually sorting, but this +is no longer the case. + +```sql +INSERT INTO prod.db.sample +SELECT id, data, category, ts FROM another_table +``` + +There are 3 options for `write.distribution-mode` + +* `none` - This is the previous default for Iceberg. +This mode does not request any shuffles or sort to be performed automatically by Spark. Because no work is done +automatically by Spark, the data must be *manually* sorted by partition value. The data must be sorted either within +each spark task, or globally within the entire dataset. A global sort will minimize the number of output files. +A sort can be avoided by using the Spark [write fanout](spark-configuration.md#write-options) property but this will cause all +file handles to remain open until each write task has completed. +* `hash` - This mode is the new default and requests that Spark uses a hash-based exchange to shuffle the incoming +write data before writing. +Practically, this means that each row is hashed based on the row's partition value and then placed +in a corresponding Spark task based upon that value. Further division and coalescing of tasks may take place because of +[Spark's Adaptive Query planning](#controlling-file-sizes). +* `range` - This mode requests that Spark perform a range based exchange to shuffle the data before writing. +This is a two stage procedure which is more expensive than the `hash` mode. The first stage samples the data to +be written based on the partition and sort columns. The second stage uses the range information to shuffle the input data into Spark +tasks. Each task gets an exclusive range of the input data which clusters the data by partition and also globally sorts. +While this is more expensive than the hash distribution, the global ordering can be beneficial for read performance if +sorted columns are used during queries. This mode is used by default if a table is created with a +sort-order. Further division and coalescing of tasks may take place because of +[Spark's Adaptive Query planning](#controlling-file-sizes). + +## Controlling File Sizes + +When writing data to Iceberg with Spark, it's important to note that Spark cannot write a file larger than a Spark +task and a file cannot span an Iceberg partition boundary. This means although Iceberg will always roll over a file +when it grows to [`write.target-file-size-bytes`](configuration.md#write-properties), but unless the Spark task is +large enough that will not happen. The size of the file created on disk will also be much smaller than the Spark task +since the on disk data will be both compressed and in columnar format as opposed to Spark's uncompressed row +representation. This means a 100 megabyte Spark task will create a file much smaller than 100 megabytes even if that +task is writing to a single Iceberg partition. If the task writes to multiple partitions, the files will be even +smaller than that. + +To control what data ends up in each Spark task use a [`write distribution mode`](#writing-distribution-modes) +or manually repartition the data. + +To adjust Spark's task size it is important to become familiar with Spark's various Adaptive Query Execution (AQE) +parameters. When the `write.distribution-mode` is not `none`, AQE will control the coalescing and splitting of Spark +tasks during the exchange to try to create tasks of `spark.sql.adaptive.advisoryPartitionSizeInBytes` size. These +settings will also affect any user performed re-partitions or sorts. +It is important again to note that this is the in-memory Spark row size and not the on disk +columnar-compressed size, so a larger value than the target file size will need to be specified. The ratio of +in-memory size to on disk size is data dependent. Future work in Spark should allow Iceberg to automatically adjust this +parameter at write time to match the `write.target-file-size-bytes`. diff --git a/1.11.0/docs/table-migration.md b/1.11.0/docs/table-migration.md new file mode 100644 index 000000000000..2e56d378c531 --- /dev/null +++ b/1.11.0/docs/table-migration.md @@ -0,0 +1,74 @@ +--- +title: "Overview" +--- + + +# Table Migration +Apache Iceberg supports converting existing tables in other formats to Iceberg tables. This section introduces the general concept of table migration, its approaches, and existing implementations in Iceberg. + +## Migration Approaches +There are two methods for executing table migration: full data migration and in-place metadata migration. + +Full data migration involves copying all data files from the source table to the new Iceberg table. This method makes the new table fully isolated from the source table, but is slower and doubles the space. +In practice, users can use operations like [Create-Table-As-Select](spark-ddl.md#create-table-as-select), [INSERT](spark-writes.md#insert-into), and Change-Data-Capture pipelines to perform such migration. + +In-place metadata migration preserves the existing data files while incorporating Iceberg metadata on top of them. +This method is not only faster but also eliminates the need for data duplication. However, the new table and the source table are not fully isolated. In other words, if any processes vacuum data files from the source table, the new table will also be affected. + +In this doc, we will describe more about in-place metadata migration. + +![In-Place Metadata Migration](assets/images/iceberg-in-place-metadata-migration.png) + +Apache Iceberg supports the in-place metadata migration approach, which includes three important actions: **Snapshot Table**, **Migrate Table**, and **Add Files**. + +## Snapshot Table +The Snapshot Table action creates a new iceberg table with a different name and with the same schema and partitioning as the source table, leaving the source table unchanged during and after the action. + +- Create a new Iceberg table with the same metadata (schema, partition spec, etc.) as the source table and a different name. Readers and Writers on the source table can continue to work. + +![Snapshot Table Step 1](assets/images/iceberg-snapshotaction-step1.png) + +- Commit all data files across all partitions to the new Iceberg table. The source table remains unchanged. Readers can be switched to the new Iceberg table. + +![Snapshot Table Step 2](assets/images/iceberg-snapshotaction-step2.png) + +- Eventually, all writers can be switched to the new Iceberg table. Once all writers are transitioned to the new Iceberg table, the migration process will be considered complete. + +## Migrate Table +The Migrate Table action also creates a new Iceberg table with the same schema and partitioning as the source table. However, during the action execution, it locks and drops the source table from the catalog. +Consequently, Migrate Table requires all modifications working on the source table to be stopped before the action is performed. + +Stop all writers interacting with the source table. Readers that also support Iceberg may continue reading. + +![Migrate Table Step 1](assets/images/iceberg-migrateaction-step1.png) + +- Create a new Iceberg table with the same identifier and metadata (schema, partition spec, etc.) as the source table. Rename the source table for a backup in case of failure and rollback. + +![Migrate Table Step 2](assets/images/iceberg-migrateaction-step2.png) + +- Commit all data files across all partitions to the new Iceberg table. Drop the source table. Writers can start writing to the new Iceberg table. + +![Migrate Table Step 3](assets/images/iceberg-migrateaction-step3.png) + +## Add Files +After the initial step (either Snapshot Table or Migrate Table), it is common to find some data files that have not been migrated. These files often originate from concurrent writers who continue writing to the source table during or after the migration process. +In practice, these files can be new data files in Hive tables or new snapshots (versions) of Delta Lake tables. The Add Files action is essential for incorporating these files into the Iceberg table. + +# Migrating From Different Table Formats +* [From Hive to Iceberg](hive-migration.md) +* [From Delta Lake to Iceberg](delta-lake-migration.md) diff --git a/1.11.0/docs/view-configuration.md b/1.11.0/docs/view-configuration.md new file mode 100644 index 000000000000..fcd7e9018f48 --- /dev/null +++ b/1.11.0/docs/view-configuration.md @@ -0,0 +1,40 @@ +--- +title: "Configuration" +--- + + +# Configuration + +## View properties + +Iceberg views support properties to configure view behavior. Below is an overview of currently available view properties. + +| Property | Default | Description | +|----------------------------------|---------------------------|------------------------------------------------------------------------------------| +| write.metadata.compression-codec | gzip | Metadata compression codec: `none` or `gzip` | +| version.history.num-entries | 10 | Controls the number of `versions` to retain | +| replace.drop-dialect.allowed | false | Controls whether a SQL dialect is allowed to be dropped during a replace operation | + +### View behavior properties + +| Property | Default | Description | +|-------------------------------------|---------------------|--------------------------------------------------------------------| +| commit.retry.num-retries | 4 | Number of times to retry a commit before failing | +| commit.retry.min-wait-ms | 100 | Minimum time in milliseconds to wait before retrying a commit | +| commit.retry.max-wait-ms | 60000 (1 min) | Maximum time in milliseconds to wait before retrying a commit | +| commit.retry.total-timeout-ms | 1800000 (30 min) | Total retry timeout period in milliseconds for a commit | diff --git a/1.11.0/mkdocs.yml b/1.11.0/mkdocs.yml new file mode 100644 index 000000000000..471590e6a1f3 --- /dev/null +++ b/1.11.0/mkdocs.yml @@ -0,0 +1,78 @@ +# 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. + +site_name: docs/1.11.0 + +plugins: + - search + +nav: + - index.md + - Concepts: + - Tables: + - branching.md + - configuration.md + - encryption.md + - evolution.md + - maintenance.md + - metrics-reporting.md + - partitioning.md + - performance.md + - reliability.md + - schemas.md + - Views: + - view-configuration.md + - API: + - Quickstart: java-api-quickstart.md + - API: api.md + - File I/O: fileio.md + - Javadoc: ../../javadoc/latest/ + - Integrations: + - Apache Spark: + - spark-getting-started.md + - spark-configuration.md + - spark-ddl.md + - spark-procedures.md + - spark-queries.md + - spark-structured-streaming.md + - spark-writes.md + - Apache Flink: + - flink.md + - flink-connector.md + - flink-ddl.md + - flink-queries.md + - flink-writes.md + - flink-maintenance.md + - flink-configuration.md + - Kafka Connect: kafka-connect.md + - Apache Hive: hive.md + - Migration: + - Overview: table-migration.md + - Hive Migration: hive-migration.md + - Delta Lake Migration: delta-lake-migration.md + - Catalogs: + - Catalog properties: catalog-properties.md + - AWS Glue: aws/#glue-catalog + - AWS DynamoDB: aws/#dynamodb-catalog + - HadoopCatalog: https://iceberg.apache.org/javadoc/nightly/org/apache/iceberg/hadoop/HadoopCatalog.html + - HiveCatalog: hive/#global-hive-catalog + - jdbc.md + - custom-catalog.md + - nessie.md + - Storage: + - AWS S3: aws/#s3-fileio + - Dell ECS: dell.md