apache/iceberg · error

ALTER TABLE contains multiple ordering clauses

Error message

ALTER TABLE contains multiple ordering clauses

What it means

Iceberg allows at most one ordering clause per ALTER TABLE ... WRITE statement. A second writeOrderingSpec element causes this AnalysisException because ordering cannot be both e.g. UNORDERED and ORDERED BY multiple specs.

Solutions

  1. Keep a single ordering clause: choose ORDERED BY, LOCALLY ORDERED BY, or UNORDERED
  2. Merge multiple ordering columns into one ORDERED BY col1, col2, ... list
  3. Run separate ALTER TABLE statements if replacing an existing write order

Example fix

// before
ALTER TABLE t WRITE ORDERED BY a LOCALLY ORDERED BY b;
// after
ALTER TABLE t WRITE LOCALLY ORDERED BY a, b;
Defensive patterns

Strategy: validation

Validate before calling

val orderCount = "(ORDERED\\s+BY|LOCALLY\\s+ORDERED\\s+BY|UNORDERED)".r.findAllIn(sqlText).size
require(orderCount <= 1, "Only one ordering clause allowed")

Try / catch

try { spark.sql(ddl) } catch { case e: AnalysisException if e.getMessage.contains("multiple ordering") => log.error("duplicate ordering clause"); throw e }

Prevention

When it happens

Trigger: Writing two ordering clauses in one statement, e.g. 'ALTER TABLE t WRITE ORDERED BY a LOCALLY ORDERED BY b' or '... ORDERED BY a UNORDERED'.

Common situations: Attempting to specify global and local ordering simultaneously; generated SQL appending an ordering clause that already exists.

Understand the failure class

Background: Conflicting config options: "cannot be used together" — configuration validation errors across open-source libraries — this error's family across 162 libraries.

Related errors


AI-assisted analysis of apache/iceberg@86d9c8fc54 (2026-09-12). Data as JSON: /api/errors/48abbdf440382814. Report an issue: GitHub.

Appendix: source

Thrown at spark/v3.5/spark-extensions/src/main/scala/org/apache/spark/sql/catalyst/parser/extensions/IcebergSqlExtensionsAstBuilder.scala:274

    val ordering = if (orderingSpec != null && orderingSpec.order != null) {
      toSeq(orderingSpec.order.fields).map(typedVisit[(Term, SortDirection, NullOrder)])
    } else {
      Seq.empty
    }

    SetWriteDistributionAndOrdering(tableName, distributionMode, ordering)
  }

  private def toDistributionAndOrderingSpec(
      writeSpec: WriteSpecContext): (WriteDistributionSpecContext, WriteOrderingSpecContext) = {

    if (writeSpec.writeDistributionSpec.size > 1) {
      throw new AnalysisException("ALTER TABLE contains multiple distribution clauses")
    }

    if (writeSpec.writeOrderingSpec.size > 1) {
      throw new AnalysisException("ALTER TABLE contains multiple ordering clauses")
    }

    val distributionSpec = toBuffer(writeSpec.writeDistributionSpec).headOption.orNull
    val orderingSpec = toBuffer(writeSpec.writeOrderingSpec).headOption.orNull

    (distributionSpec, orderingSpec)
  }

  /**
   * Create an order field.
   */
  override def visitOrderField(ctx: OrderFieldContext): (Term, SortDirection, NullOrder) = {
    val term = Spark3Util.toIcebergTerm(typedVisit[Transform](ctx.transform))
    val direction = Option(ctx.ASC)
      .map(_ => SortDirection.ASC)
      .orElse(Option(ctx.DESC).map(_ => SortDirection.DESC))
      .getOrElse(SortDirection.ASC)
    val nullOrder = Option(ctx.FIRST)

View on GitHub (pinned to 86d9c8fc54)