ui: DataGrid: Improve SQL datasource performance (#4140)

- Don't reload row count if only sorting has changed.
- Reduce flicker on reloads.
- Improve and simplify API to SQLDataSource.
diff --git a/ui/src/components/aggregation_adapter.ts b/ui/src/components/aggregation_adapter.ts
index 8e5f052..cc86aa7 100644
--- a/ui/src/components/aggregation_adapter.ts
+++ b/ui/src/components/aggregation_adapter.ts
@@ -30,6 +30,7 @@
 import {AggregationPanel} from './aggregation_panel';
 import {Pivot} from './widgets/datagrid/model';
 import {SQLDataSource} from './widgets/datagrid/sql_data_source';
+import {createSimpleSchema} from './widgets/datagrid/sql_schema';
 import {BarChartData, ColumnDef} from './aggregation';
 import {
   createPerfettoTable,
@@ -237,7 +238,8 @@
             data = await aggregation?.prepareData(trace.engine);
             dataSource = new SQLDataSource({
               engine: trace.engine,
-              baseQuery: data.tableName,
+              sqlSchema: createSimpleSchema(data.tableName),
+              rootSchemaName: 'query',
             });
           }
         });
diff --git a/ui/src/components/widgets/datagrid/add_column_menu.ts b/ui/src/components/widgets/datagrid/add_column_menu.ts
index 9c347a0..bdcf02e 100644
--- a/ui/src/components/widgets/datagrid/add_column_menu.ts
+++ b/ui/src/components/widgets/datagrid/add_column_menu.ts
@@ -101,8 +101,7 @@
     } else if (isParameterizedColumnDef(entry)) {
       // Parameterized column - show available keys from datasource
       const title = typeof entry.title === 'string' ? entry.title : columnName;
-      const availableKeys =
-        context.dataSource.result?.parameterKeys?.get(fullPath);
+      const availableKeys = context.dataSource.parameterKeys?.get(fullPath);
       menuItems.push(
         m(
           MenuItem,
diff --git a/ui/src/components/widgets/datagrid/data_source.ts b/ui/src/components/widgets/datagrid/data_source.ts
index 1060226..8b09e06 100644
--- a/ui/src/components/widgets/datagrid/data_source.ts
+++ b/ui/src/components/widgets/datagrid/data_source.ts
@@ -21,7 +21,20 @@
 }
 
 export interface DataSource {
-  readonly result?: DataSourceResult;
+  // The row data for the current data grid state (filters, sorting, pagination,
+  // etc)
+  readonly rows?: DataSourceRows;
+
+  // Available distinct values for specified columns (for filter dropdowns)
+  readonly distinctValues?: ReadonlyMap<string, readonly SqlValue[]>;
+
+  // Available parameter keys for parameterized columns (e.g., for 'args' ->
+  // ['foo', 'bar'])
+  readonly parameterKeys?: ReadonlyMap<string, readonly string[]>;
+
+  // Computed aggregate totals for each aggregate column (grand total across all
+  // filtered rows)
+  readonly aggregateTotals?: ReadonlyMap<string, SqlValue>;
 
   // Whether the data source is currently loading data/updating.
   readonly isLoading?: boolean;
@@ -36,23 +49,33 @@
 }
 
 export interface DataSourceModel {
+  // The columns to display, including their sort direction if any
   readonly columns?: readonly Column[];
+
+  // Active filters to apply to the data
   readonly filters?: readonly Filter[];
+
+  // Pagination settings (offset and limit for the current page)
   readonly pagination?: Pagination;
+
+  // Pivot configuration for grouped/aggregated views
   readonly pivot?: Pivot;
+
+  // Columns for which to fetch distinct values (for filter dropdowns)
   readonly distinctValuesColumns?: ReadonlySet<string>;
-  // Request parameter keys for these parameterized column prefixes (e.g., 'args', 'skills')
+
+  // Parameterized column prefixes for which to fetch available keys (e.g.,
+  // 'args')
   readonly parameterKeyColumns?: ReadonlySet<string>;
 }
 
-export interface DataSourceResult {
+export interface DataSourceRows {
+  // The total number of rows available in the dataset
   readonly totalRows: number;
+
+  // The offset of the first row in this batch
   readonly rowOffset: number;
+
+  // The actual row data for this batch
   readonly rows: readonly Row[];
-  readonly isLoading?: boolean;
-  readonly distinctValues?: ReadonlyMap<string, readonly SqlValue[]>;
-  // Available parameter keys for parameterized columns (e.g., for 'args' -> ['foo', 'bar'])
-  readonly parameterKeys?: ReadonlyMap<string, readonly string[]>;
-  // Computed aggregate totals for each aggregate column (grand total across all filtered rows)
-  readonly aggregateTotals?: ReadonlyMap<string, SqlValue>;
 }
diff --git a/ui/src/components/widgets/datagrid/datagrid.ts b/ui/src/components/widgets/datagrid/datagrid.ts
index 98f1bdd..c4867a6 100644
--- a/ui/src/components/widgets/datagrid/datagrid.ts
+++ b/ui/src/components/widgets/datagrid/datagrid.ts
@@ -287,7 +287,7 @@
   readonly schema: SchemaRegistry;
   readonly rootSchema: string;
   readonly datasource: DataSource;
-  readonly result: DataSource['result'];
+  readonly result: DataSource['rows'];
   readonly columnInfoCache: Map<string, ReturnType<typeof getColumnInfo>>;
   readonly structuredQueryCompatMode: boolean;
 }
@@ -300,7 +300,7 @@
   readonly schema: SchemaRegistry;
   readonly rootSchema: string;
   readonly datasource: DataSource;
-  readonly result: DataSource['result'];
+  readonly result: DataSource['rows'];
   readonly pivot: Pivot;
   readonly structuredQueryCompatMode: boolean;
 }
@@ -390,12 +390,12 @@
         );
       },
       getRowCount: () => {
-        return datasource?.result?.totalRows;
+        return datasource?.rows?.totalRows;
       },
     });
 
     // Extract the result from the datasource
-    const result = datasource.result;
+    const result = datasource.rows;
 
     // Determine if we're in pivot mode (has groupBy columns and not drilling down)
     const isPivotMode =
@@ -979,7 +979,6 @@
       schema,
       rootSchema,
       datasource,
-      result,
       columnInfoCache,
       structuredQueryCompatMode,
     } = ctx;
