Query results and plans
You can read a query's result rows and inspect its execution metrics. You can also inspect the plan before you run the query.
Here we use a reviews table with a movie review ID in id and review
text in body. The table is already registered with a session. The query
returns the IDs of reviews that discuss the movie's ending:
query = session.sql("""
SELECT r.id
FROM reviews r
WHERE AI.IF(PROMPT('Does this review discuss the ending? {0}', r.body))
""", dialect="bq")For queries submitted to Quail Server, see the server guide.
Read the results
Call run() to execute the query, then collect() to read its rows:
result = query.run()
rows = result.collect()
print(rows.to_pydict())run() returns a QueryResult with execution metrics and access to the
result rows. collect() reads the rows into a pyarrow.Table.
Calling result.collect() does not run the model again.
query.collect() is a shortcut for query.run().collect(). It returns
the table directly. Use query.run() when you also want a QueryResult
to inspect the execution metrics and plan.
To read the completed query's results in batches, use
result.execute_stream(). It returns an Arrow reader:
with result.execute_stream(batch_rows=1024) as reader:
for batch in reader:
print(batch.to_pydict())You can pass limit to collect() or execute_stream() to read fewer
result rows. The limit does not reduce the model execution already performed.
Classification answers
For an AI.CLASSIFY query, result.answer_tables includes all computed
labels, including those for rows removed by later filters.
For example, classify sentiment in the registered reviews table:
result = session.sql("""
SELECT r.id,
AI.CLASSIFY(r.body, ARRAY['positive', 'negative', 'mixed']) AS sentiment
FROM reviews r
""", dialect="bq").run()
labels = result.answer_tables["classifies"]["sentiment"]
print(labels.to_pydict())The table contains r and sentiment. The r values are input row
indices starting at zero, rather than review IDs. For joined rows,
there is an index column for each table alias.
Preview the plan
Call explain() to inspect the plan without running the model:
print(query.explain())Quail plans the query but does not load the model onto the GPU. The output contains a logical plan and a physical plan.
Read the logical plan
A logical plan describes the operations requested by the query. For the review query, the logical plan has this structure:
Project: r.id
SemanticFilter
PROMPT('DOCUMENT:\n{0}\n\nDoes this review discuss the ending?', r.body)
Scan reviews as r [body, id]Read the plan from the bottom upward:
Scanreads the review text and IDs.SemanticFilteruses the model to answer the question and keeps reviews with aTRUEanswer.Projectselects the ID column for the result.
The planner can change the order of operations while preserving the query's meaning. See Logical plans for more detail.
Read the physical plan
A physical plan specifies the operators Quail will execute. The output shows the backend, model, GPU count, and token budgets. It also shows estimated row counts and GPU times for the operators.
For example, for three input reviews, the physical plan includes lines like these. The excerpt omits the token budgets and prompt text:
physical: backend=quail, model=qwen3-4b-fp8, gpus=1
est. rows est. pass est. time
Project: r.id 0.6
AiFilter: r 0.6 1.09 ms
predicate 1 3 20%*
Scan reviews as r 3
* no selectivity was given; the planner assumes 20%Quail expects the filter to keep 20% of the three reviews, or 0.6 rows. An estimated row count can be fractional. The result of an executed query always has a whole number of rows.
A predicate is a condition used to keep or remove rows. In the review
query, the predicate is the question passed to AI.IF.
On an operator line, est. rows is the estimated number of output rows.
On a predicate line, it is the estimated number of documents or pairs
that reach the predicate.
Selectivity is the fraction of rows that pass a condition.
The est. pass column shows the selectivity used by the planner.
If you did not supply a selectivity, the output marks the default
with * and explains it below the plan.
Quail estimates GPU time using peak compute speed and memory bandwidth. These estimates are often too optimistic. See Cost estimates for more details.
Show execution settings
Use query.explain(verbose=True) when you need to inspect execution
settings or edit a physical plan.
It adds operator IDs, connections, and KV retention settings.
It also shows the prefix sharing and attention settings described
in Execution.
Measure the plan
After running the query, call result.explain() to compare measurements
with the plan's estimates:
print(result.explain())The method uses the completed run. It does not run the model again. It returns the physical plan with measurement columns, as shown below.
| Columns | Meaning |
|---|---|
est. rows, rows | Estimated and measured output rows on an operator line, or documents and pairs evaluated on a predicate line. |
est. pass, pass | Estimated and measured fractions that passed a predicate. |
est. time, time | Estimated GPU time and measured operator time. |
fresh tokens | Token positions computed by the model. Computing a position again counts again. Reading saved KV does not count. |
If a predicate passes more rows than expected, later operators may receive more input than the planner estimated. Compare the row counts and pass fractions before trying to explain a difference in time.
For query time, model startup time, and token counts, read
result.report. The Metrics page explains
the fields. Use result.explain(verbose=True) for additional operator
metrics, including KV hits and misses.
Run a query to inspect its measurements
If you do not need to keep the result rows, you can use:
print(query.explain(analyze=True))The method runs the query once and returns the logical plan, the physical plan with measurements, and a summary of the run.
The excerpt below illustrates a run where the model keeps two of three reviews. The measured times and token counts are illustrative, rather than performance measurements:
est. rows rows est. pass pass est. time time fresh tokens
Project: r.id 0.6 2 <1 ms
AiFilter: r 0.6 2 1.09 ms 1 s 80
predicate 1 3 3 20%* 66.7%
Scan reviews as r 3 3 <1 ms
run:
query time 1 s (model startup excluded)
startup 500 ms
tokens 80 freshThe filter kept two reviews, compared with the estimate of 0.6 rows.
The predicate evaluated three reviews and passed 66.7%, compared with
the estimated 20%. The time column shows the measured operator time;
est. time shows the planner's estimate. The summary lists model startup
separately from query time.
result.explain() returns the physical plan portion of the output.
Read the query totals from result.report when you use that method.
Each call with analyze=True runs the query again.
To keep the rows and metrics from the same run, use query.run()
and inspect the returned result as shown above.
Refusals
A refusal explains why Quail cannot execute a query with the selected configuration. For example, a review and its prompt may exceed the number of tokens that fit in one model forward pass.
When planning returns a refusal, query.explain() displays its reason
instead of a plan. query.run() raises quail.RefusalError.
Read the reason to identify which input or configuration needs to change.