<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Regular Expressions on OLAP Studio — Independent DuckDB Technical Blog</title><link>https://duckdblab.org/en/tags/regular-expressions/</link><description>Recent content in Regular Expressions on OLAP Studio — Independent DuckDB Technical Blog</description><generator>Hugo -- gohugo.io</generator><language>en-US</language><lastBuildDate>Thu, 12 Nov 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://duckdblab.org/en/tags/regular-expressions/index.xml" rel="self" type="application/rss+xml"/><item><title>DuckDB COLUMNS() in Depth: Batch-Renaming Columns with Regex Capture Groups</title><link>https://duckdblab.org/en/post/duckdb-columns-regex-capture-rename/</link><pubDate>Thu, 12 Nov 2026 00:00:00 +0000</pubDate><guid>https://duckdblab.org/en/post/duckdb-columns-regex-capture-rename/</guid><description>&lt;img src="https://duckdblab.org/images/posts/duckdb-columns-regex-capture-rename/architecture.png" alt="Featured image of post DuckDB COLUMNS() in Depth: Batch-Renaming Columns with Regex Capture Groups" /&gt;&lt;p&gt;&lt;img alt="COLUMNS() regex capture-group renaming: one SQL line replaces 40 lines of SELECT … AS" loading="lazy" sizes="(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px" src="https://duckdblab.org/images/posts/duckdb-columns-regex-capture-rename/architecture.png"&gt;&lt;/p&gt;
&lt;h2 id="the-problem-renaming-wide-table-columns-shouldnt-hurt-this-much"&gt;The problem: renaming wide-table columns shouldn&amp;rsquo;t hurt this much
&lt;/h2&gt;&lt;p&gt;If you work in data engineering, you&amp;rsquo;ve almost certainly hit this: an upstream BI export ships a wide table whose columns carry unit suffixes — &lt;code&gt;web_spend&lt;/code&gt;, &lt;code&gt;app_spend&lt;/code&gt;, &lt;code&gt;store_spend&lt;/code&gt;. Or a quarterly report with &lt;code&gt;q1_amount&lt;/code&gt;, &lt;code&gt;q2_amount&lt;/code&gt;, &lt;code&gt;q3_amount&lt;/code&gt;. The downstream consumer wants uniform naming (say, everything ending in &lt;code&gt;_usd&lt;/code&gt;), so you write:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;web_spend&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;web_spend_usd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;app_spend&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;app_spend_usd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;store_spend&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;store_spend_usd&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;marketing&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Three columns is fine. But when the wide table has 40 channels or 12 monthly columns, that SELECT balloons into dozens of lines of hand-copied code. Worse: &lt;strong&gt;when upstream adds a new channel, the SQL doesn&amp;rsquo;t error — it silently drops the column&lt;/strong&gt;. That&amp;rsquo;s the classic &amp;ldquo;silent data incident.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;Most teams end up writing a Python script that queries &lt;code&gt;information_schema.columns&lt;/code&gt; and assembles SQL strings. But the SQL-generating code itself needs maintenance, plus quote, case, and reserved-word handling. Could DuckDB do this itself at compile time?&lt;/p&gt;
&lt;p&gt;Yes. DuckDB&amp;rsquo;s &lt;code&gt;COLUMNS()&lt;/code&gt; expression supports &lt;strong&gt;regex capture-group renaming&lt;/strong&gt; — one SQL statement that simultaneously selects columns by pattern and renames them by capture group.&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;&lt;strong&gt;Scope of this article&lt;/strong&gt;: focuses on &lt;strong&gt;regex capture-group renaming&lt;/strong&gt; (&lt;code&gt;\1&lt;/code&gt;, &lt;code&gt;\0&lt;/code&gt;, &lt;code&gt;(?i)&lt;/code&gt; backreferences). For star modifiers (EXCLUDE / REPLACE / RENAME) and UNPIVOT de-duplication, see the &lt;a class="link" href="https://duckdblab.org/en/post/duckdb-columns-expression-star-modifiers/" &gt;predecessor foundational guide&lt;/a&gt;.&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;h2 id="1-minimal-runnable-example"&gt;1. Minimal runnable example
&lt;/h2&gt;&lt;p&gt;Let&amp;rsquo;s get the base syntax running first. Paste this straight into the DuckDB CLI:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;COLUMNS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;(\w*)_spend&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;\1_spend_usd&amp;#39;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;web_spend&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;app_spend&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;store_spend&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Output:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;┌───────────────┬───────────────┬─────────────────┐
│ web_spend_usd │ app_spend_usd │ store_spend_usd │
│ int32 │ int32 │ int32 │
├───────────────┼───────────────┼─────────────────┤
│ 10 │ 20 │ 30 │
└───────────────┴───────────────┴─────────────────┘
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Three things happen in that statement:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;'(\w*)_spend'&lt;/code&gt; is a regex&lt;/strong&gt;, not a LIKE wildcard. When you put a string constant in the &lt;code&gt;COLUMNS()&lt;/code&gt; argument position, DuckDB treats it as a &lt;strong&gt;regular expression&lt;/strong&gt; to match column names.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;(\w*)&lt;/code&gt; is a capture group&lt;/strong&gt;, capturing &lt;code&gt;web&lt;/code&gt; / &lt;code&gt;app&lt;/code&gt; / &lt;code&gt;store&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The &lt;code&gt;\1&lt;/code&gt; in &lt;code&gt;AS '\1_spend_usd'&lt;/code&gt; is replaced with the first capture group&amp;rsquo;s content&lt;/strong&gt; — so the final column names become &lt;code&gt;web_spend_usd&lt;/code&gt; and friends.&lt;/li&gt;
&lt;/ol&gt;

 &lt;blockquote&gt;
 &lt;p&gt;The official docs use &lt;code&gt;SELECT COLUMNS('(\w{3}).*') AS '\1'&lt;/code&gt;. &lt;code&gt;\0&lt;/code&gt; is the entire match, &lt;code&gt;\1&lt;/code&gt;/&lt;code&gt;\2&lt;/code&gt; are the first/second capture groups, matching sed / Python &lt;code&gt;re.sub&lt;/code&gt; backreference semantics.&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Why not &lt;code&gt;* REPLACE&lt;/code&gt;?&lt;/strong&gt; Because &lt;code&gt;REPLACE&lt;/code&gt; requires you to write &lt;code&gt;expression AS new_name&lt;/code&gt;, and the name must be a constant. Batch renaming would mean one line per column. &lt;code&gt;COLUMNS()&lt;/code&gt; + capture groups is the only way to &amp;ldquo;batch rename by rule.&amp;rdquo;&lt;/p&gt;
