Skip to main content
When you call execute(), FiQueLa processes the query through a fixed pipeline. Understanding this order helps you write correct queries and diagnose unexpected results.

Execution order

1

FROM and JOIN

Data is loaded from the source specified in the FROM clause. If one or more JOIN clauses are present, the joined data is merged with the primary data at this point, before any filtering occurs.
2

WHERE

Row-level filters are applied. Only rows matching all WHERE conditions pass through. Filtering happens against the raw data before any grouping or computed columns are available.
3

DISTINCT

If DISTINCT is specified, duplicate rows are removed based on the selected columns. This step is skipped when GROUP BY is present, because grouping already eliminates duplicates.
4

GROUP BY

Rows are grouped by the columns listed in GROUP BY. This prepares the data for aggregate functions such as SUM, COUNT, AVG, MIN, and MAX.
5

SELECT

Column expressions, scalar functions, and aggregate functions defined in SELECT are evaluated. This is the only place where functions may be invoked.
6

HAVING

Filters the output produced by SELECT. Primarily used to filter groups after aggregation — for example, keeping only groups where SUM(sales) > 1000. Reference computed columns by their alias.
7

ORDER BY

Results are sorted by the specified columns or aliases. Supported sort directions are ASC, DESC, SHUFFLE, and NATSORT. Sorting requires all results to be loaded into memory.
8

EXCLUDE

Listed columns are removed from the output rows. EXCLUDE runs after SELECT so you can compute a value in SELECT, use it in HAVING or ORDER BY, and then drop it from the final output.
9

LIMIT and OFFSET

OFFSET skips the specified number of rows, then LIMIT caps the total number of rows returned. When no ORDER BY is present, FiQueLa applies the limit directly in the stream without buffering the full result set.
10

UNION / UNION ALL

Each UNION or UNION ALL subquery runs its own independent pipeline (FROM → WHERE → SELECT → …) and its results are appended to the main result. UNION removes duplicate rows using hash-based deduplication; UNION ALL keeps all rows. All combined queries must select the same number of columns.
Functions can only be used in the SELECT clause. To filter or sort by a function’s result, assign it an alias in SELECT and reference that alias in HAVING or ORDER BY.
SELECT price, ROUND(price, 2) AS rounded_price
FROM csv(products.csv).*
HAVING rounded_price > 100
ORDER BY rounded_price DESC

EXPLAIN and EXPLAIN ANALYZE

You can inspect the query plan without running the full query, or run it and collect real metrics. EXPLAIN — returns a plan-only table with null metrics. Use it to understand which phases will run and in what order before executing. EXPLAIN ANALYZE — executes the query and records real row counts, timing (time_ms, duration_pct), and peak memory usage (mem_peak_kb) for each phase. Phases reported correspond directly to the lifecycle stages above: stream, join, where, group, having, sort, limit. When unions are present, each branch reports its own sub-phases (e.g. union_stream, union_where) followed by a summary row (union). Multiple unions are indexed: union_1_stream, union_2_stream, and so on.
use FQL\Query;

$query = Query\Provider::fromFileQuery('xml(customers.xml).customers.customer')
    ->select('name, SUM(sales)')->as('total_sales')
    ->from('customers.customer')
    ->where('age', \FQL\Enum\Operator::GREATER_THAN, 30)
    ->groupBy('name')
    ->having('total_sales', \FQL\Enum\Operator::GREATER_THAN, 1000)
    ->orderBy('total_sales')->desc()
    ->limit(10);

// Plan only — no execution
$plan = $query->execute()->explain();

// Plan with real metrics
$analysis = $query->execute()->explain(true);
See EXPLAIN & benchmarking for full output format details.

Worked example

The following query demonstrates every phase of the lifecycle:
SELECT
    name,
    SUM(sales) AS total_sales,
    ROUND(total_sales, 2) AS rounded_sales
EXCLUDE total_sales
FROM xml(customers.xml).customers.customer
WHERE age > 30
GROUP BY name
HAVING total_sales > 1000
ORDER BY total_sales DESC
LIMIT 10
UNION ALL
SELECT
    name,
    SUM(sales) AS total_sales,
    ROUND(total_sales, 2) AS rounded_sales
FROM xml(partners.xml).partners.partner
WHERE age > 25
GROUP BY name;
Execution order for the main query:
  1. FROM — load customers.xml, navigate to customers.customer
  2. WHERE — keep rows where age > 30
  3. GROUP BY — group rows by name
  4. SELECT — compute SUM(sales) per group and ROUND(total_sales, 2)
  5. HAVING — keep groups where total_sales > 1000
  6. ORDER BY — sort by total_sales descending (loads all results into memory)
  7. EXCLUDE — drop the total_sales column from output rows
  8. LIMIT — return the top 10 rows
  9. UNION ALL — execute the partner subquery independently and append all its rows