Repository: StarRocks/starrocks
Pull request #56692: [Enhancement]cache aware dictionary decoder with filter
Author: login: zombee0
Created: 2025-03-07T02:58:55Z | Merged: 2025-03-21T08:11:05Z

## Why I'm doing:

## What I'm doing:

Fixes #issue

## What type of PR is this:

- [ ] BugFix
- [ ] Feature
- [x] Enhancement
- [ ] Refactor
- [ ] UT
- [ ] Doc
- [ ] Tool

Does this PR entail a change in behavior?

- [ ] Yes, this PR will result in a change in behavior.
- [x] No, this PR will not result in a change in behavior.

If yes, please specify the type of change:

- [ ] Interface/UI changes: syntax, type conversion, expression evaluation, display information
- [ ] Parameter changes: default values, similar parameters but with different default values
- [ ] Policy changes: use new policy to replace old one, functionality automatically enabled
- [ ] Feature removed
- [ ] Miscellaneous: upgrade & downgrade compatibility, etc.

## Checklist:

- [ ] I have added test cases for my bug fix or my new feature
- [ ] This pr needs user documentation (for new or modified features or behaviors)
  - [ ] I have added documentation for my new feature or new function
- [ ] This is a backport pr

## Bugfix cherry-pick branch check:
- [x] I have checked the version labels which the pr will be auto-backported to the target branch
  - [ ] 3.4
  - [ ] 3.3
  - [ ] 3.2
  - [ ] 3.1
  - [ ] 3.0


Diff:
diff --git a/be/src/common/config.h b/be/src/common/config.h
index a60edc615bdbaa..a74116b7313cf1 100644
--- a/be/src/common/config.h
+++ b/be/src/common/config.h
@@ -954,6 +954,7 @@ CONF_mBool(parquet_reader_bloom_filter_enable, "true");
 CONF_mBool(parquet_statistics_process_more_filter_enable, "true");
 CONF_mBool(parquet_fast_timezone_conversion, "false");
 CONF_mBool(parquet_push_down_filter_to_decoder_enable, "true");
+CONF_mBool(parquet_cache_aware_dict_decoder_enable, "true");
 
 CONF_mBool(parquet_reader_enable_adpative_bloom_filter, "true");
 
diff --git a/be/src/formats/parquet/encoding_dict.h b/be/src/formats/parquet/encoding_dict.h
index a3b7f3d12468e3..83ccc50befe180 100644
--- a/be/src/formats/parquet/encoding_dict.h
+++ b/be/src/formats/parquet/encoding_dict.h
@@ -20,10 +20,12 @@
 #include "column/column.h"
 #include "column/column_helper.h"
 #include "column/nullable_column.h"
+#include "common/config.h"
 #include "common/status.h"
 #include "formats/parquet/encoding.h"
 #include "simd/simd.h"
 #include "util/coding.h"
+#include "util/cpu_info.h"
 #include "util/rle_encoding.h"
 #include "util/slice.h"
 
@@ -81,9 +83,60 @@ class DictEncoder final : public Encoder {
     faststring _buffer;
 };
 
