Skip to content

SQL

mink-query is the SQL engine. It is a separate process built on DataFusion with the cluster registered as a catalog. A statement is planned into one read per bucket, the reads run in parallel, and the result streams out as Arrow. Nothing is cached between statements and nothing is written.

SQL clientFlight SQL, shellmink-queryDataFusion plancatalog, pushdown, splitsMinkScanone stream per splitmerge, filter, aggregateIceberg catalogsnapshot, file listnodes :9123offsets, log tailobject storagedata, delete files

Catalog

DataFusionMink
Catalog minkThe cluster
SchemaDatabase
TableTable, with its Arrow schema from the descriptor

The catalog is refreshed before every statement with list_databases and list_tables, so a table created a moment ago is visible. A table referenced by a statement is opened on first use: descriptor, partition list, and a lookup client when it has a primary key. information_schema is on. Unqualified names resolve in the database given by -d, or default.

Pushdown

DataFusion hands the table provider a projection, a limit and a list of filters. Each filter is classified:

FilterClassificationEffect
Pins only partition keysExactPrunes partitions. Not re-applied
Pins a partition, bucket or primary-key columnInexactRoutes to buckets or keys. Re-applied above the scan
Converts to a lake predicateInexactSkips Iceberg files and row groups. Re-applied above the scan
Anything elseUnsupportedEvaluated by DataFusion above the scan

A filter pins a column when it is column = literal, column IN (literals), an OR of those on one column, or an AND of pins. Pins on the same column across filters intersect. A lake predicate is a comparison of a column with a literal, IN, IS NULL, IS NOT NULL, NOT, AND or OR, over the types the Iceberg scan can evaluate. A widening cast that DataFusion inserts around a column folds into the literal. A narrowing one does not.

Projection is pushed to every split as field indexes: the node projects log batches before sending and the Iceberg reader projects Parquet columns. The limit stops each split's stream at limit rows and DataFusion applies the global limit above.

Splits

One statement over one table becomes a set of splits. The shape of the table and what the filters pin decide the kind:

primary key pinnedLookup, one splityesnolake snapshotprimary keybucket keysnoyesSnapshotone per bucketUnion per bucketlog onlyLake per partition+ tail per bucketUnion per bucketlake + tailyesnoyesno
SplitReadsFrom
LookupThe pinned keys, at most 1024 combinations, one lookup actionBucket leaders
SnapshotThe KV store of one bucketBucket leader
UnionLake files for one bucket at the pinned snapshot, then the log tail from log_end_offset to the high watermark, merged as in Union readObject storage and the bucket leader
LakeLake files for one partition, no bucket. Used when rows are not bucketed by key so files cannot be assigned to a bucketObject storage

Which buckets appear:

PinnedBuckets
Every partition key and every bucket key, at most 1024 combinationsOnly the buckets the rows route to
Some partition keysEvery bucket of the matching partitions
Nothing routableEvery bucket

The lake snapshot is pinned once per statement from lake_snapshot: one snapshot_id and each bucket's log_end_offset. Every split reads the same snapshot, so the statement is consistent across buckets. Iceberg planning happens once per table with the lake predicate applied and the resulting file tasks are handed to the splits.

Execution

MinkScan is the physical operator. It declares one DataFusion partition per split, so a scan over a table with 16 buckets runs 16 streams concurrently. target_partitions does not change this. It sets the parallelism of the operators above the scan.

Each stream is opened when DataFusion polls it:

  • A Union split reads Parquet from object storage through the Iceberg catalog in [lake] and the log tail from the bucket leader with a Scan bounded to log_end_offset through the high watermark. The merge runs in mink-query on the same code the node uses for its Union ticket.
  • A Lake split reads its files the same way, with no log.
  • A Snapshot split is one Snapshot ticket to the leader.
  • A Lookup split is one lookup action with every key row.

EXPLAIN shows the scan as MinkScan: table=, splits=, files=, keys=, filter=, projection=, limit=. Row and byte statistics come from the Iceberg file metadata and the log offsets, so DataFusion can choose join sides.

SettingDefaultEffect
batch_size8192Rows per Arrow batch through the plan
target_partitionsCoresParallelism above the scan
memory_limitNoneFair spill pool for sorts, joins and aggregates

Flight SQL

mink-query serve exposes Flight SQL on 0.0.0.0:9130. The statement handle is the SQL text, so any mink-query replica behind a load balancer can serve any ticket.

CommandResult
CommandStatementQueryPlans once for the schema, then plans and executes on DoGet
CreatePreparedStatement, CommandPreparedStatementQuerySame, with the SQL as the handle. No parameters
GetCatalogs, GetDbSchemas, GetTables, GetTableTypesFrom the refreshed catalog. One catalog, one table type TABLE
GetSqlInfoServer name, version, Arrow version, FlightSqlServerReadOnly = true

DDL and DML are not served.

Configuration

FlagEnvironmentDefault
-b, --bootstrapMINK_BOOTSTRAPgrpc://127.0.0.1:9123, comma separated for several
-c, --configMINK_QUERY_CONFIGNone. TOML file with [lake] and engine settings
-d, --databaseMINK_QUERY_DATABASEdefault
--listen on serveMINK_QUERY_LISTEN0.0.0.0:9130

The TOML file carries the same [lake] block as a node, so the engine reads the tables the node tiers, plus batch_size, target_partitions and memory_limit. MINK_QUERY_* environment variables override the file. Without a [lake] block every table is read from the log and KV state alone.

toml
memory_limit = 4294967296

[lake]
format = "iceberg"
catalog = "rest"
uri = "http://iceberg-rest:8181"
warehouse = "s3://mink-lake"

[lake.properties]
"s3.endpoint" = "http://rustfs:9000"
"s3.region" = "us-east-1"
CommandPurpose
serveFlight SQL
exec -e <sql>, exec -f <file>, exec < stdinRun statements. --format table, json or csv
shellInteractive