This is an automated email from the ASF dual-hosted git repository.
HappenLee 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 678df4e859a [improvement](function) Add dictionary fast path for day
and week arithmetic (#67184)
678df4e859a is described below
commit 678df4e859a1a10a6a8f2fbfb726e5b676214b87
Author: Jerry Hu <[email protected]>
AuthorDate: Thu Aug 27 19:38:54 2026 +0800
[improvement](function) Add dictionary fast path for day and week
arithmetic (#67184)
`days_add`, `days_sub`, `weeks_add`, and `weeks_sub` on DATEV2 and
DATETIMEV2
currently use the generic `DateV2Value::date_add_interval` path for
every row.
That path constructs a `TimeInterval`, converts the date through
second-level
arithmetic, and rebuilds date and time fields even though day and week
intervals
only move the date part.
This PR adds an inline `DateV2Value::date_add_days` fast path. Dates in
the
existing 1900-2039 day-offset dictionary use direct day-number and
reverse-date
lookups; inputs or results outside that dictionary retain the generic
implementation. DATETIMEV2 keeps its time fields unchanged, and result
range
checks preserve the existing out-of-range behavior.
An author microbenchmark measured approximately 8.8 ns to 2.6 ns per row
for
clustered dates and 8.8 ns to 3.5 ns for dates spread across 1950-2030.
Differential coverage compares the new helper with the generic
implementation
across the supported date domain. Focused function tests cover leap
years,
boundaries, large deltas, DATEV2, DATETIMEV2, add/subtract paths, and
out-of-range results.
### Release note
None
### Check List (For Author)
- Test
- [x] Unit Test
- [ ] Regression test
- [ ] Manual test
- [ ] No need to test or manual test
- Behavior changed:
- [x] No
- [ ] Yes
- Does this need documentation?
- [x] No
- [ ] Yes
Validation:
- `./run-be-ut.sh --run
--filter='VDateTimeValueTest.date_add_days_matches_date_add_interval:VTimestampFunctionsTest.days_add_v2_test:VTimestampFunctionsTest.days_add_v2_boundary_test:VTimestampFunctionsTest.days_sub_v2_test:VTimestampFunctionsTest.weeks_add_v2_test:VTimestampFunctionsTest.weeks_add_v2_boundary_test:VTimestampFunctionsTest.weeks_sub_v2_test'
-j16` — 7 tests passed under ASAN_UT.
- `build-support/check-build-hygiene.sh` — passed.
- `build-support/clang-format.sh`, `build-support/check-format.sh`, and
`git diff --check origin/master...HEAD` — passed.
- `build-support/run-clang-tidy.sh --base origin/master --build-dir
be/ut_build_ASAN` — no diagnostics on changed ranges after the targeted
suppression for GTest macro expansion; the overall command remains
non-zero because of pre-existing diagnostics outside the diff and
toolchain header-resolution errors such as missing `stddef.h`.
---
be/src/core/value/vdatetime_value.h | 44 +++++++++++++
.../function_date_or_datetime_computation.h | 26 ++++++--
be/test/core/value/vdatetime_value_test.cpp | 51 +++++++++++++++
be/test/exprs/function/function_time_test.cpp | 74 ++++++++++++++++++++++
4 files changed, 189 insertions(+), 6 deletions(-)
diff --git a/be/src/core/value/vdatetime_value.h
b/be/src/core/value/vdatetime_value.h
index e906c4f842a..d7929bb101c 100644
--- a/be/src/core/value/vdatetime_value.h
+++ b/be/src/core/value/vdatetime_value.h
@@ -1089,6 +1089,12 @@ public:
template <TimeUnit unit, bool need_check = true>
bool date_add_interval(const TimeInterval& interval);
+ // Fast path for DAY / WEEK intervals: only the date part moves (daynr +-
days), the time part
+ // is untouched. Defined inline below `calc_daynr` so that per-row callers
fully inline it;
+ // falls back to the generic `date_add_interval<DAY>` outside the
day-offset dictionary.
+ template <bool need_check = true>
+ bool date_add_days(int64_t days);
+
template <TimeUnit unit>
bool date_set_interval(const TimeInterval& interval);
@@ -1764,6 +1770,44 @@ inline uint32_t calc_daynr(uint16_t year, uint8_t month,
uint8_t day) {
return delsum + y / 4 - y / 100 + y / 400;
}
+// DAY / WEEK fast path. Real workloads hold dates that cluster in a narrow
range, so the two
+// dictionary lookups below (`daynr(y, m, d)` and `daynr -> date`) are
L1-resident; measured about
+// 3x faster than the generic `date_add_interval<DAY>` (no TimeInterval, no
second-level
+// arithmetic, fully inlined into the caller's loop). Closed-form calendar
arithmetic was tried
+// and is ~2.5x SLOWER than the generic path: its chain of constant divisions
costs far more than
+// two warm table loads. Everything outside the dictionary (years < 1900 or >
2039, results
+// outside it, and year 0 with its MySQL daynr quirk) takes the generic path
unchanged.
+template <typename T>
+template <bool need_check>
+inline bool DateV2Value<T>::date_add_days(int64_t days) {
+ if constexpr (need_check) {
+ if (!is_valid_date()) [[unlikely]] {
+ return false;
+ }
+ } else {
+ DCHECK(is_valid_date());
+ }
+ if (date_day_offset_dict::can_speed_up_calc_daynr(date_v2_value_.year_) &&
+ LIKELY(date_day_offset_dict::get_dict_init())) [[likely]] {
+ const int64_t day_nr =
+ date_day_offset_dict::get().daynr(date_v2_value_.year_,
date_v2_value_.month_,
+ date_v2_value_.day_) +
+ days;
+ // range-check before narrowing: `days` may be up to INT32 * 7
+ if (day_nr > 0 && day_nr <= DATE_MAX_DAYNR &&
+
date_day_offset_dict::can_speed_up_daynr_to_date(static_cast<int>(day_nr)))
[[likely]] {
+ const auto to =
date_day_offset_dict::get()[date_day_offset_dict::get_offset_by_daynr(
+ static_cast<int>(day_nr))];
+ date_v2_value_.year_ = to.year();
+ date_v2_value_.month_ = to.month();
+ date_v2_value_.day_ = to.day();
+ return true;
+ }
+ }
+ return date_add_interval<TimeUnit::DAY, need_check>(
+ TimeInterval(TimeUnit::DAY, days < 0 ? -days : days, days < 0));
+}
+
class DatetimeValueUtil {
public:
template <bool only_time>
diff --git a/be/src/exprs/function/function_date_or_datetime_computation.h
b/be/src/exprs/function/function_date_or_datetime_computation.h
index db1bd7441fc..cd2a369a9b3 100644
--- a/be/src/exprs/function/function_date_or_datetime_computation.h
+++ b/be/src/exprs/function/function_date_or_datetime_computation.h
@@ -77,13 +77,27 @@ auto date_time_add(const typename
PrimitiveTypeTraits<ArgType>::DataType::FieldT
IntervalType delta) {
// e.g.: for DatatypeDatetimeV2, cast from u64 to
DateV2Value<DateTimeV2ValueType>
auto ts_value = t;
- TimeInterval interval(unit, std::abs(delta), delta < 0);
- if (!(ts_value.template date_add_interval<unit>(interval))) [[unlikely]] {
- throw_out_of_bound_date_int(get_time_unit_name(unit), t, delta);
- }
+ if constexpr ((unit == TimeUnit::DAY || unit == TimeUnit::WEEK) &&
+ is_date_v2_or_datetime_v2(ArgType)) {
+ // DAY / WEEK only move the date part. `date_add_days` is inline, so
the whole per-row
+ // computation folds into the caller's loop: no TimeInterval, no
second-level arithmetic,
+ // just two L1-resident dictionary lookups. The input comes from a
column and is already
+ // a valid date, so the per-row validity pre-check is skipped
(DCHECK'd in debug builds);
+ // the result is still range-checked.
+ const int64_t days = static_cast<int64_t>(delta) * (unit ==
TimeUnit::WEEK ? 7 : 1);
+ if (!ts_value.template date_add_days<false>(days)) [[unlikely]] {
+ throw_out_of_bound_date_int(get_time_unit_name(unit), t, delta);
+ }
+ return ts_value;
+ } else {
+ TimeInterval interval(unit, std::abs(delta), delta < 0);
+ if (!(ts_value.template date_add_interval<unit>(interval)))
[[unlikely]] {
+ throw_out_of_bound_date_int(get_time_unit_name(unit), t, delta);
+ }
- // here DateValueType = ResultDateValueType
- return ts_value;
+ // here DateValueType = ResultDateValueType
+ return ts_value;
+ }
}
#define ADD_TIME_FUNCTION_IMPL(CLASS, NAME, UNIT)
\
diff --git a/be/test/core/value/vdatetime_value_test.cpp
b/be/test/core/value/vdatetime_value_test.cpp
index 8cb701d97d5..6b3cd7488ae 100644
--- a/be/test/core/value/vdatetime_value_test.cpp
+++ b/be/test/core/value/vdatetime_value_test.cpp
@@ -1536,4 +1536,55 @@ TEST(VDateTimeValueTest,
date_add_interval_edge_cases_test) {
}
}
+// `date_add_days` (the DAY/WEEK fast path) must be observationally identical
to the generic
+// `date_add_interval<DAY>`: same success/failure, same date part, time part
untouched.
+// GTest assertions in this exhaustive differential test expand to nested
control flow that
+// clang-tidy counts as test-body complexity.
+// NOLINTNEXTLINE(readability-function-cognitive-complexity)
+TEST(VDateTimeValueTest, date_add_days_matches_date_add_interval) {
+ const int64_t deltas[] = {0, 1, -1, 28, -28, 29,
-29, 59, -59,
+ 60, -60, 61, -61, 365, -365,
366, -366, 1000,
+ -1000, 100000, -100000, 719528, -719528,
3652424, -3652424};
+ int64_t compared = 0;
+ // every 97th day of the whole domain, plus the first 120 days (year-0
quirk window)
+ for (int64_t daynr = 1; daynr <= DATE_MAX_DAYNR; daynr += (daynr < 120 ? 1
: 97)) {
+ DateV2Value<DateV2ValueType> base_date;
+ ASSERT_TRUE(base_date.get_date_from_daynr(daynr)) << daynr;
+ DateV2Value<DateTimeV2ValueType> base_dt;
+ ASSERT_TRUE(base_dt.get_date_from_daynr(daynr)) << daynr;
+ ASSERT_TRUE(base_dt.check_range_and_set_time(0, 0, 0, 23, 59, 58,
999999, true));
+ for (int64_t delta : deltas) {
+ {
+ auto expected = base_date;
+ auto actual = base_date;
+ const bool ok_expected =
expected.date_add_interval<TimeUnit::DAY>(
+ TimeInterval(TimeUnit::DAY, delta < 0 ? -delta :
delta, delta < 0));
+ const bool ok_actual = actual.date_add_days(delta);
+ ASSERT_EQ(ok_actual, ok_expected) << base_date << " + " <<
delta;
+ if (ok_expected) {
+ ASSERT_EQ(actual.to_int64(), expected.to_int64())
+ << base_date << " + " << delta;
+ }
+ }
+ {
+ auto expected = base_dt;
+ auto actual = base_dt;
+ const bool ok_expected =
expected.date_add_interval<TimeUnit::DAY>(
+ TimeInterval(TimeUnit::DAY, delta < 0 ? -delta :
delta, delta < 0));
+ const bool ok_actual = actual.date_add_days(delta);
+ ASSERT_EQ(ok_actual, ok_expected) << base_dt << " + " << delta;
+ if (ok_expected) {
+ ASSERT_EQ(actual.to_int64(), expected.to_int64()) <<
base_dt << " + " << delta;
+ ASSERT_EQ(actual.hour(), 23);
+ ASSERT_EQ(actual.minute(), 59);
+ ASSERT_EQ(actual.second(), 58);
+ ASSERT_EQ(actual.microsecond(), 999999);
+ }
+ }
+ ++compared;
+ }
+ }
+ EXPECT_GT(compared, 900000);
+}
+
} // namespace doris
diff --git a/be/test/exprs/function/function_time_test.cpp
b/be/test/exprs/function/function_time_test.cpp
index e012c7784e9..0ddc2771a59 100644
--- a/be/test/exprs/function/function_time_test.cpp
+++ b/be/test/exprs/function/function_time_test.cpp
@@ -1049,6 +1049,80 @@ TEST(VTimestampFunctionsTest, days_add_v2_test) {
}
}
+TEST(VTimestampFunctionsTest, days_add_v2_boundary_test) {
+ std::string func_name = "days_add";
+ {
+ InputTypeSet input_types = {PrimitiveType::TYPE_DATEV2,
PrimitiveType::TYPE_INT};
+ DataSet data_set = {
+ // leap-day and century rules
+ {{std::string("2020-02-28"), 1}, std::string("2020-02-29")},
+ {{std::string("2021-02-28"), 1}, std::string("2021-03-01")},
+ {{std::string("1900-02-28"), 1}, std::string("1900-03-01")},
+ {{std::string("2000-02-28"), 1}, std::string("2000-02-29")},
+ {{std::string("2100-02-28"), 1}, std::string("2100-03-01")},
+ // month / year boundaries in both directions
+ {{std::string("2020-12-31"), 1}, std::string("2021-01-01")},
+ {{std::string("2021-01-01"), -1}, std::string("2020-12-31")},
+ {{std::string("2020-03-01"), -1}, std::string("2020-02-29")},
+ // large deltas
+ {{std::string("1970-01-01"), 20000},
std::string("2024-10-04")},
+ {{std::string("2024-10-04"), -20000},
std::string("1970-01-01")},
+ // domain edges
+ {{std::string("9999-12-30"), 1}, std::string("9999-12-31")},
+ {{std::string("0001-01-01"), -1}, std::string("0000-12-31")},
+ {{std::string("0000-03-01"), 1}, std::string("0000-03-02")},
+ // year-0 quirk window keeps the historical (MySQL calc_daynr)
behaviour
+ {{std::string("0000-03-01"), -1}, std::string("0000-02-28")},
+ {{std::string("0000-01-01"), 1}, std::string("0000-01-02")},
+ {{Null(), 1}, Null()},
+ };
+ static_cast<void>(check_function<DataTypeDateV2, true>(func_name,
input_types, data_set));
+ }
+ {
+ // the time part is untouched, including the last microsecond of the
day
+ InputTypeSet input_types = {{PrimitiveType::TYPE_DATETIMEV2, 6},
PrimitiveType::TYPE_INT};
+ DataSet data_set = {
+ {{std::string("2020-02-28 23:59:59.999999"), 1},
+ std::string("2020-02-29 23:59:59.999999")},
+ {{std::string("2021-01-01 00:00:00.000001"), -1},
+ std::string("2020-12-31 00:00:00.000001")},
+ {{std::string("9999-12-30 23:59:59.999999"), 1},
+ std::string("9999-12-31 23:59:59.999999")},
+ };
+ static_cast<void>(
+ check_function<DataTypeDateTimeV2, true>(func_name,
input_types, data_set, 6));
+ }
+ {
+ // out of range must still raise
+ InputTypeSet input_types = {PrimitiveType::TYPE_DATEV2,
PrimitiveType::TYPE_INT};
+ DataSet data_set = {
+ {{std::string("9999-12-31"), 1}, Null()},
+ };
+ static_cast<void>(check_function<DataTypeDateV2, true>(func_name,
input_types, data_set, -1,
+ -1, true));
+ }
+ {
+ InputTypeSet input_types = {PrimitiveType::TYPE_DATEV2,
PrimitiveType::TYPE_INT};
+ DataSet data_set = {
+ {{std::string("0000-01-01"), -1}, Null()},
+ };
+ static_cast<void>(check_function<DataTypeDateV2, true>(func_name,
input_types, data_set, -1,
+ -1, true));
+ }
+}
+
+TEST(VTimestampFunctionsTest, weeks_add_v2_boundary_test) {
+ std::string func_name = "weeks_add";
+ InputTypeSet input_types = {PrimitiveType::TYPE_DATEV2,
PrimitiveType::TYPE_INT};
+ DataSet data_set = {
+ {{std::string("2020-12-31"), 1}, std::string("2021-01-07")},
+ {{std::string("2020-02-25"), 1}, std::string("2020-03-03")},
+ {{std::string("2021-01-07"), -1}, std::string("2020-12-31")},
+ {{std::string("2000-01-01"), 1043}, std::string("2019-12-28")},
+ };
+ static_cast<void>(check_function<DataTypeDateV2, true>(func_name,
input_types, data_set));
+}
+
TEST(VTimestampFunctionsTest, days_sub_v2_test) {
std::string func_name = "days_sub";
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]