Repository: squashql/squashql
Pull request #222 (2024-01-04 05:33:21)

Add vector aggregation capability 
# Description

In addition to vector aggregation, the work started with #193 has been push further. Compiled objects are used everywhere now (query cache, table...) and the CompiledMeasure objects do not contain their *Non* Compiled counterpart anymore.

See `core/src/main/java/io/squashql/query/VectorAggMeasure.java`. This measure is not exposed in the TS lib. It will be done in another PR. 


# Example

Using the following dataset:
```
+--------+------------+--------------+----------+
| ticker |       date |     riskType |    value |
+--------+------------+--------------+----------+
|    MMM | 2023-01-01 |  TotalReturn |      1.0 |
|    MMM | 2023-01-01 |     FXReturn |      2.0 |
|    MMM | 2023-01-01 | EquityReturn |      3.0 |
|    MMM | 2023-01-02 |  TotalReturn |     10.0 |
|    MMM | 2023-01-02 |     FXReturn |     11.0 |
|    MMM | 2023-01-02 | EquityReturn |     12.0 |
|    MMM | 2023-01-03 |  TotalReturn |    100.0 |
|    MMM | 2023-01-03 |     FXReturn |    101.0 |
|    MMM | 2023-01-03 | EquityReturn |    102.0 |
|  VBLAX | 2023-01-01 |  TotalReturn |   1000.0 |
|  VBLAX | 2023-01-01 |     FXReturn |   2000.0 |
|  VBLAX | 2023-01-01 | EquityReturn |   3000.0 |
|  VBLAX | 2023-01-02 |  TotalReturn |  10000.0 |
|  VBLAX | 2023-01-02 |     FXReturn |  11000.0 |
|  VBLAX | 2023-01-02 | EquityReturn |  12000.0 |
|  VBLAX | 2023-01-03 |  TotalReturn | 100000.0 |
|  VBLAX | 2023-01-03 |     FXReturn | 101000.0 |
|  VBLAX | 2023-01-03 | EquityReturn | 102000.0 |
+--------+------------+--------------+----------+
```

We can execute the following query:

```java
Field ticker = new TableField(this.storeName, "ticker");
Field riskType = new TableField(this.storeName, "riskType");
Field value = new TableField(this.storeName, "value");
Field date = new TableField(this.storeName, "date");
Measure vector = new VectorAggMeasure("vector", value, SUM, date);
QueryDto query = Query
        .from(this.storeName)
        .select(List.of(ticker, riskType), List.of(vector))
        .rollup(List.of(ticker, riskType))
        .build();
```

The result is:

```
+-----------------------------------------+-------------------------------------------+-----------------------------+
| storetestduckdbvectoraggregation.ticker | storetestduckdbvectoraggregation.riskType |                      vector |
+-----------------------------------------+-------------------------------------------+-----------------------------+
|                             Grand Total |                               Grand Total | [6006.0, 303303.0, 33033.0] |
|                                     MMM |                                     Total |          [303.0, 6.0, 33.0] |
|                                     MMM |                              EquityReturn |          [12.0, 102.0, 3.0] |
|                                     MMM |                                  FXReturn |          [2.0, 101.0, 11.0] |
|                                     MMM |                               TotalReturn |          [1.0, 100.0, 10.0] |
|                                   VBLAX |                                     Total | [303000.0, 6000.0, 33000.0] |
|                                   VBLAX |                              EquityReturn | [102000.0, 3000.0, 12000.0] |
|                                   VBLAX |                                  FXReturn | [2000.0, 11000.0, 101000.0] |
|                                   VBLAX |                               TotalReturn | [1000.0, 10000.0, 100000.0] |
+-----------------------------------------+-------------------------------------------+-----------------------------+
```


Patch:
diff --git a/bigquery/src/main/java/io/squashql/query/database/BigQueryEngine.java b/bigquery/src/main/java/io/squashql/query/database/BigQueryEngine.java
index 3b89427..b3a4067 100644
--- a/bigquery/src/main/java/io/squashql/query/database/BigQueryEngine.java
+++ b/bigquery/src/main/java/io/squashql/query/database/BigQueryEngine.java
@@ -4,14 +4,14 @@ import com.google.cloud.bigquery.*;
 import io.squashql.BigQueryDatastore;
 import io.squashql.BigQueryUtil;
 import io.squashql.query.Header;
-import io.squashql.query.compiled.CompiledMeasure;
 import io.squashql.table.ColumnarTable;
 import io.squashql.table.RowTable;
 import io.squashql.table.Table;
 import org.eclipse.collections.api.tuple.Pair;
 
+import java.time.LocalDate;
+import java.util.HashSet;
 import java.util.List;
-import java.util.stream.Collectors;
 
 public class BigQueryEngine extends AQueryEngine<BigQueryDatastore> {
 
@@ -54,7 +54,7 @@ public class BigQueryEngine extends AQueryEngine<BigQueryDatastore> {
               (i, fieldValueList) -> getTypeValue(fieldValueList, schema, i));
       return new ColumnarTable(
               result.getOne(),
-              query.measures.stream().map(CompiledMeasure::measure).collect(Collectors.toSet()),
+              new HashSet<>(query.measures),
               result.getTwo());
     } catch (InterruptedException e) {
       throw new RuntimeException(e);
@@ -89,13 +89,18 @@ public class BigQueryEngine extends AQueryEngine<BigQueryDatastore> {
       return null;
     }
     com.google.cloud.bigquery.Field field = schema.getFields().get(index);
-    return switch (field.getType().getStandardType()) {
-      case BOOL -> fieldValue.getBooleanValue();
-      case INT64 -> fieldValue.getLongValue();
-      case FLOAT64 -> fieldValue.getDoubleValue();
-      case BYTES -> fieldValue.getBytesValue();
-      default -> fieldValue.getValue();
-    };
+    if (field.getMode() != Field.Mode.REPEATED) {
+      return switch (field.getType().getStandardType()) {
+        case BOOL -> fieldValue.getBooleanValue();
+        case INT64 -> fieldValue.getLongValue();
+        case FLOAT64 -> fieldValue.getDoubleValue();
+        case BYTES -> fieldValue.getBytesValue();
+        case DATE -> LocalDate.parse(fieldValue.getStringValue());
+        default -> fieldValue.getValue();
+      };
+    } else {
+      return fieldValue.getValue(); // FieldValueList
+    }
   }
 
   @Override
diff --git a/clickhouse/src/main/java/io/squashql/query/database/ClickHouseQueryEngine.java b/clickhouse/src/main/java/io/squashql/query/database/ClickHouseQueryEngine.java
index 28a57e2..fe99246 100644
--- a/clickhouse/src/main/java/io/squashql/query/database/ClickHouseQueryEngine.java
+++ b/clickhouse/src/main/java/io/squashql/query/database/ClickHouseQueryEngine.java
@@ -11,15 +11,15 @@ import com.clickhouse.data.ClickHouseValue;
 import io.squashql.ClickHouseDatastore;
 import io.squashql.ClickHouseUtil;
 import io.squashql.query.Header;
-import io.squashql.query.compiled.CompiledMeasure;
 import io.squashql.table.ColumnarTable;
 import io.squashql.table.RowTable;
 import io.squashql.table.Table;
 import org.eclipse.collections.api.tuple.Pair;
 
+import java.util.Arrays;
+import java.util.HashSet;
 import java.util.List;
 import java.util.concurrent.ExecutionException;
-import java.util.stream.Collectors;
 
 public class ClickHouseQueryEngine extends AQueryEngine<ClickHouseDatastore> {
 
@@ -64,7 +64,7 @@ public class ClickHouseQueryEngine extends AQueryEngine<ClickHouseDatastore> {
               (index, r) -> getValue(r, index, response.getColumns()));
       return new ColumnarTable(
               result.getOne(),
-              query.measures.stream().map(CompiledMeasure::measure).collect(Collectors.toSet()),
+              new HashSet<>(query.measures),
               result.getTwo());
     } catch (ExecutionException | InterruptedException e) {
       throw new RuntimeException(e);
@@ -112,6 +112,7 @@ public class ClickHouseQueryEngine extends AQueryEngine<ClickHouseDatastore> {
       case Float32 -> fieldValue.asFloat();
       case Float64 -> fieldValue.asDouble();
       case String, FixedString -> fieldValue.asString();
+      case Array -> Arrays.stream(fieldValue.asArray()).toList();
       default -> throw new RuntimeException("Unexpected type " + column.getDataType());
     };
   }
diff --git a/core/src/main/java/io/squashql/jdbc/JdbcQueryEngine.java b/core/src/main/java/io/squashql/jdbc/JdbcQueryEngine.java
index 055e160..b46c1d0 100644
--- a/core/src/main/java/io/squashql/jdbc/JdbcQueryEngine.java
+++ b/core/src/main/java/io/squashql/jdbc/JdbcQueryEngine.java
@@ -1,7 +1,6 @@
 package io.squashql.jdbc;
 
 import io.squashql.query.Header;
-import io.squashql.query.compiled.CompiledMeasure;
 import io.squashql.query.database.AQueryEngine;
 import io.squashql.query.database.DatabaseQuery;
 import io.squashql.table.ColumnarTable;
@@ -11,10 +10,10 @@ import org.eclipse.collections.api.tuple.Pair;
 
 import java.io.Serializable;
 import java.math.BigInteger;
+import java.sql.Date;
 import java.sql.*;
 import java.util.*;
 import java.util.function.BiFunction;
-import java.util.stream.Collectors;
 import java.util.stream.IntStream;
 
 public abstract class JdbcQueryEngine<T extends JdbcDatastore> extends AQueryEngine<T> {
@@ -41,7 +40,7 @@ public abstract class JdbcQueryEngine<T extends JdbcDatastore> extends AQueryEng
               (i, fieldValues) -> fieldValues[i]);
       return new ColumnarTable(
               result.getOne(),
-              query.measures.stream().map(CompiledMeasure::measure).collect(Collectors.toSet()),
+              new HashSet<>(query.measures),
               result.getTwo());
     });
   }
@@ -113,7 +112,10 @@ public abstract class JdbcQueryEngine<T extends JdbcDatastore> extends AQueryEng
         case Types.REAL, Types.FLOAT -> tableResult.getFloat(1 + index);
         case Types.DECIMAL, Types.DOUBLE -> tableResult.getDouble(1 + index);
         case Types.BINARY, Types.VARBINARY, Types.LONGVARBINARY -> tableResult.getBytes(1 + index);
