LPAD and RPAD: copy the padding from a per-call plan instead of decoding it character by character (#27858)

Fixes #27846.

`InsertPadding` decoded one pad character per output character, flushing
the pad whenever it ran out, so a 200-character pad cost 200 decodes and
as many one-byte inserts per row. Since the decoder became an
out-of-line call (#26485) and the result size check counted the pad
again on every row (#26482), `lpad(x, 200, '0')` takes 1.75x as long on
v2.0.0 as on v1.5.5 in the report, and about ten times as long as
`repeat('0', 200) || x` on both.

**The change**
- The pad's geometry is counted once per call: its characters, the whole
copies the padding needs, and the bytes of the remainder. The size check
uses the same plan instead of counting again, the buffer is reserved to
the result size, and the padding is copied whole, a single byte at a
time for a one-byte pad. LEFT and RIGHT share one function.
- Results and errors are unchanged: the existing tests pass, including
the multibyte remainders and the maximum-size errors.

**Measured** on an M-series Mac, one thread, 2M rows, seconds, best of
3:

| statement | v1.5.5 | v2.0.0 | main | this PR |
|---|---:|---:|---:|---:|
| `lpad(i::VARCHAR, 18, '0')` | 0.131 | 0.151 | 0.145 | 0.098 |
| `lpad(i::VARCHAR, 50, '0')` | 0.277 | 0.347 | 0.328 | 0.115 |
| `lpad(i::VARCHAR, 200, '0')` | 0.984 | 1.227 | 1.114 | 0.126 |
| `rpad(i::VARCHAR, 200, '0')` | 0.966 | 1.212 | 1.112 | 0.129 |
| `lpad(i::VARCHAR, 200, 'ab😀')` | 0.979 | 1.313 | 1.264 | 0.628 |
| `repeat('0', 200) \|\| i::VARCHAR` | 0.102 | 0.085 | 0.081 | 0.082 |

**Verification**
- `test/sql/function/string/test_pad.test` gains one-byte, one-character
multibyte and longer-than-needed pads, a multibyte pad counted in
characters, and the empty pad with and without padding needed; the
string function group passes.
- `benchmark/micro/string/lpad.benchmark`, registered for the regression
job, tracks the one-byte case.
This commit is contained in:
Mark authored and GitHub committed 2026-10-27 21:39:20 +00:00
commit 3dc6a1a971
4 files changed
+91 -72

No files matched your search

+1
View File
@@ -48,3 +48,4 @@ benchmark/micro/update/upsert_enum_key.benchmark
benchmark/micro/aggregate/min_double.benchmark
benchmark/micro/aggregate/max_double.benchmark
benchmark/micro/pushdown/view_filter_below_delim_join.benchmark
benchmark/micro/string/lpad.benchmark
1 benchmark/micro/cast/cast_date_string.benchmark
48 benchmark/micro/aggregate/min_double.benchmark
49 benchmark/micro/aggregate/max_double.benchmark
50 benchmark/micro/pushdown/view_filter_below_delim_join.benchmark
51 benchmark/micro/string/lpad.benchmark
+12
View File
@@ -0,0 +1,12 @@
# name: benchmark/micro/string/lpad.benchmark
# description: LPAD of short strings to 200 characters with a one-byte pad
# group: [string]
name LPAD
group string
run
SELECT max(strlen(lpad(i::VARCHAR, 200, '0'))) FROM range(2000000) t(i)
result I
200
+50 -72
View File
@@ -22,21 +22,33 @@ static pair<idx_t, idx_t> PadCountChars(const idx_t len, const char *data, const
return pair<idx_t, idx_t>(nbytes, nchars);
}
static idx_t PaddingByteCount(const idx_t len, const string_t &pad) {
// Count the bytes InsertPadding will write
auto data = pad.GetData();
auto size = pad.GetSize();
if (len == 0 || size == 0) {
return 0;
//! The padding a call writes: whole copies of the pad, then the bytes of its first remainder characters. Decided from
//! the pad's geometry, counted once per call, so that the output is copied rather than decoded character by character.
struct PadPlan {
idx_t pad_bytes = 0;
idx_t pad_chars = 0;
idx_t copies = 0;
idx_t remainder_bytes = 0;
idx_t Bytes() const {
return copies * pad_bytes + remainder_bytes;
}
auto pad_chars = PadCountChars(NumericLimits<idx_t>::Maximum(), data, size).second;
auto remainder = PadCountChars(len % pad_chars, data, size).first;
return (len / pad_chars) * size + remainder;
};
static PadPlan PlanPadding(const idx_t len, const string_t &pad) {
PadPlan plan;
plan.pad_bytes = pad.GetSize();
plan.pad_chars = PadCountChars(NumericLimits<idx_t>::Maximum(), pad.GetData(), plan.pad_bytes).second;
if (len == 0 || plan.pad_chars == 0) {
return plan;
}
plan.copies = len / plan.pad_chars;
plan.remainder_bytes = PadCountChars(len % plan.pad_chars, pad.GetData(), plan.pad_bytes).first;
return plan;
}
static void CheckPadResultSize(const idx_t len, const string_t &pad, const pair<idx_t, idx_t> &written,
const char *name) {
auto result_size = written.first + PaddingByteCount(len - written.second, pad);
static void CheckPadResultSize(const idx_t written_bytes, const PadPlan &plan, const char *name) {
auto result_size = written_bytes + plan.Bytes();
if (result_size > string_t::MAX_STRING_SIZE) {
throw OutOfRangeException(
"Cannot create a string of size: '%d' in %s, the maximum supported string size is: '%d'", result_size, name,
@@ -44,37 +56,21 @@ static void CheckPadResultSize(const idx_t len, const string_t &pad, const pair<
}
}
static bool InsertPadding(const idx_t len, const string_t &pad, vector<char> &result) {
// Copy the padding until the output is long enough
static void InsertPadding(const string_t &pad, const PadPlan &plan, vector<char> &result) {
auto data = pad.GetData();
auto size = pad.GetSize();
// Check whether we need data that we don't have
if (len > 0 && size == 0) {
return false;
if (plan.pad_bytes == 1) {
result.insert(result.end(), plan.copies, data[0]);
return;
}
// Insert characters until we have all we need.
idx_t nbytes = 0;
for (idx_t nchars = 0; nchars < len; ++nchars) {
// If we are at the end of the pad, flush all of it and loop back
if (nbytes >= size) {
result.insert(result.end(), data, data + size);
nbytes = 0;
}
// Write the next character
int32_t codepoint;
nbytes += DecodeCodepoint(data + nbytes, size - nbytes, codepoint);
for (idx_t i = 0; i < plan.copies; i++) {
result.insert(result.end(), data, data + plan.pad_bytes);
}
// Flush the remaining pad
result.insert(result.end(), data, data + nbytes);
return true;
result.insert(result.end(), data, data + plan.remainder_bytes);
}
static string_t LeftPadFunction(const string_t &str, const int32_t len, const string_t &pad, vector<char> &result) {
template <bool LEFT>
static string_t PadString(const string_t &str, const int32_t len, const string_t &pad, vector<char> &result,
const char *name) {
// Reuse the buffer
result.clear();
@@ -82,55 +78,37 @@ static string_t LeftPadFunction(const string_t &str, const int32_t len, const st
auto data_str = str.GetData();
auto size_str = str.GetSize();
// Count how much of str will fit in the output
// Count how much of str will fit in the output, and pad by the number of characters still needed
auto written = PadCountChars(UnsafeNumericCast<idx_t>(len), data_str, size_str);
CheckPadResultSize(UnsafeNumericCast<idx_t>(len), pad, written, "LPAD");
// Left pad by the number of characters still needed
if (!InsertPadding(UnsafeNumericCast<idx_t>(len) - written.second, pad, result)) {
throw InvalidInputException("Insufficient padding in LPAD.");
auto padding_chars = UnsafeNumericCast<idx_t>(len) - written.second;
auto plan = PlanPadding(padding_chars, pad);
CheckPadResultSize(written.first, plan, name);
if (padding_chars > 0 && plan.pad_chars == 0) {
throw InvalidInputException("Insufficient padding in %s.", name);
}
result.reserve(written.first + plan.Bytes());
// Append as much of the original string as fits
result.insert(result.end(), data_str, data_str + written.first);
if (LEFT) {
InsertPadding(pad, plan, result);
result.insert(result.end(), data_str, data_str + written.first);
} else {
result.insert(result.end(), data_str, data_str + written.first);
InsertPadding(pad, plan, result);
}
return string_t(result.data(), UnsafeNumericCast<uint32_t>(result.size()));
}
struct LeftPadOperator {
static inline string_t Operation(const string_t &str, const int32_t len, const string_t &pad,
vector<char> &result) {
return LeftPadFunction(str, len, pad, result);
return PadString<true>(str, len, pad, result, "LPAD");
}
};
static string_t RightPadFunction(const string_t &str, const int32_t len, const string_t &pad, vector<char> &result) {
// Reuse the buffer
result.clear();
// Get information about the base string
auto data_str = str.GetData();
auto size_str = str.GetSize();
// Count how much of str will fit in the output
auto written = PadCountChars(UnsafeNumericCast<idx_t>(len), data_str, size_str);
CheckPadResultSize(UnsafeNumericCast<idx_t>(len), pad, written, "RPAD");
// Append as much of the original string as fits
result.insert(result.end(), data_str, data_str + written.first);
// Right pad by the number of characters still needed
if (!InsertPadding(UnsafeNumericCast<idx_t>(len) - written.second, pad, result)) {
throw InvalidInputException("Insufficient padding in RPAD.");
};
return string_t(result.data(), UnsafeNumericCast<uint32_t>(result.size()));
}
struct RightPadOperator {
static inline string_t Operation(const string_t &str, const int32_t len, const string_t &pad,
vector<char> &result) {
return RightPadFunction(str, len, pad, result);
return PadString<false>(str, len, pad, result, "RPAD");
}
};
+28
View File
@@ -142,3 +142,31 @@ query I
select RPAD('ab', 7, 'xy😀')
----
abxy😀xy
# a one-byte pad, a one-character multibyte pad, and a pad longer than the padding
query IIII
select LPAD('x', 10, '0'), LPAD('x', 4, '😀'), LPAD('x', 3, '😀ab'), RPAD('x', 3, '😀ab')
----
000000000x 😀😀😀x 😀ax x😀a
# the padding is counted in characters whatever the byte width of the pad
query III
select length(LPAD('', 1000, 'é')), strlen(LPAD('', 1000, 'é')), LPAD('ab', 5, 'é') = 'éééab'
----
1000 2000 true
# no padding needed: the pad is not read
query II
select LPAD('abc', 3, ''), RPAD('abcd', 3, '')
----
abc abc
statement error
select LPAD('abc', 4, '')
----
Insufficient padding in LPAD
statement error
select RPAD('abc', 4, '')
----
Insufficient padding in RPAD