+class CacheAwareDictDecoder : public Decoder {
+public:
+    CacheAwareDictDecoder() { _dict_size_threshold = CpuInfo::get_l2_cache_size(); }
+    ~CacheAwareDictDecoder() override = default;
+
+    Status next_batch(size_t count, ColumnContentType content_type, Column* dst, const FilterData* filter) override {
+        switch (content_type) {
+        case DICT_CODE: {
+            FixedLengthColumn<int32_t>* data_column;
+            if (dst->is_nullable()) {
+                auto nullable_column = down_cast<NullableColumn*>(dst);
+                nullable_column->null_column()->append_default(count);
+                data_column = down_cast<FixedLengthColumn<int32_t>*>(nullable_column->data_column().get());
+            } else {
+                data_column = down_cast<FixedLengthColumn<int32_t>*>(dst);
+            }
+            size_t cur_size = data_column->size();
+            data_column->resize_uninitialized(cur_size + count);
+            int32_t* __restrict__ data = data_column->get_data().data() + cur_size;
+            auto decoded_num = _rle_batch_reader.GetBatch(reinterpret_cast<uint32_t*>(data), count);
+            if (decoded_num < count) {
+                return Status::InternalError("didn't get enough data from dict-decoder");
+            }
+            break;
+        }
+        case VALUE: {
+#ifdef BE_TEST
+            return _next_batch_value(count, dst, filter);
+#else
+            if (_get_dict_size() > _dict_size_threshold && config::parquet_cache_aware_dict_decoder_enable) {
+                return _next_batch_value(count, dst, filter);
+            } else {
+                return _next_batch_value(count, dst, nullptr);
+            }
+#endif // BE_TEST
+        }
+        default:
+            return Status::NotSupported("read type not supported");
+        }
+        return Status::OK();
+    }
+
+protected:
+    virtual size_t _get_dict_size() const = 0;
+    virtual Status _next_batch_value(size_t count, Column* dst, const FilterData* filter) = 0;
+    RleBatchDecoder<uint32_t> _rle_batch_reader;
+
+private:
+    size_t _dict_size_threshold = 0;
+};
+
 // TODO(zc): support read run later. however should add more interface to Column first
 template <typename T>