-        case Types.DATE -> tableResult.getDate(1 + index);
+        case Types.DATE -> {
+          Date d = tableResult.getDate(1 + index);
+          yield d == null ? null : d.toLocalDate();
+        }
         case Types.TIME -> tableResult.getTime(1 + index);
         case Types.TIMESTAMP -> tableResult.getTimestamp(1 + index);
         default -> {
diff --git a/core/src/main/java/io/squashql/jdbc/JdbcUtil.java b/core/src/main/java/io/squashql/jdbc/JdbcUtil.java
index 7775928..47146e2 100644
--- a/core/src/main/java/io/squashql/jdbc/JdbcUtil.java
+++ b/core/src/main/java/io/squashql/jdbc/JdbcUtil.java
@@ -7,6 +7,8 @@ import io.squashql.type.TableTypedField;
 import io.squashql.store.Store;
 
 import java.sql.*;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
@@ -67,8 +69,8 @@ public final class JdbcUtil {
       case Types.REAL, Types.FLOAT -> float.class;
       case Types.DECIMAL, Types.DOUBLE -> double.class;
       case Types.BINARY, Types.VARBINARY, Types.LONGVARBINARY -> byte[].class;
-      case Types.DATE -> Date.class;
-      case Types.TIME -> Time.class;
+      case Types.DATE -> LocalDate.class;
+      case Types.TIME -> LocalDateTime.class;
       case Types.TIMESTAMP -> Timestamp.class;
       default -> Object.class;
     };
diff --git a/core/src/main/java/io/squashql/query/AComparisonExecutor.java b/core/src/main/java/io/squashql/query/AComparisonExecutor.java
index 6272cf6..5813cb2 100644
--- a/core/src/main/java/io/squashql/query/AComparisonExecutor.java
+++ b/core/src/main/java/io/squashql/query/AComparisonExecutor.java
@@ -42,10 +42,10 @@ public abstract class AComparisonExecutor {
     Object[] buffer = new Object[readFromTableColumnsCount];
     Header[] headers = new Header[readFromTableColumnsCount];
     List<Object> result = new ArrayList<>((int) writeToTable.count());
-    List<Object> readAggregateValues = readFromTable.getAggregateValues(cm.reference().measure());
-    List<Object> writeAggregateValues = writeToTable.getAggregateValues(cm.reference().measure());
+    List<Object> readAggregateValues = readFromTable.getAggregateValues(cm.measure());
+    List<Object> writeAggregateValues = writeToTable.getAggregateValues(cm.measure());
     BiFunction<Number, Number, Number> comparisonBiFunction = BinaryOperations.createComparisonBiFunction(
-            cm.measure().comparisonMethod, readFromTable.getHeader(cm.reference().measure()).type());
+            cm.comparisonMethod(), readFromTable.getHeader(cm.measure()).type());
     int[] rowIndex = new int[1];
     IntIntMap mapping = buildMapping(writeToTable, readFromTable); // columns might be in a different order
     writeToTable.forEach(row -> {
diff --git a/core/src/main/java/io/squashql/query/AggregatedMeasure.java b/core/src/main/java/io/squashql/query/AggregatedMeasure.java
index 68964a7..ee755db 100644
--- a/core/src/main/java/io/squashql/query/AggregatedMeasure.java
+++ b/core/src/main/java/io/squashql/query/AggregatedMeasure.java
@@ -54,5 +54,4 @@ public class AggregatedMeasure implements BasicMeasure {
   public String expression() {
     return this.expression;
   }
-
 }
diff --git a/core/src/main/java/io/squashql/query/BucketComparisonExecutor.java b/core/src/main/java/io/squashql/query/BucketComparisonExecutor.java
index f282ae8..6ffecb6 100644
--- a/core/src/main/java/io/squashql/query/BucketComparisonExecutor.java
+++ b/core/src/main/java/io/squashql/query/BucketComparisonExecutor.java
@@ -1,7 +1,9 @@
 package io.squashql.query;
 
+import io.squashql.query.compiled.CompiledBucketColumnSet;
 import io.squashql.query.compiled.CompiledComparisonMeasure;
-import io.squashql.query.dto.BucketColumnSetDto;
+import io.squashql.query.database.SqlUtils;
+import io.squashql.type.TypedField;
 import org.eclipse.collections.api.map.primitive.ObjectIntMap;
 import org.eclipse.collections.api.tuple.Pair;
 import org.eclipse.collections.impl.tuple.Tuples;
@@ -14,15 +16,15 @@ import java.util.function.BiPredicate;
 
 public class BucketComparisonExecutor extends AComparisonExecutor {
 
-  final BucketColumnSetDto cSet;
+  final CompiledBucketColumnSet cSet;
 
-  public BucketComparisonExecutor(BucketColumnSetDto cSet) {
+  public BucketComparisonExecutor(CompiledBucketColumnSet cSet) {
     this.cSet = cSet;
   }
 
   @Override
   protected BiPredicate<Object[], Header[]> createShiftProcedure(CompiledComparisonMeasure cm, ObjectIntMap<String> indexByColumn) {
-    return new ShiftProcedure(this.cSet, cm.measure().referencePosition, indexByColumn);
+    return new ShiftProcedure(this.cSet, cm.referencePosition(), indexByColumn);
   }
 
   static class ShiftProcedure implements BiPredicate<Object[], Header[]> {
@@ -31,13 +33,13 @@ public class BucketComparisonExecutor extends AComparisonExecutor {
     final ObjectIntMap<String> indexByColumn;
     final Map<String, List<String>> valuesByBucket = new LinkedHashMap<>();
 
-    ShiftProcedure(BucketColumnSetDto cSet, Map<Field, String> referencePosition, ObjectIntMap<String> indexByColumn) {
-      this.valuesByBucket.putAll(cSet.values);
+    ShiftProcedure(CompiledBucketColumnSet cSet, Map<TypedField, String> referencePosition, ObjectIntMap<String> indexByColumn) {
+      this.valuesByBucket.putAll(cSet.values());
       this.indexByColumn = indexByColumn;
       this.transformationByColumn = new ArrayList<>();
       // Order does matter here
-      this.transformationByColumn.add(Tuples.pair(cSet.newField.name(), parse(referencePosition.get(cSet.newField))));
-      this.transformationByColumn.add(Tuples.pair(cSet.field.name(), parse(referencePosition.get(cSet.field))));
+      this.transformationByColumn.add(Tuples.pair(SqlUtils.squashqlExpression(cSet.newField()), parse(referencePosition.get(cSet.newField()))));
+      this.transformationByColumn.add(Tuples.pair(SqlUtils.squashqlExpression(cSet.field()), parse(referencePosition.get(cSet.field()))));
     }
 
     @Override
diff --git a/core/src/main/java/io/squashql/query/CaffeineQueryCache.java b/core/src/main/java/io/squashql/query/CaffeineQueryCache.java
index 80ca282..c9ecf90 100644
--- a/core/src/main/java/io/squashql/query/CaffeineQueryCache.java
+++ b/core/src/main/java/io/squashql/query/CaffeineQueryCache.java
@@ -6,6 +6,8 @@ import com.github.benmanes.caffeine.cache.RemovalListener;
 import com.github.benmanes.caffeine.cache.stats.CacheStats;
 import com.github.benmanes.caffeine.cache.stats.ConcurrentStatsCounter;
 import com.github.benmanes.caffeine.cache.stats.StatsCounter;
+import io.squashql.query.compiled.CompiledAggregatedMeasure;
+import io.squashql.query.compiled.CompiledMeasure;
 import io.squashql.query.database.SqlUtils;
 import io.squashql.query.dto.CacheStatsDto;
 import io.squashql.table.ColumnarTable;
@@ -13,10 +15,7 @@ import io.squashql.table.Table;
 import io.squashql.type.TypedField;
 
 import java.time.Duration;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.Set;
+import java.util.*;
 
 public class CaffeineQueryCache implements QueryCache {
 
@@ -28,14 +27,14 @@ public class CaffeineQueryCache implements QueryCache {
   /**
    * The cached results.
    */
-  private final Cache<PrefetchQueryScope, Table> results;
+  private final Cache<QueryCacheKey, Table> results;
 
   public CaffeineQueryCache() {
     this(MAX_SIZE, (a, b, c) -> {
     });
   }
 
-  public CaffeineQueryCache(int maxSize, RemovalListener<PrefetchQueryScope, Table> evictionListener) {
+  public CaffeineQueryCache(int maxSize, RemovalListener<QueryCacheKey, Table> evictionListener) {
     this.results = Caffeine.newBuilder()
             .maximumSize(maxSize)
             .expireAfterWrite(Duration.ofMinutes(5))
@@ -46,25 +45,25 @@ public class CaffeineQueryCache implements QueryCache {
   }
 
   @Override
-  public ColumnarTable createRawResult(PrefetchQueryScope scope) {
-    Set<TypedField> columns = scope.columns();
+  public ColumnarTable createRawResult(QueryCacheKey key) {
+    Set<TypedField> columns = new LinkedHashSet<>(key.scope().columns());
     List<Header> headers = new ArrayList<>(columns.stream().map(column -> new Header(SqlUtils.squashqlExpression(column), column.type(), false)).toList());
     headers.add(new Header(CountMeasure.ALIAS, long.class, true));
 
     List<List<Object>> values = new ArrayList<>();
-    Table table = this.results.getIfPresent(scope);
+    Table table = this.results.getIfPresent(key);
     for (TypedField f : columns) {
       values.add(table.getColumnValues(SqlUtils.squashqlExpression(f)));
     }
-    values.add(table.getAggregateValues(CountMeasure.INSTANCE));
+    values.add(table.getAggregateValues(CompiledAggregatedMeasure.COMPILED_COUNT));
     return new ColumnarTable(
             headers,
-            Collections.singleton(CountMeasure.INSTANCE),
+            Collections.singleton(CompiledAggregatedMeasure.COMPILED_COUNT),
             values);
   }
 
   @Override
-  public boolean contains(Measure measure, PrefetchQueryScope scope) {
+  public boolean contains(CompiledMeasure measure, QueryCacheKey scope) {
     Table table = this.results.getIfPresent(scope);
     if (table != null) {
       return table.measures().contains(measure);
@@ -73,13 +72,13 @@ public class CaffeineQueryCache implements QueryCache {
   }
 
   @Override
-  public void contributeToCache(Table result, Set<Measure> measures, PrefetchQueryScope scope) {
+  public void contributeToCache(Table result, Set<CompiledMeasure> measures, QueryCacheKey scope) {
     Table cache = this.results.get(scope, s -> {
       this.measureCounter.recordMisses(measures.size());
       return result;
     });
 
-    for (Measure measure : measures) {
+    for (CompiledMeasure measure : measures) {
       if (!cache.measures().contains(measure)) {
         // Not in the previousResult, add it.
         cache.transferAggregates(result, measure);
@@ -89,12 +88,12 @@ public class CaffeineQueryCache implements QueryCache {
   }
 
   @Override
-  public void contributeToResult(Table result, Set<Measure> measures, PrefetchQueryScope scope) {
+  public void contributeToResult(Table result, Set<CompiledMeasure> measures, QueryCacheKey scope) {
     if (measures.isEmpty()) {
       return;
     }
     Table cacheResult = this.results.getIfPresent(scope);
-    for (Measure measure : measures) {
+    for (CompiledMeasure measure : measures) {
       result.transferAggregates(cacheResult, measure);
       this.measureCounter.recordHits(1);
     }
diff --git a/core/src/main/java/io/squashql/query/EmptyQueryCache.java b/core/src/main/java/io/squashql/query/EmptyQueryCache.java
index b281b50..04fe1df 100644
--- a/core/src/main/java/io/squashql/query/EmptyQueryCache.java
+++ b/core/src/main/java/io/squashql/query/EmptyQueryCache.java
@@ -1,5 +1,6 @@
 package io.squashql.query;
 
+import io.squashql.query.compiled.CompiledMeasure;
 import io.squashql.query.dto.CacheStatsDto;
 import io.squashql.table.ColumnarTable;
 import io.squashql.table.Table;
@@ -14,22 +15,22 @@ public class EmptyQueryCache implements QueryCache {
   }
 
   @Override
-  public ColumnarTable createRawResult(PrefetchQueryScope scope) {
+  public ColumnarTable createRawResult(QueryCacheKey scope) {
     throw new IllegalStateException();
   }
 
   @Override
-  public boolean contains(Measure measure, PrefetchQueryScope scope) {
+  public boolean contains(CompiledMeasure measure, QueryCacheKey scope) {
     return false;
   }
 
   @Override
-  public void contributeToCache(Table result, Set<Measure> measures, PrefetchQueryScope scope) {
+  public void contributeToCache(Table result, Set<CompiledMeasure> measures, QueryCacheKey scope) {
     // NOOP
   }
 
   @Override
-  public void contributeToResult(Table result, Set<Measure> measures, PrefetchQueryScope scope) {
+  public void contributeToResult(Table result, Set<CompiledMeasure> measures, QueryCacheKey scope) {
     // NOOP
   }
 
diff --git a/core/src/main/java/io/squashql/query/GlobalCache.java b/core/src/main/java/io/squashql/query/GlobalCache.java
index c882697..5b0b3cc 100644
--- a/core/src/main/java/io/squashql/query/GlobalCache.java
+++ b/core/src/main/java/io/squashql/query/GlobalCache.java
@@ -1,5 +1,6 @@
 package io.squashql.query;
 
+import io.squashql.query.compiled.CompiledMeasure;
 import io.squashql.query.dto.CacheStatsDto;
 import io.squashql.table.ColumnarTable;
 import io.squashql.table.Table;
@@ -26,27 +27,27 @@ public class GlobalCache implements QueryCache {
     return user == null ? ANONYMOUS : user;
   }
 
-  protected CaffeineQueryCache getCache(PrefetchQueryScope scope) {
+  protected CaffeineQueryCache getCache(QueryCacheKey scope) {
     return this.cacheByUser.computeIfAbsent(user(scope.user()), u -> this.cacheSupplier.get());
   }
 
   @Override
-  public ColumnarTable createRawResult(PrefetchQueryScope scope) {
+  public ColumnarTable createRawResult(QueryCacheKey scope) {
     return getCache(scope).createRawResult(scope);
   }
 
   @Override
-  public boolean contains(Measure measure, PrefetchQueryScope scope) {
+  public boolean contains(CompiledMeasure measure, QueryCacheKey scope) {
     return getCache(scope).contains(measure, scope);
   }
 
   @Override
-  public void contributeToCache(Table result, Set<Measure> measures, PrefetchQueryScope scope) {
+  public void contributeToCache(Table result, Set<CompiledMeasure> measures, QueryCacheKey scope) {
     getCache(scope).contributeToCache(result, measures, scope);
   }
 
   @Override
-  public void contributeToResult(Table result, Set<Measure> measures, PrefetchQueryScope scope) {
+  public void contributeToResult(Table result, Set<CompiledMeasure> measures, QueryCacheKey scope) {
     getCache(scope).contributeToResult(result, measures, scope);
   }
 
diff --git a/core/src/main/java/io/squashql/query/GraphPrinter.java b/core/src/main/java/io/squashql/query/GraphPrinter.java
index a7577f1..f423e2d 100644
--- a/core/src/main/java/io/squashql/query/GraphPrinter.java
+++ b/core/src/main/java/io/squashql/query/GraphPrinter.java
@@ -61,7 +61,7 @@ public class GraphPrinter {
     if (o instanceof QueryExecutor.QueryPlanNodeKey key) {
       StringBuilder sb = new StringBuilder();
       sb.append("scope=").append("#").append(idProvider.apply(key.queryScope()));
-      sb.append(", measure=[").append(key.measure().alias()).append("]; [").append(MeasureUtils.createExpression(key.measure().measure())).append("]");
+      sb.append(", measure=[").append(key.measure().alias()).append("]; [").append(key.measure()).append("]");
       return sb.toString();
     } else {
       return String.valueOf(o);
diff --git a/core/src/main/java/io/squashql/query/MeasureUtils.java b/core/src/main/java/io/squashql/query/MeasureUtils.java
index 96d4c59..fe56d03 100644
--- a/core/src/main/java/io/squashql/query/MeasureUtils.java
+++ b/core/src/main/java/io/squashql/query/MeasureUtils.java
@@ -29,7 +29,7 @@ public final class MeasureUtils {
 
   public static String createExpression(Measure m) {
     if (m instanceof AggregatedMeasure a) {
-      final CompiledAggregatedMeasure compiled = (CompiledAggregatedMeasure) new ExpressionResolver(m).getMeasures().get(0);
+      final CompiledAggregatedMeasure compiled = (CompiledAggregatedMeasure) new ExpressionResolver(m).getMeasures().values().iterator().next();
       final String fieldExpression = compiled.field().sqlExpression(BASIC);
       if (compiled.criteria() == null) {
         return a.aggregationFunction + "(" + fieldExpression + ")";
@@ -110,7 +110,8 @@ public final class MeasureUtils {
             queryScope.havingCriteria(),
             new ArrayList<>(rollupColumns),
             new ArrayList<>(queryScope.groupingSets()), // FIXME should handle groupingSets
-            queryScope.virtualTable());
+            queryScope.virtualTable(),
+            queryScope.limit());
   }
 
   private static CompiledCriteria removeCriteriaOnField(TypedField field, CompiledCriteria root) {
diff --git a/core/src/main/java/io/squashql/query/PeriodComparisonExecutor.java b/core/src/main/java/io/squashql/query/PeriodComparisonExecutor.java
index 7a8d250..7d21d9c 100644
--- a/core/src/main/java/io/squashql/query/PeriodComparisonExecutor.java
+++ b/core/src/main/java/io/squashql/query/PeriodComparisonExecutor.java
@@ -1,8 +1,9 @@
 package io.squashql.query;
 
 import io.squashql.query.compiled.CompiledComparisonMeasure;
+import io.squashql.query.compiled.CompiledPeriod;
 import io.squashql.query.database.SQLTranslator;
-import io.squashql.query.dto.Period;
+import io.squashql.type.TypedField;
 import org.eclipse.collections.api.map.primitive.MutableObjectIntMap;
 import org.eclipse.collections.api.map.primitive.ObjectIntMap;
 import org.eclipse.collections.impl.map.mutable.primitive.ObjectIntHashMap;
@@ -23,14 +24,14 @@ public class PeriodComparisonExecutor extends AComparisonExecutor {
     this.cmrp = cmrp;
   }
 
-  public Map<Field, PeriodUnit> mapping(Period period) {
-    if (period instanceof Period.Quarter q) {
+  public Map<TypedField, PeriodUnit> mapping(CompiledPeriod period) {
+    if (period instanceof CompiledPeriod.Quarter q) {
       return Map.of(q.quarter(), QUARTER, q.year(), YEAR);
-    } else if (period instanceof Period.Year y) {
+    } else if (period instanceof CompiledPeriod.Year y) {
       return Map.of(y.year(), YEAR);
-    } else if (period instanceof Period.Month m) {
+    } else if (period instanceof CompiledPeriod.Month m) {
       return Map.of(m.month(), MONTH, m.year(), YEAR);
-    } else if (period instanceof Period.Semester s) {
+    } else if (period instanceof CompiledPeriod.Semester s) {
       return Map.of(s.semester(), SEMESTER, s.year(), YEAR);
     } else {
       throw new RuntimeException(period + " not supported yet");
@@ -40,15 +41,15 @@ public class PeriodComparisonExecutor extends AComparisonExecutor {
   @Override
   protected BiPredicate<Object[], Header[]> createShiftProcedure(CompiledComparisonMeasure cm, ObjectIntMap<String> indexByColumn) {
     Map<PeriodUnit, String> referencePosition = new HashMap<>();
-    Period period = this.cmrp.measure().period;
-    Map<Field, PeriodUnit> mapping = mapping(period);
+    CompiledPeriod period = this.cmrp.period();
+    Map<TypedField, PeriodUnit> mapping = mapping(period);
     MutableObjectIntMap<PeriodUnit> indexByPeriodUnit = new ObjectIntHashMap<>();
-    for (Map.Entry<Field, String> entry : cm.measure().referencePosition.entrySet()) {
+    for (Map.Entry<TypedField, String> entry : cm.referencePosition().entrySet()) {
       PeriodUnit pu = mapping.get(entry.getKey());
       referencePosition.put(pu, entry.getValue());
       indexByPeriodUnit.put(pu, indexByColumn.getIfAbsent(entry.getKey().name(), -1));
     }
-    for (Map.Entry<Field, PeriodUnit> entry : mapping.entrySet()) {
+    for (Map.Entry<TypedField, PeriodUnit> entry : mapping.entrySet()) {
       PeriodUnit pu = mapping.get(entry.getKey());
       referencePosition.putIfAbsent(pu, "c"); // constant for missing ref.
       indexByPeriodUnit.getIfAbsentPut(pu, indexByColumn.getIfAbsent(entry.getKey().name(), -1));
@@ -58,12 +59,12 @@ public class PeriodComparisonExecutor extends AComparisonExecutor {
 
   static class ShiftProcedure implements BiPredicate<Object[], Header[]> {
 
-    final Period period;
+    final CompiledPeriod period;
     final Map<PeriodUnit, String> referencePosition;
     final ObjectIntMap<PeriodUnit> indexByPeriodUnit;
     final Map<PeriodUnit, Object> transformationByPeriodUnit;
 
-    ShiftProcedure(Period period,
+    ShiftProcedure(CompiledPeriod period,
                    Map<PeriodUnit, String> referencePosition,
                    ObjectIntMap<PeriodUnit> indexByPeriodUnit) {
       this.period = period;
@@ -90,7 +91,7 @@ public class PeriodComparisonExecutor extends AComparisonExecutor {
       Object semesterTransformation = this.transformationByPeriodUnit.get(PeriodUnit.SEMESTER);
       Object quarterTransformation = this.transformationByPeriodUnit.get(PeriodUnit.QUARTER);
       Object monthTransformation = this.transformationByPeriodUnit.get(PeriodUnit.MONTH);
-      if (this.period instanceof Period.Quarter) {
+      if (this.period instanceof CompiledPeriod.Quarter) {
         // YEAR, QUARTER
         if (this.referencePosition.containsKey(PeriodUnit.YEAR) && yearTransformation != null) {
           int year = readAsLong(row[yearIndex]);
@@ -109,7 +110,7 @@ public class PeriodComparisonExecutor extends AComparisonExecutor {
           write(row, quarterIndex, headers[quarterIndex], (int) IsoFields.QUARTER_OF_YEAR.getFrom(newDate));
           write(row, yearIndex, headers[yearIndex], newDate.getYear());// year might have changed
         }
-      } else if (this.period instanceof Period.Year) {
+      } else if (this.period instanceof CompiledPeriod.Year) {
         // YEAR
         if (this.referencePosition.containsKey(PeriodUnit.YEAR) && yearTransformation != null) {
           int year = readAsLong(row[yearIndex]);
@@ -118,7 +119,7 @@ public class PeriodComparisonExecutor extends AComparisonExecutor {
           }
           write(row, yearIndex, headers[yearIndex], year + (int) yearTransformation);
         }
-      } else if (this.period instanceof Period.Month) {
+      } else if (this.period instanceof CompiledPeriod.Month) {
         // YEAR, MONTH
         if (this.referencePosition.containsKey(PeriodUnit.YEAR) && yearTransformation != null) {
           int year = readAsLong(row[yearIndex]);
@@ -137,7 +138,7 @@ public class PeriodComparisonExecutor extends AComparisonExecutor {
           write(row, monthIndex, headers[monthIndex], newDate.getMonthValue());
           write(row, yearIndex, headers[yearIndex], newDate.getYear()); // year might have changed
         }
-      } else if (this.period instanceof Period.Semester) {
+      } else if (this.period instanceof CompiledPeriod.Semester) {
         // YEAR, SEMESTER
         if (this.referencePosition.containsKey(PeriodUnit.YEAR) && yearTransformation != null) {
           int year = readAsLong(row[yearIndex]);
@@ -179,14 +180,14 @@ public class PeriodComparisonExecutor extends AComparisonExecutor {
       }
     }
 
-    private static PeriodUnit[] getPeriodUnits(Period period) {
-      if (period instanceof Period.Quarter) {
+    private static PeriodUnit[] getPeriodUnits(CompiledPeriod period) {
+      if (period instanceof CompiledPeriod.Quarter) {
         return new PeriodUnit[]{PeriodUnit.YEAR, PeriodUnit.QUARTER};
-      } else if (period instanceof Period.Year) {
+      } else if (period instanceof CompiledPeriod.Year) {
         return new PeriodUnit[]{PeriodUnit.YEAR};
-      } else if (period instanceof Period.Month) {
+      } else if (period instanceof CompiledPeriod.Month) {
         return new PeriodUnit[]{PeriodUnit.YEAR, PeriodUnit.MONTH};
-      } else if (period instanceof Period.Semester) {
+      } else if (period instanceof CompiledPeriod.Semester) {
         return new PeriodUnit[]{PeriodUnit.YEAR, PeriodUnit.SEMESTER};
       } else {
         throw new RuntimeException(period + " not supported yet");
diff --git a/core/src/main/java/io/squashql/query/QueryCache.java b/core/src/main/java/io/squashql/query/QueryCache.java
index 596eb03..c439ff7 100644
--- a/core/src/main/java/io/squashql/query/QueryCache.java
+++ b/core/src/main/java/io/squashql/query/QueryCache.java
@@ -1,26 +1,21 @@
 package io.squashql.query;
 
-import io.squashql.query.compiled.CompiledCriteria;
 import io.squashql.query.compiled.CompiledMeasure;
-import io.squashql.query.compiled.CompiledTable;
 import io.squashql.query.dto.CacheStatsDto;
-import io.squashql.query.dto.VirtualTableDto;
 import io.squashql.table.ColumnarTable;
 import io.squashql.table.Table;
-import io.squashql.type.TypedField;
 
-import java.util.List;
 import java.util.Set;
 
 public interface QueryCache {
 
-  ColumnarTable createRawResult(PrefetchQueryScope scope);
+  ColumnarTable createRawResult(QueryCacheKey scope);
 
-  boolean contains(Measure measure, PrefetchQueryScope scope);
+  boolean contains(CompiledMeasure measure, QueryCacheKey scope);
 
-  void contributeToCache(Table result, Set<Measure> measures, PrefetchQueryScope scope);
+  void contributeToCache(Table result, Set<CompiledMeasure> measures, QueryCacheKey scope);
 
-  void contributeToResult(Table result, Set<Measure> measures, PrefetchQueryScope scope);
+  void contributeToResult(Table result, Set<CompiledMeasure> measures, QueryCacheKey scope);
 
   /**
    * Invalidates the cache associated to the given user.
@@ -36,32 +31,6 @@ public interface QueryCache {
 
   CacheStatsDto stats(SquashQLUser user);
 
-  record TableScope(CompiledTable table,
-                    Set<TypedField> columns,
-                    CompiledCriteria whereCriteria,
-                    CompiledCriteria havingCriteria,
-                    List<TypedField> rollupColumns,
-                    List<List<TypedField>> groupingSets,
-                    VirtualTableDto virtualTable,
-                    SquashQLUser user,
-                    int limit) implements PrefetchQueryScope {
-  }
-
-  record SubQueryScope(QueryExecutor.QueryScope subQuery,
-                       List<CompiledMeasure> measures,
-                       Set<TypedField> columns,
-                       CompiledCriteria whereCriteriaDto,
-                       CompiledCriteria havingCriteriaDto,
-                       SquashQLUser user,
-                       int limit) implements PrefetchQueryScope {
-  }
-
-  /**
-   * Marker interface.
-   */
-  interface PrefetchQueryScope {
-    Set<TypedField> columns();
-
-    SquashQLUser user();
+  record QueryCacheKey(QueryExecutor.QueryScope scope, SquashQLUser user) {
   }
 }
diff --git a/core/src/main/java/io/squashql/query/QueryExecutor.java b/core/src/main/java/io/squashql/query/QueryExecutor.java
index b322afa..23c7118 100644
--- a/core/src/main/java/io/squashql/query/QueryExecutor.java
+++ b/core/src/main/java/io/squashql/query/QueryExecutor.java
@@ -1,11 +1,10 @@
 package io.squashql.query;
 
 import io.squashql.jackson.JacksonUtil;
-import io.squashql.query.QueryCache.SubQueryScope;
-import io.squashql.query.QueryCache.TableScope;
 import io.squashql.query.compiled.*;
 import io.squashql.query.database.DatabaseQuery;
 import io.squashql.query.database.QueryEngine;
+import io.squashql.query.database.SqlUtils;
 import io.squashql.query.dto.*;
 import io.squashql.query.parameter.QueryCacheParameter;
 import io.squashql.table.ColumnarTable;
@@ -21,6 +20,7 @@ import java.util.function.IntConsumer;
 import java.util.stream.Collectors;
 
 import static io.squashql.query.ColumnSetKey.BUCKET;
+import static io.squashql.query.agg.AggregationFunction.GROUPING;
 import static io.squashql.query.compiled.CompiledAggregatedMeasure.COMPILED_COUNT;
 
 @Slf4j
@@ -142,10 +142,11 @@ public class QueryExecutor {
                             boolean replaceTotalCellsAndOrderRows,
                             IntConsumer limitNotifier) {
     int queryLimit = query.limit < 0 ? LIMIT_DEFAULT_VALUE : query.limit;
+    query.limit = queryLimit;
 
     final QueryResolver queryResolver = new QueryResolver(query, new HashMap<>(this.queryEngine.datastore().storesByName()));
     DependencyGraph<QueryPlanNodeKey> dependencyGraph = computeDependencyGraph(
-            queryResolver.getColumns(), queryResolver.getBucketColumns(), queryResolver.getMeasures(), queryResolver.getScope());
+            queryResolver.getColumns(), queryResolver.getBucketColumns(), queryResolver.getMeasures().values(), queryResolver.getScope());
     // Compute what needs to be prefetched
     Map<QueryScope, DatabaseQuery> prefetchQueryByQueryScope = new HashMap<>();
     Map<QueryScope, Set<CompiledMeasure>> measuresByQueryScope = new HashMap<>();
@@ -161,35 +162,36 @@ public class QueryExecutor {
     for (QueryScope scope : prefetchQueryByQueryScope.keySet()) {
       DatabaseQuery prefetchQuery = prefetchQueryByQueryScope.get(scope);
       Set<CompiledMeasure> measures = measuresByQueryScope.get(scope);
-      QueryCache.PrefetchQueryScope prefetchQueryScope = createPrefetchQueryScope(scope, prefetchQuery, user);
+      QueryCache.QueryCacheKey queryCacheKey = new QueryCache.QueryCacheKey(scope, user);
       QueryCache queryCache = getQueryCache((QueryCacheParameter) query.parameters.getOrDefault(QueryCacheParameter.KEY, new QueryCacheParameter(QueryCacheParameter.Action.USE)), user);
 
-      Set<Measure> cached = new HashSet<>();
-      Map<Measure, CompiledMeasure> notCached = new HashMap<>();
+      Set<CompiledMeasure> cached = new HashSet<>();
+      Set<CompiledMeasure> notCached = new HashSet<>();
       for (CompiledMeasure measure : measures) {
         if (MeasureUtils.isPrimitive(measure)) {
-          if (queryCache.contains(measure.measure(), prefetchQueryScope)) {
-            cached.add(measure.measure());
+          if (queryCache.contains(measure, queryCacheKey)) {
+            cached.add(measure);
           } else {
-            notCached.put(measure.measure(), measure);
+            notCached.add(measure);
           }
         }
       }
 
       Table result;
       if (!notCached.isEmpty()) {
-        notCached.put(CountMeasure.INSTANCE, COMPILED_COUNT);
-        notCached.forEach((k, v) -> prefetchQuery.withMeasure(v));
+        notCached.add(COMPILED_COUNT);
+        notCached.forEach(prefetchQuery::withMeasure);
         result = this.queryEngine.execute(prefetchQuery);
       } else {
         // Create an empty result that will be populated by the query cache
-        result = queryCache.createRawResult(prefetchQueryScope);
+        result = queryCache.createRawResult(queryCacheKey);
       }
 
-      queryCache.contributeToResult(result, cached, prefetchQueryScope);
-      queryCache.contributeToCache(result, notCached.keySet(), prefetchQueryScope);
+      queryCache.contributeToResult(result, cached, queryCacheKey);
+      queryCache.contributeToCache(result, notCached, queryCacheKey);
 
-      tableByScope.put(scope, result);
+      // The table in the cache contains null values for totals but in this map, we need to replace the nulls with totals
+      tableByScope.put(scope, TableUtils.replaceNullCellsByTotal(result, scope));
     }
 
     if (query.columnSets.containsKey(BUCKET)) {
@@ -205,7 +207,12 @@ public class QueryExecutor {
     final Evaluator evaluator = new Evaluator();
     ExecutionPlan<QueryPlanNodeKey> globalPlan = new ExecutionPlan<>(dependencyGraph, (queryNode) -> {
       if (visited.add(queryNode)) {
-        final ExecutionContext executionContext = new ExecutionContext(queryNode.queryScope, tableByScope, queryResolver.getColumns(), queryResolver.getBucketColumns(), query.columnSets, queryLimit);
+        final ExecutionContext executionContext = new ExecutionContext(queryNode.queryScope,
+                tableByScope,
+                queryResolver.getColumns(),
+                queryResolver.getBucketColumns(),
+                queryResolver.getCompiledColumnSets(),
+                queryLimit);
         evaluator.accept(queryNode, executionContext);
       }
     });
@@ -234,57 +241,39 @@ public class QueryExecutor {
   private static DependencyGraph<QueryPlanNodeKey> computeDependencyGraph(
           List<TypedField> columns,
           List<TypedField> bucketColumns,
-          List<CompiledMeasure> measures,
+          Collection<CompiledMeasure> measures,
           QueryScope queryScope) {
     GraphDependencyBuilder<QueryPlanNodeKey> builder = new GraphDependencyBuilder<>(nodeKey -> {
       Map<QueryScope, Set<CompiledMeasure>> dependencies = nodeKey.measure.accept(new PrefetchVisitor(columns, bucketColumns, nodeKey.queryScope));
       Set<QueryPlanNodeKey> set = new HashSet<>();
       for (Map.Entry<QueryScope, Set<CompiledMeasure>> entry : dependencies.entrySet()) {
+        QueryScope key = entry.getKey();
         for (CompiledMeasure measure : entry.getValue()) {
-          set.add(new QueryPlanNodeKey(entry.getKey(), measure));
+          set.add(new QueryPlanNodeKey(key, measure));
+        }
+
+        Collection<CompiledMeasure> additionalMeasures = generateGroupingMeasures(key).values();
+        for (CompiledMeasure measure : additionalMeasures) {
+          set.add(new QueryPlanNodeKey(key, measure));
         }
       }
       return set;
     });
     Set<CompiledMeasure> queriedMeasures = new HashSet<>(measures);
     queriedMeasures.add(COMPILED_COUNT);
+    queriedMeasures.addAll(generateGroupingMeasures(queryScope).values());
     return builder.build(queriedMeasures.stream().map(m -> new QueryPlanNodeKey(queryScope, m)).toList());
   }
 
-  private static QueryCache.PrefetchQueryScope createPrefetchQueryScope(
-          QueryScope queryScope,
-          DatabaseQuery prefetchQuery,
-          SquashQLUser user) {
-    Set<TypedField> fields = new LinkedHashSet<>(prefetchQuery.select);
-    if (queryScope.table != null) {
-      return new TableScope(queryScope.table,
-              fields,
-              queryScope.whereCriteria,
-              queryScope.havingCriteria,
-              queryScope.rollupColumns,
-              queryScope.groupingSets,
-              queryScope.virtualTable,
-              user,
-              prefetchQuery.limit);
-    } else {
-      return new SubQueryScope(queryScope.subQuery,
-              prefetchQuery.subQuery == null ? Collections.emptyList() : prefetchQuery.subQuery.measures,
-              fields,
-              queryScope.whereCriteria,
-              queryScope.havingCriteria,
-              user,
-              prefetchQuery.limit);
-    }
-  }
-
   public record QueryScope(CompiledTable table,
-                           QueryScope subQuery,
+                           DatabaseQuery subQuery,
                            List<TypedField> columns,
                            CompiledCriteria whereCriteria,
                            CompiledCriteria havingCriteria,
                            List<TypedField> rollupColumns,
                            List<List<TypedField>> groupingSets,
-                           VirtualTableDto virtualTable) {
+                           VirtualTableDto virtualTable,
+                           int limit) {
   }
 
   public record QueryPlanNodeKey(QueryScope queryScope, CompiledMeasure measure) {
@@ -294,10 +283,10 @@ public class QueryExecutor {
                                  Map<QueryScope, Table> tableByScope,
                                  List<TypedField> columns,
                                  List<TypedField> bucketColumns,
-                                 Map<ColumnSetKey, ColumnSet> columnSets,
+                                 Map<ColumnSetKey, CompiledColumnSet> columnSets,
                                  int queryLimit) {
     public Table getWriteToTable() {
-      return tableByScope.get(queryScope);
+      return this.tableByScope.get(this.queryScope);
     }
   }
 
@@ -340,4 +329,29 @@ public class QueryExecutor {
   public Table executeQueryMerge(QueryDto first, QueryDto second, JoinType joinType, SquashQLUser user) {
     return QueryMergeExecutor.executeQueryMerge(this, first, second, joinType, user);
   }
+
+  /**
+   * Generates grouping measures based on the provided query scope.
+   *
+   * @param queryScope The query scope containing rollup columns and grouping sets.
+   *
+   * @return A map of compiled measures, where the key is the squashql expression of the fields used in the rollup and
+   * the value is the compiled measure.
+   */
+  public static Map<String, CompiledMeasure> generateGroupingMeasures(QueryScope queryScope) {
+    Map<String, CompiledMeasure> measures = new HashMap();
+    List<TypedField> rollups = new ArrayList<>();
+    rollups.addAll(queryScope.rollupColumns);
+    rollups.addAll(queryScope.groupingSets
+            .stream()
+            .flatMap(Collection::stream)
+            .toList());
+    if (!rollups.isEmpty()) {
+      rollups.forEach(f -> {
+        String expression = SqlUtils.squashqlExpression(f);
+        measures.put(expression, new CompiledAggregatedMeasure(SqlUtils.groupingAlias(expression.replace(".", "_")), f, GROUPING, null, false));
+      });
+    }
+    return measures;
+  }
 }
diff --git a/core/src/main/java/io/squashql/query/QueryResolver.java b/core/src/main/java/io/squashql/query/QueryResolver.java
index ebd7b1d..826c199 100644
--- a/core/src/main/java/io/squashql/query/QueryResolver.java
+++ b/core/src/main/java/io/squashql/query/QueryResolver.java
@@ -27,8 +27,9 @@ public class QueryResolver {
   private final List<TypedField> bucketColumns;
   private final List<TypedField> columns;
   private final CompilationCache cache = new CompilationCache();
-  private final List<CompiledMeasure> subQueryMeasures;
-  private final List<CompiledMeasure> measures;
+  private final Map<Measure, CompiledMeasure> subQueryMeasures;
+  private final Map<Measure, CompiledMeasure> measures;
+  private final Map<ColumnSetKey, CompiledColumnSet> compiledColumnSets;
 
   public QueryResolver(QueryDto query, Map<String, Store> storesByName) {
     this.query = query;
@@ -39,9 +40,10 @@ public class QueryResolver {
     this.columns = query.columns.stream().map(this::resolveField).toList();
     this.bucketColumns = Optional.ofNullable(query.columnSets.get(ColumnSetKey.BUCKET))
             .stream().flatMap(cs -> cs.getColumnsForPrefetching().stream()).map(this::resolveField).toList();
+    this.subQueryMeasures = query.subQuery == null ? Collections.emptyMap() : compileMeasures(query.subQuery.measures, false);
     this.scope = toQueryScope(query);
-    this.subQueryMeasures = query.subQuery == null ? Collections.emptyList() : compileMeasures(query.subQuery.measures, false);
     this.measures = compileMeasures(query.measures, true);
+    this.compiledColumnSets = compiledColumnSets(query.columnSets);
   }
 
   public TypedField getTypedField(Field field) {
@@ -136,7 +138,8 @@ public class QueryResolver {
             compileCriteria(query.havingCriteriaDto),
             rollupColumns,
             groupingSets,
-            query.virtualTableDto);
+            query.virtualTableDto,
+            query.limit);
   }
 
   protected void checkQuery(final QueryDto query) {
@@ -147,21 +150,24 @@ public class QueryResolver {
     }
   }
 
-  private QueryExecutor.QueryScope toSubQuery(final QueryDto subQuery) {
+  private DatabaseQuery toSubQuery(final QueryDto subQuery) {
     checkSubQuery(subQuery);
     final CompiledTable table = compileTable(subQuery.table);
     final List<TypedField> select = subQuery.columns.stream().map(this::resolveField).toList();
     final CompiledCriteria whereCriteria = compileCriteria(subQuery.whereCriteriaDto);
     final CompiledCriteria havingCriteria = compileCriteria(subQuery.havingCriteriaDto);
     // should we check groupingSet and rollup as well are empty ?
-    return new QueryExecutor.QueryScope(table,
+    DatabaseQuery query = new DatabaseQuery(null,
+            table,
             null,
-            select,
+            new HashSet<>(select),
             whereCriteria,
             havingCriteria,
             Collections.emptyList(),
             Collections.emptyList(),
-            null);
+            subQuery.limit);
+    this.subQueryMeasures.values().forEach(query::withMeasure);
+    return query;
   }
 
   private void checkSubQuery(final QueryDto subQuery) {
@@ -182,7 +188,7 @@ public class QueryResolver {
   public DatabaseQuery toDatabaseQuery(final QueryExecutor.QueryScope query, final int limit) {
     return new DatabaseQuery(query.virtualTable(),
             query.table(),
-            query.subQuery() == null ? null : toSubQuery(query.subQuery()),
+            query.subQuery(),
             new LinkedHashSet<>(query.columns()),
             query.whereCriteria(),
             query.havingCriteria(),
@@ -191,20 +197,6 @@ public class QueryResolver {
             limit);
   }
 
-  private DatabaseQuery toSubQuery(final QueryExecutor.QueryScope subQuery) {
-    final DatabaseQuery query = new DatabaseQuery(subQuery.virtualTable(),
-            subQuery.table(),
-            null,
-            new LinkedHashSet<>(subQuery.columns()),
-            subQuery.whereCriteria(),
-            subQuery.havingCriteria(),
-            subQuery.rollupColumns(),
-            subQuery.groupingSets(),
-            -1); // no limit for subQuery
-    this.subQueryMeasures.forEach(query::withMeasure); // sub-query measures are included for all the same way
-    return query;
-  }
-
   /**
    * Table
    */
@@ -230,10 +222,12 @@ public class QueryResolver {
   }
 
   /**
-   * measures
+   * Compiles measures
    */
-  private List<CompiledMeasure> compileMeasures(final List<Measure> measures, boolean topMeasures) {
-    return measures.stream().map(m -> compileMeasure(m, topMeasures)).collect(Collectors.toList());
+  private Map<Measure, CompiledMeasure> compileMeasures(final List<Measure> measures, boolean topMeasures) {
+    return measures
+            .stream()
+            .collect(Collectors.toMap(Function.identity(), m -> compileMeasure(m, topMeasures)));
   }
 
   protected CompiledMeasure compileMeasure(Measure measure, boolean topMeasure) {
@@ -249,6 +243,8 @@ public class QueryResolver {
         compiledMeasure = compileBinaryOperationMeasure((BinaryOperationMeasure) m, topMeasure);
       } else if (m instanceof ComparisonMeasureReferencePosition) {
         compiledMeasure = compileComparisonMeasure((ComparisonMeasureReferencePosition) m, topMeasure);
+      } else if (m instanceof VectorAggMeasure v) {
+        compiledMeasure = compileVectorAggMeasure(v);
       } else {
         throw new IllegalArgumentException("Unknown type of measure " + m.getClass().getSimpleName());
       }
@@ -266,24 +262,35 @@ public class QueryResolver {
     if (m.equals(CountMeasure.INSTANCE)) {
       return COMPILED_COUNT;
     }
-    return new CompiledAggregatedMeasure(m, resolveWithFallback(m.field), compileCriteria(m.criteria));
+    return new CompiledAggregatedMeasure(m.alias, resolveWithFallback(m.field), m.aggregationFunction, compileCriteria(m.criteria), m.distinct);
   }
 
   private CompiledMeasure compileExpressionMeasure(ExpressionMeasure m) {
-    return new CompiledExpressionMeasure(m);
+    return new CompiledExpressionMeasure(m.alias, m.expression);
   }
 
   private CompiledMeasure compileConstantMeasure(ConstantMeasure<?> m) {
-    return new CompiledConstantMeasure(m);
+    if (m instanceof DoubleConstantMeasure dc) {
+      return new CompiledDoubleConstantMeasure(dc.value);
+    } else if (m instanceof LongConstantMeasure lc) {
+      return new CompiledLongConstantMeasure(lc.value);
+    } else {
+      throw new IllegalArgumentException("unexpected type " + m.getClass());
+    }
   }
 
   private CompiledMeasure compileBinaryOperationMeasure(BinaryOperationMeasure m, boolean topMeasure) {
-    return new CompiledBinaryOperationMeasure(m, compileMeasure(m.leftOperand, topMeasure), compileMeasure(m.rightOperand, topMeasure));
+    return new CompiledBinaryOperationMeasure(m.alias, m.operator, compileMeasure(m.leftOperand, topMeasure), compileMeasure(m.rightOperand, topMeasure));
   }
 
   private CompiledMeasure compileComparisonMeasure(ComparisonMeasureReferencePosition m, boolean topMeasure) {
-    return new CompiledComparisonMeasure(m, compileMeasure(m.measure, topMeasure),
+    return new CompiledComparisonMeasure(
+            m.alias,
+            m.comparisonMethod,
+            compileMeasure(m.measure, topMeasure),
+            m.referencePosition == null ? null : m.referencePosition.entrySet().stream().collect(Collectors.toMap(e -> resolveField(e.getKey()), Map.Entry::getValue)),
             compilePeriod(m.period),
+            m.columnSetKey,
             m.ancestors == null ? null : m.ancestors.stream().map(this::resolveField).collect(Collectors.toList()));
   }
 
@@ -304,8 +311,24 @@ public class QueryResolver {
     }
   }
 
-  List<CompiledMeasure> getMeasures() {
-    return this.measures;
+  private CompiledMeasure compileVectorAggMeasure(VectorAggMeasure m) {
+    return new CompiledVectorAggMeasure(m.alias, resolveField(m.fieldToAggregate), m.aggregationFunction, resolveField(m.vectorAxis));
+  }
+
+  private Map<ColumnSetKey, CompiledColumnSet> compiledColumnSets(Map<ColumnSetKey, ColumnSet> columnSets) {
+    Map<ColumnSetKey, CompiledColumnSet> m = new HashMap<>();
+    for (Map.Entry<ColumnSetKey, ColumnSet> entry : columnSets.entrySet()) {
+      if (entry.getKey() != BUCKET) {
+        throw new IllegalArgumentException("unexpected column set " + entry.getValue());
+      }
+      BucketColumnSetDto bucket = (BucketColumnSetDto) entry.getValue();
+      m.put(entry.getKey(), new CompiledBucketColumnSet(
+              bucket.getColumnsForPrefetching().stream().map(this::resolveField).toList(),
+              bucket.getNewColumns().stream().map(this::resolveField).toList(),
+              entry.getKey(),
+              bucket.values));
+    }
+    return m;
   }
 
   @Value
diff --git a/core/src/main/java/io/squashql/query/TotalCountMeasure.java b/core/src/main/java/io/squashql/query/TotalCountMeasure.java
index c08c5eb..2f62cbc 100644
--- a/core/src/main/java/io/squashql/query/TotalCountMeasure.java
+++ b/core/src/main/java/io/squashql/query/TotalCountMeasure.java
@@ -9,5 +9,4 @@ public class TotalCountMeasure extends ExpressionMeasure {
   private TotalCountMeasure() {
     super(ALIAS, EXPRESSION);
   }
-
 }
diff --git a/core/src/main/java/io/squashql/query/VectorAggMeasure.java b/core/src/main/java/io/squashql/query/VectorAggMeasure.java
new file mode 100644
index 0000000..0494bf2
--- /dev/null
+++ b/core/src/main/java/io/squashql/query/VectorAggMeasure.java
@@ -0,0 +1,34 @@
+package io.squashql.query;
+
+import lombok.*;
+
+@ToString
+@EqualsAndHashCode
+@NoArgsConstructor // For Jackson
+@AllArgsConstructor
+public class VectorAggMeasure implements Measure {
+
+  public String alias;
+  public Field fieldToAggregate;
+  public String aggregationFunction;
+  public Field vectorAxis;
+  @With
+  public String expression;
+
+  public VectorAggMeasure(String alias, Field fieldToAggregate, String aggregationFunction, Field vectorAxis) {
+    this.alias = alias;
+    this.fieldToAggregate = fieldToAggregate;
+    this.aggregationFunction = aggregationFunction;
+    this.vectorAxis = vectorAxis;
+  }
+
+  @Override
+  public String alias() {
+    return this.alias;
+  }
+
+  @Override
+  public String expression() {
+    return this.expression;
+  }
+}
diff --git a/core/src/main/java/io/squashql/query/agg/AggregationFunction.java b/core/src/main/java/io/squashql/query/agg/AggregationFunction.java
index 503937b..9fb526a 100644
--- a/core/src/main/java/io/squashql/query/agg/AggregationFunction.java
+++ b/core/src/main/java/io/squashql/query/agg/AggregationFunction.java
@@ -7,6 +7,8 @@ public final class AggregationFunction {
   public static final String MAX = "max";
   public static final String AVG = "avg";
   public static final String COUNT = "count";
+  public static final String GROUPING = "grouping";
+  public static final String ARRAY_AGG = "array_agg";
 
   AggregationFunction() {
   }
diff --git a/core/src/main/java/io/squashql/query/compiled/CompiledAggregatedMeasure.java b/core/src/main/java/io/squashql/query/compiled/CompiledAggregatedMeasure.java
index ad4b156..99edd2c 100644
--- a/core/src/main/java/io/squashql/query/compiled/CompiledAggregatedMeasure.java
+++ b/core/src/main/java/io/squashql/query/compiled/CompiledAggregatedMeasure.java
@@ -1,17 +1,20 @@
 package io.squashql.query.compiled;
 
-import io.squashql.query.AggregatedMeasure;
 import io.squashql.query.CountMeasure;
+import io.squashql.query.agg.AggregationFunction;
 import io.squashql.query.database.QueryRewriter;
 import io.squashql.query.database.SqlUtils;
 import io.squashql.type.TableTypedField;
 import io.squashql.type.TypedField;
 
-public record CompiledAggregatedMeasure(AggregatedMeasure measure, TypedField field,
-                                        CompiledCriteria criteria) implements CompiledMeasure {
+public record CompiledAggregatedMeasure(String alias,
+                                        TypedField field,
+                                        String aggregationFunction,
+                                        CompiledCriteria criteria,
+                                        boolean distinct) implements CompiledMeasure {
 
   public static final CompiledMeasure COMPILED_COUNT = new CompiledAggregatedMeasure(
-          CountMeasure.INSTANCE, new TableTypedField(null, CountMeasure.FIELD_NAME, long.class), null);
+          CountMeasure.INSTANCE.alias, new TableTypedField(null, CountMeasure.FIELD_NAME, long.class), AggregationFunction.COUNT, null, false);
 
   @Override
   public String sqlExpression(QueryRewriter queryRewriter, boolean withAlias) {
@@ -22,21 +25,20 @@ public record CompiledAggregatedMeasure(AggregatedMeasure measure, TypedField fi
     } else {
       valuesToAggregate = fieldExpression;
     }
-    if (measure.distinct) {
+    if (this.distinct) {
       valuesToAggregate = "distinct(" + valuesToAggregate + ")";
     }
-    final String sql = this.measure.aggregationFunction + "(" + valuesToAggregate + ")";
+    final String sql = this.aggregationFunction + "(" + valuesToAggregate + ")";
     return withAlias ? SqlUtils.appendAlias(sql, queryRewriter, alias()) : sql;
   }
 
   @Override
   public String alias() {
-    return measure().alias();
+    return this.alias;
   }
 
   @Override
   public <R> R accept(MeasureVisitor<R> visitor) {
     return visitor.visit(this);
   }
-
 }
diff --git a/core/src/main/java/io/squashql/query/compiled/CompiledBinaryOperationMeasure.java b/core/src/main/java/io/squashql/query/compiled/CompiledBinaryOperationMeasure.java
index 85e8cb1..6594c39 100644
--- a/core/src/main/java/io/squashql/query/compiled/CompiledBinaryOperationMeasure.java
+++ b/core/src/main/java/io/squashql/query/compiled/CompiledBinaryOperationMeasure.java
@@ -1,10 +1,11 @@
 package io.squashql.query.compiled;
 
-import io.squashql.query.BinaryOperationMeasure;
+import io.squashql.query.BinaryOperator;
 import io.squashql.query.database.QueryRewriter;
 import io.squashql.query.database.SqlUtils;
 
-public record CompiledBinaryOperationMeasure(BinaryOperationMeasure measure,
+public record CompiledBinaryOperationMeasure(String alias,
+                                             BinaryOperator operator,
                                              CompiledMeasure leftOperand,
                                              CompiledMeasure rightOperand) implements CompiledMeasure {
   @Override
@@ -12,16 +13,16 @@ public record CompiledBinaryOperationMeasure(BinaryOperationMeasure measure,
     String sql = new StringBuilder()
             .append("(")
             .append(this.leftOperand.sqlExpression(queryRewriter, false))
-            .append(this.measure.operator.infix)
+            .append(this.operator.infix)
             .append(this.rightOperand.sqlExpression(queryRewriter, false))
             .append(")")
             .toString();
-    return withAlias ? SqlUtils.appendAlias(sql, queryRewriter, this.measure.alias) : sql;
+    return withAlias ? SqlUtils.appendAlias(sql, queryRewriter, this.alias) : sql;
   }
 
   @Override
   public String alias() {
-    return this.measure.alias();
+    return this.alias;
   }
 
   @Override
diff --git a/core/src/main/java/io/squashql/query/compiled/CompiledBucketColumnSet.java b/core/src/main/java/io/squashql/query/compiled/CompiledBucketColumnSet.java
new file mode 100644
index 0000000..e56808b
--- /dev/null
+++ b/core/src/main/java/io/squashql/query/compiled/CompiledBucketColumnSet.java
@@ -0,0 +1,21 @@
+package io.squashql.query.compiled;
+
+import io.squashql.query.ColumnSetKey;
+import io.squashql.type.TypedField;
+
+import java.util.List;
+import java.util.Map;
+
+public record CompiledBucketColumnSet(List<TypedField> columnsForPrefetching,
+                                      List<TypedField> newColumns,
+                                      ColumnSetKey columnSetKey,
+                                      Map<String, List<String>> values) implements CompiledColumnSet {
+
+  public TypedField newField() {
+    return this.newColumns.get(0);
+  }
+
+  public TypedField field() {
+    return this.columnsForPrefetching.get(0);
+  }
+}
diff --git a/core/src/main/java/io/squashql/query/compiled/CompiledColumnSet.java b/core/src/main/java/io/squashql/query/compiled/CompiledColumnSet.java
new file mode 100644
index 0000000..e0dc939
--- /dev/null
+++ b/core/src/main/java/io/squashql/query/compiled/CompiledColumnSet.java
@@ -0,0 +1,15 @@
+package io.squashql.query.compiled;
+
+import io.squashql.query.ColumnSetKey;
+import io.squashql.type.TypedField;
+
+import java.util.List;
+
+public interface CompiledColumnSet {
+
+  List<TypedField> columnsForPrefetching();
+
+  List<TypedField> newColumns();
+
+  ColumnSetKey columnSetKey();
+}
diff --git a/core/src/main/java/io/squashql/query/compiled/CompiledComparisonMeasure.java b/core/src/main/java/io/squashql/query/compiled/CompiledComparisonMeasure.java
index 2002f67..58af178 100644
--- a/core/src/main/java/io/squashql/query/compiled/CompiledComparisonMeasure.java
+++ b/core/src/main/java/io/squashql/query/compiled/CompiledComparisonMeasure.java
@@ -1,23 +1,30 @@
 package io.squashql.query.compiled;
 
-import io.squashql.query.ComparisonMeasureReferencePosition;
+import io.squashql.query.ColumnSetKey;
+import io.squashql.query.ComparisonMethod;
 import io.squashql.query.database.QueryRewriter;
 import io.squashql.type.TypedField;
 
 import java.util.List;
 import java.util.Map;
 
-public record CompiledComparisonMeasure(ComparisonMeasureReferencePosition measure, CompiledMeasure reference, CompiledPeriod period, List<TypedField> ancestors) implements CompiledMeasure {
+public record CompiledComparisonMeasure(String alias,
+                                        ComparisonMethod comparisonMethod,
+                                        CompiledMeasure measure,
+                                        Map<TypedField, String> referencePosition,
+                                        CompiledPeriod period,
+                                        ColumnSetKey columnSetKey,
+                                        List<TypedField> ancestors) implements CompiledMeasure {
 
 
   @Override
   public String sqlExpression(QueryRewriter queryRewriter, boolean withAlias) {
-    return null;
+    throw new IllegalStateException("incorrect path of execution");
   }
 
   @Override
   public String alias() {
-    return this.measure.alias;
+    return this.alias;
   }
 
   @Override
diff --git a/core/src/main/java/io/squashql/query/compiled/CompiledCriteria.java b/core/src/main/java/io/squashql/query/compiled/CompiledCriteria.java
index 5393b39..75ed320 100644
--- a/core/src/main/java/io/squashql/query/compiled/CompiledCriteria.java
+++ b/core/src/main/java/io/squashql/query/compiled/CompiledCriteria.java
@@ -13,7 +13,8 @@ import java.util.List;
 import java.util.function.Function;
 import java.util.stream.Collectors;
 
-public record CompiledCriteria(ConditionDto condition, ConditionType conditionType, TypedField field, TypedField fieldOther, CompiledMeasure measure, List<CompiledCriteria> children){
+public record CompiledCriteria(ConditionDto condition, ConditionType conditionType, TypedField field,
+                               TypedField fieldOther, CompiledMeasure measure, List<CompiledCriteria> children) {
 
   public String sqlExpression(QueryRewriter queryRewriter) {
     if (this.field != null && condition() != null) {
diff --git a/core/src/main/java/io/squashql/query/compiled/CompiledConstantMeasure.java b/core/src/main/java/io/squashql/query/compiled/CompiledDoubleConstantMeasure.java
similarity index 60%
rename from core/src/main/java/io/squashql/query/compiled/CompiledConstantMeasure.java
rename to core/src/main/java/io/squashql/query/compiled/CompiledDoubleConstantMeasure.java
index 5eb6ae9..ab0ccd8 100644
--- a/core/src/main/java/io/squashql/query/compiled/CompiledConstantMeasure.java
+++ b/core/src/main/java/io/squashql/query/compiled/CompiledDoubleConstantMeasure.java
@@ -1,23 +1,21 @@
 package io.squashql.query.compiled;
 
-import io.squashql.query.ConstantMeasure;
 import io.squashql.query.database.QueryRewriter;
 
-public record CompiledConstantMeasure(ConstantMeasure<?> measure) implements CompiledMeasure {
+public record CompiledDoubleConstantMeasure(Double value) implements CompiledMeasure {
 
   @Override
   public String sqlExpression(QueryRewriter queryRewriter, boolean withAlias) {
-    return this.measure.getValue().toString();
+    return String.valueOf(this.value);
   }
 
   @Override
   public String alias() {
-    return this.measure.alias();
+    return "constant(" + this.value + ")";
   }
 
   @Override
   public <R> R accept(MeasureVisitor<R> visitor) {
     return visitor.visit(this);
   }
-
 }
diff --git a/core/src/main/java/io/squashql/query/compiled/CompiledExpressionMeasure.java b/core/src/main/java/io/squashql/query/compiled/CompiledExpressionMeasure.java
index bec3273..5d43c53 100644
--- a/core/src/main/java/io/squashql/query/compiled/CompiledExpressionMeasure.java
+++ b/core/src/main/java/io/squashql/query/compiled/CompiledExpressionMeasure.java
@@ -1,19 +1,25 @@
 package io.squashql.query.compiled;
 
-import io.squashql.query.Measure;
+import io.squashql.query.TotalCountMeasure;
 import io.squashql.query.database.QueryRewriter;
 import io.squashql.query.database.SqlUtils;
 
-public record CompiledExpressionMeasure(Measure measure) implements CompiledMeasure {
+public record CompiledExpressionMeasure(String alias, String expression) implements CompiledMeasure {
+
+  /**
+   * Represents a compiled total count measure.
+   */
+  public static final CompiledMeasure COMPILED_TOTAL_COUNT = new CompiledExpressionMeasure(TotalCountMeasure.ALIAS, TotalCountMeasure.EXPRESSION);
+
 
   @Override
   public String sqlExpression(QueryRewriter queryRewriter, boolean withAlias) {
-    return withAlias ? SqlUtils.appendAlias(measure().expression(), queryRewriter, alias()) : measure.expression();
+    return withAlias ? SqlUtils.appendAlias(this.expression, queryRewriter, alias()) : this.expression;
   }
 
   @Override
   public String alias() {
-    return this.measure.alias();
+    return this.alias;
   }
 
   @Override
diff --git a/core/src/main/java/io/squashql/query/compiled/CompiledLongConstantMeasure.java b/core/src/main/java/io/squashql/query/compiled/CompiledLongConstantMeasure.java
new file mode 100644
index 0000000..25000eb
--- /dev/null
+++ b/core/src/main/java/io/squashql/query/compiled/CompiledLongConstantMeasure.java
@@ -0,0 +1,21 @@
+package io.squashql.query.compiled;
+
+import io.squashql.query.database.QueryRewriter;
+
+public record CompiledLongConstantMeasure(Long value) implements CompiledMeasure {
+
+  @Override
+  public String sqlExpression(QueryRewriter queryRewriter, boolean withAlias) {
+    return String.valueOf(this.value);
+  }
+
+  @Override
+  public String alias() {
+    return "constant(" + this.value + ")";
+  }
+
+  @Override
+  public <R> R accept(MeasureVisitor<R> visitor) {
+    return visitor.visit(this);
+  }
+}
diff --git a/core/src/main/java/io/squashql/query/compiled/CompiledMeasure.java b/core/src/main/java/io/squashql/query/compiled/CompiledMeasure.java
index d125c93..14be931 100644
--- a/core/src/main/java/io/squashql/query/compiled/CompiledMeasure.java
+++ b/core/src/main/java/io/squashql/query/compiled/CompiledMeasure.java
@@ -1,6 +1,5 @@
 package io.squashql.query.compiled;
 
-import io.squashql.query.Measure;
 import io.squashql.query.database.QueryRewriter;
 
 public interface CompiledMeasure {
@@ -9,8 +8,5 @@ public interface CompiledMeasure {
 
   String alias();
 
-  Measure measure();
-
   <R> R accept(MeasureVisitor<R> visitor);
-
 }
diff --git a/core/src/main/java/io/squashql/query/compiled/CompiledVectorAggMeasure.java b/core/src/main/java/io/squashql/query/compiled/CompiledVectorAggMeasure.java
new file mode 100644
index 0000000..4d888e8
--- /dev/null
+++ b/core/src/main/java/io/squashql/query/compiled/CompiledVectorAggMeasure.java
@@ -0,0 +1,25 @@
+package io.squashql.query.compiled;
+
+import io.squashql.query.database.QueryRewriter;
+import io.squashql.type.TypedField;
+
+public record CompiledVectorAggMeasure(String alias,
+                                       TypedField fieldToAggregate,
+                                       String aggregationFunction,
+                                       TypedField vectorAxis) implements CompiledMeasure {
+
+  @Override
+  public String sqlExpression(QueryRewriter queryRewriter, boolean withAlias) {
+    throw new IllegalStateException("Incorrect path of execution");
+  }
+
+  @Override
+  public String alias() {
+    return this.alias;
+  }
+
+  @Override
+  public <R> R accept(MeasureVisitor<R> visitor) {
+    return visitor.visit(this);
+  }
+}
diff --git a/core/src/main/java/io/squashql/query/compiled/Evaluator.java b/core/src/main/java/io/squashql/query/compiled/Evaluator.java
index 0ecbbaf..34bc1d5 100644
--- a/core/src/main/java/io/squashql/query/compiled/Evaluator.java
+++ b/core/src/main/java/io/squashql/query/compiled/Evaluator.java
@@ -4,7 +4,6 @@ import io.squashql.query.*;
 import io.squashql.query.QueryExecutor.ExecutionContext;
 import io.squashql.query.QueryExecutor.QueryPlanNodeKey;
 import io.squashql.query.comp.BinaryOperations;
-import io.squashql.query.dto.BucketColumnSetDto;
 import io.squashql.table.Table;
 import io.squashql.type.TypedField;
 
@@ -23,7 +22,7 @@ public class Evaluator implements BiConsumer<QueryPlanNodeKey, ExecutionContext>
   @Override
   public void accept(QueryPlanNodeKey queryPlanNodeKey, ExecutionContext executionContext) {
     CompiledMeasure measure = queryPlanNodeKey.measure();
-    if (executionContext.getWriteToTable().measures().contains(measure.measure())) {
+    if (executionContext.getWriteToTable().measures().contains(measure)) {
       return; // Nothing to do
     }
     this.executionContext = executionContext;
@@ -33,30 +32,30 @@ public class Evaluator implements BiConsumer<QueryPlanNodeKey, ExecutionContext>
   @Override
   public Void visit(CompiledBinaryOperationMeasure bom) {
     Table intermediateResult = this.executionContext.getWriteToTable();
-    List<Object> lo = intermediateResult.getAggregateValues(bom.leftOperand().measure());
-    List<Object> ro = intermediateResult.getAggregateValues(bom.rightOperand().measure());
+    List<Object> lo = intermediateResult.getAggregateValues(bom.leftOperand());
+    List<Object> ro = intermediateResult.getAggregateValues(bom.rightOperand());
     List<Object> r = new ArrayList<>(lo.size());
 
-    Class<?> lType = intermediateResult.getHeader(bom.leftOperand().measure()).type();
-    Class<?> rType = intermediateResult.getHeader(bom.rightOperand().measure()).type();
-    BiFunction<Number, Number, Number> operation = BinaryOperations.createBiFunction(bom.measure().operator, lType, rType);
+    Class<?> lType = intermediateResult.getHeader(bom.leftOperand()).type();
+    Class<?> rType = intermediateResult.getHeader(bom.rightOperand()).type();
+    BiFunction<Number, Number, Number> operation = BinaryOperations.createBiFunction(bom.operator(), lType, rType);
     for (int i = 0; i < lo.size(); i++) {
       r.add(operation.apply((Number) lo.get(i), (Number) ro.get(i)));
     }
-    Header header = new Header(bom.alias(), BinaryOperations.getOutputType(bom.measure().operator, lType, rType), true);
-    intermediateResult.addAggregates(header, bom.measure(), r);
+    Header header = new Header(bom.alias(), BinaryOperations.getOutputType(bom.operator(), lType, rType), true);
+    intermediateResult.addAggregates(header, bom, r);
     return null;
   }
 
   @Override
   public Void visit(CompiledComparisonMeasure cm) {
     AComparisonExecutor executor;
-    if (cm.measure().columnSetKey == BUCKET) {
-      ColumnSet cs = this.executionContext.columnSets().get(BUCKET);
+    if (cm.columnSetKey() == BUCKET) {
+      CompiledColumnSet cs = this.executionContext.columnSets().get(BUCKET);
       if (cs == null) {
         throw new IllegalArgumentException(String.format("columnSet %s is not specified in the query but is used in a comparison measure: %s", BUCKET, cm));
       }
-      executor = new BucketComparisonExecutor((BucketColumnSetDto) cs);
+      executor = new BucketComparisonExecutor((CompiledBucketColumnSet) cs);
     } else if (cm.period() != null) {
       for (TypedField field : cm.period().getTypedFields()) {
         if (!this.executionContext.columns().contains(field)) {
@@ -82,17 +81,23 @@ public class Evaluator implements BiConsumer<QueryPlanNodeKey, ExecutionContext>
 
   private static void executeComparator(CompiledComparisonMeasure cm, Table writeToTable, Table readFromTable, AComparisonExecutor executor) {
     List<Object> agg = executor.compare(cm, writeToTable, readFromTable);
-    Header header = new Header(cm.alias(), BinaryOperations.getComparisonOutputType(cm.measure().comparisonMethod, writeToTable.getHeader(cm.measure().measure).type()), true);
-    writeToTable.addAggregates(header, cm.measure(), agg);
+    Header header = new Header(cm.alias(), BinaryOperations.getComparisonOutputType(cm.comparisonMethod(), writeToTable.getHeader(cm.measure()).type()), true);
+    writeToTable.addAggregates(header, cm, agg);
   }
 
   @Override
-  public Void visit(CompiledConstantMeasure measure) {
-    executeConstantOperation(measure.measure(), this.executionContext.getWriteToTable());
+  public Void visit(CompiledDoubleConstantMeasure measure) {
+    executeConstantOperation(measure, this.executionContext.getWriteToTable());
     return null;
   }
 
-  private static void executeConstantOperation(ConstantMeasure<?> cm, Table intermediateResult) {
+  @Override
+  public Void visit(CompiledLongConstantMeasure measure) {
+    executeConstantOperation(measure, this.executionContext.getWriteToTable());
+    return null;
+  }
+
+  private static void executeConstantOperation(CompiledMeasure cm, Table intermediateResult) {
     Object v;
     Class<?> type;
     if (cm instanceof DoubleConstantMeasure dcm) {
@@ -102,7 +107,7 @@ public class Evaluator implements BiConsumer<QueryPlanNodeKey, ExecutionContext>
       v = ((Number) lcm.value).longValue();
       type = long.class;
     } else {
-      throw new IllegalArgumentException("Unexpected type " + cm.getValue().getClass() + ". Only double and long are supported");
+      throw new IllegalArgumentException("Unexpected type " + cm.getClass() + ". Only double and long are supported");
     }
     Header header = new Header(cm.alias(), type, true);
     List<Object> r = Collections.nCopies((int) intermediateResult.count(), v);
@@ -120,4 +125,18 @@ public class Evaluator implements BiConsumer<QueryPlanNodeKey, ExecutionContext>
   public Void visit(CompiledExpressionMeasure measure) {
     throw new IllegalStateException(CompiledExpressionMeasure.class.getSimpleName());
   }
+
+  @Override
+  public Void visit(CompiledVectorAggMeasure measure) {
+    // Retrieve the query scope use for the prefetch, the logic should be the same to retrieve the result.
+    QueryExecutor.QueryScope prefetchQueryScope = new PrefetchVisitor(this.executionContext.columns(), this.executionContext.bucketColumns(), this.executionContext.queryScope())
+            .visit(measure)
+            .keySet()
+            .iterator()
+            .next();
+    Table readTable = this.executionContext.tableByScope().get(prefetchQueryScope);
+    Table writeToTable = this.executionContext.getWriteToTable();
+    writeToTable.transferAggregates(readTable, measure);
+    return null;
+  }
 }
diff --git a/core/src/main/java/io/squashql/query/compiled/MeasureVisitor.java b/core/src/main/java/io/squashql/query/compiled/MeasureVisitor.java
index 4fbb3b0..107f435 100644
--- a/core/src/main/java/io/squashql/query/compiled/MeasureVisitor.java
+++ b/core/src/main/java/io/squashql/query/compiled/MeasureVisitor.java
@@ -10,6 +10,9 @@ public interface MeasureVisitor<R> {
 
   R visit(CompiledComparisonMeasure measure);
 
-  R visit(CompiledConstantMeasure measure);
+  R visit(CompiledDoubleConstantMeasure measure);
 
+  R visit(CompiledLongConstantMeasure measure);
+
+  R visit(CompiledVectorAggMeasure measure);
 }
diff --git a/core/src/main/java/io/squashql/query/compiled/PrefetchVisitor.java b/core/src/main/java/io/squashql/query/compiled/PrefetchVisitor.java
index 0fc30c1..f42ccaf 100644
--- a/core/src/main/java/io/squashql/query/compiled/PrefetchVisitor.java
+++ b/core/src/main/java/io/squashql/query/compiled/PrefetchVisitor.java
@@ -2,12 +2,17 @@ package io.squashql.query.compiled;
 
 import io.squashql.query.MeasureUtils;
 import io.squashql.query.QueryExecutor.QueryScope;
+import io.squashql.query.database.DatabaseQuery;
+import io.squashql.query.database.SqlUtils;
+import io.squashql.type.AliasedTypedField;
 import io.squashql.type.TypedField;
 import lombok.RequiredArgsConstructor;
 import org.eclipse.collections.impl.set.mutable.MutableSetFactoryImpl;
 
 import java.util.*;
 
+import static io.squashql.query.agg.AggregationFunction.*;
+
 @RequiredArgsConstructor
 public class PrefetchVisitor implements MeasureVisitor<Map<QueryScope, Set<CompiledMeasure>>> {
 
@@ -40,15 +45,131 @@ public class PrefetchVisitor implements MeasureVisitor<Map<QueryScope, Set<Compi
 
   @Override
   public Map<QueryScope, Set<CompiledMeasure>> visit(CompiledComparisonMeasure cmrp) {
-    QueryScope readScope = MeasureUtils.getReadScopeComparisonMeasureReferencePosition(columns, bucketColumns, cmrp, this.originalQueryScope);
-    Map<QueryScope, Set<CompiledMeasure>> result = new HashMap<>(Map.of(this.originalQueryScope, Set.of(cmrp.reference())));
-    result.put(readScope, Set.of(cmrp.reference()));
+    QueryScope readScope = MeasureUtils.getReadScopeComparisonMeasureReferencePosition(this.columns, this.bucketColumns, cmrp, this.originalQueryScope);
+    Map<QueryScope, Set<CompiledMeasure>> result = new HashMap<>(Map.of(this.originalQueryScope, Set.of(cmrp.measure())));
+    result.put(readScope, Set.of(cmrp.measure()));
     return result;
   }
 
   @Override
-  public Map<QueryScope, Set<CompiledMeasure>> visit(CompiledConstantMeasure measure) {
+  public Map<QueryScope, Set<CompiledMeasure>> visit(CompiledDoubleConstantMeasure measure) {
+    return empty();
+  }
+
+  @Override
+  public Map<QueryScope, Set<CompiledMeasure>> visit(CompiledLongConstantMeasure measure) {
     return empty();
   }
 
+  @Override
+  public Map<QueryScope, Set<CompiledMeasure>> visit(CompiledVectorAggMeasure vectorAggMeasure) {
+    /*
+     * The trick here is to generate a query with a subquery that first compute the aggregate of measure.fieldToAggregate
+     * with the aggregation function measure.aggregationFunction + the correct rollup and then the top query computes
+     * the final aggregates to create the vector. To do that, we use aliases and grouping functions.
+     * Let's say the original query looks like this: select a,b,vector(vectorAlias,c,sum,date) from table group by rollup(a,b).
+     * The following db query is generated:
+     *
+     * subquery = select
+     *                   ticker AS ___alias___ticker___,
+     *                   date AS ___alias___date___,
+     *                   riskType AS ___alias___riskType___,
+     *                   grouping(ticker) as ___alias___grouping_ticker___,
+     *                   grouping(riskType) as ___alias___grouping_riskType___,
+     *                   sum(value) as value_sum
+     *                   from MYTABLE
+     *                   group by ___alias___date___, rollup(___alias___ticker___, ___alias___riskType___)
+     * (we do not care about totals on date)
+     *
+     * query = select
+     *          			___alias___ticker___,
+     *          			___alias___riskType___,
+     *          			array_agg(value_sum) as vector,
+     *          			max(___alias___grouping_riskType___) as ___grouping______alias___riskType______, // To make __total__ appear
+     *          			max(___alias___grouping_ticker___) as ___grouping______alias___ticker______ // To make __total__ appear
+     *          			from (subquery)
+     *          			group by ___alias___ticker___, ___alias___riskType___
+     *
+     * If there is no rollup, it is much simpler:
+     *
+     * subquery = select
+     *                  ticker AS ___alias___ticker___,
+     *                  date AS ___alias___date___,
+     *                  riskType AS ___alias___riskType___,
+     *                  sum(value) as value_sum
+     *                  from MYTABLE
+     *                  group by ___alias___ticker___, ___alias___date___, ___alias___riskType___
+     *
+     * query = select
+		 *               ___alias___ticker___,
+		 *               ___alias___riskType___,
+		 *               array_agg(value_sum) as vector,
+		 *               from (subquery)
+		 *               group by ___alias___ticker___, ___alias___riskType___
+     */
+    TypedField vectorAxis = vectorAggMeasure.vectorAxis();
+    if (this.originalQueryScope.columns().contains(vectorAggMeasure.vectorAxis())) {
+      var m = new CompiledAggregatedMeasure(vectorAggMeasure.alias(), vectorAggMeasure.fieldToAggregate(), vectorAggMeasure.aggregationFunction(), null, false);
+      return Map.of(this.originalQueryScope, Set.of(m));
+    } else {
+      boolean hasRollup = !this.originalQueryScope.rollupColumns().isEmpty();
+
+      List<CompiledMeasure> subQueryMeasures = new ArrayList<>();
+      List<TypedField> topQuerySelectColumns = new ArrayList<>();
+      List<TypedField> subQuerySelectColumns = new ArrayList<>();
+      Set<CompiledMeasure> topQueryMeasures = new HashSet<>();
+      for (TypedField selectColumn : this.originalQueryScope.columns()) {
+        // Here we can choose any alias but for debugging purpose, we create one from the expression.
+        String expression = SqlUtils.squashqlExpression(selectColumn);
+        String alias = SqlUtils.columnAlias(expression).replace(".", "_");
+        subQuerySelectColumns.add(selectColumn.as(alias));
+        topQuerySelectColumns.add(new AliasedTypedField(alias));
+        if (hasRollup) {
+          String groupingAlias = SqlUtils.columnAlias("grouping_" + expression);
+          // groupingAlias should not contain '.' !! this is a defect that will be fixed in the future
+          groupingAlias = groupingAlias.replace(".", "_");
+          subQueryMeasures.add(new CompiledAggregatedMeasure(groupingAlias, selectColumn, GROUPING, null, false));
+          topQueryMeasures.add(new CompiledAggregatedMeasure(SqlUtils.groupingAlias(alias), new AliasedTypedField(groupingAlias), MAX, null, false));
+        }
+      }
+      String vectorAxisAlias = SqlUtils.columnAlias(SqlUtils.squashqlExpression(vectorAxis)).replace(".", "_");
+      subQuerySelectColumns.add(vectorAxis.as(vectorAxisAlias));
+
+      String subQueryMeasureAlias = (vectorAggMeasure.fieldToAggregate().name() + "_" + vectorAggMeasure.aggregationFunction()).replace(".", "_");
+      subQueryMeasures.add(new CompiledAggregatedMeasure(subQueryMeasureAlias, vectorAggMeasure.fieldToAggregate(), vectorAggMeasure.aggregationFunction(), null, false));
+
+      List<TypedField> subQueryRollupColumns = new ArrayList<>();
+      for (TypedField r : this.originalQueryScope.rollupColumns()) {
+        // Here we can choose any alias but for debugging purpose, we create one from the expression.
+        String expression = SqlUtils.squashqlExpression(r);
+        String alias = SqlUtils.columnAlias(expression).replace(".", "_");
+        subQueryRollupColumns.add(r.as(alias));
+      }
+
+      DatabaseQuery subQuery = new DatabaseQuery(this.originalQueryScope.virtualTable(),
+              this.originalQueryScope.table(),
+              this.originalQueryScope.subQuery(),
+              new HashSet<>(subQuerySelectColumns),
+              this.originalQueryScope.whereCriteria(),
+              this.originalQueryScope.havingCriteria(),
+              subQueryRollupColumns,
+              this.originalQueryScope.groupingSets(),
+              -1);
+      subQueryMeasures.forEach(subQuery::withMeasure);
+
+      QueryScope topQueryScope = new QueryScope(
+              this.originalQueryScope.table(),
+              subQuery,
+              topQuerySelectColumns,
+              this.originalQueryScope.whereCriteria(),
+              this.originalQueryScope.havingCriteria(),
+              Collections.emptyList(), // remove rollup, it has been computed in the subquery
+              Collections.emptyList(),
+              this.originalQueryScope.virtualTable(),
+              this.originalQueryScope.limit());
+
+      topQueryMeasures.add(new CompiledAggregatedMeasure(vectorAggMeasure.alias(), new AliasedTypedField(subQueryMeasureAlias), ARRAY_AGG, null, false));
+      return Map.of(topQueryScope, topQueryMeasures);
+    }
+  }
 }
diff --git a/core/src/main/java/io/squashql/query/compiled/PrimitiveMeasureVisitor.java b/core/src/main/java/io/squashql/query/compiled/PrimitiveMeasureVisitor.java
index cefe81e..33ce12f 100644
--- a/core/src/main/java/io/squashql/query/compiled/PrimitiveMeasureVisitor.java
+++ b/core/src/main/java/io/squashql/query/compiled/PrimitiveMeasureVisitor.java
@@ -18,7 +18,12 @@ public class PrimitiveMeasureVisitor implements MeasureVisitor<Boolean> {
   }
 
   @Override
-  public Boolean visit(CompiledConstantMeasure measure) {
+  public Boolean visit(CompiledDoubleConstantMeasure measure) {
+    return true;
+  }
+
+  @Override
+  public Boolean visit(CompiledLongConstantMeasure measure) {
     return true;
   }
 
@@ -26,4 +31,10 @@ public class PrimitiveMeasureVisitor implements MeasureVisitor<Boolean> {
   public Boolean visit(CompiledComparisonMeasure measure) {
     return false;
   }
+
+  @Override
+  public Boolean visit(CompiledVectorAggMeasure measure) {
+    // This measure is "indirectly" computed by the underlying DB but it is not a primitive. See PrefetchVisitor.
+    return false;
+  }
 }
diff --git a/core/src/main/java/io/squashql/query/database/AQueryEngine.java b/core/src/main/java/io/squashql/query/database/AQueryEngine.java
index 46d589a..b7438c3 100644
--- a/core/src/main/java/io/squashql/query/database/AQueryEngine.java
+++ b/core/src/main/java/io/squashql/query/database/AQueryEngine.java
@@ -3,10 +3,7 @@ package io.squashql.query.database;
 import io.squashql.query.Header;
 import io.squashql.store.Datastore;
 import io.squashql.store.Store;
-import io.squashql.table.ColumnarTable;
 import io.squashql.table.Table;
-import io.squashql.type.TypedField;
-import io.squashql.util.Queries;
 import lombok.extern.slf4j.Slf4j;
 import org.eclipse.collections.api.tuple.Pair;
 import org.eclipse.collections.impl.tuple.Tuples;
@@ -14,7 +11,6 @@ import org.eclipse.collections.impl.tuple.Tuples;
 import java.util.ArrayList;
 import java.util.Iterator;
 import java.util.List;
-import java.util.Objects;
 import java.util.function.BiFunction;
 import java.util.function.Function;
 import java.util.stream.IntStream;
@@ -46,8 +42,7 @@ public abstract class AQueryEngine<T extends Datastore> implements QueryEngine<T
     }
     String sql = createSqlStatement(query);
     log.info(query + " translated into " + System.lineSeparator() + "sql=" + sql);
-    Table aggregates = retrieveAggregates(query, sql);
-    return postProcessDataset(aggregates, query);
+    return retrieveAggregates(query, sql);
   }
 
   protected String createSqlStatement(DatabaseQuery query) {
@@ -56,66 +51,6 @@ public abstract class AQueryEngine<T extends Datastore> implements QueryEngine<T
 
   protected abstract Table retrieveAggregates(DatabaseQuery query, String sql);
 
-  /**
-   * Changes the content of the input table to remove columns corresponding to grouping() (columns that help to identify
-   * rows containing totals) and write {@link SQLTranslator#TOTAL_CELL} in the corresponding cells. The modifications
-   * happen in-place i.e. in the input table columns directly.
-   * <pre>
-   *   Input:
-   *   +----------+----------+---------------------------+---------------------------+------+----------------------+----+
-   *   | scenario | category | ___grouping___scenario___ | ___grouping___category___ |    p | _contributors_count_ |  q |
-   *   +----------+----------+---------------------------+---------------------------+------+----------------------+----+
-   *   |     base |    drink |                         0 |                         0 |  2.0 |                    1 | 10 |
-   *   |     base |     food |                         0 |                         0 |  3.0 |                    1 | 20 |
-   *   |     base |    cloth |                         0 |                         0 | 10.0 |                    1 |  3 |
-   *   |     null |     null |                         1 |                         1 | 15.0 |                    3 | 33 |
-   *   |     base |     null |                         0 |                         1 | 15.0 |                    3 | 33 |
-   *   +----------+----------+---------------------------+---------------------------+------+----------------------+----+
-   *   Output:
-   *   +-------------+-------------+------+----------------------+----+
-   *   |    scenario |    category |    p | _contributors_count_ |  q |
-   *   +-------------+-------------+------+----------------------+----+
-   *   |        base |       drink |  2.0 |                    1 | 10 |
-   *   |        base |        food |  3.0 |                    1 | 20 |
-   *   |        base |       cloth | 10.0 |                    1 |  3 |
-   *   | ___total___ | ___total___ | 15.0 |                    3 | 33 |
-   *   |        base | ___total___ | 15.0 |                    3 | 33 |
-   *   +-------------+-------------+------+----------------------+----+
-   * </pre>
-   */
-  protected Table postProcessDataset(Table input, DatabaseQuery query) {
-    List<TypedField> groupingSelects = Queries.generateGroupingSelect(query);
-    if (!groupingSelects.isEmpty()) {
-      List<Header> newHeaders = new ArrayList<>();
-      List<List<Object>> newValues = new ArrayList<>();
-      for (int i = 0; i < input.headers().size(); i++) {
-        Header header = input.headers().get(i);
-        List<Object> columnValues = input.getColumn(i);
-        if (i < query.select.size() || i >= query.select.size() + groupingSelects.size()) {
-          newHeaders.add(header);
-          newValues.add(columnValues);
-        } else {
-          String baseName = Objects.requireNonNull(SqlUtils.extractFieldFromGroupingAlias(header.name()));
-          List<Object> baseColumnValues = input.getColumnValues(baseName);
-          for (int rowIndex = 0; rowIndex < columnValues.size(); rowIndex++) {
-            if (((Number) columnValues.get(rowIndex)).longValue() == 1) {
-              // It is a total if == 1. It is cast as Number because the type is Byte with Spark, Long with
-              // ClickHouse...
-              baseColumnValues.set(rowIndex, SQLTranslator.TOTAL_CELL);
-            }
-          }
-        }
-      }
-
-      return new ColumnarTable(
-              newHeaders,
-              input.measures(),
-              newValues);
-    } else {
-      return input;
-    }
-  }
-
   public <Column, Record> Pair<List<Header>, List<List<Object>>> transformToColumnFormat(
           DatabaseQuery query,
           List<Column> columns,
@@ -124,15 +59,13 @@ public abstract class AQueryEngine<T extends Datastore> implements QueryEngine<T
           BiFunction<Integer, Record, Object> recordToFieldValue) {
     List<Header> headers = new ArrayList<>();
     List<String> fieldNames = new ArrayList<>(query.select.stream().map(SqlUtils::squashqlExpression).toList());
-    List<TypedField> groupingSelects = Queries.generateGroupingSelect(query);
-    groupingSelects.forEach(r -> fieldNames.add(SqlUtils.groupingAlias(SqlUtils.squashqlExpression(r))));
     query.measures.forEach(m -> fieldNames.add(m.alias()));
     List<List<Object>> values = new ArrayList<>(columns.size());
     for (int i = 0; i < columns.size(); i++) {
       headers.add(new Header(
               fieldNames.get(i),
               columnTypeProvider.apply(columns.get(i), fieldNames.get(i)),
-              i >= query.select.size() + groupingSelects.size()));
+              i >= query.select.size()));
       values.add(new ArrayList<>());
     }
     recordIterator.forEachRemaining(r -> {
@@ -155,5 +88,4 @@ public abstract class AQueryEngine<T extends Datastore> implements QueryEngine<T
             IntStream.range(0, headers.size()).mapToObj(i -> recordToFieldValue.apply(i, r)).toList()));
     return Tuples.pair(headers, rows);
   }
-
 }
diff --git a/core/src/main/java/io/squashql/query/database/QueryRewriter.java b/core/src/main/java/io/squashql/query/database/QueryRewriter.java
index e947dae..45c05b1 100644
--- a/core/src/main/java/io/squashql/query/database/QueryRewriter.java
+++ b/core/src/main/java/io/squashql/query/database/QueryRewriter.java
@@ -82,7 +82,7 @@ public interface QueryRewriter {
     } else if (f instanceof FunctionTypedField ftf) {
       sb.append(functionExpression(ftf));
     } else if (f instanceof AliasedTypedField atf) {
-      sb.append(escapeAlias(atf.alias()));
+      return escapeAlias(atf.alias());
     } else {
       throw new IllegalArgumentException(f.getClass().getName());
     }
diff --git a/core/src/main/java/io/squashql/query/database/SQLTranslator.java b/core/src/main/java/io/squashql/query/database/SQLTranslator.java
index 52c2259..a1df754 100644
--- a/core/src/main/java/io/squashql/query/database/SQLTranslator.java
+++ b/core/src/main/java/io/squashql/query/database/SQLTranslator.java
@@ -5,7 +5,6 @@ import io.squashql.query.compiled.CompiledCriteria;
 import io.squashql.query.dto.VirtualTableDto;
 import io.squashql.store.UnknownType;
 import io.squashql.type.TypedField;
-import io.squashql.util.Queries;
 
 import java.util.*;
 import java.util.function.Function;
@@ -30,9 +29,6 @@ public class SQLTranslator {
     });
     query.measures.forEach(m -> aggregates.add(m.sqlExpression(queryRewriter, true))); // Alias is needed when using sub-queries
 
-    // Use grouping to identify totals
-    Queries.generateGroupingSelect(query).forEach(f -> selects.add(String.format("grouping(%s)", queryRewriter.grouping(f))));
-
     selects.addAll(aggregates);
 
     StringBuilder statement = new StringBuilder();
diff --git a/core/src/main/java/io/squashql/query/database/SqlUtils.java b/core/src/main/java/io/squashql/query/database/SqlUtils.java
index 980ff07..99d0b44 100644
--- a/core/src/main/java/io/squashql/query/database/SqlUtils.java
+++ b/core/src/main/java/io/squashql/query/database/SqlUtils.java
@@ -7,9 +7,11 @@ import io.squashql.type.TypedField;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
+import static io.squashql.query.agg.AggregationFunction.GROUPING;
+
 public class SqlUtils {
 
-  static final Pattern GROUPING_PATTERN = Pattern.compile("___grouping___(.*)___");
+  static final Pattern GROUPING_PATTERN = Pattern.compile(String.format("___%s___(.*)___", GROUPING));
 
   public static String backtickEscape(String column) {
     return "`" + column + "`";
@@ -69,7 +71,11 @@ public class SqlUtils {
    * {@link SqlUtils#GROUPING_PATTERN}.
    */
   public static String groupingAlias(String column) {
-    return String.format("___grouping___%s___", column);
+    return String.format("___%s___%s___", GROUPING, column);
+  }
+
+  public static String columnAlias(String column) {
+    return String.format("___%s___%s___", "alias", column);
   }
 
   public static String escapeSingleQuote(String s, String escapeCharacter) {
diff --git a/core/src/main/java/io/squashql/table/ColumnarTable.java b/core/src/main/java/io/squashql/table/ColumnarTable.java
index d3a40f0..254fc5b 100644
--- a/core/src/main/java/io/squashql/table/ColumnarTable.java
+++ b/core/src/main/java/io/squashql/table/ColumnarTable.java
@@ -2,7 +2,7 @@ package io.squashql.table;
 
 import com.google.common.base.Suppliers;
 import io.squashql.query.Header;
-import io.squashql.query.Measure;
+import io.squashql.query.compiled.CompiledMeasure;
 import io.squashql.query.dictionary.ObjectArrayDictionary;
 
 import java.util.*;
@@ -11,14 +11,14 @@ import java.util.function.Supplier;
 public class ColumnarTable implements Table {
 
   protected final List<Header> headers;
-  protected final Set<Measure> measures;
+  protected final Set<CompiledMeasure> measures;
 
   public final Supplier<ObjectArrayDictionary> pointDictionary;
   protected final List<List<Object>> values;
 
-  public ColumnarTable(List<Header> headers, Set<Measure> measures, List<List<Object>> values) {
+  public ColumnarTable(List<Header> headers, Set<CompiledMeasure> measures, List<List<Object>> values) {
     if (headers.stream().filter(Header::isMeasure)
-            .anyMatch(measureHeader -> !measures.stream().map(Measure::alias).toList()
+            .anyMatch(measureHeader -> !measures.stream().map(CompiledMeasure::alias).toList()
                     .contains(measureHeader.name()))) {
       throw new IllegalArgumentException("Every header measure should have its description in measures.");
     }
@@ -45,13 +45,27 @@ public class ColumnarTable implements Table {
   }
 
   @Override
-  public void addAggregates(Header header, Measure measure, List<Object> values) {
+  public void addAggregates(Header header, CompiledMeasure measure, List<Object> values) {
     this.headers.add(new Header(header.name(), header.type(), true));
     this.measures.add(measure);
     this.values.add(values);
   }
 
-  public void transferAggregates(Table from, Measure measure) {
+
+  /**
+   * BE CAREFUL !! This method assumes this table and the table from passed in arguments have the same headers
+   * {@code Header#isMeasure == false} in the same order.
+   */
+  public void transferAggregates(Table from, CompiledMeasure measure) {
+    if (this.headers.stream().filter(h -> !h.isMeasure()).count() !=
+            from.headers().stream().filter(h -> !h.isMeasure()).count()) {
+      List<String> toHeaderNames = this.headers.stream().filter(h -> !h.isMeasure()).map(Header::name).toList();
+      List<String> fromHeaderNames = from.headers().stream().filter(h -> !h.isMeasure()).map(Header::name).toList();
+      throw new IllegalArgumentException(
+              "The aggregates you are trying to transfer comes from a table that has the following headers " + fromHeaderNames
+                      + " but does not match the headers of the destination table " + toHeaderNames);
+    }
+
     if (from instanceof ColumnarTable ct) {
       List<Object> values = new ArrayList<>((int) count());
       for (int i = 0; i < (int) count(); i++) {
@@ -93,7 +107,7 @@ public class ColumnarTable implements Table {
   }
 
   @Override
-  public Set<Measure> measures() {
+  public Set<CompiledMeasure> measures() {
     return this.measures;
   }
 
diff --git a/core/src/main/java/io/squashql/table/MergeTables.java b/core/src/main/java/io/squashql/table/MergeTables.java
index 712ca32..22529a9 100644
--- a/core/src/main/java/io/squashql/table/MergeTables.java
+++ b/core/src/main/java/io/squashql/table/MergeTables.java
@@ -3,7 +3,7 @@ package io.squashql.table;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Sets;
 import io.squashql.query.Header;
-import io.squashql.query.Measure;
+import io.squashql.query.compiled.CompiledMeasure;
 import io.squashql.query.database.SQLTranslator;
 import io.squashql.query.dictionary.ObjectArrayDictionary;
 import io.squashql.query.dto.JoinType;
@@ -51,7 +51,7 @@ public class MergeTables {
     }
 
     final Holder mergedTableHeaders = mergeHeaders(leftTable, rightTable);
-    final Set<Measure> mergedTableMeasures = mergeMeasures(leftTable.measures(), rightTable.measures());
+    final Set<CompiledMeasure> mergedTableMeasures = mergeMeasures(leftTable.measures(), rightTable.measures());
     final List<List<Object>> mergedValues = mergeValues(mergedTableHeaders, leftTable, rightTable, joinType);
 
     return new ColumnarTable(
@@ -95,7 +95,7 @@ public class MergeTables {
     return new Holder(leftTable, rightTable, mergedTableHeaders, leftMappingList, rightMappingList);
   }
 
-  private static Set<Measure> mergeMeasures(Set<Measure> leftMeasures, Set<Measure> rightMeasures) {
+  private static Set<CompiledMeasure> mergeMeasures(Set<CompiledMeasure> leftMeasures, Set<CompiledMeasure> rightMeasures) {
     return Sets.newHashSet(Iterables.concat(leftMeasures, rightMeasures));
   }
 
@@ -116,8 +116,8 @@ public class MergeTables {
 
     BitSet alreadyVisited = new BitSet();
 
-    List<Measure> leftMeasures = getMeasures((ColumnarTable) leftTable);
-    List<Measure> rightMeasures = getMeasures((ColumnarTable) rightTable);
+    List<CompiledMeasure> leftMeasures = getMeasures((ColumnarTable) leftTable);
+    List<CompiledMeasure> rightMeasures = getMeasures((ColumnarTable) rightTable);
     final int[] position = {-1};
 
     leftTable.pointDictionary().forEach((point, row) -> {
@@ -194,7 +194,7 @@ public class MergeTables {
     return result;
   }
 
-  private static List<Measure> getMeasures(ColumnarTable table) {
+  private static List<CompiledMeasure> getMeasures(ColumnarTable table) {
     return table.headers().stream()
             .filter(Header::isMeasure)
             .map(h -> table.measures().stream().filter(m -> m.alias().equals(h.name())).findFirst().orElseThrow(() -> new IllegalStateException("Cannot find measure with name " + h.name())))
diff --git a/core/src/main/java/io/squashql/table/RowTable.java b/core/src/main/java/io/squashql/table/RowTable.java
index 23d412f..fba3e3a 100644
--- a/core/src/main/java/io/squashql/table/RowTable.java
+++ b/core/src/main/java/io/squashql/table/RowTable.java
@@ -1,13 +1,10 @@
 package io.squashql.table;
 
 import io.squashql.query.Header;
-import io.squashql.query.Measure;
+import io.squashql.query.compiled.CompiledMeasure;
 import io.squashql.query.dictionary.ObjectArrayDictionary;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Set;
+
+import java.util.*;
 
 public class RowTable implements Table {
 
@@ -24,12 +21,12 @@ public class RowTable implements Table {
   }
 
   @Override
-  public void addAggregates(Header header, Measure measure, List<Object> values) {
+  public void addAggregates(Header header, CompiledMeasure measure, List<Object> values) {
     throwNotSupportedException();
   }
 
   @Override
-  public void transferAggregates(Table from, Measure measure) {
+  public void transferAggregates(Table from, CompiledMeasure measure) {
     throwNotSupportedException();
   }
 
@@ -51,7 +48,7 @@ public class RowTable implements Table {
   }
 
   @Override
-  public Set<Measure> measures() {
+  public Set<CompiledMeasure> measures() {
     return Collections.emptySet();
   }
 
diff --git a/core/src/main/java/io/squashql/table/Table.java b/core/src/main/java/io/squashql/table/Table.java
index a6989a6..41f1604 100644
--- a/core/src/main/java/io/squashql/table/Table.java
+++ b/core/src/main/java/io/squashql/table/Table.java
@@ -2,8 +2,7 @@ package io.squashql.table;
 
 import io.squashql.query.Field;
 import io.squashql.query.Header;
-import io.squashql.query.Measure;
-import io.squashql.query.TotalCountMeasure;
+import io.squashql.query.compiled.CompiledMeasure;
 import io.squashql.query.dictionary.ObjectArrayDictionary;
 import io.squashql.query.dto.QueryDto;
 import org.eclipse.collections.api.list.primitive.IntList;
@@ -15,22 +14,24 @@ import java.util.Iterator;
 import java.util.List;
 import java.util.Set;
 
+import static io.squashql.query.compiled.CompiledExpressionMeasure.COMPILED_TOTAL_COUNT;
+
 public interface Table extends Iterable<List<Object>> {
 
   ObjectArrayDictionary pointDictionary();
 
   List<Header> headers();
 
-  Set<Measure> measures();
+  Set<CompiledMeasure> measures();
 
   /**
    * Adds the given aggregates values corresponding to this measure to the table (adds a new column). The order of the
    * aggregates is expected to match the order of the rows in this table. If the order is not known, it is better to use
-   * {@link #transferAggregates(Table, Measure)}.
+   * {@link #transferAggregates(Table, CompiledMeasure)}.
    */
-  void addAggregates(Header header, Measure measure, List<Object> values);
+  void addAggregates(Header header, CompiledMeasure measure, List<Object> values);
 
-  void transferAggregates(Table from, Measure measure);
+  void transferAggregates(Table from, CompiledMeasure measure);
 
   default List<Object> getColumn(int columnIndex) {
     List<Object> elements = new ArrayList<>();
@@ -44,7 +45,7 @@ public interface Table extends Iterable<List<Object>> {
     return getColumn(columnIndex(column));
   }
 
-  default List<Object> getAggregateValues(Measure measure) {
+  default List<Object> getAggregateValues(CompiledMeasure measure) {
     int index = headers().indexOf(getHeader(measure));
     if (index < 0) {
       throw new IllegalArgumentException("no aggregate values for " + measure);
@@ -52,7 +53,7 @@ public interface Table extends Iterable<List<Object>> {
     return getColumn(index);
   }
 
-  default Header getHeader(Measure measure) {
+  default Header getHeader(CompiledMeasure measure) {
     return headers().stream().filter(header -> header.name().equals(measure.alias()))
             .findAny().orElseThrow(() -> new IllegalArgumentException("no header for " + measure));
   }
@@ -107,8 +108,9 @@ public interface Table extends Iterable<List<Object>> {
    * Returns the total number of rows before applying the query limit as in {@link QueryDto#limit}.
    */
   default long totalCount() {
-    return measures().contains(TotalCountMeasure.INSTANCE)
-            ? (long) getAggregateValues(TotalCountMeasure.INSTANCE).get(0) : -1;
+    return measures().contains(COMPILED_TOTAL_COUNT)
+            ? (long) getAggregateValues(COMPILED_TOTAL_COUNT).get(0)
+            : -1;
   }
 
   default void show(int numRows) {
@@ -127,13 +129,13 @@ public interface Table extends Iterable<List<Object>> {
 
       @Override
       public boolean hasNext() {
-        return c[0] < numRows ? underlying.hasNext() : false;
+        return this.c[0] < numRows ? this.underlying.hasNext() : false;
       }
 
       @Override
       public List<Object> next() {
-        c[0]++;
-        return underlying.next();
+        this.c[0]++;
+        return this.underlying.next();
       }
     }, h -> ((Header) h).name(), String::valueOf);
   }
diff --git a/core/src/main/java/io/squashql/table/TableUtils.java b/core/src/main/java/io/squashql/table/TableUtils.java
index c25ebd7..82f0747 100644
--- a/core/src/main/java/io/squashql/table/TableUtils.java
+++ b/core/src/main/java/io/squashql/table/TableUtils.java
@@ -2,6 +2,7 @@ package io.squashql.table;
 
 import com.google.common.base.Suppliers;
 import io.squashql.query.*;
+import io.squashql.query.compiled.CompiledMeasure;
 import io.squashql.query.database.QueryEngine;
 import io.squashql.query.database.SQLTranslator;
 import io.squashql.query.database.SqlUtils;
@@ -117,16 +118,13 @@ public class TableUtils {
   public static List<MetadataItem> buildTableMetadata(Table t) {
     List<MetadataItem> metadata = new ArrayList<>();
     for (Header header : t.headers()) {
-      Optional<Measure> optionalMeasure = t.measures().stream()
+      Optional<CompiledMeasure> optionalMeasure = t.measures().stream()
               .filter(m -> m.alias().equals(header.name()))
               .findAny();
       if (header.isMeasure() && optionalMeasure.isPresent()) {
-        Measure measure = optionalMeasure.get();
-        String expression = measure.expression();
-        if (expression == null) {
-          measure = measure.withExpression(MeasureUtils.createExpression(measure));
-        }
-        metadata.add(new MetadataItem(header.name(), measure.expression(), header.type()));
+        // FIXME deactivated for now
+        metadata.add(new MetadataItem(header.name(), header.name(), header.type()));
+//        metadata.add(new MetadataItem(header.name(), MeasureUtils.createExpression(optionalMeasure.get()), header.type()));
       } else {
         metadata.add(new MetadataItem(header.name(), header.name(), header.type()));
       }
@@ -152,17 +150,24 @@ public class TableUtils {
 
     // ... and then get their string representation.
     List<String> finalColumns = finalFields.stream().map(SqlUtils::squashqlExpression).toList();
-    return selectAndOrderColumns(table, finalColumns, queryDto.measures);
+    List<CompiledMeasure> measures = new ArrayList<>();
+    for (Measure measure : queryDto.measures) {
+      CompiledMeasure compiledMeasure = queryResolver.getMeasures().get(measure);
+      if (compiledMeasure != null) {
+        measures.add(compiledMeasure);
+      }
+    }
+    return selectAndOrderColumns(table, finalColumns, measures);
   }
 
-  public static ColumnarTable selectAndOrderColumns(ColumnarTable table, List<String> columns, List<Measure> measures) {
+  public static ColumnarTable selectAndOrderColumns(ColumnarTable table, List<String> columns, List<CompiledMeasure> measures) {
     List<Header> headers = new ArrayList<>();
     List<List<Object>> values = new ArrayList<>();
     for (String finalColumn : columns) {
       headers.add(table.getHeader(finalColumn));
       values.add(Objects.requireNonNull(table.getColumnValues(finalColumn)));
     }
-    for (Measure measure : measures) {
+    for (CompiledMeasure measure : measures) {
       headers.add(table.getHeader(measure));
       values.add(Objects.requireNonNull(table.getAggregateValues(measure)));
     }
@@ -299,4 +304,95 @@ public class TableUtils {
 
     return lazilyCreated[0] ? finalTable.get() : table;
   }
+
+  /**
+   * Changes the content of the input table to remove columns corresponding to grouping() (columns that help to identify
+   * rows containing totals) and write {@link SQLTranslator#TOTAL_CELL} in the corresponding cells. The input table is
+   * <b>NOT</b> modified, a copy is created instead.
+   * <pre>
+   *   Input:
+   *   +----------+----------+---------------------------+---------------------------+------+----------------------+----+
+   *   | scenario | category | ___grouping___scenario___ | ___grouping___category___ |    p | _contributors_count_ |  q |
+   *   +----------+----------+---------------------------+---------------------------+------+----------------------+----+
+   *   |     base |    drink |                         0 |                         0 |  2.0 |                    1 | 10 |
+   *   |     base |     food |                         0 |                         0 |  3.0 |                    1 | 20 |
+   *   |     base |    cloth |                         0 |                         0 | 10.0 |                    1 |  3 |
+   *   |     null |     null |                         1 |                         1 | 15.0 |                    3 | 33 |
+   *   |     base |     null |                         0 |                         1 | 15.0 |                    3 | 33 |
+   *   +----------+----------+---------------------------+---------------------------+------+----------------------+----+
+   *   Output:
+   *   +-------------+-------------+------+----------------------+----+
+   *   |    scenario |    category |    p | _contributors_count_ |  q |
+   *   +-------------+-------------+------+----------------------+----+
+   *   |        base |       drink |  2.0 |                    1 | 10 |
+   *   |        base |        food |  3.0 |                    1 | 20 |
+   *   |        base |       cloth | 10.0 |                    1 |  3 |
+   *   | ___total___ | ___total___ | 15.0 |                    3 | 33 |
+   *   |        base | ___total___ | 15.0 |                    3 | 33 |
+   *   +-------------+-------------+------+----------------------+----+
+   * </pre>
+   */
+  public static Table replaceNullCellsByTotal(Table input, QueryExecutor.QueryScope scope) {
+    Map<String, String> groupingHeaders = findGroupingHeaderNamesByBaseName(input.headers(), scope);
+    if (!groupingHeaders.isEmpty()) {
+      List<List<Object>> newValues = new ArrayList<>();
+      for (int i = 0; i < input.headers().size(); i++) {
+        newValues.add(new ArrayList<>(input.getColumn(i)));
+      }
+      ColumnarTable copy = new ColumnarTable(input.headers(), input.measures(), newValues);
+
+      Set<String> groupingNames = groupingHeaders.keySet();
+      for (int i = 0; i < copy.headers().size(); i++) {
+        Header header = copy.headers().get(i);
+        List<Object> columnValues = copy.getColumn(i);
+        if (groupingNames.contains(header.name())) {
+          String baseName = groupingHeaders.get(header.name());
+          List<Object> baseColumnValues = copy.getColumnValues(baseName);
+          for (int rowIndex = 0; rowIndex < columnValues.size(); rowIndex++) {
+            if (((Number) columnValues.get(rowIndex)).longValue() == 1) {
+              // It is a total if == 1. It is cast as Number because the type is Byte with Spark, Long with
+              // ClickHouse...
+              baseColumnValues.set(rowIndex, SQLTranslator.TOTAL_CELL);
+            }
+          }
+        }
+      }
+      return copy;
+    }
+    return input;
+  }
+
+  /**
+   * [___grouping___field_name_a___, field_name_a]
+   * [___grouping___field_name_b___, field_name_b]
+   * ...
+   */
+  private static Map<String, String> findGroupingHeaderNamesByBaseName(List<Header> headers, QueryExecutor.QueryScope scope) {
+    Set<String> rollupExpressions = QueryExecutor.generateGroupingMeasures(scope).keySet();
+    Map<String, String> groupingHeaders = new HashMap<>();
+    // rollupExpressions can be empty when working with vector. In that case, we rely on the header name only.
+    // Do it in that order. First this...
+    for (Header header : headers) {
+      String baseName = SqlUtils.extractFieldFromGroupingAlias(header.name());
+      if (baseName != null) {
+        groupingHeaders.put(header.name(), baseName);
+      }
+    }
+
+    // Then this.
+    for (Header header : headers) {
+      if (!header.isMeasure() && rollupExpressions.contains(header.name())) {
+        String groupingAlias = SqlUtils.groupingAlias(header.name().replace(".", "_")); // The alias we are looking for
+        for (Header m : headers) {
+          if (m.isMeasure() && m.name().equals(groupingAlias)) {
+            String baseName = SqlUtils.extractFieldFromGroupingAlias(groupingAlias);
+            if (baseName != null) {
+              groupingHeaders.put(m.name(), header.name());
+            }
+          }
+        }
+      }
+    }
+    return groupingHeaders;
+  }
 }
diff --git a/core/src/main/java/io/squashql/type/AliasedTypedField.java b/core/src/main/java/io/squashql/type/AliasedTypedField.java
index 8eb24ec..2002aaa 100644
--- a/core/src/main/java/io/squashql/type/AliasedTypedField.java
+++ b/core/src/main/java/io/squashql/type/AliasedTypedField.java
@@ -7,7 +7,7 @@ public record AliasedTypedField(String alias) implements TypedField {
 
   @Override
   public String sqlExpression(QueryRewriter queryRewriter) {
-    return this.alias;
+    return queryRewriter.escapeAlias(this.alias);
   }
 
   @Override
@@ -19,4 +19,9 @@ public record AliasedTypedField(String alias) implements TypedField {
   public String name() {
     return this.alias;
   }
+
+  @Override
+  public TypedField as(String alias) {
+    return new AliasedTypedField(alias); // does not make sense...
+  }
 }
diff --git a/core/src/main/java/io/squashql/type/BinaryOperationTypedField.java b/core/src/main/java/io/squashql/type/BinaryOperationTypedField.java
index 6840fad..3a9e6de 100644
--- a/core/src/main/java/io/squashql/type/BinaryOperationTypedField.java
+++ b/core/src/main/java/io/squashql/type/BinaryOperationTypedField.java
@@ -26,4 +26,9 @@ public record BinaryOperationTypedField(BinaryOperator operator, TypedField left
   public String name() {
     throw new IllegalStateException("Incorrect path of execution");
   }
+
+  @Override
+  public TypedField as(String alias) {
+    return new BinaryOperationTypedField(this.operator, this.leftOperand, this.rightOperand, alias);
+  }
 }
diff --git a/core/src/main/java/io/squashql/type/ConstantTypedField.java b/core/src/main/java/io/squashql/type/ConstantTypedField.java
index bd8872f..09b9938 100644
--- a/core/src/main/java/io/squashql/type/ConstantTypedField.java
+++ b/core/src/main/java/io/squashql/type/ConstantTypedField.java
@@ -25,4 +25,9 @@ public record ConstantTypedField(Object value) implements TypedField {
   public String alias() {
     throw new IllegalStateException("Incorrect path of execution");
   }
+
+  @Override
+  public TypedField as(String alias) {
+    throw new IllegalStateException("Incorrect path of execution");
+  }
 }
diff --git a/core/src/main/java/io/squashql/type/FunctionTypedField.java b/core/src/main/java/io/squashql/type/FunctionTypedField.java
index 50cfd97..ad87571 100644
--- a/core/src/main/java/io/squashql/type/FunctionTypedField.java
+++ b/core/src/main/java/io/squashql/type/FunctionTypedField.java
@@ -25,4 +25,9 @@ public record FunctionTypedField(TableTypedField field, String function, String
   public String name() {
     throw new IllegalStateException("Incorrect path of execution");
   }
+
+  @Override
+  public TypedField as(String alias) {
+    return new FunctionTypedField(this.field, this.function, alias);
+  }
 }
diff --git a/core/src/main/java/io/squashql/type/TableTypedField.java b/core/src/main/java/io/squashql/type/TableTypedField.java
index d8bc60e..108b244 100644
--- a/core/src/main/java/io/squashql/type/TableTypedField.java
+++ b/core/src/main/java/io/squashql/type/TableTypedField.java
@@ -24,4 +24,9 @@ public record TableTypedField(String store, String name, Class<?> type, String a
       return queryRewriter.getFieldFullName(this);
     }
   }
+
+  @Override
+  public TypedField as(String alias) {
+    return new TableTypedField(this.store, this.name, this.type, alias);
+  }
 }
diff --git a/core/src/main/java/io/squashql/type/TypedField.java b/core/src/main/java/io/squashql/type/TypedField.java
index 9519ba0..800ee75 100644
--- a/core/src/main/java/io/squashql/type/TypedField.java
+++ b/core/src/main/java/io/squashql/type/TypedField.java
@@ -13,4 +13,6 @@ public interface TypedField {
   String name();
 
   String alias();
+
+  TypedField as(String alias);
 }
diff --git a/core/src/main/java/io/squashql/util/Queries.java b/core/src/main/java/io/squashql/util/Queries.java
index 8c984d5..4a4bc57 100644
--- a/core/src/main/java/io/squashql/util/Queries.java
+++ b/core/src/main/java/io/squashql/util/Queries.java
@@ -3,12 +3,9 @@ package io.squashql.util;
 import io.squashql.query.ColumnSet;
 import io.squashql.query.ColumnSetKey;
 import io.squashql.query.Field;
-import io.squashql.query.database.DatabaseQuery;
 import io.squashql.query.dto.*;
-import io.squashql.type.TypedField;
 
 import java.util.*;
-import java.util.stream.Collectors;
 
 import static io.squashql.query.dto.OrderKeywordDto.DESC;
 
@@ -46,15 +43,4 @@ public final class Queries {
 
     return res;
   }
-
-  public static List<TypedField> generateGroupingSelect(DatabaseQuery query) {
-    List<TypedField> selects = new ArrayList<>();
-    selects.addAll(query.rollup);
-    // order matters, this is why a LinkedHashSet is used.
-    selects.addAll(query.groupingSets
-            .stream()
-            .flatMap(Collection::stream)
-            .collect(Collectors.toCollection(LinkedHashSet::new)));
-    return selects;
-  }
 }
diff --git a/pom.xml b/pom.xml
index 4581c12..b64a9cb 100644
--- a/pom.xml
+++ b/pom.xml
@@ -274,7 +274,8 @@
         <version>${maven-surefire-plugin.version}</version>
         <configuration>
           <!-- See here https://www.jacoco.org/jacoco/trunk/doc/prepare-agent-mojo.html to understand why `@{argLine}` is needed-->
-          <argLine>--add-opens=java.base/sun.nio.ch=ALL-UNNAMED</argLine>
+          <!-- https://stackoverflow.com/questions/73465937/apache-spark-3-3-0-breaks-on-java-17-with-cannot-access-class-sun-nio-ch-direct-->
+          <argLine>--add-opens=java.base/sun.nio.ch=ALL-UNNAMED --add-opens=java.base/sun.util.calendar=ALL-UNNAMED</argLine>
         </configuration>
       </plugin>
       <plugin>
diff --git a/spark/src/main/java/io/squashql/SparkUtil.java b/spark/src/main/java/io/squashql/SparkUtil.java
index 1fc974b..6f4d93d 100644
--- a/spark/src/main/java/io/squashql/SparkUtil.java
+++ b/spark/src/main/java/io/squashql/SparkUtil.java
@@ -2,6 +2,7 @@ package io.squashql;
 
 import io.squashql.type.TableTypedField;
 import io.squashql.util.Types;
+import org.apache.spark.sql.types.ArrayType;
 import org.apache.spark.sql.types.DataType;
 import org.apache.spark.sql.types.DataTypes;
 import org.apache.spark.sql.types.StructType;
@@ -36,6 +37,8 @@ public final class SparkUtil {
     } else {
       if (type.sql().contains("DECIMAL")) {
         return BigDecimal.class;
+      } else if (type.getClass().equals(ArrayType.class)) {
+        return Object.class;
       }
       throw new IllegalArgumentException("Unsupported field type " + type);
     }
@@ -69,12 +72,13 @@ public final class SparkUtil {
   public static Object getTypeValue(Object o) {
     if (o instanceof BigDecimal bd) {
       return Types.castToDouble(bd);
+    } else if (o instanceof java.sql.Date d) {
+      return d.toLocalDate();
     } else {
       return o;
     }
   }
 
-
   public static StructType createSchema(List<TableTypedField> fields) {
     StructType schema = new StructType();
     for (TableTypedField field : fields) {
diff --git a/spark/src/main/java/io/squashql/query/database/SparkQueryEngine.java b/spark/src/main/java/io/squashql/query/database/SparkQueryEngine.java
index 8901047..b347858 100644
--- a/spark/src/main/java/io/squashql/query/database/SparkQueryEngine.java
+++ b/spark/src/main/java/io/squashql/query/database/SparkQueryEngine.java
@@ -3,7 +3,6 @@ package io.squashql.query.database;
 import io.squashql.SparkDatastore;
 import io.squashql.SparkUtil;
 import io.squashql.query.Header;
-import io.squashql.query.compiled.CompiledMeasure;
 import io.squashql.table.ColumnarTable;
 import io.squashql.table.RowTable;
 import io.squashql.table.Table;
@@ -12,8 +11,8 @@ import org.apache.spark.sql.Row;
 import org.eclipse.collections.api.tuple.Pair;
 
 import java.util.Arrays;
+import java.util.HashSet;
 import java.util.List;
-import java.util.stream.Collectors;
 
 import static io.squashql.SparkUtil.datatypeToClass;
 
@@ -53,7 +52,7 @@ public class SparkQueryEngine extends AQueryEngine<SparkDatastore> {
             (i, r) -> SparkUtil.getTypeValue(r.get(i)));
     return new ColumnarTable(
             result.getOne(),
-            query.measures.stream().map(CompiledMeasure::measure).collect(Collectors.toSet()),
+            new HashSet<>(query.measures),
             result.getTwo());
   }
 


Test patch:
diff --git a/.github/workflows/cloud-tests.yml b/.github/workflows/cloud-tests.yml
index dbcf8e9..5fc34b9 100644
--- a/.github/workflows/cloud-tests.yml
+++ b/.github/workflows/cloud-tests.yml
@@ -27,3 +27,23 @@ jobs:
           SERVICE_ACCOUNT_KEY: ${{ secrets.SERVICE_ACCOUNT_KEY }}
           PROJECT_ID: ${{ secrets.PROJECT_ID }}
           DATASET_NAME: ${{ secrets.DATASET_NAME }}
+
+  snowflake:
+    runs-on: ubuntu-latest
+    timeout-minutes: 10
+    steps:
+      - uses: actions/checkout@v3
+      - name: Set up JDK 17
+        uses: actions/setup-java@v3
+        with:
+          distribution: 'temurin'
+          java-version: '17'
+          cache: 'maven'
+      - name: Build with Maven
+        run: mvn -pl :squashql-snowflake -am clean install --no-transfer-progress -DskipTests
+      - name: Run snowflake tests
+        run: mvn -pl :squashql-snowflake --no-transfer-progress -Psnowflake-test test
+        env:
+          SNOWFLAKE_JDBC_URL: ${{ secrets.SNOWFLAKE_JDBC_URL }}
+          SNOWFLAKE_USER: ${{ secrets.SNOWFLAKE_USER }}
+          SNOWFLAKE_PASSWORD: ${{ secrets.SNOWFLAKE_PASSWORD }}
diff --git a/bigquery/src/test/java/io/squashql/query/ATestBigQueryVectorAggregation.java b/bigquery/src/test/java/io/squashql/query/ATestBigQueryVectorAggregation.java
new file mode 100644
index 0000000..a4cf0ca
--- /dev/null
+++ b/bigquery/src/test/java/io/squashql/query/ATestBigQueryVectorAggregation.java
@@ -0,0 +1,19 @@
+package io.squashql.query;
+
+import com.google.cloud.bigquery.FieldValueList;
+import io.squashql.TestClass;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.List;
+
+@TestClass
+public abstract class ATestBigQueryVectorAggregation extends ATestVectorAggregation {
+
+  @Override
+  protected List<Number> getVectorValue(Object actualVector) {
+    List<Number> r = new ArrayList<>();
+    ((FieldValueList) actualVector).forEach(e -> r.add(new BigDecimal((String) e.getValue()).doubleValue()));
+    return r;
+  }
+}
diff --git a/bigquery/src/test/java/io/squashql/query/TestBigQueryVectorAggregation.java b/bigquery/src/test/java/io/squashql/query/TestBigQueryVectorAggregation.java
new file mode 100644
index 0000000..a561580
--- /dev/null
+++ b/bigquery/src/test/java/io/squashql/query/TestBigQueryVectorAggregation.java
@@ -0,0 +1,51 @@
+package io.squashql.query;
+
+import io.squashql.BigQueryDatastore;
+import io.squashql.BigQueryServiceAccountDatastore;
+import io.squashql.query.database.BigQueryEngine;
+import io.squashql.query.database.QueryEngine;
+import io.squashql.store.Datastore;
+import io.squashql.transaction.BigQueryDataLoader;
+import io.squashql.transaction.DataLoader;
+import org.junit.jupiter.api.AfterAll;
+
+import static io.squashql.query.BigQueryTestUtil.DATASET_NAME;
+import static io.squashql.query.BigQueryTestUtil.PROJECT_ID;
+
+/**
+ * Do not edit this class, it has been generated automatically by {@link io.squashql.template.BigQueryClassTemplateGenerator}.
+ */
+public class TestBigQueryVectorAggregation extends ATestBigQueryVectorAggregation {
+
+  @AfterAll
+  void tearDown() {
+    this.fieldsByStore.forEach((store, fields) -> BigQueryTestUtil.deleteTable(((BigQueryDatastore) this.datastore).getBigquery(), store));
+  }
+
+  @Override
+  protected void createTables() {
+    BigQueryDataLoader tm = (BigQueryDataLoader) this.tm;
+    BigQueryTestUtil.createDatasetIfDoesNotExist(tm.getBigQuery(), DATASET_NAME);
+    this.fieldsByStore.forEach((store, fields) -> tm.dropAndCreateInMemoryTable(store, fields));
+  }
+
+  @Override
+  protected QueryEngine createQueryEngine(Datastore datastore) {
+    return new BigQueryEngine((BigQueryDatastore) datastore);
+  }
+
+  @Override
+  protected Datastore createDatastore() {
+    return new BigQueryServiceAccountDatastore(BigQueryTestUtil.createServiceAccountCredentials(), PROJECT_ID, DATASET_NAME);
+  }
+
+  @Override
+  protected DataLoader createDataLoader() {
+    return new BigQueryDataLoader(((BigQueryDatastore) this.datastore).getBigquery(), DATASET_NAME);
+  }
+
+  @Override
+  protected Object translate(Object o) {
+    return BigQueryTestUtil.translate(o);
+  }
+}
diff --git a/clickhouse/src/test/java/io/squashql/query/ATestClickHouseVectorAggregation.java b/clickhouse/src/test/java/io/squashql/query/ATestClickHouseVectorAggregation.java
new file mode 100644
index 0000000..64c8eb8
--- /dev/null
+++ b/clickhouse/src/test/java/io/squashql/query/ATestClickHouseVectorAggregation.java
@@ -0,0 +1,14 @@
+package io.squashql.query;
+
+import io.squashql.TestClass;
+
+import java.util.List;
+
+@TestClass
+public abstract class ATestClickHouseVectorAggregation extends ATestVectorAggregation {
+
+  @Override
+  protected List<Number> getVectorValue(Object actualVector) {
+    return (List<Number>) actualVector;
+  }
+}
diff --git a/clickhouse/src/test/java/io/squashql/query/TestClickHousePeriodComparison.java b/clickhouse/src/test/java/io/squashql/query/TestClickHousePeriodComparison.java
index 85b9b5b..78eb46f 100644
--- a/clickhouse/src/test/java/io/squashql/query/TestClickHousePeriodComparison.java
+++ b/clickhouse/src/test/java/io/squashql/query/TestClickHousePeriodComparison.java
@@ -50,19 +50,4 @@ public class TestClickHousePeriodComparison extends ATestPeriodComparison {
   protected DataLoader createDataLoader() {
     return new ClickHouseDataLoader(((ClickHouseDatastore) this.datastore).dataSource);
   }
-
-  @Override
-  protected Object yearType(int i) {
-    return i;
-  }
-
-  @Override
-  protected Object quarterType(int i) {
-    return i;
-  }
-
-  @Override
-  protected Object monthType(int i) {
-    return i;
-  }
 }
diff --git a/clickhouse/src/test/java/io/squashql/query/TestClickHouseVectorAggregation.java b/clickhouse/src/test/java/io/squashql/query/TestClickHouseVectorAggregation.java
new file mode 100644
index 0000000..d230b84
--- /dev/null
+++ b/clickhouse/src/test/java/io/squashql/query/TestClickHouseVectorAggregation.java
@@ -0,0 +1,53 @@
+package io.squashql.query;
+
+import io.squashql.ClickHouseDatastore;
+import io.squashql.query.database.ClickHouseQueryEngine;
+import io.squashql.query.database.QueryEngine;
+import io.squashql.store.Datastore;
+import io.squashql.template.ClickHouseClassTemplateGenerator;
+import io.squashql.transaction.ClickHouseDataLoader;
+import io.squashql.transaction.DataLoader;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+
+/**
+ * Do not edit this class, it has been generated automatically by {@link ClickHouseClassTemplateGenerator}.
+ */
+public class TestClickHouseVectorAggregation extends ATestClickHouseVectorAggregation {
+
+  public org.testcontainers.containers.GenericContainer container = ClickHouseTestUtil.createClickHouseContainer();
+
+  @BeforeAll
+  @Override
+  void setup() {
+    this.container.start();
+    super.setup();
+  }
+
+  @AfterAll
+  void tearDown() {
+    // we do not stop the container to be able to reuse it between tests.
+    ((ClickHouseDataLoader) this.tm).dropTables(this.fieldsByStore.keySet());
+  }
+
+  @Override
+  protected void createTables() {
+    ClickHouseDataLoader tm = (ClickHouseDataLoader) this.tm;
+    this.fieldsByStore.forEach((store, fields) -> tm.dropAndCreateInMemoryTable(store, fields));
+  }
+
+  @Override
+  protected QueryEngine createQueryEngine(Datastore datastore) {
+    return new ClickHouseQueryEngine((ClickHouseDatastore) datastore);
+  }
+
+  @Override
+  protected Datastore createDatastore() {
+    return new ClickHouseDatastore(ClickHouseTestUtil.jdbcUrl.apply(this.container));
+  }
+
+  @Override
+  protected DataLoader createDataLoader() {
+    return new ClickHouseDataLoader(((ClickHouseDatastore) this.datastore).dataSource);
+  }
+}
diff --git a/core/src/test/java/io/squashql/query/ATestPeriodComparison.java b/core/src/test/java/io/squashql/query/ATestPeriodComparison.java
index 78127cc..c32d6e7 100644
--- a/core/src/test/java/io/squashql/query/ATestPeriodComparison.java
+++ b/core/src/test/java/io/squashql/query/ATestPeriodComparison.java
@@ -415,15 +415,29 @@ public abstract class ATestPeriodComparison extends ABaseTestQuery {
   }
 
   protected Object yearType(int i) {
-    return (long) i;
+    String qesn = this.queryEngine.getClass().getSimpleName();
+    if (qesn.contains(TestClass.Type.CLICKHOUSE.className) || qesn.contains(TestClass.Type.SPARK.className)) {
+      return i;
+    } else {
+      return (long) i;
+    }
   }
 
   protected Object quarterType(int i) {
-    return (long) i;
+    String qesn = this.queryEngine.getClass().getSimpleName();
+    if (qesn.contains(TestClass.Type.CLICKHOUSE.className) || qesn.contains(TestClass.Type.SPARK.className)) {
+      return i;
+    } else {
+      return (long) i;
+    }
   }
 
   protected Object monthType(int i) {
-    return (long) i;
+    String qesn = this.queryEngine.getClass().getSimpleName();
+    if (qesn.contains(TestClass.Type.CLICKHOUSE.className) || qesn.contains(TestClass.Type.SPARK.className)) {
+      return i;
+    } else {
+      return (long) i;
+    }
   }
-
 }
diff --git a/core/src/test/java/io/squashql/query/ATestQueryCache.java b/core/src/test/java/io/squashql/query/ATestQueryCache.java
index b7147b3..05fc7ff 100644
--- a/core/src/test/java/io/squashql/query/ATestQueryCache.java
+++ b/core/src/test/java/io/squashql/query/ATestQueryCache.java
@@ -7,6 +7,7 @@ import io.squashql.query.dto.*;
 import io.squashql.query.parameter.QueryCacheParameter;
 import io.squashql.table.Table;
 import io.squashql.type.TableTypedField;
+import io.squashql.util.TestUtil;
 import org.assertj.core.api.Assertions;
 import org.junit.jupiter.api.Assumptions;
 import org.junit.jupiter.api.BeforeEach;
@@ -32,7 +33,6 @@ public abstract class ATestQueryCache extends ABaseTestQuery {
 
   protected String storeName = "store" + getClass().getSimpleName().toLowerCase();
   protected String competitorStoreName = "competitor" + getClass().getSimpleName().toLowerCase();
-
   protected GlobalCache queryCache;
 
   @Override
@@ -314,17 +314,17 @@ public abstract class ATestQueryCache extends ABaseTestQuery {
             .withMeasure(new AggregatedMeasure("ps", "price", AggregationFunction.SUM));
     // Scope 1 added to the cache
     executor.executeQuery(querySupplier.get());
-    assertCacheStats(cache.stats(), 0, 2);
+    TestUtil.assertCacheStats(cache.stats(), 0, 2);
 
     // Scope 2 added to the cache
     executor.executeQuery(querySupplier.get().withColumn(tableField("category")));
-    assertCacheStats(cache.stats(), 0, 4);
+    TestUtil.assertCacheStats(cache.stats(), 0, 4);
 
     // Scope 3, should evict an entry in the cache
     executor.executeQuery(querySupplier.get().withCondition(tableField("category"), Functions.eq("drink")));
     latch.await(60, TimeUnit.SECONDS);
     CacheStatsDto stats = cache.stats();
-    assertCacheStats(stats, 0, 6);
+    TestUtil.assertCacheStats(cache.stats(), 0, 6);
     Assertions.assertThat(c.getAndIncrement()).isEqualTo(1);
     Assertions.assertThat(stats.evictionCount).isEqualTo(1);
   }
@@ -496,31 +496,24 @@ public abstract class ATestQueryCache extends ABaseTestQuery {
             .build();
     int base = 0;
     this.executor.executePivotQuery(new PivotTableQueryDto(q, List.of(category), List.of(ean)));
-    assertCacheStats(0, (base = base + 2));
+    assertCacheStats(0, (base = base + 4));
     this.executor.executePivotQuery(new PivotTableQueryDto(q, List.of(category, ean), List.of()));
-    assertCacheStats(0, (base = base + 2));
+    assertCacheStats(0, (base = base + 4));
     this.executor.executePivotQuery(new PivotTableQueryDto(q, List.of(), List.of(category, ean)));
-    assertCacheStats(2, base); // same as the previous
+    assertCacheStats(4, base); // same as the previous
     this.executor.executePivotQuery(new PivotTableQueryDto(q, List.of(ean), List.of(category)));
-    assertCacheStats(2, (base = base + 2));
+    assertCacheStats(4, (base = base + 4));
     // Same as the first query
     this.executor.executePivotQuery(new PivotTableQueryDto(q, List.of(category), List.of(ean)));
-    assertCacheStats(4, base);
+    assertCacheStats(8, base);
   }
 
   private void assertCacheStats(int hitCount, int missCount) {
-    CacheStatsDto stats = this.queryCache.stats(null);
-    assertCacheStats(stats, hitCount, missCount);
+    TestUtil.assertCacheStats(this.queryCache, hitCount, missCount);
   }
 
   private void assertCacheStats(int hitCount, int missCount, SquashQLUser user) {
-    CacheStatsDto stats = this.queryCache.stats(user);
-    assertCacheStats(stats, hitCount, missCount);
-  }
-
-  private void assertCacheStats(CacheStatsDto stats, int hitCount, int missCount) {
-    Assertions.assertThat(stats.hitCount).isEqualTo(hitCount);
-    Assertions.assertThat(stats.missCount).isEqualTo(missCount);
+    TestUtil.assertCacheStats(this.queryCache, hitCount, missCount, user);
   }
 
   private static Table execute(QueryExecutor executor, QueryDto query, SquashQLUser user) {
diff --git a/core/src/test/java/io/squashql/query/ATestQueryExecutionLocality.java b/core/src/test/java/io/squashql/query/ATestQueryExecutionLocality.java
index dcde8c2..ee9217f 100644
--- a/core/src/test/java/io/squashql/query/ATestQueryExecutionLocality.java
+++ b/core/src/test/java/io/squashql/query/ATestQueryExecutionLocality.java
@@ -59,7 +59,7 @@ public abstract class ATestQueryExecutionLocality extends ABaseTestQuery {
             List.of("bottle", 10d),
             List.of("cookie", 30d),
             List.of("shirt", 15d));
-    Assertions.assertThat(interceptor.lastExecutedDatabaseQuery.measures.stream().map(CompiledMeasure::measure)).contains(divide);
+    Assertions.assertThat(interceptor.lastExecutedDatabaseQuery.measures.stream().map(CompiledMeasure::alias)).contains(divide.alias());
   }
 
   private static class QueryEngineInterceptor<T extends Datastore> implements QueryEngine<T> {
diff --git a/core/src/test/java/io/squashql/query/ATestQueryExecutor.java b/core/src/test/java/io/squashql/query/ATestQueryExecutor.java
index e017beb..406402f 100644
--- a/core/src/test/java/io/squashql/query/ATestQueryExecutor.java
+++ b/core/src/test/java/io/squashql/query/ATestQueryExecutor.java
@@ -2,6 +2,7 @@ package io.squashql.query;
 
 import io.squashql.TestClass;
 import io.squashql.query.builder.Query;
+import io.squashql.query.compiled.CompiledExpressionMeasure;
 import io.squashql.query.database.QueryRewriter;
 import io.squashql.query.database.SqlUtils;
 import io.squashql.query.dto.*;
@@ -791,7 +792,7 @@ public abstract class ATestQueryExecutor extends ABaseTestQuery {
 
     Table result = this.executor.executeQuery(query);
     Assertions.assertThat(result.count()).isEqualTo(limit); // we don't care about the result, and we can't know what lines will be returned
-    Assertions.assertThat(result.getAggregateValues(TotalCountMeasure.INSTANCE).get(0)).isEqualTo(3L);
+    Assertions.assertThat(result.getAggregateValues(CompiledExpressionMeasure.COMPILED_TOTAL_COUNT).get(0)).isEqualTo(3L);
   }
 
   @Test
diff --git a/core/src/test/java/io/squashql/query/ATestVectorAggregation.java b/core/src/test/java/io/squashql/query/ATestVectorAggregation.java
new file mode 100644
index 0000000..9794f64
--- /dev/null
+++ b/core/src/test/java/io/squashql/query/ATestVectorAggregation.java
@@ -0,0 +1,341 @@
+package io.squashql.query;
+
+import io.squashql.query.builder.Query;
+import io.squashql.query.dictionary.ObjectArrayDictionary;
+import io.squashql.query.dto.QueryDto;
+import io.squashql.table.ColumnarTable;
+import io.squashql.table.Table;
+import io.squashql.type.TableTypedField;
+import io.squashql.util.TestUtil;
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+
+import java.time.LocalDate;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import static io.squashql.query.agg.AggregationFunction.SUM;
+import static io.squashql.query.database.QueryEngine.GRAND_TOTAL;
+import static io.squashql.query.database.QueryEngine.TOTAL;
+
+@TestInstance(TestInstance.Lifecycle.PER_CLASS)
+public abstract class ATestVectorAggregation extends ABaseTestQuery {
+
+  String storeName = "store" + getClass().getSimpleName().toLowerCase();
+  GlobalCache queryCache;
+  LocalDate d1 = LocalDate.of(2023, 1, 1);
+  LocalDate d2 = LocalDate.of(2023, 1, 2);
+  LocalDate d3 = LocalDate.of(2023, 1, 3);
+
+  @Override
+  protected void afterSetup() {
+    this.queryCache = (GlobalCache) this.executor.queryCache;
+  }
+
+  @Override
+  protected Map<String, List<TableTypedField>> getFieldsByStore() {
+    TableTypedField ticker = new TableTypedField(this.storeName, "ticker", String.class);
+    TableTypedField date = new TableTypedField(this.storeName, "date", LocalDate.class);
+    TableTypedField riskType = new TableTypedField(this.storeName, "riskType", String.class);
+    TableTypedField value = new TableTypedField(this.storeName, "value", double.class);
+    TableTypedField valueInt = new TableTypedField(this.storeName, "valueInt", int.class); // same but type is different
+    return Map.of(this.storeName, List.of(ticker, date, riskType, value, valueInt));
+  }
+
+  @Override
+  protected void loadData() {
+    String mmm = "MMM";
+    String vblax = "VBLAX";
+    String totalReturn = "TotalReturn";
+    String fxReturn = "FXReturn";
+    String equityReturn = "EquityReturn";
+    this.tm.load(this.storeName, List.of(
+            new Object[]{mmm, d1, totalReturn, 1d, 1},
+            new Object[]{mmm, d1, fxReturn, 2d, 2},
+            new Object[]{mmm, d1, equityReturn, 3d, 3},
+            new Object[]{mmm, d2, totalReturn, 10d, 10},
+            new Object[]{mmm, d2, fxReturn, 11d, 11},
+            new Object[]{mmm, d2, equityReturn, 12d, 12},
+            new Object[]{mmm, d3, totalReturn, 100d, 100},
+            new Object[]{mmm, d3, fxReturn, 101d, 101},
+            new Object[]{mmm, d3, equityReturn, 102d, 102},
+
+            new Object[]{vblax, d1, totalReturn, 1000d, 1000},
+            new Object[]{vblax, d1, fxReturn, 2000d, 2000},
+            new Object[]{vblax, d1, equityReturn, 3000d, 3000},
+            new Object[]{vblax, d2, totalReturn, 10000d, 10000},
+            new Object[]{vblax, d2, fxReturn, 11000d, 11000},
+            new Object[]{vblax, d2, equityReturn, 12000d, 12000},
+            new Object[]{vblax, d3, totalReturn, 100000d, 100000},
+            new Object[]{vblax, d3, fxReturn, 101000d, 101000},
+            new Object[]{vblax, d3, equityReturn, 102000d, 102000}
+    ));
+  }
+
+  @Test
+  void testCrossjoinOneWithTotals() {
+    Field ticker = new TableField(this.storeName, "ticker");
+    Field value = new TableField(this.storeName, "value");
+    Field date = new TableField(this.storeName, "date");
+
+    Measure vector = new VectorAggMeasure("vector", value, SUM, date);
+    QueryDto query = Query
+            .from(this.storeName)
+            .select(List.of(ticker), List.of(vector))
+            .rollup(List.of(ticker))
+            .build();
+    Table result = this.executor.executeQuery(query);
+    Assertions.assertThat(result.headers().stream().map(Header::name))
+            .containsExactly(ticker.name(), vector.alias());
+    List<List<Object>> points = List.of(List.of(GRAND_TOTAL), List.of("MMM"), List.of("VBLAX"));
+    List<List<Number>> expectedVectors = List.of(
+            List.of(6006d, 33033d, 303303d),
+            List.of(6d, 33d, 303d),
+            List.of(6000d, 33000d, 303000d));
+    assertVectorValues((ColumnarTable) result, vector, points, expectedVectors);
+  }
+
+  @Test
+  void testCrossjoinOneWithoutTotals() {
+    Field ticker = new TableField(this.storeName, "ticker");
+    Field value = new TableField(this.storeName, "value");
+    Field date = new TableField(this.storeName, "date");
+
+    Measure vector = new VectorAggMeasure("vector", value, SUM, date);
+    QueryDto query = Query
+            .from(this.storeName)
+            .select(List.of(ticker), List.of(vector, CountMeasure.INSTANCE))
+            .build();
+    Table result = this.executor.executeQuery(query);
+    Assertions.assertThat(result.headers().stream().map(Header::name))
+            .containsExactly(ticker.name(), vector.alias(), CountMeasure.ALIAS);
+    List<List<Object>> points = List.of(List.of("MMM"), List.of("VBLAX"));
+    List<List<Number>> expectedVectors = List.of(
+            List.of(6d, 33d, 303d),
+            List.of(6000d, 33000d, 303000d));
+    assertVectorValues((ColumnarTable) result, vector, points, expectedVectors);
+    assertValues((ColumnarTable) result, CountMeasure.INSTANCE, points, List.of(9L, 9L));
+  }
+
+  @Test
+  void testCrossjoinTwoWithTotals() {
+    Field ticker = new TableField(this.storeName, "ticker");
+    Field riskType = new TableField(this.storeName, "riskType");
+    Field value = new TableField(this.storeName, "value");
+    Field date = new TableField(this.storeName, "date");
+
+    Measure vector = new VectorAggMeasure("vector", value, SUM, date);
+    QueryDto query = Query
+            .from(this.storeName)
+            .select(List.of(ticker, riskType), List.of(vector))
+            .rollup(List.of(ticker, riskType))
+            .build();
+    Table result = this.executor.executeQuery(query);
+    Assertions.assertThat(result.headers().stream().map(Header::name))
+            .containsExactly(ticker.name(), riskType.name(), vector.alias());
+    List<List<Object>> points = List.of(
+            List.of(GRAND_TOTAL, GRAND_TOTAL),
+            List.of("MMM", TOTAL),
+            List.of("MMM", "EquityReturn"),
+            List.of("MMM", "FXReturn"),
+            List.of("MMM", "TotalReturn"),
+            List.of("VBLAX", TOTAL),
+            List.of("VBLAX", "EquityReturn"),
+            List.of("VBLAX", "FXReturn"),
+            List.of("VBLAX", "TotalReturn"));
+    List<List<Number>> expectedVectors = List.of(
+            List.of(6006d, 33033d, 303303d),
+            List.of(6d, 33d, 303d),
+            List.of(3.0, 12.0, 102.0),
+            List.of(2.0, 11.0, 101.0),
+            List.of(1.0, 10.0, 100.0),
+            List.of(6000d, 33000d, 303000d),
+            List.of(3000.0, 12000.0, 102000.0),
+            List.of(2000.0, 11000.0, 101000.0),
+            List.of(1000.0, 10000.0, 100000.0));
+    assertVectorValues((ColumnarTable) result, vector, points, expectedVectors);
+  }
+
+  @Test
+  void testCrossjoinTwoWithoutTotals() {
+    Field ticker = new TableField(this.storeName, "ticker");
+    Field riskType = new TableField(this.storeName, "riskType");
+    Field value = new TableField(this.storeName, "value");
+    Field date = new TableField(this.storeName, "date");
+
+    Measure vector = new VectorAggMeasure("vector", value, SUM, date);
+    QueryDto query = Query
+            .from(this.storeName)
+            .select(List.of(ticker, riskType), List.of(vector, CountMeasure.INSTANCE))
+            .build();
+    Table result = this.executor.executeQuery(query);
+    Assertions.assertThat(result.headers().stream().map(Header::name))
+            .containsExactly(ticker.name(), riskType.name(), vector.alias(), CountMeasure.ALIAS);
+    List<List<Object>> points = List.of(
+            List.of("MMM", "EquityReturn"),
+            List.of("MMM", "FXReturn"),
+            List.of("MMM", "TotalReturn"),
+            List.of("VBLAX", "EquityReturn"),
+            List.of("VBLAX", "FXReturn"),
+            List.of("VBLAX", "TotalReturn"));
+    List<List<Number>> expectedVectors = List.of(
+            List.of(3.0, 12.0, 102.0),
+            List.of(2.0, 11.0, 101.0),
+            List.of(1.0, 10.0, 100.0),
+            List.of(3000.0, 12000.0, 102000.0),
+            List.of(2000.0, 11000.0, 101000.0),
+            List.of(1000.0, 10000.0, 100000.0));
+    assertVectorValues((ColumnarTable) result, vector, points, expectedVectors);
+  }
+
+  /**
+   * When compute with the vector axis in the query, the aggregation is as it would be without any vectorization.
+   */
+  @Test
+  void testSimpleWithVectorAxisInSelect() {
+    Field ticker = new TableField(this.storeName, "ticker");
+    Field value = new TableField(this.storeName, "value");
+    Field date = new TableField(this.storeName, "date");
+
+    Measure vector = new VectorAggMeasure("vector", value, SUM, date);
+    Measure vectorSum = new AggregatedMeasure("vectorSum", value, SUM, false);
+    QueryDto query = Query
+            .from(this.storeName)
+            .select(List.of(ticker, date), List.of(vector, vectorSum))
+            .rollup(List.of(ticker, date))
+            .build();
+    Table result = this.executor.executeQuery(query);
+    Assertions.assertThat(result.headers().stream().map(Header::name))
+            .containsExactly(ticker.name(), date.name(), vector.alias(), vectorSum.alias());
+    Assertions.assertThat(result).containsExactly(
+            List.of(GRAND_TOTAL, GRAND_TOTAL, 342342d, 342342d),
+            List.of("MMM", TOTAL, 342d, 342d),
+            List.of("MMM", d1, 6d, 6d),
+            List.of("MMM", d2, 33d, 33d),
+            List.of("MMM", d3, 303d, 303d),
+            List.of("VBLAX", TOTAL, 342000d, 342000d),
+            List.of("VBLAX", d1, 6000d, 6000d),
+            List.of("VBLAX", d2, 33000d, 33000d),
+            List.of("VBLAX", d3, 303000d, 303000d));
+  }
+
+  @Test
+  void testSimpleWithOtherMeasure() {
+    Field ticker = new TableField(this.storeName, "ticker");
+    Field riskType = new TableField(this.storeName, "riskType");
+    Field value = new TableField(this.storeName, "value");
+    Field date = new TableField(this.storeName, "date");
+
+    Measure vector = new VectorAggMeasure("vector", value, SUM, date);
+    QueryDto query = Query
+            .from(this.storeName)
+            .select(List.of(ticker, riskType), List.of(vector, CountMeasure.INSTANCE))
+            .rollup(List.of(ticker, riskType))
+            .build();
+    Table result = this.executor.executeQuery(query);
+    Assertions.assertThat(result.headers().stream().map(Header::name))
+            .containsExactly(ticker.name(), riskType.name(), vector.alias(), CountMeasure.ALIAS);
+    List<List<Object>> points = List.of(
+            List.of(GRAND_TOTAL, GRAND_TOTAL),
+            List.of("MMM", TOTAL),
+            List.of("MMM", "EquityReturn"),
+            List.of("MMM", "FXReturn"),
+            List.of("MMM", "TotalReturn"),
+            List.of("VBLAX", TOTAL),
+            List.of("VBLAX", "EquityReturn"),
+            List.of("VBLAX", "FXReturn"),
+            List.of("VBLAX", "TotalReturn"));
+    List<List<Number>> expectedVectors = List.of(
+            List.of(6006d, 33033d, 303303d),
+            List.of(6d, 33d, 303d),
+            List.of(3.0, 12.0, 102.0),
+            List.of(2.0, 11.0, 101.0),
+            List.of(1.0, 10.0, 100.0),
+            List.of(6000d, 33000d, 303000d),
+            List.of(3000.0, 12000.0, 102000.0),
+            List.of(2000.0, 11000.0, 101000.0),
+            List.of(1000.0, 10000.0, 100000.0));
+    assertVectorValues((ColumnarTable) result, vector, points, expectedVectors);
+    List<Number> expectedValues = List.of(18L, 9L, 3L, 3L, 3L, 9L, 3L, 3L, 3L);
+    assertValues((ColumnarTable) result, CountMeasure.INSTANCE, points, expectedValues);
+  }
+
+  @Test
+  void testCache() {
+    Field ticker = new TableField(this.storeName, "ticker");
+    Field riskType = new TableField(this.storeName, "riskType");
+    Field value = new TableField(this.storeName, "value");
+    Field date = new TableField(this.storeName, "date");
+
+    Measure vector = new VectorAggMeasure("vector", value, SUM, date);
+    QueryDto query = Query
+            .from(this.storeName)
+            .select(List.of(ticker, riskType), List.of(vector))
+            .rollup(List.of(ticker, riskType))
+            .build();
+
+    Runnable r = () -> {
+      Table result = this.executor.executeQuery(query);
+      Assertions.assertThat(result.headers().stream().map(Header::name))
+              .containsExactly(ticker.name(), riskType.name(), vector.alias());
+      List<List<Object>> points = List.of(
+              List.of(GRAND_TOTAL, GRAND_TOTAL),
+              List.of("MMM", TOTAL),
+              List.of("MMM", "EquityReturn"),
+              List.of("MMM", "FXReturn"),
+              List.of("MMM", "TotalReturn"),
+              List.of("VBLAX", TOTAL),
+              List.of("VBLAX", "EquityReturn"),
+              List.of("VBLAX", "FXReturn"),
+              List.of("VBLAX", "TotalReturn"));
+      List<List<Number>> expectedVectors = List.of(
+              List.of(6006d, 33033d, 303303d),
+              List.of(6d, 33d, 303d),
+              List.of(3.0, 12.0, 102.0),
+              List.of(2.0, 11.0, 101.0),
+              List.of(1.0, 10.0, 100.0),
+              List.of(6000d, 33000d, 303000d),
+              List.of(3000.0, 12000.0, 102000.0),
+              List.of(2000.0, 11000.0, 101000.0),
+              List.of(1000.0, 10000.0, 100000.0));
+      assertVectorValues((ColumnarTable) result, vector, points, expectedVectors);
+    };
+
+    int hitCount = (int) this.queryCache.stats(null).hitCount;
+    int missCount = (int) this.queryCache.stats(null).missCount;
+    r.run();
+    TestUtil.assertCacheStats(this.queryCache, hitCount + 0, missCount + 7);
+
+    r.run();
+    TestUtil.assertCacheStats(this.queryCache, hitCount + 6, missCount + 7);
+  }
+
+  private void assertVectorValues(ColumnarTable result, Measure vectorMeasure, List<List<Object>> points, List<List<Number>> expectedVectors) {
+    List<Object> aggregateValues = result.getColumnValues(vectorMeasure.alias());
+    for (int i = 0; i < points.size(); i++) {
+      ObjectArrayDictionary dictionary = result.pointDictionary.get();
+      int position = dictionary.getPosition(points.get(i).toArray());
+      Object actual = aggregateValues.get(position);
+      // SORT to have a deterministic comparison
+      List<Number> vector = new ArrayList<>(expectedVectors.get(i)).stream().sorted().toList();
+      List<Number> actualVector = new ArrayList<>(getVectorValue(actual)).stream().sorted().toList();
+      Assertions.assertThat(actualVector).containsExactlyElementsOf(vector);
+    }
+  }
+
+  private void assertValues(ColumnarTable result, Measure otherMeasure, List<List<Object>> points, List<Number> expectedValues) {
+    List<Object> aggregateValues = result.getColumnValues(otherMeasure.alias());
+    for (int i = 0; i < points.size(); i++) {
+      ObjectArrayDictionary dictionary = result.pointDictionary.get();
+      int position = dictionary.getPosition(points.get(i).toArray());
+      Object actual = aggregateValues.get(position);
+      Assertions.assertThat(actual).isEqualTo(expectedValues.get(i));
+    }
+  }
+
+  protected List<Number> getVectorValue(Object actualVector) {
+    throw new RuntimeException("not implemented");
+  }
+}
diff --git a/core/src/test/java/io/squashql/query/TestPeriodShiftProcedure.java b/core/src/test/java/io/squashql/query/TestPeriodShiftProcedure.java
index b19e163..4af9f06 100644
--- a/core/src/test/java/io/squashql/query/TestPeriodShiftProcedure.java
+++ b/core/src/test/java/io/squashql/query/TestPeriodShiftProcedure.java
@@ -1,6 +1,7 @@
 package io.squashql.query;
 
-import io.squashql.query.dto.Period;
+import io.squashql.query.compiled.CompiledPeriod;
+import io.squashql.type.AliasedTypedField;
 import org.assertj.core.api.Assertions;
 import org.eclipse.collections.api.map.primitive.MutableObjectIntMap;
 import org.eclipse.collections.impl.map.mutable.primitive.ObjectIntHashMap;
@@ -9,13 +10,11 @@ import org.junit.jupiter.api.Test;
 import java.util.Map;
 import java.util.function.BiFunction;
 
-import static io.squashql.query.TableField.tableField;
-
 public class TestPeriodShiftProcedure {
 
   @Test
   void testQuarterFromQuarterYear() {
-    Period period = new Period.Quarter(tableField(""), tableField(""));
+    CompiledPeriod period = new CompiledPeriod.Quarter(new AliasedTypedField(""), new AliasedTypedField(""));
     MutableObjectIntMap<PeriodUnit> indexByPeriodUnit = new ObjectIntHashMap<>();
     indexByPeriodUnit.put(PeriodUnit.YEAR, 0);
     indexByPeriodUnit.put(PeriodUnit.QUARTER, 1);
@@ -42,7 +41,7 @@ public class TestPeriodShiftProcedure {
 
   @Test
   void testMonthFromMonthYear() {
-    Period period = new Period.Month(tableField(""), tableField(""));
+    CompiledPeriod period = new CompiledPeriod.Month(new AliasedTypedField(""), new AliasedTypedField(""));
     MutableObjectIntMap<PeriodUnit> indexByPeriodUnit = new ObjectIntHashMap<>();
     indexByPeriodUnit.put(PeriodUnit.YEAR, 0);
     indexByPeriodUnit.put(PeriodUnit.MONTH, 1);
@@ -69,7 +68,7 @@ public class TestPeriodShiftProcedure {
 
   @Test
   void testSemesterFromSemesterYear() {
-    Period period = new Period.Semester(tableField(""), tableField(""));
+    CompiledPeriod period = new CompiledPeriod.Semester(new AliasedTypedField(""), new AliasedTypedField(""));
     MutableObjectIntMap<PeriodUnit> indexByPeriodUnit = new ObjectIntHashMap<>();
     indexByPeriodUnit.put(PeriodUnit.YEAR, 0);
     indexByPeriodUnit.put(PeriodUnit.SEMESTER, 1);
diff --git a/core/src/test/java/io/squashql/query/TestSQLTranslator.java b/core/src/test/java/io/squashql/query/TestSQLTranslator.java
index f23a2bc..7289a92 100644
--- a/core/src/test/java/io/squashql/query/TestSQLTranslator.java
+++ b/core/src/test/java/io/squashql/query/TestSQLTranslator.java
@@ -6,10 +6,7 @@ import io.squashql.query.database.DefaultQueryRewriter;
 import io.squashql.query.database.SqlUtils;
 import io.squashql.query.dto.*;
 import io.squashql.store.Store;
-import io.squashql.type.BinaryOperationTypedField;
-import io.squashql.type.ConstantTypedField;
-import io.squashql.type.TableTypedField;
-import io.squashql.type.TypedField;
+import io.squashql.type.*;
 import org.assertj.core.api.Assertions;
 import org.junit.jupiter.api.Test;
 
@@ -49,20 +46,28 @@ public class TestSQLTranslator {
         return new BinaryOperationTypedField(bf.operator, resolveField(bf.leftOperand), resolveField(bf.rightOperand), field.alias());
       } else if (field instanceof ConstantField cf) {
         return new ConstantTypedField(cf.value);
+      } else if (field instanceof AliasedField) {
+        return new AliasedTypedField(field.alias());
       }
       String[] split = field.name().split("\\.");
       if (split.length > 1) {
         String tableName = split[0];
         String fieldNameInTable = split[1];
-        return new TableTypedField(tableName, fieldNameInTable, type.apply(fieldNameInTable));
+        return new TableTypedField(tableName, fieldNameInTable, type.apply(fieldNameInTable), field.alias());
       } else {
-        return new TableTypedField(null, split[0], type.apply(split[0]));
+        return new TableTypedField(null, split[0], type.apply(split[0]), field.alias());
       }
     }
 
     DatabaseQuery toDatabaseQuery() {
       final DatabaseQuery compiled = toDatabaseQuery(getScope(), this.limit);
-      getMeasures().forEach(compiled::withMeasure);
+      // Keep the order
+      for (Measure measure : getQuery().measures) {
+        CompiledMeasure compiledMeasure = getMeasures().get(measure);
+        if (compiledMeasure != null) {
+          compiled.withMeasure(compiledMeasure);
+        }
+      }
       return compiled;
     }
 
@@ -88,7 +93,7 @@ public class TestSQLTranslator {
     return new QueryResolver(query, stores) {
       DatabaseQuery toDatabaseQuery() {
         final DatabaseQuery compiled = toDatabaseQuery(getScope(), query.limit);
-        getMeasures().forEach(compiled::withMeasure);
+        getMeasures().values().forEach(compiled::withMeasure);
         return compiled;
       }
     }.toDatabaseQuery();
@@ -172,7 +177,6 @@ public class TestSQLTranslator {
     Assertions.assertThat(translate(compileQuery(query)))
             .isEqualTo("""
                     select `scenario`, `type`,
-                     grouping(`scenario`), grouping(`type`),
                      sum(`price`) as `pnl.sum`
                      from `baseStore` group by rollup(`scenario`, `type`)
                     """.replaceAll(System.lineSeparator(), ""));
@@ -193,7 +197,6 @@ public class TestSQLTranslator {
     Assertions.assertThat(translate(compileQuery(query)))
             .isEqualTo("""
                     select `baseStore`.`scenario`, `baseStore`.`type`,
-                     grouping(`baseStore`.`scenario`), grouping(`baseStore`.`type`),
                      sum(`price`) as `pnl.sum`
                      from `baseStore` group by rollup(`baseStore`.`scenario`, `baseStore`.`type`)
                     """.replaceAll(System.lineSeparator(), ""));
@@ -210,7 +213,6 @@ public class TestSQLTranslator {
 
     Assertions.assertThat(translate(compileQuery(query)))
             .isEqualTo("select `scenario`, `type`," +
-                    " grouping(`scenario`)," +
                     " sum(`price`) as `pnl.sum`" +
                     " from `baseStore` group by `type`, rollup(`scenario`)");
   }
@@ -315,22 +317,23 @@ public class TestSQLTranslator {
   }
 
   @Test
-  void testSelectFromSelect() {
+  void testSelectFromSelectWithAlias() {
     // Kind of leaf agg. !!!
     TableDto a = new TableDto("a");
     final QueryDto subQuery = new QueryDto()
             .table(a)
-            .withColumn(tableField("c1"))
+            .withColumn(tableField("c1").as("alias_c1"))
             .withColumn(tableField("c3"))
             .withMeasure(avg("mean", "c2"));
 
     final QueryDto query = new QueryDto()
             .table(subQuery)
             .withColumn(tableField("c3")) // c3 needs to be in the subquery
+            .withMeasure(sum("sum c1", new AliasedField("alias_c1"))) // alias_c1 is a field from the subquery
             .withMeasure(sum("sum GT", "mean"))
             .withWhereCriteria(criterion("type", eq("myType")));
     Assertions.assertThat(translate(compileQuery(query)))
-            .isEqualTo("select `c3`, sum(`mean`) as `sum GT` from (select `c1`, `c3`, avg(`c2`) as `mean` from `a` group by `c1`, `c3`) where `type` = 'myType' group by `c3`");
+            .isEqualTo("select `c3`, sum(`alias_c1`) as `sum c1`, sum(`mean`) as `sum GT` from (select `c3`, `c1` AS `alias_c1`, avg(`c2`) as `mean` from `a` group by `c3`, `alias_c1`) where `type` = 'myType' group by `c3`");
   }
 
   @Test
@@ -427,7 +430,7 @@ public class TestSQLTranslator {
             List.of(a), // total a
             List.of(a, b));
     Assertions.assertThat(translate(compileQuery(query)))
-            .isEqualTo("select `a`, `b`, grouping(`a`), grouping(`b`), sum(`pnl`) as `pnl.sum` from `baseStore` group by grouping sets((), (`a`), (`a`,`b`))");
+            .isEqualTo("select `a`, `b`, sum(`pnl`) as `pnl.sum` from `baseStore` group by grouping sets((), (`a`), (`a`,`b`))");
   }
 
   @Test
diff --git a/core/src/test/java/io/squashql/table/ATestMergeTables.java b/core/src/test/java/io/squashql/table/ATestMergeTables.java
index 36566cb..6d91206 100644
--- a/core/src/test/java/io/squashql/table/ATestMergeTables.java
+++ b/core/src/test/java/io/squashql/table/ATestMergeTables.java
@@ -1,7 +1,10 @@
 package io.squashql.table;
 
-import io.squashql.query.*;
+import io.squashql.query.Header;
+import io.squashql.query.compiled.CompiledAggregatedMeasure;
 import io.squashql.query.dto.JoinType;
+import io.squashql.type.TableTypedField;
+import io.squashql.type.TypedField;
 import org.assertj.core.api.Assertions;
 import org.junit.jupiter.api.Test;
 
@@ -10,8 +13,15 @@ import java.util.Arrays;
 import java.util.List;
 import java.util.Set;
 
+import static io.squashql.query.agg.AggregationFunction.AVG;
+import static io.squashql.query.agg.AggregationFunction.SUM;
+
 abstract class ATestMergeTables {
 
+  static TypedField price = new TableTypedField(null, "price", double.class, null);
+  static CompiledAggregatedMeasure priceSum = new CompiledAggregatedMeasure("price.sum", price, SUM, null, false);
+  static CompiledAggregatedMeasure priceAvg = new CompiledAggregatedMeasure("price.avg", price, AVG, null, false);
+
   abstract JoinType getJoinType();
 
   @Test
@@ -27,7 +37,7 @@ abstract class ATestMergeTables {
             List.of(new Header("typology", String.class, false),
                     new Header("category", String.class, false),
                     new Header("price.sum", int.class, true)),
-            Set.of(new AggregatedMeasure("price.sum", "price", "sum")),
+            Set.of(priceSum),
             List.of(
                     new ArrayList<>(Arrays.asList("MDD", "MN", "MN")),
                     new ArrayList<>(Arrays.asList("C", "A", "B")),
@@ -43,7 +53,7 @@ abstract class ATestMergeTables {
             List.of(new Header("typology", String.class, false),
                     new Header("category", String.class, false),
                     new Header("price.avg", int.class, true)),
-            Set.of(new AggregatedMeasure("price.avg", "price", "avg")),
+            Set.of(priceAvg),
             List.of(
                     new ArrayList<>(Arrays.asList("MDD", "MN", "MN")),
                     new ArrayList<>(Arrays.asList("A", "A", "B")),
@@ -78,7 +88,7 @@ abstract class ATestMergeTables {
             List.of(new Header("typology", String.class, false),
                     new Header("category", String.class, false),
                     new Header("price.sum", int.class, true)),
-            Set.of(new AggregatedMeasure("price.sum", "price", "sum")),
+            Set.of(priceSum),
             List.of(
                     new ArrayList<>(Arrays.asList("MN", "MN", "MDD", "MDD")),
                     new ArrayList<>(Arrays.asList("A", "B", "A", "C")),
@@ -95,7 +105,7 @@ abstract class ATestMergeTables {
             List.of(new Header("typology", String.class, false),
                     new Header("category", String.class, false),
                     new Header("price.avg", int.class, true)),
-            Set.of(new AggregatedMeasure("price.avg", "price", "avg")),
+            Set.of(priceAvg),
             List.of(
                     new ArrayList<>(Arrays.asList("MN", "MN", "MDD", "MDD")),
                     new ArrayList<>(Arrays.asList("A", "B", "A", "C")),
@@ -129,7 +139,7 @@ abstract class ATestMergeTables {
             List.of(new Header("typology", String.class, false),
                     new Header("category", String.class, false),
                     new Header("price.sum", int.class, true)),
-            Set.of(new AggregatedMeasure("price.sum", "price", "sum")),
+            Set.of(priceSum),
             List.of(
                     new ArrayList<>(Arrays.asList("MDD", "MN", "MN")),
                     new ArrayList<>(Arrays.asList("C", "A", "B")),
@@ -147,7 +157,7 @@ abstract class ATestMergeTables {
                     new Header("category", String.class, false),
                     new Header("company", String.class, false),
                     new Header("price.avg", int.class, true)),
-            Set.of(new AggregatedMeasure("price.avg", "price", "avg")),
+            Set.of(priceAvg),
             List.of(
                     new ArrayList<>(Arrays.asList("MDD", "MN", "MN", "MN")),
                     new ArrayList<>(Arrays.asList("A", "A", "A", "B")),
@@ -182,7 +192,7 @@ abstract class ATestMergeTables {
             List.of(new Header("typology", String.class, false),
                     new Header("category", String.class, false),
                     new Header("price.sum", int.class, true)),
-            Set.of(new AggregatedMeasure("price.sum", "price", "sum")),
+            Set.of(priceSum),
             List.of(
                     new ArrayList<>(Arrays.asList("MDD", "MN", "MN")),
                     new ArrayList<>(Arrays.asList("C", "A", "B")),
@@ -200,7 +210,7 @@ abstract class ATestMergeTables {
                     new Header("category", String.class, false),
                     new Header("company", String.class, false),
                     new Header("price.avg", int.class, true)),
-            Set.of(new AggregatedMeasure("price.avg", "price", "avg")),
+            Set.of(priceAvg),
             List.of(
                     new ArrayList<>(Arrays.asList("MN", "MDD", "MN", "MN")),
                     new ArrayList<>(Arrays.asList("A", "A", "A", "B")),
@@ -239,7 +249,7 @@ abstract class ATestMergeTables {
             List.of(new Header("typology", String.class, false),
                     new Header("category", String.class, false),
                     new Header("price.sum", int.class, true)),
-            Set.of(new AggregatedMeasure("price.sum", "price", "sum")),
+            Set.of(priceSum),
             List.of(
                     new ArrayList<>(Arrays.asList("MDD", "MN", "MN")),
                     new ArrayList<>(Arrays.asList("C", "A", "B")),
@@ -257,7 +267,7 @@ abstract class ATestMergeTables {
                     new Header("category", String.class, false),
                     new Header("typology", String.class, false),
                     new Header("price.avg", int.class, true)),
-            Set.of(new AggregatedMeasure("price.avg", "price", "avg")),
+            Set.of(priceAvg),
             List.of(
                     new ArrayList<>(Arrays.asList("___total___", "AUCHAN", "LECLERC", "SUPER U")),
                     new ArrayList<>(Arrays.asList("A", "A", "A", "B")),
@@ -291,7 +301,7 @@ abstract class ATestMergeTables {
             List.of(new Header("typology", String.class, false),
                     new Header("category", String.class, false),
                     new Header("price.sum", int.class, true)),
-            Set.of(new AggregatedMeasure("price.sum", "price", "sum")),
+            Set.of(priceSum),
             List.of(
                     new ArrayList<>(Arrays.asList("MDD", "MN", "MN", "ZZ")),
                     new ArrayList<>(Arrays.asList("A", "A", "B", "B")),
@@ -309,7 +319,7 @@ abstract class ATestMergeTables {
             List.of(new Header("typology", String.class, false),
                     new Header("company", String.class, false),
                     new Header("price.avg", int.class, true)),
-            Set.of(new AggregatedMeasure("price.avg", "price", "avg")),
+            Set.of(priceAvg),
             List.of(
                     new ArrayList<>(Arrays.asList("MDD", "MN", "MN", "MN", "XX")),
                     new ArrayList<>(Arrays.asList("CARREFOUR", "___total___", "LECLERC", "SUPER U", "AUCHAN")),
@@ -345,7 +355,7 @@ abstract class ATestMergeTables {
             List.of(new Header("typology", String.class, false),
                     new Header("category", String.class, false),
                     new Header("price.sum", int.class, true)),
-            Set.of(new AggregatedMeasure("price.sum", "price", "sum")),
+            Set.of(priceSum),
             List.of(
                     new ArrayList<>(Arrays.asList("___total___", "MDD", "MDD", "MN", "MN")),
                     new ArrayList<>(Arrays.asList("___total___", "___total___", "B", "___total___", "A")),
@@ -360,7 +370,7 @@ abstract class ATestMergeTables {
     Table rightTable = new ColumnarTable(
             List.of(new Header("typology", String.class, false),
                     new Header("price.avg", int.class, true)),
-            Set.of(new AggregatedMeasure("price.avg", "price", "avg")),
+            Set.of(priceAvg),
             List.of(
                     new ArrayList<>(Arrays.asList("___total___", "MDD", "PP")),
                     new ArrayList<>(Arrays.asList(5.3, 2.3, 3.))));
@@ -393,7 +403,7 @@ abstract class ATestMergeTables {
     Table leftTable = new ColumnarTable(
             List.of(new Header("typology", String.class, false),
                     new Header("price.sum", int.class, true)),
-            Set.of(new AggregatedMeasure("price.sum", "price", "sum")),
+            Set.of(priceSum),
             List.of(
                     new ArrayList<>(Arrays.asList("___total___", "MDD", "MN", "PP")),
                     new ArrayList<>(Arrays.asList(45, 15, 12, 18))));
@@ -407,7 +417,7 @@ abstract class ATestMergeTables {
     Table rightTable = new ColumnarTable(
             List.of(new Header("category", String.class, false),
                     new Header("price.avg", int.class, true)),
-            Set.of(new AggregatedMeasure("price.avg", "price", "avg")),
+            Set.of(priceAvg),
             List.of(
                     new ArrayList<>(Arrays.asList("___total___", "A", "B")),
                     new ArrayList<>(Arrays.asList(5.3, 2.3, 3))));
diff --git a/core/src/test/java/io/squashql/table/FullJoinTestMergeTables.java b/core/src/test/java/io/squashql/table/FullJoinTestMergeTables.java
index 21b2966..d22530f 100644
--- a/core/src/test/java/io/squashql/table/FullJoinTestMergeTables.java
+++ b/core/src/test/java/io/squashql/table/FullJoinTestMergeTables.java
@@ -1,6 +1,5 @@
 package io.squashql.table;
 
-import io.squashql.query.AggregatedMeasure;
 import io.squashql.query.Header;
 import io.squashql.query.dto.JoinType;
 
@@ -31,8 +30,7 @@ class FullJoinTestMergeTables extends ATestMergeTables {
                     new Header("category", String.class, false),
                     new Header("price.sum", int.class, true),
                     new Header("price.avg", int.class, true)),
-            Set.of(new AggregatedMeasure("price.sum", "price", "sum"),
-                    new AggregatedMeasure("price.avg", "price", "avg")),
+            Set.of(priceSum, priceAvg),
             List.of(
                     new ArrayList<>(Arrays.asList("MDD", "MDD", "MN", "MN")),
                     new ArrayList<>(Arrays.asList("A", "C", "A", "B")),
@@ -55,8 +53,7 @@ class FullJoinTestMergeTables extends ATestMergeTables {
                     new Header("category", String.class, false),
                     new Header("price.sum", int.class, true),
                     new Header("price.avg", int.class, true)),
-            Set.of(new AggregatedMeasure("price.sum", "price", "sum"),
-                    new AggregatedMeasure("price.avg", "price", "avg")),
+            Set.of(priceSum, priceAvg),
             List.of(
                     new ArrayList<>(Arrays.asList("MN", "MN", "MDD", "MDD")),
                     new ArrayList<>(Arrays.asList("A", "B", "A", "C")),
@@ -83,8 +80,7 @@ class FullJoinTestMergeTables extends ATestMergeTables {
                     new Header("company", String.class, false),
                     new Header("price.sum", int.class, true),
                     new Header("price.avg", int.class, true)),
-            Set.of(new AggregatedMeasure("price.sum", "price", "sum"),
-                    new AggregatedMeasure("price.avg", "price", "avg")),
+            Set.of(priceSum, priceAvg),
             List.of(
                     new ArrayList<>(Arrays.asList("MDD", "MDD", "MN", "MN", "MN", "MN", "MN")),
                     new ArrayList<>(Arrays.asList("A", "C", "A", "A", "A", "B", "B")),
@@ -112,8 +108,7 @@ class FullJoinTestMergeTables extends ATestMergeTables {
                     new Header("company", String.class, false),
                     new Header("price.sum", int.class, true),
                     new Header("price.avg", int.class, true)),
-            Set.of(new AggregatedMeasure("price.sum", "price", "sum"),
-                    new AggregatedMeasure("price.avg", "price", "avg")),
+            Set.of(priceSum, priceAvg),
             List.of(
                     new ArrayList<>(Arrays.asList("MDD", "MN", "MN", "MN", "MN", "MDD")),
                     new ArrayList<>(Arrays.asList("C", "A", "A", "B", "B", "A")),
@@ -143,8 +138,7 @@ class FullJoinTestMergeTables extends ATestMergeTables {
                     new Header("company", String.class, false),
                     new Header("price.sum", int.class, true),
                     new Header("price.avg", int.class, true)),
-            Set.of(new AggregatedMeasure("price.sum", "price", "sum"),
-                    new AggregatedMeasure("price.avg", "price", "avg")),
+            Set.of(priceSum, priceAvg),
             List.of(
                     new ArrayList<>(Arrays.asList("MDD", "MDD", "MN", "MN", "MN", "MN", "MN", "XX", "ZZ")),
                     new ArrayList<>(
@@ -171,8 +165,7 @@ class FullJoinTestMergeTables extends ATestMergeTables {
                     new Header("category", String.class, false),
                     new Header("price.sum", int.class, true),
                     new Header("price.avg", int.class, true)),
-            Set.of(new AggregatedMeasure("price.sum", "price", "sum"),
-                    new AggregatedMeasure("price.avg", "price", "avg")),
+            Set.of(priceSum, priceAvg),
             List.of(
                     new ArrayList<>(Arrays.asList("___total___", "MDD", "MDD", "MN", "MN", "PP")),
                     new ArrayList<>(
@@ -198,8 +191,7 @@ class FullJoinTestMergeTables extends ATestMergeTables {
                     new Header("category", String.class, false),
                     new Header("price.sum", int.class, true),
                     new Header("price.avg", int.class, true)),
-            Set.of(new AggregatedMeasure("price.sum", "price", "sum"),
-                    new AggregatedMeasure("price.avg", "price", "avg")),
+            Set.of(priceSum, priceAvg),
             List.of(
                     new ArrayList<>(Arrays.asList("___total___", "___total___", "___total___", "MDD", "MN", "PP")),
                     new ArrayList<>(
diff --git a/core/src/test/java/io/squashql/table/InnerJoinTestMergeTables.java b/core/src/test/java/io/squashql/table/InnerJoinTestMergeTables.java
index a622c5e..d5b89d4 100644
--- a/core/src/test/java/io/squashql/table/InnerJoinTestMergeTables.java
+++ b/core/src/test/java/io/squashql/table/InnerJoinTestMergeTables.java
@@ -1,6 +1,5 @@
 package io.squashql.table;
 
-import io.squashql.query.AggregatedMeasure;
 import io.squashql.query.Header;
 import io.squashql.query.dto.JoinType;
 
@@ -29,8 +28,7 @@ class InnerJoinTestMergeTables extends ATestMergeTables {
                     new Header("category", String.class, false),
                     new Header("price.sum", int.class, true),
                     new Header("price.avg", int.class, true)),
-            Set.of(new AggregatedMeasure("price.sum", "price", "sum"),
-                    new AggregatedMeasure("price.avg", "price", "avg")),
+            Set.of(priceSum, priceAvg),
             List.of(
                     new ArrayList<>(Arrays.asList("MN", "MN")),
                     new ArrayList<>(Arrays.asList("A", "B")),
@@ -53,8 +51,7 @@ class InnerJoinTestMergeTables extends ATestMergeTables {
                     new Header("category", String.class, false),
                     new Header("price.sum", int.class, true),
                     new Header("price.avg", int.class, true)),
-            Set.of(new AggregatedMeasure("price.sum", "price", "sum"),
-                    new AggregatedMeasure("price.avg", "price", "avg")),
+            Set.of(priceSum, priceAvg),
             List.of(
                     new ArrayList<>(Arrays.asList("MN", "MN", "MDD", "MDD")),
                     new ArrayList<>(Arrays.asList("A", "B", "A", "C")),
@@ -79,8 +76,7 @@ class InnerJoinTestMergeTables extends ATestMergeTables {
                     new Header("company", String.class, false),
                     new Header("price.sum", int.class, true),
                     new Header("price.avg", int.class, true)),
-            Set.of(new AggregatedMeasure("price.sum", "price", "sum"),
-                    new AggregatedMeasure("price.avg", "price", "avg")),
+            Set.of(priceSum, priceAvg),
             List.of(
                     new ArrayList<>(Arrays.asList("MN", "MN", "MN", "MN", "MN")),
                     new ArrayList<>(Arrays.asList("A", "A", "A", "B", "B")),
@@ -106,8 +102,7 @@ class InnerJoinTestMergeTables extends ATestMergeTables {
                     new Header("company", String.class, false),
                     new Header("price.sum", int.class, true),
                     new Header("price.avg", int.class, true)),
-            Set.of(new AggregatedMeasure("price.sum", "price", "sum"),
-                    new AggregatedMeasure("price.avg", "price", "avg")),
+            Set.of(priceSum, priceAvg),
             List.of(
                     new ArrayList<>(Arrays.asList("MN", "MN", "MN", "MN")),
                     new ArrayList<>(Arrays.asList("A", "A", "B", "B")),
@@ -135,8 +130,7 @@ class InnerJoinTestMergeTables extends ATestMergeTables {
                     new Header("company", String.class, false),
                     new Header("price.sum", int.class, true),
                     new Header("price.avg", int.class, true)),
-            Set.of(new AggregatedMeasure("price.sum", "price", "sum"),
-                    new AggregatedMeasure("price.avg", "price", "avg")),
+            Set.of(priceSum, priceAvg),
             List.of(
                     new ArrayList<>(Arrays.asList("MDD", "MDD", "MN", "MN", "MN", "MN", "MN")),
                     new ArrayList<>(
@@ -160,8 +154,7 @@ class InnerJoinTestMergeTables extends ATestMergeTables {
                     new Header("category", String.class, false),
                     new Header("price.sum", int.class, true),
                     new Header("price.avg", int.class, true)),
-            Set.of(new AggregatedMeasure("price.sum", "price", "sum"),
-                    new AggregatedMeasure("price.avg", "price", "avg")),
+            Set.of(priceSum, priceAvg),
             List.of(
                     new ArrayList<>(Arrays.asList("___total___", "MDD", "MDD")),
                     new ArrayList<>(Arrays.asList("___total___", "___total___", "B")),
@@ -186,8 +179,7 @@ class InnerJoinTestMergeTables extends ATestMergeTables {
                     new Header("category", String.class, false),
                     new Header("price.sum", int.class, true),
                     new Header("price.avg", int.class, true)),
-            Set.of(new AggregatedMeasure("price.sum", "price", "sum"),
-                    new AggregatedMeasure("price.avg", "price", "avg")),
+            Set.of(priceSum, priceAvg),
             List.of(
                     new ArrayList<>(Arrays.asList("___total___", "___total___", "___total___", "MDD", "MN", "PP")),
                     new ArrayList<>(
diff --git a/core/src/test/java/io/squashql/table/LeftJoinTestMergeTables.java b/core/src/test/java/io/squashql/table/LeftJoinTestMergeTables.java
index 13d9d79..6ad449c 100644
--- a/core/src/test/java/io/squashql/table/LeftJoinTestMergeTables.java
+++ b/core/src/test/java/io/squashql/table/LeftJoinTestMergeTables.java
@@ -1,6 +1,5 @@
 package io.squashql.table;
 
-import io.squashql.query.AggregatedMeasure;
 import io.squashql.query.Header;
 import io.squashql.query.dto.JoinType;
 
@@ -30,8 +29,7 @@ class LeftJoinTestMergeTables extends ATestMergeTables {
                     new Header("category", String.class, false),
                     new Header("price.sum", int.class, true),
                     new Header("price.avg", int.class, true)),
-            Set.of(new AggregatedMeasure("price.sum", "price", "sum"),
-                    new AggregatedMeasure("price.avg", "price", "avg")),
+            Set.of(priceSum, priceAvg),
             List.of(
                     new ArrayList<>(Arrays.asList("MDD", "MN", "MN")),
                     new ArrayList<>(Arrays.asList("C", "A", "B")),
@@ -54,8 +52,7 @@ class LeftJoinTestMergeTables extends ATestMergeTables {
                     new Header("category", String.class, false),
                     new Header("price.sum", int.class, true),
                     new Header("price.avg", int.class, true)),
-            Set.of(new AggregatedMeasure("price.sum", "price", "sum"),
-                    new AggregatedMeasure("price.avg", "price", "avg")),
+            Set.of(priceSum, priceAvg),
             List.of(
                     new ArrayList<>(Arrays.asList("MN", "MN", "MDD", "MDD")),
                     new ArrayList<>(Arrays.asList("A", "B", "A", "C")),
@@ -81,8 +78,7 @@ class LeftJoinTestMergeTables extends ATestMergeTables {
                     new Header("company", String.class, false),
                     new Header("price.sum", int.class, true),
                     new Header("price.avg", int.class, true)),
-            Set.of(new AggregatedMeasure("price.sum", "price", "sum"),
-                    new AggregatedMeasure("price.avg", "price", "avg")),
+            Set.of(priceSum, priceAvg),
             List.of(
                     new ArrayList<>(Arrays.asList("MDD", "MN", "MN", "MN", "MN", "MN")),
                     new ArrayList<>(Arrays.asList("C", "A", "A", "A", "B", "B")),
@@ -109,8 +105,7 @@ class LeftJoinTestMergeTables extends ATestMergeTables {
                     new Header("company", String.class, false),
                     new Header("price.sum", int.class, true),
                     new Header("price.avg", int.class, true)),
-            Set.of(new AggregatedMeasure("price.sum", "price", "sum"),
-                    new AggregatedMeasure("price.avg", "price", "avg")),
+            Set.of(priceSum, priceAvg),
             List.of(
                     new ArrayList<>(Arrays.asList("MDD", "MN", "MN", "MN", "MN")),
                     new ArrayList<>(Arrays.asList("C", "A", "A", "B", "B")),
@@ -139,8 +134,7 @@ class LeftJoinTestMergeTables extends ATestMergeTables {
                     new Header("company", String.class, false),
                     new Header("price.sum", int.class, true),
                     new Header("price.avg", int.class, true)),
-            Set.of(new AggregatedMeasure("price.sum", "price", "sum"),
-                    new AggregatedMeasure("price.avg", "price", "avg")),
+            Set.of(priceSum, priceAvg),
             List.of(
                     new ArrayList<>(Arrays.asList("MDD", "MDD", "MN", "MN", "MN", "MN", "MN", "ZZ")),
                     new ArrayList<>(
@@ -166,8 +160,7 @@ class LeftJoinTestMergeTables extends ATestMergeTables {
                     new Header("category", String.class, false),
                     new Header("price.sum", int.class, true),
                     new Header("price.avg", int.class, true)),
-            Set.of(new AggregatedMeasure("price.sum", "price", "sum"),
-                    new AggregatedMeasure("price.avg", "price", "avg")),
+            Set.of(priceSum, priceAvg),
             List.of(
                     new ArrayList<>(Arrays.asList("___total___", "MDD", "MDD", "MN", "MN")),
                     new ArrayList<>(Arrays.asList("___total___", "___total___", "B", "___total___", "A")),
@@ -192,8 +185,7 @@ class LeftJoinTestMergeTables extends ATestMergeTables {
                     new Header("category", String.class, false),
                     new Header("price.sum", int.class, true),
                     new Header("price.avg", int.class, true)),
-            Set.of(new AggregatedMeasure("price.sum", "price", "sum"),
-                    new AggregatedMeasure("price.avg", "price", "avg")),
+            Set.of(priceSum, priceAvg),
             List.of(
                     new ArrayList<>(Arrays.asList("___total___", "___total___", "___total___", "MDD", "MN", "PP")),
                     new ArrayList<>(
diff --git a/core/src/test/java/io/squashql/table/TestMergeTablesEdgeCases.java b/core/src/test/java/io/squashql/table/TestMergeTablesEdgeCases.java
index 13ccea6..66c73cc 100644
--- a/core/src/test/java/io/squashql/table/TestMergeTablesEdgeCases.java
+++ b/core/src/test/java/io/squashql/table/TestMergeTablesEdgeCases.java
@@ -1,6 +1,5 @@
 package io.squashql.table;
 
-import io.squashql.query.AggregatedMeasure;
 import io.squashql.query.Header;
 import io.squashql.query.dto.JoinType;
 import org.assertj.core.api.Assertions;
@@ -12,6 +11,8 @@ import java.util.Arrays;
 import java.util.List;
 import java.util.Set;
 
+import static io.squashql.table.ATestMergeTables.priceSum;
+
 class TestMergeTablesEdgeCases {
 
   @Test
@@ -28,7 +29,7 @@ class TestMergeTablesEdgeCases {
             List.of(new Header("typology", String.class, false),
                     new Header("category", String.class, false),
                     new Header("price.sum", int.class, true)),
-            Set.of(new AggregatedMeasure("price.sum", "price", "sum")),
+            Set.of(priceSum),
             List.of(
                     new ArrayList<>(Arrays.asList("MN", "MN", "MDD", "MDD")),
                     new ArrayList<>(Arrays.asList("A", "B", "A", "C")),
@@ -54,7 +55,7 @@ class TestMergeTablesEdgeCases {
     Table leftTable = new ColumnarTable(
             List.of(new Header("typology", String.class, false),
                     new Header("price.sum", int.class, true)),
-            Set.of(new AggregatedMeasure("price.sum", "price", "sum")),
+            Set.of(priceSum),
             List.of(
                     new ArrayList<>(Arrays.asList("MN", "MDD")),
                     new ArrayList<>(Arrays.asList(20, 12))));
@@ -68,7 +69,7 @@ class TestMergeTablesEdgeCases {
     Table rightTable = new ColumnarTable(
             List.of(new Header("category", String.class, false),
                     new Header("price.sum", int.class, true)),
-            Set.of(new AggregatedMeasure("price.sum", "price", "sum")),
+            Set.of(priceSum),
             List.of(
                     new ArrayList<>(Arrays.asList("A", "B", "C")),
                     new ArrayList<>(Arrays.asList(2.3, 3, 5))));
diff --git a/core/src/test/java/io/squashql/table/TestMergeThreeTables.java b/core/src/test/java/io/squashql/table/TestMergeThreeTables.java
index c914c8a..e80ad54 100644
--- a/core/src/test/java/io/squashql/table/TestMergeThreeTables.java
+++ b/core/src/test/java/io/squashql/table/TestMergeThreeTables.java
@@ -1,8 +1,9 @@
 package io.squashql.table;
 
-import io.squashql.query.AggregatedMeasure;
 import io.squashql.query.Header;
+import io.squashql.query.compiled.CompiledAggregatedMeasure;
 import io.squashql.query.dto.JoinType;
+import io.squashql.type.AliasedTypedField;
 import org.assertj.core.api.Assertions;
 import org.junit.jupiter.api.Test;
 
@@ -11,11 +12,18 @@ import java.util.Arrays;
 import java.util.List;
 import java.util.Set;
 
+import static io.squashql.query.agg.AggregationFunction.AVG;
+import static io.squashql.query.agg.AggregationFunction.SUM;
 import static io.squashql.query.database.SQLTranslator.TOTAL_CELL;
 import static io.squashql.table.ATestMergeTables.orderRows;
 
 class TestMergeThreeTables {
 
+  static CompiledAggregatedMeasure turnover = new CompiledAggregatedMeasure("Turnover", new AliasedTypedField("unit_turnover"), SUM, null, false);
+  static CompiledAggregatedMeasure margin = new CompiledAggregatedMeasure("Margin", new AliasedTypedField("unit_margin"), SUM, null, false);
+  static CompiledAggregatedMeasure priceVariation = new CompiledAggregatedMeasure("PriceVariation", new AliasedTypedField("price_variation"), AVG, null, false);
+  static CompiledAggregatedMeasure priceIndex = new CompiledAggregatedMeasure("PriceIndex", new AliasedTypedField("price_index"), AVG, null, false);
+
   @Test
   void mergeThreeTables() {
     /*
@@ -30,8 +38,7 @@ class TestMergeThreeTables {
             List.of(new Header("typology", String.class, false),
                     new Header("Turnover", double.class, true),
                     new Header("Margin", double.class, true)),
-            Set.of(new AggregatedMeasure("Turnover", "unit_turnover", "sum"),
-                    new AggregatedMeasure("Margin", "unit_margin", "sum")),
+            Set.of(turnover, margin),
             List.of(
                     new ArrayList<>(Arrays.asList(TOTAL_CELL, "MDD", "MN", "PP")),
                     new ArrayList<>(Arrays.asList(2950, 1000, 2500, 450)),
@@ -50,7 +57,7 @@ class TestMergeThreeTables {
             List.of(new Header("typology", String.class, false),
                     new Header("category", String.class, false),
                     new Header("PriceVariation", double.class, true)),
-            Set.of(new AggregatedMeasure("PriceVariation", "price_variation", "avg")),
+            Set.of(priceVariation),
             List.of(
                     new ArrayList<>(Arrays.asList(TOTAL_CELL, "MDD", "MDD", "MN", "MN", "MN")),
                     new ArrayList<>(Arrays.asList(TOTAL_CELL, TOTAL_CELL, "A", TOTAL_CELL, "B", "C")),
@@ -67,7 +74,7 @@ class TestMergeThreeTables {
     Table table3 = new ColumnarTable(
             List.of(new Header("company", String.class, false),
                     new Header("PriceIndex", double.class, true)),
-            Set.of(new AggregatedMeasure("PriceIndex", "price_index", "avg")),
+            Set.of(priceIndex),
             List.of(
                     new ArrayList<>(Arrays.asList(TOTAL_CELL, "CARREFOUR", "LECLERC", "SUPER U")),
                     new ArrayList<>(Arrays.asList(101.5, 99.27, 105.1, 101))));
@@ -95,10 +102,7 @@ class TestMergeThreeTables {
                     new Header("Margin", double.class, true),
                     new Header("PriceVariation", double.class, true),
                     new Header("PriceIndex", double.class, true)),
-            Set.of(new AggregatedMeasure("Turnover", "unit_turnover", "sum"),
-                    new AggregatedMeasure("Margin", "unit_margin", "sum"),
-                    new AggregatedMeasure("PriceVariation", "price_variation", "avg"),
-                    new AggregatedMeasure("PriceIndex", "price_index", "avg")),
+            Set.of(turnover, margin, priceVariation, priceIndex),
             List.of(
                     new ArrayList<>(
                             Arrays.asList(TOTAL_CELL, TOTAL_CELL, TOTAL_CELL, TOTAL_CELL, "MDD", "MDD",
@@ -130,8 +134,7 @@ class TestMergeThreeTables {
             List.of(new Header("typology", String.class, false),
                     new Header("Turnover", double.class, true),
                     new Header("Margin", double.class, true)),
-            Set.of(new AggregatedMeasure("Turnover", "unit_turnover", "sum"),
-                    new AggregatedMeasure("Margin", "unit_margin", "sum")),
+            Set.of(turnover, margin),
             List.of(
                     new ArrayList<>(Arrays.asList("MN", "MDD", "PP", TOTAL_CELL)),
                     new ArrayList<>(Arrays.asList(2500, 1000, 450, 2950)),
@@ -150,7 +153,7 @@ class TestMergeThreeTables {
             List.of(new Header("PriceVariation", double.class, true),
                     new Header("category", String.class, false),
                     new Header("typology", String.class, false)),
-            Set.of(new AggregatedMeasure("PriceVariation", "price_variation", "avg")),
+            Set.of(priceVariation),
             List.of(
                     new ArrayList<>(Arrays.asList(0.09, -0.01, 0.02, -0.05, 0.15, 0.15)),
                     new ArrayList<>(Arrays.asList(TOTAL_CELL, TOTAL_CELL, "B", "C", TOTAL_CELL, "A")),
@@ -167,7 +170,7 @@ class TestMergeThreeTables {
     Table table3 = new ColumnarTable(
             List.of(new Header("company", String.class, false),
                     new Header("PriceIndex", double.class, true)),
-            Set.of(new AggregatedMeasure("PriceIndex", "price_index", "avg")),
+            Set.of(priceIndex),
             List.of(
                     new ArrayList<>(Arrays.asList(TOTAL_CELL, "CARREFOUR", "LECLERC", "SUPER U")),
                     new ArrayList<>(Arrays.asList(101.5, 99.27, 105.1, 101))));
@@ -195,10 +198,7 @@ class TestMergeThreeTables {
                     new Header("Margin", double.class, true),
                     new Header("PriceVariation", double.class, true),
                     new Header("PriceIndex", double.class, true)),
-            Set.of(new AggregatedMeasure("Turnover", "unit_turnover", "sum"),
-                    new AggregatedMeasure("Margin", "unit_margin", "sum"),
-                    new AggregatedMeasure("PriceVariation", "price_variation", "avg"),
-                    new AggregatedMeasure("PriceIndex", "price_index", "avg")),
+            Set.of(turnover, margin, priceVariation, priceIndex),
             List.of(
                     new ArrayList<>(
                             Arrays.asList(TOTAL_CELL, TOTAL_CELL, TOTAL_CELL, TOTAL_CELL, "MDD", "MDD",
diff --git a/core/src/test/java/io/squashql/table/TestTable.java b/core/src/test/java/io/squashql/table/TestTable.java
index fb92a13..21e74b7 100644
--- a/core/src/test/java/io/squashql/table/TestTable.java
+++ b/core/src/test/java/io/squashql/table/TestTable.java
@@ -1,7 +1,8 @@
 package io.squashql.table;
 
-import io.squashql.query.AggregatedMeasure;
 import io.squashql.query.Header;
+import io.squashql.query.compiled.CompiledAggregatedMeasure;
+import io.squashql.type.AliasedTypedField;
 import org.assertj.core.api.Assertions;
 import org.junit.jupiter.api.Test;
 
@@ -9,6 +10,7 @@ import java.util.Arrays;
 import java.util.List;
 import java.util.Set;
 
+import static io.squashql.query.agg.AggregationFunction.AVG;
 import static io.squashql.table.ATestMergeTables.orderRows;
 
 public class TestTable {
@@ -18,7 +20,7 @@ public class TestTable {
     Header pop = new Header("population.avg", double.class, true);
     Header city = new Header("city", String.class, false);
     Header country = new Header("country", String.class, false);
-    AggregatedMeasure popAvg = new AggregatedMeasure("population.avg", "population", "avg");
+    CompiledAggregatedMeasure popAvg = new CompiledAggregatedMeasure("population.avg", new AliasedTypedField("population"), AVG, null, false);
     ColumnarTable table = new ColumnarTable(
             List.of(country, city, pop),
             Set.of(popAvg),
@@ -28,7 +30,7 @@ public class TestTable {
                     Arrays.asList(1d, 2d, 3d, 4d)));
     // Make sure the order of rows in table 2 is different.
     Header emission = new Header("co2emission.avg", double.class, true);
-    AggregatedMeasure emissionAvg = new AggregatedMeasure("co2emission.avg", "co2emission", "avg");
+    CompiledAggregatedMeasure emissionAvg = new CompiledAggregatedMeasure("co2emission.avg", new AliasedTypedField("co2emission"), AVG, null, false);
     ColumnarTable from = new ColumnarTable(
             List.of(country, city, emission),
             Set.of(emissionAvg),
@@ -50,4 +52,27 @@ public class TestTable {
     table.transferAggregates(from, emissionAvg);
     Assertions.assertThat(orderRows(table)).containsExactlyInAnyOrderElementsOf(orderRows(result));
   }
+
+  @Test
+  void testIncorrectTransferAggregates() {
+    Header pop = new Header("population.avg", double.class, true);
+    Header city = new Header("city", String.class, false);
+    Header country = new Header("country", String.class, false);
+    CompiledAggregatedMeasure popAvg = new CompiledAggregatedMeasure("population.avg", new AliasedTypedField("population"), AVG, null, false);
+    ColumnarTable table = new ColumnarTable(
+            List.of(country, city, pop),
+            Set.of(popAvg),
+            List.of());
+    // Make sure the order of rows in table 2 is different.
+    Header emission = new Header("co2emission.avg", double.class, true);
+    CompiledAggregatedMeasure emissionAvg = new CompiledAggregatedMeasure("co2emission.avg", new AliasedTypedField("co2emission"), AVG, null, false);
+    ColumnarTable from = new ColumnarTable(
+            List.of(country, emission),
+            Set.of(emissionAvg),
+            List.of());
+
+    Assertions.assertThatThrownBy(() -> table.transferAggregates(from, emissionAvg))
+            .isInstanceOf(IllegalArgumentException.class)
+            .hasMessageContaining("does not match the headers of the destination table");
+  }
 }
diff --git a/core/src/test/java/io/squashql/template/ClassTemplateGenerator.java b/core/src/test/java/io/squashql/template/ClassTemplateGenerator.java
index 8708857..1185633 100644
--- a/core/src/test/java/io/squashql/template/ClassTemplateGenerator.java
+++ b/core/src/test/java/io/squashql/template/ClassTemplateGenerator.java
@@ -49,7 +49,8 @@ public class ClassTemplateGenerator {
     String prefix = "Test" + testClassType.className;
     List<Path> classGenerated = new ArrayList<>();
     for (ClassPath.ClassInfo parentTestClass : parentTestClasses) {
-      String classSuffix = parentTestClass.getSimpleName().replace("ATest", "");
+      String classSuffix = parentTestClass.getSimpleName().replace("ATest" + testClassType.className, ""); // start with specific parent class
+      classSuffix = classSuffix.replace("ATest", "");
       String fileName = prefix + classSuffix + ".java";
       Path path = Paths.get(rootTestClasses.getAbsolutePath(), fileName);
       classGenerated.add(path);
diff --git a/core/src/test/java/io/squashql/util/TestUtil.java b/core/src/test/java/io/squashql/util/TestUtil.java
index 62bb3cb..28abef1 100644
--- a/core/src/test/java/io/squashql/util/TestUtil.java
+++ b/core/src/test/java/io/squashql/util/TestUtil.java
@@ -3,6 +3,8 @@ package io.squashql.util;
 import com.google.common.collect.ImmutableList;
 import io.squashql.jackson.JacksonUtil;
 import io.squashql.query.*;
+import io.squashql.query.compiled.CompiledMeasure;
+import io.squashql.query.dto.CacheStatsDto;
 import io.squashql.table.ColumnarTable;
 import io.squashql.table.RowTable;
 import io.squashql.table.Table;
@@ -23,6 +25,21 @@ import java.util.stream.Collectors;
 
 public class TestUtil {
 
+  public static void assertCacheStats(GlobalCache cache, int hitCount, int missCount) {
+    CacheStatsDto stats = cache.stats(null);
+    assertCacheStats(stats, hitCount, missCount);
+  }
+
+  public static void assertCacheStats(GlobalCache cache, int hitCount, int missCount, SquashQLUser user) {
+    CacheStatsDto stats = cache.stats(user);
+    assertCacheStats(stats, hitCount, missCount);
+  }
+
+  public static void assertCacheStats(CacheStatsDto stats, int hitCount, int missCount) {
+    Assertions.assertThat(stats.hitCount).isEqualTo(hitCount);
+    Assertions.assertThat(stats.missCount).isEqualTo(missCount);
+  }
+
   public static ThrowableAssert<Throwable> assertThatThrownBy(ThrowableAssert.ThrowingCallable shouldRaiseThrowable) {
     try {
       shouldRaiseThrowable.call();
@@ -77,7 +94,7 @@ public class TestUtil {
     return content.toString();
   }
 
-  public static ColumnarTable convert(RowTable rowTable, Set<Measure> measures) {
+  public static ColumnarTable convert(RowTable rowTable, Set<CompiledMeasure> measures) {
     List<List<Object>> values = new ArrayList<>(rowTable.headers().size());
     for (int i = 0; i < rowTable.headers().size(); i++) {
       values.add(new ArrayList<>());
@@ -88,7 +105,7 @@ public class TestUtil {
       }
     });
     List<Header> headers = new ArrayList<>();
-    Set<String> measureNames = measures.stream().map(Measure::alias).collect(Collectors.toSet());
+    Set<String> measureNames = measures.stream().map(CompiledMeasure::alias).collect(Collectors.toSet());
     for (Header header : rowTable.headers()) {
       if (measureNames.contains(header.name())) {
         headers.add(new Header(header.name(), header.type(), true));
diff --git a/duckdb/src/test/java/io/squashql/TestDuckDBDataLoader.java b/duckdb/src/test/java/io/squashql/TestDuckDBDataLoader.java
index b766955..b1e9197 100644
--- a/duckdb/src/test/java/io/squashql/TestDuckDBDataLoader.java
+++ b/duckdb/src/test/java/io/squashql/TestDuckDBDataLoader.java
@@ -1,10 +1,12 @@
 package io.squashql;
 
 import io.squashql.query.*;
+import io.squashql.query.compiled.CompiledAggregatedMeasure;
 import io.squashql.query.database.DuckDBQueryEngine;
 import io.squashql.table.ColumnarTable;
 import io.squashql.table.Table;
 import io.squashql.transaction.DuckDBDataLoader;
+import io.squashql.type.AliasedTypedField;
 import org.assertj.core.api.Assertions;
 import org.junit.jupiter.api.Test;
 
@@ -21,7 +23,7 @@ public class TestDuckDBDataLoader {
             List.of(new Header("typology", String.class, false),
                     new Header("category", String.class, false),
                     new Header("price.avg", int.class, true)),
-            Set.of(new AggregatedMeasure("price.avg", "price", "avg")),
+            Set.of(new CompiledAggregatedMeasure("price.avg", new AliasedTypedField("price"), "avg", null, false)),
             List.of(
                     new ArrayList<>(Arrays.asList("MN", "MN", "MDD", "MDD")),
                     new ArrayList<>(Arrays.asList("A", "B", "A", "C")),
diff --git a/duckdb/src/test/java/io/squashql/query/ATestDuckDBVectorAggregation.java b/duckdb/src/test/java/io/squashql/query/ATestDuckDBVectorAggregation.java
new file mode 100644
index 0000000..8267dba
--- /dev/null
+++ b/duckdb/src/test/java/io/squashql/query/ATestDuckDBVectorAggregation.java
@@ -0,0 +1,27 @@
+package io.squashql.query;
+
+import io.squashql.TestClass;
+import org.duckdb.DuckDBArray;
+
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.List;
+
+@TestClass
+public abstract class ATestDuckDBVectorAggregation extends ATestVectorAggregation {
+
+  @Override
+  protected List<Number> getVectorValue(Object actualVector) {
+    DuckDBArray v = (DuckDBArray) actualVector;
+    try {
+      Object[] a = (Object[]) v.getArray();
+      List<Number> l = new ArrayList<>();
+      for (int i = 0; i < a.length; i++) {
+        l.add((Number) a[i]);
+      }
+      return l;
+    } catch (SQLException e) {
+      throw new RuntimeException(e);
+    }
+  }
+}
diff --git a/duckdb/src/test/java/io/squashql/query/TestDuckDBVectorAggregation.java b/duckdb/src/test/java/io/squashql/query/TestDuckDBVectorAggregation.java
new file mode 100644
index 0000000..e6ebad0
--- /dev/null
+++ b/duckdb/src/test/java/io/squashql/query/TestDuckDBVectorAggregation.java
@@ -0,0 +1,35 @@
+package io.squashql.query;
+
+import io.squashql.DuckDBDatastore;
+import io.squashql.query.database.DuckDBQueryEngine;
+import io.squashql.query.database.QueryEngine;
+import io.squashql.store.Datastore;
+import io.squashql.transaction.DataLoader;
+import io.squashql.transaction.DuckDBDataLoader;
+
+/**
+ * Do not edit this class, it has been generated automatically by {@link io.squashql.template.DuckDBClassTemplateGenerator}.
+ */
+public class TestDuckDBVectorAggregation extends ATestDuckDBVectorAggregation {
+
+  @Override
+  protected QueryEngine createQueryEngine(Datastore datastore) {
+    return new DuckDBQueryEngine((DuckDBDatastore) datastore);
+  }
+
+  @Override
+  protected Datastore createDatastore() {
+    return new DuckDBDatastore();
+  }
+
+  @Override
+  protected DataLoader createDataLoader() {
+    return new DuckDBDataLoader((DuckDBDatastore) this.datastore);
+  }
+
+  @Override
+  protected void createTables() {
+    DuckDBDataLoader tm = (DuckDBDataLoader) this.tm;
+    this.fieldsByStore.forEach(tm::createOrReplaceTable);
+  }
+}
diff --git a/server/src/test/java/io/squashql/client/HttpClientQuerierTest.java b/server/src/test/java/io/squashql/client/HttpClientQuerierTest.java
index fa66272..72206cd 100644
--- a/server/src/test/java/io/squashql/client/HttpClientQuerierTest.java
+++ b/server/src/test/java/io/squashql/client/HttpClientQuerierTest.java
@@ -66,7 +66,8 @@ public class HttpClientQuerierTest {
     assertQuery(response.table);
     Assertions.assertThat(response.metadata).containsExactly(
             new MetadataItem(SCENARIO_FIELD_NAME, SCENARIO_FIELD_NAME, String.class),
-            new MetadataItem("qs", "sum(quantity)", long.class));
+            new MetadataItem("qs", "qs", long.class));
+//            new MetadataItem("qs", "sum(quantity)", long.class));
 
     Assertions.assertThat(response.debug.cache).isNotNull();
   }
@@ -91,8 +92,10 @@ public class HttpClientQuerierTest {
     Assertions.assertThat(response.table.columns).containsExactly(SCENARIO_FIELD_NAME, "qs", "qa");
     Assertions.assertThat(response.metadata).containsExactly(
             new MetadataItem(SCENARIO_FIELD_NAME, SCENARIO_FIELD_NAME, String.class),
-            new MetadataItem("qs", "sum(quantity)", long.class),
-            new MetadataItem("qa", "avg(quantity)", double.class));
+            new MetadataItem("qs", "qs", long.class),
+            new MetadataItem("qa", "qa", double.class));
+//            new MetadataItem("qs", "sum(quantity)", long.class),
+//            new MetadataItem("qa", "avg(quantity)", double.class));
 
     Assertions.assertThat(response.debug).isNull();
   }
diff --git a/server/src/test/java/io/squashql/spring/web/rest/QueryControllerTest.java b/server/src/test/java/io/squashql/spring/web/rest/QueryControllerTest.java
index 06e0bd1..67711db 100644
--- a/server/src/test/java/io/squashql/spring/web/rest/QueryControllerTest.java
+++ b/server/src/test/java/io/squashql/spring/web/rest/QueryControllerTest.java
@@ -84,9 +84,13 @@ public class QueryControllerTest {
               Assertions.assertThat(queryResult.metadata).containsExactly(
                       new MetadataItem(SCENARIO_FIELD_NAME, SCENARIO_FIELD_NAME, String.class),
                       new MetadataItem("ean", "ean", String.class),
-                      new MetadataItem("capdv", "sum(capdv)", double.class),
-                      new MetadataItem("capdv_concurrents", "sum(competitor_price * quantity)", double.class),
-                      new MetadataItem("indice_prix", "sum(capdv) / sum(competitor_price * quantity)", double.class)
+                      // Expression of measures is deactivate for now.
+                      // new MetadataItem("capdv", "sum(capdv)", double.class),
+                      // new MetadataItem("capdv_concurrents", "sum(competitor_price * quantity)", double.class),
+                      // new MetadataItem("indice_prix", "sum(capdv) / sum(competitor_price * quantity)", double.class)
+                      new MetadataItem("capdv", "capdv", double.class),
+                      new MetadataItem("capdv_concurrents", "capdv_concurrents", double.class),
+                      new MetadataItem("indice_prix", "indice_prix", double.class)
               );
 
               Assertions.assertThat(queryResult.debug.cache).isNotNull();
diff --git a/snowflake/src/test/java/io/squashql/query/ATestSnowflakeVectorAggregation.java b/snowflake/src/test/java/io/squashql/query/ATestSnowflakeVectorAggregation.java
new file mode 100644
index 0000000..2c0b1bd
--- /dev/null
+++ b/snowflake/src/test/java/io/squashql/query/ATestSnowflakeVectorAggregation.java
@@ -0,0 +1,28 @@
+package io.squashql.query;
+
+import io.squashql.TestClass;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.List;
+
+@TestClass
+public abstract class ATestSnowflakeVectorAggregation extends ATestVectorAggregation {
+
+  @Override
+  protected List<Number> getVectorValue(Object actualVector) {
+    // It is a string that needs to be parsed, see https://github.com/snowflakedb/snowflake-jdbc/issues/462
+    String[] split = ((String) actualVector).replace("\n", "")
+            .replace("[", "")
+            .replace("]", "")
+            .split(",");
+    List<Number> r = new ArrayList<>();
+    for (String s : split) {
+      String trim = s.trim();
+      BigDecimal bigDecimal = new BigDecimal(trim);
+      double v = bigDecimal.doubleValue();
+      r.add(v);
+    }
+    return r;
+  }
+}
diff --git a/snowflake/src/test/java/io/squashql/query/SnowflakeTestUtil.java b/snowflake/src/test/java/io/squashql/query/SnowflakeTestUtil.java
index a218ad9..062ca72 100644
--- a/snowflake/src/test/java/io/squashql/query/SnowflakeTestUtil.java
+++ b/snowflake/src/test/java/io/squashql/query/SnowflakeTestUtil.java
@@ -4,7 +4,7 @@ import java.util.Properties;
 
 public class SnowflakeTestUtil {
 
-  public static final String jdbcUrl = System.getenv().getOrDefault("JDBC_URL", System.getProperty("snowflake.test.jdbc.url"));
+  public static final String jdbcUrl = System.getenv().getOrDefault("SNOWFLAKE_JDBC_URL", System.getProperty("snowflake.test.jdbc.url"));
   public static final String schema = "PUBLIC";
   public static final String database = "TEST";
 
diff --git a/snowflake/src/test/java/io/squashql/query/TestSnowflakeVectorAggregation.java b/snowflake/src/test/java/io/squashql/query/TestSnowflakeVectorAggregation.java
new file mode 100644
index 0000000..92b58c3
--- /dev/null
+++ b/snowflake/src/test/java/io/squashql/query/TestSnowflakeVectorAggregation.java
@@ -0,0 +1,52 @@
+package io.squashql.query;
+
+import io.squashql.SnowflakeDatastore;
+import io.squashql.query.database.QueryEngine;
+import io.squashql.query.database.SnowflakeQueryEngine;
+import io.squashql.store.Datastore;
+import io.squashql.transaction.DataLoader;
+import io.squashql.transaction.SnowflakeDataLoader;
+import org.junit.jupiter.api.AfterAll;
+
+/**
+ * Do not edit this class, it has been generated automatically by {@link io.squashql.template.SnowflakeClassTemplateGenerator}.
+ */
+public class TestSnowflakeVectorAggregation extends ATestSnowflakeVectorAggregation {
+
+  @AfterAll
+  void tearDown() {
+    SnowflakeDataLoader tm = (SnowflakeDataLoader) this.tm;
+    this.fieldsByStore.forEach((storeName, storeFields) -> tm.dropTable(storeName));
+  }
+
+  @Override
+  protected void createTables() {
+    SnowflakeDataLoader tm = (SnowflakeDataLoader) this.tm;
+    this.fieldsByStore.forEach(tm::createOrReplaceTable);
+  }
+
+  @Override
+  protected QueryEngine createQueryEngine(Datastore datastore) {
+    return new SnowflakeQueryEngine((SnowflakeDatastore) datastore);
+  }
+
+  @Override
+  protected Datastore createDatastore() {
+    return new SnowflakeDatastore(
+            SnowflakeTestUtil.jdbcUrl,
+            SnowflakeTestUtil.database,
+            SnowflakeTestUtil.schema,
+            SnowflakeTestUtil.properties
+    );
+  }
+
+  @Override
+  protected DataLoader createDataLoader() {
+    return new SnowflakeDataLoader((SnowflakeDatastore) this.datastore);
+  }
+
+  @Override
+  protected Object translate(Object o) {
+    return SnowflakeTestUtil.translate(o);
+  }
+}
diff --git a/spark/src/test/java/io/squashql/query/ATestSparkVectorAggregation.java b/spark/src/test/java/io/squashql/query/ATestSparkVectorAggregation.java
new file mode 100644
index 0000000..4ff3fd3
--- /dev/null
+++ b/spark/src/test/java/io/squashql/query/ATestSparkVectorAggregation.java
@@ -0,0 +1,16 @@
+package io.squashql.query;
+
+import io.squashql.TestClass;
+import scala.collection.JavaConverters;
+import scala.collection.mutable.WrappedArray;
+
+import java.util.List;
+
+@TestClass
+public abstract class ATestSparkVectorAggregation extends ATestVectorAggregation {
+
+  @Override
+  protected List<Number> getVectorValue(Object actualVector) {
+    return JavaConverters.seqAsJavaList(((WrappedArray.ofRef) actualVector).seq());
+  }
+}
diff --git a/spark/src/test/java/io/squashql/query/TestSparkPeriodComparison.java b/spark/src/test/java/io/squashql/query/TestSparkPeriodComparison.java
index 2b2ae7a..3dafb8a 100644
--- a/spark/src/test/java/io/squashql/query/TestSparkPeriodComparison.java
+++ b/spark/src/test/java/io/squashql/query/TestSparkPeriodComparison.java
@@ -33,24 +33,10 @@ public class TestSparkPeriodComparison extends ATestPeriodComparison {
     SparkDatastore ds = (SparkDatastore) this.datastore;
     return new SparkDataLoader(ds.spark);
   }
+
   @Override
   protected void createTables() {
     SparkDataLoader tm = (SparkDataLoader) this.tm;
     this.fieldsByStore.forEach((store, fields) -> tm.createTemporaryTable(store, fields));
   }
-
-  @Override
-  protected Object yearType(int i) {
-    return i;
-  }
-
-  @Override
-  protected Object quarterType(int i) {
-    return i;
-  }
-
-  @Override
-  protected Object monthType(int i) {
-    return i;
-  }
 }
diff --git a/spark/src/test/java/io/squashql/query/TestSparkVectorAggregation.java b/spark/src/test/java/io/squashql/query/TestSparkVectorAggregation.java
new file mode 100644
index 0000000..aaf9a70
--- /dev/null
+++ b/spark/src/test/java/io/squashql/query/TestSparkVectorAggregation.java
@@ -0,0 +1,42 @@
+package io.squashql.query;
+
+import io.squashql.SparkDatastore;
+import io.squashql.query.database.QueryEngine;
+import io.squashql.query.database.SparkQueryEngine;
+import io.squashql.store.Datastore;
+import io.squashql.transaction.DataLoader;
+import io.squashql.transaction.SparkDataLoader;
+import org.junit.jupiter.api.AfterAll;
+
+/**
+ * Do not edit this class, it has been generated automatically by {@link io.squashql.template.SparkClassTemplateGenerator}.
+ */
+public class TestSparkVectorAggregation extends ATestSparkVectorAggregation {
+
+  @AfterAll
+  void tearDown() {
+    this.fieldsByStore.keySet().forEach(storeName -> ((SparkDatastore) this.datastore).spark.catalog().dropTempView(storeName));
+  }
+
+  @Override
+  protected QueryEngine createQueryEngine(Datastore datastore) {
+    return new SparkQueryEngine((SparkDatastore) datastore);
+  }
+
+  @Override
+  protected Datastore createDatastore() {
+    return new SparkDatastore();
+  }
+
+  @Override
+  protected DataLoader createDataLoader() {
+    SparkDatastore ds = (SparkDatastore) this.datastore;
+    return new SparkDataLoader(ds.spark);
+  }
+
+  @Override
+  protected void createTables() {
+    SparkDataLoader tm = (SparkDataLoader) this.tm;
+    this.fieldsByStore.forEach((store, fields) -> tm.createTemporaryTable(store, fields));
+  }
+}