This is an automated email from the ASF dual-hosted git repository.
yiguolei pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/master by this push:
new f9b151744d optimize topn query if order by columns is prefix of sort
keys of table (#10694)
f9b151744d is described below
commit f9b151744d0870f97efd824bdde3273f04fa0756
Author: Kang <[email protected]>
AuthorDate: Tue Aug 9 09:08:44 2022 +0800
optimize topn query if order by columns is prefix of sort keys of table
(#10694)
* [feature](planner): push limit to olapscan when meet sort.
* if olap_scan_node's sort_info is set, push sort_limit, read_orderby_key
and read_orderby_key_reverse for olap scanner
* There is a common query pattern to find latest time serials data.
eg. SELECT * from t_log WHERE t>t1 AND t<t2 ORDER BY t DESC LIMIT 100
If the ORDER BY columns is the prefix of the sort key of table, it can
be greatly optimized to read much fewer data instead of read all data
between t1 and t2.
By leveraging the same order of ORDER BY columns and sort key of table,
just read the LIMIT N rows for each related segment and merge N rows.
1. set read_orderby_key to true for read_params and _reader_context
if olap_scan_node's sort info is set.
2. set read_orderby_key_reverse to true for read_params and _reader_context
if is_asc_order is false.
3. rowset reader force merge read segments if read_orderby_key is true.
4. block reader and tablet reader force merge read rowsets if
read_orderby_key is true.
5. for ORDER BY DESC, read and compare in reverse order
5.1 segment iterator read backward using a new BackwardBitmapRangeIterator
and
reverse the result block before return to caller.
5.2 VCollectIterator::LevelIteratorComparator, VMergeIteratorContext return
opposite result for _is_reverse order in its compare function.
Co-authored-by: jackwener <[email protected]>
---
be/src/olap/iterators.h | 4 +
be/src/olap/reader.cpp | 41 +++++++++++
be/src/olap/reader.h | 11 +++
be/src/olap/rowset/beta_rowset_reader.cpp | 8 +-
be/src/olap/rowset/rowset_reader_context.h | 4 +
be/src/olap/rowset/segment_v2/segment_iterator.cpp | 66 ++++++++++++++++-
be/src/olap/rowset/segment_v2/segment_iterator.h | 1 +
be/src/vec/core/block.h | 18 +++++
be/src/vec/exec/volap_scan_node.cpp | 4 +
be/src/vec/exec/volap_scan_node.h | 1 +
be/src/vec/exec/volap_scanner.cpp | 17 +++++
be/src/vec/exec/volap_scanner.h | 2 +
be/src/vec/olap/block_reader.cpp | 2 +-
be/src/vec/olap/vcollect_iterator.cpp | 35 ++++++---
be/src/vec/olap/vcollect_iterator.h | 20 ++++-
be/src/vec/olap/vgeneric_iterators.cpp | 30 +++++---
be/src/vec/olap/vgeneric_iterators.h | 2 +-
be/test/vec/exec/vgeneric_iterators_test.cpp | 6 +-
.../org/apache/doris/planner/OlapScanNode.java | 85 +++++++++++++++++++++-
.../org/apache/doris/planner/OriginalPlanner.java | 40 ++++++++++
.../java/org/apache/doris/planner/PlannerTest.java | 21 ++++++
gensrc/thrift/PlanNodes.thrift | 26 ++++---
22 files changed, 395 insertions(+), 49 deletions(-)
diff --git a/be/src/olap/iterators.h b/be/src/olap/iterators.h
index ca6aa6d216..2b793b91c9 100644
--- a/be/src/olap/iterators.h
+++ b/be/src/olap/iterators.h
@@ -94,6 +94,10 @@ public:
TabletSchemaSPtr tablet_schema = nullptr;
bool record_rowids = false;
+ // used for special optimization for query : ORDER BY key DESC LIMIT n
+ bool read_orderby_key_reverse = false;
+ // columns for orderby keys
+ std::vector<uint32_t>* read_orderby_key_columns = nullptr;
};
// Used to read data in RowBlockV2 one by one
diff --git a/be/src/olap/reader.cpp b/be/src/olap/reader.cpp
index 35b07cacc8..99fd896d2a 100644
--- a/be/src/olap/reader.cpp
+++ b/be/src/olap/reader.cpp
@@ -198,13 +198,20 @@ Status TabletReader::_capture_rs_readers(const
ReaderParams& read_params,
// it's ok for rowset to return unordered result
need_ordered_result = false;
}
+
+ if (read_params.read_orderby_key) {
+ need_ordered_result = true;
+ }
}
_reader_context.reader_type = read_params.reader_type;
_reader_context.version = read_params.version;
_reader_context.tablet_schema = _tablet_schema;
_reader_context.need_ordered_result = need_ordered_result;
+ _reader_context.read_orderby_key_reverse =
read_params.read_orderby_key_reverse;
_reader_context.return_columns = &_return_columns;
+ _reader_context.read_orderby_key_columns =
+ _orderby_key_columns.size() > 0 ? &_orderby_key_columns : nullptr;
_reader_context.seek_columns = &_seek_columns;
_reader_context.load_bf_columns = &_load_bf_columns;
_reader_context.load_bf_all_columns = &_load_bf_all_columns;
@@ -264,6 +271,12 @@ Status TabletReader::_init_params(const ReaderParams&
read_params) {
return res;
}
+ res = _init_orderby_keys_param(read_params);
+ if (!res.ok()) {
+ LOG(WARNING) << "fail to init orderby keys param. res=" << res;
+ return res;
+ }
+
_init_seek_columns();
if (_tablet_schema->has_sequence_col()) {
@@ -443,6 +456,34 @@ Status TabletReader::_init_keys_param(const ReaderParams&
read_params) {
return Status::OK();
}
+Status TabletReader::_init_orderby_keys_param(const ReaderParams& read_params)
{
+ if (read_params.start_key.empty()) {
+ return Status::OK();
+ }
+
+ // UNIQUE_KEYS will compare all keys as before
+ if (_tablet_schema->keys_type() == DUP_KEYS) {
+ // find index in vector _return_columns
+ // for the read_orderby_key_num_prefix_columns orderby keys
+ for (uint32_t i = 0; i <
read_params.read_orderby_key_num_prefix_columns; i++) {
+ for (uint32_t idx = 0; idx < _return_columns.size(); idx++) {
+ if (_return_columns[idx] == i) {
+ _orderby_key_columns.push_back(idx);
+ break;
+ }
+ }
+ }
+ if (read_params.read_orderby_key_num_prefix_columns !=
_orderby_key_columns.size()) {
+ LOG(WARNING) << "read_orderby_key_num_prefix_columns !=
_orderby_key_columns.size "
+ << read_params.read_orderby_key_num_prefix_columns <<
" vs. "
+ << _orderby_key_columns.size();
+ return Status::OLAPInternalError(OLAP_ERR_OTHER_ERROR);
+ }
+ }
+
+ return Status::OK();
+}
+
void TabletReader::_init_conditions_param(const ReaderParams& read_params) {
_conditions.set_tablet_schema(_tablet_schema);
_all_conditions.set_tablet_schema(_tablet_schema);
diff --git a/be/src/olap/reader.h b/be/src/olap/reader.h
index f5d2817ead..b02f70ddef 100644
--- a/be/src/olap/reader.h
+++ b/be/src/olap/reader.h
@@ -94,6 +94,12 @@ public:
// used for comapction to record row ids
bool record_rowids = false;
+ // used for special optimization for query : ORDER BY key LIMIT n
+ bool read_orderby_key = false;
+ // used for special optimization for query : ORDER BY key DESC LIMIT n
+ bool read_orderby_key_reverse = false;
+ // num of columns for orderby key
+ size_t read_orderby_key_num_prefix_columns = 0;
void check_validation() const;
@@ -154,6 +160,8 @@ protected:
Status _init_keys_param(const ReaderParams& read_params);
+ Status _init_orderby_keys_param(const ReaderParams& read_params);
+
void _init_conditions_param(const ReaderParams& read_params);
ColumnPredicate* _parse_to_predicate(const TCondition& condition, bool
opposite = false) const;
@@ -179,6 +187,9 @@ protected:
std::set<uint32_t> _load_bf_columns;
std::set<uint32_t> _load_bf_all_columns;
std::vector<uint32_t> _return_columns;
+ // used for special optimization for query : ORDER BY key [ASC|DESC] LIMIT
n
+ // columns for orderby keys
+ std::vector<uint32_t> _orderby_key_columns;
// only use in outer join which change the column nullable which must keep
same in
// vec query engine
std::unordered_set<uint32_t>* _tablet_columns_convert_to_null_set =
nullptr;
diff --git a/be/src/olap/rowset/beta_rowset_reader.cpp
b/be/src/olap/rowset/beta_rowset_reader.cpp
index d699b3ed0d..f7e93bfca9 100644
--- a/be/src/olap/rowset/beta_rowset_reader.cpp
+++ b/be/src/olap/rowset/beta_rowset_reader.cpp
@@ -97,6 +97,8 @@ Status BetaRowsetReader::init(RowsetReaderContext*
read_context) {
read_options.use_page_cache = read_context->use_page_cache;
read_options.tablet_schema = read_context->tablet_schema;
read_options.record_rowids = read_context->record_rowids;
+ read_options.read_orderby_key_reverse =
read_context->read_orderby_key_reverse;
+ read_options.read_orderby_key_columns =
read_context->read_orderby_key_columns;
// load segments
RETURN_NOT_OK(SegmentLoader::instance()->load_segments(
@@ -128,8 +130,12 @@ Status BetaRowsetReader::init(RowsetReaderContext*
read_context) {
_rowset->rowset_meta()->is_segments_overlapping()) {
final_iterator = vectorized::new_merge_iterator(
iterators, read_context->sequence_id_idx,
read_context->is_unique,
- read_context->merged_rows);
+ read_context->read_orderby_key_reverse,
read_context->merged_rows);
} else {
+ if (read_context->read_orderby_key_reverse) {
+ // reverse iterators to read backward for ORDER BY key DESC
+ std::reverse(iterators.begin(), iterators.end());
+ }
final_iterator = vectorized::new_union_iterator(iterators);
}
} else {
diff --git a/be/src/olap/rowset/rowset_reader_context.h
b/be/src/olap/rowset/rowset_reader_context.h
index 5e96774c15..0ee9026f6a 100644
--- a/be/src/olap/rowset/rowset_reader_context.h
+++ b/be/src/olap/rowset/rowset_reader_context.h
@@ -36,6 +36,10 @@ struct RowsetReaderContext {
TabletSchemaSPtr tablet_schema = nullptr;
// whether rowset should return ordered rows.
bool need_ordered_result = true;
+ // used for special optimization for query : ORDER BY key DESC LIMIT n
+ bool read_orderby_key_reverse = false;
+ // columns for orderby keys
+ std::vector<uint32_t>* read_orderby_key_columns = nullptr;
// projection columns: the set of columns rowset reader should return
const std::vector<uint32_t>* return_columns = nullptr;
// set of columns used to prune rows that doesn't satisfy key ranges and
`conditions`.
diff --git a/be/src/olap/rowset/segment_v2/segment_iterator.cpp
b/be/src/olap/rowset/segment_v2/segment_iterator.cpp
index 0566e29b15..24035a62fc 100644
--- a/be/src/olap/rowset/segment_v2/segment_iterator.cpp
+++ b/be/src/olap/rowset/segment_v2/segment_iterator.cpp
@@ -41,9 +41,12 @@ namespace segment_v2 {
// Example:
// input bitmap: [0 1 4 5 6 7 10 15 16 17 18 19]
// output ranges: [0,2), [4,8), [10,11), [15,20) (when max_range_size=10)
-// output ranges: [0,2), [4,8), [10,11), [15,18), [18,20) (when
max_range_size=3)
+// output ranges: [0,2), [4,7), [7,8), [10,11), [15,18), [18,20) (when
max_range_size=3)
class SegmentIterator::BitmapRangeIterator {
public:
+ BitmapRangeIterator() {}
+ virtual ~BitmapRangeIterator() = default;
+
explicit BitmapRangeIterator(const roaring::Roaring& bitmap) {
roaring_init_iterator(&bitmap.roaring, &_iter);
_read_next_batch();
@@ -53,7 +56,7 @@ public:
// read next range into [*from, *to) whose size <= max_range_size.
// return false when there is no more range.
- bool next_range(const uint32_t max_range_size, uint32_t* from, uint32_t*
to) {
+ virtual bool next_range(const uint32_t max_range_size, uint32_t* from,
uint32_t* to) {
if (_eof) {
return false;
}
@@ -104,6 +107,43 @@ private:
bool _eof = false;
};
+// A backward range iterator for roaring bitmap. Output ranges use closed-open
form, like [from, to).
+// Example:
+// input bitmap: [0 1 4 5 6 7 10 15 16 17 18 19]
+// output ranges: , [15,20), [10,11), [4,8), [0,2) (when max_range_size=10)
+// output ranges: [17,20), [15,17), [10,11), [5,8), [4, 5), [0,2) (when
max_range_size=3)
+class SegmentIterator::BackwardBitmapRangeIterator : public
SegmentIterator::BitmapRangeIterator {
+public:
+ explicit BackwardBitmapRangeIterator(const roaring::Roaring& bitmap) {
+ roaring_init_iterator_last(&bitmap.roaring, &_riter);
+ }
+
+ bool has_more_range() const { return !_riter.has_value; }
+
+ // read next range into [*from, *to) whose size <= max_range_size.
+ // return false when there is no more range.
+ bool next_range(const uint32_t max_range_size, uint32_t* from, uint32_t*
to) override {
+ if (!_riter.has_value) {
+ return false;
+ }
+
+ uint32_t range_size = 0;
+ *to = _riter.current_value + 1;
+
+ do {
+ *from = _riter.current_value;
+ range_size++;
+ roaring_previous_uint32_iterator(&_riter);
+ } while (range_size < max_range_size && _riter.has_value &&
+ _riter.current_value + 1 == *from);
+
+ return true;
+ }
+
+private:
+ roaring::api::roaring_uint32_iterator_t _riter;
+};
+
SegmentIterator::SegmentIterator(std::shared_ptr<Segment> segment, const
Schema& schema)
: _segment(std::move(segment)),
_schema(schema),
@@ -158,7 +198,11 @@ Status SegmentIterator::_init(bool is_vec) {
_row_bitmap -= *(_opts.delete_bitmap[segment_id()]);
_opts.stats->rows_del_by_bitmap += (pre_size -
_row_bitmap.cardinality());
}
- _range_iter.reset(new BitmapRangeIterator(_row_bitmap));
+ if (_opts.read_orderby_key_reverse) {
+ _range_iter.reset(new BackwardBitmapRangeIterator(_row_bitmap));
+ } else {
+ _range_iter.reset(new BitmapRangeIterator(_row_bitmap));
+ }
return Status::OK();
}
@@ -918,7 +962,8 @@ Status SegmentIterator::_read_columns_by_index(uint32_t
nrows_read_limit, uint32
} else {
nrows_read += rows_to_read;
}
- } while (nrows_read < nrows_read_limit);
+ // if _opts.read_orderby_key_reverse is true, only read one range for
fast reverse purpose
+ } while (nrows_read < nrows_read_limit && !_opts.read_orderby_key_reverse);
return Status::OK();
}
@@ -1139,6 +1184,19 @@ Status SegmentIterator::next_batch(vectorized::Block*
block) {
if (UNLIKELY(_estimate_row_size) && block->rows() > 0) {
_update_max_row(block);
}
+
+ // reverse block row order
+ if (_opts.read_orderby_key_reverse) {
+ size_t num_rows = block->rows();
+ size_t num_columns = block->columns();
+ vectorized::IColumn::Permutation permutation;
+ for (size_t i = 0; i < num_rows; ++i)
permutation.emplace_back(num_rows - 1 - i);
+
+ for (size_t i = 0; i < num_columns; ++i)
+ block->get_by_position(i).column =
+ block->get_by_position(i).column->permute(permutation,
num_rows);
+ }
+
return Status::OK();
}
diff --git a/be/src/olap/rowset/segment_v2/segment_iterator.h
b/be/src/olap/rowset/segment_v2/segment_iterator.h
index 76ad8f3ff5..d57889ccbe 100644
--- a/be/src/olap/rowset/segment_v2/segment_iterator.h
+++ b/be/src/olap/rowset/segment_v2/segment_iterator.h
@@ -160,6 +160,7 @@ private:
private:
class BitmapRangeIterator;
+ class BackwardBitmapRangeIterator;
std::shared_ptr<Segment> _segment;
const Schema& _schema;
diff --git a/be/src/vec/core/block.h b/be/src/vec/core/block.h
index 1d51cbf733..984a3cc98b 100644
--- a/be/src/vec/core/block.h
+++ b/be/src/vec/core/block.h
@@ -299,6 +299,24 @@ public:
return 0;
}
+ int compare_at(size_t n, size_t m, const std::vector<uint32_t>*
compare_columns,
+ const Block& rhs, int nan_direction_hint) const {
+ DCHECK_GE(columns(), compare_columns->size());
+ DCHECK_GE(rhs.columns(), compare_columns->size());
+
+ DCHECK_LE(n, rows());
+ DCHECK_LE(m, rhs.rows());
+ for (auto i : *compare_columns) {
+
DCHECK(get_by_position(i).type->equals(*rhs.get_by_position(i).type));
+ auto res = get_by_position(i).column->compare_at(n, m,
*(rhs.get_by_position(i).column),
+
nan_direction_hint);
+ if (res) {
+ return res;
+ }
+ }
+ return 0;
+ }
+
//note(wb) no DCHECK here, because this method is only used after
compare_at now, so no need to repeat check here.
// If this method is used in more places, you can add DCHECK case by case.
int compare_column_at(size_t n, size_t m, size_t col_idx, const Block& rhs,
diff --git a/be/src/vec/exec/volap_scan_node.cpp
b/be/src/vec/exec/volap_scan_node.cpp
index 865b2d838b..c3572adaac 100644
--- a/be/src/vec/exec/volap_scan_node.cpp
+++ b/be/src/vec/exec/volap_scan_node.cpp
@@ -65,6 +65,10 @@ VOlapScanNode::VOlapScanNode(ObjectPool* pool, const
TPlanNode& tnode, const Des
_max_materialized_blocks(config::doris_scanner_queue_size) {
_materialized_blocks.reserve(_max_materialized_blocks);
_free_blocks.reserve(_max_materialized_blocks);
+ // if sort_info is set, push _limit to each olap scanner
+ if (_olap_scan_node.__isset.sort_info &&
_olap_scan_node.__isset.sort_limit) {
+ _limit_per_scanner = _olap_scan_node.sort_limit;
+ }
}
Status VOlapScanNode::init(const TPlanNode& tnode, RuntimeState* state) {
diff --git a/be/src/vec/exec/volap_scan_node.h
b/be/src/vec/exec/volap_scan_node.h
index e047f34c10..718ec8755b 100644
--- a/be/src/vec/exec/volap_scan_node.h
+++ b/be/src/vec/exec/volap_scan_node.h
@@ -338,6 +338,7 @@ private:
phmap::flat_hash_set<VExpr*> _rf_vexpr_set;
std::vector<std::unique_ptr<VExprContext*>> _stale_vexpr_ctxs;
+ int64_t _limit_per_scanner = -1;
phmap::flat_hash_map<int, std::pair<SlotDescriptor*, ColumnValueRangeType>>
_id_to_slot_column_value_range;
};
diff --git a/be/src/vec/exec/volap_scanner.cpp
b/be/src/vec/exec/volap_scanner.cpp
index 6ce3182c1c..3073156816 100644
--- a/be/src/vec/exec/volap_scanner.cpp
+++ b/be/src/vec/exec/volap_scanner.cpp
@@ -242,6 +242,17 @@ Status VOlapScanner::_init_tablet_reader_params(
_tablet_reader_params.delete_bitmap =
&_tablet->tablet_meta()->delete_bitmap();
+ if (_parent->_olap_scan_node.__isset.sort_info &&
+ _parent->_olap_scan_node.sort_info.is_asc_order.size() > 0) {
+ _limit = _parent->_limit_per_scanner;
+ _tablet_reader_params.read_orderby_key = true;
+ if (!_parent->_olap_scan_node.sort_info.is_asc_order[0]) {
+ _tablet_reader_params.read_orderby_key_reverse = true;
+ }
+ _tablet_reader_params.read_orderby_key_num_prefix_columns =
+ _parent->_olap_scan_node.sort_info.is_asc_order.size();
+ }
+
return Status::OK();
}
@@ -327,6 +338,12 @@ Status VOlapScanner::get_block(RuntimeState* state,
vectorized::Block* block, bo
// But checking raw_bytes_threshold is still added here for consistency
with raw_rows_threshold
// and olap_scanner.cpp.
+ // set eof to true if per scanner limit is reached
+ // currently for query: ORDER BY key LIMIT n
+ if (_limit > 0 && _num_rows_read > _limit) {
+ *eof = true;
+ }
+
return Status::OK();
}
diff --git a/be/src/vec/exec/volap_scanner.h b/be/src/vec/exec/volap_scanner.h
index 7584eb47c9..f36fd55adf 100644
--- a/be/src/vec/exec/volap_scanner.h
+++ b/be/src/vec/exec/volap_scanner.h
@@ -108,6 +108,8 @@ private:
// to record which runtime filters have been used
std::vector<bool> _runtime_filter_marks;
+ int64_t _limit = -1;
+
int _id;
bool _is_open;
bool _aggregation;
diff --git a/be/src/vec/olap/block_reader.cpp b/be/src/vec/olap/block_reader.cpp
index a9167e8f55..d003df213d 100644
--- a/be/src/vec/olap/block_reader.cpp
+++ b/be/src/vec/olap/block_reader.cpp
@@ -34,7 +34,7 @@ BlockReader::~BlockReader() {
Status BlockReader::_init_collect_iter(const ReaderParams& read_params,
std::vector<RowsetReaderSharedPtr>*
valid_rs_readers) {
- _vcollect_iter.init(this);
+ _vcollect_iter.init(this, read_params.read_orderby_key,
read_params.read_orderby_key_reverse);
std::vector<RowsetReaderSharedPtr> rs_readers;
auto res = _capture_rs_readers(read_params, &rs_readers);
if (!res.ok()) {
diff --git a/be/src/vec/olap/vcollect_iterator.cpp
b/be/src/vec/olap/vcollect_iterator.cpp
index 251913bba3..27e39ced95 100644
--- a/be/src/vec/olap/vcollect_iterator.cpp
+++ b/be/src/vec/olap/vcollect_iterator.cpp
@@ -34,7 +34,7 @@ VCollectIterator::~VCollectIterator() {}
}
\
} while (false)
-void VCollectIterator::init(TabletReader* reader) {
+void VCollectIterator::init(TabletReader* reader, bool force_merge, bool
is_reverse) {
_reader = reader;
// when aggregate is enabled or key_type is DUP_KEYS, we don't merge
// multiple data to aggregate for better performance
@@ -44,6 +44,11 @@ void VCollectIterator::init(TabletReader* reader) {
_reader->_tablet->enable_unique_key_merge_on_write()))) {
_merge = false;
}
+
+ if (force_merge) {
+ _merge = true;
+ }
+ _is_reverse = is_reverse;
}
Status VCollectIterator::add_child(RowsetReaderSharedPtr rs_reader) {
@@ -103,19 +108,21 @@ Status
VCollectIterator::build_heap(std::vector<RowsetReaderSharedPtr>& rs_reade
}
++i;
}
- Level1Iterator* cumu_iter = new Level1Iterator(cumu_children,
_reader,
-
cumu_children.size() > 1, _skip_same);
+ Level1Iterator* cumu_iter = new Level1Iterator(
+ cumu_children, _reader, cumu_children.size() > 1,
_is_reverse, _skip_same);
RETURN_IF_NOT_EOF_AND_OK(cumu_iter->init());
std::list<LevelIterator*> children;
children.push_back(*base_reader_child);
children.push_back(cumu_iter);
- _inner_iter.reset(new Level1Iterator(children, _reader, _merge,
_skip_same));
+ _inner_iter.reset(
+ new Level1Iterator(children, _reader, _merge, _is_reverse,
_skip_same));
} else {
// _children.size() == 1
- _inner_iter.reset(new Level1Iterator(_children, _reader, _merge,
_skip_same));
+ _inner_iter.reset(
+ new Level1Iterator(_children, _reader, _merge,
_is_reverse, _skip_same));
}
} else {
- _inner_iter.reset(new Level1Iterator(_children, _reader, _merge,
_skip_same));
+ _inner_iter.reset(new Level1Iterator(_children, _reader, _merge,
_is_reverse, _skip_same));
}
RETURN_IF_NOT_EOF_AND_OK(_inner_iter->init());
// Clear _children earlier to release any related references
@@ -127,11 +134,14 @@ bool
VCollectIterator::LevelIteratorComparator::operator()(LevelIterator* lhs, L
const IteratorRowRef& lhs_ref = *lhs->current_row_ref();
const IteratorRowRef& rhs_ref = *rhs->current_row_ref();
- int cmp_res =
- lhs_ref.block->compare_at(lhs_ref.row_pos, rhs_ref.row_pos,
- lhs->tablet_schema().num_key_columns(),
*rhs_ref.block, -1);
+ int cmp_res = UNLIKELY(lhs->compare_columns())
+ ? lhs_ref.block->compare_at(lhs_ref.row_pos,
rhs_ref.row_pos,
+ lhs->compare_columns(),
*rhs_ref.block, -1)
+ : lhs_ref.block->compare_at(lhs_ref.row_pos,
rhs_ref.row_pos,
+
lhs->tablet_schema().num_key_columns(),
+ *rhs_ref.block, -1);
if (cmp_res != 0) {
- return cmp_res > 0;
+ return UNLIKELY(_is_reverse) ? cmp_res < 0 : cmp_res > 0;
}
if (_sequence != -1) {
@@ -270,11 +280,12 @@ Status
VCollectIterator::Level0Iterator::current_block_row_locations(
VCollectIterator::Level1Iterator::Level1Iterator(
const std::list<VCollectIterator::LevelIterator*>& children,
TabletReader* reader,
- bool merge, bool skip_same)
+ bool merge, bool is_reverse, bool skip_same)
: LevelIterator(reader),
_children(children),
_reader(reader),
_merge(merge),
+ _is_reverse(is_reverse),
_skip_same(skip_same) {
_ref.row_pos = -1; // represent eof
_batch_size = reader->_batch_size;
@@ -351,7 +362,7 @@ Status VCollectIterator::Level1Iterator::init() {
break;
}
}
- _heap.reset(new MergeHeap {LevelIteratorComparator(sequence_loc)});
+ _heap.reset(new MergeHeap {LevelIteratorComparator(sequence_loc,
_is_reverse)});
for (auto child : _children) {
DCHECK(child != nullptr);
//DCHECK(child->current_row().ok());
diff --git a/be/src/vec/olap/vcollect_iterator.h
b/be/src/vec/olap/vcollect_iterator.h
index 510b579618..eabb0ad2e5 100644
--- a/be/src/vec/olap/vcollect_iterator.h
+++ b/be/src/vec/olap/vcollect_iterator.h
@@ -45,7 +45,7 @@ public:
// Hold reader point to get reader params
~VCollectIterator();
- void init(TabletReader* reader);
+ void init(TabletReader* reader, bool force_merge, bool is_reverse);
Status add_child(RowsetReaderSharedPtr rs_reader);
@@ -79,7 +79,9 @@ private:
// then merged with other rowset readers.
class LevelIterator {
public:
- LevelIterator(TabletReader* reader) : _schema(reader->tablet_schema())
{};
+ LevelIterator(TabletReader* reader)
+ : _schema(reader->tablet_schema()),
+
_compare_columns(reader->_reader_context.read_orderby_key_columns) {};
virtual Status init() = 0;
@@ -99,6 +101,8 @@ private:
const TabletSchema& tablet_schema() const { return _schema; };
+ const inline std::vector<uint32_t>* compare_columns() const { return
_compare_columns; };
+
virtual RowLocation current_row_location() = 0;
virtual Status current_block_row_locations(std::vector<RowLocation>*
row_location) = 0;
@@ -106,18 +110,22 @@ private:
protected:
const TabletSchema& _schema;
IteratorRowRef _ref;
+ std::vector<uint32_t>* _compare_columns;
};
// Compare row cursors between multiple merge elements,
// if row cursors equal, compare data version.
class LevelIteratorComparator {
public:
- LevelIteratorComparator(int sequence = -1) : _sequence(sequence) {}
+ LevelIteratorComparator(int sequence, bool is_reverse)
+ : _sequence(sequence), _is_reverse(is_reverse) {}
bool operator()(LevelIterator* lhs, LevelIterator* rhs);
private:
int _sequence;
+ // reverse the compare order
+ bool _is_reverse = false;
};
#ifdef USE_LIBCPP
@@ -159,7 +167,7 @@ private:
class Level1Iterator : public LevelIterator {
public:
Level1Iterator(const std::list<LevelIterator*>& children,
TabletReader* reader, bool merge,
- bool skip_same);
+ bool is_reverse, bool skip_same);
Status init() override;
@@ -198,6 +206,8 @@ private:
// from the first rowset, the second rowset, .., the last rowset. The
output of CollectorIterator is also
// *partially* ordered.
bool _merge = true;
+ // reverse the compare order
+ bool _is_reverse = false;
bool _skip_same;
// used when `_merge == true`
@@ -216,6 +226,8 @@ private:
std::list<LevelIterator*> _children;
bool _merge = true;
+ // reverse the compare order
+ bool _is_reverse = false;
// Hold reader point to access read params, such as fetch conditions.
TabletReader* _reader = nullptr;
diff --git a/be/src/vec/olap/vgeneric_iterators.cpp
b/be/src/vec/olap/vgeneric_iterators.cpp
index b80f677052..557ef8e057 100644
--- a/be/src/vec/olap/vgeneric_iterators.cpp
+++ b/be/src/vec/olap/vgeneric_iterators.cpp
@@ -119,12 +119,15 @@ Status VAutoIncrementIterator::init(const
StorageReadOptions& opts) {
// }
class VMergeIteratorContext {
public:
- VMergeIteratorContext(RowwiseIterator* iter, int sequence_id_idx, bool
is_unique)
+ VMergeIteratorContext(RowwiseIterator* iter, int sequence_id_idx, bool
is_unique,
+ bool is_reverse, std::vector<uint32_t>*
read_orderby_key_columns)
: _iter(iter),
_sequence_id_idx(sequence_id_idx),
_is_unique(is_unique),
+ _is_reverse(is_reverse),
_num_columns(iter->schema().num_column_ids()),
- _num_key_columns(iter->schema().num_key_columns()) {}
+ _num_key_columns(iter->schema().num_key_columns()),
+ _compare_columns(read_orderby_key_columns) {}
VMergeIteratorContext(const VMergeIteratorContext&) = delete;
VMergeIteratorContext(VMergeIteratorContext&&) = delete;
@@ -161,10 +164,14 @@ public:
Status init(const StorageReadOptions& opts);
bool compare(const VMergeIteratorContext& rhs) const {
- int cmp_res = this->_block.compare_at(_index_in_block,
rhs._index_in_block,
- _num_key_columns, rhs._block,
-1);
+ int cmp_res = UNLIKELY(_compare_columns)
+ ? this->_block.compare_at(_index_in_block,
rhs._index_in_block,
+ _compare_columns,
rhs._block, -1)
+ : this->_block.compare_at(_index_in_block,
rhs._index_in_block,
+ _num_key_columns,
rhs._block, -1);
+
if (cmp_res != 0) {
- return cmp_res > 0;
+ return UNLIKELY(_is_reverse) ? cmp_res < 0 : cmp_res > 0;
}
auto col_cmp_res = 0;
@@ -228,12 +235,14 @@ private:
int _sequence_id_idx = -1;
bool _is_unique = false;
+ bool _is_reverse = false;
bool _valid = false;
mutable bool _skip = false;
size_t _index_in_block = -1;
int _block_row_max = 4096;
int _num_columns;
int _num_key_columns;
+ std::vector<uint32_t>* _compare_columns;
std::vector<RowLocation> _block_row_locations;
bool _record_rowids = false;
};
@@ -289,10 +298,11 @@ class VMergeIterator : public RowwiseIterator {
public:
// VMergeIterator takes the ownership of input iterators
VMergeIterator(std::vector<RowwiseIterator*>& iters, int sequence_id_idx,
bool is_unique,
- uint64_t* merged_rows)
+ bool is_reverse, uint64_t* merged_rows)
: _origin_iters(iters),
_sequence_id_idx(sequence_id_idx),
_is_unique(is_unique),
+ _is_reverse(is_reverse),
_merged_rows(merged_rows) {}
~VMergeIterator() override {
@@ -336,6 +346,7 @@ private:
int block_row_max = 0;
int _sequence_id_idx = -1;
bool _is_unique = false;
+ bool _is_reverse = false;
uint64_t* _merged_rows = nullptr;
bool _record_rowids = false;
std::vector<RowLocation> _block_row_locations;
@@ -349,7 +360,8 @@ Status VMergeIterator::init(const StorageReadOptions& opts)
{
_record_rowids = opts.record_rowids;
for (auto iter : _origin_iters) {
- auto ctx = std::make_unique<VMergeIteratorContext>(iter,
_sequence_id_idx, _is_unique);
+ auto ctx = std::make_unique<VMergeIteratorContext>(
+ iter, _sequence_id_idx, _is_unique, _is_reverse,
opts.read_orderby_key_columns);
RETURN_IF_ERROR(ctx->init(opts));
if (!ctx->valid()) {
continue;
@@ -473,11 +485,11 @@ Status
VUnionIterator::current_block_row_locations(std::vector<RowLocation>* loc
}
RowwiseIterator* new_merge_iterator(std::vector<RowwiseIterator*>& inputs, int
sequence_id_idx,
- bool is_unique, uint64_t* merged_rows) {
+ bool is_unique, bool is_reverse, uint64_t*
merged_rows) {
if (inputs.size() == 1) {
return *(inputs.begin());
}
- return new VMergeIterator(inputs, sequence_id_idx, is_unique, merged_rows);
+ return new VMergeIterator(inputs, sequence_id_idx, is_unique, is_reverse,
merged_rows);
}
RowwiseIterator* new_union_iterator(std::vector<RowwiseIterator*>& inputs) {
diff --git a/be/src/vec/olap/vgeneric_iterators.h
b/be/src/vec/olap/vgeneric_iterators.h
index 3b9be7c922..abab6d20fe 100644
--- a/be/src/vec/olap/vgeneric_iterators.h
+++ b/be/src/vec/olap/vgeneric_iterators.h
@@ -28,7 +28,7 @@ namespace vectorized {
// Inputs iterators' ownership is taken by created merge iterator. And client
// should delete returned iterator after usage.
RowwiseIterator* new_merge_iterator(std::vector<RowwiseIterator*>& inputs, int
sequence_id_idx,
- bool is_unique, uint64_t* merged_rows);
+ bool is_unique, bool is_reverse, uint64_t*
merged_rows);
// Create a union iterator for input iterators. Union iterator will read
// input iterators one by one.
diff --git a/be/test/vec/exec/vgeneric_iterators_test.cpp
b/be/test/vec/exec/vgeneric_iterators_test.cpp
index 2963da2f9f..6d8b307116 100644
--- a/be/test/vec/exec/vgeneric_iterators_test.cpp
+++ b/be/test/vec/exec/vgeneric_iterators_test.cpp
@@ -145,7 +145,7 @@ TEST(VGenericIteratorsTest, MergeAgg) {
inputs.push_back(vectorized::new_auto_increment_iterator(schema, 200));
inputs.push_back(vectorized::new_auto_increment_iterator(schema, 300));
- auto iter = vectorized::new_merge_iterator(inputs, -1, false, nullptr);
+ auto iter = vectorized::new_merge_iterator(inputs, -1, false, false,
nullptr);
StorageReadOptions opts;
auto st = iter->init(opts);
EXPECT_TRUE(st.ok());
@@ -194,7 +194,7 @@ TEST(VGenericIteratorsTest, MergeUnique) {
inputs.push_back(vectorized::new_auto_increment_iterator(schema, 200));
inputs.push_back(vectorized::new_auto_increment_iterator(schema, 300));
- auto iter = vectorized::new_merge_iterator(inputs, -1, true, nullptr);
+ auto iter = vectorized::new_merge_iterator(inputs, -1, true, false,
nullptr);
StorageReadOptions opts;
auto st = iter->init(opts);
EXPECT_TRUE(st.ok());
@@ -316,7 +316,7 @@ TEST(VGenericIteratorsTest, MergeWithSeqColumn) {
seq_id_in_every_file));
}
- auto iter = vectorized::new_merge_iterator(inputs, seq_column_id, true,
nullptr);
+ auto iter = vectorized::new_merge_iterator(inputs, seq_column_id, true,
false, nullptr);
StorageReadOptions opts;
auto st = iter->init(opts);
EXPECT_TRUE(st.ok());
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/planner/OlapScanNode.java
b/fe/fe-core/src/main/java/org/apache/doris/planner/OlapScanNode.java
index 176b35fbb1..c46b1615d0 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/planner/OlapScanNode.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/planner/OlapScanNode.java
@@ -27,6 +27,7 @@ import org.apache.doris.analysis.IntLiteral;
import org.apache.doris.analysis.PartitionNames;
import org.apache.doris.analysis.SlotDescriptor;
import org.apache.doris.analysis.SlotRef;
+import org.apache.doris.analysis.SortInfo;
import org.apache.doris.analysis.TupleDescriptor;
import org.apache.doris.analysis.TupleId;
import org.apache.doris.catalog.ColocateTableIndex;
@@ -67,6 +68,7 @@ import org.apache.doris.thrift.TPrimitiveType;
import org.apache.doris.thrift.TScanRange;
import org.apache.doris.thrift.TScanRangeLocation;
import org.apache.doris.thrift.TScanRangeLocations;
+import org.apache.doris.thrift.TSortInfo;
import com.google.common.base.Joiner;
import com.google.common.base.MoreObjects;
@@ -145,6 +147,12 @@ public class OlapScanNode extends ScanNode {
private Collection<Long> selectedPartitionIds = Lists.newArrayList();
private long totalBytes = 0;
+ private SortInfo sortInfo = null;
+
+ // When scan match sort_info, we can push limit into OlapScanNode.
+ // It's limit for scanner instead of scanNode so we add a new limit.
+ private long sortLimit = -1;
+
// List of tablets will be scanned by current olap_scan_node
private ArrayList<Long> scanTabletIds = Lists.newArrayList();
@@ -203,10 +211,26 @@ public class OlapScanNode extends ScanNode {
return selectedTabletsNum;
}
+ public SortInfo getSortInfo() {
+ return sortInfo;
+ }
+
+ public void setSortInfo(SortInfo sortInfo) {
+ this.sortInfo = sortInfo;
+ }
+
+ public void setSortLimit(long sortLimit) {
+ this.sortLimit = sortLimit;
+ }
+
public Collection<Long> getSelectedPartitionIds() {
return selectedPartitionIds;
}
+ public void setTupleIds(ArrayList<TupleId> tupleIds) {
+ this.tupleIds = tupleIds;
+ }
+
// only used for UT
public void setSelectedPartitionIds(Collection<Long> selectedPartitionIds)
{
this.selectedPartitionIds = selectedPartitionIds;
@@ -217,7 +241,7 @@ public class OlapScanNode extends ScanNode {
* selectedIndexId.
* It makes sure that the olap scan node must scan the base data rather
than
* scan the materialized view data.
- *
+ * <p>
* This function is mainly used to update stmt.
* Update stmt also needs to scan data like normal queries.
* But its syntax is different from ordinary queries,
@@ -723,6 +747,40 @@ public class OlapScanNode extends ScanNode {
}
}
+ /**
+ * Check Parent sort node can push down to child olap scan.
+ */
+ public boolean checkPushSort(SortNode sortNode) {
+ // Ensure all isAscOrder is same, ande length != 0.
+ // Can't be zorder.
+ if (sortNode.getSortInfo().getIsAscOrder().stream().distinct().count()
!= 1
+ || olapTable.isZOrderSort()) {
+ return false;
+ }
+
+ // Tablet's order by key only can be the front part of schema.
+ // Like: schema: a.b.c.d.e.f.g order by key: a.b.c (no a,b,d)
+ // Do **prefix match** to check if order by key can be pushed down.
+ // olap order by key: a.b.c.d
+ // sort key: (a) (a,b) (a,b,c) (a,b,c,d) is ok
+ // (a,c) (a,c,d), (a,c,b) (a,c,f) (a,b,c,d,e)is NOT ok
+ List<Expr> sortExprs =
sortNode.getSortInfo().getMaterializedOrderingExprs();
+ if (sortExprs.size() > olapTable.getDataSortInfo().getColNum()) {
+ return false;
+ }
+ for (int i = 0; i < sortExprs.size(); i++) {
+ // table key.
+ Column tableKey = olapTable.getFullSchema().get(i);
+ // sort slot.
+ Expr sortExpr = sortExprs.get(i);
+ if (!(sortExpr instanceof SlotRef) || !tableKey.equals(((SlotRef)
sortExpr).getColumn())) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
/**
* We query Palo Meta to get request's data location
* extra result info will pass to backend ScanNode
@@ -748,10 +806,18 @@ public class OlapScanNode extends ScanNode {
}
output.append("\n");
- if (null != sortColumn) {
+ if (sortColumn != null) {
output.append(prefix).append("SORT COLUMN:
").append(sortColumn).append("\n");
}
-
+ if (sortInfo != null) {
+ output.append(prefix).append("SORT INFO:\n");
+ sortInfo.getMaterializedOrderingExprs().forEach(expr -> {
+
output.append(prefix).append(prefix).append(expr.toSql()).append("\n");
+ });
+ }
+ if (sortLimit != -1) {
+ output.append(prefix).append("SORT LIMIT:
").append(sortLimit).append("\n");
+ }
if (!conjuncts.isEmpty()) {
output.append(prefix).append("PREDICATES:
").append(getExplainString(conjuncts)).append("\n");
}
@@ -807,6 +873,19 @@ public class OlapScanNode extends ScanNode {
if (null != sortColumn) {
msg.olap_scan_node.setSortColumn(sortColumn);
}
+ if (sortInfo != null) {
+ TSortInfo tSortInfo = new TSortInfo(
+ Expr.treesToThrift(sortInfo.getOrderingExprs()),
+ sortInfo.getIsAscOrder(),
+ sortInfo.getNullsFirst());
+ if (sortInfo.getSortTupleSlotExprs() != null) {
+
tSortInfo.setSortTupleSlotExprs(Expr.treesToThrift(sortInfo.getSortTupleSlotExprs()));
+ }
+ msg.olap_scan_node.setSortInfo(tSortInfo);
+ }
+ if (sortLimit != -1) {
+ msg.olap_scan_node.setSortLimit(sortLimit);
+ }
msg.olap_scan_node.setKeyType(olapTable.getKeysType().toThrift());
msg.olap_scan_node.setTableName(olapTable.getName());
}
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/planner/OriginalPlanner.java
b/fe/fe-core/src/main/java/org/apache/doris/planner/OriginalPlanner.java
index 39e5824fd4..947d529a58 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/planner/OriginalPlanner.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/planner/OriginalPlanner.java
@@ -200,6 +200,12 @@ public class OriginalPlanner extends Planner {
fragments = distributedPlanner.createPlanFragments(singleNodePlan);
}
+ // Push sort node down to the bottom of olapscan.
+ // Because the olapscan must be in the end. So get the last two nodes.
+ if (VectorizedUtil.isVectorized()) {
+ pushSortToOlapScan();
+ }
+
// Optimize the transfer of query statistic when query doesn't contain
limit.
PlanFragment rootFragment = fragments.get(fragments.size() - 1);
QueryStatisticsTransferOptimizer queryStatisticTransferOptimizer
@@ -319,6 +325,40 @@ public class OriginalPlanner extends Planner {
topPlanFragment.getPlanRoot().resetTupleIds(Lists.newArrayList(fileStatusDesc.getId()));
}
+ /**
+ * Push sort down to olap scan.
+ */
+ private void pushSortToOlapScan() {
+ for (PlanFragment fragment : fragments) {
+ PlanNode node = fragment.getPlanRoot();
+ PlanNode parent = null;
+
+ // OlapScanNode is the last node.
+ // So, just get the last two node and check if they are SortNode
and OlapScan.
+ while (node.getChildren().size() != 0) {
+ parent = node;
+ node = node.getChildren().get(0);
+ }
+
+ if (!(node instanceof OlapScanNode) || !(parent instanceof
SortNode)) {
+ continue;
+ }
+ SortNode sortNode = (SortNode) parent;
+ OlapScanNode scanNode = (OlapScanNode) node;
+ if (!scanNode.checkPushSort(sortNode)) {
+ continue;
+ }
+ // If offset > 0, we push down (limit: limit + offset) to olap
scan.
+ if (sortNode.getOffset() > 0) {
+ scanNode.setSortLimit(sortNode.getLimit() +
sortNode.getOffset());
+ } else {
+ scanNode.setSortLimit(sortNode.getLimit());
+ }
+ scanNode.setSortInfo(sortNode.getSortInfo());
+
scanNode.getSortInfo().setSortTupleSlotExprs(sortNode.resolvedTupleExprs);
+ }
+ }
+
/**
* Construct a tuple for file status, the tuple schema as following:
* | FileNumber | Int |
diff --git a/fe/fe-core/src/test/java/org/apache/doris/planner/PlannerTest.java
b/fe/fe-core/src/test/java/org/apache/doris/planner/PlannerTest.java
index 34ee988dff..8d894ef05b 100644
--- a/fe/fe-core/src/test/java/org/apache/doris/planner/PlannerTest.java
+++ b/fe/fe-core/src/test/java/org/apache/doris/planner/PlannerTest.java
@@ -506,4 +506,25 @@ public class PlannerTest extends TestWithFeService {
Assert.assertEquals(MysqlStateType.ERR, state.getStateType());
Assert.assertTrue(state.getErrorMessage().contains("failed to execute
update stmt"));
}
+
+ @Test
+ public void testPushSortToOlapScan() throws Exception {
+ // Push sort successfully
+ String sql1 = "explain select k1 from db1.tbl1 order by k1, k2";
+ StmtExecutor stmtExecutor1 = new StmtExecutor(connectContext, sql1);
+ stmtExecutor1.execute();
+ Planner planner1 = stmtExecutor1.planner();
+ String plan1 = planner1.getExplainString(new ExplainOptions(false,
false));
+ Assertions.assertTrue(plan1.contains("SORT INFO:\n `k1`\n
`k2`"));
+ Assertions.assertTrue(plan1.contains("SORT LIMIT:"));
+
+ // Push sort failed
+ String sql2 = "explain select k1, k2, k3 from db1.tbl1 order by k1,
k3, k2";
+ StmtExecutor stmtExecutor2 = new StmtExecutor(connectContext, sql2);
+ stmtExecutor2.execute();
+ Planner planner2 = stmtExecutor2.planner();
+ String plan2 = planner2.getExplainString(new ExplainOptions(false,
false));
+ Assertions.assertFalse(plan2.contains("SORT INFO:"));
+ Assertions.assertFalse(plan2.contains("SORT LIMIT:"));
+ }
}
diff --git a/gensrc/thrift/PlanNodes.thrift b/gensrc/thrift/PlanNodes.thrift
index ddd63bc92a..6d6e7bc203 100644
--- a/gensrc/thrift/PlanNodes.thrift
+++ b/gensrc/thrift/PlanNodes.thrift
@@ -412,6 +412,17 @@ struct TMetaScanNode {
5: optional string user
}
+struct TSortInfo {
+ 1: required list<Exprs.TExpr> ordering_exprs
+ 2: required list<bool> is_asc_order
+ // Indicates, for each expr, if nulls should be listed first or last. This is
+ // independent of is_asc_order.
+ 3: required list<bool> nulls_first
+ // Expressions evaluated over the input row that materialize the tuple to be
sorted.
+ // Contains one expr per slot in the materialized tuple.
+ 4: optional list<Exprs.TExpr> sort_tuple_slot_exprs
+}
+
struct TOlapScanNode {
1: required Types.TTupleId tuple_id
2: required list<string> key_column_name
@@ -421,6 +432,10 @@ struct TOlapScanNode {
6: optional Types.TKeysType keyType
7: optional string table_name
8: required list<Descriptors.TColumn> columns_desc
+ 9: optional TSortInfo sort_info
+ // When scan match sort_info, we can push limit into OlapScanNode.
+ // It's limit for scanner instead of scanNode so we add a new limit.
+ 10: optional i64 sort_limit
}
struct TEqJoinCondition {
@@ -567,17 +582,6 @@ struct TPreAggregationNode {
2: required list<Exprs.TExpr> aggregate_exprs
}
-struct TSortInfo {
- 1: required list<Exprs.TExpr> ordering_exprs
- 2: required list<bool> is_asc_order
- // Indicates, for each expr, if nulls should be listed first or last. This is
- // independent of is_asc_order.
- 3: required list<bool> nulls_first
- // Expressions evaluated over the input row that materialize the tuple to be
sorted.
- // Contains one expr per slot in the materialized tuple.
- 4: optional list<Exprs.TExpr> sort_tuple_slot_exprs
-}
-
struct TSortNode {
1: required TSortInfo sort_info
// Indicates whether the backend service should use topn vs. sorting
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]