Repository: apache/skywalking (language: python)

Issue:
[Feature] MQE support `sort/sort_labels` functions.
### Search before asking

- [X] I had searched in the [issues](https://github.com/apache/skywalking/issues?q=is%3Aissue) and found no similar feature requirement.


### Description

- `sort` function: Sort the expression result values, support in ascending order and descending order.
- `sort_labels`: Sort the expression result labels, support in ascending order and descending order. The order should be [natural sort order](https://en.wikipedia.org/wiki/Natural_sort_order)


### Use case

_No response_

### Related issues

_No response_

### Are you willing to submit a pull request to implement this on your own?

- [X] Yes I am willing to submit a pull request on my own!

### Code of Conduct

- [X] I agree to follow this project's [Code of Conduct](https://www.apache.org/foundation/policies/conduct)



Discussion / hints:

Fix patch:
diff --git a/docs/en/api/metrics-query-expression.md b/docs/en/api/metrics-query-expression.md
index 95a7d88060ef..b43c615d0f4b 100644
--- a/docs/en/api/metrics-query-expression.md
+++ b/docs/en/api/metrics-query-expression.md
@@ -232,9 +232,8 @@ Expression:
 top_n(<metric_name>, <top_number>, <order>)
 ```
 
-`top_number` is the number of the top results, should be a positive integer.
-
-`order` is the order of the top results. The value of `order` can be `asc` or `des`.
+- `top_number` is the number of the top results, should be a positive integer.
+- `order` is the order of the top results. The value of `order` can be `asc` or `des`.
 
 For example:
 If we want to query the current service's top 10 instances with the highest `service_instance_cpm` metric value, we can use the following expression
@@ -273,11 +272,11 @@ AggregateLabels Operation takes an expression and performs an aggregate calculat
 
 Expression:
 ```text
-aggregate_labels(Expression, AggregateType<Optional>(<label1_name>,<label2_name>...))
+aggregate_labels(Expression, <AggregateType>(<label1_name>,<label2_name>...))
 ```
 
 - `AggregateType` is the type of the aggregation operation.
-- `<label1_name>,<label2_name>...` is the label names that need to be aggregated. If not specified, all labels will be aggregated.
+- `<label1_name>,<label2_name>...` is the label names that need to be aggregated. If not specified, all labels will be aggregated. Optional.
 
 | AggregateType | Definition                                         | ExpressionResultType |
 |---------------|----------------------------------------------------|----------------------|
@@ -379,6 +378,62 @@ V(T1)-V(T1-2), V(T2)-V(T1-1), V(T3)-V(T1)
 ### Result Type
 TIME_SERIES_VALUES.
 
+## Sort Operation
+### SortValues Operation
+SortValues Operation takes an expression and sorts the values of the input expression result.
+
+Expression:
+```text
+sort_values(Expression, <limit>, <order>)
+```
+- `limit` is the number of the sort results, should be a positive integer, if not specified, will return all results. Optional.
+- `order` is the order of the sort results. The value of `order` can be `asc` or `des`.
+
+For example:
+If we want to sort the `service_resp_time` metric values in descending order and get the top 10 values, we can use the following expression:
+```text
+sort_values(service_resp_time, 10, des)
+```
+
+#### Result Type
+The result type follows the input expression.
+
+### SortLabelValues Operation
+SortLabelValues Operation takes an expression and sorts the label values of the input expression result. This function uses `natural sort order`.
+
+Expression:
+```text
+sort_label_values(Expression, <order>, <label1_name>, <label2_name> ...)
+```
+- `order` is the order of the sort results. The value of `order` can be `asc` or `des`.
+- `<label1_name>, <label2_name> ...` is the label names that need to be sorted by their values. At least one label name should be specified.
+The labels in the head of the list will be sorted first, and if the label not be included in the expression result will be ignored.
+
+For example:
+If we want to sort the `service_percentile` metric label values in descending order by the `p` label, we can use the following expression:
+```text
+sort_label_values(service_percentile{p='50,75,90,95,99'}, des, p)
+```
+
+For multiple labels, assume the metric has 2 labels：
+```text
+metric{label1='a', label2='2a'} 
+metric{label1='a', label2='2c'}
+metric{label1='b', label2='2a'}
+metric{label1='b', label2='2c'}
+```
+If we want to sort the `metric` metric label values in descending order by the `label1` and `label2` labels, we can use the following expression:
+```text
+sort_label_values(metric, des, label1, label2)
+```
+And the result will be:
+```text
+metric{label1='b', label2='2c'}
+metric{label1='b', label2='2a'}
+metric{label1='a', label2='2c'}
+metric{label1='a', label2='2a'}
+```
+
 ## Expression Query Example
 ### Labeled Value Metrics
 ```text
diff --git a/docs/en/changes/changes.md b/docs/en/changes/changes.md
index a14d03b635cc..8b9f28a1d277 100644
--- a/docs/en/changes/changes.md
+++ b/docs/en/changes/changes.md
@@ -120,6 +120,7 @@
 * Add component definition(ID=152) for `c3p0`(JDBC3 Connection and Statement Pooling).
 * Fix MQE `top_n` global query. 
 * Fix inaccurate Pulsar and Bookkeeper metrics.
+* MQE support `sort_values` and `sort_label_values` functions.
 
 #### UI
 
diff --git a/oap-server/mqe-grammar/src/main/antlr4/org/apache/skywalking/mqe/rt/grammar/MQELexer.g4 b/oap-server/mqe-grammar/src/main/antlr4/org/apache/skywalking/mqe/rt/grammar/MQELexer.g4
index 7302c7ed7ba9..b91139ee8552 100644
--- a/oap-server/mqe-grammar/src/main/antlr4/org/apache/skywalking/mqe/rt/grammar/MQELexer.g4
+++ b/oap-server/mqe-grammar/src/main/antlr4/org/apache/skywalking/mqe/rt/grammar/MQELexer.g4
@@ -81,6 +81,10 @@ DES options { caseInsensitive=true; }: 'des';
 // AGGREGATE_LABELS
 AGGREGATE_LABELS:   'aggregate_labels';
 
+// Sort
+SORT_VALUES: 'sort_values';
+SORT_LABEL_VALUES: 'sort_label_values';
+
 // Literals
 INTEGER: Digit+;
 DECIMAL: Digit+ DOT Digit+;
diff --git a/oap-server/mqe-grammar/src/main/antlr4/org/apache/skywalking/mqe/rt/grammar/MQEParser.g4 b/oap-server/mqe-grammar/src/main/antlr4/org/apache/skywalking/mqe/rt/grammar/MQEParser.g4
index b8768392b850..d83c72365c82 100644
--- a/oap-server/mqe-grammar/src/main/antlr4/org/apache/skywalking/mqe/rt/grammar/MQEParser.g4
+++ b/oap-server/mqe-grammar/src/main/antlr4/org/apache/skywalking/mqe/rt/grammar/MQEParser.g4
@@ -36,6 +36,8 @@ expression
     | topN L_PAREN metric COMMA INTEGER COMMA order R_PAREN  #topNOP
     | relabels L_PAREN expression COMMA label COMMA replaceLabel R_PAREN #relablesOP
     | aggregateLabels L_PAREN expression COMMA aggregateLabelsFunc R_PAREN #aggregateLabelsOp
+    | sort_values L_PAREN expression (COMMA INTEGER)? COMMA order R_PAREN #sortValuesOP
+    | sort_label_values L_PAREN expression COMMA order COMMA labelNameList R_PAREN #sortLabelValuesOP
     ;
 
 expressionList
@@ -91,3 +93,9 @@ aggregateLabelsFunc: aggregateLabelsFuncName (L_PAREN labelNameList R_PAREN)?;
 
 aggregateLabelsFuncName:
     AVG | SUM | MAX | MIN;
+
+sort_values:
+    SORT_VALUES;
+
+sort_label_values:
+    SORT_LABEL_VALUES;
diff --git a/oap-server/mqe-rt/src/main/java/org/apache/skywalking/mqe/rt/MQEVisitorBase.java b/oap-server/mqe-rt/src/main/java/org/apache/skywalking/mqe/rt/MQEVisitorBase.java
index 23b2d677d630..4416176b83ea 100644
--- a/oap-server/mqe-rt/src/main/java/org/apache/skywalking/mqe/rt/MQEVisitorBase.java
+++ b/oap-server/mqe-rt/src/main/java/org/apache/skywalking/mqe/rt/MQEVisitorBase.java
@@ -22,6 +22,7 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.skywalking.mqe.rt.grammar.MQEParser;
 import org.apache.skywalking.mqe.rt.grammar.MQEParserBaseVisitor;
@@ -31,6 +32,8 @@
 import org.apache.skywalking.mqe.rt.operation.CompareOp;
 import org.apache.skywalking.mqe.rt.operation.LogicalFunctionOp;
 import org.apache.skywalking.mqe.rt.operation.MathematicalFunctionOp;
+import org.apache.skywalking.mqe.rt.operation.SortLabelValuesOp;
+import org.apache.skywalking.mqe.rt.operation.SortValuesOp;
 import org.apache.skywalking.mqe.rt.operation.TrendOp;
 import org.apache.skywalking.mqe.rt.type.ExpressionResult;
 import org.apache.skywalking.mqe.rt.exception.IllegalExpressionException;
@@ -311,6 +314,42 @@ public ExpressionResult visitTrendOP(MQEParser.TrendOPContext ctx) {
         }
     }
 
+    @Override
+    public ExpressionResult visitSortValuesOP(MQEParser.SortValuesOPContext ctx) {
+        ExpressionResult result = visit(ctx.expression());
+        int order = ctx.order().getStart().getType();
+        Optional<Integer> limit = Optional.empty();
+        if (ctx.INTEGER() != null) {
+            limit = Optional.of(Integer.valueOf(ctx.INTEGER().getText()));
+        }
+        try {
+            return SortValuesOp.doSortValuesOp(result, limit, order);
+        } catch (IllegalExpressionException e) {
+            ExpressionResult errorResult = new ExpressionResult();
+            errorResult.setType(ExpressionResultType.UNKNOWN);
+            errorResult.setError(e.getMessage());
+            return errorResult;
+        }
+    }
+
+    @Override
+    public ExpressionResult visitSortLabelValuesOP(MQEParser.SortLabelValuesOPContext ctx) {
+        ExpressionResult result = visit(ctx.expression());
+        int order = ctx.order().getStart().getType();
+        List<String> labelNames = new ArrayList<>();
+        for (MQEParser.LabelNameContext labelNameContext : ctx.labelNameList().labelName()) {
+            labelNames.add(labelNameContext.getText());
+        }
+        try {
+            return SortLabelValuesOp.doSortLabelValuesOp(result, order, labelNames);
+        } catch (IllegalExpressionException e) {
+            ExpressionResult errorResult = new ExpressionResult();
+            errorResult.setType(ExpressionResultType.UNKNOWN);
+            errorResult.setError(e.getMessage());
+            return errorResult;
+        }
+    }
+
     @Override
     public abstract ExpressionResult visitMetric(MQEParser.MetricContext ctx);
 
diff --git a/oap-server/mqe-rt/src/main/java/org/apache/skywalking/mqe/rt/operation/SortLabelValuesOp.java b/oap-server/mqe-rt/src/main/java/org/apache/skywalking/mqe/rt/operation/SortLabelValuesOp.java
new file mode 100644
index 000000000000..828e5e84a55e
--- /dev/null
+++ b/oap-server/mqe-rt/src/main/java/org/apache/skywalking/mqe/rt/operation/SortLabelValuesOp.java
@@ -0,0 +1,106 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package org.apache.skywalking.mqe.rt.operation;
+
+import java.util.Comparator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.skywalking.mqe.rt.exception.IllegalExpressionException;
+import org.apache.skywalking.mqe.rt.grammar.MQEParser;
+import org.apache.skywalking.mqe.rt.type.ExpressionResult;
+import org.apache.skywalking.mqe.rt.type.MQEValues;
+import org.apache.skywalking.oap.server.core.query.type.KeyValue;
+import org.apache.skywalking.oap.server.library.util.CollectionUtils;
+
+import static java.util.stream.Collectors.groupingBy;
+import static java.util.stream.Collectors.toList;
+
+@Slf4j
+public class SortLabelValuesOp {
+    public static ExpressionResult doSortLabelValuesOp(ExpressionResult expResult,
+                                                      int order,
+                                                      List<String> labelNames) throws IllegalExpressionException {
+        if (CollectionUtils.isNotEmpty(labelNames)) {
+            labelNames = labelNames.stream().distinct().collect(toList());
+            if (MQEParser.ASC == order) {
+                expResult.setResults(
+                    sort(expResult.getResults(), labelNames, labelNames.get(0), Comparator.naturalOrder()));
+            } else if (MQEParser.DES == order) {
+                expResult.setResults(
+                    sort(expResult.getResults(), labelNames, labelNames.get(0), Comparator.reverseOrder()));
+            } else {
+                throw new IllegalExpressionException("Unsupported sort order.");
+            }
+        }
+        return expResult;
+    }
+
+    private static List<MQEValues> sort(List<MQEValues> results,
+                                        List<String> sortLabels,
+                                        String currentSortLabel,
+                                        Comparator<String> comparator) {
+        if (!sortLabels.contains(currentSortLabel)) {
+            log.error("Current sort label {} not found in the sort labels {} ", currentSortLabel, sortLabels);
+            return results;
+        }
+        if (sortLabels.indexOf(
+            currentSortLabel) == sortLabels.size() - 1) { //only one label or the latest label no need to group
+            results.sort(Comparator.comparing(mqeValues -> mqeValues.getMetric()
+                                                                    .getLabels()
+                                                                    .stream()
+                                                                    .filter(kv -> kv.getKey().equals(currentSortLabel))
+                                                                    .findFirst()
+                                                                    .orElse(new KeyValue(currentSortLabel, ""))
+                                                                    .getValue(), comparator));
+        } else {
+            LinkedHashMap<KeyValue, List<MQEValues>> groupResult = group(results, currentSortLabel);
+            LinkedHashMap<KeyValue, List<MQEValues>> sortedGroup = new LinkedHashMap<>(groupResult.size());
+            for (Map.Entry<KeyValue, List<MQEValues>> entry : groupResult.entrySet()) {
+                //sort by next label
+                List<MQEValues> sortedResult = sort(
+                    entry.getValue(), sortLabels, sortLabels.get(sortLabels.indexOf(currentSortLabel) + 1), comparator);
+                sortedGroup.put(entry.getKey(), sortedResult);
+            }
+            //combine the sorted group
+            results = sortedGroup.keySet()
+                                 .stream()
+                                 .sorted(Comparator.comparing(KeyValue::getValue, comparator))
+                                 .flatMap(keyValue -> sortedGroup.get(keyValue).stream())
+                                 .collect(toList());
+        }
+        return results;
+    }
+
+    //group by current label for sub sorting
+    private static LinkedHashMap<KeyValue, List<MQEValues>> group(List<MQEValues> results, String labelName) {
+        return results
+            .stream()
+            .collect(groupingBy(
+                mqeValues -> mqeValues.getMetric().getLabels()
+                                      .stream()
+                                      .filter(kv -> kv.getKey().equals(labelName))
+                                      .findFirst().orElse(new KeyValue(labelName, "")),
+                LinkedHashMap::new,
+                toList()
+            ));
+    }
+}
+
diff --git a/oap-server/mqe-rt/src/main/java/org/apache/skywalking/mqe/rt/operation/SortValuesOp.java b/oap-server/mqe-rt/src/main/java/org/apache/skywalking/mqe/rt/operation/SortValuesOp.java
new file mode 100644
index 000000000000..44053555f994
--- /dev/null
+++ b/oap-server/mqe-rt/src/main/java/org/apache/skywalking/mqe/rt/operation/SortValuesOp.java
@@ -0,0 +1,57 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package org.apache.skywalking.mqe.rt.operation;
+
+import java.util.Comparator;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+import org.apache.skywalking.mqe.rt.exception.IllegalExpressionException;
+import org.apache.skywalking.mqe.rt.grammar.MQEParser;
+import org.apache.skywalking.mqe.rt.type.ExpressionResult;
+import org.apache.skywalking.mqe.rt.type.MQEValue;
+
+public class SortValuesOp {
+    public static ExpressionResult doSortValuesOp(ExpressionResult expResult,
+                                                 Optional<Integer> limit,
+                                                 int order) throws IllegalExpressionException {
+        if (MQEParser.ASC == order || MQEParser.DES == order) {
+            expResult.getResults().forEach(mqeValues -> {
+                List<MQEValue> values = mqeValues.getValues()
+                                                 .stream()
+                                                 // Filter out empty values
+                                                 .filter(mqeValue -> !mqeValue.isEmptyValue())
+                                                 .sorted(MQEParser.ASC == order ? Comparator.comparingDouble(
+                                                     MQEValue::getDoubleValue) :
+                                                             Comparator.comparingDouble(MQEValue::getDoubleValue)
+                                                                       .reversed())
+                                                 .collect(
+                                                     Collectors.toList());
+                if (limit.isPresent() && limit.get() < values.size()) {
+                    mqeValues.setValues(values.subList(0, limit.get()));
+                } else {
+                    mqeValues.setValues(values);
+                }
+            });
+        } else {
+            throw new IllegalExpressionException("Unsupported sort order.");
+        }
+        return expResult;
+    }
+}
diff --git a/skywalking-ui b/skywalking-ui
index 13b2693f2919..e4b2203cf6f3 160000
--- a/skywalking-ui
+++ b/skywalking-ui
@@ -1,1 +1,1 @@
-Subproject commit 13b2693f291903349ebaffa661092e2747141f1c
+Subproject commit e4b2203cf6f33b1fd84b79ced30ee7c50e15e6d8


Test patch:
diff --git a/oap-server/mqe-rt/src/test/java/org/apache/skywalking/mqe/rt/MockData.java b/oap-server/mqe-rt/src/test/java/org/apache/skywalking/mqe/rt/MockData.java
index 92fa7a8a6e33..2a53df8f0dca 100644
--- a/oap-server/mqe-rt/src/test/java/org/apache/skywalking/mqe/rt/MockData.java
+++ b/oap-server/mqe-rt/src/test/java/org/apache/skywalking/mqe/rt/MockData.java
@@ -65,6 +65,61 @@ public ExpressionResult newSeriesLabeledResult() {
         return seriesLabeled;
     }
 
+    public ExpressionResult newSeriesComplexLabeledResult() {
+        ExpressionResult seriesLabeled = new ExpressionResult();
+        seriesLabeled.setLabeledResult(true);
+        seriesLabeled.setType(ExpressionResultType.TIME_SERIES_VALUES);
+        MQEValues mqeValues111 = new MQEValues();
+        mqeValues111.setMetric(newMetadata("label", "a"));
+        mqeValues111.getMetric().getLabels().add(new KeyValue("label2", "2a"));
+        mqeValues111.getMetric().getLabels().add(new KeyValue("label3", "31"));
+        mqeValues111.getValues().add(newMQEValue("100", 100));
+        MQEValues mqeValues112 = new MQEValues();
+        mqeValues112.setMetric(newMetadata("label", "a"));
+        mqeValues112.getMetric().getLabels().add(new KeyValue("label2", "2a"));
+        mqeValues112.getMetric().getLabels().add(new KeyValue("label3", "32"));
+        mqeValues112.getValues().add(newMQEValue("100", 101));
+        MQEValues mqeValues121 = new MQEValues();
+        mqeValues121.setMetric(newMetadata("label", "a"));
+        mqeValues121.getMetric().getLabels().add(new KeyValue("label2", "2b"));
+        mqeValues121.getMetric().getLabels().add(new KeyValue("label3", "31"));
+        mqeValues121.getValues().add(newMQEValue("100", 102));
+        MQEValues mqeValues122 = new MQEValues();
+        mqeValues122.setMetric(newMetadata("label", "a"));
+        mqeValues122.getMetric().getLabels().add(new KeyValue("label2", "2b"));
+        mqeValues122.getMetric().getLabels().add(new KeyValue("label3", "32"));
+        mqeValues122.getValues().add(newMQEValue("100", 103));
+        MQEValues mqeValues211 = new MQEValues();
+        mqeValues211.setMetric(newMetadata("label", "b"));
+        mqeValues211.getMetric().getLabels().add(new KeyValue("label2", "2a"));
+        mqeValues211.getMetric().getLabels().add(new KeyValue("label3", "31"));
+        mqeValues211.getValues().add(newMQEValue("100", 104));
+        MQEValues mqeValues212 = new MQEValues();
+        mqeValues212.setMetric(newMetadata("label", "b"));
+        mqeValues212.getMetric().getLabels().add(new KeyValue("label2", "2a"));
+        mqeValues212.getMetric().getLabels().add(new KeyValue("label3", "32"));
+        mqeValues212.getValues().add(newMQEValue("100", 105));
+        MQEValues mqeValues221 = new MQEValues();
+        mqeValues221.setMetric(newMetadata("label", "b"));
+        mqeValues221.getMetric().getLabels().add(new KeyValue("label2", "2b"));
+        mqeValues221.getMetric().getLabels().add(new KeyValue("label3", "31"));
+        mqeValues221.getValues().add(newMQEValue("100", 106));
+        MQEValues mqeValues222 = new MQEValues();
+        mqeValues222.setMetric(newMetadata("label", "b"));
+        mqeValues222.getMetric().getLabels().add(new KeyValue("label2", "2b"));
+        mqeValues222.getMetric().getLabels().add(new KeyValue("label3", "32"));
+        mqeValues222.getValues().add(newMQEValue("100", 107));
+        seriesLabeled.getResults().add(mqeValues111);
+        seriesLabeled.getResults().add(mqeValues112);
+        seriesLabeled.getResults().add(mqeValues121);
+        seriesLabeled.getResults().add(mqeValues122);
+        seriesLabeled.getResults().add(mqeValues211);
+        seriesLabeled.getResults().add(mqeValues212);
+        seriesLabeled.getResults().add(mqeValues221);
+        seriesLabeled.getResults().add(mqeValues222);
+        return seriesLabeled;
+    }
+
     public ExpressionResult newSeriesLabeledResult(double id1001, double id3001, double id1002, double id3002) {
         ExpressionResult seriesLabeled = new ExpressionResult();
         seriesLabeled.setLabeledResult(true);
diff --git a/oap-server/mqe-rt/src/test/java/org/apache/skywalking/mqe/rt/SortLabelValuesOpTest.java b/oap-server/mqe-rt/src/test/java/org/apache/skywalking/mqe/rt/SortLabelValuesOpTest.java
new file mode 100644
index 000000000000..61ab428bccec
--- /dev/null
+++ b/oap-server/mqe-rt/src/test/java/org/apache/skywalking/mqe/rt/SortLabelValuesOpTest.java
@@ -0,0 +1,115 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package org.apache.skywalking.mqe.rt;
+
+import java.util.List;
+import org.apache.skywalking.mqe.rt.exception.IllegalExpressionException;
+import org.apache.skywalking.mqe.rt.grammar.MQEParser;
+import org.apache.skywalking.mqe.rt.operation.SortLabelValuesOp;
+import org.apache.skywalking.mqe.rt.type.ExpressionResult;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class SortLabelValuesOpTest {
+    private final MockData mockData = new MockData();
+
+    @Test
+    public void sortLabelValueTest() throws IllegalExpressionException {
+        //des
+        ExpressionResult des = SortLabelValuesOp.doSortLabelValuesOp(mockData.newSeriesComplexLabeledResult(),
+                                                                    MQEParser.DES, List.of("label", "label2", "label3")
+        );
+        assertDes(des);
+        //asc
+        ExpressionResult asc = SortLabelValuesOp.doSortLabelValuesOp(des,
+                                                                    MQEParser.ASC, List.of("label", "label2", "label3")
+        );
+        assertAsc(asc);
+        // distinct
+        ExpressionResult distinctDes = SortLabelValuesOp.doSortLabelValuesOp(mockData.newSeriesComplexLabeledResult(),
+                                                                            MQEParser.DES, List.of("label", "label2", "label3", "label")
+        );
+        assertDes(distinctDes);
+        // ignore
+        ExpressionResult ignoreDes = SortLabelValuesOp.doSortLabelValuesOp(mockData.newSeriesComplexLabeledResult(),
+                                                                          MQEParser.DES, List.of("label", "labelIgnore", "label2", "label3")
+        );
+        assertDes(ignoreDes);
+    }
+
+    private void assertDes(ExpressionResult des) {
+        //label
+        Assertions.assertEquals("b", des.getResults().get(0).getMetric().getLabels().get(0).getValue());
+        Assertions.assertEquals("b", des.getResults().get(1).getMetric().getLabels().get(0).getValue());
+        Assertions.assertEquals("b", des.getResults().get(2).getMetric().getLabels().get(0).getValue());
+        Assertions.assertEquals("b", des.getResults().get(3).getMetric().getLabels().get(0).getValue());
+        Assertions.assertEquals("a", des.getResults().get(4).getMetric().getLabels().get(0).getValue());
+        Assertions.assertEquals("a", des.getResults().get(5).getMetric().getLabels().get(0).getValue());
+        Assertions.assertEquals("a", des.getResults().get(6).getMetric().getLabels().get(0).getValue());
+        Assertions.assertEquals("a", des.getResults().get(7).getMetric().getLabels().get(0).getValue());
+        // label2
+        Assertions.assertEquals("2b", des.getResults().get(0).getMetric().getLabels().get(1).getValue());
+        Assertions.assertEquals("2b", des.getResults().get(1).getMetric().getLabels().get(1).getValue());
+        Assertions.assertEquals("2a", des.getResults().get(2).getMetric().getLabels().get(1).getValue());
+        Assertions.assertEquals("2a", des.getResults().get(3).getMetric().getLabels().get(1).getValue());
+        Assertions.assertEquals("2b", des.getResults().get(4).getMetric().getLabels().get(1).getValue());
+        Assertions.assertEquals("2b", des.getResults().get(5).getMetric().getLabels().get(1).getValue());
+        Assertions.assertEquals("2a", des.getResults().get(6).getMetric().getLabels().get(1).getValue());
+        Assertions.assertEquals("2a", des.getResults().get(7).getMetric().getLabels().get(1).getValue());
+        // label3
+        Assertions.assertEquals("32", des.getResults().get(0).getMetric().getLabels().get(2).getValue());
+        Assertions.assertEquals("31", des.getResults().get(1).getMetric().getLabels().get(2).getValue());
+        Assertions.assertEquals("32", des.getResults().get(2).getMetric().getLabels().get(2).getValue());
+        Assertions.assertEquals("31", des.getResults().get(3).getMetric().getLabels().get(2).getValue());
+        Assertions.assertEquals("32", des.getResults().get(4).getMetric().getLabels().get(2).getValue());
+        Assertions.assertEquals("31", des.getResults().get(5).getMetric().getLabels().get(2).getValue());
+        Assertions.assertEquals("32", des.getResults().get(6).getMetric().getLabels().get(2).getValue());
+        Assertions.assertEquals("31", des.getResults().get(7).getMetric().getLabels().get(2).getValue());
+    }
+
+    private void assertAsc(ExpressionResult asc) {
+        //label
+        Assertions.assertEquals("a", asc.getResults().get(0).getMetric().getLabels().get(0).getValue());
+        Assertions.assertEquals("a", asc.getResults().get(1).getMetric().getLabels().get(0).getValue());
+        Assertions.assertEquals("a", asc.getResults().get(2).getMetric().getLabels().get(0).getValue());
+        Assertions.assertEquals("a", asc.getResults().get(3).getMetric().getLabels().get(0).getValue());
+        Assertions.assertEquals("b", asc.getResults().get(4).getMetric().getLabels().get(0).getValue());
+        Assertions.assertEquals("b", asc.getResults().get(5).getMetric().getLabels().get(0).getValue());
+        Assertions.assertEquals("b", asc.getResults().get(6).getMetric().getLabels().get(0).getValue());
+        Assertions.assertEquals("b", asc.getResults().get(7).getMetric().getLabels().get(0).getValue());
+        // label2
+        Assertions.assertEquals("2a", asc.getResults().get(0).getMetric().getLabels().get(1).getValue());
+        Assertions.assertEquals("2a", asc.getResults().get(1).getMetric().getLabels().get(1).getValue());
+        Assertions.assertEquals("2b", asc.getResults().get(2).getMetric().getLabels().get(1).getValue());
+        Assertions.assertEquals("2b", asc.getResults().get(3).getMetric().getLabels().get(1).getValue());
+        Assertions.assertEquals("2a", asc.getResults().get(4).getMetric().getLabels().get(1).getValue());
+        Assertions.assertEquals("2a", asc.getResults().get(5).getMetric().getLabels().get(1).getValue());
+        Assertions.assertEquals("2b", asc.getResults().get(6).getMetric().getLabels().get(1).getValue());
+        Assertions.assertEquals("2b", asc.getResults().get(7).getMetric().getLabels().get(1).getValue());
+        // label3
+        Assertions.assertEquals("31", asc.getResults().get(0).getMetric().getLabels().get(2).getValue());
+        Assertions.assertEquals("32", asc.getResults().get(1).getMetric().getLabels().get(2).getValue());
+        Assertions.assertEquals("31", asc.getResults().get(2).getMetric().getLabels().get(2).getValue());
+        Assertions.assertEquals("32", asc.getResults().get(3).getMetric().getLabels().get(2).getValue());
+        Assertions.assertEquals("31", asc.getResults().get(4).getMetric().getLabels().get(2).getValue());
+        Assertions.assertEquals("32", asc.getResults().get(5).getMetric().getLabels().get(2).getValue());
+        Assertions.assertEquals("31", asc.getResults().get(6).getMetric().getLabels().get(2).getValue());
+        Assertions.assertEquals("32", asc.getResults().get(7).getMetric().getLabels().get(2).getValue());
+    }
+}
diff --git a/oap-server/mqe-rt/src/test/java/org/apache/skywalking/mqe/rt/SortValuesOpTest.java b/oap-server/mqe-rt/src/test/java/org/apache/skywalking/mqe/rt/SortValuesOpTest.java
new file mode 100644
index 000000000000..fd964b266650
--- /dev/null
+++ b/oap-server/mqe-rt/src/test/java/org/apache/skywalking/mqe/rt/SortValuesOpTest.java
@@ -0,0 +1,66 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package org.apache.skywalking.mqe.rt;
+
+import java.util.Optional;
+import org.apache.skywalking.mqe.rt.exception.IllegalExpressionException;
+import org.apache.skywalking.mqe.rt.grammar.MQEParser;
+import org.apache.skywalking.mqe.rt.operation.SortValuesOp;
+import org.apache.skywalking.mqe.rt.type.ExpressionResult;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class SortValuesOpTest {
+    private final MockData mockData = new MockData();
+
+    @Test
+    public void sortValueTest() throws IllegalExpressionException {
+        //no label
+        ExpressionResult des = SortValuesOp.doSortValuesOp(mockData.newSeriesNoLabeledResult(), Optional.of(3),
+                                                          MQEParser.DES);
+        Assertions.assertEquals(300, des.getResults().get(0).getValues().get(0).getDoubleValue());
+        Assertions.assertEquals(100, des.getResults().get(0).getValues().get(1).getDoubleValue());
+        ExpressionResult asc = SortValuesOp.doSortValuesOp(mockData.newSeriesNoLabeledResult(), Optional.of(3),
+                                                          MQEParser.ASC);
+        Assertions.assertEquals(100, asc.getResults().get(0).getValues().get(0).getDoubleValue());
+        Assertions.assertEquals(300, asc.getResults().get(0).getValues().get(1).getDoubleValue());
+
+        //labeled
+        ExpressionResult desLabeled = SortValuesOp.doSortValuesOp(mockData.newSeriesLabeledResult(), Optional.of(3),
+                                                                 MQEParser.DES);
+        Assertions.assertEquals(300, desLabeled.getResults().get(0).getValues().get(0).getDoubleValue());
+        Assertions.assertEquals(100, desLabeled.getResults().get(0).getValues().get(1).getDoubleValue());
+        Assertions.assertEquals(301, desLabeled.getResults().get(1).getValues().get(0).getDoubleValue());
+        Assertions.assertEquals(101, desLabeled.getResults().get(1).getValues().get(1).getDoubleValue());
+        ExpressionResult ascLabeled = SortValuesOp.doSortValuesOp(mockData.newSeriesLabeledResult(), Optional.of(2),
+                                                                 MQEParser.ASC);
+        Assertions.assertEquals(100, ascLabeled.getResults().get(0).getValues().get(0).getDoubleValue());
+        Assertions.assertEquals(300, ascLabeled.getResults().get(0).getValues().get(1).getDoubleValue());
+        Assertions.assertEquals(101, ascLabeled.getResults().get(1).getValues().get(0).getDoubleValue());
+        Assertions.assertEquals(301, ascLabeled.getResults().get(1).getValues().get(1).getDoubleValue());
+
+        //limit
+        ExpressionResult desLabeledLimit = SortValuesOp.doSortValuesOp(mockData.newSeriesLabeledResult(), Optional.of(1),
+                                                                      MQEParser.DES);
+        Assertions.assertEquals(1, desLabeledLimit.getResults().get(0).getValues().size());
+        Assertions.assertEquals(1, desLabeledLimit.getResults().get(1).getValues().size());
+        Assertions.assertEquals(300, desLabeledLimit.getResults().get(0).getValues().get(0).getDoubleValue());
+        Assertions.assertEquals(301, desLabeledLimit.getResults().get(1).getValues().get(0).getDoubleValue());
+    }
+}
diff --git a/test/e2e-v2/cases/mqe/expected/sort-label-value-OP.yml b/test/e2e-v2/cases/mqe/expected/sort-label-value-OP.yml
new file mode 100644
index 000000000000..5fedeb3c0a7f
--- /dev/null
+++ b/test/e2e-v2/cases/mqe/expected/sort-label-value-OP.yml
@@ -0,0 +1,59 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+type: TIME_SERIES_VALUES
+results:
+  {{- contains .results }}
+- metric:
+    labels:
+      - key: p
+        value: "90"
+  values:
+    {{- contains .values }}
+    - id: {{ notEmpty .id }}
+      value: {{ .value }}
+      traceid: null
+    - id: {{ notEmpty .id }}
+      value: null
+      traceid: null
+    {{- end}}
+- metric:
+    labels:
+      - key: p
+        value: "75"
+  values:
+    {{- contains .values }}
+    - id: {{ notEmpty .id }}
+      value: {{ .value }}
+      traceid: null
+    - id: {{ notEmpty .id }}
+      value: null
+      traceid: null
+    {{- end}}
+- metric:
+    labels:
+      - key: p
+        value: "50"
+  values:
+    {{- contains .values }}
+    - id: {{ notEmpty .id }}
+      value: {{ .value }}
+      traceid: null
+    - id: {{ notEmpty .id }}
+      value: null
+      traceid: null
+    {{- end}}
+  {{- end}}
+error: null
diff --git a/test/e2e-v2/cases/mqe/expected/sort-value-OP.yml b/test/e2e-v2/cases/mqe/expected/sort-value-OP.yml
new file mode 100644
index 000000000000..ee5bd130dd3e
--- /dev/null
+++ b/test/e2e-v2/cases/mqe/expected/sort-value-OP.yml
@@ -0,0 +1,35 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+type: TIME_SERIES_VALUES
+results:
+  {{- contains .results }}
+  - metric:
+      labels:
+        {{- contains .metric.labels }}
+        - key: p
+          value: {{ .value }}
+        {{- end}}
+    values:
+      {{- contains .values }}
+      - id: {{ notEmpty .id }}
+        value: {{ .value }}
+        traceid: null
+      - id: {{ notEmpty .id }}
+        value: {{ .value }}
+        traceid: null
+      {{- end}}
+  {{- end}}
+error: null
diff --git a/test/e2e-v2/cases/mqe/mqe-cases.yaml b/test/e2e-v2/cases/mqe/mqe-cases.yaml
index 197336934318..772bb756650c 100644
--- a/test/e2e-v2/cases/mqe/mqe-cases.yaml
+++ b/test/e2e-v2/cases/mqe/mqe-cases.yaml
@@ -81,3 +81,14 @@ cases:
     expected: expected/trend-OP.yml
   - query: swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql metrics exec --expression="rate(service_resp_time,2)" --service-name=e2e-service-provider
     expected: expected/trend-OP.yml
+
+  # sort-OP e2e used for test MQE expression, more tests can refer to the UT
+  # sort-value-OP
+  - query: swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql metrics exec --expression="sort_values(service_percentile,2,asc)" --service-name=e2e-service-provider
+    expected: expected/sort-value-OP.yml
+  - query: swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql metrics exec --expression="sort_values(service_percentile,des)" --service-name=e2e-service-provider
+    expected: expected/sort-value-OP.yml
+
+  # sort-label-value-OP
+  - query: swctl --display yaml --base-url=http://${oap_host}:${oap_12800}/graphql metrics exec --expression="sort_label_values(service_percentile{p='50,75,90'},des,p)" --service-name=e2e-service-provider
+    expected: expected/sort-label-value-OP.yml