From ce21aa64dea117a6e8c0d561eb5d50bc5fea33b8 Mon Sep 17 00:00:00 2001 From: youngjinj Date: Sat, 8 Aug 2026 08:35:53 +0900 Subject: [PATCH 1/2] Set an exact-domain attribute value by struct copy heap_attrinfo_set runs the full generic pipeline for every value of every row -- attrvalue locate, type table lookup, domain re-init, tp_domain_check, setval dispatch -- although the verdict is the same for every row of a multi-row INSERT. A value that already fits the column domain is now handed over by struct copy after the per-row guards (NULL, string length, codeset, collation); anything else keeps the generic path, so coercion and error semantics are unchanged. The slow path passes strings by reference as well (setval with copy == false), so the shallow copy does not change ownership, and the copy carries the column's precision the way the coercion would. Precision counts characters, so byte size alone would exclude a multi-byte value that fills the column; the character count is asked for only when byte size does not already settle it, which is the case the generic path would count as well. Marginal cost per value drops from 0.77us to 0.40us (1-column vs 9-column INSERT..SELECT scaling, 400k rows). Co-Authored-By: Claude Fable 5 --- src/storage/heap_file.c | 68 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/src/storage/heap_file.c b/src/storage/heap_file.c index 1b519271823..b1d65ac9bc8 100644 --- a/src/storage/heap_file.c +++ b/src/storage/heap_file.c @@ -11594,6 +11594,74 @@ heap_attrinfo_set (const OID * inst_oid, ATTR_ID attrid, DB_VALUE * attr_val, HE goto exit_on_error; } + /* Hand over a value whose type already matches the column domain by struct copy, + * skipping the domain check, the domain init and the setval dispatch, + * which repeat the same verdict for every row of a multi-row INSERT. + * The slow path below passes strings by reference as well (setval with copy == false), + * so the shallow copy does not change ownership. + * CHAR needs pad semantics and set/LOB types need real cloning -- slow path. */ + if (!DB_IS_NULL (attr_val)) + { + TP_DOMAIN *domain = value->last_attrepr->domain; + DB_TYPE src_type = DB_VALUE_DOMAIN_TYPE (attr_val); + + if (src_type == TP_DOMAIN_TYPE (domain)) + { + bool use_fast_path = false; + + switch (src_type) + { + case DB_TYPE_INTEGER: + case DB_TYPE_BIGINT: + case DB_TYPE_SHORT: + case DB_TYPE_FLOAT: + case DB_TYPE_DOUBLE: + use_fast_path = true; + break; + case DB_TYPE_VARCHAR: + /* Precision counts characters. Byte size settles it whenever it already fits, + * which covers a single-byte codeset and anything with room to spare; + * only a multi-byte value that fills the column needs the character count, + * and that is a value the generic path would count as well. */ + if (attr_val->data.ch.info.is_max_string == false + && db_get_string_codeset (attr_val) == TP_DOMAIN_CODESET (domain) + && db_get_string_collation (attr_val) == TP_DOMAIN_COLLATION (domain) + && (db_get_string_size (attr_val) <= domain->precision + || db_get_string_length (attr_val) <= domain->precision)) + { + use_fast_path = true; + } + break; + default: + break; + } + + if (use_fast_path) + { + ret = pr_clear_value (&value->dbvalue); + if (ret != NO_ERROR) + { + goto exit_on_error; + } + + value->dbvalue = *attr_val; + value->dbvalue.need_clear = false; + if (src_type == DB_TYPE_VARCHAR) + { + value->dbvalue.data.ch.info.compressed_need_clear = false; + /* The generic path coerces into the column domain, so the value it leaves + * carries the column's precision. Carry it here too, or the same column + * would hold values whose precision depends on which path set them. */ + value->dbvalue.domain.char_info.length = domain->precision; + } + + value->state = HEAP_WRITTEN_ATTRVALUE; + + return NO_ERROR; + } + } + } + pr_type = pr_type_from_id (value->last_attrepr->type); if (pr_type == NULL) { From 2d91b7e50a4ab52388b174cec177bc2f1ba346ce Mon Sep 17 00:00:00 2001 From: youngjinj Date: Fri, 14 Aug 2026 08:56:37 +0900 Subject: [PATCH 2/2] Take the value shortcut for CHAR as well CHAR was left out of the shortcut because the column keeps trailing-space padding, and a struct copy does not pad. But the padding does not happen when the value is placed -- it happens on the way out, in mr_lengthval_char_internal and mr_writeval_char_internal, and it is keyed off the precision the value carries. So the shortcut only has to stamp the column precision, which it already does for VARCHAR. Without the stamp the precision stays floating, which is how both a literal and a bound value arrive, and the padding block is skipped entirely -- for CHAR that stamp is data correctness, not display consistency. Nothing reads the value between heap_attrinfo_set and heap_attrinfo_transform_to_disk: index keys are read back from the built record, and REPLACE builds its own record first. So handing the value over still unpadded is not observable. heap_attrinfo_set drops from 0.78% to 0.22% of server samples on a bound CHAR(120) + CHAR(60) insert, matching what VARCHAR already cost, and the coercion symbols leave the profile. Co-Authored-By: Claude Opus 5 (1M context) --- src/storage/heap_file.c | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/storage/heap_file.c b/src/storage/heap_file.c index b1d65ac9bc8..386c76c0528 100644 --- a/src/storage/heap_file.c +++ b/src/storage/heap_file.c @@ -11619,10 +11619,17 @@ heap_attrinfo_set (const OID * inst_oid, ATTR_ID attrid, DB_VALUE * attr_val, HE use_fast_path = true; break; case DB_TYPE_VARCHAR: + case DB_TYPE_CHAR: /* Precision counts characters. Byte size settles it whenever it already fits, * which covers a single-byte codeset and anything with room to spare; * only a multi-byte value that fills the column needs the character count, - * and that is a value the generic path would count as well. */ + * and that is a value the generic path would count as well. + * CHAR carries trailing-space padding, but the padding is applied on the way out, + * by mr_lengthval_char_internal, keyed off the precision the value carries -- + * which is why the stamp below is not optional for CHAR. Nothing between here and + * heap_attrinfo_transform_to_disk reads the value: index keys are read back from + * the built record, and REPLACE builds its own record first. So handing the value + * over still unpadded is not observable. */ if (attr_val->data.ch.info.is_max_string == false && db_get_string_codeset (attr_val) == TP_DOMAIN_CODESET (domain) && db_get_string_collation (attr_val) == TP_DOMAIN_COLLATION (domain) @@ -11646,12 +11653,15 @@ heap_attrinfo_set (const OID * inst_oid, ATTR_ID attrid, DB_VALUE * attr_val, HE value->dbvalue = *attr_val; value->dbvalue.need_clear = false; - if (src_type == DB_TYPE_VARCHAR) + if (src_type == DB_TYPE_VARCHAR || src_type == DB_TYPE_CHAR) { value->dbvalue.data.ch.info.compressed_need_clear = false; /* The generic path coerces into the column domain, so the value it leaves * carries the column's precision. Carry it here too, or the same column - * would hold values whose precision depends on which path set them. */ + * would hold values whose precision depends on which path set them. + * For CHAR this is what makes the trailing-space padding happen at all: + * mr_writeval_char_internal skips padding on a floating precision, which + * is what a literal and a bound value both arrive with. */ value->dbvalue.domain.char_info.length = domain->precision; }