-class DictDecoder final : public Decoder {
+class DictDecoder final : public CacheAwareDictDecoder {
 public:
     DictDecoder() = default;
     ~DictDecoder() override = default;
@@ -115,7 +168,10 @@ class DictDecoder final : public Decoder {
         return Status::OK();
     }
 
-    Status next_batch(size_t count, ColumnContentType content_type, Column* dst, const FilterData* filter) override {
+private:
+    size_t _get_dict_size() const override { return _dict.size() * SIZE_OF_TYPE; }
+
+    Status _next_batch_value(size_t count, Column* dst, const FilterData* filter) override {
         FixedLengthColumn<T>* data_column /* = nullptr */;
         if (dst->is_nullable()) {
             auto nullable_column = down_cast<NullableColumn*>(dst);
@@ -129,24 +185,45 @@ class DictDecoder final : public Decoder {
         data_column->resize_uninitialized(cur_size + count);
         T* __restrict__ data = data_column->get_data().data() + cur_size;
 
-        auto ret = _rle_batch_reader.GetBatchWithDict(_dict.data(), _dict.size(), data, count);
-        if (UNLIKELY(ret <= 0)) {
-            return Status::InternalError("DictDecoder GetBatchWithDict failed");
+        if (filter) {
+            _indexes.reserve(count);
+            auto decoded_num = _rle_batch_reader.GetBatch(&_indexes[0], count);
+            if (decoded_num < count) {
+                return Status::InternalError("didn't get enough data from dict-decoder");
+            }
+
+            auto flag = 0;
+            size_t size = _dict.size();
+            for (int i = 0; i < count; i++) {
+                flag |= _indexes[i] >= size;
+            }
+            if (UNLIKELY(flag)) {
+                return Status::InternalError("Index not in dictionary bounds");
+            }
+
+            for (int i = 0; i < count; i++) {
+                if (filter[i]) {
+                    data[i] = _dict[_indexes[i]];
+                }
+            }
+        } else {
+            auto ret = _rle_batch_reader.GetBatchWithDict(_dict.data(), _dict.size(), data, count);
+            if (UNLIKELY(ret <= 0)) {
+                return Status::InternalError("DictDecoder GetBatchWithDict failed");
+            }
         }
 
         return Status::OK();
     }
 
-private:
     enum { SIZE_OF_TYPE = sizeof(T) };
 
-    RleBatchDecoder<uint32_t> _rle_batch_reader;
     std::vector<T> _dict;
     std::vector<uint32_t> _indexes;
 };
 
 template <>
-class DictDecoder<Slice> final : public Decoder {
+class DictDecoder<Slice> final : public CacheAwareDictDecoder {
 public:
     DictDecoder() = default;
     ~DictDecoder() override = default;
@@ -251,24 +328,36 @@ class DictDecoder<Slice> final : public Decoder {
         return Status::OK();
     }
 
-    Status next_batch(size_t count, ColumnContentType content_type, Column* dst, const FilterData* filter) override {
-        switch (content_type) {
-        case DICT_CODE: {
-            FixedLengthColumn<int32_t>* data_column;
+private:
+    size_t _get_dict_size() const override { return _dict_data.size(); }
+
+    Status _next_batch_value(size_t count, Column* dst, const FilterData* filter) override {
+        if (filter) {
+            _indexes.reserve(count);
+            auto decoded_num = _rle_batch_reader.GetBatch(&_indexes[0], count);
+            if (decoded_num < count) {
+                return Status::InternalError("didn't get enough data from dict-decoder");
+            }
+            auto flag = 0;
+            size_t size = _dict.size();
+            for (int i = 0; i < count; i++) {
+                flag |= _indexes[i] >= size;
+            }
+            if (UNLIKELY(flag)) {
+                return Status::InternalError("Index not in dictionary bounds");
+            }
             if (dst->is_nullable()) {
-                auto nullable_column = down_cast<NullableColumn*>(dst);
-                nullable_column->null_column()->append_default(count);
-                data_column = down_cast<FixedLengthColumn<int32_t>*>(nullable_column->data_column().get());
-            } else {
-                data_column = down_cast<FixedLengthColumn<int32_t>*>(dst);
+                down_cast<NullableColumn*>(dst)->mutable_null_column()->append_default(count);
             }
-            size_t cur_size = data_column->size();
-            data_column->resize_uninitialized(cur_size + count);
-            int32_t* __restrict__ data = data_column->get_data().data() + cur_size;
-            _rle_batch_reader.GetBatch(reinterpret_cast<uint32_t*>(data), count);
-            break;
-        }
-        case VALUE: {
+            auto* binary_column = ColumnHelper::get_binary_column(dst);
+            for (int i = 0; i < count; ++i) {
+                if (filter[i]) {
+                    binary_column->append(_dict[_indexes[i]]);
+                } else {
+                    binary_column->append_default();
+                }
+            }
+        } else {
             raw::stl_vector_resize_uninitialized(&_slices, count);
             auto ret = _rle_batch_reader.GetBatchWithDict(_dict.data(), _dict.size(), _slices.data(), count);
             if (UNLIKELY(ret <= 0)) {
@@ -278,19 +367,12 @@ class DictDecoder<Slice> final : public Decoder {
             if (UNLIKELY(!ret)) {
                 return Status::InternalError("DictDecoder append strings to column failed");
             }
-            break;
         }
-        default:
-            return Status::NotSupported("read type not supported");
-        }
-
         return Status::OK();
     }
 
-private:
     enum { SIZE_OF_DICT_CODE_TYPE = sizeof(int32_t) };
 
-    RleBatchDecoder<uint32_t> _rle_batch_reader;
     std::vector<uint8_t> _dict_data;
     std::vector<Slice> _dict;
     std::vector<uint32_t> _indexes;
diff --git a/be/src/util/cpu_info.h b/be/src/util/cpu_info.h
index edc8f7fdbc1e23..987e073282eb6e 100644
--- a/be/src/util/cpu_info.h
+++ b/be/src/util/cpu_info.h
@@ -97,6 +97,11 @@ class CpuInfo {
         return cache_sizes;
     }
 
+    static long get_l2_cache_size() {
+        const auto& cache_sizes = CpuInfo::get_cache_sizes();
+        return cache_sizes[CpuInfo::L2_CACHE] ? cache_sizes[CpuInfo::L2_CACHE] : DEFAULT_L2_CACHE_SIZE;
+    }
+
     static long get_l3_cache_size() {
         auto& cache_sizes = get_cache_sizes();
         return cache_sizes[CacheLevel::L3_CACHE] ? cache_sizes[CacheLevel::L3_CACHE]
@@ -120,6 +125,8 @@ class CpuInfo {
     static int64_t* TEST_mutable_hardware_flags() { return &hardware_flags_; }
 
 private:
+    static constexpr size_t DEFAULT_L2_CACHE_SIZE = 1 * 1024 * 1024;
+
     /// Initialize NUMA-related state - called from Init();
     static void _init_numa();
 
diff --git a/be/test/formats/parquet/file_reader_test.cpp b/be/test/formats/parquet/file_reader_test.cpp
index 1a12b653537d8c..d606d6856dd822 100644
--- a/be/test/formats/parquet/file_reader_test.cpp
+++ b/be/test/formats/parquet/file_reader_test.cpp
@@ -4250,4 +4250,48 @@ TEST_F(FileReaderTest, plain_string_decode) {
     EXPECT_EQ(2, total_row_nums);
 }
 
+TEST_F(FileReaderTest, test_filter_to_dict_decoder) {
+    auto chunk = std::make_shared<Chunk>();
+    chunk->append_column(ColumnHelper::create_column(TYPE_INT_DESC, true), chunk->num_columns());
+    chunk->append_column(ColumnHelper::create_column(TYPE_INT_DESC, true), chunk->num_columns());
+    chunk->append_column(ColumnHelper::create_column(TYPE_VARCHAR_DESC, true), chunk->num_columns());
+    chunk->append_column(ColumnHelper::create_column(TYPE_VARCHAR_ARRAY_DESC, true), chunk->num_columns());
+
+    const std::string file_path = "./be/test/formats/parquet/test_data/big_string_dict_with_plain_code.parquet";
+
+    Utils::SlotDesc slot_descs[] = {{"c0", TYPE_INT_DESC},
+                                    {"c1", TYPE_INT_DESC},
+                                    {"c2", TYPE_VARCHAR_DESC},
+                                    {"c3", TYPE_VARCHAR_ARRAY_DESC},
+                                    {""}};
+
+    std::set<int32_t> in_oprands{1, 100};
+    std::vector<TExpr> t_conjuncts;
+    ParquetUTBase::create_in_predicate_int_conjunct_ctxs(TExprOpcode::FILTER_IN, 0, in_oprands, &t_conjuncts);
+    std::vector<ExprContext*> expr_ctxs;
+    ParquetUTBase::create_conjunct_ctxs(&_pool, _runtime_state, &t_conjuncts, &expr_ctxs);
+    auto ctx = _create_file_random_read_context(file_path, slot_descs);
+    ctx->conjunct_ctxs_by_slot.insert({0, expr_ctxs});
+
+    TupleDescriptor* tuple_desc = Utils::create_tuple_descriptor(_runtime_state, &_pool, slot_descs);
+    ParquetUTBase::setup_conjuncts_manager(ctx->conjunct_ctxs_by_slot[0], nullptr, tuple_desc, _runtime_state, ctx);
+
+    auto file_reader = _create_file_reader(file_path);
+    Status status = file_reader->init(ctx);
+    ASSERT_TRUE(status.ok());
+    size_t total_row_nums = 0;
+    while (!status.is_end_of_file()) {
+        chunk->reset();
+        status = file_reader->get_next(&chunk);
+        chunk->check_or_die();
+        total_row_nums += chunk->num_rows();
+        if (chunk->num_rows() == 2) {
+            ASSERT_EQ(chunk->debug_row(0), "[1, 100000, '0000000001', ['1','','0']]");
+            ASSERT_EQ(chunk->debug_row(1), "[100, 99901, '', []]");
+        }
+    }
+
+    EXPECT_EQ(2, total_row_nums);
+}
+
 } // namespace starrocks::parquet
diff --git a/be/test/formats/parquet/test_data/big_string_dict_with_plain_code.parquet b/be/test/formats/parquet/test_data/big_string_dict_with_plain_code.parquet
new file mode 100644
index 00000000000000..a965e9c69e9e6b
Binary files /dev/null and b/be/test/formats/parquet/test_data/big_string_dict_with_plain_code.parquet differ


Reviews:
user: login: trueeyu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-14T07:13:44Z
user: login: trueeyu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-14T07:15:45Z
user: login: zombee0
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-14T08:28:55Z
user: login: trueeyu
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T05:57:58Z
user: login: dirtysalt
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-21T08:10:58Z

Review comments:
diff_hunk: @@ -81,9 +82,67 @@ class DictEncoder final : public Encoder {
     faststring _buffer;
 };
 
+class CacheAwareDictDecoder : public Decoder {
+public:
+    CacheAwareDictDecoder() { _set_dict_size_threshold(); }
+    ~CacheAwareDictDecoder() override = default;
+
+    Status next_batch(size_t count, ColumnContentType content_type, Column* dst, const FilterData* filter) override {
+        switch (content_type) {
+        case DICT_CODE: {
+            FixedLengthColumn<int32_t>* data_column;
+            if (dst->is_nullable()) {
+                auto nullable_column = down_cast<NullableColumn*>(dst);
+                nullable_column->null_column()->append_default(count);
+                data_column = down_cast<FixedLengthColumn<int32_t>*>(nullable_column->data_column().get());
+            } else {
+                data_column = down_cast<FixedLengthColumn<int32_t>*>(dst);
+            }
+            size_t cur_size = data_column->size();
+            data_column->resize_uninitialized(cur_size + count);
+            int32_t* __restrict__ data = data_column->get_data().data() + cur_size;
+            auto decoded_num = _rle_batch_reader.GetBatch(reinterpret_cast<uint32_t*>(data), count);
+            if (decoded_num < count) {
+                return Status::InternalError("didn't get enough data from dict-decoder");
+            }
+            break;
+        }
+        case VALUE: {
+#ifdef BE_TEST
+            return _next_batch_value(count, dst, filter);
+#else
+            if (_get_dict_size() > _dict_size_threshold && config::parquet_cache_aware_dict_decoder_enable) {
+                return _next_batch_value(count, dst, filter);
+            } else {
+                return _next_batch_value(count, dst, nullptr);
+            }
+#endif // BE_TEST
+        }
+        default:
+            return Status::NotSupported("read type not supported");
+        }
+        return Status::OK();
+    }
+
+protected:
+    virtual size_t _get_dict_size() const = 0;
+    virtual Status _next_batch_value(size_t count, Column* dst, const FilterData* filter) = 0;
+    RleBatchDecoder<uint32_t> _rle_batch_reader;
+
+private:
+    static const size_t DEFAULT_L2_CACHE_SIZE = 1 * 1024 * 1024;
+    void _set_dict_size_threshold() {
+        const auto& cache_sizes = CpuInfo::get_cache_sizes();
path: be/src/formats/parquet/encoding_dict.h
user: login: trueeyu
body: Why not use static const 
created_at: 2025-03-14T07:13:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -81,9 +82,67 @@ class DictEncoder final : public Encoder {
     faststring _buffer;
 };
 
+class CacheAwareDictDecoder : public Decoder {
+public:
+    CacheAwareDictDecoder() { _set_dict_size_threshold(); }
+    ~CacheAwareDictDecoder() override = default;
+
+    Status next_batch(size_t count, ColumnContentType content_type, Column* dst, const FilterData* filter) override {
+        switch (content_type) {
+        case DICT_CODE: {
+            FixedLengthColumn<int32_t>* data_column;
+            if (dst->is_nullable()) {
+                auto nullable_column = down_cast<NullableColumn*>(dst);
+                nullable_column->null_column()->append_default(count);
+                data_column = down_cast<FixedLengthColumn<int32_t>*>(nullable_column->data_column().get());
+            } else {
+                data_column = down_cast<FixedLengthColumn<int32_t>*>(dst);
+            }
+            size_t cur_size = data_column->size();
+            data_column->resize_uninitialized(cur_size + count);
+            int32_t* __restrict__ data = data_column->get_data().data() + cur_size;
+            auto decoded_num = _rle_batch_reader.GetBatch(reinterpret_cast<uint32_t*>(data), count);
+            if (decoded_num < count) {
+                return Status::InternalError("didn't get enough data from dict-decoder");
+            }
+            break;
+        }
+        case VALUE: {
+#ifdef BE_TEST
+            return _next_batch_value(count, dst, filter);
+#else
+            if (_get_dict_size() > _dict_size_threshold && config::parquet_cache_aware_dict_decoder_enable) {
+                return _next_batch_value(count, dst, filter);
+            } else {
+                return _next_batch_value(count, dst, nullptr);
+            }
+#endif // BE_TEST
+        }
+        default:
+            return Status::NotSupported("read type not supported");
+        }
+        return Status::OK();
+    }
+
+protected:
+    virtual size_t _get_dict_size() const = 0;
+    virtual Status _next_batch_value(size_t count, Column* dst, const FilterData* filter) = 0;
+    RleBatchDecoder<uint32_t> _rle_batch_reader;
+
+private:
+    static const size_t DEFAULT_L2_CACHE_SIZE = 1 * 1024 * 1024;
+    void _set_dict_size_threshold() {
+        const auto& cache_sizes = CpuInfo::get_cache_sizes();
+        auto l2_cache_size = cache_sizes[CpuInfo::L2_CACHE];
path: be/src/formats/parquet/encoding_dict.h
user: login: trueeyu
body: implement one function CpuInfo::get_l2_cache_size like 'static long get_l3_cache_size() `?
created_at: 2025-03-14T07:15:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -81,9 +82,67 @@ class DictEncoder final : public Encoder {
     faststring _buffer;
 };
 
+class CacheAwareDictDecoder : public Decoder {
+public:
+    CacheAwareDictDecoder() { _set_dict_size_threshold(); }
+    ~CacheAwareDictDecoder() override = default;
+
+    Status next_batch(size_t count, ColumnContentType content_type, Column* dst, const FilterData* filter) override {
+        switch (content_type) {
+        case DICT_CODE: {
+            FixedLengthColumn<int32_t>* data_column;
+            if (dst->is_nullable()) {
+                auto nullable_column = down_cast<NullableColumn*>(dst);
+                nullable_column->null_column()->append_default(count);
+                data_column = down_cast<FixedLengthColumn<int32_t>*>(nullable_column->data_column().get());
+            } else {
+                data_column = down_cast<FixedLengthColumn<int32_t>*>(dst);
+            }
+            size_t cur_size = data_column->size();
+            data_column->resize_uninitialized(cur_size + count);
+            int32_t* __restrict__ data = data_column->get_data().data() + cur_size;
+            auto decoded_num = _rle_batch_reader.GetBatch(reinterpret_cast<uint32_t*>(data), count);
+            if (decoded_num < count) {
+                return Status::InternalError("didn't get enough data from dict-decoder");
+            }
+            break;
+        }
+        case VALUE: {
+#ifdef BE_TEST
+            return _next_batch_value(count, dst, filter);
+#else
+            if (_get_dict_size() > _dict_size_threshold && config::parquet_cache_aware_dict_decoder_enable) {
+                return _next_batch_value(count, dst, filter);
+            } else {
+                return _next_batch_value(count, dst, nullptr);
+            }
+#endif // BE_TEST
+        }
+        default:
+            return Status::NotSupported("read type not supported");
+        }
+        return Status::OK();
+    }
+
+protected:
+    virtual size_t _get_dict_size() const = 0;
+    virtual Status _next_batch_value(size_t count, Column* dst, const FilterData* filter) = 0;
+    RleBatchDecoder<uint32_t> _rle_batch_reader;
+
+private:
+    static const size_t DEFAULT_L2_CACHE_SIZE = 1 * 1024 * 1024;
+    void _set_dict_size_threshold() {
+        const auto& cache_sizes = CpuInfo::get_cache_sizes();
+        auto l2_cache_size = cache_sizes[CpuInfo::L2_CACHE];
path: be/src/formats/parquet/encoding_dict.h
user: login: zombee0
body: ok, will do it.
created_at: 2025-03-14T08:28:55Z
author_association: CONTRIBUTOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-03-18T07:51:53Z
body: #### [Java-Extensions Incremental Coverage Report]


:white_check_mark: **pass** : 0 / 0 (0%)




author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-03-18T07:51:55Z
body: #### [FE Incremental Coverage Report]


:white_check_mark: **pass** : 0 / 0 (0%)




author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-03-18T08:01:52Z
body: #### [BE Incremental Coverage Report]

:white_check_mark: **pass** : 58 / 66 (87.88%)




#### file detail

|   |path|covered_line|new_line|coverage|not_covered_line_detail|
|----|----|----|----|----|----|
|:large_blue_circle:|be/src/formats/parquet/encoding_dict.h|55|63|87.30%|[107, 116, 122, 123, 192, 201, 339, 347]|
|:large_blue_circle:|be/src/util/cpu_info.h|3|3|100.00%|[]|




author_association: CONTRIBUTOR