Repository: squashql/squashql
Pull request #300 (2024-04-04 13:44:47)

Single group comparison improvements
See doc https://github.com/squashql/squashql/pull/300/files#diff-10aa48f93a7e5c014cae3311fec3090dd09e74e8da1cf43536df9ab21969c3f3


Patch:
diff --git a/core/src/main/java/io/squashql/query/AComparisonExecutor.java b/core/src/main/java/io/squashql/query/AComparisonExecutor.java
index 8ce1399..2958972 100644
--- a/core/src/main/java/io/squashql/query/AComparisonExecutor.java
+++ b/core/src/main/java/io/squashql/query/AComparisonExecutor.java
@@ -22,7 +22,8 @@ public abstract class AComparisonExecutor<T extends CompiledComparisonMeasure> {
   public static final String REF_POS_FIRST = "first";
 
   protected abstract BiPredicate<Object[], Header[]> createShiftProcedure(T cm,
-                                                                         ObjectIntMap<String> indexByColumn);
+                                                                          ObjectIntMap<String> indexByColumn,
+                                                                          Table readFromTable);
 
   public List<Object> compare(
           T cm,
@@ -36,7 +37,7 @@ public abstract class AComparisonExecutor<T extends CompiledComparisonMeasure> {
         indexByColumn.put(header.name(), index++);
       }
     }
-    BiPredicate<Object[], Header[]> procedure = createShiftProcedure(cm, indexByColumn);
+    BiPredicate<Object[], Header[]> procedure = createShiftProcedure(cm, indexByColumn, readFromTable);
 
     int readFromTableColumnsCount = (int) readFromTable.headers().stream().filter(header -> !header.isMeasure()).count();
     Object[] buffer = new Object[readFromTableColumnsCount];
