SICE-FM β€” cross-system query runtime prediction

A foundation model that predicts query execution time from a query plan, trained on one set of database systems and applied to another.

file what it is
pretrain_tsw_tgtspark_imdb_pd0.5_model.pt the bank β€” start here. 173 MB, 1,221 tensors, sha256 24218d31…60da809
pretrain_tsw_tgtspark_imdb_pd0.5_ctx8c_model.pt same recipe plus a system-context module (181 MB). Only if you want that ablation β€” see Which file.

Trained on PostgreSQL (170,987 plans) + DuckDB (171,000 plans). The imdb workload is excluded from pretraining everywhere, so imdb is a clean evaluation target.

⚠️ Read the filename correctly: tgtspark does not mean "trained on Spark"

The tgt<system> prefix names the system the bank is aimed at β€” the one deliberately held out of training β€” not the data it saw. tgtspark therefore means:

  • trained on: PostgreSQL + DuckDB
  • target (never seen in training): Spark

The naming comes from the training harness, where a bank is built by holding one system out and training on the rest. This is the PostgreSQL + DuckDB bank. If you want to predict on PostgreSQL or DuckDB plans it still works β€” those are systems it trained on β€” but the Spark numbers are the cross-system transfer result.

Training workloads

19 databases, each contributing ~9,000 plans per system:

accidents, airline, baseball, basketball, carcinogenesis, consumer, credit, employee, fhnk, financial, geneea, genome, hepatitis, movielens, seznam, ssb, tournament, tpc_h, walmart

imdb is excluded from every training system, which is what makes the published imdb results a genuine held-out evaluation rather than a memory check.

Which file

Take the first one. It is the plain model: plan-dropout 0.5, no context module (ctx: None), no meta-learning (reptile: None). The ctx8c variant adds a system-context encoder that helps on some targets and hurts on others β€” in our own STATS evaluation the simpler bank wins on 2 of 3 targets β€” so it is published for completeness, not as the default.

Nothing else from the paper lives in these weights. In particular the anchor is applied during few-shot adaptation, not pretraining, so it is not part of a bank and there is no "with/without anchor" checkpoint to choose between.

Architecture

61.9M parameters, two encoders fused into a scalar head.

  • Plan text β†’ all-MiniLM-L12-v2 (12 layers, hidden 384, vocab 30,522) with LoRA adapters. The trunk is adapted, so the full checkpoint is required β€” a base-model ID plus a head is not enough.
  • Structure β†’ a PRICE-style transformer, d_model 256, 8 heads Γ— 32.

Inputs

Two descriptions of the same query, both required:

  1. Cleaned plan text β€” the engine's EXPLAIN output with every measured quantity stripped (no latency, no actual rows, no timings). Leaving them in leaks the label.

  2. Structural features, as typed tokens β€” not one flat vector:

    slot count width contributes
    join histogram 10 40 400
    fanout 10 42 420
    table 6 4 24
    filter 11 75 825

    Concatenated that is a 1669-wide vector per clause, and the attention mask covers 38 positions β€” 37 feature tokens (10+10+6+11) plus one CLS. These come from database statistics, not from the plan text.

    (The embedding layers in the checkpoint are nn.Linear(41, 256) and nn.Linear(43, 256) for the first two slots. The extra input is appended by the model itself; supply 40 and 42.)

Output β€” converting a prediction to milliseconds

The model emits a scalar in (0, 1). Labels were normalized in log space, so invert with:

import numpy as np

# carrier = postgres, so the result is in MILLISECONDS
MINI, MAXI = 0.40413088509502776, 15.00133486099299

def to_ms(pred_norm):
    return np.exp(pred_norm * (MAXI - MINI) + MINI) - 0.001

Sanity check: to_ms(0.25) β‰ˆ 57.6 ms, to_ms(0.5) β‰ˆ 2214 ms. The training labels span 48.0 – 16,549 ms, i.e. roughly 0.24 – 0.65 in normalized space.

Normalization clips to [0.001, 1], so queries far outside that range saturate.

(For reference, the DuckDB fold of the same bank uses mini=12.887167490096259, maxi=24.141273665166107 and is in nanoseconds. The carrier above is the operative one.)

Training recipe

30 epochs, Adam lr 1e-4, batch 24, seed 42, plan-dropout 0.5, mixed batching (global shuffle over the concatenated systems).

Intended use and limitations

Research on learned cost estimation and cross-system transfer. Measured limits:

  • Unseen engines are where it wins β€” 20–46% better median q-error on an unseen workload.
  • In-system tail is weaker. On a system already represented in training, the adaptation-time distillation degrades p95 (+35% end-to-end in one setting).
  • Predictions are only as good as the statistics feeding the structural encoder. Queries with no usable statistics fall back to plan text alone and should not be trusted.

How to run it

You need three things. Get these right and the model will score any query plan.

input what it must be
--plans a CSV with a json column, one EXPLAIN document per row
--sql the matching SQL, one statement per line, line i ↔ plan row i
--db + --stats-dir a directory of PRICE statistics for that database
./score.py \
  --bank  pretrain_tsw_tgtspark_imdb_pd0.5 \
  --plans long_raw_postgres_imdb_job-light_c8220.csv \
  --sql   imdb_job-light_c8220.sql \
  --db    imdb \
  --engine postgres \
  --out   preds.csv

Output (preds.csv) carries its own provenance, then one row per query:

# bank: pretrain_tsw_tgtspark_imdb_pd0.5
# trained on: duckdb, postgres
# carrier: postgres (ms); scored engine: postgres
csv_row,pred_ms,pred_carrier_ms,pred_norm,true_ms,qerror,num_clauses
1,32.481995,32.481995,0.210765,32.787000,1.009390,1

true_ms and qerror are filled only when the plan document carries a measured runtime. An EXPLAIN without ANALYZE still predicts β€” it simply cannot be scored.

Getting the inputs right

  • Plans must come from the engine you pass to --engine. Plan text is cleaned with engine-specific rules; using the wrong one leaves measured columns in the text, which feeds the answer to the model. score.py refuses to guess rather than default.
  • SQL must be row-aligned with the plans, one statement per line, using the database's real table names. The statistics are built from the SQL text, never from the plan.
  • Statistics must cover the schema. score.py reports N succeeded, M failed; a query with no usable statistics falls back to plan text alone and is not trustworthy. If every query fails, it exits with an error rather than printing confident nonsense.

What to expect

Scoring 50 queries takes about 9 seconds on one GPU, including cold feature extraction.

A bank on its own is zero-shot β€” no few-shot adaptation. On 50 held-out imdb job-light queries this bank gives median q-error 2.86 (p90 5.5). The much lower numbers in the paper come after the few-shot adaptation phase, which is not part of these weights. Use the zero-shot figure as your plumbing check: if you see roughly this, your inputs are correct.

Status

The weights and the input contract above are final. The reference implementation (score.py and the feature/cleaning code it calls) is being published as a single repository; until that lands you can reproduce the pipeline from the specification in this card.

License

Apache-2.0, inherited from the all-MiniLM-L12-v2 base model.

Downloads last month

-

Downloads are not tracked for this model. How to track
Inference Providers NEW
This model isn't deployed by any Inference Provider. πŸ™‹ Ask for provider support