&lt;h2 id="2-scenario-1-batch-renaming-a-quarterly-wide-table"&gt;2. Scenario 1: batch-renaming a quarterly wide table
&lt;/h2&gt;&lt;p&gt;A common real-world report shape: one row per region/channel, followed by three quarterly columns. We want &lt;code&gt;q1_amount&lt;/code&gt; → &lt;code&gt;amount_q1&lt;/code&gt; (swap the prefix and suffix).&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;OR&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;REPLACE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;TABLE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;sales&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;VALUES&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;north&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;web&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;150&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;north&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;app&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;140&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;130&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;110&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;south&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;web&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;180&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;240&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;region&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;q1_amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;q2_amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;q3_amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;cost&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;region&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;COLUMNS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;q([0-9])_amount&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;amount_q\1&amp;#39;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;sales&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Output:&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;┌─────────┬─────────┬──────────────┬──────────────┬──────────────┐
│ region │ channel │ amount_q1 │ amount_q2 │ amount_q3 │
│ varchar │ varchar │ decimal(5,2) │ decimal(5,2) │ decimal(5,2) │
├─────────┼─────────┼──────────────┼──────────────┼──────────────┤
│ north │ web │ 100.00 │ 150.00 │ 120.00 │
│ north │ app │ 140.00 │ 130.00 │ 110.00 │
│ south │ web │ 200.00 │ 180.00 │ 240.00 │
└─────────┴─────────┴──────────────┴──────────────┴──────────────┘
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The &lt;code&gt;cost&lt;/code&gt; column is automatically dropped because the regex only matches &lt;code&gt;q[0-9]_amount&lt;/code&gt;. This is the second value of &lt;code&gt;COLUMNS()&lt;/code&gt; over &lt;code&gt;SELECT *&lt;/code&gt;: &lt;strong&gt;selection and renaming happen in one operation&lt;/strong&gt; — no separate SELECT then EXCLUDE.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;DESCRIBE&lt;/code&gt; confirms the names really changed (not just the display header):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;DESCRIBE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;COLUMNS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;q([0-9])_amount&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;amount_q\1&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;sales&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;┌───────────────────┐
│ sales │
│ │
│ amount_q1 decimal │
│ amount_q2 decimal │
│ amount_q3 decimal │
└───────────────────┘
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="3-scenario-2-adding-a-uniform-suffix-to-multi-currency-metrics"&gt;3. Scenario 2: adding a uniform suffix to multi-currency metrics
&lt;/h2&gt;&lt;p&gt;A more finance-flavored case: original columns &lt;code&gt;usd_sales&lt;/code&gt;, &lt;code&gt;eur_sales&lt;/code&gt;, &lt;code&gt;gbp_sales&lt;/code&gt;; downstream wants a &amp;ldquo;currency_metric_unit&amp;rdquo; three-part name like &lt;code&gt;usd_sales_usd&lt;/code&gt;.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;COLUMNS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;^(\w{3})_sales$&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;\1_sales_usd&amp;#39;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;usd_sales&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;eur_sales&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;gbp_sales&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Two regex details deserve their own note:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;^&lt;/code&gt; and &lt;code&gt;$&lt;/code&gt; anchoring&lt;/strong&gt;: without anchors, &lt;code&gt;\w{3}_sales&lt;/code&gt; would also match the &lt;code&gt;usd_sales&lt;/code&gt; substring inside &lt;code&gt;total_usd_sales&lt;/code&gt; — but the capture group would grab the wrong offset. With many wide-table columns, anchoring is mandatory insurance.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;\w&lt;/code&gt; includes underscores and digits&lt;/strong&gt;, so &lt;code&gt;\w{3}&lt;/code&gt; precisely matches &lt;code&gt;usd&lt;/code&gt; but also matches &lt;code&gt;a_1&lt;/code&gt;. For letters only, use &lt;code&gt;[a-z]{3}&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;An often-overlooked point: &lt;strong&gt;capture group numbering starts at &lt;code&gt;\1&lt;/code&gt;&lt;/strong&gt;; &lt;code&gt;\0&lt;/code&gt; is the whole match. The shortest way to add a uniform prefix to every column is:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;COLUMNS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;.*_amount&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;amt_\0&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;sales&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Column names become &lt;code&gt;amt_q1_amount&lt;/code&gt;, &lt;code&gt;amt_q2_amount&lt;/code&gt;, &lt;code&gt;amt_q3_amount&lt;/code&gt; — &lt;code&gt;\0&lt;/code&gt; keeps the full original match.&lt;/p&gt;
&lt;h2 id="4-scenario-3-mixed-case-column-selection-and-normalization"&gt;4. Scenario 3: mixed-case column selection and normalization
&lt;/h2&gt;&lt;p&gt;Upstream exports often use camelCase names like &lt;code&gt;Amount&lt;/code&gt; and &lt;code&gt;AmountUSD&lt;/code&gt;. &lt;code&gt;COLUMNS()&lt;/code&gt; regexes are &lt;strong&gt;case-sensitive&lt;/strong&gt; by default; add the inline flag &lt;code&gt;(?i)&lt;/code&gt; when you need case-insensitivity:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-- Case-sensitive: a.* only matches lowercase &amp;#39;apple&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;COLUMNS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;a.*&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;apple&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-- Result: one column, apple
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-- Case-insensitive: (?i)a.* matches both
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;COLUMNS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;(?i)a.*&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;apple&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-- Result: two columns, Amount and apple
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;(?i)&lt;/code&gt; is an inline-flag syntax that DuckDB&amp;rsquo;s regex engine supports — verified that under &lt;code&gt;(?i)&lt;/code&gt;, &lt;code&gt;a.*&lt;/code&gt; matches both uppercase &lt;code&gt;Amount&lt;/code&gt; and lowercase &lt;code&gt;apple&lt;/code&gt;.&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;&lt;strong&gt;Pitfall, measured&lt;/strong&gt;: the anchor &lt;code&gt;\A&lt;/code&gt; (PCRE &amp;ldquo;start of subject&amp;rdquo;) does &lt;strong&gt;not&lt;/strong&gt; behave per PCRE semantics for column-name matching in this engine — &lt;code&gt;COLUMNS('\A.*')&lt;/code&gt; and &lt;code&gt;COLUMNS('(?i)a.*')&lt;/code&gt; returned exactly the same two columns in testing. Don&amp;rsquo;t use &lt;code&gt;\A&lt;/code&gt; hoping to &amp;ldquo;anchor to uppercase A only&amp;rdquo;; &lt;code&gt;(?i)&lt;/code&gt; plus explicit case is the reliable approach.&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Lambda selection + &lt;code&gt;\0&lt;/code&gt; renaming works&lt;/strong&gt; (an easily-missed combination): the lambda side handles &lt;strong&gt;column selection&lt;/strong&gt;, the AS side handles &lt;strong&gt;renaming&lt;/strong&gt; — clean separation:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-- Lambda selects (case-insensitively, names starting with &amp;#39;a&amp;#39;), AS adds prefix/suffix via \0
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;COLUMNS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;ILIKE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;a%&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;x\0y&amp;#39;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;apple&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Measured output names: &lt;code&gt;xAmounty&lt;/code&gt;, &lt;code&gt;xappley&lt;/code&gt; — lambda selection and &lt;code&gt;\0&lt;/code&gt; renaming in one SQL line.&lt;/p&gt;
&lt;p&gt;But note: &lt;strong&gt;with lambda selection, the AS side cannot use &lt;code&gt;\1&lt;/code&gt;&lt;/strong&gt; (there&amp;rsquo;s no capture group to reference):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-- Binder Error: Only the backslash escape code \0 can be used when no regex is supplied to COLUMNS(*)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;COLUMNS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;ILIKE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;a%&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;\1&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;apple&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;You also cannot write a second &lt;code&gt;AS&lt;/code&gt; inside the lambda&lt;/strong&gt; — this is a syntax error, whatever some tutorials claim:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-- Parser Error: syntax error at or near &amp;#34;AS&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;COLUMNS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;ILIKE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;a%&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;...;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;To actually turn &lt;code&gt;Amount&lt;/code&gt; into &lt;code&gt;amount&lt;/code&gt;&lt;/strong&gt; (a real case change), neither capture groups nor &lt;code&gt;\0&lt;/code&gt; concatenation can do it — they only &amp;ldquo;keep the original and add material around it.&amp;rdquo; The viable paths are: select with lambda → grab the original name via &lt;code&gt;\0&lt;/code&gt; → wrap an explicit alias in the &lt;strong&gt;outer&lt;/strong&gt; query, or just use &lt;code&gt;ALTER TABLE ... RENAME COLUMN&lt;/code&gt; per column.&lt;/p&gt;
&lt;p&gt;Conclusion: &lt;strong&gt;use capture groups &lt;code&gt;'\1_suffix'&lt;/code&gt; for structured batch renaming; use &lt;code&gt;(?i)&lt;/code&gt; or lambda &lt;code&gt;ILIKE&lt;/code&gt; for case-insensitive selection; case normalization can only happen in the outer SQL or DDL — &lt;code&gt;COLUMNS()&lt;/code&gt; itself has no transformation power.&lt;/strong&gt; Keeping these three boundaries straight is what prevents &amp;ldquo;thought I changed the case, but it silently didn&amp;rsquo;t&amp;rdquo; bugs.&lt;/p&gt;
&lt;h2 id="5-scenario-4-batch-aggregation-with-self-documenting-names"&gt;5. Scenario 4: batch aggregation with self-documenting names
&lt;/h2&gt;&lt;p&gt;The most overlooked capability of &lt;code&gt;COLUMNS()&lt;/code&gt;: it isn&amp;rsquo;t limited to renaming in the SELECT list — it also works &lt;strong&gt;as an argument to aggregate functions&lt;/strong&gt;, and capture-group naming still applies.&lt;/p&gt;
&lt;p&gt;Here we compute min and max of the three quarterly columns per region, with semantically self-describing result names:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;COLUMNS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;q([0-9])_amount&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;lo_q\1&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;COLUMNS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;q([0-9])_amount&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;hi_q\1&amp;#39;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;sales&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Output (&lt;strong&gt;min and max each expand to 3 quarterly columns, 6 columns total, no duplicate names&lt;/strong&gt;):&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;┌──────────────┬──────────────┬──────────────┬──────────────┬──────────────┬──────────────┐
│ lo_q1 │ lo_q2 │ lo_q3 │ hi_q1 │ hi_q2 │ hi_q3 │
│ decimal(5,2) │ decimal(5,2) │ decimal(5,2) │ decimal(5,2) │ decimal(5,2) │ decimal(5,2) │
├──────────────┼──────────────┼──────────────┼──────────────┼──────────────┼──────────────┤
│ 100.00 │ 130.00 │ 110.00 │ 200.00 │ 180.00 │ 240.00 │
└──────────────┴──────────────┴──────────────┴──────────────┴──────────────┴──────────────┘
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This is the most useful combination in the article. Without capture groups, &lt;code&gt;AS lo&lt;/code&gt; expands into 3 identically-named &lt;code&gt;lo&lt;/code&gt; columns — fine for positional reads, useless to the human eye. With &lt;code&gt;\1&lt;/code&gt;, names carry the quarter, so a CSV export to downstream won&amp;rsquo;t misalign.&lt;/p&gt;
&lt;h2 id="6-boundaries-when-the-capture-group-approach-doesnt-work"&gt;6. Boundaries: when the capture-group approach doesn&amp;rsquo;t work
&lt;/h2&gt;&lt;p&gt;Four hard limits we measured — you must know them before writing:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. Two different &lt;code&gt;COLUMNS()&lt;/code&gt; in the same expression are not allowed.&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-- Error: Binder Error: Multiple different STAR/COLUMNS in the same expression are not supported
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;COLUMNS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;.*_sales&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;COLUMNS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;.*_cost&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;To compute column-pair differences like &amp;ldquo;revenue minus cost,&amp;rdquo; you must UNPIVOT to long format first or explicitly list each pair. This is the most critical current design limitation of &lt;code&gt;COLUMNS()&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. The regex must match at least one column, or it errors immediately.&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-- Binder Error: Star expression &amp;#34;COLUMNS(...)&amp;#34; resulted in an empty set of columns
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;COLUMNS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;typo_prefix_.*&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;sales&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This differs from the lenient &lt;code&gt;SELECT *&lt;/code&gt; behavior. The upside: &lt;strong&gt;upstream renames blow up immediately instead of silently dropping columns&lt;/strong&gt; — the antidote to the &amp;ldquo;silent data incident&amp;rdquo; from section 1.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. Alias collisions.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;SELECT COLUMNS('.*_amount') AS 'x' FROM sales&lt;/code&gt; produces 3 columns all named &lt;code&gt;x&lt;/code&gt;. DuckDB permits this, but downstream &lt;code&gt;df['x']&lt;/code&gt; only gets the first. Either guarantee uniqueness with &lt;code&gt;\0&lt;/code&gt;/&lt;code&gt;\1&lt;/code&gt;, or UNPIVOT afterward.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4. &lt;code&gt;COLUMNS()&lt;/code&gt; cannot be used inside &lt;code&gt;* REPLACE&lt;/code&gt; / &lt;code&gt;* RENAME&lt;/code&gt;.&lt;/strong&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;-- Error: STAR expression is not supported here
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;REPLACE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;COLUMNS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;.*_amount&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;q1_amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;sales&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The right side of &lt;code&gt;REPLACE&lt;/code&gt; must be a concrete expression, not another star expansion.&lt;/p&gt;
&lt;h2 id="7-zero-cost-capture-groups-dont-affect-the-plan"&gt;7. Zero cost: capture groups don&amp;rsquo;t affect the plan
&lt;/h2&gt;&lt;p&gt;Does renaming introduce an extra projection or a performance penalty? On a 2M-row table, &lt;code&gt;SUM(COLUMNS('m[0-9]'))&lt;/code&gt; vs hand-written &lt;code&gt;SUM(m1) + SUM(m2)&lt;/code&gt; both land in the ~10 ms range (measured 9 ms vs 7 ms locally; the gap is noise). The stricter check is comparing &lt;code&gt;EXPLAIN&lt;/code&gt; output: &lt;strong&gt;the physical plans are byte-identical&lt;/strong&gt; (verified with &lt;code&gt;md5sum&lt;/code&gt;) — capture groups are resolved in the binder phase, so by the time the optimizer sees them they&amp;rsquo;re ordinary column references.&lt;/p&gt;
&lt;p&gt;In other words: &lt;strong&gt;renaming with &lt;code&gt;COLUMNS()&lt;/code&gt; + capture groups costs a one-time SQL parse, not a per-row runtime cost.&lt;/strong&gt; (The &lt;a class="link" href="https://duckdblab.org/en/post/duckdb-columns-expression-star-modifiers/" &gt;predecessor guide&lt;/a&gt; Section 5 runs the same zero-overhead check on a 200k × 50-column table — this echoes it.)&lt;/p&gt;
&lt;h2 id="8-when-to-use-it--when-not-to"&gt;8. When to use it / when not to
&lt;/h2&gt;&lt;table&gt;
 &lt;thead&gt;
 &lt;tr&gt;
 &lt;th&gt;Scenario&lt;/th&gt;
 &lt;th&gt;Recommended&lt;/th&gt;
 &lt;th&gt;Why&lt;/th&gt;
 &lt;/tr&gt;
 &lt;/thead&gt;
 &lt;tbody&gt;
 &lt;tr&gt;
 &lt;td&gt;Batch-rename wide-table columns by pattern (homogeneous)&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;COLUMNS('regex') AS '\1_suffix'&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;One line replaces dozens&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Batch aggregation with readable names&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;min(COLUMNS('regex')) AS 'lo_\1'&lt;/code&gt;&lt;/td&gt;
 &lt;td&gt;Avoids duplicate names&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Arithmetic between column pairs&lt;/td&gt;
 &lt;td&gt;UNPIVOT then JOIN&lt;/td&gt;
 &lt;td&gt;Multiple &lt;code&gt;COLUMNS()&lt;/code&gt; unsupported&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Few columns (&amp;lt; 5)&lt;/td&gt;
 &lt;td&gt;Write it out&lt;/td&gt;
 &lt;td&gt;Regex is less readable&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Column names need case changes&lt;/td&gt;
 &lt;td&gt;lambda select + outer alias / DDL&lt;/td&gt;
 &lt;td&gt;Capture groups only concatenate&lt;/td&gt;
 &lt;/tr&gt;
 &lt;tr&gt;
 &lt;td&gt;Column may be missing and must pass silently&lt;/td&gt;
 &lt;td&gt;&lt;code&gt;SELECT *&lt;/code&gt; or lambda&lt;/td&gt;
 &lt;td&gt;COLUMNS errors on no match&lt;/td&gt;
 &lt;/tr&gt;
 &lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id="keep-going"&gt;Keep going
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;a class="link" href="https://duckdblab.org/post/duckdb-columns-expression-star-modifiers/" &gt;DuckDB COLUMNS() and Star Modifiers: The Complete Guide&lt;/a&gt; — EXCLUDE / REPLACE / RENAME / UNPIVOT fundamentals&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://duckdblab.org/post/duckdb-unpivot-grouping-sets/" &gt;DuckDB UNPIVOT and GROUPING SETS&lt;/a&gt; — the right way to go wide-to-long&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://duckdblab.org/post/duckdb-complete-guide/" &gt;The Complete DuckDB Guide&lt;/a&gt; — roadmap from install to advanced&lt;/li&gt;
&lt;li&gt;&lt;a class="link" href="https://duckdblab.org/post/duckdb-list-functions-complete-guide/" &gt;DuckDB List Functions: The Complete Guide&lt;/a&gt; — pairing with capture groups for nested structures&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="reproducible-verification"&gt;Reproducible verification
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Version&lt;/strong&gt;: DuckDB 1.5.2 (Variegata), Linux x86_64, executed directly in the CLI.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Note&lt;/strong&gt;: &lt;code&gt;\1&lt;/code&gt; backreferences require real single backslashes in the file. Passing SQL via &lt;code&gt;duckdb -c &amp;quot;...\1...&amp;quot;&lt;/code&gt; lets the shell swallow the backslashes — write the SQL into a &lt;code&gt;.sql&lt;/code&gt; file and run &lt;code&gt;duckdb &amp;lt; file.sql&lt;/code&gt;. Every example in this article was verified that way.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Official references&lt;/strong&gt;: &lt;a class="link" href="https://duckdb.org/docs/2.0/sql/expressions/star.html" target="_blank" rel="noopener"
 &gt;Star Expression (including COLUMNS regex and capture groups)&lt;/a&gt;, &lt;a class="link" href="https://duckdb.org/docs/stable/sql/functions/lambda" target="_blank" rel="noopener"
 &gt;Lambda Functions&lt;/a&gt;, &lt;a class="link" href="https://duckdb.org/docs/stable/sql/query_syntax/groupby" target="_blank" rel="noopener"
 &gt;GROUP BY ALL&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Article info&lt;/strong&gt;: last verified 2026-11-12. Found an issue? File a &lt;a class="link" href="https://github.com/pengzz9527/duckdb-blog/issues" target="_blank" rel="noopener"
 &gt;GitHub Issue&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;</description></item></channel></rss>