diff --git a/core/src/main/java/io/squashql/query/ComparisonMeasureReferencePosition.java b/core/src/main/java/io/squashql/query/ComparisonMeasureReferencePosition.java
index f6177eb..f4c5f40 100644
--- a/core/src/main/java/io/squashql/query/ComparisonMeasureReferencePosition.java
+++ b/core/src/main/java/io/squashql/query/ComparisonMeasureReferencePosition.java
@@ -22,6 +22,7 @@ public class ComparisonMeasureReferencePosition implements Measure {
   @Getter
   public Measure measure;
   public ColumnSetKey columnSetKey;
+  public List<?> elements;
   public Map<Field, String> referencePosition;
   public Period period;
   public List<Field> ancestors;
@@ -32,7 +33,7 @@ public class ComparisonMeasureReferencePosition implements Measure {
                                             @NonNull Measure measure,
                                             @NonNull Map<Field, String> referencePosition,
                                             @NonNull Period period) {
-    this(alias, comparisonMethod, null, measure, referencePosition, period, null, null, false);
+    this(alias, comparisonMethod, null, measure, referencePosition, period, null, null, null, false);
   }
 
   public ComparisonMeasureReferencePosition(@NonNull String alias,
@@ -40,14 +41,29 @@ public class ComparisonMeasureReferencePosition implements Measure {
                                             @NonNull Measure measure,
                                             @NonNull Map<Field, String> referencePosition,
                                             @NonNull ColumnSetKey columnSetKey) {
-    this(alias, comparisonMethod, null, measure, referencePosition, null, columnSetKey, null, false);
+    this(alias, comparisonMethod, null, measure, referencePosition, null, columnSetKey, null, null, false);
+  }
+
+  public ComparisonMeasureReferencePosition(@NonNull String alias,
+                                            @NonNull ComparisonMethod comparisonMethod,
+                                            @NonNull Measure measure,
+                                            @NonNull Map<Field, String> referencePosition) {
+    this(alias, comparisonMethod, null, measure, referencePosition, null, null, null, null, false);
+  }
+
+  public ComparisonMeasureReferencePosition(@NonNull String alias,
+                                            @NonNull ComparisonMethod comparisonMethod,
+                                            @NonNull Measure measure,
+                                            @NonNull Map<Field, String> referencePosition,
+                                            @NonNull List<?> elements) {
+    this(alias, comparisonMethod, null, measure, referencePosition, null, null, elements, null, false);
   }
 
   public ComparisonMeasureReferencePosition(@NonNull String alias,
                                             @NonNull ComparisonMethod comparisonMethod,
                                             @NonNull Measure measure,
                                             @NonNull List<Field> ancestors) {
-    this(alias, comparisonMethod, null, measure, null, null, null, ancestors, false);
+    this(alias, comparisonMethod, null, measure, null, null, null, null, ancestors, false);
   }
 
   public ComparisonMeasureReferencePosition(@NonNull String alias,
@@ -55,7 +71,7 @@ public class ComparisonMeasureReferencePosition implements Measure {
                                             @NonNull Measure measure,
                                             @NonNull List<Field> ancestors,
                                             boolean grandTotalAlongAncestors) {
-    this(alias, comparisonMethod, null, measure, null, null, null, ancestors, grandTotalAlongAncestors);
+    this(alias, comparisonMethod, null, measure, null, null, null, null, ancestors, grandTotalAlongAncestors);
   }
 
   /**
@@ -65,7 +81,7 @@ public class ComparisonMeasureReferencePosition implements Measure {
                                             @NonNull BiFunction<Object, Object, Object> comparisonOperator,
                                             @NonNull Measure measure,
                                             @NonNull List<Field> ancestors) {
-    this(alias, null, comparisonOperator, measure, null, null, null, ancestors, false);
+    this(alias, null, comparisonOperator, measure, null, null, null, null, ancestors, false);
   }
 
   private ComparisonMeasureReferencePosition(String alias,
@@ -75,6 +91,7 @@ public class ComparisonMeasureReferencePosition implements Measure {
                                              Map<Field, String> referencePosition,
                                              Period period,
                                              ColumnSetKey columnSetKey,
+                                             List<?> elements,
                                              List<Field> ancestors,
                                              boolean grandTotalAlongAncestors) {
     this.alias = alias;
@@ -84,6 +101,7 @@ public class ComparisonMeasureReferencePosition implements Measure {
     this.referencePosition = referencePosition;
     this.period = period;
     this.columnSetKey = columnSetKey;
+    this.elements = elements;
     this.ancestors = ancestors;
     this.grandTotalAlongAncestors = grandTotalAlongAncestors;
   }
diff --git a/core/src/main/java/io/squashql/query/GrandTotalComparisonExecutor.java b/core/src/main/java/io/squashql/query/GrandTotalComparisonExecutor.java
index 921786d..fbb4a79 100644
--- a/core/src/main/java/io/squashql/query/GrandTotalComparisonExecutor.java
+++ b/core/src/main/java/io/squashql/query/GrandTotalComparisonExecutor.java
@@ -2,6 +2,7 @@ package io.squashql.query;
 
 import io.squashql.query.compiled.CompiledGrandTotalComparisonMeasure;
 import io.squashql.query.database.SQLTranslator;
+import io.squashql.table.Table;
 import org.eclipse.collections.api.map.primitive.ObjectIntMap;
 
 import java.util.function.BiPredicate;
@@ -9,7 +10,9 @@ import java.util.function.BiPredicate;
 public class GrandTotalComparisonExecutor extends AComparisonExecutor<CompiledGrandTotalComparisonMeasure> {
 
   @Override
-  protected BiPredicate<Object[], Header[]> createShiftProcedure(CompiledGrandTotalComparisonMeasure cm, ObjectIntMap<String> indexByColumn) {
+  protected BiPredicate<Object[], Header[]> createShiftProcedure(CompiledGrandTotalComparisonMeasure cm,
+                                                                 ObjectIntMap<String> indexByColumn,
+                                                                 Table readFromTable) {
     return (row, __) -> {
       indexByColumn.forEachKeyValue((value, index) -> row[index] = SQLTranslator.TOTAL_CELL);
       return true;
diff --git a/core/src/main/java/io/squashql/query/GroupComparisonExecutor.java b/core/src/main/java/io/squashql/query/GroupComparisonExecutor.java
index a2b7609..f8cd761 100644
--- a/core/src/main/java/io/squashql/query/GroupComparisonExecutor.java
+++ b/core/src/main/java/io/squashql/query/GroupComparisonExecutor.java
@@ -3,6 +3,7 @@ package io.squashql.query;
 import io.squashql.query.compiled.CompiledGroupColumnSet;
 import io.squashql.query.compiled.CompiledComparisonMeasureReferencePosition;
 import io.squashql.query.database.SqlUtils;
+import io.squashql.table.Table;
 import io.squashql.type.TypedField;
 import org.eclipse.collections.api.map.primitive.ObjectIntMap;
 import org.eclipse.collections.api.tuple.Pair;
@@ -23,49 +24,54 @@ public class GroupComparisonExecutor extends AComparisonExecutor<CompiledCompari
   }
 
   @Override
-  protected BiPredicate<Object[], Header[]> createShiftProcedure(CompiledComparisonMeasureReferencePosition cm, ObjectIntMap<String> indexByColumn) {
-    return new ShiftProcedure(this.cSet, cm.referencePosition(), indexByColumn);
+  protected BiPredicate<Object[], Header[]> createShiftProcedure(CompiledComparisonMeasureReferencePosition cm,
+                                                                 ObjectIntMap<String> indexByColumn,
+                                                                 Table readFromTable) {
+    return new GroupComparisonShiftProcedure(this.cSet, cm.referencePosition(), indexByColumn);
   }
 
-  static class ShiftProcedure implements BiPredicate<Object[], Header[]> {
+  static class GroupComparisonShiftProcedure implements BiPredicate<Object[], Header[]> {
 
-    final List<Pair<String, Object>> transformationByColumn;
-    final ObjectIntMap<String> indexByColumn;
-    final Map<String, List<String>> valuesByGroup = new LinkedHashMap<>();
+    private final List<Pair<String, Object>> columnAndTransformations;
+    private final ObjectIntMap<String> indexByColumn;
+    private final Map<Object, List<Object>> valuesByGroup = new LinkedHashMap<>();
 
-    ShiftProcedure(CompiledGroupColumnSet cSet, Map<TypedField, String> referencePosition, ObjectIntMap<String> indexByColumn) {
+    private GroupComparisonShiftProcedure(CompiledGroupColumnSet cSet, Map<TypedField, String> referencePosition, ObjectIntMap<String> indexByColumn) {
       this.valuesByGroup.putAll(cSet.values());
       this.indexByColumn = indexByColumn;
-      this.transformationByColumn = new ArrayList<>();
+      this.columnAndTransformations = new ArrayList<>();
       // Order does matter here
-      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()))));
+      this.columnAndTransformations.add(Tuples.pair(SqlUtils.squashqlExpression(cSet.newField()), parse(referencePosition.get(cSet.newField()))));
+      this.columnAndTransformations.add(Tuples.pair(SqlUtils.squashqlExpression(cSet.field()), parse(referencePosition.get(cSet.field()))));
     }
 
     @Override
     public boolean test(Object[] row, Header[] headers) {
-      Object groupTransformation = this.transformationByColumn.get(0).getTwo();
-      int groupIndex = this.indexByColumn.getIfAbsent(this.transformationByColumn.get(0).getOne(), -1);
+      Object groupTransformation = this.columnAndTransformations.get(0).getTwo();
+      int groupIndex = this.indexByColumn.getIfAbsent(this.columnAndTransformations.get(0).getOne(), -1);
       if (groupTransformation != null) {
         throw new RuntimeException("comparison with a different group value is not yet supported");
       }
 
-      Object fieldTransformation = this.transformationByColumn.get(1).getTwo();
-      if (fieldTransformation != null) {
-        int fieldIndex = this.indexByColumn.getIfAbsent(this.transformationByColumn.get(1).getOne(), -1);
-        String b = (String) row[groupIndex];
-        List<String> values = this.valuesByGroup.get(b);
-        if (fieldTransformation instanceof Integer) {
-          String fieldValue = (String) row[fieldIndex];
-          int index = values.indexOf(fieldValue);
-          row[fieldIndex] = values.get(Math.max(index + (int) fieldTransformation, 0));
-        } else if (fieldTransformation.equals(REF_POS_FIRST)) {
-          row[fieldIndex] = values.get(0);
-        } else {
-          throw new RuntimeException("not supported");
-        }
-      }
+      String b = (String) row[groupIndex];
+      List<Object> values = this.valuesByGroup.get(b);
+      String column = this.columnAndTransformations.get(1).getOne();
+      Object fieldTransformation = this.columnAndTransformations.get(1).getTwo();
+      int fieldIndex = this.indexByColumn.getIfAbsent(column, -1);
+      shift(fieldIndex, fieldTransformation, values, row);
       return true;
     }
+
+    static void shift(int fieldIndex, Object fieldTransformation, List<Object> values, Object[] row) {
+      if (fieldTransformation instanceof Integer) {
+        String fieldValue = (String) row[fieldIndex];
+        int index = values.indexOf(fieldValue);
+        row[fieldIndex] = values.get(Math.max(index + (int) fieldTransformation, 0));
+      } else if (fieldTransformation.equals(REF_POS_FIRST)) {
+        row[fieldIndex] = values.get(0);
+      } else {
+        throw new RuntimeException("not supported");
+      }
+    }
   }
 }
diff --git a/core/src/main/java/io/squashql/query/GrouperExecutor.java b/core/src/main/java/io/squashql/query/GrouperExecutor.java
index d8c8601..483028f 100644
--- a/core/src/main/java/io/squashql/query/GrouperExecutor.java
+++ b/core/src/main/java/io/squashql/query/GrouperExecutor.java
@@ -64,16 +64,16 @@ public class GrouperExecutor {
   }
 
   private static Function<Object[], List<Object[]>> createGrouper(GroupColumnSetDto groupColumnSetDto) {
-    Map<String, List<String>> groupsByValue = new HashMap<>();
-    for (Map.Entry<String, List<String>> value : groupColumnSetDto.values.entrySet()) {
-      for (String v : value.getValue()) {
+    Map<Object, List<Object>> groupsByValue = new HashMap<>();
+    for (Map.Entry<Object, List<Object>> value : groupColumnSetDto.values.entrySet()) {
+      for (Object v : value.getValue()) {
         groupsByValue
                 .computeIfAbsent(v, k -> new ArrayList<>())
                 .add(value.getKey());
       }
     }
     Function<Object[], List<Object[]>> grouper = toGroupColumnValues -> {
-      List<String> groups = groupsByValue.get(toGroupColumnValues[0]);
+      List<Object> groups = groupsByValue.get(toGroupColumnValues[0]);
       return groups == null ? Collections.emptyList()
               : groups.stream().map(b -> new Object[] {b, toGroupColumnValues[0]}).toList();
     };
diff --git a/core/src/main/java/io/squashql/query/MeasureUtils.java b/core/src/main/java/io/squashql/query/MeasureUtils.java
index 90bcad2..62254aa 100644
--- a/core/src/main/java/io/squashql/query/MeasureUtils.java
+++ b/core/src/main/java/io/squashql/query/MeasureUtils.java
@@ -85,6 +85,9 @@ public final class MeasureUtils {
     AtomicReference<CompiledCriteria> copy = new AtomicReference<>(queryScope.whereCriteria() == null ? null : CompiledCriteria.deepCopy(queryScope.whereCriteria()));
     Consumer<TypedField> criteriaRemover = field -> copy.set(removeCriteriaOnField(field, copy.get()));
     groupColumns.forEach(criteriaRemover);
+    Optional.ofNullable(cm.referencePosition())
+            // to handle ComparisonMeasure with no GroupColumn => previous member, first member...
+            .ifPresent(ref -> ref.keySet().forEach(criteriaRemover));
     Optional.ofNullable(cm.period())
             .ifPresent(p -> getColumnsForPrefetching(p).forEach(criteriaRemover));
     Set<TypedField> rollupColumns = new LinkedHashSet<>(queryScope.rollup()); // order does matter
diff --git a/core/src/main/java/io/squashql/query/ParentComparisonExecutor.java b/core/src/main/java/io/squashql/query/ParentComparisonExecutor.java
index 4f0dcb8..ca21ab1 100644
--- a/core/src/main/java/io/squashql/query/ParentComparisonExecutor.java
+++ b/core/src/main/java/io/squashql/query/ParentComparisonExecutor.java
@@ -3,6 +3,7 @@ package io.squashql.query;
 import io.squashql.query.compiled.CompiledComparisonMeasureReferencePosition;
 import io.squashql.query.database.SQLTranslator;
 import io.squashql.query.database.SqlUtils;
+import io.squashql.table.Table;
 import io.squashql.type.TypedField;
 import org.eclipse.collections.api.map.primitive.ObjectIntMap;
 
@@ -20,7 +21,9 @@ public class ParentComparisonExecutor extends AComparisonExecutor<CompiledCompar
   }
 
   @Override
-  protected BiPredicate<Object[], Header[]> createShiftProcedure(CompiledComparisonMeasureReferencePosition cm, ObjectIntMap<String> indexByColumn) {
+  protected BiPredicate<Object[], Header[]> createShiftProcedure(CompiledComparisonMeasureReferencePosition cm,
+                                                                 ObjectIntMap<String> indexByColumn,
+                                                                 Table readFromTable) {
     List<TypedField> ancestors = new ArrayList<>(this.pcm.ancestors());
     Collections.reverse(ancestors);
     return new AncestorShiftProcedure(ancestors, indexByColumn, cm.grandTotalAlongAncestors());
diff --git a/core/src/main/java/io/squashql/query/PeriodComparisonExecutor.java b/core/src/main/java/io/squashql/query/PeriodComparisonExecutor.java
index 81514ce..6779141 100644
--- a/core/src/main/java/io/squashql/query/PeriodComparisonExecutor.java
+++ b/core/src/main/java/io/squashql/query/PeriodComparisonExecutor.java
@@ -4,6 +4,7 @@ import io.squashql.query.compiled.CompiledComparisonMeasureReferencePosition;
 import io.squashql.query.compiled.CompiledPeriod;
 import io.squashql.query.database.SQLTranslator;
 import io.squashql.query.database.SqlUtils;
+import io.squashql.table.Table;
 import io.squashql.type.TypedField;
 import org.eclipse.collections.api.map.primitive.MutableObjectIntMap;
 import org.eclipse.collections.api.map.primitive.ObjectIntMap;
@@ -40,7 +41,9 @@ public class PeriodComparisonExecutor extends AComparisonExecutor<CompiledCompar
   }
 
   @Override
-  protected BiPredicate<Object[], Header[]> createShiftProcedure(CompiledComparisonMeasureReferencePosition cm, ObjectIntMap<String> indexByColumn) {
+  protected BiPredicate<Object[], Header[]> createShiftProcedure(CompiledComparisonMeasureReferencePosition cm,
+                                                                 ObjectIntMap<String> indexByColumn,
+                                                                 Table readFromTable) {
     Map<PeriodUnit, String> referencePosition = new HashMap<>();
     CompiledPeriod period = this.cmrp.period();
     Map<TypedField, PeriodUnit> mapping = mapping(period);
diff --git a/core/src/main/java/io/squashql/query/QueryExecutor.java b/core/src/main/java/io/squashql/query/QueryExecutor.java
index 85cbee0..2620929 100644
--- a/core/src/main/java/io/squashql/query/QueryExecutor.java
+++ b/core/src/main/java/io/squashql/query/QueryExecutor.java
@@ -199,13 +199,6 @@ public class QueryExecutor {
             .evictionCount(stats.evictionCount)
             .missCount(stats.missCount);
 
-    if (preparedQuery.columnSets.containsKey(GROUP)) {
-      GroupColumnSetDto columnSet = (GroupColumnSetDto) preparedQuery.columnSets.get(GROUP);
-      if (columnSet.values.size() == 1) { // only one group, no need to keep the additional column
-        result.removeColumn(SqlUtils.squashqlExpression(columnSet.newField));
-      }
-    }
-
     return result;
   }
 
diff --git a/core/src/main/java/io/squashql/query/QueryResolver.java b/core/src/main/java/io/squashql/query/QueryResolver.java
index cc3116e..0c7e22d 100644
--- a/core/src/main/java/io/squashql/query/QueryResolver.java
+++ b/core/src/main/java/io/squashql/query/QueryResolver.java
@@ -341,6 +341,7 @@ public class QueryResolver {
             m.referencePosition == null ? null : m.referencePosition.entrySet().stream().collect(Collectors.toMap(e -> resolveField(e.getKey()), Map.Entry::getValue)),
             compilePeriod(m.period),
             m.columnSetKey,
+            m.elements,
             m.ancestors == null ? null : m.ancestors.stream().map(this::resolveField).collect(Collectors.toList()),
             m.grandTotalAlongAncestors);
   }
diff --git a/core/src/main/java/io/squashql/query/SingleGroupComparisonExecutor.java b/core/src/main/java/io/squashql/query/SingleGroupComparisonExecutor.java
new file mode 100644
index 0000000..3fd4491
--- /dev/null
+++ b/core/src/main/java/io/squashql/query/SingleGroupComparisonExecutor.java
@@ -0,0 +1,54 @@
+package io.squashql.query;
+
+import io.squashql.query.compiled.CompiledComparisonMeasureReferencePosition;
+import io.squashql.query.database.SqlUtils;
+import io.squashql.table.Table;
+import io.squashql.type.TypedField;
+import io.squashql.util.CustomExplicitOrdering;
+import lombok.AllArgsConstructor;
+import org.eclipse.collections.api.map.primitive.ObjectIntMap;
+import org.eclipse.collections.api.tuple.Pair;
+import org.eclipse.collections.impl.tuple.Tuples;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.function.BiPredicate;
+
+@AllArgsConstructor
+public class SingleGroupComparisonExecutor extends AComparisonExecutor<CompiledComparisonMeasureReferencePosition> {
+
+  private final TypedField field;
+
+  @Override
+  protected BiPredicate<Object[], Header[]> createShiftProcedure(CompiledComparisonMeasureReferencePosition cm,
+                                                                 ObjectIntMap<String> indexByColumn,
+                                                                 Table readFromTable) {
+    return new ShiftProcedure(this.field, cm.referencePosition(), indexByColumn, cm.elements(), readFromTable);
+  }
+
+  private static class ShiftProcedure implements BiPredicate<Object[], Header[]> {
+
+    private final Pair<String, Object> columnAndTransformation;
+    private final ObjectIntMap<String> indexByColumn;
+    private final List<Object> values;
+
+    private ShiftProcedure(TypedField field, Map<TypedField, String> referencePosition, ObjectIntMap<String> indexByColumn, List<?> elements, Table readFromTable) {
+      this.indexByColumn = indexByColumn;
+      // Order does matter here
+      String column = SqlUtils.squashqlExpression(field);
+      this.columnAndTransformation = Tuples.pair(column, parse(referencePosition.get(field)));
+      Object[] array = readFromTable.getColumnValues(column).toArray(new Object[0]);
+      Arrays.sort(array, elements == null ? null : new CustomExplicitOrdering(elements));
+      this.values = Arrays.asList(array);
+    }
+
+    @Override
+    public boolean test(Object[] row, Header[] headers) {
+      Object fieldTransformation = this.columnAndTransformation.getTwo();
+      int fieldIndex = this.indexByColumn.getIfAbsent(this.columnAndTransformation.getOne(), -1);
+      GroupComparisonExecutor.GroupComparisonShiftProcedure.shift(fieldIndex, fieldTransformation, this.values, row);
+      return true;
+    }
+  }
+}
diff --git a/core/src/main/java/io/squashql/query/compiled/CompiledComparisonMeasureReferencePosition.java b/core/src/main/java/io/squashql/query/compiled/CompiledComparisonMeasureReferencePosition.java
index 368458c..677622f 100644
--- a/core/src/main/java/io/squashql/query/compiled/CompiledComparisonMeasureReferencePosition.java
+++ b/core/src/main/java/io/squashql/query/compiled/CompiledComparisonMeasureReferencePosition.java
@@ -16,6 +16,7 @@ public record CompiledComparisonMeasureReferencePosition(String alias,
                                                          Map<TypedField, String> referencePosition,
                                                          CompiledPeriod period,
                                                          ColumnSetKey columnSetKey,
+                                                         List<?> elements,
                                                          List<TypedField> ancestors,
                                                          boolean grandTotalAlongAncestors) implements CompiledComparisonMeasure {
 
diff --git a/core/src/main/java/io/squashql/query/compiled/CompiledGroupColumnSet.java b/core/src/main/java/io/squashql/query/compiled/CompiledGroupColumnSet.java
index 37daca8..07912d2 100644
--- a/core/src/main/java/io/squashql/query/compiled/CompiledGroupColumnSet.java
+++ b/core/src/main/java/io/squashql/query/compiled/CompiledGroupColumnSet.java
@@ -9,7 +9,7 @@ import java.util.Map;
 public record CompiledGroupColumnSet(List<TypedField> columnsForPrefetching,
                                      List<TypedField> newColumns,
                                      ColumnSetKey columnSetKey,
-                                     Map<String, List<String>> values) implements CompiledColumnSet {
+                                     Map<Object, List<Object>> values) implements CompiledColumnSet {
 
   public TypedField newField() {
     return this.newColumns.get(0);
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 a7c5586..d20d3f3 100644
--- a/core/src/main/java/io/squashql/query/compiled/Evaluator.java
+++ b/core/src/main/java/io/squashql/query/compiled/Evaluator.java
@@ -53,13 +53,7 @@ public class Evaluator implements BiConsumer<QueryPlanNodeKey, ExecutionContext>
   @Override
   public Void visit(CompiledComparisonMeasureReferencePosition cm) {
     AComparisonExecutor<CompiledComparisonMeasureReferencePosition> executor;
-    if (cm.columnSetKey() == GROUP) {
-      CompiledColumnSet cs = this.executionContext.columnSets().get(GROUP);
-      if (cs == null) {
-        throw new IllegalArgumentException(String.format("columnSet %s is not specified in the query but is used in a comparison measure: %s", GROUP, cm));
-      }
-      executor = new GroupComparisonExecutor((CompiledGroupColumnSet) cs);
-    } else if (cm.period() != null) {
+    if (cm.period() != null) {
       for (TypedField field : cm.period().getTypedFields()) {
         if (!this.executionContext.columns().contains(field)) {
           throw new IllegalArgumentException(String.format("%s is not specified in the query but is used in a comparison measure: %s", field.name(), cm));
@@ -69,7 +63,14 @@ public class Evaluator implements BiConsumer<QueryPlanNodeKey, ExecutionContext>
     } else if (cm.ancestors() != null) {
       executor = new ParentComparisonExecutor(cm);
     } else {
-      throw new IllegalArgumentException(String.format("Comparison measure not correctly defined (%s). It should have a period or columnSetKey parameter", cm));
+      CompiledColumnSet cs = this.executionContext.columnSets().get(GROUP);
+      if (cs != null) {
+        executor = new GroupComparisonExecutor((CompiledGroupColumnSet) cs);
+      } else {
+        // No CS. it is a single group comparison. Order => ASC
+        TypedField next = cm.referencePosition().keySet().iterator().next();
+        executor = new SingleGroupComparisonExecutor(next);
+      }
     }
 
     QueryScope readScope = MeasureUtils.getReadScopeComparisonMeasureReferencePosition(
diff --git a/core/src/main/java/io/squashql/query/dto/GroupColumnSetDto.java b/core/src/main/java/io/squashql/query/dto/GroupColumnSetDto.java
index 0690f91..81d980e 100644
--- a/core/src/main/java/io/squashql/query/dto/GroupColumnSetDto.java
+++ b/core/src/main/java/io/squashql/query/dto/GroupColumnSetDto.java
@@ -22,7 +22,7 @@ public class GroupColumnSetDto implements ColumnSet {
 
   public Field field;
 
-  public Map<String, List<String>> values = new LinkedHashMap<>();
+  public Map<Object, List<Object>> values = new LinkedHashMap<>();
 
   public GroupColumnSetDto(String name, Field field) {
     this.newField = new TableField(name);
diff --git a/core/src/main/java/io/squashql/table/ColumnarTable.java b/core/src/main/java/io/squashql/table/ColumnarTable.java
index 10628bd..78bdd93 100644
--- a/core/src/main/java/io/squashql/table/ColumnarTable.java
+++ b/core/src/main/java/io/squashql/table/ColumnarTable.java
@@ -1,5 +1,6 @@
 package io.squashql.table;
 
+import com.google.common.base.Suppliers;
 import io.squashql.query.Header;
 import io.squashql.query.compiled.CompiledMeasure;
 import io.squashql.query.dictionary.ObjectArrayDictionary;
@@ -10,7 +11,7 @@ import java.util.function.Supplier;
 
 public class ColumnarTable implements Table {
 
-  private final MemoizingSupplier<ObjectArrayDictionary> pointDictionary;
+  private final Supplier<ObjectArrayDictionary> pointDictionary;
   private final List<Header> headers;
   private final Set<CompiledMeasure> measures;
   private final List<List<Object>> values;
@@ -24,7 +25,7 @@ public class ColumnarTable implements Table {
     this.headers = new ArrayList<>(headers);
     this.measures = new HashSet<>(measures);
     this.values = new ArrayList<>(values);
-    this.pointDictionary = new MemoizingSupplier<>(() -> createPointDictionary(this));
+    this.pointDictionary = Suppliers.memoize(() -> createPointDictionary(this));
   }
 
   public static ObjectArrayDictionary createPointDictionary(Table table) {
@@ -83,18 +84,6 @@ public class ColumnarTable implements Table {
     }
   }
 
-  @Override
-  public void removeColumn(String column) {
-    int index = columnIndex(column);
-    Header header = this.headers.remove(index);
-    if (header.isMeasure()) {
-      this.measures.removeIf(m -> m.alias().equals(header.name()));
-    } else {
-      this.pointDictionary.forget();
-    }
-    this.values.remove(index);
-  }
-
   @Override
   public List<Object> getColumn(int columnIndex) {
     return this.values.get(columnIndex);
@@ -175,38 +164,4 @@ public class ColumnarTable implements Table {
   public int hashCode() {
     return Objects.hash(this.headers, this.measures, this.values);
   }
-
-  private static class MemoizingSupplier<T> implements Supplier<T> {
-
-    private final Supplier<T> delegate;
-    private boolean initialized;
-    private T value;
-
-    MemoizingSupplier(Supplier<T> delegate) {
-      this.delegate = delegate;
-    }
-
-    @Override
-    public T get() {
-      if (!this.initialized) {
-        T t = this.delegate.get();
-        this.value = t;
-        this.initialized = true;
-        return t;
-      }
-      return this.value;
-    }
-
-    public void forget() {
-      this.value = null;
-      this.initialized = false;
-    }
-
-    @Override
-    public String toString() {
-      return "Suppliers.memoize("
-              + (this.initialized ? "<supplier that returned " + this.value + ">" : this.delegate)
-              + ")";
-    }
-  }
 }
diff --git a/core/src/main/java/io/squashql/table/RowTable.java b/core/src/main/java/io/squashql/table/RowTable.java
index f29c9ac..fba3e3a 100644
--- a/core/src/main/java/io/squashql/table/RowTable.java
+++ b/core/src/main/java/io/squashql/table/RowTable.java
@@ -30,11 +30,6 @@ public class RowTable implements Table {
     throwNotSupportedException();
   }
 
-  @Override
-  public void removeColumn(String column) {
-    throwNotSupportedException();
-  }
-
   @Override
   public List<Object> getColumn(int columnIndex) {
     throwNotSupportedException();
diff --git a/core/src/main/java/io/squashql/table/Table.java b/core/src/main/java/io/squashql/table/Table.java
index 67afd3c..22cb5ab 100644
--- a/core/src/main/java/io/squashql/table/Table.java
+++ b/core/src/main/java/io/squashql/table/Table.java
@@ -29,8 +29,6 @@ public interface Table extends Iterable<List<Object>> {
 
   void transferAggregates(Table from, CompiledMeasure measure);
 
-  void removeColumn(String column);
-
   default List<Object> getColumn(int columnIndex) {
     List<Object> elements = new ArrayList<>();
     for (List<Object> objects : this) {
diff --git a/documentation/QUERY.md b/documentation/QUERY.md
index 1a91e27..e97d949 100644
--- a/documentation/QUERY.md
+++ b/documentation/QUERY.md
@@ -1127,25 +1127,58 @@ Result
 +--------+----------+---------+-------------------+
 ```
 
-Note: if only one group is specified, such as:
+It is also possible to perform a "single group" comparison. Let's say we only want to compare s1, s2 and s3.
+To compare each value with the previous one:
 ```typescript
-const values = new Map(Object.entries({
-  "group4": ["s1", "s2", "s3"],
-}))
+const revenueComparison = comparisonMeasureWithinSameGroup("revenueComparison",
+        ComparisonMethod.ABSOLUTE_DIFFERENCE,
+        revenue,
+        new Map([[scenario, "s-1"]]))
+const query = from("myTable")
+        .select([scenario], [], [revenue, revenueComparison])
+        .build()
 ```
 
-the additional column named "group" is not added to the final result.
-
+Result
 ```
 +----------+---------+-------------------+
 | scenario | revenue | revenueComparison |
 +----------+---------+-------------------+
-|       s1 |   372.0 |              12.0 |
+|       s1 |   372.0 |               0.0 |
 |       s2 |   300.0 |             -72.0 |
 |       s3 |   350.0 |              50.0 |
 +----------+---------+-------------------+
 ```
 
+Replace `s-1` with `first` to compare s2 with s1 and s3 with s1 (first element of the group)
+
+The order in which the comparison is performed is following the natural order of the values. If you want to change the order, 
+you can pass a list of values:
+
+```typescript
+const elements = ["s3", "s1", "s2"]
+const revenueComparison = comparisonMeasureWithinSameGroupInOrder("revenueComparison",
+        ComparisonMethod.ABSOLUTE_DIFFERENCE,
+        revenue,
+        new Map([[scenario, "s-1"]]),
+        elements)
+const query = from("myTable")
+        .select([scenario], [], [revenue, revenueComparison])
+        .orderByFirstElements(scenario, elements) // orderByFirstElements to display correctly the elements
+        .build()
+```
+
+Result
+```
++----------+---------+-------------------+
+| scenario | revenue | revenueComparison |
++----------+---------+-------------------+
+|       s3 |   350.0 |               0.0 |
+|       s1 |   372.0 |              22.0 |
+|       s2 |   300.0 |             -72.0 |
++----------+---------+-------------------+
+```
+
 ## Parameters
 
 Once built, a query can accept additional parameters to change the way the query will be executed. Here's 
diff --git a/js/typescript-library/package-lock.json b/js/typescript-library/package-lock.json
index cbd2ecf..132bd97 100644
--- a/js/typescript-library/package-lock.json
+++ b/js/typescript-library/package-lock.json
@@ -1,12 +1,12 @@
 {
   "name": "@squashql/squashql-js",
-  "version": "1.15.1",
+  "version": "1.15.3",
   "lockfileVersion": 2,
   "requires": true,
   "packages": {
     "": {
       "name": "@squashql/squashql-js",
-      "version": "1.15.1",
+      "version": "1.15.3",
       "license": "Apache-2.0",
       "dependencies": {
         "axios": "^1.6.7"
diff --git a/js/typescript-library/src/generate-json-queryDto.ts b/js/typescript-library/src/generate-json-queryDto.ts
index 4588b51..f9f7882 100644
--- a/js/typescript-library/src/generate-json-queryDto.ts
+++ b/js/typescript-library/src/generate-json-queryDto.ts
@@ -6,7 +6,7 @@ import {
   BinaryOperator,
   comparisonMeasureWithGrandTotal,
   comparisonMeasureWithGrandTotalAlongAncestors,
-  comparisonMeasureWithinSameGroup,
+  comparisonMeasureWithinSameGroup, comparisonMeasureWithinSameGroupInOrder,
   comparisonMeasureWithParent,
   comparisonMeasureWithPeriod,
   ComparisonMethod,
@@ -72,9 +72,11 @@ export function generateFromQueryDto() {
   q.withMeasure(avgIf("whatever", f1.divide(one.plus(rate)), criterion_(f1.plus(f2).as("f1+f2"), one, ConditionType.GT)))
 
   q.withMeasure(comparisonMeasureWithinSameGroup("comp group", ComparisonMethod.ABSOLUTE_DIFFERENCE, price, new Map([
-    [tableField("group"), "g"],
     [tableField("scenario"), "s-1"]
   ])))
+  q.withMeasure(comparisonMeasureWithinSameGroupInOrder("comp group in order", ComparisonMethod.ABSOLUTE_DIFFERENCE, price, new Map([
+    [tableField("scenario"), "s-1"]
+  ]), ["base", "s1", "s2"]))
   q.withMeasure(comparisonMeasureWithPeriod("growth", ComparisonMethod.DIVIDE, price, new Map([
     [tableField("Year"), "y-1"],
     [tableField("Month"), "m"]
diff --git a/js/typescript-library/src/measure.ts b/js/typescript-library/src/measure.ts
index 00db766..053d508 100644
--- a/js/typescript-library/src/measure.ts
+++ b/js/typescript-library/src/measure.ts
@@ -94,6 +94,7 @@ export class ComparisonMeasureReferencePosition implements Measure {
               readonly measure: Measure,
               readonly referencePosition?: Map<Field, string>,
               readonly columnSetKey?: ColumnSetKey,
+              readonly elements?: Array<any>,
               readonly period?: Period,
               readonly ancestors?: Array<Field>,
               readonly grandTotalAlongAncestors?: boolean) {
@@ -106,6 +107,7 @@ export class ComparisonMeasureReferencePosition implements Measure {
       "comparisonMethod": this.comparisonMethod,
       "measure": this.measure,
       "columnSetKey": this.columnSetKey,
+      "elements": this.elements,
       "period": this.period,
       "referencePosition": this.referencePosition ? Object.fromEntries(serializeMap(this.referencePosition)) : undefined,
       "ancestors": this.ancestors,
@@ -282,28 +284,36 @@ export function comparisonMeasureWithPeriod(alias: string,
                                             measure: Measure,
                                             referencePosition: Map<Field, string>,
                                             period: Period): Measure {
-  return new ComparisonMeasureReferencePosition(alias, comparisonMethod, measure, referencePosition, undefined, period)
+  return new ComparisonMeasureReferencePosition(alias, comparisonMethod, measure, referencePosition, undefined, undefined, period)
 }
 
 export function comparisonMeasureWithinSameGroup(alias: string,
                                                  comparisonMethod: ComparisonMethod,
                                                  measure: Measure,
                                                  referencePosition: Map<Field, string>): Measure {
-  return new ComparisonMeasureReferencePosition(alias, comparisonMethod, measure, referencePosition, ColumnSetKey.GROUP)
+  return new ComparisonMeasureReferencePosition(alias, comparisonMethod, measure, referencePosition)
+}
+
+export function comparisonMeasureWithinSameGroupInOrder(alias: string,
+                                                        comparisonMethod: ComparisonMethod,
+                                                        measure: Measure,
+                                                        referencePosition: Map<Field, string>,
+                                                        elements: Array<any>): Measure {
+  return new ComparisonMeasureReferencePosition(alias, comparisonMethod, measure, referencePosition, undefined, elements)
 }
 
 export function comparisonMeasureWithParent(alias: string,
                                             comparisonMethod: ComparisonMethod,
                                             measure: Measure,
                                             ancestors: Array<Field>): Measure {
-  return new ComparisonMeasureReferencePosition(alias, comparisonMethod, measure, undefined, undefined, undefined, ancestors, false)
+  return new ComparisonMeasureReferencePosition(alias, comparisonMethod, measure, undefined, undefined, undefined, undefined, ancestors, false)
 }
 
 export function comparisonMeasureWithGrandTotalAlongAncestors(alias: string,
                                                               comparisonMethod: ComparisonMethod,
                                                               measure: Measure,
                                                               ancestors: Array<Field>): Measure {
-  return new ComparisonMeasureReferencePosition(alias, comparisonMethod, measure, undefined, undefined, undefined, ancestors, true)
+  return new ComparisonMeasureReferencePosition(alias, comparisonMethod, measure, undefined, undefined, undefined, undefined, ancestors, true)
 }
 
 export function comparisonMeasureWithGrandTotal(alias: string,


Test patch:
diff --git a/core/src/test/java/io/squashql/query/ATestGroupComparison.java b/core/src/test/java/io/squashql/query/ATestGroupComparison.java
index 10286b3..0a16266 100644
--- a/core/src/test/java/io/squashql/query/ATestGroupComparison.java
+++ b/core/src/test/java/io/squashql/query/ATestGroupComparison.java
@@ -10,14 +10,13 @@ import io.squashql.type.TableTypedField;
 import org.assertj.core.api.Assertions;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.TestInstance;
-import org.junit.jupiter.params.ParameterizedTest;
-import org.junit.jupiter.params.provider.ValueSource;
 
 import java.util.List;
 import java.util.Map;
 
 import static io.squashql.query.ComparisonMethod.RELATIVE_DIFFERENCE;
 import static io.squashql.query.Functions.eq;
+import static io.squashql.query.Functions.sum;
 import static io.squashql.query.TableField.tableField;
 import static io.squashql.query.TableField.tableFields;
 import static io.squashql.transaction.DataLoader.MAIN_SCENARIO_NAME;
@@ -59,22 +58,20 @@ public abstract class ATestGroupComparison extends ABaseTestQuery {
 
     this.tm.load("s1", this.storeName, List.of(
             new Object[]{"bottle", "drink", 4d, 9},
-            new Object[]{"cookie", "food", 3d, 20},
-            new Object[]{"shirt", "cloth", 10d, 3}
+            new Object[]{"cookie", "food", 4d, 20},
+            new Object[]{"shirt", "cloth", 11d, 3}
     ));
 
     this.tm.load("s2", this.storeName, List.of(
             new Object[]{"bottle", "drink", 1.5d, 12},
-            new Object[]{"cookie", "food", 3d, 20},
-            new Object[]{"shirt", "cloth", 10d, 3}
+            new Object[]{"cookie", "food", 2.5d, 20},
+            new Object[]{"shirt", "cloth", 9d, 3}
     ));
   }
 
-  @ParameterizedTest
-  @ValueSource(booleans = {true, false})
-  void testAbsoluteDifferenceWithFirst(boolean fullName) {
-    Field scenario = fullName ? new TableField(this.storeName, SCENARIO_FIELD_NAME) : new TableField(SCENARIO_FIELD_NAME);
-    GroupColumnSetDto group = new GroupColumnSetDto(groupOfScenario, scenario)
+  @Test
+  void testAbsoluteDifferenceWithFirst() {
+    GroupColumnSetDto group = new GroupColumnSetDto(groupOfScenario, this.scenario)
             .withNewGroup("group1", List.of(MAIN_SCENARIO_NAME, "s1"))
             .withNewGroup("group2", List.of(MAIN_SCENARIO_NAME, "s2"))
             .withNewGroup("group3", List.of(MAIN_SCENARIO_NAME, "s1", "s2"));
@@ -85,7 +82,7 @@ public abstract class ATestGroupComparison extends ABaseTestQuery {
             ComparisonMethod.ABSOLUTE_DIFFERENCE,
             price,
             Map.of(
-                    scenario, AComparisonExecutor.REF_POS_FIRST,
+                    this.scenario, AComparisonExecutor.REF_POS_FIRST,
                     tableField(groupOfScenario), "g"
             ),
             ColumnSetKey.GROUP);
@@ -95,7 +92,7 @@ public abstract class ATestGroupComparison extends ABaseTestQuery {
             ComparisonMethod.ABSOLUTE_DIFFERENCE,
             quantity,
             Map.of(
-                    scenario, AComparisonExecutor.REF_POS_FIRST,
+                    this.scenario, AComparisonExecutor.REF_POS_FIRST,
                     tableField(groupOfScenario), "g"
             ),
             ColumnSetKey.GROUP);
@@ -107,29 +104,29 @@ public abstract class ATestGroupComparison extends ABaseTestQuery {
 
     Table dataset = this.executor.executeQuery(query);
     Assertions.assertThat(dataset.headers().stream().map(Header::name)).containsExactly(
-            groupOfScenario, fullName ? SqlUtils.getFieldFullName(this.storeName, SCENARIO_FIELD_NAME) : SCENARIO_FIELD_NAME,
+            groupOfScenario, SqlUtils.squashqlExpression(this.scenario),
             "priceDiff", "p",
             "quantityDiff", "q");
-    Assertions.assertThat(dataset).containsExactlyInAnyOrder(
-            List.of("group1", "base", 0d, 15d, 0l, 34l),
-            List.of("group1", "s1", 2d, 17d, -2l, 32l),
-            List.of("group2", "base", 0d, 15d, 0l, 34l),
-            List.of("group2", "s2", -0.5d, 14.5d, 1l, 35l),
-            List.of("group3", "base", 0d, 15d, 0l, 34l),
-            List.of("group3", "s1", 2d, 17d, -2l, 32l),
-            List.of("group3", "s2", -0.5d, 14.5d, 1l, 35l));
+    Assertions.assertThat(dataset).containsExactly(
+            List.of("group1", MAIN_SCENARIO_NAME, 0d, 15d, 0l, 34l),
+            List.of("group1", "s1", 4d, 19d, -2l, 32l),
+            List.of("group2", MAIN_SCENARIO_NAME, 0d, 15d, 0l, 34l),
+            List.of("group2", "s2", -2d, 13d, 1l, 35l),
+            List.of("group3", MAIN_SCENARIO_NAME, 0d, 15d, 0l, 34l),
+            List.of("group3", "s1", 4d, 19d, -2l, 32l),
+            List.of("group3", "s2", -2d, 13d, 1l, 35l));
 
     // Add a condition
     query = Query
             .from(this.storeName)
-            .where(scenario, eq("s1"))
+            .where(this.scenario, eq("s1"))
             .select_(List.of(group), List.of(priceComp))
             .build();
 
     dataset = this.executor.executeQuery(query);
-    Assertions.assertThat(dataset).containsExactlyInAnyOrder(
-            List.of("group1", "s1", 2d),
-            List.of("group3", "s1", 2d));
+    Assertions.assertThat(dataset).containsExactly(
+            List.of("group1", "s1", 4d),
+            List.of("group3", "s1", 4d));
   }
 
   @Test
@@ -152,27 +149,23 @@ public abstract class ATestGroupComparison extends ABaseTestQuery {
             Map.of(tableField(SCENARIO_FIELD_NAME), "s-1", tableField(groupOfScenario), "g"),
             ColumnSetKey.GROUP);
 
-    var query = new QueryDto()
-            .table(this.storeName)
-            .withColumnSet(ColumnSetKey.GROUP, this.groupCS)
-            .withMeasure(priceComp)
-            .withMeasure(price)
-            .withMeasure(quantityComp)
-            .withMeasure(quantity);
+    var query = Query.from(this.storeName)
+            .select(List.of(), List.of(this.groupCS), List.of(priceComp, price, quantityComp, quantity))
+            .build();
 
     Table dataset = this.executor.executeQuery(query);
     Assertions.assertThat(dataset.headers().stream().map(Header::name)).containsExactly(
             groupOfScenario, SCENARIO_FIELD_NAME,
             "priceDiff", "p",
             "quantityDiff", "q");
-    Assertions.assertThat(dataset).containsExactlyInAnyOrder(
+    Assertions.assertThat(dataset).containsExactly(
             List.of("group1", MAIN_SCENARIO_NAME, 0d, 15d, 0l, 34l),
-            List.of("group1", "s1", 2d, 17d, -2l, 32l),
+            List.of("group1", "s1", 4d, 19d, -2l, 32l),
             List.of("group2", MAIN_SCENARIO_NAME, 0d, 15d, 0l, 34l),
-            List.of("group2", "s2", -0.5d, 14.5d, 1l, 35l),
+            List.of("group2", "s2", -2d, 13d, 1l, 35l),
             List.of("group3", MAIN_SCENARIO_NAME, 0d, 15d, 0l, 34l),
-            List.of("group3", "s1", 2d, 17d, -2l, 32l),
-            List.of("group3", "s2", -2.5, 14.5d, 3l, 35l));
+            List.of("group3", "s1", 4d, 19d, -2l, 32l),
+            List.of("group3", "s2", -6d, 13d, 3l, 35l));
   }
 
   @Test
@@ -211,14 +204,14 @@ public abstract class ATestGroupComparison extends ABaseTestQuery {
             groupOfScenario, SCENARIO_FIELD_NAME,
             "priceDiff", "p",
             "quantityDiff", "q");
-    Assertions.assertThat(dataset).containsExactlyInAnyOrder(
+    Assertions.assertThat(dataset).containsExactly(
             List.of("group1", MAIN_SCENARIO_NAME, 0d, 15d, 0d, 34l),
-            List.of("group1", "s1", 0.13333333333333333d, 17d, -0.058823529411764705d, 32l),
+            List.of("group1", "s1", 0.26666666666666666d, 19d, -0.058823529411764705d, 32l),
             List.of("group2", MAIN_SCENARIO_NAME, 0d, 15d, 0d, 34l),
-            List.of("group2", "s2", -0.03333333333333333d, 14.5d, 0.029411764705882353d, 35l),
+            List.of("group2", "s2", -0.13333333333333333d, 13d, 0.029411764705882353d, 35l),
             List.of("group3", MAIN_SCENARIO_NAME, 0d, 15d, 0d, 34l),
-            List.of("group3", "s1", 0.13333333333333333d, 17d, -0.058823529411764705d, 32l),
-            List.of("group3", "s2", -0.03333333333333333d, 14.5d, 0.029411764705882353d, 35l));
+            List.of("group3", "s1", 0.26666666666666666d, 19d, -0.058823529411764705d, 32l),
+            List.of("group3", "s2", -0.13333333333333333d, 13d, 0.029411764705882353d, 35l));
   }
 
   @Test
@@ -308,31 +301,171 @@ public abstract class ATestGroupComparison extends ABaseTestQuery {
   }
 
   @Test
-  void testAbsoluteDifferenceWithPreviousSingleGroup() {
-    GroupColumnSetDto group = new GroupColumnSetDto(groupOfScenario, this.scenario)
-            .withNewGroup("whatever", List.of(MAIN_SCENARIO_NAME, "s1", "s2")); // Name of the group is not important when single group
+  void testAbsoluteDifferenceWithPreviousSingleGroupUndefined() {
+    Measure price = sum("p", this.price);
+    ComparisonMeasureReferencePosition priceCompPrev = new ComparisonMeasureReferencePosition(
+            "priceCompPrev",
+            ComparisonMethod.ABSOLUTE_DIFFERENCE,
+            price,
+            Map.of(this.scenario, "s-1"));
 
-    AggregatedMeasure price = new AggregatedMeasure("p", "price", "sum");
-    ComparisonMeasureReferencePosition priceComp = new ComparisonMeasureReferencePosition(
-            "priceDiff",
+    ComparisonMeasureReferencePosition priceCompFirst = new ComparisonMeasureReferencePosition(
+            "priceCompFirst",
+            ComparisonMethod.ABSOLUTE_DIFFERENCE,
+            price,
+            Map.of(this.scenario, "first"));
+
+    var query = Query
+            .from(this.storeName)
+            .select(List.of(this.scenario), List.of(price, priceCompPrev, priceCompFirst))
+            .build();
+
+    Table dataset = this.executor.executeQuery(query);
+    Assertions.assertThat(dataset.headers().stream().map(Header::name)).containsExactly(
+            SqlUtils.getFieldFullName(this.storeName, SCENARIO_FIELD_NAME),
+            "p",
+            "priceCompPrev",
+            "priceCompFirst");
+    Assertions.assertThat(dataset).containsExactly(
+            List.of("base", 15d, 0d, 0d),
+            List.of("s1", 19d, 4d, 4d),
+            List.of("s2", 13d, -6d, -2d));
+
+    // With a filter
+    query = Query
+            .from(this.storeName)
+            .where(this.scenario, eq("s2"))
+            .select(List.of(this.scenario), List.of(price, priceCompPrev, priceCompFirst))
+            .build();
+
+    dataset = this.executor.executeQuery(query);
+    Assertions.assertThat(dataset.headers().stream().map(Header::name)).containsExactly(
+            SqlUtils.getFieldFullName(this.storeName, SCENARIO_FIELD_NAME),
+            "p",
+            "priceCompPrev",
+            "priceCompFirst");
+    Assertions.assertThat(dataset).containsExactly(List.of("s2", 13d, -6d, -2d));
+
+    query = Query
+            .from(this.storeName)
+            .select(List.of(this.scenario, this.ean), List.of(price, priceCompPrev, priceCompFirst))
+            .build();
+
+    dataset = this.executor.executeQuery(query);
+    Assertions.assertThat(dataset).containsExactly(
+            List.of(MAIN_SCENARIO_NAME, "bottle", 2d, 0d, 0d),
+            List.of(MAIN_SCENARIO_NAME, "cookie", 3d, 0d, 0d),
+            List.of(MAIN_SCENARIO_NAME, "shirt", 10d, 0d, 0d),
+            List.of("s1", "bottle", 4d, 2d, 2d),
+            List.of("s1", "cookie", 4d, 1d, 1d),
+            List.of("s1", "shirt", 11d, 1d, 1d),
+            List.of("s2", "bottle", 1.5d, -2.5d, -0.5d),
+            List.of("s2", "cookie", 2.5d, -1.5d, -0.5d),
+            List.of("s2", "shirt", 9d, -2d, -1d));
+
+    query = Query
+            .from(this.storeName)
+            .select(List.of(this.ean, this.scenario), List.of(price, priceCompPrev, priceCompFirst))
+            .build();
+
+    dataset = this.executor.executeQuery(query);
+    Assertions.assertThat(dataset).containsExactly(
+            List.of("bottle", MAIN_SCENARIO_NAME, 2d, 0d, 0d),
+            List.of("bottle", "s1", 4d, 2d, 2d),
+            List.of("bottle", "s2", 1.5d, -2.5d, -0.5d),
+            List.of("cookie", MAIN_SCENARIO_NAME, 3d, 0d, 0d),
+            List.of("cookie", "s1", 4d, 1d, 1d),
+            List.of("cookie", "s2", 2.5d, -1.5d, -0.5d),
+            List.of("shirt", MAIN_SCENARIO_NAME, 10d, 0d, 0d),
+            List.of("shirt", "s1", 11d, 1d, 1d),
+            List.of("shirt", "s2", 9d, -2d, -1d));
+  }
+
+  @Test
+  void testAbsoluteDifferenceWithPreviousSingleGroupExplicitlyDefined() {
+    List<String> elements = List.of("s2", MAIN_SCENARIO_NAME, "s1");
+    Measure price = sum("p", this.price);
+    ComparisonMeasureReferencePosition priceCompPrev = new ComparisonMeasureReferencePosition(
+            "priceCompPrev",
             ComparisonMethod.ABSOLUTE_DIFFERENCE,
             price,
             Map.of(this.scenario, "s-1"),
-            ColumnSetKey.GROUP);
+            elements);
+
+    ComparisonMeasureReferencePosition priceCompFirst = new ComparisonMeasureReferencePosition(
+            "priceCompFirst",
+            ComparisonMethod.ABSOLUTE_DIFFERENCE,
+            price,
+            Map.of(this.scenario, "first"),
+            elements);
 
     var query = Query
             .from(this.storeName)
-            .select_(List.of(group), List.of(priceComp, price))
+            .select(List.of(this.scenario), List.of(price, priceCompPrev, priceCompFirst))
+            .orderBy(this.scenario, elements)
             .build();
 
     Table dataset = this.executor.executeQuery(query);
     Assertions.assertThat(dataset.headers().stream().map(Header::name)).containsExactly(
             SqlUtils.getFieldFullName(this.storeName, SCENARIO_FIELD_NAME),
-            "priceDiff",
-            "p");
-    Assertions.assertThat(dataset).containsExactlyInAnyOrder(
-            List.of("base", 0d, 15d),
-            List.of("s1", 2d, 17d),
-            List.of("s2", -2.5d, 14.5d));
+            "p",
+            "priceCompPrev",
+            "priceCompFirst");
+    Assertions.assertThat(dataset).containsExactly(
+            List.of("s2", 13d, 0d, 0d),
+            List.of("base", 15d, 2d, 2d),
+            List.of("s1", 19d, 4d, 6d));
+
+    // With a filter
+    query = Query
+            .from(this.storeName)
+            .where(this.scenario, eq("s1"))
+            .select(List.of(this.scenario), List.of(price, priceCompPrev, priceCompFirst))
+            .orderBy(this.scenario, elements)
+            .build();
+
+    dataset = this.executor.executeQuery(query);
+    Assertions.assertThat(dataset.headers().stream().map(Header::name)).containsExactly(
+            SqlUtils.getFieldFullName(this.storeName, SCENARIO_FIELD_NAME),
+            "p",
+            "priceCompPrev",
+            "priceCompFirst");
+    Assertions.assertThat(dataset).containsExactly(List.of("s1", 19d, 4d, 6d));
+
+    query = Query
+            .from(this.storeName)
+            .select(List.of(this.scenario, this.ean), List.of(price, priceCompPrev, priceCompFirst))
+            .orderBy(this.scenario, elements)
+            .build();
+
+    dataset = this.executor.executeQuery(query);
+    Assertions.assertThat(dataset).containsExactly(
+            List.of("s2", "bottle", 1.5d, 0d, 0d),
+            List.of("s2", "cookie", 2.5d, 0d, 0d),
+            List.of("s2", "shirt", 9d, 0d, 0d),
+            List.of(MAIN_SCENARIO_NAME, "bottle", 2d, 0.5d, 0.5d),
+            List.of(MAIN_SCENARIO_NAME, "cookie", 3d, 0.5d, 0.5d),
+            List.of(MAIN_SCENARIO_NAME, "shirt", 10d, 1d, 1d),
+            List.of("s1", "bottle", 4d, 2d, 2.5d),
+            List.of("s1", "cookie", 4d, 1d, 1.5d),
+            List.of("s1", "shirt", 11d, 1d, 2d));
+
+    query = Query
+            .from(this.storeName)
+            .select(List.of(this.ean, this.scenario), List.of(price, priceCompPrev, priceCompFirst))
+            .orderBy(this.scenario, elements)
+            .build();
+
+    dataset = this.executor.executeQuery(query);
+    Assertions.assertThat(dataset).containsExactly(
+            List.of("bottle", "s2", 1.5d, 0d, 0d),
+            List.of("cookie", "s2", 2.5d, 0d, 0d),
+            List.of("shirt", "s2", 9d, 0d, 0d),
+            List.of("bottle", MAIN_SCENARIO_NAME, 2d, 0.5d, 0.5d),
+            List.of("cookie", MAIN_SCENARIO_NAME, 3d, 0.5d, 0.5d),
+            List.of("shirt", MAIN_SCENARIO_NAME, 10d, 1d, 1d),
+            List.of("bottle", "s1", 4d, 2d, 2.5d),
+            List.of("cookie", "s1", 4d, 1d, 1.5d),
+            List.of("shirt", "s1", 11d, 1d, 2d));
   }
 }
diff --git a/core/src/test/java/io/squashql/query/ATestQueryExecutor.java b/core/src/test/java/io/squashql/query/ATestQueryExecutor.java
index c6dfe59..ae7cd50 100644
--- a/core/src/test/java/io/squashql/query/ATestQueryExecutor.java
+++ b/core/src/test/java/io/squashql/query/ATestQueryExecutor.java
@@ -298,7 +298,7 @@ public abstract class ATestQueryExecutor extends ABaseTestQuery {
             .select(tableFields(List.of(SCENARIO_FIELD_NAME)), List.of(sum("p", "price"), sum("q", "quantity")))
             .build();
     Table table = this.executor.executeQuery(query);
-    Assertions.assertThat(table).containsExactlyInAnyOrder(
+    Assertions.assertThat(table).containsExactly(
             List.of("s1", 17.0d, 33L),
             List.of("s2", 14.5d, 33L));
   }
@@ -311,7 +311,7 @@ public abstract class ATestQueryExecutor extends ABaseTestQuery {
             .select(tableFields(List.of(SCENARIO_FIELD_NAME)), List.of(sum("p", "price"), sum("q", "quantity")))
             .build();
     Table table = this.executor.executeQuery(query);
-    Assertions.assertThat(table).containsExactlyInAnyOrder(List.of("s1", 17.0d, 33L));
+    Assertions.assertThat(table).containsExactly(List.of("s1", 17.0d, 33L));
   }
 
   @Test
@@ -326,7 +326,7 @@ public abstract class ATestQueryExecutor extends ABaseTestQuery {
             .build();
 
     Table table = this.executor.executeQuery(query);
-    Assertions.assertThat(table).containsExactlyInAnyOrder(List.of("drink", "starbuck's coffee", 10L));
+    Assertions.assertThat(table).containsExactly(List.of("drink", "starbuck's coffee", 10L));
 
     query = Query
             .from(this.storeName)
@@ -431,7 +431,7 @@ public abstract class ATestQueryExecutor extends ABaseTestQuery {
                             sumIf("quantity filtered", this.storeName + ".quantity", criterion(this.storeName + ".category", or))))
             .build();
     Table result = this.executor.executeQuery(query);
-    Assertions.assertThat(result).containsExactlyInAnyOrder(
+    Assertions.assertThat(result).containsExactly(
             List.of(MAIN_SCENARIO_NAME, 30L, 30L),
             List.of("s1", 30L, 30L),
             List.of("s2", 30L, 30L));
@@ -446,7 +446,7 @@ public abstract class ATestQueryExecutor extends ABaseTestQuery {
                     List.of(sumIf("quantity filtered", "quantity", Functions.all(category, criterion("subcategory", eq("biscuit"))))))
             .build();
     result = this.executor.executeQuery(query);
-    Assertions.assertThat(result).containsExactlyInAnyOrder(
+    Assertions.assertThat(result).containsExactly(
             List.of(MAIN_SCENARIO_NAME, 20L),
             List.of("s1", 20L),
             List.of("s2", 20L));
@@ -461,7 +461,7 @@ public abstract class ATestQueryExecutor extends ABaseTestQuery {
                     List.of(sumIf("quantity filtered", this.storeName + ".quantity", criterion(this.storeName + ".category", or))))
             .build();
     Table result = this.executor.executeQuery(query);
-    Assertions.assertThat(result).containsExactlyInAnyOrder(
+    Assertions.assertThat(result).containsExactly(
             List.of(MAIN_SCENARIO_NAME, 30L),
             List.of("s1", 30L),
             List.of("s2", 30L));
@@ -482,10 +482,10 @@ public abstract class ATestQueryExecutor extends ABaseTestQuery {
                             countDistinct("count distinct categories", this.storeName + ".category")))
             .build();
     Table result = this.executor.executeQuery(query);
-    Assertions.assertThat(result).containsExactlyInAnyOrder(
+    Assertions.assertThat(result).containsExactly(
+            List.of("cloth", 3L, 1L),
             List.of("drink", 3L, 1L),
-            List.of("food", 3L, 1L),
-            List.of("cloth", 3L, 1L));
+            List.of("food", 3L, 1L));
     Assertions.assertThat(result.headers().stream().map(Header::name))
             .containsExactly("category", "count categories", "count distinct categories");
   }
@@ -505,7 +505,7 @@ public abstract class ATestQueryExecutor extends ABaseTestQuery {
                                     criterion(new TableField(this.storeName + ".isFood"), eq(true)))))
             .build();
     Table result = this.executor.executeQuery(query);
-    Assertions.assertThat(result).containsExactlyInAnyOrder(List.of(6L, 2L));
+    Assertions.assertThat(result).containsExactly(List.of(6L, 2L));
     Assertions.assertThat(result.headers().stream().map(Header::name))
             .containsExactly("count categories if food", "count distinct categories if food");
   }
diff --git a/core/src/test/java/io/squashql/table/TestTable.java b/core/src/test/java/io/squashql/table/TestTable.java
index a6c596c..21e74b7 100644
--- a/core/src/test/java/io/squashql/table/TestTable.java
+++ b/core/src/test/java/io/squashql/table/TestTable.java
@@ -75,33 +75,4 @@ public class TestTable {
             .isInstanceOf(IllegalArgumentException.class)
             .hasMessageContaining("does not match the headers of the destination table");
   }
-
-  @Test
-  void testRemoveColumn() {
-    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(
-                    Arrays.asList("france", "france", "spain", "spain"),
-                    Arrays.asList("paris", "toulouse", "madrid", "barcelona"),
-                    Arrays.asList(1d, 2d, 3d, 4d)));
-    Assertions.assertThat(table.headers().stream().map(Header::name)).containsExactly(country.name(), city.name(), pop.name());
-    Assertions.assertThat(table.measures()).containsExactly(popAvg);
-    Assertions.assertThat(table.pointDictionary().getPointLength()).isEqualTo(2);
-
-    table.removeColumn(country.name());
-    // Make sure the point dictionary is rebuilt
-    Assertions.assertThat(table.headers().stream().map(Header::name)).containsExactly(city.name(), pop.name());
-    Assertions.assertThat(table.measures()).containsExactly(popAvg);
-    Assertions.assertThat(table.pointDictionary().getPointLength()).isEqualTo(1);
-
-    table.removeColumn(pop.name());
-    Assertions.assertThat(table.headers().stream().map(Header::name)).containsExactly(city.name());
-    Assertions.assertThat(table.measures()).isEmpty();
-    Assertions.assertThat(table.pointDictionary().getPointLength()).isEqualTo(1);
-  }
 }
diff --git a/js/typescript-library/src/__tests__/dependencies/computeMeasureDependencies.test.ts b/js/typescript-library/src/__tests__/dependencies/computeMeasureDependencies.test.ts
index d1dd38e..5d450a5 100644
--- a/js/typescript-library/src/__tests__/dependencies/computeMeasureDependencies.test.ts
+++ b/js/typescript-library/src/__tests__/dependencies/computeMeasureDependencies.test.ts
@@ -70,6 +70,7 @@ describe('computeMeasureDependencies', () => {
             mockMeasure1,
             referencePosition,
             ColumnSetKey.GROUP,
+            undefined,
             mockPeriod,
             ancestors
     )
diff --git a/server/src/test/java/io/squashql/js/TestJavascriptLibrary.java b/server/src/test/java/io/squashql/js/TestJavascriptLibrary.java
index 50d3b48..455b7da 100644
--- a/server/src/test/java/io/squashql/js/TestJavascriptLibrary.java
+++ b/server/src/test/java/io/squashql/js/TestJavascriptLibrary.java
@@ -62,8 +62,12 @@ public class TestJavascriptLibrary {
     q.withMeasure(new ComparisonMeasureReferencePosition("comp group",
             ComparisonMethod.ABSOLUTE_DIFFERENCE,
             price,
-            Map.of(tableField("scenario"), "s-1", tableField("group"), "g"),
-            ColumnSetKey.GROUP));
+            Map.of(tableField("scenario"), "s-1")));
+    q.withMeasure(new ComparisonMeasureReferencePosition("comp group in order",
+            ComparisonMethod.ABSOLUTE_DIFFERENCE,
+            price,
+            Map.of(tableField("scenario"), "s-1"),
+            List.of("base", "s1", "s2")));
 
     Period.Month month = new Period.Month(tableField("Month"), tableField("Year"));
     q.withMeasure(new ComparisonMeasureReferencePosition("growth",