@@ -1007,7 +1006,7 @@
         m(FilterMenu, {
           columnType,
           structuredQueryCompatMode,
-          distinctValues: result?.distinctValues?.get(field),
+          distinctValues: datasource.distinctValues?.get(field),
           valueFormatter: (v) => colInfo?.cellFormatter?.(v, {}) ?? String(v),
           onFilterAdd: (filter) => this.addFilter({field, ...filter}, attrs),
           onRequestDistinctValues: () => this.distinctValuesColumns.add(field),
@@ -1086,7 +1085,7 @@
       // Build subContent showing grand total if column has an aggregate
       let subContent: m.Children;
       if (aggregate) {
-        const totalValue = result?.aggregateTotals?.get(field);
+        const totalValue = datasource.aggregateTotals?.get(field);
         const isLoading = totalValue === undefined;
         // Don't show grand total for ANY aggregation (it's just an arbitrary value)
         subContent =
@@ -1184,7 +1183,6 @@
       rootSchema,
       datasource,
       pivot,
-      result,
       structuredQueryCompatMode,
     } = ctx;
 
@@ -1221,7 +1219,7 @@
         m(FilterMenu, {
           columnType,
           structuredQueryCompatMode,
-          distinctValues: result?.distinctValues?.get(field),
+          distinctValues: datasource.distinctValues?.get(field),
           valueFormatter: (v) => colInfo?.cellFormatter?.(v, {}) ?? String(v),
           onFilterAdd: (filter) => this.addFilter({field, ...filter}, attrs),
           onRequestDistinctValues: () => this.distinctValuesColumns.add(field),
@@ -1362,7 +1360,7 @@
 
       // Build subContent showing grand total with aggregate symbol
       // Don't show grand total for ANY aggregation (it's just an arbitrary value)
-      const aggregateTotalValue = result?.aggregateTotals?.get(alias);
+      const aggregateTotalValue = datasource.aggregateTotals?.get(alias);
       const symbol = agg.function;
       const isLoading = aggregateTotalValue === undefined;
       const subContent =
diff --git a/ui/src/components/widgets/datagrid/in_memory_data_source.ts b/ui/src/components/widgets/datagrid/in_memory_data_source.ts
index 5bcfd7b..c3fdb38 100644
--- a/ui/src/components/widgets/datagrid/in_memory_data_source.ts
+++ b/ui/src/components/widgets/datagrid/in_memory_data_source.ts
@@ -15,7 +15,7 @@
 import {stringifyJsonWithBigints} from '../../../base/json_utils';
 import {assertUnreachable} from '../../../base/logging';
 import {Row, SqlValue} from '../../../trace_processor/query_result';
-import {DataSource, DataSourceModel, DataSourceResult} from './data_source';
+import {DataSource, DataSourceModel, DataSourceRows} from './data_source';
 import {Column, Filter, Pivot} from './model';
 
 export class InMemoryDataSource implements DataSource {
@@ -35,17 +35,32 @@
     this.filteredSortedData = data;
   }
 
-  get result(): DataSourceResult {
+  get rows(): DataSourceRows {
     return {
       rowOffset: 0,
       rows: this.filteredSortedData,
       totalRows: this.filteredSortedData.length,
-      distinctValues: this.distinctValuesCache,
-      parameterKeys: this.parameterKeysCache,
-      aggregateTotals: this.aggregateTotalsCache,
     };
   }
 
+  get distinctValues(): ReadonlyMap<string, readonly SqlValue[]> | undefined {
+    return this.distinctValuesCache.size > 0
+      ? this.distinctValuesCache
+      : undefined;
+  }
+
+  get parameterKeys(): ReadonlyMap<string, readonly string[]> | undefined {
+    return this.parameterKeysCache.size > 0
+      ? this.parameterKeysCache
+      : undefined;
+  }
+
+  get aggregateTotals(): ReadonlyMap<string, SqlValue> | undefined {
+    return this.aggregateTotalsCache.size > 0
+      ? this.aggregateTotalsCache
+      : undefined;
+  }
+
   notify({
     columns,
     filters = [],
diff --git a/ui/src/components/widgets/datagrid/in_memory_data_source_unittest.ts b/ui/src/components/widgets/datagrid/in_memory_data_source_unittest.ts
index 239895d..3da25b6 100644
--- a/ui/src/components/widgets/datagrid/in_memory_data_source_unittest.ts
+++ b/ui/src/components/widgets/datagrid/in_memory_data_source_unittest.ts
@@ -76,7 +76,7 @@
   });
 
   test('initialization', () => {
-    const result = dataSource.result;
+    const result = dataSource.rows;
     expect(result.rowOffset).toBe(0);
     expect(result.totalRows).toBe(sampleData.length);
     expect(result.rows).toEqual(sampleData);
@@ -86,7 +86,7 @@
     test('equality filter', () => {
       const filters: Filter[] = [{field: 'name', op: '=', value: 'Alice'}];
       dataSource.notify({filters});
-      const result = dataSource.result;
+      const result = dataSource.rows;
       expect(result.totalRows).toBe(1);
       expect(result.rows[0].name).toBe('Alice');
     });
@@ -94,7 +94,7 @@
     test('inequality filter', () => {
       const filters: Filter[] = [{field: 'active', op: '!=', value: 1}];
       dataSource.notify({filters});
-      const result = dataSource.result;
+      const result = dataSource.rows;
       expect(result.totalRows).toBe(3); // Bob, David, Mallory
       result.rows.forEach((row) => expect(row.active).toBe(0));
     });
@@ -102,7 +102,7 @@
     test('less than filter', () => {
       const filters: Filter[] = [{field: 'value', op: '<', value: 150}];
       dataSource.notify({filters});
-      const result = dataSource.result;
+      const result = dataSource.rows;
       // David (null), Alice (100), Eve (100)
       expect(result.totalRows).toBe(3);
       expect(result.rows.map((r) => r.id).sort()).toEqual([1, 4, 5]);
@@ -111,7 +111,7 @@
     test('less than or equal filter', () => {
       const filters: Filter[] = [{field: 'value', op: '<=', value: 150}];
       dataSource.notify({filters});
-      const result = dataSource.result;
+      const result = dataSource.rows;
       // David (null), Alice (100), Charlie (150), Eve (100)
       expect(result.totalRows).toBe(4);
       expect(result.rows.map((r) => r.id).sort()).toEqual([1, 3, 4, 5]);
@@ -120,7 +120,7 @@
     test('greater than filter', () => {
       const filters: Filter[] = [{field: 'value', op: '>', value: 200}];
       dataSource.notify({filters});
-      const result = dataSource.result;
+      const result = dataSource.rows;
       expect(result.totalRows).toBe(2); // Mallory (300n), Trent (250n)
       expect(result.rows.map((r) => r.id).sort()).toEqual([6, 7]);
     });
@@ -128,7 +128,7 @@
     test('greater than or equal filter with bigint', () => {
       const filters: Filter[] = [{field: 'value', op: '>=', value: 250n}];
       dataSource.notify({filters});
-      const result = dataSource.result;
+      const result = dataSource.rows;
       expect(result.totalRows).toBe(2); // Mallory, Trent
       expect(result.rows.map((r) => r.id).sort()).toEqual([6, 7]);
     });
@@ -136,7 +136,7 @@
     test('is null filter', () => {
       const filters: Filter[] = [{field: 'value', op: 'is null'}];
       dataSource.notify({filters});
-      const result = dataSource.result;
+      const result = dataSource.rows;
       expect(result.totalRows).toBe(1);
       expect(result.rows[0].id).toBe(4); // David
     });
@@ -144,7 +144,7 @@
     test('is not null filter', () => {
       const filters: Filter[] = [{field: 'blob', op: 'is not null'}];
       dataSource.notify({filters});
-      const result = dataSource.result;
+      const result = dataSource.rows;
       expect(result.totalRows).toBe(6); // All except Charlie
       expect(result.rows.find((r) => r.id === 3)).toBeUndefined();
     });
@@ -152,7 +152,7 @@
     test('glob filter', () => {
       const filters: Filter[] = [{field: 'name', op: 'glob', value: 'A*e'}];
       dataSource.notify({filters});
-      const result = dataSource.result;
+      const result = dataSource.rows;
       expect(result.totalRows).toBe(1);
       expect(result.rows[0].name).toBe('Alice');
     });
@@ -160,7 +160,7 @@
     test('glob filter with ?', () => {
       const filters: Filter[] = [{field: 'name', op: 'glob', value: 'B?b'}];
       dataSource.notify({filters});
-      const result = dataSource.result;
+      const result = dataSource.rows;
       expect(result.totalRows).toBe(1);
       expect(result.rows[0].name).toBe('Bob');
     });
@@ -171,7 +171,7 @@
         {field: 'tag', op: '=', value: 'A'},
       ];
       dataSource.notify({filters});
-      const result = dataSource.result;
+      const result = dataSource.rows;
       expect(result.totalRows).toBe(3); // Alice, Charlie, Trent
       result.rows.forEach((row) => {
         expect(row.active).toBe(1);
@@ -184,7 +184,7 @@
         {field: 'name', op: '=', value: 'NonExistent'},
       ];
       dataSource.notify({filters});
-      const result = dataSource.result;
+      const result = dataSource.rows;
       expect(result.totalRows).toBe(0);
       expect(result.rows.length).toBe(0);
     });
@@ -194,7 +194,7 @@
     test('sort by string ascending', () => {
       const columns: Column[] = [{field: 'name', sort: 'ASC'}];
       dataSource.notify({columns, filters: []});
-      const result = dataSource.result;
+      const result = dataSource.rows;
       expect(result.rows.map((r) => r.name)).toEqual([
         'Alice',
         'Bob',
@@ -209,7 +209,7 @@
     test('sort by string descending', () => {
       const columns: Column[] = [{field: 'name', sort: 'DESC'}];
       dataSource.notify({columns, filters: []});
-      const result = dataSource.result;
+      const result = dataSource.rows;
       expect(result.rows.map((r) => r.name)).toEqual([
         'Trent',
         'Mallory',
@@ -224,7 +224,7 @@
     test('sort by number ascending (includes nulls)', () => {
       const columns: Column[] = [{field: 'value', sort: 'ASC'}];
       dataSource.notify({columns, filters: []});
-      const result = dataSource.result;
+      const result = dataSource.rows;
       // Nulls first, then 100, 100, 150, 200, 250n, 300n
       expect(result.rows.map((r) => r.id)).toEqual([4, 1, 5, 3, 2, 7, 6]);
     });
@@ -232,7 +232,7 @@
     test('sort by number descending (includes nulls and bigint)', () => {
       const columns: Column[] = [{field: 'value', sort: 'DESC'}];
       dataSource.notify({columns, filters: []});
-      const result = dataSource.result;
+      const result = dataSource.rows;
       // 300n, 250n, 200, 150, 100, 100, Nulls last
       expect(result.rows.map((r) => r.id)).toEqual([6, 7, 2, 3, 1, 5, 4]);
     });
@@ -240,14 +240,14 @@
     test('sort by boolean ascending', () => {
       const columns: Column[] = [{field: 'active', sort: 'ASC'}]; // 0 then 1
       dataSource.notify({columns, filters: []});
-      const result = dataSource.result;
+      const result = dataSource.rows;
       expect(result.rows.map((r) => r.active)).toEqual([0, 0, 0, 1, 1, 1, 1]);
     });
 
     test('sort by Uint8Array ascending (by length)', () => {
       const columns: Column[] = [{field: 'blob', sort: 'ASC'}];
       dataSource.notify({columns, filters: []});
-      const result = dataSource.result;
+      const result = dataSource.rows;
       // null (Charlie, id:3), len 1 (David id:4, Mallory id:6), len 2 (Alice id:1, Trent id:7), len 3 (Bob id:2), len 4 (Eve id:5)
       // Original order for same length: David before Mallory, Alice before Trent.
       expect(result.rows.map((r) => r.id)).toEqual([3, 4, 6, 1, 7, 2, 5]);
@@ -256,7 +256,7 @@
     test('sort by Uint8Array descending (by length)', () => {
       const columns: Column[] = [{field: 'blob', sort: 'DESC'}];
       dataSource.notify({columns, filters: []});
-      const result = dataSource.result;
+      const result = dataSource.rows;
       // len 4, len 3, len 2, len 2, len 1, len 0, null
       expect(result.rows.map((r) => r.id)).toEqual([5, 2, 1, 7, 4, 6, 3]);
     });
@@ -268,7 +268,7 @@
       });
       // Then unsort
       dataSource.notify({});
-      const result = dataSource.result;
+      const result = dataSource.rows;
       // Should revert to original order if no filters applied
       expect(result.rows.map((r) => r.id)).toEqual(sampleData.map((r) => r.id));
     });
@@ -279,7 +279,7 @@
       const filters: Filter[] = [{field: 'active', op: '=', value: 1}];
       const columns: Column[] = [{field: 'value', sort: 'DESC'}];
       dataSource.notify({columns, filters});
-      const result = dataSource.result;
+      const result = dataSource.rows;
       // Active: Alice (100), Charlie (150), Eve (100), Trent (250n)
       // Sorted by value desc: Trent, Charlie, Alice, Eve (Alice/Eve order by original due to stable sort on value)
       expect(result.rows.map((r) => r.id)).toEqual([7, 3, 1, 5]);
@@ -293,12 +293,12 @@
       const columns: Column[] = [{field: 'name', sort: 'ASC'}];
 
       dataSource.notify({columns, filters});
-      const result1 = dataSource.result.rows; // Access internal array
+      const result1 = dataSource.rows.rows; // Access internal array
 
       // Spy on internal methods if possible, or check object identity
       // For this test, we'll check if the returned array reference is the same
       dataSource.notify({columns, filters}); // Identical call
-      const result2 = dataSource.result.rows;
+      const result2 = dataSource.rows.rows;
 
       expect(result1).toBe(result2); // Should be the same array instance due to caching
     });
@@ -309,10 +309,10 @@
       const columns2: Column[] = [{field: 'name', sort: 'DESC'}];
 
       dataSource.notify({columns: columns1, filters});
-      const result1 = dataSource.result.rows;
+      const result1 = dataSource.rows.rows;
 
       dataSource.notify({columns: columns2, filters}); // Different sort
-      const result2 = dataSource.result.rows;
+      const result2 = dataSource.rows.rows;
 
       expect(result1).not.toBe(result2);
       expect(result1.map((r) => r.id)).not.toEqual(result2.map((r) => r.id));
@@ -324,10 +324,10 @@
       const columns: Column[] = [{field: 'name', sort: 'ASC'}];
 
       dataSource.notify({columns, filters: filters1});
-      const result1 = dataSource.result.rows;
+      const result1 = dataSource.rows.rows;
 
       dataSource.notify({columns, filters: filters2}); // Different filters
-      const result2 = dataSource.result.rows;
+      const result2 = dataSource.rows.rows;
 
       expect(result1).not.toBe(result2);
       expect(result1.map((r) => r.id)).not.toEqual(result2.map((r) => r.id));
@@ -342,12 +342,12 @@
       ];
 
       dataSource.notify({filters: filters1});
-      const result1 = dataSource.result.rows;
+      const result1 = dataSource.rows.rows;
       expect(result1.length).toBe(1);
       expect(result1[0].id).toBe(1);
 
       dataSource.notify({filters: filters2});
-      const result2 = dataSource.result.rows;
+      const result2 = dataSource.rows.rows;
       expect(result2.length).toBe(1);
       expect(result2[0].id).toBe(2);
 
@@ -357,7 +357,7 @@
 
   test('empty data source', () => {
     const emptyDataSource = new InMemoryDataSource([]);
-    const result = emptyDataSource.result;
+    const result = emptyDataSource.rows;
     expect(result.rowOffset).toBe(0);
     expect(result.totalRows).toBe(0);
     expect(result.rows).toEqual([]);
@@ -366,7 +366,7 @@
       columns: [{field: 'id', sort: 'DESC'}],
       filters: [{field: 'name', op: '=', value: 'test'}],
     });
-    const resultAfterUpdate = emptyDataSource.result;
+    const resultAfterUpdate = emptyDataSource.rows;
     expect(resultAfterUpdate.totalRows).toBe(0);
     expect(resultAfterUpdate.rows).toEqual([]);
   });
diff --git a/ui/src/components/widgets/datagrid/sql_data_source.ts b/ui/src/components/widgets/datagrid/sql_data_source.ts
index a88fc89..975167f 100644
--- a/ui/src/components/widgets/datagrid/sql_data_source.ts
+++ b/ui/src/components/widgets/datagrid/sql_data_source.ts
@@ -21,7 +21,7 @@
 import {
   DataSource,
   DataSourceModel,
-  DataSourceResult,
+  DataSourceRows,
   Pagination,
 } from './data_source';
 import {Column, Filter, Pivot} from './model';
@@ -33,52 +33,79 @@
 
 /**
  * Configuration for SQLDataSource.
- * Either provide a base query string, or a schema with root schema name.
  */
 export interface SQLDataSourceConfig {
   /**
    * The trace processor engine to run queries against.
    */
-  engine: Engine;
-
-  /**
-   * Base SQL query. Required when not using a schema.
-   * When using a schema, this is ignored (the base table comes from the schema).
-   */
-  baseQuery?: string;
+  readonly engine: Engine;
 
   /**
    * SQL schema registry defining tables and their relationships.
-   * When provided, enables automatic JOIN generation for nested column paths.
+   * Enables automatic JOIN generation for nested column paths.
+   *
+   * For simple queries without explicit column definitions, use
+   * createSimpleSchema() to generate a passthrough schema.
    */
-  sqlSchema?: SQLSchemaRegistry;
+  readonly sqlSchema: SQLSchemaRegistry;
 
   /**
-   * The root schema name to query from (e.g., 'slice').
-   * Required when sqlSchema is provided.
+   * The root schema name to query from (e.g., 'slice', 'query').
    */
-  rootSchemaName?: string;
+  readonly rootSchemaName: string;
+
+  /**
+   * Optional SQL prelude to execute before each query.
+   * Useful for imports like "INCLUDE PERFETTO MODULE xyz;"
+   */
+  readonly preamble?: string;
+}
+
+// Cache entry for row count resolution
+interface RowCountCache {
+  query: string;
+  count: number;
+}
+
+// Cache entry for rows resolution
+interface RowsCache {
+  query: string;
+  pagination: Pagination | undefined;
+  offset: number;
+  rows: Row[];
+}
+
+// Cache entry for aggregate resolution
+interface AggregatesCache {
+  query: string;
+  totals: Map<string, SqlValue>;
+}
+
+// Cache entry for distinct values resolution
+interface DistinctValuesCache {
+  query: string;
+  values: ReadonlyArray<SqlValue>;
 }
 
 /**
- * SQL data source for DataGrid that can operate in two modes:
+ * SQL data source for DataGrid.
  *
- * 1. **Simple mode** (baseQuery only): Columns are queried directly from the
- *    base query. Column names must match exactly what's in the query.
+ * Generates optimized SQL queries with JOINs based on column paths like
+ * 'parent.name' or 'thread.process.pid'. Supports parameterized columns
+ * like 'args.foo'.
  *
- * 2. **Schema mode** (sqlSchema + rootSchemaName): Generates optimized SQL
- *    queries with JOINs based on column paths like 'parent.name' or
- *    'thread.process.pid'. Supports parameterized columns like 'args.foo'.
- *
- * Example usage (simple mode):
+ * For arbitrary queries without explicit schema, use createSimpleSchema():
  * ```typescript
+ * import {createSimpleSchema} from './sql_schema';
+ *
  * const dataSource = new SQLDataSource({
  *   engine,
- *   baseQuery: 'SELECT * FROM slice WHERE dur > 0',
+ *   sqlSchema: createSimpleSchema('SELECT * FROM slice WHERE dur > 0'),
+ *   rootSchemaName: 'query',
  * });
  * ```
  *
- * Example usage (schema mode):
+ * For tables with relationships:
  * ```typescript
  * const schema: SQLSchemaRegistry = {
  *   slice: {
@@ -87,10 +114,6 @@
  *       id: {},
  *       name: {},
  *       parent: { ref: 'slice', foreignKey: 'parent_id' },
- *       args: {
- *         expression: (alias, key) => `extract_arg(${alias}.arg_set_id, '${key}')`,
- *         parameterized: true,
- *       },
  *     },
  *   },
  * };
@@ -105,45 +128,34 @@
 export class SQLDataSource implements DataSource {
   private readonly engine: Engine;
   private readonly limiter = new AsyncLimiter();
-  private readonly baseQuery?: string;
-  private readonly sqlSchema?: SQLSchemaRegistry;
-  private readonly rootSchemaName?: string;
+  private readonly sqlSchema: SQLSchemaRegistry;
+  private readonly rootSchemaName: string;
+  private readonly prelude?: string;
 
-  private workingQuery = '';
-  private pagination?: Pagination;
-  private pivot?: Pivot;
-  private cachedResult?: DataSourceResult;
-  private isLoadingFlag = false;
+  // Cache for each resolution type
+  private rowCountCache?: RowCountCache;
+  private rowsCache?: RowsCache;
+  private aggregatesCache?: AggregatesCache;
+  private distinctValuesCache = new Map<string, DistinctValuesCache>();
   private parameterKeysCache = new Map<string, ReadonlyArray<string>>();
 
+  // Current results
+  private cachedResult?: DataSourceRows;
+  private cachedDistinctValues?: ReadonlyMap<string, ReadonlyArray<SqlValue>>;
+  private cachedAggregateTotals?: ReadonlyMap<string, SqlValue>;
+  private isLoadingFlag = false;
+
   constructor(config: SQLDataSourceConfig) {
     this.engine = config.engine;
-    this.baseQuery = config.baseQuery;
     this.sqlSchema = config.sqlSchema;
     this.rootSchemaName = config.rootSchemaName;
-
-    // Validate configuration
-    if (!this.baseQuery && !this.sqlSchema) {
-      throw new Error('SQLDataSource requires either baseQuery or sqlSchema');
-    }
-    if (this.sqlSchema && !this.rootSchemaName) {
-      throw new Error(
-        'SQLDataSource requires rootSchemaName when sqlSchema is provided',
-      );
-    }
-  }
-
-  /**
-   * Returns true if this data source is using schema mode.
-   */
-  private get useSchema(): boolean {
-    return this.sqlSchema !== undefined && this.rootSchemaName !== undefined;
+    this.prelude = config.preamble;
   }
 
   /**
    * Getter for the current rows result
    */
-  get result(): DataSourceResult | undefined {
+  get rows(): DataSourceRows | undefined {
     return this.cachedResult;
   }
 
@@ -151,146 +163,63 @@
     return this.isLoadingFlag;
   }
 
+  get distinctValues(): ReadonlyMap<string, readonly SqlValue[]> | undefined {
+    return this.cachedDistinctValues;
+  }
+
+  get parameterKeys(): ReadonlyMap<string, readonly string[]> | undefined {
+    return this.parameterKeysCache.size > 0
+      ? this.parameterKeysCache
+      : undefined;
+  }
+
+  get aggregateTotals(): ReadonlyMap<string, SqlValue> | undefined {
+    return this.cachedAggregateTotals;
+  }
+
+  /**
+   * Get the current working query for the datasource.
+   * Useful for debugging or creating debug tracks.
+   */
+  getCurrentQuery(): string {
+    return this.rowsCache?.query ?? '';
+  }
+
   /**
    * Notify of parameter changes and trigger data update
    */
-  notify({
-    columns,
-    filters = [],
-    pagination,
-    pivot,
-    distinctValuesColumns,
-    parameterKeyColumns,
-  }: DataSourceModel): void {
+  notify(model: DataSourceModel): void {
     this.limiter.schedule(async () => {
+      // Defer setting loading flag to avoid setting it synchronously during the
+      // view() call that triggered notify(). This avoids the bug that the
+      // current frame always has isLoading = true.
+      await Promise.resolve();
       this.isLoadingFlag = true;
 
       try {
-        const workingQuery = this.buildWorkingQuery(columns, filters, pivot);
+        // Resolve row count
+        const rowCount = await this.resolveRowCount(model);
 
-        if (
-          workingQuery !== this.workingQuery ||
-          !arePivotsEqual(this.pivot, pivot)
-        ) {
-          this.workingQuery = workingQuery;
-          this.pivot = pivot;
+        // Resolve aggregates
+        const aggregateTotals = await this.resolveAggregates(model);
 
-          // Clear the cache
-          this.cachedResult = undefined;
-          this.pagination = undefined;
+        // Resolve rows
+        const {offset, rows} = await this.resolveRows(model);
 
-          // Update the cache with the total row count
-          const rowCount = await this.getRowCount(workingQuery);
+        // Resolve distinct values
+        const distinctValues = await this.resolveDistinctValues(model);
 
-          // Compute aggregate totals for pivot mode (but not drill-down mode)
-          let aggregateTotals: Map<string, SqlValue> | undefined;
-          const pivotAggregates = pivot?.aggregates ?? [];
-          if (pivot && !pivot.drillDown && pivotAggregates.length > 0) {
-            const aggregates = await this.getPivotAggregates(
-              columns,
-              filters,
-              pivot,
-            );
-            aggregateTotals = new Map<string, SqlValue>();
-            for (const [key, value] of Object.entries(aggregates)) {
-              aggregateTotals.set(key, value);
-            }
-          }
+        // Resolve parameter keys
+        await this.resolveParameterKeys(model);
 
-          // Compute column-level aggregations (non-pivot mode)
-          const columnsWithAggregation = columns?.filter((c) => c.aggregate);
-          if (
-            columnsWithAggregation &&
-            columnsWithAggregation.length > 0 &&
-            !pivot
-          ) {
-            const columnAggregates = await this.getColumnAggregates(
-              filters,
-              columnsWithAggregation,
-            );
-            aggregateTotals = aggregateTotals ?? new Map<string, SqlValue>();
-            for (const [key, value] of Object.entries(columnAggregates)) {
-              aggregateTotals.set(key, value as SqlValue);
-            }
-          }
-
-          this.cachedResult = {
-            rowOffset: 0,
-            totalRows: rowCount,
-            rows: [],
-            distinctValues: new Map<string, ReadonlyArray<SqlValue>>(),
-            parameterKeys: this.parameterKeysCache,
-            aggregateTotals,
-          };
-        }
-
-        // Fetch data if pagination has changed
-        if (!comparePagination(this.pagination, pagination)) {
-          this.pagination = pagination;
-          const {offset, rows} = await this.getRows(workingQuery, pagination);
-          this.cachedResult = {
-            ...this.cachedResult!,
-            rowOffset: offset,
-            rows,
-          };
-        }
-
-        // Handle distinct values requests
-        if (distinctValuesColumns) {
-          for (const columnPath of distinctValuesColumns) {
-            if (!this.cachedResult?.distinctValues?.has(columnPath)) {
-              const query = this.buildDistinctValuesQuery(columnPath);
-              if (query) {
-                const result = await runQueryForQueryTable(query, this.engine);
-                const values = result.rows.map((r) => r['value']);
-                this.cachedResult = {
-                  ...this.cachedResult!,
-                  distinctValues: new Map<string, ReadonlyArray<SqlValue>>([
-                    ...this.cachedResult!.distinctValues!,
-                    [columnPath, values],
-                  ]),
-                };
-              }
-            }
-          }
-        }
-
-        // Handle parameter keys requests (schema mode only)
-        if (parameterKeyColumns && this.useSchema) {
-          for (const prefix of parameterKeyColumns) {
-            if (!this.parameterKeysCache.has(prefix)) {
-              const schema = this.sqlSchema![this.rootSchemaName!];
-              const colDef = maybeUndefined(schema?.columns[prefix]);
-
-              if (
-                colDef &&
-                isSQLExpressionDef(colDef) &&
-                colDef.parameterized
-              ) {
-                if (colDef.parameterKeysQuery) {
-                  const baseTable = schema.table;
-                  const baseAlias = `${baseTable}_0`;
-                  const query = colDef.parameterKeysQuery(baseTable, baseAlias);
-
-                  try {
-                    const result = await runQueryForQueryTable(
-                      query,
-                      this.engine,
-                    );
-                    const keys = result.rows.map((r) => String(r['key']));
-                    this.parameterKeysCache.set(prefix, keys);
-                    this.cachedResult = {
-                      ...this.cachedResult!,
-                      parameterKeys: this.parameterKeysCache,
-                    };
-                  } catch {
-                    this.parameterKeysCache.set(prefix, []);
-                  }
-                }
-              }
-            }
-          }
-        }
+        // Build final result
+        this.cachedResult = {
+          rowOffset: offset,
+          totalRows: rowCount,
+          rows,
+        };
+        this.cachedDistinctValues = distinctValues;
+        this.cachedAggregateTotals = aggregateTotals;
       } finally {
         this.isLoadingFlag = false;
       }
@@ -298,195 +227,286 @@
   }
 
   /**
-   * Export all data with current filters/sorting applied.
+   * Resolves the row count. Compares query against cache and reuses if unchanged.
    */
-  async exportData(): Promise<Row[]> {
-    if (!this.workingQuery) {
-      return [];
+  private async resolveRowCount(model: DataSourceModel): Promise<number> {
+    // Build query without ORDER BY - ordering is irrelevant for counting
+    const countQuery = this.buildQuery(model, {includeOrderBy: false});
+
+    // Check cache
+    if (this.rowCountCache?.query === countQuery) {
+      return this.rowCountCache.count;
     }
 
-    const query = `SELECT * FROM (${this.workingQuery})`;
-    const result = await runQueryForQueryTable(query, this.engine);
-    return result.rows;
+    // Fetch new count
+    const result = await this.engine.query(
+      this.wrapQueryWithPrelude(`
+      WITH data AS (${countQuery})
+      SELECT COUNT(*) AS total_count
+      FROM data
+    `),
+    );
+    const count = result.firstRow({total_count: NUM}).total_count;
+
+    // Update cache
+    this.rowCountCache = {query: countQuery, count};
+
+    return count;
   }
 
   /**
-   * Builds a complete SQL query based on the current mode.
+   * Resolves the rows for the current page. Compares query and pagination against cache.
    */
-  private buildWorkingQuery(
-    columns: ReadonlyArray<Column> | undefined,
-    filters: ReadonlyArray<Filter>,
-    pivot?: Pivot,
-  ): string {
-    if (this.useSchema) {
-      return this.buildSchemaWorkingQuery(columns, filters, pivot);
-    } else {
-      return this.buildSimpleWorkingQuery(columns, filters, pivot);
+  private async resolveRows(
+    model: DataSourceModel,
+  ): Promise<{offset: number; rows: Row[]}> {
+    const {pagination} = model;
+
+    // Build query with ORDER BY for proper pagination ordering
+    const rowsQuery = this.buildQuery(model, {includeOrderBy: true});
+
+    // Check cache - both query and pagination must match
+    if (
+      this.rowsCache?.query === rowsQuery &&
+      comparePagination(this.rowsCache.pagination, pagination)
+    ) {
+      return {offset: this.rowsCache.offset, rows: this.rowsCache.rows};
     }
+
+    // Fetch new rows
+    let query = `
+      WITH data AS (${rowsQuery})
+      SELECT *
+      FROM data
+    `;
+
+    if (pagination) {
+      query += `LIMIT ${pagination.limit} OFFSET ${pagination.offset}`;
+    }
+
+    const result = await runQueryForQueryTable(
+      this.wrapQueryWithPrelude(query),
+      this.engine,
+    );
+
+    const offset = pagination?.offset ?? 0;
+    const rows = result.rows;
+
+    // Update cache
+    this.rowsCache = {query: rowsQuery, pagination, offset, rows};
+
+    return {offset, rows};
   }
 
   /**
-   * Builds a query for simple mode (no schema, direct column access).
+   * Resolves aggregate totals. Handles both pivot aggregates and column aggregates.
    */
-  private buildSimpleWorkingQuery(
-    columns: ReadonlyArray<Column> | undefined,
-    filters: ReadonlyArray<Filter>,
-    pivot?: Pivot,
-  ): string {
-    const colNames = columns?.map((c) => c.field) ?? ['*'];
+  private async resolveAggregates(
+    model: DataSourceModel,
+  ): Promise<Map<string, SqlValue> | undefined> {
+    const {columns, filters = [], pivot} = model;
 
-    // Include column aggregates in the query string so changes trigger a reload
-    const aggregateSuffix = columns
-      ?.filter((c) => c.aggregate)
-      .map((c) => `${c.field}:${c.aggregate}`)
-      .join(',');
+    // Build a unique query string for the aggregates
+    const aggregateQuery = this.buildAggregateQuery(model);
 
-    let query: string;
+    // If no aggregates needed, return undefined
+    if (!aggregateQuery) {
+      this.aggregatesCache = undefined;
+      return undefined;
+    }
 
-    if (pivot && !pivot.drillDown) {
-      // Pivot mode: Build aggregate columns from pivot.aggregates
-      const aggregates = pivot.aggregates ?? [];
-      const valCols = aggregates
-        .map((agg) => {
-          if (agg.function === 'COUNT') {
-            return `COUNT(*) AS __count__`;
+    // Check cache
+    if (this.aggregatesCache?.query === aggregateQuery) {
+      return this.aggregatesCache.totals;
+    }
+
+    // Compute aggregates
+    const totals = new Map<string, SqlValue>();
+
+    // Pivot aggregates (but not drill-down mode)
+    const pivotAggregates = pivot?.aggregates ?? [];
+    if (pivot && !pivot.drillDown && pivotAggregates.length > 0) {
+      const aggregates = await this.fetchPivotAggregates(filters, pivot);
+      for (const [key, value] of Object.entries(aggregates)) {
+        totals.set(key, value);
+      }
+    }
+
+    // Column-level aggregations (non-pivot mode)
+    const columnsWithAggregation = columns?.filter((c) => c.aggregate);
+    if (columnsWithAggregation && columnsWithAggregation.length > 0 && !pivot) {
+      const columnAggregates = await this.fetchColumnAggregates(
+        filters,
+        columnsWithAggregation,
+      );
+      for (const [key, value] of Object.entries(columnAggregates)) {
+        totals.set(key, value as SqlValue);
+      }
+    }
+
+    // Update cache
+    this.aggregatesCache = {query: aggregateQuery, totals};
+
+    return totals;
+  }
+
+  /**
+   * Builds a unique string representing the aggregate query for cache comparison.
+   */
+  private buildAggregateQuery(model: DataSourceModel): string | undefined {
+    const {columns, filters = [], pivot} = model;
+
+    const parts: string[] = [];
+
+    // Include pivot aggregates
+    if (pivot && !pivot.drillDown && Boolean(pivot.aggregates?.length)) {
+      parts.push(`pivot:${JSON.stringify(pivot.aggregates)}`);
+    }
+
+    // Include column aggregates
+    const columnsWithAggregation = columns?.filter((c) => c.aggregate);
+    if (columnsWithAggregation && columnsWithAggregation.length > 0 && !pivot) {
+      const colAggs = columnsWithAggregation.map(
+        (c) => `${c.field}:${c.aggregate}`,
+      );
+      parts.push(`columns:${colAggs.join(',')}`);
+    }
+
+    if (parts.length === 0) {
+      return undefined;
+    }
+
+    // Include filters in the cache key
+    const filterKey = filters.map((f) => {
+      const value = 'value' in f ? f.value : '';
+      return `${f.field}:${f.op}:${value}`;
+    });
+    parts.push(`filters:${filterKey.join(',')}`);
+
+    return parts.join('|');
+  }
+
+  /**
+   * Resolves distinct values for requested columns.
+   */
+  private async resolveDistinctValues(
+    model: DataSourceModel,
+  ): Promise<Map<string, ReadonlyArray<SqlValue>>> {
+    const {distinctValuesColumns} = model;
+
+    const result = new Map<string, ReadonlyArray<SqlValue>>();
+
+    if (!distinctValuesColumns) {
+      return result;
+    }
+
+    for (const columnPath of distinctValuesColumns) {
+      const query = this.buildDistinctValuesQuery(columnPath);
+      if (!query) continue;
+
+      // Check cache
+      const cached = this.distinctValuesCache.get(columnPath);
+      if (cached?.query === query) {
+        result.set(columnPath, cached.values);
+        continue;
+      }
+
+      // Fetch new values
+      const queryResult = await runQueryForQueryTable(
+        this.wrapQueryWithPrelude(query),
+        this.engine,
+      );
+      const values = queryResult.rows.map((r) => r['value']);
+
+      // Update cache
+      this.distinctValuesCache.set(columnPath, {query, values});
+      result.set(columnPath, values);
+    }
+
+    return result;
+  }
+
+  /**
+   * Resolves parameter keys for parameterized columns.
+   */
+  private async resolveParameterKeys(model: DataSourceModel): Promise<void> {
+    const {parameterKeyColumns} = model;
+
+    if (!parameterKeyColumns) {
+      return;
+    }
+
+    for (const prefix of parameterKeyColumns) {
+      // Already cached
+      if (this.parameterKeysCache.has(prefix)) {
+        continue;
+      }
+
+      const schema = this.sqlSchema[this.rootSchemaName];
+      const colDef = maybeUndefined(schema?.columns[prefix]);
+
+      if (colDef && isSQLExpressionDef(colDef) && colDef.parameterized) {
+        if (colDef.parameterKeysQuery) {
+          const baseTable = schema.table;
+          const baseAlias = `${baseTable}_0`;
+          const query = colDef.parameterKeysQuery(baseTable, baseAlias);
+
+          try {
+            const result = await runQueryForQueryTable(
+              this.wrapQueryWithPrelude(query),
+              this.engine,
+            );
+            const keys = result.rows.map((r) => String(r['key']));
+            this.parameterKeysCache.set(prefix, keys);
+          } catch {
+            this.parameterKeysCache.set(prefix, []);
           }
-          const field = 'field' in agg ? agg.field : null;
-          if (!field) return null;
-          const alias = this.pathToAlias(field);
-          if (agg.function === 'ANY') {
-            return `MIN(${field}) AS ${alias}`;
-          }
-          return `${agg.function}(${field}) AS ${alias}`;
-        })
-        .filter(Boolean)
-        .join(', ');
-
-      const groupByFields = pivot.groupBy.map(({field}) => field);
-      if (groupByFields.length > 0) {
-        const groupCols = groupByFields.join(', ');
-        const selectCols = valCols ? `${groupCols}, ${valCols}` : groupCols;
-        query = `
-          SELECT ${selectCols}
-          FROM (${this.baseQuery})
-          GROUP BY ${groupCols}
-        `;
-      } else {
-        query = `
-          SELECT ${valCols}
-          FROM (${this.baseQuery})
-        `;
-      }
-    } else if (pivot?.drillDown) {
-      // Drill-down mode
-      query = `\nSELECT ${colNames.join(', ')} FROM (${this.baseQuery})`;
-
-      const drillDownConditions = pivot.groupBy
-        .map((col) => {
-          const field = col.field;
-          const value = pivot.drillDown![field];
-          if (value === null) {
-            return `${field} IS NULL`;
-          }
-          return `${field} = ${sqlValue(value)}`;
-        })
-        .join(' AND ');
-
-      if (drillDownConditions) {
-        query = `SELECT * FROM (${query}) WHERE ${drillDownConditions}`;
-      }
-    } else {
-      query = `\nSELECT ${colNames.join(', ')} FROM (${this.baseQuery})`;
-    }
-
-    // Add WHERE clause for filters
-    if (filters.length > 0) {
-      const whereConditions = filters.map(simpleFilter2Sql).join(' AND ');
-      query = `SELECT * FROM (${query}) WHERE ${whereConditions}`;
-    }
-
-    // Add ORDER BY clause - find sorted column from columns or pivot
-    const sortedColumn = this.findSortedColumn(columns, pivot);
-    if (sortedColumn) {
-      const {field, direction} = sortedColumn;
-      let columnExists: boolean;
-      if (pivot && !pivot.drillDown) {
-        const groupByFields = pivot.groupBy.map(({field}) => field);
-        const aggregateFields = (pivot.aggregates ?? []).map((a) =>
-          'field' in a ? a.field : '__count__',
-        );
-        const pivotColumns = [...groupByFields, ...aggregateFields];
-        columnExists = pivotColumns.includes(field);
-      } else {
-        columnExists = columns === undefined || colNames.includes(field);
-      }
-      if (columnExists) {
-        query += `\nORDER BY ${field} ${direction.toUpperCase()}`;
+        }
       }
     }
+  }
 
-    // Append aggregate suffix as a comment so changes trigger reload
-    if (aggregateSuffix) {
-      query += ` /* aggregates: ${aggregateSuffix} */`;
+  private wrapQueryWithPrelude(query: string): string {
+    if (this.prelude) {
+      return `${this.prelude};\n${query}`;
     }
-
     return query;
   }
 
   /**
-   * Find the column that has sorting applied.
+   * Export all data with current filters/sorting applied.
    */
-  private findSortedColumn(
-    columns: ReadonlyArray<Column> | undefined,
-    pivot?: Pivot,
-  ): {field: string; direction: 'ASC' | 'DESC'} | undefined {
-    // Check pivot groupBy columns for sort
-    if (pivot) {
-      for (const col of pivot.groupBy) {
-        if (typeof col !== 'string' && col.sort) {
-          return {field: col.field, direction: col.sort};
-        }
-      }
-      // Check pivot aggregates for sort
-      for (const agg of pivot.aggregates ?? []) {
-        if (agg.sort) {
-          const field = 'field' in agg ? agg.field : '__count__';
-          return {field, direction: agg.sort};
-        }
-      }
+  async exportData(): Promise<Row[]> {
+    const workingQuery = this.rowsCache?.query;
+    if (!workingQuery) {
+      return [];
     }
 
-    // Check regular columns for sort
-    if (columns) {
-      for (const col of columns) {
-        if (col.sort) {
-          return {field: col.field, direction: col.sort};
-        }
-      }
-    }
-
-    return undefined;
+    const query = `SELECT * FROM (${workingQuery})`;
+    const result = await runQueryForQueryTable(
+      this.wrapQueryWithPrelude(query),
+      this.engine,
+    );
+    return result.rows;
   }
 
   /**
-   * Builds a query for schema mode (with JOINs based on column paths).
+   * Builds a complete SQL query from the model.
    */
-  private buildSchemaWorkingQuery(
-    columns: ReadonlyArray<Column> | undefined,
-    filters: ReadonlyArray<Filter>,
-    pivot?: Pivot,
+  private buildQuery(
+    model: DataSourceModel,
+    options: {includeOrderBy: boolean},
   ): string {
-    const resolver = new SQLSchemaResolver(
-      this.sqlSchema!,
-      this.rootSchemaName!,
-    );
+    const {columns, filters = [], pivot} = model;
+
+    const resolver = new SQLSchemaResolver(this.sqlSchema, this.rootSchemaName);
 
     const baseTable = resolver.getBaseTable();
     const baseAlias = resolver.getBaseAlias();
 
     // For pivot mode without drill-down, we build aggregates differently
     if (pivot && !pivot.drillDown) {
-      return this.buildSchemaPivotQuery(resolver, filters, pivot);
+      return this.buildPivotQuery(resolver, filters, pivot, options);
     }
 
     // Normal mode or drill-down: select individual columns
@@ -506,6 +526,13 @@
       resolver.resolveColumnPath(filter.field);
     }
 
+    // Resolve drill-down groupBy fields to ensure their JOINs are added
+    if (pivot?.drillDown) {
+      for (const col of pivot.groupBy) {
+        resolver.resolveColumnPath(col.field);
+      }
+    }
+
     // If no columns specified, select all from base table
     if (selectExprs.length === 0) {
       selectExprs.push(`${baseAlias}.*`);
@@ -553,26 +580,38 @@
       }
     }
 
-    // Add ORDER BY clause - find sorted column
-    const sortedColumn = this.findSortedColumn(columns, pivot);
-    if (sortedColumn) {
-      const {field, direction} = sortedColumn;
-      const sqlExpr = resolver.resolveColumnPath(field);
-      if (sqlExpr) {
-        query += `\nORDER BY ${sqlExpr} ${direction.toUpperCase()}`;
+    // Add ORDER BY clause if requested
+    if (options.includeOrderBy) {
+      const sortedColumn = this.findSortedColumn(columns, pivot);
+      if (sortedColumn) {
+        const {field, direction} = sortedColumn;
+        const sqlExpr = resolver.resolveColumnPath(field);
+        if (sqlExpr) {
+          query += `\nORDER BY ${sqlExpr} ${direction.toUpperCase()}`;
+        }
       }
     }
 
+    // Include column aggregates in the query string so changes trigger a reload
+    const aggregateSuffix = columns
+      ?.filter((c) => c.aggregate)
+      .map((c) => `${c.field}:${c.aggregate}`)
+      .join(',');
+    if (aggregateSuffix) {
+      query += ` /* aggregates: ${aggregateSuffix} */`;
+    }
+
     return query;
   }
 
   /**
-   * Builds a pivot query with GROUP BY and aggregations (schema mode).
+   * Builds a pivot query with GROUP BY and aggregations.
    */
-  private buildSchemaPivotQuery(
+  private buildPivotQuery(
     resolver: SQLSchemaResolver,
     filters: ReadonlyArray<Filter>,
     pivot: Pivot,
+    options: {includeOrderBy: boolean},
   ): string {
     const baseTable = resolver.getBaseTable();
     const baseAlias = resolver.getBaseAlias();
@@ -642,19 +681,21 @@
       query += `\nGROUP BY ${groupByOrigExprs.join(', ')}`;
     }
 
-    // Add ORDER BY - find sorted column from pivot
-    const sortedColumn = this.findSortedColumn(undefined, pivot);
-    if (sortedColumn) {
-      const {field, direction} = sortedColumn;
-      const aggregateFields = aggregates.map((a) =>
-        'field' in a ? a.field : '__count__',
-      );
-      const pivotColumns = [...groupByFields, ...aggregateFields];
-      if (pivotColumns.includes(field)) {
-        const alias = groupByFields.includes(field)
-          ? this.pathToAlias(field)
-          : field;
-        query += `\nORDER BY ${alias} ${direction.toUpperCase()}`;
+    // Add ORDER BY if requested
+    if (options.includeOrderBy) {
+      const sortedColumn = this.findSortedColumn(undefined, pivot);
+      if (sortedColumn) {
+        const {field, direction} = sortedColumn;
+        const aggregateFields = aggregates.map((a) =>
+          'field' in a ? a.field : '__count__',
+        );
+        const pivotColumns = [...groupByFields, ...aggregateFields];
+        if (pivotColumns.includes(field)) {
+          const alias = groupByFields.includes(field)
+            ? this.pathToAlias(field)
+            : field;
+          query += `\nORDER BY ${alias} ${direction.toUpperCase()}`;
+        }
       }
     }
 
@@ -662,34 +703,70 @@
   }
 
   /**
-   * Builds a distinct values query based on the current mode.
+   * Find the column that has sorting applied.
+   */
+  private findSortedColumn(
+    columns: ReadonlyArray<Column> | undefined,
+    pivot?: Pivot,
+  ): {field: string; direction: 'ASC' | 'DESC'} | undefined {
+    // In drill-down mode, we display flat columns, so only check those for sort
+    if (pivot?.drillDown) {
+      if (columns) {
+        for (const col of columns) {
+          if (col.sort) {
+            return {field: col.field, direction: col.sort};
+          }
+        }
+      }
+      return undefined;
+    }
+
+    // Check pivot groupBy columns for sort
+    if (pivot) {
+      for (const col of pivot.groupBy) {
+        if (typeof col !== 'string' && col.sort) {
+          return {field: col.field, direction: col.sort};
+        }
+      }
+      // Check pivot aggregates for sort
+      for (const agg of pivot.aggregates ?? []) {
+        if (agg.sort) {
+          const field = 'field' in agg ? agg.field : '__count__';
+          return {field, direction: agg.sort};
+        }
+      }
+    }
+
+    // Check regular columns for sort
+    if (columns) {
+      for (const col of columns) {
+        if (col.sort) {
+          return {field: col.field, direction: col.sort};
+        }
+      }
+    }
+
+    return undefined;
+  }
+
+  /**
+   * Builds a distinct values query.
    */
   private buildDistinctValuesQuery(columnPath: string): string | undefined {
-    if (this.useSchema) {
-      const resolver = new SQLSchemaResolver(
-        this.sqlSchema!,
-        this.rootSchemaName!,
-      );
-      const sqlExpr = resolver.resolveColumnPath(columnPath);
-      if (!sqlExpr) return undefined;
+    const resolver = new SQLSchemaResolver(this.sqlSchema, this.rootSchemaName);
+    const sqlExpr = resolver.resolveColumnPath(columnPath);
+    if (!sqlExpr) return undefined;
 
-      const baseTable = resolver.getBaseTable();
-      const baseAlias = resolver.getBaseAlias();
-      const joinClauses = resolver.buildJoinClauses();
+    const baseTable = resolver.getBaseTable();
+    const baseAlias = resolver.getBaseAlias();
+    const joinClauses = resolver.buildJoinClauses();
 
-      return `
-        SELECT DISTINCT ${sqlExpr} AS value
-        FROM ${baseTable} AS ${baseAlias}
-        ${joinClauses}
-        ORDER BY ${sqlExpr} IS NULL, ${sqlExpr}
-      `;
-    } else {
-      return `
-        SELECT DISTINCT ${columnPath} AS value
-        FROM (${this.baseQuery})
-        ORDER BY ${columnPath} IS NULL, ${columnPath}
-      `;
-    }
+    return `
+      SELECT DISTINCT ${sqlExpr} AS value
+      FROM ${baseTable} AS ${baseAlias}
+      ${joinClauses}
+      ORDER BY ${sqlExpr} IS NULL, ${sqlExpr}
+    `;
   }
 
   /**
@@ -731,75 +808,11 @@
     }
   }
 
-  private async getRowCount(workingQuery: string): Promise<number> {
-    const result = await this.engine.query(`
-      WITH data AS (${workingQuery})
-      SELECT COUNT(*) AS total_count
-      FROM data
-    `);
-    return result.firstRow({total_count: NUM}).total_count;
-  }
-
-  private async getPivotAggregates(
-    _columns: ReadonlyArray<Column> | undefined,
+  private async fetchPivotAggregates(
     filters: ReadonlyArray<Filter>,
     pivot: Pivot,
   ): Promise<Row> {
-    if (this.useSchema) {
-      return this.getSchemaPivotAggregates(filters, pivot);
-    } else {
-      return this.getSimplePivotAggregates(filters, pivot);
-    }
-  }
-
-  private async getSimplePivotAggregates(
-    filters: ReadonlyArray<Filter>,
-    pivot: Pivot,
-  ): Promise<Row> {
-    let filteredBaseQuery = `SELECT * FROM (${this.baseQuery})`;
-    if (filters.length > 0) {
-      const whereConditions = filters.map(simpleFilter2Sql).join(' AND ');
-      filteredBaseQuery = `SELECT * FROM (${filteredBaseQuery}) WHERE ${whereConditions}`;
-    }
-
-    const aggregates = pivot.aggregates ?? [];
-    const selectClauses = aggregates
-      .map((agg) => {
-        if (agg.function === 'COUNT') {
-          return `COUNT(*) AS __count__`;
-        }
-        const field = 'field' in agg ? agg.field : null;
-        if (!field) return null;
-        const alias = this.pathToAlias(field);
-        if (agg.function === 'ANY') {
-          return `NULL AS ${alias}`;
-        }
-        return `${agg.function}(${field}) AS ${alias}`;
-      })
-      .filter(Boolean)
-      .join(', ');
-
-    if (!selectClauses) {
-      return {};
-    }
-
-    const query = `
-      SELECT ${selectClauses}
-      FROM (${filteredBaseQuery})
-    `;
-
-    const result = await runQueryForQueryTable(query, this.engine);
-    return result.rows[0] ?? {};
-  }
-
-  private async getSchemaPivotAggregates(
-    filters: ReadonlyArray<Filter>,
-    pivot: Pivot,
-  ): Promise<Row> {
-    const resolver = new SQLSchemaResolver(
-      this.sqlSchema!,
-      this.rootSchemaName!,
-    );
+    const resolver = new SQLSchemaResolver(this.sqlSchema, this.rootSchemaName);
 
     const baseTable = resolver.getBaseTable();
     const baseAlias = resolver.getBaseAlias();
@@ -843,8 +856,8 @@
 
     if (filters.length > 0) {
       const filterResolver = new SQLSchemaResolver(
-        this.sqlSchema!,
-        this.rootSchemaName!,
+        this.sqlSchema,
+        this.rootSchemaName,
       );
       const whereConditions = filters.map((filter) => {
         const sqlExpr = filterResolver.resolveColumnPath(filter.field);
@@ -853,63 +866,18 @@
       query += `\nWHERE ${whereConditions.join(' AND ')}`;
     }
 
-    const result = await runQueryForQueryTable(query, this.engine);
-    return result.rows[0] ?? {};
-  }
-
-  private async getColumnAggregates(
-    filters: ReadonlyArray<Filter>,
-    columns: ReadonlyArray<Column>,
-  ): Promise<Row> {
-    if (this.useSchema) {
-      return this.getSchemaColumnAggregates(filters, columns);
-    } else {
-      return this.getSimpleColumnAggregates(filters, columns);
-    }
-  }
-
-  private async getSimpleColumnAggregates(
-    filters: ReadonlyArray<Filter>,
-    columns: ReadonlyArray<Column>,
-  ): Promise<Row> {
-    let filteredBaseQuery = `SELECT * FROM (${this.baseQuery})`;
-    if (filters.length > 0) {
-      const whereConditions = filters.map(simpleFilter2Sql).join(' AND ');
-      filteredBaseQuery = `SELECT * FROM (${filteredBaseQuery}) WHERE ${whereConditions}`;
-    }
-
-    const selectClauses = columns
-      .filter((col) => col.aggregate)
-      .map((col) => {
-        const func = col.aggregate!;
-        if (func === 'ANY') {
-          return `MIN(${col.field}) AS ${col.field}`;
-        }
-        return `${func}(${col.field}) AS ${col.field}`;
-      })
-      .join(', ');
-
-    if (!selectClauses) {
-      return {};
-    }
-
-    const query = `
-      SELECT ${selectClauses}
-      FROM (${filteredBaseQuery})
-    `;
-
-    const result = await runQueryForQueryTable(query, this.engine);
-    return result.rows[0] ?? {};
-  }
-
-  private async getSchemaColumnAggregates(
-    filters: ReadonlyArray<Filter>,
-    columns: ReadonlyArray<Column>,
-  ): Promise<Row> {
-    const resolver = new SQLSchemaResolver(
-      this.sqlSchema!,
-      this.rootSchemaName!,
+    const result = await runQueryForQueryTable(
+      this.wrapQueryWithPrelude(query),
+      this.engine,
     );
+    return result.rows[0] ?? {};
+  }
+
+  private async fetchColumnAggregates(
+    filters: ReadonlyArray<Filter>,
+    columns: ReadonlyArray<Column>,
+  ): Promise<Row> {
+    const resolver = new SQLSchemaResolver(this.sqlSchema, this.rootSchemaName);
 
     const baseTable = resolver.getBaseTable();
     const baseAlias = resolver.getBaseAlias();
@@ -955,57 +923,12 @@
       query += `\nWHERE ${whereConditions.join(' AND ')}`;
     }
 
-    const result = await runQueryForQueryTable(query, this.engine);
+    const result = await runQueryForQueryTable(
+      this.wrapQueryWithPrelude(query),
+      this.engine,
+    );
     return result.rows[0] ?? {};
   }
-
-  private async getRows(
-    workingQuery: string,
-    pagination?: Pagination,
-  ): Promise<{offset: number; rows: Row[]}> {
-    let query = `
-      WITH data AS (${workingQuery})
-      SELECT *
-      FROM data
-    `;
-
-    if (pagination) {
-      query += `LIMIT ${pagination.limit} OFFSET ${pagination.offset}`;
-    }
-
-    const result = await runQueryForQueryTable(query, this.engine);
-
-    return {
-      offset: pagination?.offset ?? 0,
-      rows: result.rows,
-    };
-  }
-}
-
-function simpleFilter2Sql(filter: Filter): string {
-  switch (filter.op) {
-    case '=':
-    case '!=':
-    case '<':
-    case '<=':
-    case '>':
-    case '>=':
-      return `${filter.field} ${filter.op} ${sqlValue(filter.value)}`;
-    case 'glob':
-      return `${filter.field} GLOB ${sqlValue(filter.value)}`;
-    case 'not glob':
-      return `${filter.field} NOT GLOB ${sqlValue(filter.value)}`;
-    case 'is null':
-      return `${filter.field} IS NULL`;
-    case 'is not null':
-      return `${filter.field} IS NOT NULL`;
-    case 'in':
-      return `${filter.field} IN (${filter.value.map(sqlValue).join(', ')})`;
-    case 'not in':
-      return `${filter.field} NOT IN (${filter.value.map(sqlValue).join(', ')})`;
-    default:
-      assertUnreachable(filter);
-  }
 }
 
 function sqlValue(value: SqlValue): string {
@@ -1025,21 +948,3 @@
   if (!a || !b) return false;
   return a.limit === b.limit && a.offset === b.offset;
 }
-
-function arePivotsEqual(a?: Pivot, b?: Pivot): boolean {
-  if (a === b) return true;
-  if (a === undefined || b === undefined) return false;
-
-  // Compare groupBy fields
-  const aGroupBy = a.groupBy.map(({field}) => field).join(',');
-  const bGroupBy = b.groupBy.map(({field}) => field).join(',');
-  if (aGroupBy !== bGroupBy) return false;
-
-  // Compare aggregates
-  if (JSON.stringify(a.aggregates) !== JSON.stringify(b.aggregates)) {
-    return false;
-  }
-  if (JSON.stringify(a.drillDown) !== JSON.stringify(b.drillDown)) return false;
-
-  return true;
-}
diff --git a/ui/src/components/widgets/datagrid/sql_schema.ts b/ui/src/components/widgets/datagrid/sql_schema.ts
index d3427e5..10bc6ee 100644
--- a/ui/src/components/widgets/datagrid/sql_schema.ts
+++ b/ui/src/components/widgets/datagrid/sql_schema.ts
@@ -421,3 +421,41 @@
     this.aliasCounter = 0;
   }
 }
+
+/**
+ * Creates a simple schema from a table name or subquery.
+ *
+ * This enables using SQLDataSource with arbitrary queries/tables without
+ * defining explicit column schemas. Columns are accessed directly by name.
+ *
+ * @param tableOrQuery A table name (e.g., 'slice') or subquery (e.g., 'SELECT * FROM slice')
+ * @param schemaName Optional name for the schema (defaults to 'query')
+ * @returns A SQLSchemaRegistry with a single schema entry
+ *
+ * Example usage:
+ * ```typescript
+ * const schema = createSimpleSchema('SELECT * FROM slice WHERE dur > 0');
+ * const dataSource = new SQLDataSource({
+ *   engine,
+ *   sqlSchema: schema,
+ *   rootSchemaName: 'query',
+ * });
+ * ```
+ */
+export function createSimpleSchema(
+  tableOrQuery: string,
+  schemaName: string = 'query',
+): SQLSchemaRegistry {
+  // If it looks like a query (contains SELECT, spaces, etc.), wrap in parens
+  const isQuery =
+    tableOrQuery.trim().toUpperCase().startsWith('SELECT') ||
+    tableOrQuery.includes(' ');
+  const table = isQuery ? `(${tableOrQuery})` : tableOrQuery;
+
+  return {
+    [schemaName]: {
+      table,
+      columns: {}, // Empty columns - all column access falls through to direct access
+    },
+  };
+}
diff --git a/ui/src/plugins/dev.perfetto.ExplorePage/query_builder/builder.ts b/ui/src/plugins/dev.perfetto.ExplorePage/query_builder/builder.ts
index b3eed2b..1d45934 100644
--- a/ui/src/plugins/dev.perfetto.ExplorePage/query_builder/builder.ts
+++ b/ui/src/plugins/dev.perfetto.ExplorePage/query_builder/builder.ts
@@ -83,6 +83,7 @@
   SplitPanelDrawerVisibility,
 } from '../../../widgets/split_panel';
 import {SQLDataSource} from '../../../components/widgets/datagrid/sql_data_source';
+import {createSimpleSchema} from '../../../components/widgets/datagrid/sql_schema';
 import {QueryResponse} from '../../../components/query_table/queries';
 import {addQueryResultsTab} from '../../../components/query_table/query_result_tab';
 import {SqlSourceNode} from './nodes/sources/sql_source';
@@ -637,7 +638,8 @@
 
     this.dataSource = new SQLDataSource({
       engine,
-      baseQuery: `SELECT * FROM ${result.tableName}`,
+      sqlSchema: createSimpleSchema(result.tableName),
+      rootSchemaName: 'query',
     });
     this.queryExecuted = true;
     this.isQueryRunning = false;