<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged dataengineering</title>
<link>https://coderlegion.com/tag/dataengineering</link>
<description>Powered by Question2Answer</description>
<item>
<title>DuckDB 2.0 vs Polars vs Spark on One Machine: 150M Rows Under a 4.5 GiB Cap</title>
<link>https://coderlegion.com/32051/duckdb-2-0-vs-polars-vs-spark-on-one-machine-150m-rows-under-a-4-5-gib-cap</link>
<description>&lt;p&gt;Originally published on DEV.to: &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/firfircelik/duckdb-20-vs-polars-vs-spark-on-one-machine-150m-rows-under-a-45-gib-cap-h38&quot;&gt;https://dev.to/firfircelik/duckdb-20-vs-polars-vs-spark-on-one-machine-150m-rows-under-a-45-gib-cap-h38&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;DuckDB 2.0.0 shipped on October 21, 2026 (&lt;a rel=&quot;nofollow&quot; href=&quot;https://duckdb.org/2026/10/21/announcing-duckdb-200&quot;&gt;announcement&lt;/a&gt;, &lt;a rel=&quot;nofollow&quot; href=&quot;https://pypi.org/project/duckdb/2.0.0/&quot;&gt;PyPI&lt;/a&gt;). The next morning I ran the same single-machine benchmark I had already run on DuckDB 1.5.6: three engines, one 8 vCPU VM, Parquet in and Parquet out, a hard memory cap, and four queries that look like real data engineering work.&lt;/p&gt;
&lt;p&gt;The thesis I started with, and that the numbers support:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;DuckDB beats Spark-on-one-box on memory-bound DE work.&lt;/strong&gt; It finished every run, at every size, 5 to 12 times faster than local PySpark at 150M rows, and it still finished the heavy queries with a 1 GB memory limit.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Polars hits a wall on window dedup.&lt;/strong&gt; At 150M rows the &lt;code&gt;ROW_NUMBER()&lt;/code&gt; style &quot;latest row per customer&quot; query never finished under the cap. Not at 4.5 GiB, not with an out-of-core budget, not at 9 GiB.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Polars is not slow.&lt;/strong&gt; On filters, joins and a high-cardinality group by it ties DuckDB or beats it, when it fits.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Everything below is measured on that one box. Where I interpret or give an opinion, I say so.&lt;/p&gt;
&lt;h3 id=&quot;the-contenders&quot;&gt;The contenders&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;&lt;/th&gt;&lt;th&gt;DuckDB 2.0.0 &lt;/th&gt;&lt;th&gt;Polars 2.0.0 &lt;/th&gt;&lt;th&gt;PySpark 4.2.0&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Model &lt;/td&gt;&lt;td&gt;In-process SQL engine &lt;/td&gt;&lt;td&gt;In-process DataFrame engine, lazy + streaming &lt;/td&gt;&lt;td&gt;JVM engine, &lt;code&gt;local[8]&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;How I drove it &lt;/td&gt;&lt;td&gt;&lt;code&gt;COPY (SQL) TO 'out.parquet'&lt;/code&gt; &lt;/td&gt;&lt;td&gt;&lt;code&gt;LazyFrame.sink_parquet()&lt;/code&gt; &lt;/td&gt;&lt;td&gt;&lt;code&gt;spark.sql(...).write.parquet()&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Memory knob &lt;/td&gt;&lt;td&gt;&lt;code&gt;SET memory_limit = '3GB'&lt;/code&gt; &lt;/td&gt;&lt;td&gt;default (plus one run with an out-of-core budget) &lt;/td&gt;&lt;td&gt;&lt;code&gt;spark.driver.memory = 3g&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Threads &lt;/td&gt;&lt;td&gt;8 &lt;/td&gt;&lt;td&gt;8 (&lt;code&gt;POLARS_MAX_THREADS&lt;/code&gt;) &lt;/td&gt;&lt;td&gt;8&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Polars 2.0.0 was published on PyPI on &lt;a rel=&quot;nofollow&quot; href=&quot;https://pypi.org/project/polars/2.0.0/&quot;&gt;October 6, 2026&lt;/a&gt;, PySpark 4.2.0 on &lt;a rel=&quot;nofollow&quot; href=&quot;https://pypi.org/project/pyspark/4.2.0/&quot;&gt;July 14, 2026&lt;/a&gt;. DuckDB 2.0.0 wheels landed on PyPI on October 21, the same day as the &lt;a rel=&quot;nofollow&quot; href=&quot;https://duckdb.org/release_calendar&quot;&gt;release calendar&lt;/a&gt; entry.&lt;/p&gt;
&lt;h3 id=&quot;setup&quot;&gt;Setup&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Hardware and versions&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;VM: 8 vCPU Intel Xeon, 15 GiB RAM, no swap, Linux 6.12. It is a shared box, so other processes were running. Every run waited until at least 5 GiB was free and CPU busy was under 30% over a 3 second window.&lt;/li&gt;
&lt;li&gt;Python 3.13.5, OpenJDK 21.0.12, pyarrow 26.0.0.&lt;/li&gt;
&lt;li&gt;DuckDB 2.0.0, Polars 2.0.0, PySpark 4.2.0. The &quot;before&quot; numbers are DuckDB 1.5.6 on the same box and data, run on October 19.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Data.&lt;/strong&gt; A seeded synthetic star schema written with pyarrow: a &lt;code&gt;sales&lt;/code&gt; fact table and a 50,000-row &lt;code&gt;products&lt;/code&gt; dimension. Three sizes: 10M rows (213 MB), 50M (1.1 GB) and 150M (3.2 GB of snappy Parquet, 30 files of 5M rows). About 10 orders per customer, so 150M rows means roughly 15M customers.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Queries.&lt;/strong&gt; The same SQL for DuckDB and Spark, the same logic as Polars expressions:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;q1_filter_agg&lt;/code&gt;: filter on a timestamp and status, group by store and status.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;q2_join_agg&lt;/code&gt;: fact-to-dimension join, group by category and brand.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;q3_window_latest&lt;/code&gt;: latest order per customer with &lt;code&gt;ROW_NUMBER()&lt;/code&gt;. The classic dedup and SCD pattern.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;q4_highcard_agg&lt;/code&gt;: one row per customer (about 15M groups at 150M).&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- q3_window_latest
SELECT customer_id, order_id, ts, qty * price AS amount
FROM (
    SELECT *, ROW_NUMBER() OVER (PARTITION BY customer_id
                                 ORDER BY ts DESC, order_id DESC) AS rn
    FROM sales
) t
WHERE rn = 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Method.&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Every run is a fresh process: interpreter start, import, session, read Parquet, compute, write Parquet. So no warm caches inside the engine, and Spark pays its JVM start every time.&lt;/li&gt;
&lt;li&gt;Engines run strictly one at a time. The engine order rotates per repetition so no engine always goes first.&lt;/li&gt;
&lt;li&gt;5 repetitions at 10M, 3 at 50M and 150M. I report &lt;strong&gt;medians&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;A watchdog samples the RSS of the whole process tree every 50 ms and kills the run if it goes above &lt;strong&gt;4.5 GiB&lt;/strong&gt;. That is the cap. It is deliberately tighter than the box, because &quot;fits in a small container&quot; is the question I care about.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Wall&lt;/strong&gt; is the whole process. &lt;strong&gt;Query&lt;/strong&gt; is read, compute and write only. The difference is setup.&lt;/li&gt;
&lt;li&gt;Correctness: a validation script compares row counts and rounded checksums of every output. Where an engine produced output, all three engines agreed, for every query and size.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;results-at-150m-rows&quot;&gt;Results at 150M rows&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Query &lt;/th&gt;&lt;th&gt;DuckDB 2.0.0 &lt;/th&gt;&lt;th&gt;Polars 2.0.0 &lt;/th&gt;&lt;th&gt;PySpark 4.2.0&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Filter + aggregate &lt;/td&gt;&lt;td&gt;&lt;strong&gt;0.87 s&lt;/strong&gt; (3/3) &lt;/td&gt;&lt;td&gt;0.96 s (3/3) &lt;/td&gt;&lt;td&gt;10.77 s (3/3)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Join + aggregate &lt;/td&gt;&lt;td&gt;1.47 s (3/3) &lt;/td&gt;&lt;td&gt;&lt;strong&gt;1.46 s&lt;/strong&gt; (3/3) &lt;/td&gt;&lt;td&gt;16.10 s (3/3)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Window dedup &lt;/td&gt;&lt;td&gt;&lt;strong&gt;7.92 s&lt;/strong&gt; (3/3) &lt;/td&gt;&lt;td&gt;killed (0/3) &lt;/td&gt;&lt;td&gt;46.29 s (2/3)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;High-cardinality group by &lt;/td&gt;&lt;td&gt;7.91 s (3/3) &lt;/td&gt;&lt;td&gt;&lt;strong&gt;4.21 s&lt;/strong&gt; (1/3) &lt;/td&gt;&lt;td&gt;39.70 s (3/3)&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Median wall seconds, runs that finished under the 4.5 GiB cap in brackets.&lt;/p&gt;
&lt;p&gt;Peak RSS (median of finished runs) tells the other half:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Query &lt;/th&gt;&lt;th&gt;DuckDB 2.0.0 &lt;/th&gt;&lt;th&gt;Polars 2.0.0 &lt;/th&gt;&lt;th&gt;PySpark 4.2.0&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Filter + aggregate &lt;/td&gt;&lt;td&gt;0.77 GiB &lt;/td&gt;&lt;td&gt;0.94 GiB &lt;/td&gt;&lt;td&gt;1.85 GiB&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Join + aggregate &lt;/td&gt;&lt;td&gt;0.42 GiB &lt;/td&gt;&lt;td&gt;1.96 GiB &lt;/td&gt;&lt;td&gt;1.61 GiB&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Window dedup &lt;/td&gt;&lt;td&gt;3.01 GiB &lt;/td&gt;&lt;td&gt;above 4.5 (killed) &lt;/td&gt;&lt;td&gt;3.74 GiB, one run hit 7.51&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;High-cardinality group by &lt;/td&gt;&lt;td&gt;3.01 GiB &lt;/td&gt;&lt;td&gt;4.49 GiB &lt;/td&gt;&lt;td&gt;4.40 GiB&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Three things stand out.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Spark on one box pays twice.&lt;/strong&gt; About 5 seconds of every Spark run is setup (median 5.0 to 5.8 s, JVM plus session) before it reads a byte. Then the query itself is slower: query-only time was 7.4x DuckDB's on the filter, 7.9x on the join, 5.2x on the window dedup and 4.3x on the group by. In wall time that is 5x to 12x. And &lt;code&gt;spark.driver.memory = 3g&lt;/code&gt; did not keep the process at 3 GiB: one window dedup run reached 7.51 GiB of process-tree RSS and was killed.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Polars is fast right up to the cap.&lt;/strong&gt; It tied DuckDB on filter and join and was the fastest engine on the high-cardinality group by (4.21 s against 7.91 s). But only 1 of 3 runs finished: that one peaked at 4.49 GiB, and the other two crossed the cap at 4.50 and 4.55 GiB. Fast and close to the edge is a fine place for a notebook and a bad place for a scheduled job.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The window dedup is where Polars stops.&lt;/strong&gt; At 150M rows it was killed in all 3 runs, within a few seconds of starting, at 4.52 to 4.66 GiB.&lt;/p&gt;
&lt;h3 id=&quot;polars-and-the-window-wall&quot;&gt;Polars and the window wall&lt;/h3&gt;
&lt;p&gt;I gave Polars two more chances on the 150M window dedup:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Polars 2.0.0, 150M window dedup &lt;/th&gt;&lt;th&gt;Result&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Default settings, 4.5 GiB cap (Oct 22) &lt;/td&gt;&lt;td&gt;0/3, killed at 4.52 to 4.66 GiB&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;POLARS_OOC_MEMORY_BUDGET_MB=3072&lt;/code&gt;, 4.5 GiB cap (Oct 22) &lt;/td&gt;&lt;td&gt;0/3, killed at 4.57 to 4.66 GiB&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Default settings, &lt;strong&gt;9 GiB&lt;/strong&gt; cap (Oct 19, same Polars 2.0.0) &lt;/td&gt;&lt;td&gt;0/3, killed at 9.04 to 9.54 GiB&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The out-of-core budget variable is one I found in the Polars 2.0.0 source, not a documented knob, so treat that row as &quot;I tried&quot;, not as guidance. The same variable did help the group by: with it, Polars finished 3/3 at 4.67 s and 4.18 to 4.30 GiB.&lt;/p&gt;
&lt;p&gt;At smaller sizes the dedup works, but the shape is already visible:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Window dedup &lt;/th&gt;&lt;th&gt;DuckDB 2.0.0 &lt;/th&gt;&lt;th&gt;Polars 2.0.0 &lt;/th&gt;&lt;th&gt;PySpark 4.2.0&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;10M rows &lt;/td&gt;&lt;td&gt;0.60 s, 0.65 GiB &lt;/td&gt;&lt;td&gt;1.83 s, 1.20 GiB &lt;/td&gt;&lt;td&gt;12.74 s, 2.94 GiB&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;50M rows &lt;/td&gt;&lt;td&gt;2.33 s, 2.19 GiB &lt;/td&gt;&lt;td&gt;10.36 s, 4.46 GiB &lt;/td&gt;&lt;td&gt;29.06 s, 3.77 GiB&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;150M rows &lt;/td&gt;&lt;td&gt;7.92 s, 3.01 GiB &lt;/td&gt;&lt;td&gt;killed &lt;/td&gt;&lt;td&gt;46.29 s, 3.74 GiB&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;At 50M Polars already used 4.46 GiB for a query DuckDB did in 2.19 GiB, and took 4.4x longer.&lt;/p&gt;
&lt;p&gt;This is the Polars expression I used. It is the direct translation of the SQL:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import polars as pl

amount = pl.col(&quot;qty&quot;).cast(pl.Float64) * pl.col(&quot;price&quot;)
rn = pl.int_range(pl.len()).over(
    &quot;customer_id&quot;, order_by=[&quot;ts&quot;, &quot;order_id&quot;], descending=True
)
(
    pl.scan_parquet(&quot;data/sales_150m/*.parquet&quot;)
    .filter(rn == 0)
    .select(&quot;customer_id&quot;, &quot;order_id&quot;, &quot;ts&quot;, amount.alias(&quot;amount&quot;))
    .sink_parquet(&quot;out/polars/150m_q3_window_latest.parquet&quot;)
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Fair caveat: I did not test other Polars formulations of the same dedup (a sort followed by &lt;code&gt;unique(keep=&quot;first&quot;)&lt;/code&gt;, or a &lt;code&gt;group_by().agg(...)&lt;/code&gt; with &lt;code&gt;sort_by().last()&lt;/code&gt;). One of them may stream better. The point stands for the common case, though: the first thing a team writes is the window, and that one did not fit.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Update (Oct 22):&lt;/strong&gt; after this went out, Victor Cid (&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Vleyked&quot;&gt;@Vleyked&lt;/a&gt;) tested exactly that in &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/firfircelik/engine-bench/pull/1&quot;&gt;engine-bench PR #1&lt;/a&gt;. His &lt;code&gt;sort&lt;/code&gt; + &lt;code&gt;unique(keep=&quot;first&quot;)&lt;/code&gt; version drops the in-memory window fallback from the plan and finished 3/3 at 150M rows under the same 4.5 GiB cap, peaking at 2.50 to 2.74 GiB, but only with &lt;code&gt;POLARS_OOC_MEMORY_BUDGET_MB=1024&lt;/code&gt;. With default settings or a 3072 MB budget it still went over the cap. The price is speed: a 90.58 s median against 17.56 s for DuckDB on the same 4 vCPU runner, so roughly 5x slower. His output matched DuckDB row for row (14,999,347 rows). So Polars can do this dedup under the cap if you rewrite it and tune the spill budget, and with a smarter query it gets close to DuckDB (see below). His runs were on different hardware, so they are not in the tables above.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Second update:&lt;/strong&gt; in &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/firfircelik/engine-bench/pull/2&quot;&gt;engine-bench PR #2&lt;/a&gt; he filtered each customer down to their max timestamp first and only sorted those rows to break ties. That brought the median down to 14.23 s at 150M rows with default Polars settings, peaking at 4.04 GiB under the same cap, with output matching DuckDB row for row. On his runner DuckDB's reference was a single 18.50 s run, so I'm not calling that a repeated cross-engine win. The real lesson is that the first way you write a Polars dedup can decide whether it finishes at all.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Third update (Oct 23):&lt;/strong&gt; Ritchie Vink, the creator of Polars, pointed out that only DuckDB was told about the memory limit, so Polars did not know it had to stay under 4 GiB, and suggested setting POLARS_OOC_MEMORY_BUDGET_MB for a fairer comparison. I reran every Polars query at 150M rows on the same box under the same 4.5 GiB cap with that budget, next to a DuckDB rerun in the same session: the high-cardinality group by finished 3/3 at 6.17 s and 3.46 GiB with a 3072 MB budget, against 9.56 s for DuckDB (the box was busier than on Oct 22 and DuckDB ran slower too, so compare within this session, not with the tables above; the Oct 22 run with the same budget gave 4.67 s). The window dedup with over(order_by) still went over the cap in every run at 3072, 1024 and 512 MB, Victor's sort plus unique version fit only at 1024 MB (31.28 s, 2.29 GiB), and the max-timestamp rewrite finished 3/3 on default settings (5.61 s, 4.02 GiB). Ritchie says Polars main now spills the group by correctly and that this ships in the next release, which was not on PyPI yet when I ran this, so I will rerun when it ships. Raw results and output checks are in &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/firfircelik/engine-bench/pull/3&quot;&gt;engine-bench PR #3&lt;/a&gt; and in results/polars_ooc_budget_2026-10-23 on the polars-ooc-budget-2026-10-23 branch.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Fourth update (Oct 26):&lt;/strong&gt; after the third update I reported the window dedup to Polars as &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/pola-rs/polars/issues/30270&quot;&gt;pola-rs/polars#30270&lt;/a&gt;: the over(order_by) window inside filter() ignored POLARS_OOC_MEMORY_BUDGET_MB and ran in an in-memory fallback node. Ritchie Vink fixed it (#30272, #30282 and #30280), and the fixes shipped in Polars 2.1.0. I reran the 150M benchmark on the official 2.1.0 wheel on the same box, with the same harness and the same 4.5 GiB cap, next to a DuckDB rerun in the same session. With a budget set, the window dedup now finishes 3/3 at every budget I tried: 18.78 s and 3.34 GiB at 3072 MB, 20.11 s and 1.29 GiB at 1024 MB, 43.01 s and 2.15 GiB at 512 MB. On 2.0.0 it failed at all three. Memory now follows the budget closely: at 512 MB, Victor's sort plus unique version peaked at 0.69 GiB (73.85 s) and the high-cardinality group by at 0.74 GiB (9.43 s). DuckDB still ran the window dedup faster in the same session, 8.07 s, about 2.3x quicker than Polars at 3072 MB, and the max-timestamp rewrite remains the fastest Polars dedup (5.12 s on default settings). Every finished output matched DuckDB row for row (14,999,347 rows). Two caveats. Without a budget, the window dedup and sort plus unique still went over the cap. My cap is a process watchdog, not a cgroup limit, so the new cgroup-aware default in 2.1.0 had nothing to detect and is untested here; set the budget explicitly. One sort plus unique run at 3072 MB took 285.64 s for no clear reason, so I report medians of 3 runs. Raw results, plans and output checks: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/firfircelik/engine-bench/pull/4&quot;&gt;engine-bench PR #4&lt;/a&gt; and the &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/firfircelik/engine-bench/blob/polars-main-window-fix-2026-10-24/results/polars_2.1.0_2026-10-25/README.md&quot;&gt;2.1.0 results README&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;duckdb-under-pressure-memory-limit-1-gb&quot;&gt;DuckDB under pressure: memory_limit = 1 GB&lt;/h3&gt;
&lt;p&gt;The interesting DuckDB question is not &quot;how fast with room to spare&quot; but &quot;what happens when the box is small&quot;. Same 150M rows, &lt;code&gt;memory_limit&lt;/code&gt; cut to 1 GB:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;150M, &lt;code&gt;memory_limit = 1GB&lt;/code&gt; &lt;/th&gt;&lt;th&gt;Wall (median) &lt;/th&gt;&lt;th&gt;Peak RSS &lt;/th&gt;&lt;th&gt;Finished&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Window dedup &lt;/td&gt;&lt;td&gt;14.97 s &lt;/td&gt;&lt;td&gt;1.12 GiB &lt;/td&gt;&lt;td&gt;3/3&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;High-cardinality group by &lt;/td&gt;&lt;td&gt;13.94 s &lt;/td&gt;&lt;td&gt;1.08 GiB &lt;/td&gt;&lt;td&gt;3/3&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Window dedup, &lt;code&gt;preserve_insertion_order = false&lt;/code&gt; &lt;/td&gt;&lt;td&gt;14.94 s &lt;/td&gt;&lt;td&gt;1.12 GiB &lt;/td&gt;&lt;td&gt;3/3&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Group by, &lt;code&gt;preserve_insertion_order = false&lt;/code&gt; &lt;/td&gt;&lt;td&gt;13.88 s &lt;/td&gt;&lt;td&gt;1.08 GiB &lt;/td&gt;&lt;td&gt;3/3&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;With a third of the memory, DuckDB spilled to disk and still beat Spark's unconstrained run by about 3x on both queries, at a quarter to a third of Spark's peak RSS. The DuckDB docs list window functions with &lt;code&gt;PARTITION BY ... ORDER BY&lt;/code&gt; among the blocking operators it can process larger than memory (&lt;a rel=&quot;nofollow&quot; href=&quot;https://duckdb.org/docs/current/guides/performance/how_to_tune_workloads&quot;&gt;How to Tune Workloads&lt;/a&gt;). This run is that sentence, measured.&lt;/p&gt;
&lt;p&gt;The setup is three lines:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import duckdb

con = duckdb.connect()
con.execute(&quot;SET memory_limit = '1GB'&quot;)
con.execute(&quot;SET threads = 8&quot;)
con.execute(&quot;SET temp_directory = '/mnt/fast-local-disk/duckdb_tmp'&quot;)  # where spills go
con.execute(&quot;&quot;&quot;
    COPY (
        SELECT customer_id, order_id, ts, qty * price AS amount
        FROM (
            SELECT *, ROW_NUMBER() OVER (PARTITION BY customer_id
                                         ORDER BY ts DESC, order_id DESC) AS rn
            FROM read_parquet('data/sales_150m/*.parquet')
        )
        WHERE rn = 1
    ) TO 'out/latest_per_customer.parquet' (FORMAT parquet)
&quot;&quot;&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;duckdb-1-5-6-vs-2-0-0&quot;&gt;DuckDB 1.5.6 vs 2.0.0&lt;/h3&gt;
&lt;p&gt;Same box, same data, same scripts. 1.5.6 ran on October 19, 2.0.0 on October 22. Median wall seconds at 150M:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Setting &lt;/th&gt;&lt;th&gt;Query &lt;/th&gt;&lt;th&gt;1.5.6 &lt;/th&gt;&lt;th&gt;2.0.0 &lt;/th&gt;&lt;th&gt;Change&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;memory_limit = 3GB&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Window dedup &lt;/td&gt;&lt;td&gt;9.09 &lt;/td&gt;&lt;td&gt;7.92 &lt;/td&gt;&lt;td&gt;-13%&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;memory_limit = 3GB&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Group by &lt;/td&gt;&lt;td&gt;9.61 &lt;/td&gt;&lt;td&gt;7.91 &lt;/td&gt;&lt;td&gt;-18%&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;memory_limit = 1GB&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Window dedup &lt;/td&gt;&lt;td&gt;17.79 &lt;/td&gt;&lt;td&gt;14.97 &lt;/td&gt;&lt;td&gt;-16%&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;memory_limit = 1GB&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Group by &lt;/td&gt;&lt;td&gt;22.06 &lt;/td&gt;&lt;td&gt;13.94 &lt;/td&gt;&lt;td&gt;-37%&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;1GB, no insertion order &lt;/td&gt;&lt;td&gt;Window dedup &lt;/td&gt;&lt;td&gt;14.85 &lt;/td&gt;&lt;td&gt;14.94 &lt;/td&gt;&lt;td&gt;+1%&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;1GB, no insertion order &lt;/td&gt;&lt;td&gt;Group by &lt;/td&gt;&lt;td&gt;15.15 &lt;/td&gt;&lt;td&gt;13.88 &lt;/td&gt;&lt;td&gt;-8%&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The useful finding is the last four rows together. On 1.5.6, &lt;code&gt;SET preserve_insertion_order = false&lt;/code&gt; was worth 16% on the dedup and 31% on the group by under a 1 GB limit. On 2.0.0 the default already lands where the tuned setting did, and the flag changes almost nothing for these two queries. If you carry that setting in job configs as a performance trick, re-measure after upgrading. It still matters for what it was designed for: the docs recommend it when large imports or exports run out of memory, and it lets DuckDB reorder rows without an &lt;code&gt;ORDER BY&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Not everything improved. The 50M window dedup was slower on 2.0.0 (1.94 s to 2.33 s), and peak RSS for the light queries went up (150M filter: 0.26 to 0.77 GiB, join: 0.23 to 0.42 GiB), while the heavy queries went down (window dedup: 3.47 to 3.01 GiB). I have not investigated why. The two runs were also three days apart on a shared box, so treat differences under about 10 to 15% as noise. The 1 GB group by drop is far outside that.&lt;/p&gt;
&lt;p&gt;The release itself is mostly a feature release: Quack and &lt;code&gt;CONNECT&lt;/code&gt; for client-server use, asynchronous I/O, a first-class &lt;code&gt;VARIANT&lt;/code&gt;, triggers, a PEG parser by default (&lt;a rel=&quot;nofollow&quot; href=&quot;https://duckdb.org/2026/10/21/announcing-duckdb-200&quot;&gt;announcement&lt;/a&gt;). It is an LTS line supported until at least October 2027. One migration note matters for pipelines: database files written by 2.0 cannot be read by versions before 2.0. My benchmark only reads and writes Parquet, so it never touched that.&lt;/p&gt;
&lt;h3 id=&quot;where-spark-still-wins&quot;&gt;Where Spark still wins&lt;/h3&gt;
&lt;p&gt;None of this says &quot;replace Spark&quot;. It says Spark on one box is the wrong comparison for small and medium jobs. Spark still wins when:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The data does not fit one node's disk and memory, or the deadline does not fit one node's cores.&lt;/strong&gt; This test stops at 3.2 GB of Parquet on purpose. DuckDB spills well, but one machine is still one machine.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The platform is built around it.&lt;/strong&gt; On Databricks and Fabric, Spark is where Unity Catalog or OneLake governance, Delta writes with concurrent writers, lineage, job orchestration and streaming already live. A DuckDB step outside that boundary is a step you have to secure, monitor and explain.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The cluster is already running.&lt;/strong&gt; The 5 second JVM start I measured per run disappears in a warm interactive cluster or a long job. The query-only gap (4x to 8x here) does not.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The team is a Spark team.&lt;/strong&gt; One engine everyone can debug at 02:00 is worth more than a faster engine one person understands.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;My opinion as a Lead DE on Azure, Databricks and Fabric: the line is not the engine, it is the size of the job. Most jobs I see are tens of millions of rows dressed up as big data and started on a cluster. For those (file-to-file transforms before landing, validation and reconciliation checks, small dimension builds, CI tests for SQL logic) a single-node engine with a memory limit is the cheaper and more predictable choice. Anything that writes governed Delta tables stays on the platform.&lt;/p&gt;
&lt;h3 id=&quot;failure-modes-i-hit-so-you-don-t-have-to&quot;&gt;Failure modes I hit, so you don't have to&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Engine memory settings are not process memory.&lt;/strong&gt; Spark's 3g driver heap still produced a 7.51 GiB process. DuckDB's whole process stayed close to its &lt;code&gt;memory_limit&lt;/code&gt;: about 3.0 GiB peak at a 3 GB limit, 1.1 GiB at 1 GB. Size containers on measured RSS, not on the setting.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&quot;Streaming&quot; does not mean bounded.&lt;/strong&gt; Polars streamed the filter, join and group by fine. The window dedup with an &lt;code&gt;over(order_by=...)&lt;/code&gt; did not stay bounded, even with 9 GiB.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Close to the cap is a failure that hasn't happened yet.&lt;/strong&gt; Polars' group by finished in 1 of 3 runs, peaking at 4.49 GiB against a 4.5 GiB cap. Same query, same data, same version. Schedule that and it fails some nights.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Shared hosts lie.&lt;/strong&gt; One Spark run at 10M was killed because free memory on the whole box fell under my 1 GiB safety floor while Spark itself was at 1.6 GiB. That was the neighbours, not Spark. Gate each run on free memory and CPU, and record both.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Validate outputs, not just timings.&lt;/strong&gt; Every engine produced the same row counts and checksums where it finished. Without that check, a fast wrong answer looks like a win.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;reproduce-it&quot;&gt;Reproduce it&lt;/h3&gt;
&lt;p&gt;Everything is public at &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/firfircelik/engine-bench&quot;&gt;firfircelik/engine-bench&lt;/a&gt;: scripts, raw results, pinned versions and the query plans. Here are the parts that matter. The data generator is seeded (&lt;code&gt;SEED = 20261019&lt;/code&gt;) and writes 5M-row chunks:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;rng = np.random.default_rng([SEED, chunk_index])
t = pa.table({
    &quot;order_id&quot;: np.arange(lo, lo + n, dtype=np.int64),
    &quot;ts&quot;: pa.array(start + rng.integers(0, 270 * 24 * 3600, n), pa.timestamp(&quot;s&quot;)),
    &quot;customer_id&quot;: rng.integers(0, rows // 10, n, dtype=np.int32),
    &quot;product_id&quot;: rng.integers(0, 50_000, n, dtype=np.int32),
    &quot;store_id&quot;: rng.integers(0, 500, n, dtype=np.int16),
    &quot;qty&quot;: rng.integers(1, 10, n, dtype=np.int16),
    &quot;price&quot;: np.round(rng.uniform(1.0, 500.0, n), 2),
    &quot;status&quot;: rng.choice(STATUSES, n, p=[0.55, 0.20, 0.12, 0.08, 0.05]),
})
pq.write_table(t, f&quot;sales_{rows // 1_000_000}m/part-{chunk_index:04d}.parquet&quot;,
               row_group_size=1_000_000, compression=&quot;snappy&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Spark session, the only non-default settings:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;spark = (SparkSession.builder.master(&quot;local[8]&quot;)
         .config(&quot;spark.driver.memory&quot;, &quot;3g&quot;)
         .config(&quot;spark.local.dir&quot;, TMP)
         .config(&quot;spark.ui.enabled&quot;, &quot;false&quot;)
         .config(&quot;spark.sql.session.timeZone&quot;, &quot;UTC&quot;)
         .getOrCreate())
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And the watchdog, which is the part I would copy into any benchmark of your own. It measures the whole process tree, because JVMs and thread pools do not show up in the parent's RSS:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import psutil, subprocess, time

GIB = 1024 ** 3

def tree_rss(p):
    total = 0
    for q in [p] + p.children(recursive=True):
        try:
            total += q.memory_info().rss
        except psutil.NoSuchProcess:
            pass
    return total

proc = subprocess.Popen(cmd)
p, peak = psutil.Process(proc.pid), 0
while proc.poll() is None:
    rss = tree_rss(p)
    peak = max(peak, rss)
    if rss &amp;gt; 4.5 * GIB:
        for q in p.children(recursive=True) + [p]:
            q.kill()
        break
    time.sleep(0.05)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;the-takeaway&quot;&gt;The takeaway&lt;/h3&gt;
&lt;p&gt;On one 8 vCPU machine with a 4.5 GiB cap, DuckDB 2.0.0 was the only engine that finished every run of every query at every size, and it did the heavy ones with 1 GB. Polars matched or beat it on the light queries and the group by, and could not finish the window dedup at 150M even with 9 GiB. Local Spark finished almost everything, 5x to 12x slower, and its memory setting did not bound its memory.&lt;/p&gt;
&lt;p&gt;If your job fits on one machine, measure it on one machine before you start a cluster for it.&lt;/p&gt;
&lt;h3 id=&quot;sources&quot;&gt;Sources&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://duckdb.org/2026/10/21/announcing-duckdb-200&quot;&gt;Announcing DuckDB 2.0.0 LTS &quot;Cinnamon Teal&quot;&lt;/a&gt;, DuckDB team, October 21, 2026&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://duckdb.org/release_calendar&quot;&gt;DuckDB release calendar&lt;/a&gt; (2.0.0 LTS on 2026-10-21, 1.5.6 on 2026-09-28)&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://pypi.org/project/duckdb/2.0.0/&quot;&gt;duckdb 2.0.0 on PyPI&lt;/a&gt;, &lt;a rel=&quot;nofollow&quot; href=&quot;https://pypi.org/project/polars/2.0.0/&quot;&gt;polars 2.0.0 on PyPI&lt;/a&gt;, &lt;a rel=&quot;nofollow&quot; href=&quot;https://pypi.org/project/pyspark/4.2.0/&quot;&gt;pyspark 4.2.0 on PyPI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://duckdb.org/docs/current/guides/performance/how_to_tune_workloads&quot;&gt;DuckDB: How to Tune Workloads&lt;/a&gt; (larger-than-memory operators, &lt;code&gt;preserve_insertion_order&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://duckdb.org/docs/current/sql/dialect/order_preservation&quot;&gt;DuckDB: Order Preservation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;All timings and memory numbers: my own runs on October 19 (DuckDB 1.5.6, and the 9 GiB Polars and Spark runs) and October 22, 2026 (DuckDB 2.0.0, Polars 2.0.0, PySpark 4.2.0).&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/32051/duckdb-2-0-vs-polars-vs-spark-on-one-machine-150m-rows-under-a-4-5-gib-cap</guid>
<pubDate>Thu, 22 Oct 2026 08:52:00 +0000</pubDate>
</item>
<item>
<title>Advanced SQL Analytics for Data Analysis.</title>
<link>https://coderlegion.com/31618/advanced-sql-analytics-for-data-analysis</link>
<description>&lt;h2 id=&quot;advanced-sql-analytics-for-running-a-hospital-database-&quot;&gt;Advanced SQL analytics for running a hospital database.&lt;/h2&gt;
&lt;h3 id=&quot;introduction&quot;&gt;Introduction&lt;/h3&gt;
&lt;p&gt;Structured Query Language is essential and common in most industry sectors. In healthcare reporting, SQL plays a critical role in analytics. PostgreSQL lets data scientists take those transaction records and pull out operational insights, so you can make decisions. In this guide we go through the main SQL techniques, using a hospital database schema; patients , doctors, departments , wards, diagnoses, procedures, and visits. &lt;/p&gt;
&lt;p&gt;First, before anything else we need to create our database schema. Connect to PostgerSQL DBMS and run a new sql script to start.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Create a schema &lt;code&gt;hospital&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;code&gt;create schema hospital;&lt;/code&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Check to which schema you are connected.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;code&gt;show search_path;&lt;/code&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;If connected to a different schema, set connection to your &lt;code&gt;hospital&lt;/code&gt; schema.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;code&gt;set search_path to hospital;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Now you are set to begin creating your tables. Run the following codes to create the different tables and insert data into the newly created tables:&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Doctors Table&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;--create doctors table

create table hospital.doctors (
doctor_id int primary key,
doctor_name varchar(50) not null,
doctor_gender varchar(20),
doctor_specialty varchar(20),
doctor_experience_years int
);

select * from hospital.doctors;

-- insert data into doctors

insert into hospital.doctors (doctor_id, doctor_name, doctor_gender, doctor_specialty, doctor_experience_years)
values 
(101, 'DR.Brian Mwangi', 'Male', 'Cardiologist', 12),
(102, 'Dr. Sarah Wanjiku', 'Female', 'General Physician', 9), 
(103, 'Dr. Peter Otieno', 'Male', 'Orthopedic Surgeon', 15),
(104, 'Dr. Faith Chebet', 'Female', 'Pediatrician', 8);

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;Departments Table&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- create departments table

create table hospital.department (
department_id int primary key,
department_name varchar (50) not null
);


select * from hospital.department;

-- insert data into departments

insert into hospital.department (department_id, department_name)
values 
(10, 'Cardiology'),
(20, 'General Medicine'),
(30, 'Orthopedics'),
(40, 'Pediatrics');
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;Departments Table&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- create diagnosis table

create table hospital.diagnosis (
diagnosis_id int primary key,
diagnosis_name varchar(50),
diagnosis_category varchar(50)
);

select * from hospital.diagnosis;

--insert data into diagnosis table

insert into hospital.diagnosis (diagnosis_id, diagnosis_name, diagnosis_category)
values 
(501,'Hypertension', 'Chronic Disease'),
(502, 'Malaria', 'Infectious Disease'),
(503, 'Fracture', 'Injury'),
(504, 'Pneumonia', 'Respiratory Disease'),
(505, 'Diabetes', 'Chronic Disease'),
(506, 'Typhoid', 'Infectious'),
(507, 'Asthma', 'Respiratory Disease'),
(508, 'Back Pian', 'Musculoskeletal'),
(509, 'Gastritis', 'Digestive Disease'),
(510, 'Heart Failure', 'Chronic Disease'),
(511, 'Flu', 'Respiratoey Disease'),
(512, 'Kidney Infection', 'Infectious Disease'),
(513, 'Anaemia', 'Blood Disorder'),
(514, 'Sprain', 'Injury'),
(515, 'Gastroenteritis', 'Digestive Disease'),
(516, 'Dislocation', 'Injury'),
(517, 'Migraine', 'Neurological Disorder'),
(518, 'Chest Pain', 'Cardiovascular Disease');
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;Wards Table&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- create wards table 

create table hospital.wards (
ward_id int primary key, 
ward_name varchar (50)
);

-- insert data into wards
insert into hospital.wards (ward_id, ward_name)
values 
(100, 'Cardiac Care Unit'),
(200, 'General Ward'),
(300, 'Surgical Ward'),
(400, 'Childrens Ward');

select * from hospital.wards;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;Patients Table&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- create patients table 
create table hospital.patients (
patient_id serial primary key, 
patient_name varchar(50) not null,
patient_gender varchar(20),
patient_age int,
county varchar(30),
sub_county varchar(30)
);

--insert data into patients
insert into hospital.patients (patient_name, patient_gender, patient_age, county, sub_county)
values 
('Amina Otieno', 'Female', 34, 'Nairobi', 'Westlands'),
('John Kariuki', 'Male', 42,' Kiambu', 'Thika'),
('Grace Naliaka', 'Female', 28, 'Kakamega', 'Lurambi'),
('David Mutua', 'Male', 55, 'Machakos', 'Athi River'),
('Esther Achieng', 'Female', 63, 'Kisumu', 'Kisumu Central'),
('Mohamed Ali', 'Male', 19, 'Mombasa', 'Nyali'),
('Lucy Wambui', 'Female', 7, 'Nakuru', 'Naivasha'),
('Samuel Kiptoo', 'Male', 39, 'Uasin Gishu', 'Eldoret East'),
('Mary Wairumu', 'Female', 31, 'Murang''a', 'Kiharu'),
('Peter Ouma', 'Male', 46, 'Siaya', 'Bondo'),
('Brian Omondi', 'Male', 24, 'Kisumu', 'Kisumu East'),
('Catherine Musyoka', 'Female', 52, 'Kitui', 'Kitui Central'),
('Daniel Maina', 'Male', 16, 'Nyeri', 'Othaya'),
('Eunice Moraa', 'Female', 44, 'Kisii', 'Bonchari'),
('Francis Njuguna', 'Male', 60, 'Laikipia', 'Nyanyuki'),
('Hellen Jepchirchir', 'Female', 37, 'Bomet', 'Sotik'),
('Isaac Kamau', 'Male', 29, 'Nairobi', 'Kasarani'),
('Jane Muthoni', 'Female', 22, 'Meru', 'Imenti North'),
('Kevin Barasa', 'Male', 48, 'Bungoma', 'Webuye'),
('Lilian Adhiambo', 'Female', 26, 'Homa Bay', 'Rangwe'),
('Moses Cheruiyot', 'Male', 33, 'Kericho', 'Ainamoi'),
('Nadia Hussein', 'Female', 5, 'Garissa', 'Dadaab'),
('Patrick Muli', 'Male', 51, 'Makueni', 'Wote'),
('Ruth Nekesa', 'Female', 41, 'Trans Nzoia', 'Kitale'),
('Josephine Wekesa', 'Female', 36, 'Busia', 'Matayos');

select * from hospital.patients;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;Procedures Table&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;create table hospital.procedures (
procedure_id int primary key,
procedure_name varchar (50) not null,
procedure_category varchar (20)
);

--insert data into procedures
insert into hospital.procedures (procedure_id, procedure_name, procedure_category)
values
(701, 'Blood Pressure Monitoring', 'Diagnostic'),
(702, 'Malaria Test', 'Laboratory'),
(703, 'X-ray', 'Imaging'),
(704, 'Chest X-ray', 'Imaging'),
(705, 'Blood Sugar Test', 'Laboratory'),
(706, 'Full Blood Count', 'Laboratory'),
(707, 'Nebulization', 'Treatment'),
(708, 'Physiotherapy', 'Treatment'),
(709, 'Endoscopy', 'Diagnostic'),
(710, 'ECG', 'Diagnostic'),
(711, 'Consultation', 'Consultation'),
(712, 'Joint Support Bandage', 'Treatment'),
(713, 'IV Fluids', 'Treatment'),
(714, 'Closed Reduction', 'Treatment'),
(715, 'Pain Management', 'Treatment');


select * from hospital.procedures;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;Visits Table&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- create visits table (Fact Table)

create table hospital.visits (
visit_id int primary key,
visit_date date not null,
patient_id int not null references hospital.patients(patient_id),
doctor_id int not null references hospital.doctors(doctor_id),
department_id int not null references hospital.department(department_id),
ward_id int references hospital.wards(ward_id),
diagnosis_id int references hospital.diagnosis(diagnosis_id),
procedure_id int references hospital.procedures(procedure_id),
admission_type varchar(20) not null
    check (admission_type in ('Outpatient', 'Inpatient', 'Emergency')),
length_of_stay_days int default 0
    check (length_of_stay_days &amp;gt;= 0),
triage_level varchar(10)
    check (triage_level in ('Red', 'Orange', 'Yellow', 'Green')),
insurance_provider varchar(50),
total_bill_kes numeric(10,2) not null
    check (total_bill_kes &amp;gt;= 0),
payment_method varchar(20),
follow_up_required varchar(3)
    check (follow_up_required in ('Yes', 'No')),
patient_satisfaction_score numeric(3,1)
    check (patient_satisfaction_score between 0 and 5)
);

--insert data into visits table
insert into hospital.visits (visit_id, visit_date, patient_id, doctor_id, department_id, ward_id, diagnosis_id,
procedure_id, admission_type, length_of_stay_days, triage_level, insurance_provider, total_bill_kes, payment_method,
follow_up_required, patient_satisfaction_score)
values
(1001, '2026-06-01', 1, 101, 10, 100, 501, 701, 'Outpatient', 0, 'Green', 'NHIF', 3500, 'Cash', 'Yes', 4.5),
(1002, '2026-06-01', 2, 102, 20, 200, 502, 702, 'Outpatient', 0, 'Yellow', 'Britam', 2500, 'Insurance', 'No', 4.1),
(1003, '2026-06-02', 3, 103, 30, 300, 503, 703, 'Inpatient', 3, 'Red', 'AAR', 28000, 'Insurance', 'Yes', 4.3),
(1004, '2026-06-02', 4, 104, 40, 400, 504, 704, 'Inpatient', 4, 'Red', 'None', 42000, 'Cash', 'Yes', 3.9),
(1005, '2026-06-03', 5, 101, 10, 100, 505, 705, 'Outpatient', 0, 'Yellow', 'CIC', 4000, 'Mobile Money', 'Yes', 4.7),
(1006, '2026-06-03', 6, 102, 20, 200, 506, 706, 'Outpatient', 0, 'Green', 'None', 3000, 'Cash', 'No', 4.0),
(1007, '2026-06-04', 7, 104, 40, 400, 507, 707, 'Outpatient', 0, 'Yellow', 'NHIF', 5500, 'Insurance', 'Yes', 4.6),
(1008, '2026-06-04', 8, 103, 30, 300, 508, 708, 'Outpatient', 0, 'Green', 'None', 6500, 'Cash', 'Yes', 4.2),
(1009, '2026-06-05', 9, 102, 20, 200, 509, 709, 'Outpatient', 0, 'Yellow', 'Jubilee', 15000, 'Insurance', 'No', 3.8),
(1010, '2026-06-05', 10, 101, 10, 100, 510, 710, 'Inpatient', 5, 'Red', 'AAR', 76000, 'Insurance', 'Yes', 4.4),
(1011, '2026-06-06', 1, 101, 10, 100, 505, 705, 'Outpatient', 0, 'Green', 'NHIF', 4200, 'Mobile Money', 'Yes', 4.8),
(1012, '2026-06-06', 11, 102, 20, 200, 502, 702, 'Outpatient', 0, 'Green', 'None', 2300, 'Cash', 'No', 4.0),
(1013, '2026-06-07', 12, 101, 10, 100, 501, 710, 'Outpatient', 0, 'Yellow', 'Britam', 7000, 'Insurance', 'Yes', 4.5),
(1014, '2026-06-07', 13, 104, 40, 400, 511, 711, 'Outpatient', 0, 'Green', 'None', 1800, 'Cash', 'No', 4.1),
(1015, '2026-06-08', 14, 103, 30, 300, 503, 703, 'Inpatient', 2, 'Red', 'CIC', 24000, 'Mobile Money', 'Yes', 4.6),
(1016, '2026-06-08', 15, 102, 20, 200, 512, 706, 'Inpatient', 3, 'Orange', 'Jubilee', 36000, 'Insurance', 'Yes', 3.7),
(1017, '2026-06-09', 16, 101, 10, 100, 510, 710, 'Inpatient', 6, 'Red', 'AAR', 82000, 'Insurance', 'Yes', 4.2),
(1018, '2026-06-09', 17, 102, 20, 200, 509, 711, 'Outpatient', 0, 'Green', 'None', 2000, 'Cash', 'No', 4.0),
(1019, '2026-06-10', 18, 104, 40, 400, 507, 707, 'Outpatient', 0, 'Yellow', 'NHIF', 5200, 'Mobile Money', 'Yes', 4.7),
(1020, '2026-06-10', 19, 103, 30, 300, 508, 708, 'Outpatient', 0, 'Green', 'Britam', 6500, 'Insurance', 'No', 4.1),
(1021, '2026-06-11', 2, 101, 10, 100, 501, 701, 'Outpatient', 0, 'Yellow', 'Britam', 3800, 'Insurance', 'Yes', 4.3),
(1022, '2026-06-11', 20, 102, 20, 200, 513, 706, 'Outpatient', 0, 'Green', 'None', 3200, 'Mobile Money', 'No', 4.4),
(1023, '2026-06-12', 21, 103, 30, 300, 514, 712, 'Outpatient', 0, 'Yellow', 'NHIF', 7200, 'Cash', 'Yes', 4.0),
(1024, '2026-06-12', 22, 104, 40, 400, 515, 713, 'Inpatient', 2, 'Orange', 'None', 18500, 'Cash', 'Yes', 4.5),
(1025, '2026-06-13', 5, 101, 10, 100, 501, 710, 'Outpatient', 0, 'Yellow', 'CIC', 7600, 'Insurance', 'Yes', 4.6),
(1026, '2026-06-13', 23, 102, 20, 200, 506, 706, 'Inpatient', 2, 'Orange', 'Jubilee', 29000, 'Insurance', 'Yes', 3.9),
(1027, '2026-06-14', 24, 103, 30, 300, 516, 714, 'Inpatient', 1, 'Red', 'AAR', 33000, 'Insurance', 'Yes', 4.2),
(1028, '2026-06-14', 7, 104, 40, 400, 511, 711, 'Outpatient', 0, 'Green', 'NHIF', 1900, 'Mobile Money', 'No', 4.8),
(1029, '2026-06-15', 25, 102, 20, 200, 517, 715, 'Outpatient', 0, 'Yellow', 'Britam', 4800, 'Cash', 'Yes', 4.1),
(1030, '2026-06-15', 10, 101, 10, 100, 518, 710, 'Emergency', 1, 'Red', 'AAR', 22000, 'Insurance', 'Yes', 4.4);

select * from hospital.visits;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After creating our database schema, we will begin modelling the data using different advanced SQL functions.&lt;/p&gt;
&lt;h3 id=&quot;1-sql-window-functions&quot;&gt;1. SQL Window Functions&lt;/h3&gt;
&lt;p&gt;Window functions do computations on related rows in a table while maintaining the individuality of each rows.&lt;/p&gt;
&lt;h4 id=&quot;over-partition-by-order-by-and-window-frames&quot;&gt;OVER, PARTITION BY, ORDER BY, and Window Frames&lt;/h4&gt;
&lt;p&gt;The &lt;code&gt;OVER&lt;/code&gt; clause describes the rows for the window function operation.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;PARTITION BY&lt;/code&gt; defines different partitions for each result set.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ORDER BY&lt;/code&gt; defines row order in each partition.&lt;/li&gt;
&lt;li&gt;Window frames define different limits such as&lt;code&gt;ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW&lt;/code&gt; for cumulative sums and &lt;code&gt;ROWS BETWEEN 2 PRECEDING AND CURRENT ROW&lt;/code&gt; for moving averages.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;ranking-window-functions&quot;&gt;Ranking Window Functions&lt;/h4&gt;
&lt;p&gt;Ranking functions provide numeric ordering according to specified criteria.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ROW_NUMBER&lt;/code&gt; provides a distinct numeric integer value to each row within a partition.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;RANK&lt;/code&gt; provides identical rank values to ties, but skips other numbers.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DENSE_RANK&lt;/code&gt; provides identical rank values to ties, but does not skip numbers.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;NTILE&lt;/code&gt; divides rows in partitions into buckets of equal size.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;em&gt;Question:&lt;/em&gt; &lt;em&gt;How can hospital management rank inpatient visits by total bill within each admission type?&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;select 
    v.visit_id,
    p.patient_name,
    v.admission_type,
    v.total_bill_kes,
    row_number() over (partition by v.admission_type order by v.total_bill_kes desc) as row_num,
    rank() over (partition by v.admission_type order by v.total_bill_kes desc) as rnk,
    dense_rank() over (partition by v.admission_type order by v.total_bill_kes desc) as dense_rnk,
    ntile(2) over (partition by v.admission_type order by v.total_bill_kes desc) as bill_tier
from hospital.visits v
join hospital.patients p on v.patient_id = p.patient_id
where v.admission_type = 'Inpatient'
limit 5;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;Expected Results&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/m1wdvqqfnf9c1tdhy7l2.png&quot; alt=&quot;Visit Report&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Explanation&lt;/em&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ROW_NUMBER&lt;/code&gt; gives values from 1 to 5. &lt;/li&gt;
&lt;li&gt;The values obtained by &lt;code&gt;RANK&lt;/code&gt; and &lt;code&gt;DENSE_RANK&lt;/code&gt; will be same as there are no duplicate bill values. &lt;/li&gt;
&lt;li&gt;NTILE assigns the best bills to Tier 1.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;value-amp-navigation-window-function&quot;&gt;Value &amp;amp; Navigation Window Function&lt;/h4&gt;
&lt;p&gt;Navigation functions retrieve values based on relative rows.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;LAG&lt;/code&gt; retrieves values from previous row.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;LEAD&lt;/code&gt; retrieves values from next row.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;FIRST_VALUE&lt;/code&gt; gives the first value of the expression in the frame.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;LAST_VALUE&lt;/code&gt; gives the last value of the expression in the frame bounds like &lt;code&gt;ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;em&gt;Question:&lt;/em&gt; &lt;em&gt;How can we monitor patient billing process through multiple hospital visits?&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;select 
    v.visit_id,
    v.patient_id,
    v.visit_date,
    v.total_bill_kes,
    lag(v.total_bill_kes, 1, 0.00) over (partition by v.patient_id order by v.visit_date) as prev_bill,
    lead(v.total_bill_kes, 1, 0.00) over (partition by v.patient_id order by v.visit_date) as next_bill,
    first_value(v.total_bill_kes) over (partition by v.patient_id order by v.visit_date) as initial_bill,
    last_value(v.total_bill_kes) over (
        partition by v.patient_id 
        order by v.visit_date 
        rows between unbounded preceding and unbounded following
    ) as final_bill
from visits v
where v.patient_id in (1, 2)
order by v.patient_id, v.visit_date;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;Expected Results&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/3iqglknz6vze0tocm3o6.png&quot; alt=&quot;Patient Billing Process Monitoring&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Explanation&lt;/em&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;LAG&lt;/code&gt; retrieves previous visit invoices and &lt;code&gt;LEAD&lt;/code&gt; retrieves future visit invoices.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;FIRST_VALUE&lt;/code&gt; and &lt;code&gt;LAST_VALUE&lt;/code&gt; store the first and last visit invoices respectively.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;aggregate-window-functions&quot;&gt;Aggregate Window Functions&lt;/h4&gt;
&lt;p&gt;Aggregate functions &lt;code&gt;SUM&lt;/code&gt;, &lt;code&gt;AVG&lt;/code&gt;, &lt;code&gt;COUNT&lt;/code&gt;, &lt;code&gt;MIN&lt;/code&gt;, and &lt;code&gt;MAX&lt;/code&gt; calculate moving sums and moving averages when combined with &lt;code&gt;OVER&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Question:&lt;/em&gt; &lt;em&gt;In what way do we determine cumulative income, rolling average billings, categories, and billings for departments on each visit?&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;select 
    v.visit_id,
    v.visit_date,
    v.total_bill_kes,
    sum(v.total_bill_kes) over (order by v.visit_date, v.visit_id) as cumulative_revenue,
    avg(v.total_bill_kes) over (
        order by v.visit_date 
        rows between 2 preceding and current row
    ) as rolling_avg_bill,
    count(*) over (partition by v.admission_type) as total_admission_type_count,
    min(v.total_bill_kes) over (partition by v.admission_type) as min_admission_bill,
    max(v.total_bill_kes) over (partition by v.admission_type) as max_admission_bill
from hospital.visits v
order by v.visit_date, v.visit_id
limit 5;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;Expected Results&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/ft1sqrdykuhpi7j0ehnv.png&quot; alt=&quot;Aggregate Window Functions&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Explanation&lt;/em&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;SUM&lt;/code&gt; forms a running total. &lt;/li&gt;
&lt;li&gt;&lt;code&gt;AVG&lt;/code&gt; calculates a 3 row moving average. &lt;/li&gt;
&lt;li&gt;&lt;code&gt;COUNT&lt;/code&gt;, &lt;code&gt;MIN&lt;/code&gt;, and &lt;code&gt;MAX&lt;/code&gt; present group statistics without collapsing rows.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;2-sql-joins&quot;&gt;2. SQL Joins&lt;/h3&gt;
&lt;p&gt;Joins combine columns from multiple tables using related key columns.&lt;/p&gt;
&lt;h4 id=&quot;inner-join&quot;&gt;INNER JOIN&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;INNER JOIN&lt;/code&gt; returns rows with matching key values in both tables.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Question:&lt;/em&gt; &lt;em&gt;How do we generate a visit report combining patient names, attending doctors, and clinical departments?&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;select 
    v.visit_id,
    p.patient_name,
    d.doctor_name,
    dept.department_name,
    v.total_bill_kes
from hospital.visits v
inner join hospital.patients p on v.patient_id = p.patient_id
inner join hospital.doctors d on v.doctor_id = d.doctor_id
inner join hospital.department dept on v.department_id = dept.department_id
limit 5;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;Expected Results&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/cpriilp4a3qc6w9wh6v5.png&quot; alt=&quot;Inner Join&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Explanation&lt;/em&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;INNER JOIN&lt;/code&gt; Matches keys across visits, patients, doctors, and department tables.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;left-join&quot;&gt;LEFT JOIN&lt;/h4&gt;
&lt;p&gt;The &lt;code&gt;LEFT JOIN&lt;/code&gt; produces all the rows from the left table and the matched rows from the right table. Fields in the right table without a match produce &lt;code&gt;NULL&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Question:&lt;/em&gt; &lt;em&gt;How can we study the number of visits and expenses of patients without losing those that have no visits at all?&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;select 
    p.patient_id,
    p.patient_name,
    count(v.visit_id) as total_visits,
    coalesce(sum(v.total_bill_kes), 0.00) as total_spent_kes
from hospital.patients p
left join hospital.visits v on p.patient_id = v.patient_id
group by p.patient_id, p.patient_name
order by total_visits desc, p.patient_id
limit 5;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;Expected Results&lt;/em&gt;&lt;br&gt;
&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/8osejvnidi5913phipk8.png&quot; alt=&quot;Left Join&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Explanation&lt;/em&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;All patient records are retained using &lt;code&gt;LEFT JOIN&lt;/code&gt;. &lt;/li&gt;
&lt;li&gt;Patients having no visits will have &lt;code&gt;COUNT&lt;/code&gt; as 0 while &lt;code&gt;COALESCE&lt;/code&gt; will convert &lt;code&gt;NULL&lt;/code&gt; sums to &lt;code&gt;0.00.&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;right-join&quot;&gt;RIGHT JOIN&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;RIGHT JOIN&lt;/code&gt; retrieves all records from right table and matching records from left table.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Question:&lt;/em&gt; &lt;em&gt;How can hospital administration ensure that all physical wards are reflected in admission records?&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;select 
    w.ward_id,
    w.ward_name,
    v.visit_id,
    v.total_bill_kes
from hospital.visits v
right join hospital.wards w on v.ward_id = w.ward_id
where v.visit_id is null or v.visit_id &amp;lt;= 1003;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;Expected Results&lt;/em&gt;&lt;br&gt;
&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/trk5b98a06x3pfebvxub.png&quot; alt=&quot;Right Join&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Explanation&lt;/em&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;All wards are kept in &lt;code&gt;RIGHT JOIN&lt;/code&gt;. &lt;/li&gt;
&lt;li&gt;The Childrens Ward will be with &lt;code&gt;NULL&lt;/code&gt; visits if no match occurs.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;full-outer-join&quot;&gt;FULL OUTER JOIN&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;FULL OUTER JOIN&lt;/code&gt; will display all records if there is a match in either left or right record.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Question:&lt;/em&gt; &lt;em&gt;How do we perform auditing of doctor and department records for unassigned doctors or unmapped departments?&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;select 
    d.doctor_name,
    dept.department_name
from hospital.doctors d
full outer join hospital.department dept on d.doctor_id = dept.department_id;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;Expected Results&lt;/em&gt;&lt;br&gt;
&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/lxx54jj6rrte3qgctuzv.png&quot; alt=&quot;Full Outer Join&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Explanation&lt;/em&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Null values will be returned in the rows where there is no relationship between doctor and department IDs.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;cross-join&quot;&gt;CROSS JOIN&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;CROSS JOIN&lt;/code&gt; results in a Cartesian product where each row in the first table is paired up with each row of the second table.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Question:&lt;/em&gt; &lt;em&gt;How can hospital managers get a complete list of all pairings of physicians with hospital wards for coverage planing?&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;select 
    d.doctor_name,
    w.ward_name
from hospital.doctors d
cross join hospital.wards w
order by d.doctor_name, w.ward_name
limit 5;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;Expected Results&lt;/em&gt;&lt;br&gt;
&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/1wr0ga53xrcehptw5ysf.png&quot; alt=&quot;Cross Join&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Explanation&lt;/em&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Joining 4 doctors with 4 wards results in 16 combination rows.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;self-join&quot;&gt;SELF JOIN&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;SELF JOIN&lt;/code&gt; involves joining a table to itself for comparing rows in the same table.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Question:&lt;/em&gt; &lt;em&gt;In what way can readmission of patients be identified by matching their first visit to their next visit?&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;select 
    v1.patient_id,
    v1.visit_id as original_visit,
    v1.visit_date as original_date,
    v2.visit_id as return_visit,
    v2.visit_date as return_date
from hospital.visits v1
inner join hospital.visits v2 on v1.patient_id = v2.patient_id and v1.visit_id &amp;lt; v2.visit_id
order by v1.patient_id
limit 5;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;Expected Results&lt;/em&gt;&lt;br&gt;
&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/6gu6l8j459l4c0l89mg4.png&quot; alt=&quot;Self Join&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Explanation&lt;/em&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Linking visits on patient ID where &lt;code&gt;v1.visit_id&lt;/code&gt; is less than &lt;code&gt;v2.visit_id&lt;/code&gt; matches distinct visits per patient.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;join-summary&quot;&gt;Join Summary&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;INNER JOIN&lt;/code&gt; matches rows in both tables.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;LEFT JOIN&lt;/code&gt; keeps all left table rows.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;RIGHT JOIN&lt;/code&gt; keeps all right table rows.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;FULL OUTER JOIN&lt;/code&gt; keeps all rows from both tables.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CROSS JOIN&lt;/code&gt; produces Cartesian pairs.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SELF JOIN&lt;/code&gt; compares rows within one table.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;3-common-table-expressions-ctes-&quot;&gt;3. Common Table Expressions (CTEs)&lt;/h3&gt;
&lt;p&gt;A CTE is a named temporary table created within a &lt;code&gt;WITH&lt;/code&gt; clause.&lt;/p&gt;
&lt;h4 id=&quot;simple-ctes&quot;&gt;Simple CTEs&lt;/h4&gt;
&lt;p&gt;&lt;em&gt;Question:&lt;/em&gt; &lt;em&gt;What approach should be used to identify costly inpatient treatments and connect them with patients and their physicians?&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;with high_cost_visits as (
    select 
        visit_id,
        patient_id,
        doctor_id,
        total_bill_kes,
        admission_type
    from hospital.visits
    where total_bill_kes &amp;gt;= 35000.00
)
select 
    h.visit_id,
    p.patient_name,
    d.doctor_name,
    h.total_bill_kes
from high_cost_visits h
join hospital.patients p on h.patient_id = p.patient_id
join hospital.doctors d on h.doctor_id = d.doctor_id;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;Expected Results&lt;/em&gt;&lt;br&gt;
&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/lx3qdtqcxagxsp5vwmxv.png&quot; alt=&quot;Simple CTE&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Explanation&lt;/em&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;CTE will filter out high cost visits first to keep the subsequent join clean.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;multiple-chained-ctes&quot;&gt;Multiple Chained CTEs&lt;/h4&gt;
&lt;p&gt;&lt;em&gt;Question:&lt;/em&gt; &lt;em&gt;How do we find out department revenues, overall average, and top performing departments?&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;with dept_revenue as (
    select 
        department_id,
        sum(total_bill_kes) as total_revenue,
        count(visit_id) as total_visits
    from hospital.visits
    group by department_id
),
dept_benchmark as (
    select 
        avg(total_revenue) as benchmark_avg_revenue
    from dept_revenue
)
select 
    d.department_name,
    dr.total_revenue,
    dr.total_visits,
    round(db.benchmark_avg_revenue, 2) as benchmark_avg
from dept_revenue dr
join hospital.department d on dr.department_id = d.department_id
cross join dept_benchmark db
where dr.total_revenue &amp;gt; db.benchmark_avg_revenue;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;Expected Results&lt;/em&gt;&lt;br&gt;
&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/449qhx3hzealxl7t6ncw.png&quot; alt=&quot;Multiple CTEs&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Explanation&lt;/em&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;dept_revenue&lt;/code&gt; calculates the total revenue for each &lt;code&gt;department&lt;/code&gt;, &lt;code&gt;dept_benchmark&lt;/code&gt; calculates the average and the last &lt;code&gt;SELECT&lt;/code&gt; statement filters departments whose revenues are greater than the average.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;recursive-ctes&quot;&gt;Recursive CTEs&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;Recursive CTEs&lt;/code&gt; create a sequence by means of an &lt;code&gt;anchor query&lt;/code&gt; and &lt;code&gt;recursive query&lt;/code&gt; linked by &lt;code&gt;UNION ALL&lt;/code&gt; operator.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Question:&lt;/em&gt; &lt;em&gt;How can the hospital management create a biweekly follow up schedule?&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;with recursive follow_up_schedule as (
    select 
        1 as cycle_number,
        cast('2026/07/01' as date) as scheduled_date
    union all
    select 
        cycle_number + 1,
        cast(scheduled_date + interval '14 days' as date)
    from follow_up_schedule
    where cycle_number &amp;lt; 4
)
select * from follow_up_schedule;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;Expected Results&lt;/em&gt;&lt;br&gt;
&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/w9avtbp5ixud7pfq9ysy.png&quot; alt=&quot;Recursive CTE&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Explanation&lt;/em&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Cycle number 1 starts from &lt;code&gt;July 1 2026&lt;/code&gt; as the &lt;code&gt;anchor date&lt;/code&gt;, and the recursive query keeps adding 14 days till cycle number is 4.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;ctes-vs-subqueries&quot;&gt;CTEs vs. Subqueries&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;CTEs&lt;/code&gt; provide well-defined top-level modules, which can be used again and again, while subqueries within the WHERE or FROM clause tend to complicate code and cannot be executed recursively.&lt;/p&gt;
&lt;h3 id=&quot;4-sql-subqueries&quot;&gt;4. SQL Subqueries&lt;/h3&gt;
&lt;p&gt;A subquery is a query nested inside another SQL query.&lt;/p&gt;
&lt;h4 id=&quot;subquery-types&quot;&gt;Subquery Types&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Scalar&lt;/code&gt; Subquery fetches one row and one column.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Non-correlated&lt;/code&gt;Subquery executes only once and independently.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Correlated&lt;/code&gt; Subquery uses columns of outer query and executes for every row of outer query.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Nested&lt;/code&gt; Subquery uses subqueries in subqueries.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;subqueries-in-select-clauses&quot;&gt;Subqueries in SELECT Clauses&lt;/h4&gt;
&lt;p&gt;&lt;em&gt;Question:&lt;/em&gt; &lt;em&gt;How can individual patient bills be shown along with the total hospital average bill?&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;select 
    visit_id,
    total_bill_kes,
    (select round(avg(total_bill_kes), 2) from hospital.visits) as overall_avg_bill
from hospital.visits
where visit_id between 1001 and 1005;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;Expected Results&lt;/em&gt;&lt;br&gt;
&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/gx3q0v7r1kr5sjyfeasm.png&quot; alt=&quot;Select Subqueries&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Explanation&lt;/em&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Scalar subquery evaluates to average &lt;code&gt;16266.67&lt;/code&gt; for all rows.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;using-subqueries-in-from-clauses-derived-table-&quot;&gt;Using Subqueries in FROM Clauses (Derived Table)&lt;/h4&gt;
&lt;p&gt;&lt;em&gt;Question:&lt;/em&gt; &lt;em&gt;How do we query the pre-aggregated metrics of patients using an inline derived table?&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;select 
    patient_summary.patient_id,
    patient_summary.visit_count,
    patient_summary.total_spent
from (
    select 
        patient_id,
        count(visit_id) as visit_count,
        sum(total_bill_kes) as total_spent
    from hospital.visits
    group by patient_id
) as patient_summary
where patient_summary.visit_count &amp;gt; 1;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;Expected Results&lt;/em&gt;&lt;br&gt;
&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/gpg7zu85w5vr1pebl4ap.png&quot; alt=&quot;From Subquery&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Explanation&lt;/em&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;FROM&lt;/code&gt; subquery groups the number of visits per patient into table alias patient_summary, subject to WHERE.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;subqueries-in-where-clause&quot;&gt;Subqueries in WHERE Clause&lt;/h4&gt;
&lt;p&gt;&lt;em&gt;Question:&lt;/em&gt; &lt;em&gt;What is the correlated subquery to find visits where the total bill is greater than the average bill of that particular department?&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;select 
    v1.visit_id,
    v1.patient_id,
    v1.department_id,
    v1.total_bill_kes
from hospital.visits v1
where v1.total_bill_kes &amp;gt; (
    select avg(v2.total_bill_kes)
    from hospital.visits v2
    where v2.department_id = v1.department_id
)
limit 5;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;Expected Results&lt;/em&gt;&lt;br&gt;
&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/08o8bu15t93paib73o1b.png&quot; alt=&quot;Where Subqueries&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Explanation&lt;/em&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The inner query calculates the average billing amount for each department based on outer query department IDs.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;em&gt;Question:&lt;/em&gt; &lt;em&gt;Which physicians have admitted any patients as inpatients?&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;select 
    d.doctor_id,
    d.doctor_name,
    d.doctor_specialty
from hospital.doctors d
where exists (
    select 1 
    from hospital.visits v 
    where v.doctor_id = d.doctor_id 
      and v.admission_type = 'inpatient'
);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;Expected Results&lt;/em&gt;&lt;br&gt;
&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/x34b2dlyj5gbu387srum.png&quot; alt=&quot;Exists&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Explanation&lt;/em&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;EXISTS&lt;/code&gt; returns true when there is a row for a visit that matches.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;em&gt;Question:&lt;/em&gt; &lt;em&gt;How can we find visits where the billing value is greater than all the outpatient visit bills?&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;select 
    visit_id,
    patient_id,
    total_bill_kes,
    admission_type
from hospital.visits
where total_bill_kes &amp;gt; all (
    select total_bill_kes
    from hospital.visits
    where admission_type = 'outpatient'
)
limit 5;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;Expected Results&lt;/em&gt;&lt;br&gt;
&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/vdfcjvi0dr5ifpb94esd.png&quot; alt=&quot;where&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Explanation&lt;/em&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The internal query locates outpatient charges up to &lt;code&gt;15000.00&lt;/code&gt;. ALL filters visits more than &lt;code&gt;15000.00&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;Understanding the concepts of window functions, joins, common table expressions, and subqueries helps in creating strong PostgreSQL queries that enable healthcare workers to review their operations and enhance financial efficiency. Such information enables healthcare providers to offer quality pateint care which meets patient outcomes. &lt;/p&gt;
&lt;h3 id=&quot;github&quot;&gt;GitHub&lt;/h3&gt;
&lt;p&gt;For the completed project visit my GitHub project &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/MakoriNyachaki/sql_for_data_science&quot;&gt;Hospital Database&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31618/advanced-sql-analytics-for-data-analysis</guid>
<pubDate>Sun, 18 Oct 2026 13:54:35 +0000</pubDate>
</item>
<item>
<title>Python 3.15 Is Not a Speed Release for Data Teams. It Is an Operations Release.</title>
<link>https://coderlegion.com/30776/python-3-15-is-not-a-speed-release-for-data-teams-it-is-an-operations-release</link>
<description>&lt;p&gt;Originally published on DEV.to: &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/firfircelik/python-315-is-not-a-speed-release-for-data-teams-it-is-an-operations-release-5fbo&quot;&gt;https://dev.to/firfircelik/python-315-is-not-a-speed-release-for-data-teams-it-is-an-operations-release-5fbo&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Python 3.15.0 shipped on October 9, 2026 (&lt;a rel=&quot;nofollow&quot; href=&quot;https://blog.python.org/2026/10/python-3150-final-is-here/&quot;&gt;release post&lt;/a&gt;). The headline number most people will repeat is in the JIT section of &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.python.org/3.15/whatsnew/3.15.html&quot;&gt;What's New&lt;/a&gt;: a &quot;7-8% geometric mean performance improvement&quot; over the standard interpreter on x86-64 Linux, and 11-12% on AArch64 macOS.&lt;/p&gt;
&lt;p&gt;For a data team, the tempting reading is &quot;upgrade the job images, pipelines get faster.&quot; I installed 3.15.0 on a VM, ran a small pandas/pyarrow workload with the JIT off and on, and then spent most of my time on three features that do not touch throughput at all. Those three are the ones that change how you run Python next week.&lt;/p&gt;
&lt;h3 id=&quot;the-thesis&quot;&gt;The thesis&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;For data teams, Python 3.15 is an operations release, not a speed release.&lt;/strong&gt; The features that matter are about startup (PEP 810 lazy imports), observability (the Tachyon sampling profiler and frame pointers on by default) and supply chain (PEP 829 &lt;code&gt;.start&lt;/code&gt; files replacing &lt;code&gt;import&lt;/code&gt; lines in &lt;code&gt;.pth&lt;/code&gt; files).&lt;/p&gt;
&lt;p&gt;Each of them also has a trap that the release notes do not spell out. I hit all of them on the VM, and they are documented below with the commands to reproduce them.&lt;/p&gt;
&lt;h3 id=&quot;the-antithesis-at-full-strength&quot;&gt;The antithesis, at full strength&lt;/h3&gt;
&lt;p&gt;The best counter-case is not silly. It goes like this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The JIT is real now.&lt;/strong&gt; What's New lists a new tracing frontend, basic register allocation, more constant propagation and reference count elimination. The pyperformance range is wide, &quot;from roughly 15% slowdown to over 100% speedup&quot;, but the geometric mean is positive on both platforms.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Data engineers write more Python-level loops than they admit.&lt;/strong&gt; Row-wise &lt;code&gt;apply&lt;/code&gt;, per-record parsing, UDFs, validation code. That is exactly the code a JIT can see.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Free-threading got a stable ABI.&lt;/strong&gt; PEP 803 adds &lt;code&gt;abi3t&lt;/code&gt;: extensions that target the Stable ABI can now be compiled so they are compatible with free-threaded builds. That is the long road to real multi-core Python for CPU-bound pipeline code.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So the antithesis is: the startup and tooling features are nice, but the throughput work is what will show up on the cloud bill.&lt;/p&gt;
&lt;p&gt;Here is what that looked like on the VM.&lt;/p&gt;
&lt;h4 id=&quot;setup-1&quot;&gt;Setup&lt;/h4&gt;
&lt;p&gt;Everything below ran on one shared VM: 8 vCPU Intel Xeon, 15 GiB RAM, Debian 13, kernel 6.12. Python 3.15.0 is the uv-managed build (Clang 22, tail-calling interpreter, JIT compiled in but off by default, turned on with &lt;code&gt;PYTHON_JIT=1&lt;/code&gt;). Libraries: numpy 2.5.4, pandas 3.0.6, pyarrow 26.0.0, all installed from &lt;code&gt;cp315&lt;/code&gt; wheels three days after the release. The VM is shared with other workloads, so I report medians of repeated fresh processes and repeat whole rounds.&lt;/p&gt;
&lt;p&gt;One thing I did not expect: the VM had uv 0.12.24, and &lt;code&gt;uv python install 3.15&lt;/code&gt; installed &lt;strong&gt;3.15.0rc3&lt;/strong&gt;, not the final release. uv 0.13.0 installed 3.15.0. If your images resolve &quot;3.15&quot; through an older tool, check &lt;code&gt;python -VV&lt;/code&gt; before you trust anything. That matters again in the profiler section.&lt;/p&gt;
&lt;h4 id=&quot;jit-off-vs-on&quot;&gt;JIT off vs on&lt;/h4&gt;
&lt;p&gt;Three workloads, timed in-process, 7 fresh processes per setting, two rounds:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Workload &lt;/th&gt;&lt;th&gt;JIT off (median) &lt;/th&gt;&lt;th&gt;JIT on (median) &lt;/th&gt;&lt;th&gt;Change&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Pure-Python parsing loop, 1M rows &lt;/td&gt;&lt;td&gt;337.6 / 332.4 ms &lt;/td&gt;&lt;td&gt;309.7 / 304.6 ms &lt;/td&gt;&lt;td&gt;about 8% faster&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;pandas row-wise &lt;code&gt;apply&lt;/code&gt;, 100k rows &lt;/td&gt;&lt;td&gt;1176.9 / 1183.6 ms &lt;/td&gt;&lt;td&gt;1126.8 / 1144.3 ms &lt;/td&gt;&lt;td&gt;about 3 to 4% faster&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;pandas vectorized &lt;code&gt;groupby().sum()&lt;/code&gt;, 2M rows &lt;/td&gt;&lt;td&gt;63.5 / 76.3 ms &lt;/td&gt;&lt;td&gt;63.3 / 71.4 ms &lt;/td&gt;&lt;td&gt;within noise (single runs ranged 60 to 103 ms)&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;jit_bench.py (the three workloads)&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;&quot;&quot;&quot;Time three data-team-shaped workloads in-process. Run with PYTHON_JIT=0 or 1.&quot;&quot;&quot;
import sys
import time

import numpy as np
import pandas as pd


def pure_python_parse(n=1_000_000):
    total = 0
    for i in range(n):
        line = f&quot;{i},{i % 97},{i * 3}&quot;
        a, b, c = line.split(&quot;,&quot;)
        if int(b) &amp;gt; 50:
            total += int(c)
    return total


def vectorized_groupby(n=2_000_000):
    rng = np.random.default_rng(42)
    df = pd.DataFrame({&quot;k&quot;: rng.integers(0, 50_000, n), &quot;v&quot;: rng.random(n)})
    return df.groupby(&quot;k&quot;)[&quot;v&quot;].sum().iloc[0]


def rowwise_apply(n=100_000):
    df = pd.DataFrame({&quot;email&quot;: [f&quot; User{i}+*Emails are not allowed* &quot; for i in range(n)]})
    return df.apply(lambda r: r[&quot;email&quot;].strip().lower().replace(&quot;+spam&quot;, &quot;&quot;), axis=1).iloc[0]


WORKLOADS = {&quot;pure_python_parse&quot;: pure_python_parse,
             &quot;vectorized_groupby&quot;: vectorized_groupby,
             &quot;rowwise_apply&quot;: rowwise_apply}

fn = WORKLOADS[sys.argv[1]]
fn(10_000)  # warm-up
t0 = time.perf_counter()
fn()
print(f&quot;{(time.perf_counter() - t0) * 1000:.1f}&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Run it as &lt;code&gt;PYTHON_JIT=0 python jit_bench.py pure_python_parse&lt;/code&gt; and &lt;code&gt;PYTHON_JIT=1 ...&lt;/code&gt;, several times each.&lt;/p&gt;
&lt;p&gt;What the numbers support (interpretation): the JIT does what the release notes say for Python-level code. The pure-Python loop landed right in the 7-8% band. But a pipeline whose hot path is already in numpy, pandas or Arrow does not get faster, because the JIT never sees that code. And the row-wise &lt;code&gt;apply&lt;/code&gt; is the honest middle: 3 to 4% is real, but the fix for a 1.2 second &lt;code&gt;apply&lt;/code&gt; is to vectorize it, not to wait for a JIT.&lt;/p&gt;
&lt;p&gt;The free-threading ABI is a packaging milestone. What's New says moving an extension to &lt;code&gt;abi3t&lt;/code&gt; &quot;usually requires some non-trivial changes to the source code&quot;, and it pays off only when the extensions you depend on ship those wheels and you run a free-threaded build (&lt;code&gt;cp315t&lt;/code&gt;). That is worth tracking, not something you collect by bumping a base image.&lt;/p&gt;
&lt;p&gt;So the antithesis is partly right: there is a free win for Python-level code. It is small, it is opt-in, and it is experimental. The three features below change your runbooks this week.&lt;/p&gt;
&lt;h3 id=&quot;1-lazy-imports-fix-startup-not-throughput&quot;&gt;1. Lazy imports fix startup, not throughput&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;The wrong assumption:&lt;/strong&gt; &quot;lazy imports make Python faster.&quot;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The mechanics:&lt;/strong&gt; &lt;code&gt;lazy import pandas as pd&lt;/code&gt; binds &lt;code&gt;pd&lt;/code&gt; to a lightweight proxy. The module is loaded the first time the name is used, a step PEP 810 calls reification. If loading fails, the error is raised at first use, and the traceback shows both places. Lazy imports are only allowed at module scope: inside a function, a class body or a &lt;code&gt;try&lt;/code&gt;/&lt;code&gt;except&lt;/code&gt; they are a &lt;code&gt;SyntaxError&lt;/code&gt;. I confirmed the &lt;code&gt;try&lt;/code&gt; case on 3.15.0, which rules out the classic &lt;code&gt;try: import orjson as json&lt;/code&gt; fallback.&lt;/p&gt;
&lt;p&gt;I wrote a toy pipeline CLI, &lt;code&gt;pipectl&lt;/code&gt;, with the imports most data tools have at the top: numpy, pandas, pyarrow, requests. It has a &lt;code&gt;version&lt;/code&gt; command, a &lt;code&gt;check&lt;/code&gt; command that validates a JSON config, and a &lt;code&gt;run&lt;/code&gt; command that aggregates 2M rows and writes Parquet. The lazy variant changes only the import lines:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;lazy import json
import argparse
import sys

lazy import numpy as np
lazy import pandas as pd
lazy import pyarrow as pa
lazy import pyarrow.parquet as pq
lazy import requests
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each command was run 30 times in a fresh process after warm-up, in three clean rounds:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Command &lt;/th&gt;&lt;th&gt;Eager (median per round) &lt;/th&gt;&lt;th&gt;Lazy (median per round)&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;python -c pass&lt;/code&gt; (floor) &lt;/td&gt;&lt;td&gt;9.3 ms &lt;/td&gt;&lt;td&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;pipectl version&lt;/code&gt; &lt;/td&gt;&lt;td&gt;351.4 / 381.2 / 344.4 ms &lt;/td&gt;&lt;td&gt;20.0 / 19.6 / 19.9 ms&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;pipectl check cfg.json&lt;/code&gt; &lt;/td&gt;&lt;td&gt;352.1 / 342.7 / 352.1 ms &lt;/td&gt;&lt;td&gt;21.2 / 21.1 / 20.8 ms&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;pipectl run&lt;/code&gt; (2M rows) &lt;/td&gt;&lt;td&gt;453.2 / 441.0 / 450.8 ms &lt;/td&gt;&lt;td&gt;400.5 / 392.8 / 400.5 ms&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;code&gt;-X importtime&lt;/code&gt; shows where the eager 350 ms goes: pandas about 180 ms cumulative, numpy about 40 ms, requests about 35 ms (medians of three runs, and importtime adds its own overhead).&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;version&lt;/code&gt; and &lt;code&gt;check&lt;/code&gt; rows are the point of the feature: about 94% less wall time, because those commands never touch pandas. That is more than the &quot;50-70% in practice&quot; that &lt;a rel=&quot;nofollow&quot; href=&quot;https://peps.python.org/pep-0810/&quot;&gt;PEP 810&lt;/a&gt; cites for command-line tools, and it should be: my CLI is the extreme case where the cheap commands use none of the heavy imports.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;run&lt;/code&gt; row is the one to read twice. Lazy saved about 50 ms, so did it make the work faster? No. I ran a third variant, eager but with the &lt;code&gt;import requests&lt;/code&gt; line deleted:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;&lt;code&gt;pipectl run&lt;/code&gt;, 20 runs each &lt;/th&gt;&lt;th&gt;Median&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;eager &lt;/td&gt;&lt;td&gt;454.9 ms&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;eager without &lt;code&gt;import requests&lt;/code&gt; &lt;/td&gt;&lt;td&gt;410.4 ms&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;lazy &lt;/td&gt;&lt;td&gt;413.3 ms&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The whole gain is the import that the &lt;code&gt;run&lt;/code&gt; path never uses. &lt;strong&gt;Lazy imports remove the imports you don't use. They don't make the ones you do use any cheaper.&lt;/strong&gt; If your job imports pandas and uses pandas, it pays the full 180 ms either way.&lt;/p&gt;
&lt;p&gt;Where this pays off for data teams (interpretation): CLIs, &lt;code&gt;--help&lt;/code&gt; in CI, pre-commit hooks, tests that import a package to run one function, notebook kernels, and anything that gets parsed often. The Airflow docs already warn that &quot;import statements also count as top-level code&quot; in Dag files and that expensive imports there hurt the scheduler (&lt;a rel=&quot;nofollow&quot; href=&quot;https://airflow.apache.org/docs/apache-airflow/stable/best-practices.html#top-level-python-code&quot;&gt;Airflow best practices&lt;/a&gt;). The &lt;code&gt;lazy&lt;/code&gt; keyword looks built for that pattern. I did not test it inside Airflow, and you need an Airflow version that supports 3.15 first.&lt;/p&gt;
&lt;h4 id=&quot;trap-1-the-global-switch-broke-pandas&quot;&gt;Trap 1: the global switch broke pandas&lt;/h4&gt;
&lt;p&gt;3.15 also has an interpreter-wide mode: &lt;code&gt;-X lazy_imports=all&lt;/code&gt; or &lt;code&gt;PYTHON_LAZY_IMPORTS=all&lt;/code&gt; makes every module-level import in every module potentially lazy, third-party code included. It is tempting to flip it in a job image and collect the startup win without touching code. On 3.15.0 with pandas 3.0.6 and numpy 2.5.4, that failed 5 out of 5 times:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;$ PYTHON_LAZY_IMPORTS=all python -c &quot;import pandas as pd; pd.DataFrame({'a': [1]})&quot;
  File &quot;.../pandas/_typing.py&quot;, line 401, in &amp;lt;module&amp;gt;
    TakeIndexer: TypeAlias = Sequence[int] | Sequence[np.integer] | npt.NDArray[np.integer]
                                                                    ~~~~~~~~~~~^^^^^^^^^^^^
TypeError: 'lazy_import' object is not subscriptable
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The minimal trigger is &lt;code&gt;npt.NDArray[np.integer]&lt;/code&gt; with &lt;code&gt;numpy.typing&lt;/code&gt; imported under the global mode. numpy alone and requests alone worked. A tiny pure-Python package with the same re-export chain worked too, so this looks specific to how &lt;code&gt;numpy.typing&lt;/code&gt; (which defines a module-level &lt;code&gt;__getattr__&lt;/code&gt;) interacts with global laziness. I did not chase it further. The lesson does not depend on the root cause: the global mode changes the import semantics of code you did not write, and the &lt;code&gt;lazy&lt;/code&gt; keyword in your own entry point does not.&lt;/p&gt;
&lt;h4 id=&quot;trap-2-side-effect-imports-silently-stop-happening&quot;&gt;Trap 2: side-effect imports silently stop happening&lt;/h4&gt;
&lt;p&gt;Plenty of data code imports a module only for what it does at import time: registering a pandas accessor, a SQLAlchemy dialect, a plugin. Linters flag these as unused, and they are the first thing someone will mark &lt;code&gt;lazy&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;lazy import pandas as pd
lazy import dq_accessors  # noqa: F401  (registers df.dq at import time)

df = pd.DataFrame({&quot;a&quot;: [1, None, 3], &quot;b&quot;: [None, None, 1]})
print(df.dq.null_ratio())
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;AttributeError: 'DataFrame' object has no attribute 'dq'. Did you mean '.eq' instead of '.dq'?
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The name &lt;code&gt;dq_accessors&lt;/code&gt; is never used, so it is never reified, and the registration never runs. The eager version prints &lt;code&gt;{'a': 0.333, 'b': 0.667}&lt;/code&gt;. PEP 810 says this directly: side effects are &quot;deferred until first use&quot;, and it recommends explicit initialization functions instead of import-time registration.&lt;/p&gt;
&lt;h4 id=&quot;trap-3-the-pep-s-kill-switch-does-not-exist-in-3-15-0&quot;&gt;Trap 3: the PEP's kill switch does not exist in 3.15.0&lt;/h4&gt;
&lt;p&gt;PEP 810 describes a third mode, &lt;code&gt;&quot;none&quot;&lt;/code&gt;, to force every import eager. The shipped 3.15.0 accepts only &lt;code&gt;normal&lt;/code&gt; and &lt;code&gt;all&lt;/code&gt;, which matches the &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.python.org/3.15/library/sys.html&quot;&gt;sys docs&lt;/a&gt;. Copying the PEP is not harmless:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;$ PYTHON_LAZY_IMPORTS=none python -c pass
Fatal Python error: config_init_lazy_imports: PYTHON_LAZY_IMPORTS: invalid value; expected 'all' or 'normal'
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Exit code 1, before any of your code runs. &lt;code&gt;sys.set_lazy_imports(&quot;none&quot;)&lt;/code&gt; raises &lt;code&gt;ValueError&lt;/code&gt;. The PEP page itself now says it is &quot;a historical document&quot;. If you want an emergency &quot;everything eager&quot; switch, a filter that returns &lt;code&gt;False&lt;/code&gt; does it (verified: &lt;code&gt;json&lt;/code&gt; loads at the &lt;code&gt;lazy import&lt;/code&gt; line):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import sys

sys.set_lazy_imports_filter(lambda importer, name, fromlist: False)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;runbook-lazy-imports&quot;&gt;Runbook: lazy imports&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;Measure first: &lt;code&gt;python -X importtime -m yourtool --help 2&amp;gt; imports.txt&lt;/code&gt;, then sort by the cumulative column.&lt;/li&gt;
&lt;li&gt;Mark only the heavy imports in your own entry point &lt;code&gt;lazy&lt;/code&gt;. Leave third-party internals alone.&lt;/li&gt;
&lt;li&gt;Keep side-effect imports eager, or replace them with an explicit &lt;code&gt;register()&lt;/code&gt; call.&lt;/li&gt;
&lt;li&gt;Do not set &lt;code&gt;PYTHON_LAZY_IMPORTS=all&lt;/code&gt; in a job image unless the full test suite passes under it.&lt;/li&gt;
&lt;li&gt;If you must support 3.14 too, use &lt;code&gt;__lazy_modules__ = [&quot;pandas&quot;, ...]&lt;/code&gt; with normal &lt;code&gt;import&lt;/code&gt; statements. 3.15 treats those imports as lazy, and older versions just see a module variable and import eagerly (checked on 3.15.0 and 3.13.5 with &lt;code&gt;json&lt;/code&gt;).&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;2-tachyon-attach-to-the-stuck-job-by-pid-and-read-the-error-rate&quot;&gt;2. Tachyon: attach to the stuck job by PID, and read the error rate&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;The wrong assumption:&lt;/strong&gt; &quot;a sampling profiler is free and always right.&quot;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The mechanics:&lt;/strong&gt; &lt;code&gt;profiling.sampling&lt;/code&gt; (PEP 799, named Tachyon) reads the target process's memory from outside. It can &lt;code&gt;run&lt;/code&gt; a script, &lt;code&gt;attach&lt;/code&gt; to a PID for a time window, or &lt;code&gt;dump&lt;/code&gt; a single traceback-style snapshot of a live process. Output formats include pstats tables, collapsed stacks, flame graphs, Firefox Profiler and a live TUI. The &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.python.org/3.15/library/profiling.sampling.html&quot;&gt;docs&lt;/a&gt; list the constraints that matter in production:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Same Python minor version for profiler and target. If either side is a pre-release, both must be the exact same version.&lt;/li&gt;
&lt;li&gt;Permissions. On Linux, root, &lt;code&gt;CAP_SYS_PTRACE&lt;/code&gt;, or a Yama &lt;code&gt;ptrace_scope&lt;/code&gt; of 0. The docs note the default &lt;code&gt;ptrace_scope&lt;/code&gt; of 1 restricts ptrace to parent processes.&lt;/li&gt;
&lt;li&gt;Non-blocking mode (the default) never stops the target, but &quot;can occasionally produce incomplete or inconsistent stack traces&quot;. &lt;code&gt;--blocking&lt;/code&gt; pauses the target for each sample.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I wrote the classic slow nightly job: a pandas row-wise &lt;code&gt;apply&lt;/code&gt; with a per-row Python function, looping forever, and pretended I did not know why it was slow.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;slow_job.py&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;&quot;&quot;&quot;A 'stuck' nightly job: a row-wise pandas apply that nobody profiled.&quot;&quot;&quot;
import time

import numpy as np
import pandas as pd


def normalize_email(row):
    return row[&quot;email&quot;].strip().lower().replace(&quot;+spam&quot;, &quot;&quot;)


def enrich(df):
    df[&quot;email_norm&quot;] = df.apply(normalize_email, axis=1)
    return df


def main():
    rng = np.random.default_rng(0)
    n = 200_000
    df = pd.DataFrame({
        &quot;id&quot;: np.arange(n),
        &quot;email&quot;: [f&quot; User{i}+*Emails are not allowed* &quot; for i in rng.integers(0, 10**6, n)],
    })
    while True:
        enrich(df.copy())
        time.sleep(0.01)


if __name__ == &quot;__main__&quot;:
    main()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The first thing you want during an incident is &quot;what is it doing right now&quot;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;$ python -m profiling.sampling dump --blocking 440208
Stack dump for PID 440208, thread 440208 (main thread, has GIL; most recent call last):
  File &quot;slow_job.py&quot;, line 31, in &amp;lt;module&amp;gt;
    main()
  File &quot;slow_job.py&quot;, line 26, in main
    enrich(df.copy())
  File &quot;slow_job.py&quot;, line 14, in enrich
    df[&quot;email_norm&quot;] = df.apply(normalize_email, axis=1)
  ...
  File &quot;slow_job.py&quot;, line 10, in normalize_email
    return row[&quot;email&quot;].strip().lower().replace(&quot;+spam&quot;, &quot;&quot;)
  File &quot;.venv/lib/python3.15/site-packages/pandas/core/series.py&quot;, line 959, in Series.__getitem__
    return self._get_value(key)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No restart, no code change, and the culprit is on screen. On this kernel there is no Yama module (&lt;code&gt;/proc/sys/kernel/yama/ptrace_scope&lt;/code&gt; does not exist), so attaching as the same unprivileged user just worked. On a host with the default &lt;code&gt;ptrace_scope&lt;/code&gt; of 1, plan for root or &lt;code&gt;CAP_SYS_PTRACE&lt;/code&gt; before the incident, not during it.&lt;/p&gt;
&lt;p&gt;Then the part I did not expect. Without &lt;code&gt;--blocking&lt;/code&gt;, &lt;code&gt;dump&lt;/code&gt; failed 8 times out of 20 with &lt;code&gt;RuntimeError: Failed to parse initial frame in chain&lt;/code&gt;. With &lt;code&gt;--blocking&lt;/code&gt; it succeeded 20 out of 20. For &lt;code&gt;attach&lt;/code&gt;, the profiler prints an &lt;code&gt;Error rate&lt;/code&gt; line, the percentage of samples it could not read. I measured it on a fixed workload (median of six &lt;code&gt;apply&lt;/code&gt; rounds per run, 1 kHz default rate, three repetitions):&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Mode &lt;/th&gt;&lt;th&gt;Target round time &lt;/th&gt;&lt;th&gt;Error rate&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;No profiler &lt;/td&gt;&lt;td&gt;1170 / 1150 / 1186 ms &lt;/td&gt;&lt;td&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;attach&lt;/code&gt; (non-blocking, default) &lt;/td&gt;&lt;td&gt;1179 / 1180 / 1150 ms &lt;/td&gt;&lt;td&gt;67.8% / 62.0% / 63.7%&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;attach --blocking&lt;/code&gt; &lt;/td&gt;&lt;td&gt;1606 / 1760 / 1657 ms &lt;/td&gt;&lt;td&gt;1.5% / 1.6% / 1.3%&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Both halves of the docs are true at once. Non-blocking mode had no overhead I could measure on the target. And on this VM, 62 to 68% of its samples were unreadable in this test (46 to 50% in two earlier 5-second runs against slow_job.py, 38% against a pure-Python recursive function). Blocking mode was clean, and it made the job 37 to 53% slower while attached.&lt;/p&gt;
&lt;p&gt;Interpretation: treat a non-blocking profile like a survey with a response rate. If 60% of samples failed, the remaining 40% may still point at the right function (here the row-wise &lt;code&gt;apply&lt;/code&gt; dominated either way), but check the error rate before you trust percentages. Use &lt;code&gt;dump --blocking&lt;/code&gt; for a one-shot snapshot, where a single pause costs nothing, and keep &lt;code&gt;--blocking&lt;/code&gt; profiles short on anything user-facing.&lt;/p&gt;
&lt;p&gt;And the version rule bit me right away. A job started on the 3.15.0rc3 that the older uv had installed could not be inspected from 3.15.0:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;Can't attach to a pre-release Python interpreter from a process running a different Python version
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;From rc3 to rc3 it worked. Pin the profiler to the job image's exact interpreter, which in practice means running it from inside that image.&lt;/p&gt;
&lt;p&gt;The quieter observability change is &lt;a rel=&quot;nofollow&quot; href=&quot;https://peps.python.org/pep-0831/&quot;&gt;PEP 831&lt;/a&gt;: CPython is now built with frame pointers by default. What's New says this makes native stack unwinding &quot;faster and more reliable&quot; for system profilers, debuggers, crash analysis tools and eBPF-based tools. The uv build I used has &lt;code&gt;-fno-omit-frame-pointer&lt;/code&gt; in its &lt;code&gt;CFLAGS&lt;/code&gt;. The catch, in the release notes' words: &quot;A single native component built without frame pointers can break stack unwinding for the whole Python process.&quot; Your own Rust or C extensions need the same flags if you want mixed native and Python profiles.&lt;/p&gt;
&lt;h4 id=&quot;runbook-profiling-a-stuck-job&quot;&gt;Runbook: profiling a stuck job&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;python -m profiling.sampling dump --blocking PID&lt;/code&gt; first. One snapshot, one short pause.&lt;/li&gt;
&lt;li&gt;Then &lt;code&gt;python -m profiling.sampling attach -d 30 --flamegraph -o job.html PID&lt;/code&gt;, and read &lt;code&gt;Error rate&lt;/code&gt; before the flame graph.&lt;/li&gt;
&lt;li&gt;Run the profiler with the job's exact interpreter. A pre-release on either side means exact versions only.&lt;/li&gt;
&lt;li&gt;Decide now who is allowed &lt;code&gt;CAP_SYS_PTRACE&lt;/code&gt; on worker hosts, and where.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;3-pth-import-lines-become-start-files-auditable-not-safe&quot;&gt;3. &lt;code&gt;.pth&lt;/code&gt; import lines become &lt;code&gt;.start&lt;/code&gt; files: auditable, not safe&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;The wrong assumption:&lt;/strong&gt; &quot;a package can only run code when I import it.&quot;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The mechanics:&lt;/strong&gt; at startup the &lt;code&gt;site&lt;/code&gt; module reads every &lt;code&gt;.pth&lt;/code&gt; file in site-packages. Lines are paths to add to &lt;code&gt;sys.path&lt;/code&gt;, except lines that start with &lt;code&gt;import&lt;/code&gt;, which are executed with &lt;code&gt;exec()&lt;/code&gt;. Every Python process in that environment runs them: &lt;code&gt;python -c pass&lt;/code&gt;, pip, pytest, the notebook kernel, the scheduler. PEP 829 calls this &quot;a broad attack surface&quot;.&lt;/p&gt;
&lt;p&gt;This is not theoretical. The advisory for the compromised litellm 1.82.7 and 1.82.8 releases (&lt;a rel=&quot;nofollow&quot; href=&quot;https://osv.dev/vulnerability/PYSEC-2026-2&quot;&gt;PYSEC-2026-2&lt;/a&gt;, published March 24, 2026) describes credential-harvesting malware, and its references point to a &lt;code&gt;litellm_init.pth&lt;/code&gt; file inside the 1.82.8 package.&lt;/p&gt;
&lt;p&gt;And it is not only malware. &lt;code&gt;setuptools&lt;/code&gt; 84.0.0, installed fresh on the VM, still ships &lt;code&gt;distutils-precedence.pth&lt;/code&gt;, whose single line starts with &lt;code&gt;import os; ...&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;What &lt;a rel=&quot;nofollow&quot; href=&quot;https://peps.python.org/pep-0829/&quot;&gt;PEP 829&lt;/a&gt; and the &lt;a rel=&quot;nofollow&quot; href=&quot;https://docs.python.org/3.15/library/site.html&quot;&gt;site docs&lt;/a&gt; change in 3.15:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A new &lt;code&gt;name.start&lt;/code&gt; file lists entry points as &lt;code&gt;pkg.mod:callable&lt;/code&gt;. They run after all &lt;code&gt;.pth&lt;/code&gt; path lines are applied.&lt;/li&gt;
&lt;li&gt;If &lt;code&gt;name.start&lt;/code&gt; exists next to &lt;code&gt;name.pth&lt;/code&gt;, the &lt;code&gt;import&lt;/code&gt; lines in that &lt;code&gt;.pth&lt;/code&gt; are ignored.&lt;/li&gt;
&lt;li&gt;Without a matching &lt;code&gt;.start&lt;/code&gt;, &lt;code&gt;import&lt;/code&gt; lines still run in 3.15, and the deprecation message appears only with &lt;code&gt;-v&lt;/code&gt;. The docs say they will be &quot;silently ignored in Python 3.18 and 3.19&quot;, with a warning in 3.20.&lt;/li&gt;
&lt;li&gt;Errors on one line no longer abort the rest of the file. A failing entry point prints a traceback and startup continues.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;On the VM, with a demo &lt;code&gt;zz_demo.pth&lt;/code&gt; containing a path line and an &lt;code&gt;import&lt;/code&gt; line:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;$ python -c &quot;import sys; print('/tmp/extra_path_dir' in sys.path)&quot;
[zz_demo.pth] import line executed at startup
True
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;python -v&lt;/code&gt; shows the new deprecation notice for both the demo file and setuptools' file:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;import lines in .../site-packages/distutils-precedence.pth are deprecated, use entry points in a .../site-packages/distutils-precedence.start file instead.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After adding &lt;code&gt;zz_demo.start&lt;/code&gt; with one line, &lt;code&gt;zz_demo_boot:init&lt;/code&gt;, the &lt;code&gt;import&lt;/code&gt; line is suppressed and the entry point runs:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;import lines in .../zz_demo.pth are suppressed due to matching .../zz_demo.start file.
Executing entry point: zz_demo_boot:init from .../zz_demo.start
[zz_demo.start] entry point zz_demo_boot:init called
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here is the part to be precise about. A &lt;code&gt;.start&lt;/code&gt; file still runs code at every interpreter start. A malicious wheel can ship one just as easily as a &lt;code&gt;.pth&lt;/code&gt;. The PEP says so: &quot;The overall pre-start code execution attack surface is not eliminated by this PEP.&quot; What 3.15 gives you is a narrower, auditable shape: startup code is a named callable in an importable module, it goes through the import system so audit hooks (PEP 578) can see it, and &quot;you can tell by listing the files in the site-dir, exactly where arbitrary code execution occurs.&quot;&lt;/p&gt;
&lt;p&gt;That listing is the useful part, and you can do it today on any Python version:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;startup_audit.py (runs on Python 3.9+)&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;&quot;&quot;&quot;List everything that can run code at interpreter start in this environment.

Run it with the interpreter you want to audit:
    .venv/bin/python startup_audit.py
&quot;&quot;&quot;
import site
import sys
from pathlib import Path

dirs = list(dict.fromkeys(site.getsitepackages() + [site.getusersitepackages()]))
found = 0
for d in map(Path, dirs):
    if not d.is_dir():
        continue
    for f in sorted(d.glob(&quot;*.pth&quot;)):
        lines = f.read_text(encoding=&quot;utf-8-sig&quot;, errors=&quot;replace&quot;).splitlines()
        code = [l for l in lines if l.startswith((&quot;import &quot;, &quot;import\t&quot;))]
        if code:
            found += 1
            shadow = f.with_suffix(&quot;.start&quot;).exists()
            note = &quot;skipped on 3.15+ (matching .start), runs on older Pythons&quot; if shadow else &quot;RUNS on every start&quot;
            print(f&quot;{f}  [{note}]&quot;)
            for l in code:
                print(f&quot;    {l[:110]}&quot;)
    for f in sorted(d.glob(&quot;*.start&quot;)):
        found += 1
        print(f&quot;{f}  [.start entry points]&quot;)
        for l in f.read_text(encoding=&quot;utf-8-sig&quot;).splitlines():
            if l.strip() and not l.lstrip().startswith(&quot;#&quot;):
                print(f&quot;    {l.strip()}&quot;)
    for name in (&quot;sitecustomize.py&quot;, &quot;usercustomize.py&quot;):
        if (d / name).exists():
            found += 1
            print(f&quot;{d / name}  [runs on every start]&quot;)
print(f&quot;{sys.version.split()[0]}: {found} startup code hook(s) found&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Output in the demo environment (with setuptools installed):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;.../site-packages/distutils-precedence.pth  [RUNS on every start]
    import os; var = 'SETUPTOOLS_USE_DISTUTILS'; enabled = os.environ.get(var, 'local') == 'local'; enabled and __
.../site-packages/zz_demo.pth  [skipped on 3.15+ (matching .start), runs on older Pythons]
    import sys; sys.stderr.write(&quot;[zz_demo.pth] import line executed at startup\n&quot;)
.../site-packages/zz_demo.start  [.start entry points]
    zz_demo_boot:init
3.15.0: 3 startup code hook(s) found
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Most &lt;code&gt;.pth&lt;/code&gt; files are harmless. An editable install of a src-layout package with setuptools wrote a path-only &lt;code&gt;.pth&lt;/code&gt; on the VM, with no code at all. The ones that execute code are few enough to put on an allowlist.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;python -S&lt;/code&gt; disables both mechanisms (I checked), but it also skips site-packages entirely, so it is a debugging tool, not a production setting.&lt;/p&gt;
&lt;h4 id=&quot;runbook-startup-code&quot;&gt;Runbook: startup code&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;Run the audit in your image build and fail the build on any startup hook that is not on an allowlist.&lt;/li&gt;
&lt;li&gt;If you maintain a package with a &lt;code&gt;.pth&lt;/code&gt; &lt;code&gt;import&lt;/code&gt; line, move the code into a zero-argument function and add a matching &lt;code&gt;.start&lt;/code&gt; file. Keep the &lt;code&gt;.pth&lt;/code&gt; line as &lt;code&gt;import pkg.mod; pkg.mod.callable()&lt;/code&gt; while you still support 3.14.&lt;/li&gt;
&lt;li&gt;Treat a new &lt;code&gt;.start&lt;/code&gt; file in a dependency diff like a new &lt;code&gt;postinstall&lt;/code&gt; script in npm: a reason to read the release.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;what-to-do-this-week&quot;&gt;What to do this week&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;If you run... &lt;/th&gt;&lt;th&gt;Do this &lt;/th&gt;&lt;th&gt;Skip this&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Python CLIs, CI tooling, notebook kernels &lt;/td&gt;&lt;td&gt;Measure with &lt;code&gt;-X importtime&lt;/code&gt;, add &lt;code&gt;lazy&lt;/code&gt; to your entry point's heavy imports &lt;/td&gt;&lt;td&gt;&lt;code&gt;PYTHON_LAZY_IMPORTS=all&lt;/code&gt; in images&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Long batch jobs on VMs or Kubernetes &lt;/td&gt;&lt;td&gt;Install the exact job interpreter in the image, rehearse &lt;code&gt;dump --blocking&lt;/code&gt; and &lt;code&gt;attach&lt;/code&gt; on a staging job, sort out &lt;code&gt;CAP_SYS_PTRACE&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Trusting a profile without reading &lt;code&gt;Error rate&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Anything that installs from PyPI &lt;/td&gt;&lt;td&gt;Add the startup audit to the image build, review new &lt;code&gt;.pth&lt;/code&gt; and &lt;code&gt;.start&lt;/code&gt; files in dependency updates &lt;/td&gt;&lt;td&gt;Assuming &lt;code&gt;.start&lt;/code&gt; files make startup code safe&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Python-heavy loops or row-wise UDFs &lt;/td&gt;&lt;td&gt;Try &lt;code&gt;PYTHON_JIT=1&lt;/code&gt; in a benchmark, expect single digits &lt;/td&gt;&lt;td&gt;Counting on the JIT to fix a row-wise &lt;code&gt;apply&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The JIT will show up in your benchmarks as a few percent. Lazy imports, Tachyon and &lt;code&gt;.start&lt;/code&gt; files will show up in your incident timeline: the CLI that starts in 20 ms instead of 350, the stuck job you can inspect without a restart, and the list of files that run code before your &lt;code&gt;main()&lt;/code&gt; does. Upgrade for those, and measure the rest.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/30776/python-3-15-is-not-a-speed-release-for-data-teams-it-is-an-operations-release</guid>
<pubDate>Mon, 12 Oct 2026 07:13:52 +0000</pubDate>
</item>
<item>
<title>Instant at 10 Million Rows, Dead at a Billion: A Decision Framework for Billion-Row SQL</title>
<link>https://coderlegion.com/30403/instant-at-10-million-rows-dead-at-a-billion-a-decision-framework-for-billion-row-sql</link>
<description>&lt;p&gt;&lt;img src=&quot;https://unrivaled-starlight-d655be.netlify.app/blog/visuals/billion-row-sql-hero.png?v=2&quot; alt=&quot;Abstract brand-toned data pathway backdrop for the billion-row SQL decision framework.&quot;&gt;&lt;/p&gt;
&lt;p&gt;A query that returns in 40 milliseconds on ten million rows can time out completely on a billion. Same SQL, same schema, same database. I've watched a lot of teams hit that wall and go hunting for the one fix: a cleverer index, a bigger instance, whatever database was trending that month. There isn't one fix. There are two questions that decide almost everything. What engine class is this workload on — row-store, columnar, or distributed? And does the query's working set fit in memory, or does it spill to disk? Answer those honestly and most &quot;billion-row problems&quot; quietly resolve. Answer them wrong and no amount of tuning bails you out.&lt;/p&gt;
&lt;h3 id=&quot;the-query-that-was-instant-at-10-million-rows-and-timed-out-at-a-billion&quot;&gt;The query that was instant at 10 million rows and timed out at a billion&lt;/h3&gt;
&lt;p&gt;Here's the shape of it. It's a composite; I've seen it often enough that the specifics blur.&lt;/p&gt;
&lt;p&gt;A team builds a dashboard on their main OLTP database. One query carries it: count distinct users, grouped by day, over a date range. Against a 10-million-row staging copy it comes back instantly, everyone signs off, ship it. Months later production crosses a billion rows — roughly the &lt;a rel=&quot;nofollow&quot; href=&quot;https://tech.marksblogg.com/benchmarks.html&quot;&gt;1.1-billion-row scale in Mark Litwintschik's cross-engine benchmarks&lt;/a&gt; — and the identical query simply stops returning. On-call adds an index; nothing. A composite index; nothing. A bigger instance; somehow slightly worse. &quot;Throw it at Spark,&quot; one person says. &quot;Just use ClickHouse,&quot; says another. Nobody can explain why either would help. That's the real problem: everyone is arguing tactics with no framework for the decision.&lt;/p&gt;
&lt;p&gt;The query didn't regress. It's doing exactly what it always did. What changed is that the workload crossed two lines at once — it outgrew the engine class that suited it at 10M, and its sorts and hashes stopped fitting in memory. Name those two lines and the mystery turns into a decision you can defend.&lt;/p&gt;
&lt;h3 id=&quot;the-two-axis-framework&quot;&gt;The two-axis framework&lt;/h3&gt;
&lt;p&gt;When a query dies at scale, the reflex is to index harder. Sometimes that's right. At a billion rows it's usually the wrong axis. Two levers actually decide whether a query survives.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Axis 1 — Engine class.&lt;/strong&gt; Every database is built around one dominant access pattern, and that choice shapes everything downstream. A &lt;strong&gt;row-store OLTP&lt;/strong&gt; engine (PostgreSQL, MySQL) keeps whole rows together and excels at &quot;fetch or update this one record&quot; and narrow ranges. A &lt;strong&gt;columnar/OLAP&lt;/strong&gt; engine (ClickHouse, DuckDB, BigQuery) stores each column separately, so scanning and aggregating hundreds of millions of rows is the design point rather than the emergency. A &lt;strong&gt;distributed&lt;/strong&gt; engine (Citus, Spark, Cassandra) spreads data across nodes for horizontal scale, trading query flexibility to get it. These are peers, not rungs on a ladder; none is &quot;more advanced.&quot; Run a workload on the wrong one and you get a query that flies at 10M and dies at 1B.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Axis 2 — Memory budget.&lt;/strong&gt; Independent of engine class, every non-trivial query builds intermediate structures: sort buffers, hash tables for &lt;code&gt;GROUP BY&lt;/code&gt; and &lt;code&gt;DISTINCT&lt;/code&gt;, join tables. Fit those in RAM and the operation runs at memory speed. Overflow the memory the engine is allowed, and it spills to disk and falls off a cliff. PostgreSQL ships &lt;code&gt;shared_buffers&lt;/code&gt; at &lt;strong&gt;128MB&lt;/strong&gt; and &lt;code&gt;work_mem&lt;/code&gt; at just &lt;strong&gt;4MB&lt;/strong&gt;; the docs are explicit that a sort or hash exceeding &lt;code&gt;work_mem&lt;/code&gt; starts &quot;writing to temporary disk files&quot; (&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.postgresql.org/docs/current/runtime-config-resource.html&quot;&gt;PostgreSQL 18 — Resource Consumption&lt;/a&gt;, as of 2026). Four megabytes is nothing at a billion rows. This is the axis most write-ups skip, and it's why the same query is fast one day and glacial the next. The SQL didn't change; the data just got big enough to spill.&lt;/p&gt;
&lt;p&gt;The axes are orthogonal. You can pick the right engine class and still hit the memory cliff, or keep memory in check and still be on an engine that's wrong for the query's shape. So the move is: work out which quadrant you're actually in, then act on the axis that matches — change engine class, or change the memory budget — instead of adding another index to the axis that was never the problem.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://unrivaled-starlight-d655be.netlify.app/blog/visuals/v01-two-axis-framework.png&quot; alt=&quot;Two-axis decision framework: engine class (row-store, columnar, distributed) by memory budget (fits RAM vs spills to disk), with each quadrant's failure mode and the right move.&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;engine-class-1-row-store-oltp-great-at-point-lookups-brutal-on-full-table-aggregates&quot;&gt;Engine class 1: Row-store OLTP — great at point lookups, brutal on full-table aggregates&lt;/h3&gt;
&lt;p&gt;Row-stores are the workhorse for good reason. Ask for &quot;order #48213 and its line items&quot; or &quot;the last 50 events for this user&quot; and a row-store with the right index is hard to beat: it seeks to a handful of rows and returns. At 10M rows, the dashboard query was riding on that.&lt;/p&gt;
&lt;p&gt;Aggregates over the whole table are a different story, and the reason is baked into how MVCC row-stores stay correct under concurrency. Because different transactions can see different versions of a row, the engine keeps no cached count of how many rows a table has. An exact &lt;code&gt;count(*)&lt;/code&gt; therefore has to visit rows and check visibility — a sequential scan that grows linearly with the table.&lt;/p&gt;
&lt;p&gt;The numbers are blunt. In Joe Nelson's &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.citusdata.com/blog/2016/10/12/count-performance/&quot;&gt;Citus benchmark on counting&lt;/a&gt; (PostgreSQL 9.5.4, 2016 — treat the milliseconds as illustrative of the mechanism, which is unchanged today, not as current figures), an exact &lt;code&gt;count(*)&lt;/code&gt; ran &lt;strong&gt;85ms at 1M rows, 161ms at 2M, and 343ms at 4M&lt;/strong&gt;: a straight line, with the scan about &lt;strong&gt;88%&lt;/strong&gt; of the cost. Extend that to a billion rows and a single count is tens of seconds before any filtering. &lt;code&gt;count(1)&lt;/code&gt; doesn't rescue you; it measured slightly &lt;em&gt;slower&lt;/em&gt; (99ms vs 85ms at 1M), because Postgres special-cases the argument-free &lt;code&gt;count(*)&lt;/code&gt; while &lt;code&gt;count(1)&lt;/code&gt; re-checks the argument on every row. And this isn't Postgres being difficult — MySQL's InnoDB shares the MVCC property and also has no O(1) table count.&lt;/p&gt;
&lt;p&gt;When you don't need an exact number, stop asking for one. Every row-store keeps a maintained estimate for its planner, and you can read it directly. In PostgreSQL that's &lt;code&gt;pg_class.reltuples&lt;/code&gt; (the &lt;a rel=&quot;nofollow&quot; href=&quot;https://wiki.postgresql.org/wiki/Count_estimate&quot;&gt;Count-estimate pattern&lt;/a&gt;: &lt;code&gt;SELECT reltuples::bigint FROM pg_class WHERE oid = 'schema.table'::regclass&lt;/code&gt;). In the same benchmark the estimate returned in about &lt;strong&gt;0.3ms against 85ms&lt;/strong&gt; for the exact count at 1M rows — roughly &lt;strong&gt;280× faster&lt;/strong&gt;, and effectively constant time at any size. The catch is accuracy: it's an estimate and can't apply a &lt;code&gt;WHERE&lt;/code&gt; or &lt;code&gt;DISTINCT&lt;/code&gt;. For &quot;roughly how many rows&quot; on a dashboard, that's the difference between a billion-row scan and reading one catalog row.&lt;/p&gt;
&lt;p&gt;None of this means &quot;Postgres is slow.&quot; It means a full-table aggregate is off-design for a row-store. Approximate counts, materialized rollups, and summary tables all soften it. But if scans and aggregates are your &lt;em&gt;dominant&lt;/em&gt; pattern, you're fighting the engine class — and that's your first crossover signal.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://unrivaled-starlight-d655be.netlify.app/blog/visuals/v02-aggregate-cliff.png&quot; alt=&quot;Exact count(*) rises linearly with table size while an approximate reltuples read stays flat and constant-time — roughly a 280x gap.&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Exact `count(&lt;/em&gt;)&lt;code&gt; vs the &lt;/code&gt;reltuples` estimate. Source: Citus / Joe Nelson, PostgreSQL 9.5.4, 2016 — illustrative of the mechanism, not current figures.*&lt;/p&gt;
&lt;h3 id=&quot;engine-class-2-columnar-olap-where-scans-and-aggregates-are-the-design-point&quot;&gt;Engine class 2: Columnar / OLAP — where scans and aggregates are the design point&lt;/h3&gt;
&lt;p&gt;If aggregates are killing your row-store, what's built for them, and how much faster is it really? This is where the framework gets counter-intuitive.&lt;/p&gt;
&lt;p&gt;Columnar engines store each column contiguously instead of packing whole rows. A scan-and-aggregate query that touches 3 of 51 columns reads only those 3 off disk; the other 48 are never opened. And because a column holds one type with heavy repetition, it compresses hard, so there's less to read to begin with. Column pruning plus compression is why &quot;scan a billion rows and aggregate&quot; is routine here instead of an incident.&lt;/p&gt;
&lt;p&gt;The headline number: Litwintschik's &lt;a rel=&quot;nofollow&quot; href=&quot;https://tech.marksblogg.com/benchmarks.html&quot;&gt;1.1-billion-row taxi benchmark&lt;/a&gt; (1.1B rows, 51 columns, ~500GB uncompressed; page updated March 2024) runs one analytical query across dozens of engines. On Query 1, a single desktop ran &lt;strong&gt;ClickHouse in 0.088s&lt;/strong&gt; and &lt;strong&gt;DuckDB in 0.498s&lt;/strong&gt;. The same query took &lt;strong&gt;2.36s on a 21-node Spark cluster&lt;/strong&gt;, &lt;strong&gt;3.54s on 21-node Presto&lt;/strong&gt;, and &lt;strong&gt;1–3s on BigQuery&lt;/strong&gt;. Postgres with the &lt;code&gt;cstore_fdw&lt;/code&gt; columnar extension took &lt;strong&gt;152–368s&lt;/strong&gt; across the four queries, and plain SQLite's format took &lt;strong&gt;31,193s on Query 1 — about 8.7 hours&lt;/strong&gt;, included to show what the wrong engine class actually costs.&lt;/p&gt;
&lt;p&gt;A single desktop beat a 21-node cluster on a 1.1-billion-row scan by more than an order of magnitude. That one result is the whole framework in miniature. It isn't that ClickHouse or DuckDB is &quot;the best database&quot; — it's that for a scan-aggregate workload a columnar engine is in the right quadrant while the cluster pays coordination overhead it doesn't need at this size. (Caveats: one benchmark, one hardware generation, recent desktop CPUs for the fastest single-node runs. Trust the shape of the gap, not the exact milliseconds.)&lt;/p&gt;
&lt;p&gt;Compression is the other half, and it's an economics lever as much as a speed one. ClickBench — the ClickHouse team's &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/ClickHouse/ClickBench&quot;&gt;scan/aggregate benchmark&lt;/a&gt; across 60+ systems — publishes a live &quot;Data Size&quot; column so you can compare on-disk footprint on the same ~100M-row dataset. Columnar layouts routinely pack this kind of wide, repetitive data down by a large multiple versus a row-store, though the exact ratio moves with engine, codec, and data, so read the live figure for the systems you're weighing rather than trusting a headline number. (ClickBench is ClickHouse-run, so treat it as a scan-workload reference, not a neutral verdict.)&lt;/p&gt;
&lt;p&gt;The honest counter-point: the same columnar engine that crushes the scan is the wrong tool for high-QPS single-row writes and point lookups. Insert a row and you touch every column store; update a field and compression works against you. The wide-column and key-value stores built for that job — Cassandra, ScyllaDB — sit at the opposite pole: excellent at point reads and writes on a known partition key, and structurally unfit for ad-hoc full-table aggregates. Cassandra's &lt;a rel=&quot;nofollow&quot; href=&quot;https://cassandra.apache.org/doc/latest/cassandra/developing/cql/dml.html&quot;&gt;CQL docs&lt;/a&gt; say it plainly: CQL &quot;only allows select queries that don't involve a full scan of all partitions,&quot; and forcing one needs &lt;code&gt;ALLOW FILTERING&lt;/code&gt;, whose performance the docs call &quot;unpredictable.&quot; A store modeled query-first around the partition key isn't a scan-aggregate engine, which is exactly why ClickBench lists Cassandra and ScyllaDB among the systems it couldn't benchmark on that suite. Right engine, wrong job, in both directions.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://unrivaled-starlight-d655be.netlify.app/blog/visuals/v03-cross-engine-scan.png&quot; alt=&quot;Litwintschik 1.1-billion-row Query 1: a single desktop running ClickHouse or DuckDB beats a 21-node Spark/Presto cluster; SQLite is the wrong-tool extreme (log scale).&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Same query, same 1.1B-row dataset, across engine classes. Source: Mark Litwintschik, 1.1 Billion Taxi Rides benchmarks, page updated March 2024.&lt;/em&gt;&lt;/p&gt;
&lt;h3 id=&quot;engine-class-3-distributed-horizontal-scale-and-the-fan-out-ceiling&quot;&gt;Engine class 3: Distributed — horizontal scale, and the fan-out ceiling&lt;/h3&gt;
&lt;p&gt;When one node genuinely can't hold the data or serve the writes, distributed engines spread the work across many. The mental model most people carry — more nodes, more speed — holds right up until it doesn't, and knowing where that line sits is the difference between a defensible cluster and a single box with a network bill attached.&lt;/p&gt;
&lt;p&gt;The line: does the query push down to the shards, or force a reshuffle? Distribute by &lt;code&gt;tenant_id&lt;/code&gt; and group by &lt;code&gt;tenant_id&lt;/code&gt;, and each node computes its slice while the coordinator stitches the results together. That's pushdown, and it parallelizes beautifully. But the moment a query needs data spread across nodes — a &lt;code&gt;count(DISTINCT)&lt;/code&gt; on a non-distribution column, or a join across the distribution boundary — the engine has to reshuffle rows between workers first. That shuffle is the ceiling.&lt;/p&gt;
&lt;p&gt;The &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.citusdata.com/blog/2016/10/12/count-performance/&quot;&gt;Citus benchmark&lt;/a&gt; puts numbers on it (100M rows, 8 nodes, 32 shards; PG 9.5.4, 2016 — again mechanism-illustrative). A plain &lt;code&gt;count(*)&lt;/code&gt; across the cluster ran about &lt;strong&gt;1.2s&lt;/strong&gt;. A &lt;code&gt;count(DISTINCT)&lt;/code&gt; on the distribution column, which pushes down, ran about &lt;strong&gt;3.4s&lt;/strong&gt;. On a non-distribution column it can't push down, and pulling every row back to the coordinator is, in Nelson's words, &quot;really no better than counting on a single database instance … plus high network overhead.&quot; An HLL approximate distinct ran &lt;strong&gt;3.2–3.8s&lt;/strong&gt; — the row-store's approximation trick again, now buying a way around the shuffle. This generalizes well past Citus: in Spark the expensive stage is almost always the shuffle, and distributed query design is mostly the craft of keeping the hot queries pushed down.&lt;/p&gt;
&lt;p&gt;So distributed isn't a &quot;scale&quot; button. It's a trade — horizontal capacity for query flexibility and operational complexity. Align your access pattern with the distribution key and it's transformative. Keep needing cross-node data and you've bought nodes &lt;em&gt;and&lt;/em&gt; network latency to arrive back at roughly single-box performance. That mismatch is its own crossover signal, and the one most likely to be discovered the hard way in production.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://unrivaled-starlight-d655be.netlify.app/blog/visuals/v04-fanout-pushdown.png&quot; alt=&quot;Two distributed query paths: one pushes down to shards and merges at the coordinator; the other forces a cross-node shuffle that collapses to a single box plus network overhead.&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;the-memory-spill-axis-the-one-most-write-ups-skip&quot;&gt;The memory / spill axis — the one most write-ups skip&lt;/h3&gt;
&lt;p&gt;Now the second axis, and the reason &quot;just add an index&quot; so often does nothing. A query can be on exactly the right engine class and still fall off a cliff, because its sorts and hash tables outgrew the memory the engine was allowed and spilled to disk. This is engine-independent: columnar and distributed engines build the same buffers and spill the same way when they overflow their limits. Postgres is just the easiest place to watch it, because its docs name the knob.&lt;/p&gt;
&lt;h4 id=&quot;the-spill-cliff&quot;&gt;The spill cliff&lt;/h4&gt;
&lt;p&gt;At the default 4MB &lt;code&gt;work_mem&lt;/code&gt; (&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.postgresql.org/docs/current/runtime-config-resource.html&quot;&gt;PostgreSQL docs&lt;/a&gt;), a &lt;code&gt;GROUP BY&lt;/code&gt;, &lt;code&gt;DISTINCT&lt;/code&gt;, or &lt;code&gt;ORDER BY&lt;/code&gt; that builds something larger switches from an in-memory hash or sort to an external merge that writes temp files to disk. The &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.citusdata.com/blog/2016/10/12/count-performance/&quot;&gt;Citus benchmark&lt;/a&gt; shows the size of the drop (PG 9.5.4, 2016; mechanism unchanged per the current docs): at default &lt;code&gt;work_mem&lt;/code&gt;, &lt;code&gt;count(DISTINCT)&lt;/code&gt; on an integer column averaged &lt;strong&gt;743ms&lt;/strong&gt;, but on a text column &lt;strong&gt;31,747ms&lt;/strong&gt; — a &lt;strong&gt;~43× gap&lt;/strong&gt; — with &lt;code&gt;Sort Method: external merge Disk&lt;/code&gt; in the plan. Give the operation enough &lt;code&gt;work_mem&lt;/code&gt; to stay in RAM and the integer case drops to about &lt;strong&gt;372ms&lt;/strong&gt; as an in-memory &lt;code&gt;HashAggregate&lt;/code&gt;. That in-RAM-versus-disk boundary is the axis. Stay on the right side and the query flies; cross it and it collapses.&lt;/p&gt;
&lt;p&gt;This is exactly why indexing didn't help the composite team. An index changes how you &lt;em&gt;find&lt;/em&gt; rows; it does nothing for a &lt;code&gt;GROUP BY&lt;/code&gt;/&lt;code&gt;DISTINCT&lt;/code&gt; whose result set is too big for &lt;code&gt;work_mem&lt;/code&gt;. The lever there is memory, or a smaller working set — not another index.&lt;/p&gt;
&lt;h4 id=&quot;when-to-re-architect-instead-of-tune&quot;&gt;When to re-architect instead of tune&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;work_mem&lt;/code&gt; is allocated per operation per query, and the docs warn total usage &quot;could be many times&quot; that value under load, so raising it globally just trades a spill problem for an out-of-memory one. Past a point the honest move is structural. PostgreSQL's &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.postgresql.org/docs/current/ddl-partitioning.html&quot;&gt;partitioning guidance&lt;/a&gt; offers a clean rule: partition &quot;when the size of the table [would] exceed the physical memory of the database server.&quot; Pruning then skips whole partitions — but it isn't free. The planner handles &quot;up to a few thousand&quot; partitions well only if pruning leaves few, since &quot;each partition requires its metadata to be loaded into the local memory of each session that touches it.&quot; Thousands of poorly-pruned partitions is its own problem.&lt;/p&gt;
&lt;h4 id=&quot;one-more-trap-deep-pagination&quot;&gt;One more trap: deep pagination&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;LIMIT ... OFFSET n&lt;/code&gt; gets slower the further you page. As Markus Winand explains in &lt;a rel=&quot;nofollow&quot; href=&quot;https://use-the-index-luke.com/no-offset&quot;&gt;&quot;No Offset&quot;&lt;/a&gt;, the database &quot;fetches and drops&quot; all &lt;code&gt;n&lt;/code&gt; preceding rows before returning the page, and results drift when rows are inserted between requests. A keyset (seek) query filters on the last-seen sort key instead and stays roughly constant-time however deep you go. Page latency that climbs with depth is a spill-adjacent signal, not a hardware one.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://unrivaled-starlight-d655be.netlify.app/blog/visuals/v05-spill-cliff.png&quot; alt=&quot;The spill cliff: an in-RAM hash aggregate vs an external merge to disk, and the collapse back to memory speed when work_mem is raised.&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;count(DISTINCT) at default vs raised work_mem. Source: Citus / Joe Nelson, PostgreSQL 9.5.4, 2016 — illustrative of the mechanism, not current figures.&lt;/em&gt;&lt;/p&gt;
&lt;h3 id=&quot;the-crossover-signal-checklist-change-engine-class-don-t-add-another-index&quot;&gt;The crossover-signal checklist: change engine class, don't add another index&lt;/h3&gt;
&lt;p&gt;This is the piece worth saving. A framework's job is to tell you &lt;em&gt;when&lt;/em&gt; you've hit the boundary and it's time to move on an axis instead of tuning further. Eight signals I watch for, each tied to the axis it moves. Two or more firing means you're past tuning and into an engine-class or memory-budget decision.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Scans and aggregates dominate the workload&lt;/strong&gt; (not point lookups). → Engine class, toward &lt;strong&gt;columnar&lt;/strong&gt;. The row-store aggregate cliff; indexing won't fix an off-design pattern.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The working set exceeds server RAM.&lt;/strong&gt; → Memory. Postgres's own partition threshold; partition, re-architect, or move to columnar/distributed.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sorts and &lt;code&gt;DISTINCT&lt;/code&gt;s keep spilling even after tuning &lt;code&gt;work_mem&lt;/code&gt;.&lt;/strong&gt; → Memory. You've hit the ceiling of per-query memory; the working set is the problem.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pagination or scan latency rises with position.&lt;/strong&gt; → Memory/scan; switch to keyset/seek before switching engines.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A single node has hit its write or IO ceiling.&lt;/strong&gt; → Engine class, toward &lt;strong&gt;distributed&lt;/strong&gt;. The one genuine &quot;add nodes&quot; signal.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Key &lt;code&gt;DISTINCT&lt;/code&gt;s or joins need data from several nodes&lt;/strong&gt; (a shuffle). → A warning on the distributed axis: without a distribution-key redesign, more nodes ≈ one box plus network.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;You need thousands of partitions to stay fast.&lt;/strong&gt; → Memory/metadata; the per-partition cost says a columnar or distributed store fits the data better.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;You're keeping 500GB+ on one box for analytics.&lt;/strong&gt; → Engine class plus economics; columnar compression and distributed storage costs start driving the decision.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;What the list deliberately doesn't do is name a winning database. Each signal points to an axis and a direction, because the right specific engine depends on your query shapes, your team's ops maturity, and your budget. &quot;Change engine class&quot; is an architectural decision with a reason attached — something you can hand a skeptical peer or a budget owner instead of &quot;the internet said use ClickHouse.&quot;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://unrivaled-starlight-d655be.netlify.app/blog/visuals/v06-crossover-checklist.png&quot; alt=&quot;The eight crossover signals, each tagged with the axis it moves (engine class or memory budget) and the recommended direction.&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;cost-and-economics-for-the-decision-makers&quot;&gt;Cost and economics for the decision-makers&lt;/h3&gt;
&lt;p&gt;If you own the stack decision, latency is half the case; the half that gets budget approved is cost and defensibility. The benchmarks reframe cleanly as money.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A single big box can be cheaper &lt;em&gt;and&lt;/em&gt; faster than a cluster.&lt;/strong&gt; The &lt;a rel=&quot;nofollow&quot; href=&quot;https://tech.marksblogg.com/benchmarks.html&quot;&gt;Litwintschik result&lt;/a&gt; — one desktop-class node beating a 21-node cluster on a 1.1B-row scan — reads louder as a cost story than a speed one: 21 nodes carry ~21× the compute plus coordination, networking, and a standing ops burden, and still lose on this workload. For &quot;big but not planetary&quot; analytics, the first move is often the right engine class on one well-sized box, not a cluster. Distributed earns its cost when you truly exceed single-node capacity or need the availability.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Compression is a storage-cost lever.&lt;/strong&gt; A columnar layout shrinks wide, repetitive data by a large multiple (measure it for your engines from &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/ClickHouse/ClickBench&quot;&gt;ClickBench's &quot;Data Size&quot; column&lt;/a&gt; — it varies by engine and codec), which is a recurring cut in bytes stored and, in a warehouse, fewer dollars per query scanned.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Per-query billing cuts both ways.&lt;/strong&gt; A serverless warehouse (BigQuery ran 1–3s per query here) fits spiky, ad-hoc analytics where an idle cluster would bleed money, and fits badly for a high-frequency dashboard hammering one aggregate, where a right-sized columnar box wins on total cost.&lt;/p&gt;
&lt;p&gt;Every price and benchmark here will rotate — pricing shifts, hardware moves the single-node ceiling, vendor benchmarks get re-run. Don't anchor the decision to one dated figure. The framework doesn't rotate: two axes, eight signals. If a price moves, re-run the comparison.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://unrivaled-starlight-d655be.netlify.app/blog/visuals/v07-cost-ops-profile.png&quot; alt=&quot;Cost and operations profile per engine class: relative storage cost, node count, operational burden, query flexibility, and best-fit workload.&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;build-it-yourself&quot;&gt;Build it yourself&lt;/h3&gt;
&lt;p&gt;The framework is cheap until you feel it on your own data. Three projects, beginner to advanced. Each names a real tool (links verified reachable in 2026, but repos move — check before you lean on them).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Project 1 — Count your table three ways (~30 min).&lt;/strong&gt; Feel the row-store aggregate cliff on real data. You'll need a Postgres or MySQL table with 10M+ rows and &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/duckdb/duckdb&quot;&gt;DuckDB&lt;/a&gt;. Run &lt;code&gt;EXPLAIN ANALYZE SELECT count(*) FROM your_table;&lt;/code&gt; and note the time and the &lt;code&gt;Seq Scan&lt;/code&gt;; then &lt;code&gt;SELECT reltuples::bigint FROM pg_class WHERE oid = 'schema.your_table'::regclass;&lt;/code&gt;; then get a slice into columnar form — export CSV (&lt;code&gt;COPY your_table TO 'slice.csv' (FORMAT csv)&lt;/code&gt;) and load it into DuckDB, or read the table directly with &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/duckdb/pg_duckdb&quot;&gt;pg_duckdb&lt;/a&gt; — and &lt;code&gt;count(*)&lt;/code&gt; there. &lt;strong&gt;Done when&lt;/strong&gt; you can state your own numbers for all three and point to the &lt;code&gt;Seq Scan&lt;/code&gt;. Stretch: add &lt;code&gt;GROUP BY day&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Project 2 — Reproduce the spill cliff (~1–2 hrs).&lt;/strong&gt; Watch a query cross the RAM→disk boundary. You'll need Postgres and a 1M+ row table with an integer and a text column (the NYC-taxi or ClickBench sets work). At default &lt;code&gt;work_mem&lt;/code&gt;, run &lt;code&gt;EXPLAIN ANALYZE SELECT count(DISTINCT int_col) FROM t;&lt;/code&gt; then the same on &lt;code&gt;text_col&lt;/code&gt;, and confirm &lt;code&gt;Sort Method: external merge Disk&lt;/code&gt; on the slow one. &lt;code&gt;SET work_mem = '1GB';&lt;/code&gt;, re-run both. &lt;strong&gt;Done when&lt;/strong&gt; you've captured your own before/after ratio and seen the plan flip to in-memory &lt;code&gt;HashAggregate&lt;/code&gt;. Stretch: chart latency as you step &lt;code&gt;work_mem&lt;/code&gt; from 4MB to 1GB and find your own cliff edge.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Project 3 — Run a slice of the 1.1B benchmark (~half a day).&lt;/strong&gt; Reproduce the crossover on your hardware. You'll need the same big table in a row-store and a columnar engine, plus the &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/ClickHouse/ClickBench&quot;&gt;ClickBench loaders&lt;/a&gt; (60+ DBMS setup scripts) and &lt;a rel=&quot;nofollow&quot; href=&quot;https://tech.marksblogg.com/benchmarks.html&quot;&gt;Litwintschik's posts&lt;/a&gt; for reference. Load an identical scan-aggregate table into each, time one representative query on both, and record the ratio and your hardware. &lt;strong&gt;Done when&lt;/strong&gt; you reproduce a &amp;gt;100× gap — or can explain from the plans why yours differs. Stretch: add a distributed engine and build a query that forces a shuffle to find your own fan-out ceiling.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://unrivaled-starlight-d655be.netlify.app/blog/visuals/v08-project-ladder.png&quot; alt=&quot;Build-it-yourself project ladder: beginner to advanced, each rung mapped to an axis — aggregate cliff, spill cliff, cross-engine crossover.&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;what-to-do-monday-morning&quot;&gt;What to do Monday morning&lt;/h3&gt;
&lt;p&gt;The whole thing, compressed. When a query dies at scale, stop asking &quot;which index?&quot; and ask the two questions that decide it. Engine class: is the dominant shape point-lookups (row-store), scans and aggregates (columnar), or genuinely bigger than one node (distributed)? Memory budget: does the working set fit in RAM, or is it spilling? Then walk the eight signals; two or more means the move is on an axis, not another index. There's no single answer to &quot;how do I handle a billion rows&quot; — the answer is a decision you can write down and defend.&lt;/p&gt;
&lt;p&gt;So go run it. Take your biggest table this week and do Project 1: &lt;code&gt;count(*)&lt;/code&gt;, then &lt;code&gt;reltuples&lt;/code&gt;, then a columnar count in &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/duckdb/duckdb&quot;&gt;DuckDB&lt;/a&gt;. Watch where the plan shows a &lt;code&gt;Seq Scan&lt;/code&gt; and how far apart the three numbers land. Want the full crossover? The &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/ClickHouse/ClickBench&quot;&gt;ClickBench repo&lt;/a&gt; has loaders for 60+ engines, so you can time the same scan on a row-store and a columnar engine side by side. Bring back your own numbers — the framework only lands once the cliff is yours.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/30403/instant-at-10-million-rows-dead-at-a-billion-a-decision-framework-for-billion-row-sql</guid>
<pubDate>Fri, 09 Oct 2026 06:03:06 +0000</pubDate>
</item>
<item>
<title>Understanding Parquet File Format</title>
<link>https://coderlegion.com/30266/understanding-parquet-file-format</link>
<description>&lt;h3 id=&quot;what-is-a-file-format-&quot;&gt;What is a File Format?&lt;/h3&gt;
&lt;p&gt;A file format defines how data is structured and stored so that software can read and write it consistently.&lt;/p&gt;
&lt;p&gt;Think of it as the container for your data. Just like music comes in MP3, WAV, or FLAC, data files also come in different formats depending on the use case.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Common data file formats:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;CSV – Simple text-based, rows separated by commas (easy to use, but&lt;br&gt;
large in size and no compression).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;JSON – Flexible, hierarchical (good for APIs but verbose).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Avro – Row-based binary format, compact, supports schema evolution &lt;br&gt;
(common in streaming).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;ORC – Columnar format optimized for Hadoop and Hive.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Parquet – Columnar format designed for analytics and compression &lt;br&gt;
efficiency.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;introduction-to-parquet&quot;&gt;Introduction to Parquet&lt;/h3&gt;
&lt;p&gt;If you work with data, you have probably heard the term Parquet come up in conversations about data lakes, analytics, or cloud storage. But what exactly is it, and why does everyone seem to love it?&lt;/p&gt;
&lt;h3 id=&quot;the-everyday-analogy-organizing-your-groceries&quot;&gt;The Everyday Analogy: Organizing Your Groceries&lt;/h3&gt;
&lt;p&gt;Imagine you run a grocery delivery service, and you have to store all the items customers ordered.&lt;/p&gt;
&lt;p&gt;You could store them in two ways:&lt;/p&gt;
&lt;p&gt; &lt;strong&gt;&lt;em&gt;Row-based storage (like CSV):&lt;/em&gt;&lt;/strong&gt; &lt;/p&gt;
&lt;p&gt;You put one of each item from an order into a bag: milk, bread, apples, and pasta all together. To find all the bread you have sold, you have to open every bag and dig around to see whether it is inside.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;em&gt;Column-based storage (like Parquet):&lt;/em&gt;&lt;/strong&gt;&lt;br&gt;
Instead of mixing, you group similar items together:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;One box for all the milk&lt;/li&gt;
&lt;li&gt;One box for all the bread&lt;/li&gt;
&lt;li&gt;One box for all the apples&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you need all the bread you have sold, you simply open the bread box, no need to touch anything else.&lt;/p&gt;
&lt;p&gt;That is Parquet in action, organized storage that makes retrieval faster and more efficient.&lt;/p&gt;
&lt;h3 id=&quot;the-technical-view&quot;&gt;The Technical View&lt;/h3&gt;
&lt;p&gt;Parquet is an open-source, columnar storage format developed by Twitter and Cloudera, now an Apache project. It is:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Self-describing – Metadata in the file footer stores schema, number&lt;br&gt;
of rows, and statistics.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Highly compressed – Supports Snappy, Gzip, ZSTD, and more.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Structured internally into:&lt;br&gt;
&lt;strong&gt;Row Groups&lt;/strong&gt; → subsets of rows (horizontal partitions).&lt;br&gt;
&lt;strong&gt;Column Chunks&lt;/strong&gt; → data for each column inside a row group (vertical partitions).&lt;br&gt;
&lt;strong&gt;Pages&lt;/strong&gt; → the smallest unit of storage, making reads efficient.&lt;br&gt;
&lt;strong&gt;Magic Number (PAR1)&lt;/strong&gt; – Ensures the file is recognized as Parquet.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This hybrid layout balances efficient columnar reads with manageable write performance.&lt;/p&gt;
&lt;h3 id=&quot;why-choose-parquet-over-other-formats-&quot;&gt;Why Choose Parquet Over Other Formats?&lt;/h3&gt;
&lt;p&gt;Now that we understand Parquet, let us see why it stands out compared to other formats.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;CSV – Human-readable but inefficient; no compression or schema&lt;br&gt;
support.&lt;/li&gt;
&lt;li&gt;JSON – Flexible for nested data, but verbose and large in size.&lt;/li&gt;
&lt;li&gt;Avro – Great for row-based streaming but not optimized for analytics.&lt;/li&gt;
&lt;li&gt;ORC – Similar to Parquet, optimized for Hadoop and Hive workloads.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Parquet vs the rest:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Much smaller file sizes due to efficient compression&lt;/li&gt;
&lt;li&gt;Faster query performance with column pruning and predicate pushdown&lt;/li&gt;
&lt;li&gt;Widely supported across Spark, Hive, Presto, Trino, BigQuery,&lt;br&gt;
Snowflake, AWS Athena, and more&lt;/li&gt;
&lt;li&gt;Ideal for data lakes and analytical workloads&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;how-data-is-written-in-parquet&quot;&gt;How Data is Written in Parquet&lt;/h3&gt;
&lt;p&gt;When writing data into a Parquet file, the process follows a layered structure:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=6044621059311078360&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt; &lt;strong&gt;Rows are collected into Row Groups&lt;/strong&gt;            &lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Instead of writing each record one by one (like CSV), Parquet groups&lt;br&gt;
multiple rows together into a Row Group (horizontal partition).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Within each Row Group, data is split into Column Chunks&lt;/strong&gt; &lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Each column’s values are stored together → Column A values go into &lt;br&gt;
Column Chunk A, Column B values into Column Chunk B, and so on.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;This preserves the columnar storage advantage.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Column Chunks are broken into Pages&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Pages are the smallest unit of storage in Parquet.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Data Pages → contain the actual column data&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Dictionary Pages → optionally store dictionaries used for dictionary encoding&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Optional Page Indexes can provide additional information for&lt;br&gt;
efficiently locating and skipping pages:&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Column Index → stores statistics such as min/max values for pages&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Offset Index → stores the locations and sizes of pages&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Metadata is written into the File Footer&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Contains schema, row count, statistics (min/max per column), and row&lt;br&gt;
group metadata.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Makes Parquet a self-describing format.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Magic Number (PAR1) is added&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;At the start and end of the file → ensures the file can be validated&lt;br&gt;
as a Parquet file.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;how-data-is-read-from-a-parquet-file&quot;&gt;How Data is Read from a Parquet File&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=17966435297597599916&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;File Footer Read First&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Parquet does not scan the entire file right away. &lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;It jumps directly to the footer, where all the metadata is stored.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Metadata tells the engine:&lt;br&gt;
Schema (what columns exist)&lt;br&gt;
Number of rows&lt;br&gt;
Statistics for each column in each row group (min/max)&lt;br&gt;
Row group and column chunk offsets (where the data lives in the file)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;➡️ &lt;em&gt;This helps the engine decide which parts of the file to actually read.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Predicate Pushdown &amp;amp; Column Pruning&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;If the query only needs certain columns (SELECT name, age), only&lt;br&gt;
those column chunks are accessed.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If the query has filters (WHERE age &amp;gt; 30), Parquet uses min/max stats&lt;br&gt;
in metadata to skip entire row groups that don’t match.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;➡️ &lt;em&gt;This reduces I/O drastically compared to row-based formats.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Reading Row Groups&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;The engine opens only the relevant row groups.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Inside each row group, it locates the column chunks needed for the&lt;br&gt;
query.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Decompression &amp;amp; Decoding Pages&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Column chunks are split into pages.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Each page is decompressed (Snappy, Gzip, ZSTD, etc.) and decoded&lt;br&gt;
(Dictionary, RLE, Delta encoding).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Pages are read into memory one at a time, which is efficient.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Reconstructing Results&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Once the engine reads, decompresses, and decodes the relevant column&lt;br&gt;
data, it reconstructs the requested rows and columns.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The engine materializes the final output as a result set for the&lt;br&gt;
query.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;parquet-file-size-small-vs-large&quot;&gt;Parquet File Size: Small vs Large&lt;/h3&gt;
&lt;p&gt;The size of each Parquet file matters a lot for performance.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Too Small Files (&amp;lt; 50 MB each)&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Too much metadata and file-open operations.&lt;/li&gt;
&lt;li&gt;Increases overhead for query engines (Spark, Presto, Hive).&lt;/li&gt;
&lt;li&gt;Known as the small files problem.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Too Large Files (&amp;gt; 1 GB each)&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Slows down reads and reduces parallelism.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;💡 &lt;strong&gt;Ideal File Size:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;256 MB to 1 GB for most big data frameworks.&lt;/li&gt;
&lt;li&gt;Large enough to compress well, small enough for parallelism.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;how-data-types-affect-compression-and-file-size-&quot;&gt;How Data Types Affect Compression (and File Size)&lt;/h3&gt;
&lt;p&gt;Parquet’s efficiency is heavily influenced by data type selection:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Strings vs Integers:&lt;/strong&gt; Integers compress far better than storing&lt;br&gt;
numbers as strings.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Decimals vs Floats:&lt;/strong&gt; Decimals (fixed precision) often compress better.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Categorical Data:&lt;/strong&gt; Repeated values (like “USA”) compress extremely&lt;br&gt;
well with dictionary encoding.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Timestamps&lt;/strong&gt;: Storing as INT64 (epoch values) is more efficient than strings.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;em&gt;Tip: Choose the most precise and smallest data type possible to save space and improve query speed.&lt;/em&gt;&lt;/p&gt;
&lt;h3 id=&quot;encoding-strategies-used-in-parquet&quot;&gt;Encoding Strategies used in Parquet&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=3891040807583055239&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Run Length Encoding (RLE)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;How it works:&lt;/strong&gt; Stores consecutive repeated values as a single value + a count.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Columns with long runs of the same value (e.g., flags, booleans, country/state codes).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Benefit:&lt;/strong&gt; Extremely efficient for repetitive data, reduces size drastically.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Example:

Raw → A A A A B B C C C
RLE → (A,4), (B,2), (C,3)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Dictionary Encoding&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;How it works:&lt;/strong&gt; Builds a dictionary of unique values in a column and replaces each value with a small index reference.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Low-cardinality columns (few unique values, but repeated often).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Benefit:&lt;/strong&gt; Saves space when values repeat often, lookups are fast.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Example:

Raw → USA, USA, CAN, USA, CAN, MEX
Dictionary → {0: USA, 1: CAN, 2: MEX}
Encoded → 0, 0, 1, 0, 1, 2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Delta Encoding&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;How it works:&lt;/strong&gt; Stores the difference (delta) between consecutive values instead of storing full values.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; Sequential/numeric data like timestamps, IDs, counters.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Benefit:&lt;/strong&gt; Great for compressing increasing sequences, works well with integers and dates.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Example:

Raw → 100, 105, 110, 115
Delta → 100, +5, +5, +5
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;parquet-optimization-amp-best-practices&quot;&gt;Parquet Optimization &amp;amp; Best Practices&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Predicate Pushdown&lt;/strong&gt; – Enable in your query engine to filter row groups&lt;br&gt;
at scan time.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Compression Codecs&lt;/strong&gt; &lt;br&gt;
Snappy: Fast, decent ratio&lt;br&gt;
Gzip: Higher compression, slower&lt;br&gt;
ZSTD: Good balance&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Avoid Small Files&lt;/strong&gt; – Merge into larger files to reduce metadata&lt;br&gt;
overhead.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Row Group Size&lt;/strong&gt; – Balance between compression (bigger row groups) and&lt;br&gt;
parallelism (smaller ones).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Encoding Strategies &lt;/strong&gt;&lt;br&gt;
Dictionary Encoding for low-cardinality strings&lt;br&gt;
Run-Length Encoding for repeated values&lt;br&gt;
Delta Encoding for incremental numbers like timestamps&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Sort Data&lt;/strong&gt; – Sorting on commonly filtered columns improves compression&lt;br&gt;
and predicate pushdown.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;ACID Transactions&lt;/strong&gt; – Parquet itself does not provide ACID, use table formats like Delta Lake, Iceberg, or Hudi for transactional&lt;br&gt;
guarantees.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;conclusion-1&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;Parquet has become a default choice in the modern data stack because it offers the best of both worlds like compact storage and fast analytics.&lt;/p&gt;
&lt;p&gt;By understanding how it works internally (row groups, column chunks, pages) and following best practices (ideal file sizes, data types, compression, and encodings), you can save costs, improve query speeds, and build scalable pipelines.&lt;/p&gt;
&lt;h3 id=&quot;further-reading&quot;&gt;Further Reading&lt;/h3&gt;
&lt;p&gt;For a deeper dive into Parquet’s internals, file layout, and optimizations, check out “&lt;a rel=&quot;nofollow&quot; href=&quot;https://vutr.substack.com/p/the-overview-of-parquet-file-format&quot;&gt;The Overview of Parquet File Format&lt;/a&gt;” by Vu Trinh. It’s a clear and insightful read for data engineers looking to go beyond the basics.&lt;/p&gt;
&lt;p&gt;In short, Parquet is the “&lt;strong&gt;organized pantry&lt;/strong&gt;” of the data world: efficient, structured, and always ready when you need it.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/30266/understanding-parquet-file-format</guid>
<pubDate>Thu, 08 Oct 2026 11:42:51 +0000</pubDate>
</item>
<item>
<title># Power BI Data Modelling: A Great Path to Great Analysis</title>
<link>https://coderlegion.com/30210/power-bi-data-modelling-a-great-path-to-great-analysis</link>
<description>&lt;h2 id=&quot;data-modeling-in-power-bi&quot;&gt;Data Modeling in Power BI&lt;/h2&gt;
&lt;h3 id=&quot;introduction-1&quot;&gt;Introduction&lt;/h3&gt;
&lt;p&gt;At a time when data is rapidly generated by different activities ranging from experiments to business, more often do we get data in different formats and structures. The data may or may not be related depending on the source. Related data in different formats and structure need to be re-organized to establish existing relationships for accurate analysis and reporting. To establish data model relationships and properties we need advanced tools that can connect to these sources. Then load the different datasets from source, and perform modelling to connect the links.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Data Modeling&lt;/em&gt; therefore, becomes very important. &lt;em&gt;Data Modeling is the process of defining the data structure, properties, and relationships within a &lt;em&gt;Data Model&lt;/em&gt;.&lt;/em&gt; Through organizing tables and columns, defining relationships between tables in the model, setting cardinality and cross-filter direction we achieve the best model which supports accurate calculations and reporting.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;A Data Model&lt;/em&gt; is the logical representation of how data is structured and related within the data model.&lt;/p&gt;
&lt;p&gt;Before modeling ensure that your data is clean and up to date, ready for modeling.&lt;/p&gt;
&lt;h5 id=&quot;why-data-modeling&quot;&gt;Why Data Modeling&lt;/h5&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;Improved Performance and Scalability&lt;/em&gt; - join optimization, filters, and aggregations lead to a good model that is efficient and reliable. However, poorly designed models with complex relationships and redundant records cause delays, making queries slow.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;Accuracy and Consistency&lt;/em&gt; - Guarantees the accuracy and consistency of the data used, making it reliable in reporting and drawing precise  insights and improved decision making.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;Supports Data Exploration&lt;/em&gt; - Users can use hierarchies and drill-down paths to support efficient data exploration.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;Ease of Maintenance&lt;/em&gt; - Any changes in one model or report are reflected everywhere in real time enabling reusability and scalability, reducing the effort required to maintain reports and models as the business grows.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;components-of-power-bi-data-model&quot;&gt;Components of Power BI Data Model&lt;/h3&gt;
&lt;p&gt;a. &lt;em&gt;Tables and Sources&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Tables&lt;/em&gt; are the raw building blocks of data models. They contain fields and records of particular data from different sources.&lt;br&gt;
&lt;em&gt;Sources,&lt;/em&gt; where we get our data from can vary depending on the context. We can get our data from SQL Database servers, Excel files, Data warehouse, SharePoint, and Power BI Dataflows.&lt;/p&gt;
&lt;h5 id=&quot;designing-source-tables&quot;&gt;Designing Source Tables&lt;/h5&gt;
&lt;p&gt;i. &lt;em&gt;Fact Tables&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Fact tables contain quantitative data, numerical data that can be aggregated and analyzed. Examples the &lt;em&gt;Sales, Orders, or Invoices.&lt;/em&gt; This information is about specific events, business transaction, or measurements at the core of the data model.&lt;/p&gt;
&lt;p&gt;Fact tables keeps one record for each transaction or event, includes only keys and metrics, and they avoid storing descriptive attributes.&lt;/p&gt;
&lt;p&gt;Fact tables contain:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;Foreign Key(FK)&lt;/em&gt; - a link to the Dimension table i.e. DateKey, ProductKey, e.t.c.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;Measures(Numeric values)&lt;/em&gt; - Quantitative data you can aggregate. i.e. Quantity, UnitPrice, TotalAmount etc.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;Degenerate dimensions&lt;/em&gt; - Descriptive data that does not warrant its dimension table. i.e. OrderNumber, InvoiceNumber.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;ii. &lt;em&gt;Dimension Tables&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Dimension tables provide context or additional details to the data in &lt;em&gt;Fact Tables&lt;/em&gt;. They contains fewer rows while fact tables can contain a large number of rows and they are on growing.&lt;/p&gt;
&lt;p&gt;Relationship are used to establish links and connections between the fact and dimension tables in Power BI.&lt;/p&gt;
&lt;p&gt;Dimension table contain:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;Primary Key(PK)&lt;/em&gt; - Unique identifier e.g.  &lt;code&gt;ProductID&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;Descriptive attributes&lt;/em&gt; - Text, categories, hierarchies e.g. &lt;code&gt;ProductName&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;Hierarchy Columns&lt;/em&gt; - Columns that form drill-down paths e.g. Country -&amp;gt; City -&amp;gt; Town.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;Natural key&lt;/em&gt; - original identifier from the source system e.g. &lt;code&gt;ProductCode&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;iii. &lt;em&gt;Reference and Lookup Tables&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;They are small but critical tables in supporting the integrity and interpretation of data. They support additional mappings  or categorizations. Example: DiscountTypes or StatusCodes.&lt;/p&gt;
&lt;h5 id=&quot;loading-data&quot;&gt;Loading Data&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;Open Power BI&lt;/li&gt;
&lt;li&gt;On the Ribbon &lt;code&gt;Home&lt;/code&gt; tab, Click on &lt;code&gt;Get Data&lt;/code&gt; from the &lt;em&gt;Data&lt;/em&gt; group option.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/u4k0yh2aocmcpqkzobxw.png&quot; alt=&quot;Get Data Icon&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;From the drop down list, choose the source of your data. i.e. &lt;code&gt;Text/CSV&lt;/code&gt; if your data is text or comma separated file.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/e7iqcv5qwsoa15ffkif8.png&quot; alt=&quot;Data Source&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Choose the path to your file i.e. &lt;code&gt;C:/Users/User/Downloads/file.csv&lt;/code&gt; and click on &lt;code&gt;Open&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Click on &lt;code&gt;Connect&lt;/code&gt; on the Power Query editor window.&lt;/li&gt;
&lt;li&gt;Your data is extracted to Power Query for cleaning. Click on &lt;code&gt;Transform - Data&lt;/code&gt; to clean explore and clean it on the Power Query editor.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/spze6r2i7ekktnp3xsy5.png&quot; alt=&quot;Query Editor Window&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/3wsmqrfwt2dov26sik8o.png&quot; alt=&quot;Cleaning Window&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Finally, on the Power Query Editor Window Ribbon, Click on &lt;code&gt;Close &amp;amp; Apply&lt;/code&gt; from the &lt;code&gt;Home&lt;/code&gt; tab.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/vgs0p47x4kyktm1t4i4p.png&quot; alt=&quot;Close and Apply Button&quot;&gt;&lt;/p&gt;
&lt;p&gt;Changes are loaded and applied to Power BI. We will load all our &lt;a rel=&quot;nofollow&quot; href=&quot;https://mavenanalytics.io/data-playground/toy-store-e-commerce-database&quot;&gt;Toy Store E-Commerce Database&lt;/a&gt; data set for modeling using the procedure above. The data is already clean, we are going to use it for demonstrating relationships in data modeling.&lt;/p&gt;
&lt;p&gt;b. &lt;em&gt;Relationships&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Relationships are an important aspect of data modeling in Power BI. Relationships are for establishing connections and links between tables of a data model. They allow related tables to communicate with each other. They are the link between &lt;em&gt;Fact Tables&lt;/em&gt; and &lt;em&gt;Dimension Tables&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Relationships determine how data in the model is filtered, aggregated, combined, and visualized. They're critical to the accuracy and performance of queries in Power BI.&lt;/p&gt;
&lt;h6 id=&quot;cardinality&quot;&gt;Cardinality&lt;/h6&gt;
&lt;p&gt;Cardinality type defines each relationship in a model. &lt;em&gt;Cardinality&lt;/em&gt; refers to the number of unique values in one table related to the number of unique values in another.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Types of Cardinality in Relationships&lt;/em&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;One-to-One(1:1)&lt;/em&gt; - Every single record in one table is related to another single record in another table. Commonly used when splitting a large table for performance and security reasons.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;One-to-Many(1:N)&lt;/em&gt; - Refers to when a single record in one table is related to many records in another table(s). Very common type of relationship in Power BI models. The &lt;em&gt;Fact&lt;/em&gt; table is on the &lt;em&gt;Many&lt;/em&gt; side while the &lt;em&gt;Dimension&lt;/em&gt; table is on the &lt;em&gt;One&lt;/em&gt; side when linking &lt;em&gt;Fact&lt;/em&gt; tables and &lt;em&gt;Dimension&lt;/em&gt; tables.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;Many-to-Many(N:N)&lt;/em&gt; - Refers to when many records in the first table are related to many other records in the second table.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h6 id=&quot;cross-filter-directions&quot;&gt;Cross Filter Directions&lt;/h6&gt;
&lt;p&gt;Used to determine the direction which filters propagate.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Single&lt;/em&gt; - When applied, only one of the tables in the relationship can filter data across the other table. Filtering data on the other table does not affect the data shown on the first table.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Bi-Directional&lt;/em&gt; - When applied, data can be filtered on either direction between the tables. Filtering data in one table affects data shown on the other table and vice versa. It is less often used and can impact performance negatively and create ambiguous filters.&lt;/p&gt;
&lt;p&gt;Cross-filter options are largely dependent on the cardinality type.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Cardinality &lt;/th&gt;&lt;th&gt;Cross-Filter Options&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;1:1 &lt;/td&gt;&lt;td&gt;Bi-Directional&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;1:* &lt;/td&gt;&lt;td&gt;Single or Bi-Directional&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;em&gt;:&lt;/em&gt; &lt;/td&gt;&lt;td&gt;Single or Bi-Directional&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;em&gt;Table 1: A table showing cardinality and their cross-filter options.&lt;/em&gt;&lt;/p&gt;
&lt;h4 id=&quot;creating-a-relationship&quot;&gt;Creating a Relationship&lt;/h4&gt;
&lt;p&gt;To create relationships, first you need to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Identify the matching columns(keys).&lt;/li&gt;
&lt;li&gt;Choose the right cardinality.&lt;/li&gt;
&lt;li&gt;Decide on the Filter direction.&lt;/li&gt;
&lt;/ul&gt;
&lt;h5 id=&quot;the-model-view-interface&quot;&gt;The Model View Interface&lt;/h5&gt;
&lt;p&gt;To manage relationships in Power BI Desktop:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Click the Model icon in the left sidebar (the icon with three connected boxes)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/4fw45c9o13jvbf7fimho.png&quot; alt=&quot;Model&quot;&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;You'll see all your tables displayed as boxes with their columns listed&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/3jhqw6m20jhiip0t3mn9.png&quot; alt=&quot;Tables in the Model&quot;&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Lines between tables represent relationships&lt;/li&gt;
&lt;li&gt;The line style indicates cardinality: 1 on one end and * on the many end&lt;/li&gt;
&lt;/ol&gt;
&lt;h5 id=&quot;auto-detect-relationships&quot;&gt;Auto-Detect Relationships&lt;/h5&gt;
&lt;p&gt;When you load tables, Power BI automatically recognizes columns with same data type and name and creates relationships. However, you shoul always create relationships manually or review those created automatically. &lt;/p&gt;
&lt;p&gt;&lt;em&gt;Just like in our case, Power BI automatically creates relationships between tables in our model. We will review them.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;When auto-detect works well:&lt;/em&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Column names match exactly (e.g., ProductID in both tables)&lt;/li&gt;
&lt;li&gt;Data types are compatible&lt;/li&gt;
&lt;li&gt;Values actually correspond between the tables&lt;/li&gt;
&lt;/ul&gt;
&lt;h5 id=&quot;creating-relationships-manually&quot;&gt;Creating Relationships Manually&lt;/h5&gt;
&lt;p&gt; Using the Managing Relationship Dialog&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;On the &lt;code&gt;Home&lt;/code&gt; tab  → &lt;code&gt;Manage Relationships&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/mqre7817gnrqv31vn7ng.png&quot; alt=&quot;Manage Relationships&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Click &lt;code&gt;New&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/k5owghexz7iiyclqvdli.png&quot; alt=&quot;New Relationship&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Select the first table and column, &lt;code&gt;From Table&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Select the second table and column, &lt;code&gt;To Table&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Configure cardinality and cross-filter direction&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/images/specifytables.png&quot; alt=&quot;Configure Relationship Tables&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Click &lt;code&gt;Save&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h6 id=&quot;step-by-step-for-creating-relationships-for-our-toy-store-e-commerce-database&quot;&gt;Step by Step for Creating Relationships for our Toy Store E-Commerce Database&lt;/h6&gt;
&lt;ol&gt;
&lt;li&gt;Load all tables: &lt;pre&gt;&lt;code&gt; ```
     products
     orders
     order_items
     order_item_refunds
     website_sessions
     website_pageviews
 ````
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;Open Model view and delete any autodetected relationship.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;em&gt;To delete a relationship:&lt;/em&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;In Model view, right-click on the line joining the &lt;code&gt;products&lt;/code&gt; and &lt;code&gt;order_items&lt;/code&gt; tables&lt;/li&gt;
&lt;li&gt;From the options displayed, Click on &lt;code&gt;Delete&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/i7ete2c79b1mp6sm1od3.png&quot; alt=&quot;Delete Relationship&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Save your work&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;Create the following relationships:&lt;/li&gt;
&lt;/ol&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;From Fact Table &lt;/th&gt;&lt;th&gt;To Dimension Table &lt;/th&gt;&lt;th&gt;Cardinality &lt;/th&gt;&lt;th&gt;Cross-Filter&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;products[product_id] &lt;/td&gt;&lt;td&gt;order_items[product_id] &lt;/td&gt;&lt;td&gt;One-to-Many &lt;/td&gt;&lt;td&gt;Single&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;orders[order_id] &lt;/td&gt;&lt;td&gt;order_items[order_id] &lt;/td&gt;&lt;td&gt;One-to-Many &lt;/td&gt;&lt;td&gt;Single&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;orders[order_id] &lt;/td&gt;&lt;td&gt;order_item_refunds[order_id] &lt;/td&gt;&lt;td&gt;One-to-Many &lt;/td&gt;&lt;td&gt;Single&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;website_sessions[website_session_id] &lt;/td&gt;&lt;td&gt;orders[website_session_id] &lt;/td&gt;&lt;td&gt;One-to-One &lt;/td&gt;&lt;td&gt;Bi-Direction&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;website_sessions[website_session_id] &lt;/td&gt;&lt;td&gt;website_pageviews[website_session_id] &lt;/td&gt;&lt;td&gt;One-to-Many &lt;/td&gt;&lt;td&gt;Single&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;order_items[order_item_id] &lt;/td&gt;&lt;td&gt;order_item_refunds[order_item_id] &lt;/td&gt;&lt;td&gt;One-to-one &lt;/td&gt;&lt;td&gt;Bi-Direction&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;em&gt;Table 2: Table showing relationships to be created.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Creating a relationship between the &lt;code&gt;products&lt;/code&gt; and &lt;code&gt;order_items&lt;/code&gt; tables&lt;/em&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;On the &lt;code&gt;Home&lt;/code&gt; tab  → &lt;code&gt;Manage Relationships&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Click &lt;code&gt;New&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Select the &lt;code&gt;products&lt;/code&gt; table and specify the column &lt;code&gt;product_id&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Select the &lt;code&gt;order_items&lt;/code&gt; table and specify the column &lt;code&gt;product_id&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Set cardinality to &lt;code&gt;One-to-Many&lt;/code&gt; and the cross-filter as &lt;code&gt;Single&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/tikqfcb3k8mogoo03723.png&quot; alt=&quot;Specify Table Configurations&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Click &lt;code&gt;Save&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/qdh3npai9fx6pj2b4jk5.png&quot; alt=&quot;Established Relationship&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Complete creating all the other relationships following the procedure. You will get something similar to:&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/fbqtjzxxq3zha4emmxfp.png&quot; alt=&quot;All Relationships&quot;&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Verify by checking that filters from any dimension table correctly filter the fact table.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;conclusion-2&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;Data modeling is very crucial to building powerful reports and visualizations. It is key to great analysis because it establishes relationships, links, and connections existing in a data model making it wholesome and complete. Further, it improves query performance increasing efficiency and reliability of queries. Correct data modeling can lead to accurate reporting and result reliability, thus informed decision making. To view the complete model and data for your practice on GitHub visit &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/MakoriNyachaki/Toy-Store-E-Commerce&quot;&gt;Toy Store E-Commerce&lt;/a&gt;.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/30210/power-bi-data-modelling-a-great-path-to-great-analysis</guid>
<pubDate>Thu, 08 Oct 2026 03:59:58 +0000</pubDate>
</item>
<item>
<title>Data Preprocessing for Machine Learning</title>
<link>https://coderlegion.com/28690/data-preprocessing-for-machine-learning</link>
<description>&lt;h3 id=&quot;introduction-2&quot;&gt;Introduction&lt;/h3&gt;
&lt;p&gt;Most ML tutorials jump straight into models and algorithms. But here is what no one tells beginners: the model isn't where the magic happens. The data preparation is.&lt;/p&gt;
&lt;p&gt;Think of it this way. You wouldn't hand a toddler a jigsaw puzzle with missing pieces, warped edges, and pieces from three different puzzles mixed, and then blame the toddler for not solving it. That's exactly what happens when we feed raw, messy data into a machine learning model and wonder why it performs terribly.&lt;/p&gt;
&lt;p&gt;Data preprocessing is the act of preparing raw data so a model can actually make sense of it. It covers everything from creating useful features to handling gaps in the data to making sure numbers play fair with each other. &lt;/p&gt;
&lt;p&gt;In this article, we will walk through four pillars of preprocessing: &lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Feature Engineering  &lt;/li&gt;
&lt;li&gt;Handling Missing Values&lt;/li&gt;
&lt;li&gt;Scaling&lt;/li&gt;
&lt;li&gt;Normalization&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;By the end, you won’t just know what these terms mean. You will understand why they exist, when they matter, and what breaks when you skip them.&lt;/p&gt;
&lt;h3 id=&quot;why-preprocessing-matters&quot;&gt;Why Preprocessing Matters&lt;/h3&gt;
&lt;p&gt;A machine learning model is only as good as the data it receives.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=8107761430198165874&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Real-world data is messy. It has gaps. It has inconsistencies. Some numbers are in the thousands, others are tiny decimals. Some information is buried inside other information and needs to be extracted before it becomes useful.&lt;/p&gt;
&lt;p&gt;Preprocessing bridges raw chaos and structured learning.&lt;/p&gt;
&lt;h3 id=&quot;the-four-pillars-of-data-preprocessing&quot;&gt;The Four Pillars of Data Preprocessing&lt;/h3&gt;
&lt;p&gt;Each pillar addresses a unique issue in your data. Neglecting any one of them will compromise your model's performance.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=7921805959609566445&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2 id=&quot;1-feature-engineering-&quot;&gt;1. Feature Engineering:&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Teaching the Machine What to Notice&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Child:&lt;/strong&gt; A child walks into a pet store. They see animals everywhere. But their parents point and say, “Look at the size. Look at the fur. Does it have a tail?” The parent isn’t changing the animals; they are teaching the child what to pay attention to.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Machine:&lt;/strong&gt; Raw data often contains information, but not in a form the model can use directly. Feature engineering is the process of creating, transforming, or selecting the right input variables (features) so the model can actually find patterns.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why it matters:&lt;/strong&gt; A column named “date_of_birth” is not useful for a model predicting insurance risk. However, a new column called “age,” derived from that date, provides valuable information for the model.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=12098858162351416374&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2 id=&quot;what-feature-engineering-looks-like-in-practice&quot;&gt;What Feature Engineering Looks Like in Practice&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Creating new features from existing ones:&lt;/strong&gt; Given a “timestamp” column, derive “hour_of_day,” “day_of_week,” and “is_weekend.” This provides a model predicting restaurant traffic with useful signals, rather than a raw timestamp it cannot interpret.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Combining Features:&lt;/strong&gt; You have “house_length” and “house_width.” Neither feature alone provides a complete picture. Multiply them to derive “house_area,” a single feature that captures information not conveyed by either feature individually.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Encoding Categories:&lt;/strong&gt; A column contains the values “Red,” “Blue,” and “Green.” Since a model cannot interpret these labels directly, they are converted into numerical representations that the model can process. One-hot encoding, for example, represents “Red” as [1, 0, 0], “Blue” as [0, 1, 0], and so on.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Takeaway:&lt;/strong&gt; Feature engineering is the art of translating human knowledge into a language the model can learn from. The model finds patterns, but you decide what it gets to look at.&lt;/p&gt;
&lt;h2 id=&quot;2-handling-missing-values-&quot;&gt;2. Handling Missing Values:&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Filling in the Gaps&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Child:&lt;/strong&gt; A child is reading a picture book, and one page is torn out. They don’t throw away the entire book. They might guess what happened on that page based on the story before and after. Or they skip it and keep reading.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Machine:&lt;/strong&gt; Real-world datasets almost always have missing values. Sensors fail. People skip survey questions. Records get corrupted. The model needs a strategy: fill the gap, remove the gap, or flag the gap.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why it matters:&lt;/strong&gt; Most ML algorithms cannot process empty cells. If you don’t handle missing values, your model either crashes or silently learns the wrong thing.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=17081745216160758294&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2 id=&quot;the-three-strategies-for-missing-data&quot;&gt;The Three Strategies for Missing Data&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Strategy 1&lt;/strong&gt; — Remove it (Drop): If only a tiny fraction of your data has gaps, sometimes the simplest move is to drop those rows. Like a child skipping the torn page, you lose a little, but the rest of the story still makes sense. &lt;em&gt;Risk&lt;/em&gt;: If too many rows have gaps, you lose valuable data.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Strategy 2&lt;/strong&gt; — Fill it (Impute): Replace the missing value with something reasonable. Common approaches include using the mean (average) for numerical data, the mode (most frequent value) for categorical data, or the median if your data has extreme outliers. Like the child guessing what happened on the missing page based on context.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Strategy 3&lt;/strong&gt; — Flag it (Indicator): Create a new column that says “this value was missing” (1 or 0). This way, the model knows the gap existed and can learn whether the absence of data is itself a signal. Sometimes, a patient skipping a question on a health survey is more informative than any answer they could have given.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Takeaway:&lt;/strong&gt; Missing data is not necessarily a problem; rather, it presents a decision point. The appropriate approach depends on the extent of the missing data, the reasons for its absence, and the requirements of the model.&lt;/p&gt;
&lt;h2 id=&quot;3-feature-scaling-&quot;&gt;3. Feature Scaling:&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Making the Numbers Play Fair&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Child:&lt;/strong&gt; Imagine two children comparing their collections. One child has 3 seashells. The other has 3,000 stickers. If you ask, “Who has the bigger collection?” the answer seems obvious, but is it fair? The scales are completely different. To compare meaningfully, you would need to put both collections on the same measuring system.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Machine:&lt;/strong&gt; ML models that calculate distances or gradients (like KNN, SVM, or neural networks) are heavily influenced by the magnitude of numbers. A feature ranging from 0 to 1,000,000 will dominate a feature ranging from 0 to 1, even if the smaller feature matters more.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why it matters:&lt;/strong&gt; Without scaling, large-magnitude features can overwhelm smaller ones and make them irrelevant. The model doesn’t know that “salary in dollars” and “years of experience” should carry equal weight; it only sees big numbers and small numbers.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=13186248235850778995&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2 id=&quot;the-two-main-scaling-methods&quot;&gt;The Two Main Scaling Methods&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Min-Max Scaling (Normalization to a range):&lt;/strong&gt; Scales all values into a fixed range, typically 0 to 1. The formula: (value - min) / (max - min). It’s like converting each child’s collection into a percentage of their personal maximum. It works well when you need bounded values and your data doesn’t have extreme outliers.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Standardization (Z-score Scaling):&lt;/strong&gt; Centers data at 0 and scales it to a standard deviation of 1. Formula: (value - mean) / standard_deviation. Like grading on a curve, each student’s score shows how far it is from average. This works well with outliers or when an algorithm assumes normally distributed data.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Takeaway:&lt;/strong&gt; Scaling does not alter the information conveyed by your data. Instead, it adjusts each feature's relative influence, ensuring no single feature disproportionately dominates the others.&lt;/p&gt;
&lt;h2 id=&quot;4-normalization-&quot;&gt;4. Normalization:&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Reshaping the Distribution&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Child:&lt;/strong&gt; A teacher asks the class to rate how much they liked a movie on a scale of 1 to 10. One child always gives everything a 9 or 10. Another child’s ratings spread evenly from 1 to 10. To compare their opinions fairly, you would need to adjust for each child’s personal rating style and their distribution.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Machine:&lt;/strong&gt; Normalization transforms the shape of your data’s distribution. Even after scaling, your data might be heavily skewed, with most values clustered on one side with a long tail. Many ML algorithms perform better when the data follows a more symmetric, bell-curve-like distribution.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why it matters:&lt;/strong&gt; Skewed distributions can mislead models. If 95% of your income data clusters between $20,000 and $80,000 but a few values shoot up to $200,000, the model might overfit to those extremes or underperform on most of the data.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=333244522848360790&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h2 id=&quot;common-normalization-techniques&quot;&gt;Common Normalization Techniques&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Log Transformation:&lt;/strong&gt; Take the logarithm of each value. This compresses the long tail and spreads out the clustered values. Extremely effective for income data, population data, or anything with exponential growth patterns.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Box-Cox Transformation:&lt;/strong&gt; A more flexible approach that identifies the optimal power transformation to make the data as closely resemble a normal distribution as possible. The method automatically determines the most appropriate transformation.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Quantile Transformation:&lt;/strong&gt; Maps values to their percentile ranks to transform the data into a specified distribution, typically uniform or normal. This highly aggressive approach ensures the desired output distribution but may distort relationships among nearby values.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Takeaway:&lt;/strong&gt; Normalization concerns the shape of your data, not the scale. It ensures your data’s distribution doesn’t secretly undermine your model’s assumptions.&lt;/p&gt;
&lt;h2 id=&quot;scaling-vs-normalization-&quot;&gt;Scaling vs. Normalization:&lt;/h2&gt;
&lt;p&gt;These two terms get mixed up constantly,&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Scaling&lt;/strong&gt; adjusts the range or magnitude of your features. It answers: “How big are these numbers relative to each other?”&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Normalization&lt;/strong&gt; adjusts the distribution shape of your features. It answers: “What does the spread of these numbers look like?”&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=9351026043128994798&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Both are often necessary. First, scale the features to comparable ranges; then, normalize them if the distribution is skewed.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Think of it this way:&lt;/strong&gt; scaling sets the volume; normalization tunes the equalizer.&lt;/p&gt;
&lt;h3 id=&quot;the-preprocessing-decision-tree&quot;&gt;The Preprocessing Decision Tree&lt;/h3&gt;
&lt;p&gt;How do you decide what preprocessing to apply?&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=12727321746549158431&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;conclusion-3&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;Data preprocessing is rarely celebrated or featured in headlines. Nevertheless, it is, without exaggeration, where most substantive machine learning work takes place.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Feature engineering decides what the model sees.&lt;/li&gt;
&lt;li&gt;Missing value handling decides how gaps are managed.&lt;/li&gt;
&lt;li&gt;Scaling ensures no feature unfairly dominates.&lt;/li&gt;
&lt;li&gt;Normalization reshapes distributions so algorithms can work as&lt;br&gt;
designed.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These are essential steps and should not be omitted. They form the foundation for every model, prediction, and result, making thorough data preparation critical.&lt;/p&gt;
&lt;p&gt;If you step back, the pattern is strikingly familiar: whether it’s a child learning to organize their toy box or a model learning to classify images, learning quality depends entirely on preparation quality.&lt;/p&gt;
&lt;p&gt;Ensure the data is accurate, and the model may exceed expectations. Without adequate preparation, no algorithm can compensate.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/28690/data-preprocessing-for-machine-learning</guid>
<pubDate>Sat, 26 Sep 2026 07:51:45 +0000</pubDate>
</item>
<item>
<title>How I Used Wren AI to Let My Marketing Team Query a Star Schema Without Writing SQL</title>
<link>https://coderlegion.com/27479/how-i-used-wren-ai-to-let-my-marketing-team-query-a-star-schema-without-writing-sql</link>
<description>&lt;p&gt;If you've worked in data engineering long enough, you know the drill.&lt;/p&gt;
&lt;p&gt;Someone from marketing pings you on a Monday morning: &quot;Hey, can you pull campaign spend by ad set for last week?&quot;&lt;/p&gt;
&lt;p&gt;You run the query. Send it. Five minutes later: &quot;Can we also add impressions?&quot;&lt;/p&gt;
&lt;p&gt;You run it again.&lt;/p&gt;
&lt;p&gt;This loop doesn't end. It just gets longer.&lt;/p&gt;
&lt;p&gt;At my previous job, I decided to break it. Here's exactly how I did it — using Microsoft Fabric, PostgreSQL, and Wren AI — and what I learned along the way.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Stack&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Before I get into the how, here's what I was working with:&lt;/p&gt;
&lt;p&gt;Source: Meta Ads data in Microsoft Fabric (Delta tables, Gold layer)&lt;br&gt;
Target: Local PostgreSQL (localhost:5432, schema: gold)&lt;br&gt;
Semantic layer: Wren AI&lt;br&gt;
Goal: Let business users ask natural language questions against a proper star schema — no SQL required&lt;br&gt;
&lt;strong&gt;Step 1: Getting the Data Out of Fabric&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;My first instinct was a direct Fabric → Postgres connection via Spark JDBC. Clean, automated, elegant.&lt;/p&gt;
&lt;p&gt;Didn't work. Fabric is cloud. My Postgres was localhost. Cloud can't reach your laptop. That's just how networking works.&lt;/p&gt;
&lt;p&gt;So I went with the practical option: export as CSV, import locally.&lt;/p&gt;
&lt;p&gt;Here's what the Gold layer looked like:&lt;br&gt;
&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=2159561977368109225&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Standard star schema. Clean. Well structured. Ready to go.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 2: Loading into PostgreSQL&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;I created a gold schema in Postgres and defined tables for each CSV — all columns as text for the initial load (you can cast later, don't overthink the first load).&lt;/p&gt;
&lt;p&gt;sql&lt;br&gt;
CREATE SCHEMA IF NOT EXISTS gold;&lt;/p&gt;
&lt;p&gt;Where I got stuck: the COPY command&lt;/p&gt;
&lt;p&gt;My first attempt was server-side COPY:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;sql&lt;/strong&gt;&lt;br&gt;
COPY gold.dim_campaign FROM 'C:/Users/myuser/Downloads/dim_campaign.csv'&lt;br&gt;
WITH (FORMAT csv, HEADER true);&lt;/p&gt;
&lt;p&gt;Got a Permission denied error immediately. The PostgreSQL service account can't read files from your Windows user profile folder. Makes sense from a security standpoint — still annoying at 11pm.&lt;/p&gt;
&lt;p&gt;The fix: Use pgAdmin's client-side Import/Export Data instead.&lt;/p&gt;
&lt;p&gt;Right-click the table → Import/Export Data → Import → select your CSV → Format: csv, Header: Yes, Encoding: UTF8.&lt;/p&gt;
&lt;p&gt;Slower than bulk COPY but it works every time.&lt;/p&gt;
&lt;p&gt;Another thing that caught me: duplicate primary keys&lt;/p&gt;
&lt;p&gt;When I tried adding a primary key to dim_adset, Postgres threw this:&lt;/p&gt;
&lt;p&gt;ERROR: could not create unique index &quot;dim_adset_pkey&quot;&lt;br&gt;
DETAIL: Key (adset_sk)=(909) is duplicated.&lt;/p&gt;
&lt;p&gt;Fix — deduplicate before adding the constraint:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;sql&lt;/strong&gt;&lt;br&gt;
DELETE FROM gold.dim_adset a&lt;br&gt;
USING gold.dim_adset b&lt;br&gt;
WHERE a.ctid &amp;lt; b.ctid&lt;br&gt;
  AND a.adset_sk = b.adset_sk;&lt;/p&gt;
&lt;p&gt;Always worth running a quick dedup check before adding PKs on imported data.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 3: The Star Schema&lt;/strong&gt;&lt;br&gt;
&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=5092458307335549653&quot; alt=&quot;&quot;&gt;&lt;br&gt;
&lt;img src=&quot;http://&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;Standard stuff if you've built a warehouse before. Same conceptual model as what I had in Fabric/Power BI.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 4: Connecting Wren AI&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This is where it gets interesting.&lt;/p&gt;
&lt;p&gt;Wren AI is an open-source semantic layer with a natural language interface. You connect it to your database, define your data model (relationships, metrics, descriptions), and business users can ask plain English questions and get SQL-backed answers.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Here's the workflow I followed:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Connect data source Point Wren AI at localhost:5432, database postgres, schema gold.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Select tables Chose all 8 core tables from the gold schema.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Define relationships In the &quot;Define relationships&quot; screen, I created Many-to-One links using the _sk and date_key columns — matching the star schema above.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Deploy This publishes your model to the Wren engine. The Deploy button was greyed out initially — that usually means there's a validation error in your relationships. Check for incomplete or circular links.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Ask questions Once deployed, anyone can type a question and get an answer.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The Bug That Wasted My Afternoon&lt;/p&gt;
&lt;p&gt;After setting everything up, I asked:&lt;/p&gt;
&lt;p&gt;&quot;Show total impressions, clicks, and spend by campaign name&quot;&lt;/p&gt;
&lt;p&gt;Result: empty.&lt;/p&gt;
&lt;p&gt;No error. No warning. Just nothing.&lt;/p&gt;
&lt;p&gt;I went to pgAdmin and ran a quick validation:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;sql&lt;/strong&gt;&lt;br&gt;
SELECT 'fact_ad_performance_daily' t, COUNT(&lt;em&gt;) c &lt;br&gt;
FROM gold.fact_ad_performance_daily&lt;br&gt;
UNION ALL&lt;br&gt;
SELECT 'dim_campaign', COUNT(&lt;/em&gt;) FROM gold.dim_campaign&lt;br&gt;
UNION ALL&lt;br&gt;
SELECT 'dim_ad', COUNT(*) FROM gold.dim_ad;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;fact_ad_performance_daily0&lt;br&gt;
dim_campaign1069&lt;br&gt;
dim_ad2813&lt;/p&gt;
&lt;p&gt;I had loaded every single dimension table and completely forgot to import the fact table. 39,000 rows. Sitting in a CSV. Not in the database.&lt;/p&gt;
&lt;p&gt;Always validate row counts before you start building the semantic layer. This is the kind of mistake that's obvious in hindsight and invisible in the moment.&lt;/p&gt;
&lt;p&gt;Once I imported daily csv with the correct ~39k rows, the same question returned a proper breakdown. Campaign names, spend, impressions, clicks — exactly what the marketing team needed.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What Wren AI Actually Does Well&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;After going through all of this, here's my honest take:&lt;/p&gt;
&lt;p&gt;It respects your existing data model. You're not reinventing your schema for Wren AI. It sits on top of what you already have. The relationships you define mirror what you'd build in any BI tool — it just adds a natural language layer on top.&lt;/p&gt;
&lt;p&gt;The semantic layer is engineer-controlled. Business users ask questions, but they're asking against a model you built and you validate. The data engineer still owns the semantics. That matters for accuracy.&lt;/p&gt;
&lt;p&gt;It genuinely removes the middleman. Once it's working, a non-technical user can type a question and get a real answer in seconds. That's not hype — it actually works, and it's a meaningful shift in how fast insight moves through a team.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Final Thought&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The actual data engineering here wasn't complicated. Star schema, surrogate keys, foreign key relationships — standard stuff. The interesting part was watching what happened after the plumbing was in place.&lt;/p&gt;
&lt;p&gt;The marketing team stopped waiting on me. They started getting answers in seconds. The data was finally reaching the people who needed it, at the speed they needed it.&lt;/p&gt;
&lt;p&gt;That's the goal, right? The pipeline isn't the point. The pipeline is what makes the point possible.&lt;/p&gt;
&lt;p&gt;Have you used Wren AI or a similar semantic layer on top of a warehouse? I'd love to hear how it went drop a comment below.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/27479/how-i-used-wren-ai-to-let-my-marketing-team-query-a-star-schema-without-writing-sql</guid>
<pubDate>Thu, 17 Sep 2026 07:45:12 +0000</pubDate>
</item>
<item>
<title>The Core Paradigms of Machine Learning</title>
<link>https://coderlegion.com/27243/the-core-paradigms-of-machine-learning</link>
<description>&lt;h3 id=&quot;introduction-3&quot;&gt;&lt;strong&gt;Introduction&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;Most machine learning explanations start with algorithms. In this blog, let's start with a simpler question: how does anything learn?&lt;/p&gt;
&lt;p&gt;At its core, machine learning isn’t magic; it’s a set of strategies for turning data into behavior. Sometimes we guide the system with explicit answers. Sometimes we let it uncover structure on its own. And sometimes we let it learn through feedback, improving with every decision it makes.&lt;/p&gt;
&lt;p&gt;In this article, we will walk through the four core learning paradigms: supervised, unsupervised, self-supervised, and reinforcement learning, not as abstract theory, but as intuitive learning patterns you have already seen in the real world. By the end, you won’t just recognize the terms; you will understand why these approaches exist, when they matter, and how they shape modern AI systems.&lt;/p&gt;
&lt;h3 id=&quot;defining-the-paradigms&quot;&gt;&lt;strong&gt;Defining the Paradigms&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;Learning paradigms are the fundamental ways an algorithm processes data to find patterns. They are not just mathematical functions; they are distinct philosophical approaches to problem-solving.&lt;/p&gt;
&lt;h3 id=&quot;the-four-paths-to-pattern-recognition&quot;&gt;&lt;strong&gt;The Four paths to pattern recognition&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;Each paradigm dictates exactly what kind of data the machine needs and how independently it can operate.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=11729807870540746567&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;1-supervised-learning-the-flashcard-method&quot;&gt;1. Supervised Learning: The Flashcard Method&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The Child:&lt;/strong&gt; You show a child a card with a picture of a 🍎 and the&lt;br&gt;
word “Apple” written on it. You repeat this until they can see the&lt;br&gt;
shape and say the word.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The Machine:&lt;/strong&gt; We give the model a Dataset (the pictures) and Labels &lt;br&gt;
(the word “Apple”). The model calculates a mathematical function to&lt;br&gt;
map the input to the correct output.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Use case:&lt;/strong&gt; This is used for Classification (Is this email spam or &lt;br&gt;
not?) and Regression (Predicting the exact price of a house).&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=8838933960596066288&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;em&gt;Ground Truth: The model relies entirely on the “Teacher” being 100% correct on the labels provided.&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id=&quot;2-unsupervised-learning-discovering-on-your-own&quot;&gt;2. Unsupervised Learning: Discovering on Your Own&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The Child:&lt;/strong&gt; A child is given a bucket of colored blocks 🟦🧱🟡.&lt;br&gt;
Without being told to, they might start putting all the red ones&lt;br&gt;
together and all the blue ones together.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The Machine:&lt;/strong&gt; The model looks for Features (color, shape, size) and&lt;br&gt;
calculates the “distance” between data points. Things that are&lt;br&gt;
“close” together get grouped into Clusters.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Use case:&lt;/strong&gt; This is used for Clustering (Grouping similar news articles&lt;br&gt;
together) and Anomaly Detection (Finding a credit card transaction&lt;br&gt;
that looks &quot;weird&quot; or &quot;different&quot; from your usual spending).&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=16867958937842685327&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;em&gt;Clustering: Finding underlying structure in chaotic data when you don’t know what you are actually looking for yet.&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id=&quot;-3-self-supervised-learning-the-context-clue&quot;&gt; 3. Self-Supervised Learning: The Context Clue&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The Child:&lt;/strong&gt; You show a child a sentence with a word missing: “The cat&lt;br&gt;
sat on the ___.” Based on their past experience, they guess “mat” or&lt;br&gt;
“floor.”&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The Machine:&lt;/strong&gt; We take a massive amount of unlabeled data (like the&lt;br&gt;
entire internet) and hide parts of it. The model tries to predict the&lt;br&gt;
missing piece.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Use case:&lt;/strong&gt; This is the &quot;secret sauce&quot; behind Large Language Models&lt;br&gt;
(LLMs) like ChatGPT. It allows machines to learn from massive amounts&lt;br&gt;
of data without humans having to label every single thing.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=11751687513847045071&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;em&gt;Pre-training: This is how &quot;Brainy&quot; models like GPT are built before they are taught specific tasks.&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;h3 id=&quot;reinforcement-learning-learning-from-consequences&quot;&gt;Reinforcement Learning: Learning from Consequences&lt;/h3&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The Child:&lt;/strong&gt; A child tries to ride a bike 🚲. They wobble (penalty),&lt;br&gt;
adjust their balance, and eventually move forward (reward).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The Machine:&lt;/strong&gt; An Agent lives in an environment. It takes an Action,&lt;br&gt;
and the environment gives it a Score (Reward). The machine’s only&lt;br&gt;
goal is to maximize that score over time.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Use case:&lt;/strong&gt; This is used for Robotics and Game AI (like AlphaGo), where&lt;br&gt;
the machine needs to make a sequence of decisions to reach a goal.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=833514813374314197&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;em&gt;Trial and Error: The model doesn't need to be told the &quot;right&quot; answer upfront, just whether its actions are moving it closer to the ultimate goal.&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id=&quot;the-architect-s-decision-tree&quot;&gt;The Architect’s Decision Tree&lt;/h3&gt;
&lt;p&gt;How to choose the right paradigm for your specific problem?&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=10244603073521130379&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;conclusion-4&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;Machine learning, at its core, is not about models; it’s about how learning is structured.&lt;/p&gt;
&lt;p&gt;Supervised learning shows us the power of guidance. Unsupervised learning reveals how structure can emerge without labels. Self-supervised learning bridges the gap, turning raw data into its own teacher. And reinforcement learning pushes systems to learn through interaction, feedback, and long-term consequences.&lt;/p&gt;
&lt;p&gt;These paradigms aren’t competing ideas; they are complementary lenses. Modern AI systems often blend them, moving fluidly from labeled data to pattern discovery to autonomous decision-making. Understanding this progression is what separates surface-level familiarity from true intuition.&lt;/p&gt;
&lt;p&gt;If you step back, the pattern is strikingly simple: whether in machines or humans, learning evolves from instruction → exploration → self-discovery → adaptation.&lt;/p&gt;
&lt;p&gt;And once you see machine learning this way, every model, every system, and every breakthrough becomes easier to reason about, not as complexity, but as a variation of these fundamental ways of learning.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/27243/the-core-paradigms-of-machine-learning</guid>
<pubDate>Tue, 15 Sep 2026 08:11:53 +0000</pubDate>
</item>
<item>
<title>The Engineering Debt Trap: Why &quot;Working Code&quot; Is Killing Modern Software Development</title>
<link>https://coderlegion.com/25924/the-engineering-debt-trap-why-working-code-is-killing-modern-software-development</link>
<description>&lt;p&gt;There is a quiet crisis unfolding in modern software architecture. On the surface, features are shipping faster than ever. Continuous Integration and Continuous Deployment (CI/CD) pipelines push updates dozens of times a day. Artificial intelligence tools auto-complete complex logic in milliseconds. Production dashboards gleam with green metrics.&lt;/p&gt;
&lt;p&gt;Yet, behind this illusion of hyper-productivity, developer velocity across the industry is hitting a structural wall.&lt;/p&gt;
&lt;p&gt;Teams that once built entire platforms in months now take quarters to roll out basic modifications. Simple bug fixes cascade into unintended regression outages. Senior engineers spend up to 70% of their time acting as archeologists—digging through layers of undocumented abstractions, legacy workarounds, and implicit dependencies just to understand &lt;em&gt;where&lt;/em&gt; a state change occurs.&lt;/p&gt;
&lt;p&gt;We have built a software ecosystem that prioritizes immediate delivery over structural longevity. We call this &quot;technical debt,&quot; but that standard financial analogy fails to capture its true nature. Debt can be leveraged strategically to generate revenue. What most software teams accumulate isn't strategic debt; it is structural decay—a steady, compounding operational tax that eventually bankrupts engineer morale and system scalability alike.&lt;/p&gt;
&lt;p&gt;If we want to build software that outlasts the quarterly planning cycle, we must radically rethink how we write, structure, and maintain complex codebases.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;1-the-fallacy-of-working-code-&quot;&gt;1. The Fallacy of &quot;Working Code&quot;&lt;/h3&gt;
&lt;p&gt;The foundational axiom taught in introductory computer science and bootcamp programs is simple: &lt;em&gt;Does the program produce the expected output for the given input?&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;If the tests pass, the code is considered functional. If it is functional, it gets merged.&lt;/p&gt;
&lt;p&gt;This framing is dangerous. Writing software that executes correctly on a machine is the easiest part of software engineering. The true challenge—the discipline that separates junior coders from staff architects—is writing software that remains understandable and adaptable to human minds over years of continuous mutation.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;+-----------------------------------------------------------------+
|                        THE CODE LIFECYCLE                       |
|                                                                 |
|   [ Written Once ]  ---&amp;gt;  [ Read 100x ]  ---&amp;gt;  [ Modified 20x ]  |
|          |                                            |         |
|          v                                            v         |
|   Machine Execution                           Human Cognition   |
|   (Cheap &amp;amp; Fast)                              (Expensive &amp;amp; Slow)|
+-----------------------------------------------------------------+

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Code is read far more often than it is written. When an engineer writes a complex nested conditional block or relies on implicit side effects to save twenty lines of code today, they save twenty minutes of their own time while burning hundreds of hours of collective cognitive bandwidth for every developer who touches that module in the future.&lt;/p&gt;
&lt;h4 id=&quot;the-cost-of-cognitive-overhead&quot;&gt;The Cost of Cognitive Overhead&lt;/h4&gt;
&lt;p&gt;Human working memory is strictly constrained. Cognitive science shows that the average human mind can hold roughly 4 to 7 operational chunks of information in working memory at any given moment.&lt;/p&gt;
&lt;p&gt;When reading code, an engineer must construct a mental model of the system’s execution context:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;What is the current state of this object?&lt;/li&gt;
&lt;li&gt;What implicit global variables exist?&lt;/li&gt;
&lt;li&gt;Which threads or async event loops can mutate this state concurrently?&lt;/li&gt;
&lt;li&gt;What side effects will invoking this function trigger down the dependency chain?&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If understanding a single function requires keeping 12 different environmental variables in mind simultaneously, the engineer's working memory overflows. To compensate, they slow down, make educated guesses, or accidentally introduce bugs.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Unmaintainable code isn't code that fails to run; it is code that overloads human cognitive capacity.&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;2-microservices-abstraction-layers-and-premature-architecture&quot;&gt;2. Microservices, Abstraction Layers, and Premature Architecture&lt;/h3&gt;
&lt;p&gt;Over the last decade, the industry attempted to solve codebase complexity through architectural segregation: enter the microservices paradigm and heavy abstraction patterns.&lt;/p&gt;
&lt;p&gt;Instead of one monolithic codebase, organizations split their logic across dozens—or hundreds—of isolated micro-repositories. The promise was alluring: independent deployments, isolated failure domains, and small, manageable codebases.&lt;/p&gt;
&lt;p&gt;In reality, many engineering organizations simply traded &lt;strong&gt;internal code complexity&lt;/strong&gt; for &lt;strong&gt;distributed network complexity&lt;/strong&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;MONOLITHIC COMPLEXITY                  DISTRIBUTED COMPLEXITY
+-----------------------+              +------+   HTTP   +------+
| Class A -&amp;gt; Class B    |              | Svc A| --------&amp;gt;| Svc B|
|   (Direct Call,       |              +------+          +------+
|    Compile Time)      |                 |                 |
|                       |                 v Async Event     v Database
| Class C -&amp;gt; Class D    |              +------------------------+
+-----------------------+              |   Kafka / RabbitMQ     |
                                       +------------------------+

&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;the-microservice-mirage&quot;&gt;The Microservice Mirage&lt;/h4&gt;
&lt;p&gt;When you break a monolith into microservices without clear bounded contexts, you do not eliminate complexity; you export it onto the network interface.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Function calls become network RPCs subject to latency, jitter, and partial network partitions.&lt;/li&gt;
&lt;li&gt;In-memory ACID transactions are replaced by saga patterns, eventual consistency, and complex distributed rollback mechanics.&lt;/li&gt;
&lt;li&gt;Debugging a single trace requires aggregating logs across twenty disparate observability tools instead of setting a breakpoint in an IDE.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If two microservices must always be deployed together, or if changing an API contract in Service A requires synchronized, breaking code updates in Service B, you do not have microservices. You have a &lt;strong&gt;distributed monolith&lt;/strong&gt;—combining all the performance overhead and operational friction of microservices with the tight coupling of a legacy monolith.&lt;/p&gt;
&lt;h4 id=&quot;the-abstraction-tax&quot;&gt;The Abstraction Tax&lt;/h4&gt;
&lt;p&gt;A similar failure mode occurs at the code level through premature abstraction. Driven by principles like DRY (&lt;em&gt;Don't Repeat Yourself&lt;/em&gt;), developers routinely create generic helper functions, base classes, and abstraction layers before fully understanding the problem space.&lt;/p&gt;
&lt;p&gt;Duplicate code is far cheaper than the wrong abstraction.&lt;/p&gt;
&lt;p&gt;When you prematurely abstract two pieces of logic that look identical today but evolve for different business reasons tomorrow, you force those distinct domains to share an increasingly convoluted set of &lt;code&gt;if/else&lt;/code&gt; flags within the generic abstraction. Over time, the abstraction becomes a brittle monstrosity that nobody dares to touch.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;3-the-architecture-of-maintainability-principles-for-long-term-value&quot;&gt;3. The Architecture of Maintainability: Principles for Long-Term Value&lt;/h3&gt;
&lt;p&gt;How do we construct software systems that resist decay? Maintainability is not an accident; it is the deliberate result of specific design trade-offs.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;+-----------------------------------------------------------------+
|                    SUSTAINABLE CODE FOUNDATIONS                 |
|                                                                 |
|  [ Explicit &amp;gt; Implicit ]    [ Localized State ]   [ Low Coupling ]|
|             \                      |                     /      |
|              v                     v                    v       |
|               HIGH-VELOCITY, LONG-LIVED ARCHITECTURE            |
+-----------------------------------------------------------------+

&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;i-explicit-over-implicit&quot;&gt;I. Explicit Over Implicit&lt;/h4&gt;
&lt;p&gt;Avoid magic. Reflection, auto-wiring dependency injection frameworks that obscure implementation locations, global dynamic state, and monkey-patching make code visually brief at the cost of operational clarity.&lt;/p&gt;
&lt;p&gt;Code should tell a direct, clear story. A reader should be able to trace execution paths by following explicit function calls and types without relying on runtime surprises or framework magic.&lt;/p&gt;
&lt;h4 id=&quot;ii-localize-state-and-embrace-immutability&quot;&gt;II. Localize State and Embrace Immutability&lt;/h4&gt;
&lt;p&gt;Shared mutable state is the primary root cause of concurrency bugs, race conditions, and unpredictable side effects.&lt;/p&gt;
&lt;p&gt;When state changes arbitrarily across an application, reasoning about system behavior requires reading every single line of code that could potentially modify that state.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Prefer Immutable Data Structures:&lt;/strong&gt; Treat data as facts that cannot be retroactively altered. When state changes, produce a new representation of the data rather than mutating the existing object in place.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Isolate Side Effects:&lt;/strong&gt; Keep your core business logic pure (inputs produce outputs with zero external side effects) and push I/O operations (database writes, API network requests, disk access) to the outer edges of your application architecture.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;iii-high-cohesion-and-low-coupling-real-bounded-contexts-&quot;&gt;III. High Cohesion and Low Coupling (Real Bounded Contexts)&lt;/h4&gt;
&lt;p&gt;Modules should be grouped by business domain capability, not by technical abstraction layers.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Bad Layered Architecture:&lt;/strong&gt; Placing all Controllers in one folder, all Services in another, and all Models in a third. Modifying a single feature requires jumping across three distinct directory trees.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Good Domain-Driven Package Structure:&lt;/strong&gt; Grouping all logic related to &lt;code&gt;Payments&lt;/code&gt; inside a self-contained module. The &lt;code&gt;Payments&lt;/code&gt; module exposes a narrow, public API interface while keeping its internal storage schemas, helper utilities, and processing steps strictly internal and private.&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;DOMAINS OVER LAYERS

  Layer-First (Fragmented)             Domain-First (Cohesive)
  ├── controllers/                     ├── payments/
  │   ├── UserController.js            │   ├── PaymentService.js
  │   └── PaymentController.js         │   ├── PaymentRepository.js
  ├── services/                        │   └── PaymentApi.js
  │   ├── UserService.js               └── users/
  │   └── PaymentService.js                ├── UserService.js
  └── models/                          └── UserRepository.js
      ├── UserModel.js
      └── PaymentModel.js

&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;4-culture-shift-refactoring-as-a-daily-discipline&quot;&gt;4. Culture Shift: Refactoring as a Daily Discipline&lt;/h3&gt;
&lt;p&gt;You cannot fix structural code rot with a dedicated &quot;refactoring sprint&quot; once a year. Technical debt is like interest on financial debt; if left untended, the interest payments swallow your entire operational capacity.&lt;/p&gt;
&lt;p&gt;Engineering organizations that maintain high velocity over decades view refactoring not as a project stage, but as a continuous habit integrated into daily development.&lt;/p&gt;
&lt;h4 id=&quot;the-boy-scout-rule&quot;&gt;The Boy Scout Rule&lt;/h4&gt;
&lt;p&gt;Leave the codebase cleaner than you found it. If you open a source file to add a feature or fix a bug, perform one small cleanup task before submitting your pull request:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Rename an ambiguous variable.&lt;/li&gt;
&lt;li&gt;Break a 100-line function into three small, pure helper methods.&lt;/li&gt;
&lt;li&gt;Add a missing test case covering an edge condition.&lt;/li&gt;
&lt;li&gt;Delete dead code or outdated comments.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These micro-improvements take minutes, but over the course of thousands of pull requests, they compound into massive structural maintenance gains.&lt;/p&gt;
&lt;h4 id=&quot;reframing-engineering-priorities-to-leadership&quot;&gt;Reframing Engineering Priorities to Leadership&lt;/h4&gt;
&lt;p&gt;Product managers and business executives rarely push back on code quality out of malice; they push back because engineers frame quality in technical terms rather than business metrics.&lt;/p&gt;
&lt;p&gt;When advocating for architectural investments:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Don't say:&lt;/strong&gt; &lt;em&gt;&quot;We need to rewrite this module using modern reactive patterns because the current code is spaghetti.&quot;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Do say:&lt;/strong&gt; &lt;em&gt;&quot;The current architecture causes 12 hours of weekly developer friction and accounts for 40% of our production regression bugs. Restructuring this domain will reduce feature delivery timelines for upcoming roadmap items by an estimated 25%.&quot;&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;conclusion-the-craftsman-s-mindset&quot;&gt;Conclusion: The Craftsman's Mindset&lt;/h3&gt;
&lt;p&gt;Software engineering is fundamentally an iterative discipline. The systems we design today will inevitably face pressures, scale constraints, and business requirements that we cannot foresee.&lt;/p&gt;
&lt;p&gt;We cannot predict the future, but we can design for change.&lt;/p&gt;
&lt;p&gt;Writing sustainable code requires stepping away from short-term shortcuts and rejecting the false metric of raw volume production. It demands that we view our code not merely as instructions executed by machines, but as a living human communication medium.&lt;/p&gt;
&lt;p&gt;When you write your next line of code, don't ask yourself simply &lt;em&gt;&quot;Does this work?&quot;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Ask yourself: &lt;em&gt;&quot;When a colleague opens this file at 2:00 AM three years from now to fix a critical production outage, will this code guide them to a clear solution, or trap them in an avoidable maze?&quot;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Building great software isn't just about solving problems today. It is about ensuring the system remains empowered to solve the problems of tomorrow.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;What are your biggest pain points with technical debt in your current codebase? How does your team balance feature velocity with long-term code quality? Share your experiences in the comments below!&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/25924/the-engineering-debt-trap-why-working-code-is-killing-modern-software-development</guid>
<pubDate>Mon, 31 Aug 2026 14:09:03 +0000</pubDate>
</item>
<item>
<title>How Open Embedding Models Can Match GPT-Class Retrieval Quality</title>
<link>https://coderlegion.com/24206/how-open-embedding-models-can-match-gpt-class-retrieval-quality</link>
<description>&lt;p&gt;RAG (Retrieval-Augmented Generation - a pattern where you fetch relevant documents before generating an answer) lives or dies on retrieval quality. A cheaper retrieval stack isn't always a compromise.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Retrieval Quality Gap Is Often in the Chunking, Not the Model&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Most teams assume the embedding model is the bottleneck when retrieval underperforms. The real culprit is usually upstream: how documents are split before they're embedded. Overlapping chunks with a recursive character splitter, combined with a well-tuned open embedding model like &lt;code&gt;bge-large-en-v1.5&lt;/code&gt; or &lt;code&gt;nomic-embed-text&lt;/code&gt;, can close a surprising amount of the gap against expensive proprietary embeddings - often without touching the LLM layer at all.&lt;/p&gt;
&lt;p&gt;The pattern that tends to work: chunk at a meaningful boundary (paragraph or sentence, not fixed character count), add a small overlap window (10 - 15% of chunk size), then embed with a model that scores well on the MTEB benchmark (Massive Text Embedding Benchmark - a standardized leaderboard for comparing retrieval and semantic similarity models). Many open models in the 1B parameter range sit competitively on MTEB against models costing 100x more per token to call via API.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Real Example&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Here's a minimal retrieval setup using LangChain with a local open embedding model via Ollama:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.embeddings import OllamaEmbeddings
from langchain_community.vectorstores import Chroma

splitter = RecursiveCharacterTextSplitter(
 chunk_size=512, chunk_overlap=64, separators=[&quot;\n\n&quot;, &quot;\n&quot;, &quot;. &quot;]
)
docs = splitter.create_documents([your_text])

embeddings = OllamaEmbeddings(model=&quot;nomic-embed-text&quot;)
vectorstore = Chroma.from_documents(docs, embeddings)

results = vectorstore.similarity_search(&quot;your query here&quot;, k=4)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Swapping &lt;code&gt;nomic-embed-text&lt;/code&gt; for &lt;code&gt;bge-large-en-v1.5&lt;/code&gt; (via HuggingFace) takes one line change. Running both against your actual dataset on a held-out set of queries - not a synthetic benchmark - is the fastest way to know which one fits your domain.&lt;/p&gt;
&lt;p&gt;The cost difference is real: embedding a million tokens locally via Ollama is effectively free after the initial model pull, versus measurable per-call costs at API scale.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Key Takeaways&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Chunking strategy (boundary type + overlap) affects retrieval quality at least as much as the embedding model choice - fix this before swapping models.&lt;/li&gt;
&lt;li&gt;Open embedding models in the 1B range score competitively on MTEB and run locally or on modest GPU instances.&lt;/li&gt;
&lt;li&gt;Evaluate retrieval quality on your own queries, not just published benchmarks - domain-specific retrieval patterns differ from general benchmarks.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you've swapped embedding models in a production RAG pipeline, which metric did you actually use to confirm the quality change - cosine similarity threshold, end-task accuracy, or something else?&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Sources referenced: HackerNews discussion - &quot;Beating GPT-5.6 Sol on retrieval with 100x cheaper open models&quot;, MTEB Leaderboard (Hugging Face)&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/24206/how-open-embedding-models-can-match-gpt-class-retrieval-quality</guid>
<pubDate>Thu, 06 Aug 2026 09:09:21 +0000</pubDate>
</item>
<item>
<title>🚀 Open Sourcing etl-pipeline-mcp: A Local-First, Decoupled Data Pipeline for AI Agents</title>
<link>https://coderlegion.com/23678/open-sourcing-etl-pipeline-mcp-a-local-first-decoupled-data-pipeline-for-ai-agents</link>
<description>&lt;p&gt;Hey CoderLegion community,&lt;br&gt;
When connecting LLMs or AI agents directly to production databases or third-party APIs (like HubSpot, Stripe, or GitHub), two major problems usually pop up:&lt;br&gt;
Unsanitized PII/PHI leaks right into the model's context.&lt;br&gt;
Fragmented identities when events come from multiple webhooks and CRMs.&lt;br&gt;
To solve this, I’ve built and released etl-pipeline-mcp — a local-first Model Context Protocol (MCP) server designed to handle high-throughput extraction, transformation, identity resolution, and data warehouse loading without relying on third-party SaaS middle layers.&lt;br&gt;
⚙️ Key Architecture &amp;amp; Features&lt;br&gt;
Decoupled Hot/Cold Storage:&lt;br&gt;
Hot Tier (RAM): High-speed in-memory SQLite buffer to handle high-velocity, real-time webhooks with backpressure throttling.&lt;br&gt;
Cold Tier (Disk): Persistent, on-disk SQLite archive for long-term storage and analytical queries.&lt;br&gt;
Automated PII Redaction &amp;amp; Compliance:&lt;br&gt;
Hashes sensitive fields (emails, IP addresses, customer IDs) using SHA-256 before saving to disk or exporting.&lt;br&gt;
Built-in alignment for GDPR, CCPA, and HIPAA requirements.&lt;br&gt;
Identity Resolution (ID Stitching):&lt;br&gt;
Stitches 5+ platform-specific IDs (Stripe Customer ID, GitHub User ID, HubSpot Lead ID, Social Handles, Email) into a single, unified mcp_root_id.&lt;br&gt;
Multi-Warehouse &amp;amp; Data Lake Exports:&lt;br&gt;
Formats and exports clean records directly to Snowflake, ClickHouse, or BigQuery.&lt;br&gt;
Generates columnar JSON-Parquet buffers for local DuckDB or S3 Data Lake ingestion.&lt;br&gt;
🛠️ Installation &amp;amp; Usage&lt;br&gt;
You can install it directly via PyPI or run it with uv:&lt;/p&gt;
&lt;h2 id=&quot;install-via-pypi&quot;&gt;Install via PyPI&lt;/h2&gt;
&lt;p&gt;pip install etl-pipeline-mcp&lt;/p&gt;
&lt;p&gt;Or connect it to Claude Desktop or Cursor in claude_desktop_config.json:&lt;br&gt;
{&lt;br&gt;
  &quot;mcpServers&quot;: {&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;seosiri-etl-pipeline&quot;: {
  &quot;command&quot;: &quot;uv&quot;,
  &quot;args&quot;: [
    &quot;run&quot;,
    &quot;--github&quot;,
    &quot;SEOSiri-Official/etl-pipeline-mcp&quot;,
    &quot;src/main_server.py&quot;
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;  }&lt;br&gt;
}&lt;/p&gt;
&lt;p&gt;🔗 Links &amp;amp; Resources&lt;br&gt;
PyPI Package: &lt;a rel=&quot;nofollow&quot; href=&quot;https://pypi.org/project/etl-pipeline-mcp/&quot;&gt;https://pypi.org/project/etl-pipeline-mcp/&lt;/a&gt;&lt;br&gt;
GitHub Repository: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/SEOSiri-Official/etl-pipeline-mcp&quot;&gt;https://github.com/SEOSiri-Official/etl-pipeline-mcp&lt;/a&gt;&lt;br&gt;
Full Architecture Guide: &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.seosiri.com/2026/07/etl-pipeline-mcp.html&quot;&gt;https://www.seosiri.com/2026/07/etl-pipeline-mcp.html&lt;/a&gt;&lt;br&gt;
Would love to hear your feedback or ideas for extending the transformation layer!&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/23678/open-sourcing-etl-pipeline-mcp-a-local-first-decoupled-data-pipeline-for-ai-agents</guid>
<pubDate>Tue, 28 Jul 2026 15:09:11 +0000</pubDate>
</item>
<item>
<title>Deploying My First Ubuntu Virtual Machine on Microsoft Azure: SSH Access, Apache2 Installation, and Web Server Configuration&quot;</title>
<link>https://coderlegion.com/22810/deploying-ubuntu-virtual-machine-microsoft-access-apache2-installation-configuration</link>
<description>&lt;p&gt;Introduction&lt;/p&gt;
&lt;p&gt;Microsoft Azure Virtual Machines (VMs) provide scalable cloud computing resources that allow users to deploy and manage operating systems in the cloud. In this project, I created an Ubuntu 22.04 Virtual Machine in Azure, connected to it using SSH from both Git Bash through the Visual Studio Code, updated the server, installed Apache2, configured network security settings, and deployed a custom web page accessible through a public IP address.&lt;/p&gt;
&lt;p&gt;This hands-on lab helped me gain practical experience with Azure Infrastructure as a Service (IaaS), Linux administration, SSH connectivity, and web server deployment.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Project Objectives&lt;/strong&gt;&lt;br&gt;
&lt;em&gt;Create an Ubuntu 22.04 Azure VM&lt;br&gt;
Download and secure a .pem private key&lt;br&gt;
Connect to the VM using Git Bash&lt;br&gt;
Update and upgrade Ubuntu packages&lt;br&gt;
Install and verify Apache2&lt;br&gt;
Create a custom web page&lt;br&gt;
Access the website through a browser&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;em&gt;Prerequisites&lt;/em&gt;&lt;/strong&gt;&lt;br&gt;
Active Azure Subscription&lt;br&gt;
Azure Portal access&lt;br&gt;
Git Bash installed&lt;br&gt;
Internet connection&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 1. Log In to Azure Portal&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;On your web browser, head over to &lt;a rel=&quot;nofollow&quot; href=&quot;https://portal.azure.com&quot;&gt;https://portal.azure.com&lt;/a&gt; and log in with your Microsoft account. If you don't have an account, create or sign up and provide the required information so your Microsoft account can be activated.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/1l2d153oycvjsi9xhfs5.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A successful login bring you to this page below,&lt;/strong&gt;&lt;br&gt;
&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/t5ymsjk8o7yjvvasdvv8.png&quot; alt=&quot;Image description&quot;&gt;&lt;br&gt;
_&lt;strong&gt;_Step 2: Create a Resources Group&lt;br&gt;
__&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;We need to create our Resources Group first before we go ahead to create our virtual machine&lt;/strong&gt;&lt;br&gt;
. &lt;em&gt;Search for Resource Group in the search bar at the top of your portal&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;page and Select/Click on Resource Group.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/b6t6p088oi67zxcojr7n.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;p&gt; &lt;em&gt;&lt;strong&gt;Click on Create to create Resource Group&lt;/strong&gt;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/vydtf3ijqg8jlbyoum4p.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;&lt;strong&gt;Type the Resource group name, select the region and click Next&lt;/strong&gt;&lt;/em&gt;&lt;br&gt;
&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/9gbn6dtcim96avl9d78f.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;em&gt;Apply tags to your resources to logically organize them by categories and click Next&lt;/em&gt;&lt;/strong&gt;&lt;br&gt;
&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/o3o4zbdumi8cc9taz6xv.png&quot; alt=&quot;Image description&quot;&gt;&lt;br&gt;
Review the resources group and click on Create&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/uzqygs3cuehmrm066ux7.png&quot; alt=&quot;Image description&quot;&gt;&lt;br&gt;
&lt;em&gt; Click on Resource Group on the left to bring out the resource group you created&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/6849l5c83shvmfzf8rdm.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 2. Create a New Virtual Machine&lt;/strong&gt;&lt;br&gt;
 Type &quot;Virtual Machines&quot; in the search bar at the top. Once you see virtual machine display click on the virtual machine&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/67xfkeu56lkuxr7h45ja.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Once you click on the virtual machine, your screen will be displayed as shown below. Click on CREATE&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/cq1vvxosdoofspcypkdu.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;When you click on  &quot;Create&quot; button, it will display a drop down as shown below. You will then click on the Azure virtual machine highlighted in red arrow. This will take you to the project details where you will begin to fill the basic configuration for you to start creating your virtual machine.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/rn9cvccxvtxt6a6e1bcf.png&quot; alt=&quot;Image description&quot;&gt;&lt;br&gt;
Create virtual Machine under the &lt;strong&gt;BASIC CONFIGURATIONS&lt;/strong&gt; by filling the following tabs as follows:&lt;/p&gt;
&lt;p&gt;_Subscription: Select your subscription(Azure Subscription 1)&lt;br&gt;
Resource Group: Use an existing or create a resource group (FlackernProj-rg)&lt;br&gt;
Virtual machine name: flackern-vm or use your desired username&lt;br&gt;
Region: Select a nearby or preferred location ((Africa) South Africa North)&lt;br&gt;
Availability options: Leave it as default (Availability Zone)&lt;br&gt;
Availability Zone: Zone 1 (For this demo we select only one zone)&lt;br&gt;
Security type: Standard&lt;br&gt;
Image: Ubuntu Server 24.04 LTS - x64 Gen2&lt;br&gt;
VM architecture: x64&lt;br&gt;
Size: Standard_B2as_v2 - 2 vcpus, 8 GiB memory ($71.32)&lt;br&gt;
Authentication type: SSH public key&lt;br&gt;
Username: Use your desired username&lt;br&gt;
SSH public key source: Generate new key pair&lt;br&gt;
SSH Key Type: RSA SSH Format&lt;br&gt;
Key pair name: flackern-vm&lt;em&gt;key or use your desired username&lt;br&gt;
Public inbound ports: Allow selected ports&lt;br&gt;
Select inbound ports: HTTP (80) and SSH (22)&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/pgujh1rsbkty8ozbt92n.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/8ur424c9uxa9hrlxas4i.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/qv6wa4g28ed6nabc5ndc.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/d3riqmpeerikaeuggskq.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/r7cye6xxnda7llnyykgx.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/y5dl5uhejziifpvnxaps.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 3. Configure Disks&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Click on Next : Disks &amp;gt;, this leads you to on the next page, you can use the default OS disk size, use the Image default (30 GiB) and for the OS disk type, use the default settings Premium SSD (locally-redundant storage) unless you have unique configuration requirements.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/oy7jv6y636dv19pdw1ci.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/0rmoadayxthrd0rl4j3x.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 4. Networking Configuration&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;On the bottom of the last page in Step 3, Click on Next : Networking &amp;gt;, this leads you to the Network interface page below:&lt;/em&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;Create your virtual network and subnet or make use of the one you have created before&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/gsie71x6xbhj5p5f9e2e.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/cpec0g0ntx4ubza09cjq.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Management,&lt;/strong&gt; &lt;strong&gt;monitoring&lt;/strong&gt;, &lt;strong&gt;Advanced&lt;/strong&gt;, &lt;strong&gt;Tags&lt;/strong&gt;: These pages usually displayed as default. Find their default screenshots below. However, you may wish to skip them and proceed to “Review+Create”.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/cp3giue9j91d0jzr7qia.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/zhdz5dwpojbtvsduiv3o.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/owy4qe0hop28gnlzcs63.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/ryvb58o3p4y58ehqwbg8.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 5. Generate SSH private Key&lt;/strong&gt;&lt;br&gt;
After a successful Linux VM deployment process, Azure will validate the configuration&lt;/p&gt;
&lt;p&gt;_Then Click &quot;Review + create&quot;. This will show validation passed as highlighted below.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/igef03hshpln5kp8ijt4.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;p&gt;_Then click “Create” . This will take you to the next page to generate new key pair. Then, you will click on “Download private key and create resource” as shown below. The new key pair will then be downloaded on your PC, while Azure will also provision your VM.&lt;br&gt;
__&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/boqmvzuzkstrr31kpwmm.png&quot; alt=&quot;Image description&quot;&gt;&lt;br&gt;
Download private key and create resource, afterwards, the deployment will start.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Wait for Deployment: Azure will now provision your virtual machine. This process may take a few minutes. Then the next page will show that your deployment is complete as shown below, then you will click on “Go to Resource”&lt;br&gt;
&lt;/em&gt;&lt;br&gt;
&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/xfbadw89bffdp84g48br.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;em&gt;Next, click on “Connect” and then “Connect”. This will take you to a page with other options to connect your VM.&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/itgep0avq8lcgurzc2c2.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Retrieve Public IP Address&lt;/strong&gt;: Once the virtual machine is deployed, note down the Public IP address on Azure portal overview page.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: Also go to your download and save the downloaded key pair into a file for later use when connecting via SSH.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 6: SSH into VM Using Git Bash -&lt;/strong&gt;Connect via SSH Key Authentication&lt;strong&gt;:&lt;/strong&gt;&lt;br&gt;
Open your Command Line Interface on Windows or Terminal on your Visual studio code using gitbash or power shell&lt;br&gt;
Move the .pem file into a secure folder.&lt;br&gt;
Set permissions:&lt;br&gt;
chmod 600 flackern-vm_key.pem&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/e87v65c55rp0immyi5c3.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 7: SSH into VM Using Azure Cloud Shell&lt;/strong&gt;&lt;br&gt;
&lt;em&gt;Connect:&lt;/em&gt;&lt;br&gt;
ssh -i flackern-vm_key.pem flackern-vm@102.37.144.183&lt;br&gt;
&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/exaw8enkyfrn1vsttm3n.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 8: Update Ubuntu Packages&lt;/strong&gt;&lt;br&gt;
Using sudo apt update and click enter.This will update our virtual machine&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/5b8v2qkgyqufu61azwyw.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 9: Install Apache2&lt;/strong&gt;&lt;br&gt;
Instal apache2 on our VM and click enter&lt;br&gt;
Install:&lt;br&gt;
sudo apt install apache2 -y&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/u9ydtbv8njgbwe3iyghj.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;p&gt;Now let us confirm if apache2! Was actually installed on the virtual machine, we can do this by going to your Azure site and copy the ip address and paste it on the web page and see the result.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Copy ip address&lt;/em&gt;&lt;br&gt;
&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/c7rwpw8dn303qf0qenyi.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Test Your Web Server&lt;/strong&gt;&lt;br&gt;
&lt;em&gt;Open a web browser and enter the public IP address you obtained earlier. If apache2 is running correctly, you should see a default welcome page.&lt;br&gt;
Congratulations! You have successfully created a Linux virtual machine on Microsoft Azure and installed the Apache2 web server. This is a foundational step for hosting web applications or deploying other web services.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/9y0b3y9skvotouus0lhi.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/22810/deploying-ubuntu-virtual-machine-microsoft-access-apache2-installation-configuration</guid>
<pubDate>Thu, 16 Jul 2026 12:51:23 +0000</pubDate>
</item>
<item>
<title>When &quot;Latest&quot; isn't Actually Latest</title>
<link>https://coderlegion.com/21755/when-latest-isnt-actually-latest</link>
<description>&lt;h2 id=&quot;when-latest-isn-t-actually-latest&quot;&gt;When &quot;Latest&quot; Isn't Actually Latest&lt;/h2&gt;
&lt;h3 id=&quot;tl-dr&quot;&gt;TL;DR&lt;/h3&gt;
&lt;p&gt;In distributed systems, the last record your system processes is not always the most recent event that happened in reality.&lt;/p&gt;
&lt;p&gt;Understanding the difference between &lt;strong&gt;Event Time&lt;/strong&gt; and &lt;strong&gt;Processing Time&lt;/strong&gt; is essential to avoid data quality issues in streaming pipelines, CDC architectures, and event-driven systems.&lt;/p&gt;
&lt;h3 id=&quot;the-assumption-that-works-until-it-doesn-t&quot;&gt;The Assumption That Works... Until It Doesn't&lt;/h3&gt;
&lt;p&gt;Most developers start their careers working with a single application and a single database.&lt;/p&gt;
&lt;p&gt;In that world, time feels simple.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A customer updates their profile.&lt;/li&gt;
&lt;li&gt;The database stores the change.&lt;/li&gt;
&lt;li&gt;Everyone sees the updated record.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The sequence appears obvious because all operations happen within the same system.&lt;/p&gt;
&lt;p&gt;Then distributed systems enter the picture.&lt;/p&gt;
&lt;h3 id=&quot;a-simple-example&quot;&gt;A Simple Example&lt;/h3&gt;
&lt;p&gt;Imagine a customer updates their address at 10:00:00.&lt;/p&gt;
&lt;p&gt;That change generates an event and begins traveling through a distributed architecture:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Application&lt;/li&gt;
&lt;li&gt;Database&lt;/li&gt;
&lt;li&gt;CDC pipeline&lt;/li&gt;
&lt;li&gt;Kafka&lt;/li&gt;
&lt;li&gt;Consumer service&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Everything looks normal.&lt;/p&gt;
&lt;p&gt;Now imagine an older address update, generated earlier, gets delayed because of a network issue or retry mechanism.&lt;/p&gt;
&lt;p&gt;The result?&lt;/p&gt;
&lt;p&gt;The older event arrives after the newer one.&lt;/p&gt;
&lt;p&gt;Your consumer processes the events in this order:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;New address&lt;/li&gt;
&lt;li&gt;Old address&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If your logic assumes that the last processed record represents the latest truth, you've just overwritten correct information with stale data.&lt;/p&gt;
&lt;h3 id=&quot;the-real-problem-time-has-multiple-meanings&quot;&gt;The Real Problem: Time Has Multiple Meanings&lt;/h3&gt;
&lt;p&gt;This is why distributed systems distinguish between two important concepts:&lt;/p&gt;
&lt;h4 id=&quot;event-time&quot;&gt;Event Time&lt;/h4&gt;
&lt;p&gt;The moment something actually happened in the real world.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Example:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Customer changed address → 10:00:00&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;processing-time&quot;&gt;Processing Time&lt;/h4&gt;
&lt;p&gt;The moment a system receives or processes the event.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Example:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Consumer receives event → 10:00:15&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Most of the time, these timestamps are close enough that nobody notices.&lt;/p&gt;
&lt;p&gt;The interesting cases appear when they aren't.&lt;/p&gt;
&lt;h3 id=&quot;where-things-get-complicated&quot;&gt;Where Things Get Complicated&lt;/h3&gt;
&lt;p&gt;Several factors can cause events to arrive out of order:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Network latency&lt;/li&gt;
&lt;li&gt;Retries&lt;/li&gt;
&lt;li&gt;Consumer lag&lt;/li&gt;
&lt;li&gt;Parallel processing&lt;/li&gt;
&lt;li&gt;System failures&lt;/li&gt;
&lt;li&gt;Distributed transactions&lt;/li&gt;
&lt;li&gt;CDC replication delays&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;As architectures become more distributed, these situations become normal rather than exceptional.&lt;/p&gt;
&lt;h3 id=&quot;what-about-kafka-&quot;&gt;What About Kafka?&lt;/h3&gt;
&lt;p&gt;Kafka helps preserve ordering within a partition through offsets.&lt;/p&gt;
&lt;p&gt;However, offsets tell us the order in which records were written to Kafka.&lt;/p&gt;
&lt;p&gt;They do not guarantee the order in which events actually occurred in the real world.&lt;/p&gt;
&lt;p&gt;That's an important distinction.&lt;/p&gt;
&lt;p&gt;A perfectly ordered Kafka partition can still contain events that represent out-of-order business actions.&lt;/p&gt;
&lt;h3 id=&quot;why-data-engineers-care&quot;&gt;Why Data Engineers Care&lt;/h3&gt;
&lt;p&gt;Many expensive data quality problems are not caused by broken infrastructure.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Pipelines may be healthy.&lt;/li&gt;
&lt;li&gt;Kafka may be running perfectly.&lt;/li&gt;
&lt;li&gt;Consumers may be processing records successfully.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The issue is often more subtle.&lt;/p&gt;
&lt;p&gt;The system answers the right question using the wrong sequence of events.&lt;/p&gt;
&lt;p&gt;When that happens, dashboards become inaccurate, customer profiles become inconsistent, and downstream decisions become unreliable.&lt;/p&gt;
&lt;h3 id=&quot;final-thought&quot;&gt;Final Thought&lt;/h3&gt;
&lt;p&gt;Distributed systems force us to rethink a concept we usually take for granted: &lt;strong&gt;time&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The challenge isn't simply moving data from one system to another.&lt;/p&gt;
&lt;p&gt;The challenge is preserving the meaning of that data while it travels through a network of independent components.&lt;/p&gt;
&lt;p&gt;Because in distributed systems, the most recent record is not always the most recent event.&lt;/p&gt;
&lt;p&gt;And understanding that difference is often the first step toward building reliable data systems.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/21755/when-latest-isnt-actually-latest</guid>
<pubDate>Thu, 02 Jul 2026 11:08:55 +0000</pubDate>
</item>
<item>
<title>The Data Engineer Roadmap for 2026 (in an AI-Native World)</title>
<link>https://coderlegion.com/21446/the-data-engineer-roadmap-for-2026-in-an-ai-native-world</link>
<description>&lt;blockquote&gt;&lt;p&gt;This is the narrated version of our free, interactive &lt;a rel=&quot;nofollow&quot; href=&quot;https://petascalelabs.com/data-engineer-roadmap&quot;&gt;Data Engineer Roadmap&lt;/a&gt;. Same areas, same order, with a focus on the one thing each layer asks of you that AI can't do for you.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Every data engineer roadmap written before early 2025 made the same quiet assumption: that the hard part was &lt;em&gt;writing the code&lt;/em&gt;. &lt;strong&gt;Learn SQL. Learn Python. Wire up a pipeline in Airflow. Ship it. Congratulations, you're a data engineer.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;That assumption is dead. AI writes the SQL now. It writes the DAG, the PySpark job, the dbt model, the masking policy - and it writes them faster than you, at 2am, without complaining. If your roadmap is a checklist of &lt;em&gt;tools to learn so you can produce that code&lt;/em&gt;, you're training for a race that has already been run.&lt;/p&gt;
&lt;p&gt;So a 2026 roadmap has to be a different shape. Not &quot;what do I learn so I can write a pipeline,&quot; but &lt;strong&gt;&quot;what do I understand so I can tell whether the AI-written pipeline is right, and fix it when it isn't.&quot;&lt;/strong&gt; That is a map of &lt;em&gt;depth&lt;/em&gt;, not a list of tools.&lt;/p&gt;
&lt;h3 id=&quot;the-one-idea-that-makes-the-whole-map-work&quot;&gt;The one idea that makes the whole map work&lt;/h3&gt;
&lt;p&gt;Most roadmaps draw becoming-senior as &lt;em&gt;new areas appearing&lt;/em&gt;: the junior does SQL and dbt, the senior does Kafka and Spark and Kubernetes. That is not how it works.&lt;/p&gt;
&lt;p&gt;A senior engineer works the &lt;strong&gt;same areas&lt;/strong&gt; a junior does. The difference is how far into each one they go.&lt;/p&gt;
&lt;p&gt;A junior knows Parquet is &quot;the fast columnar format&quot; and can partition a table. A senior reasons about row groups, page statistics, dictionary encoding, and why a scan cost what it cost. A junior writes a Spark job. A senior debugs its shuffle and its skew. Same topic, different altitude.&lt;/p&gt;
&lt;p&gt;That matters more now than ever, because &lt;strong&gt;AI raises the floor to roughly the junior line.&lt;/strong&gt; It reliably gets you the partitioned table and the working Spark job. The depth above that line is exactly the part it can't reason about for you, and exactly where your career value now lives.&lt;/p&gt;
&lt;p&gt;So as we walk the areas, watch for the pattern: &lt;strong&gt;AI does the surface; you own the depth.&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id=&quot;foundations-and-sql&quot;&gt;Foundations and SQL&lt;/h3&gt;
&lt;p&gt;Joins, window functions, CTEs, Python, the command line, Git, ETL vs ELT.&lt;/p&gt;
&lt;p&gt;AI writes almost all of this now. That doesn't make SQL optional, it makes it table stakes. You learn it not to produce it, but to &lt;strong&gt;catch when the generated query is quietly wrong&lt;/strong&gt;: the join that fans out and double-counts revenue, the &lt;code&gt;WHERE&lt;/code&gt; that silently drops NULLs, the window frame that is off by one row. The senior depth is reading an &lt;code&gt;EXPLAIN&lt;/code&gt; plan and knowing &lt;em&gt;why&lt;/em&gt; a query is slow. AI hands you the query; understanding it is still yours.&lt;/p&gt;
&lt;h3 id=&quot;data-modeling-and-transformation&quot;&gt;Data modeling and transformation&lt;/h3&gt;
&lt;p&gt;Dimensional modeling, star and snowflake schemas, fact vs dimension tables, dbt models and tests. Then the depth: Slowly Changing Dimensions, grain, conformed dimensions, the One Big Table pattern, Data Vault.&lt;/p&gt;
&lt;p&gt;AI drafts the model. What it can't do is the judgement calls: what is the grain of this fact table, what does &quot;one customer&quot; mean across three source systems, which dimension is conformed across marts. The classic trap is Slowly Changing Dimensions - everyone can recite the types, almost nobody internalizes which version of a dimension their facts join to. Get it wrong and &quot;revenue by region last quarter&quot; reports a number that was never true.&lt;/p&gt;
&lt;p&gt;Replay a change timeline yourself in the free, in-browser &lt;a rel=&quot;nofollow&quot; href=&quot;https://petascalelabs.com/tools/scd-playground&quot;&gt;SCD Playground&lt;/a&gt;, then practice the area in the &lt;a rel=&quot;nofollow&quot; href=&quot;https://petascalelabs.com/curriculum/dimensional-data-modeling&quot;&gt;Dimensional Data Modeling track&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;orchestration-and-pipelines&quot;&gt;Orchestration and pipelines&lt;/h3&gt;
&lt;p&gt;Airflow DAGs, scheduling, sensors, backfills, retries, idempotency. Senior: scheduler and executor internals, data-aware scheduling, lineage, freshness SLAs, being on-call.&lt;/p&gt;
&lt;p&gt;AI generates the DAG, and it is good at it. What it doesn't generate is the &lt;em&gt;understanding of failure modes&lt;/em&gt; the job actually requires, because the real work here isn't the happy path, it's the 3am page. Why did this task hang? Why did the backfill double-write? Is this retry safe, or did it just send the same email twice? Idempotency is a property you reason about, not a snippet AI sprinkles in. See the &lt;a rel=&quot;nofollow&quot; href=&quot;https://petascalelabs.com/curriculum/orchestration-and-pipelines&quot;&gt;Orchestration and Pipelines track&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;storage-and-file-formats&quot;&gt;Storage and file formats&lt;/h3&gt;
&lt;p&gt;Parquet, row vs columnar, compression, object storage, partitioning. Senior: row groups, page statistics, predicate pushdown, encoding, the small-file problem, the internals of ORC, Avro and Arrow.&lt;/p&gt;
&lt;p&gt;This is where AI is least useful and depth pays the most, because &lt;strong&gt;why a scan costs what it costs is a property of the bytes on disk, not the query text.&lt;/strong&gt; AI reads and writes Parquet fine. It can't tell you why two files with identical rows differ tenfold in scan cost - that is row group sizing, encoding choice, and whether min/max statistics let the engine skip pages.&lt;/p&gt;
&lt;p&gt;Point the free &lt;a rel=&quot;nofollow&quot; href=&quot;https://petascalelabs.com/tools/parquet-viewer&quot;&gt;Parquet Viewer&lt;/a&gt; at your own files (100% in-browser, nothing is uploaded) to see the row groups and statistics yourself. Track: &lt;a rel=&quot;nofollow&quot; href=&quot;https://petascalelabs.com/curriculum/storage-and-file-formats&quot;&gt;Storage and File Formats&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;data-lakes-and-table-formats&quot;&gt;Data lakes and table formats&lt;/h3&gt;
&lt;p&gt;Lake vs warehouse vs lakehouse, Iceberg and Delta, time travel, schema evolution. Senior: ACID and snapshot isolation internals, compaction, catalogs, the Iceberg-vs-Delta-vs-Hudi tradeoffs.&lt;/p&gt;
&lt;p&gt;AI scaffolds the table operations happily. The part that bites, and that it won't warn you about, is &lt;strong&gt;what happens when two writers commit at once.&lt;/strong&gt; Snapshot isolation, optimistic concurrency, conflict resolution, compaction fighting your ingest job: that is distributed-systems reasoning, not autocomplete. Track: &lt;a rel=&quot;nofollow&quot; href=&quot;https://petascalelabs.com/curriculum/open-table-formats&quot;&gt;Open Table Formats&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;ingestion-and-streaming&quot;&gt;Ingestion and streaming&lt;/h3&gt;
&lt;p&gt;Batch ingestion, Kafka basics, producers and consumers, event time vs processing time. Senior: exactly-once semantics, consumer group rebalancing, Change Data Capture, stream processing in Flink or Kafka Streams.&lt;/p&gt;
&lt;p&gt;AI writes the producer and the consumer. Where it goes quiet is &lt;strong&gt;where data-quality bugs are actually born&lt;/strong&gt;: the gap between event time and processing time that makes your windowed aggregates wrong, the rebalance that reprocessed a batch, the &quot;exactly-once&quot; guarantee that was only ever at-least-once because of how you committed offsets. Track: &lt;a rel=&quot;nofollow&quot; href=&quot;https://petascalelabs.com/curriculum/ingestion-and-transport&quot;&gt;Ingestion and Transport&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;distributed-compute&quot;&gt;Distributed compute&lt;/h3&gt;
&lt;p&gt;Spark DataFrames, transformations vs actions, lazy evaluation. Senior: shuffle and partitioning, broadcast joins and data skew, Catalyst and codegen, memory and fault tolerance.&lt;/p&gt;
&lt;p&gt;AI writes the transformation. It cannot tune the &lt;em&gt;execution&lt;/em&gt;. Why did this job spill to disk? Why is one task taking 40 times longer than the other 199 (hello, data skew)? Should this join broadcast or shuffle? That reasoning, about how a logical DataFrame becomes physical work across a cluster, is squarely yours. Track: &lt;a rel=&quot;nofollow&quot; href=&quot;https://petascalelabs.com/curriculum/compute-engines&quot;&gt;Compute Engines&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;query-engines-and-olap&quot;&gt;Query engines and OLAP&lt;/h3&gt;
&lt;p&gt;What OLAP is, warehouse vs query engine, ClickHouse, Trino. Senior: MergeTree and projections, federation and pushdown, execution models, cost-based optimization, real-time OLAP, &lt;code&gt;EXPLAIN&lt;/code&gt; literacy.&lt;/p&gt;
&lt;p&gt;AI writes the SQL the dashboard runs. Why that dashboard is &lt;em&gt;slow&lt;/em&gt;, and how to fix it at the engine rather than by rewriting the query, is senior work. It lives in how the engine sorts and merges data, what it can push down, and what its optimizer chose. Track: &lt;a rel=&quot;nofollow&quot; href=&quot;https://petascalelabs.com/curriculum/query-engines-and-olap&quot;&gt;Query Engines and OLAP&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;semantic-and-metrics-layer&quot;&gt;Semantic and metrics layer&lt;/h3&gt;
&lt;p&gt;Metrics and dashboards, the semantic layer, data-quality tests. Senior: data contracts, schema registries, metric governance, reverse ETL.&lt;/p&gt;
&lt;p&gt;AI drafts a metric definition. What it can't do is the &lt;em&gt;organizational&lt;/em&gt; work of making &quot;revenue&quot; mean exactly one thing across finance, sales and product. That is a human contract - negotiated, governed, enforced - and it is the layer where data finally becomes shared business language instead of seven conflicting spreadsheets.&lt;/p&gt;
&lt;h3 id=&quot;governance-quality-and-cloud&quot;&gt;Governance, quality and cloud&lt;/h3&gt;
&lt;p&gt;PII basics, GDPR and CCPA, cloud, CI/CD for data. Senior: masking and tokenization, row and column access control, right-to-erasure across a lakehouse, infrastructure as code, data observability at scale.&lt;/p&gt;
&lt;p&gt;AI flags the obvious PII column. What it can't design is right-to-erasure across a lakehouse with time travel and immutable snapshots - that is architecture, not autocomplete. The masking itself is full of guarantee-breaking gotchas: an unsalted hash is a lookup table, a redacted ZIP that keeps five digits still re-identifies people. Generate the DDL with the free &lt;a rel=&quot;nofollow&quot; href=&quot;https://petascalelabs.com/tools/pii-masking-generator&quot;&gt;PII Masking Policy Generator&lt;/a&gt;. Track: &lt;a rel=&quot;nofollow&quot; href=&quot;https://petascalelabs.com/curriculum/pii-data-governance&quot;&gt;PII and Data Governance&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;so-will-ai-replace-data-engineers-&quot;&gt;So, will AI replace data engineers?&lt;/h3&gt;
&lt;p&gt;It raises the floor and moves the value up.&lt;/p&gt;
&lt;p&gt;AI now does the old junior checklist well: queries, DAGs, glue code, boilerplate pipelines. What's left for you is the durable part - reasoning about the system. Why a scan costs what it costs. What happens when two writers commit. Why a job spilled to disk.&lt;/p&gt;
&lt;p&gt;AI doesn't replace the engineer who understands that depth, it gives them leverage. They direct the AI through the surface work and spend their judgement on the part it can't reach. The engineer who only knew the surface is the one under pressure now, because the surface is free.&lt;/p&gt;
&lt;p&gt;That is the whole premise of the map. You touch every area early - junior and senior work the same areas. What stretches out over a career is how deep you go into each, and the deep end is precisely the part AI can't shortcut for you.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://petascalelabs.com/data-engineer-roadmap&quot;&gt;Open the full interactive Data Engineer Roadmap&lt;/a&gt;&lt;/strong&gt; to see every topic on a single timeline, with a &quot;Going senior&quot; toggle that reveals the depth in each layer. Then if you want to practice that depth on real engines instead of slideware, that is what the &lt;a rel=&quot;nofollow&quot; href=&quot;https://petascalelabs.com/curriculum&quot;&gt;curriculum&lt;/a&gt; and the free &lt;a rel=&quot;nofollow&quot; href=&quot;https://petascalelabs.com/tools&quot;&gt;in-browser tools&lt;/a&gt; are for.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Originally published on the &lt;a rel=&quot;nofollow&quot; href=&quot;https://petascalelabs.com/blog/data-engineer-roadmap-2026&quot;&gt;Petascale Labs blog&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/21446/the-data-engineer-roadmap-for-2026-in-an-ai-native-world</guid>
<pubDate>Sat, 27 Jun 2026 09:09:07 +0000</pubDate>
</item>
<item>
<title>The Story Behind Apache Iceberg's Format-Version: v1 to v4</title>
<link>https://coderlegion.com/21445/the-story-behind-apache-icebergs-format-version-v1-to-v4</link>
<description>&lt;p&gt;Most engineers meet Apache Iceberg as a one-line answer: &quot;it's the thing that&lt;br&gt;
gives you ACID transactions and time travel on object storage.&quot; That's true, and&lt;br&gt;
it's also where most people stop. But Iceberg has a version dial baked into every&lt;br&gt;
table - a single integer called &lt;code&gt;format-version&lt;/code&gt;, currently 1 through 4 - and&lt;br&gt;
each turn of that dial is a chapter in a single, surprisingly coherent story.&lt;/p&gt;
&lt;p&gt;It's a story about pushing transactional behavior down through layers. v1 made a&lt;br&gt;
table &lt;em&gt;atomic&lt;/em&gt; on top of immutable files, but it could only append and overwrite&lt;br&gt;
whole files. v2 pushed correctness down to the &lt;em&gt;row&lt;/em&gt;. v3 pushed identity down to&lt;br&gt;
the row, so a row keeps the same id across compactions. v4 turned around and&lt;br&gt;
refactored the &lt;em&gt;metadata itself&lt;/em&gt; so it can scale and move without rewrites.&lt;/p&gt;
&lt;p&gt;To follow that arc you need the one mental model the rest of this post hangs&lt;br&gt;
on. So we start there, then walk the four versions in order, keeping the parts&lt;br&gt;
that actually matter for understanding what's on disk.&lt;/p&gt;
&lt;h3 id=&quot;the-primer-an-iceberg-table-is-a-tree-of-files-not-a-folder&quot;&gt;The primer: an Iceberg table is a tree of files, not a folder&lt;/h3&gt;
&lt;p&gt;The instinct everyone brings from Hive is that a table &lt;em&gt;is&lt;/em&gt; a directory: point at&lt;br&gt;
a path, list the files under it, that's your table. Iceberg breaks that instinct&lt;br&gt;
on purpose. An Iceberg table is &lt;strong&gt;not&lt;/strong&gt; a directory of data files. It is a&lt;br&gt;
&lt;strong&gt;tree of immutable metadata files&lt;/strong&gt; rooted at a single pointer held in a&lt;br&gt;
catalog.&lt;/p&gt;
&lt;p&gt;Every commit &lt;em&gt;appends&lt;/em&gt; new files to that tree and atomically moves the pointer.&lt;br&gt;
Nothing is ever mutated in place. That single design choice - immutable files,&lt;br&gt;
one movable pointer - is what makes everything else possible.&lt;/p&gt;
&lt;p&gt;The tree has five layers:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/kjeddcvjzmn9xdip9wn6.png&quot; alt=&quot;Iceberg's five metadata layers as a top-down flow.    Layer 0, the Catalog (REST, Hive, Glue, or JDBC),    holds the current pointer for a table and points at    one metadata.json. Layer 1, the table metadata JSON,    holds schemas, partition specs, sort orders,    snapshots, refs, and properties; its snapshot's    manifest-list points to Layer 2. Layer 2, the    manifest list (one per snapshot), lists manifests    with partition summaries; each manifest_path points    to Layer 3. Layer 3, a manifest in Avro, indexes data   and delete files with per-column stats; each    data_file.file_path points to Layer 4. Layer 4 holds    the data, delete, and DV files in Parquet, ORC, Avro,   or Puffin    format.&quot;&gt;&lt;/p&gt;
&lt;p&gt;Read it top to bottom and you have the whole format:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Layer 0 - the catalog.&lt;/strong&gt; It holds one fact per table: &quot;the current pointer&lt;br&gt;
for &lt;code&gt;prod.db.sales&lt;/code&gt; is &lt;em&gt;this&lt;/em&gt; metadata file.&quot; The catalog is the only mutable&lt;br&gt;
thing in the entire system, and it lives &lt;em&gt;outside&lt;/em&gt; the format spec (REST&lt;br&gt;
catalog, Hive Metastore, Glue, JDBC, and so on).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Layer 1 - the table metadata JSON.&lt;/strong&gt; A new one is written on every commit.&lt;br&gt;
It holds the schemas, partition specs, sort orders, table properties, the list&lt;br&gt;
of snapshots, and named references. This is the file the catalog points at.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Layer 2 - the manifest list.&lt;/strong&gt; One per snapshot. An Avro file listing the&lt;br&gt;
manifests that make up that snapshot, each row carrying a partition summary so a&lt;br&gt;
scan can skip whole manifests it doesn't need.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Layer 3 - the manifests.&lt;/strong&gt; Avro files, each an index over a set of data or&lt;br&gt;
delete files, with per-column statistics for each file.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Layer 4 - the data.&lt;/strong&gt; The actual Parquet/ORC/Avro files, plus delete files&lt;br&gt;
and deletion-vector blobs.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Why build a table this way? Two reasons, and they're the whole pitch:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Atomic commits without coordination.&lt;/strong&gt; Because everything below the catalog&lt;br&gt;
is content-addressed by path and never mutated, readers and writers can build&lt;br&gt;
files independently with no locking. The &lt;em&gt;only&lt;/em&gt; contended operation in the&lt;br&gt;
entire system is the catalog pointer swap. One compare-and-set, and the commit&lt;br&gt;
is either visible or it isn't.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cheap reads.&lt;/strong&gt; A scan loads the manifest list (roughly 100 KB even for a&lt;br&gt;
huge table), prunes manifests by partition, and only then opens any data file.&lt;br&gt;
You never list a directory; you walk an index.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;On disk, a table looks roughly like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;s3://warehouse/db/sales/                 &amp;lt;- table location
  metadata/
    00001-....metadata.json              &amp;lt;- Layer 1: root JSON, one per commit
    00002-....metadata.json
    00042-....metadata.json              &amp;lt;- currently pointed at by the catalog
    snap-....avro                        &amp;lt;- Layer 2: a manifest list per snapshot
    8ef6-....avro                        &amp;lt;- Layer 3: manifests
    9b21-....avro
    stats-....puffin                     &amp;lt;- optional: table stats (NDV sketches)
  data/
    year=2025/month=06/00000-0-....parquet   &amp;lt;- Layer 4: data
    year=2025/month=06/00001-0-....parquet
    dv-....puffin                            &amp;lt;- Layer 4: deletion vectors
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Hold that picture. Every version below is a change to one or more of these five&lt;br&gt;
layers, and the format-version integer in Layer 1 is what tells a reader which&lt;br&gt;
rules apply. A reader that supports up to version &lt;em&gt;N&lt;/em&gt; will refuse to open a table&lt;br&gt;
whose &lt;code&gt;format-version&lt;/code&gt; is higher than &lt;em&gt;N&lt;/em&gt; rather than silently misread it.&lt;/p&gt;
&lt;p&gt;Now the four chapters.&lt;/p&gt;
&lt;h3 id=&quot;v1-a-table-that-s-atomic-but-only-at-the-file&quot;&gt;v1: a table that's atomic, but only at the file&lt;/h3&gt;
&lt;p&gt;v1 is the foundation, and it nails the hard part: snapshot isolation and atomic&lt;br&gt;
commits on immutable files. A v1 table already has schemas, partition specs,&lt;br&gt;
snapshots, and time travel. If all you ever do is &lt;em&gt;append&lt;/em&gt; data and occasionally&lt;br&gt;
&lt;em&gt;overwrite whole files&lt;/em&gt;, v1 is a complete, correct table format.&lt;/p&gt;
&lt;p&gt;Its limits are exactly the things the later versions go after.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;It can't delete or update a single row.&lt;/strong&gt; The smallest unit v1 can change is a&lt;br&gt;
file. Want to delete one row? Rewrite the entire file without it, then swap the&lt;br&gt;
file in a new snapshot. This is copy-on-write, and on a wide table it means&lt;br&gt;
rewriting gigabytes to remove a handful of rows.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Its metadata carries a few quirks that v2 had to clean up:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The schema and partition spec were stored as &lt;strong&gt;singular&lt;/strong&gt; fields - one schema,&lt;br&gt;
one spec - rather than a list of historical ones. A v1 metadata file has a&lt;br&gt;
&lt;code&gt;schema&lt;/code&gt; and a &lt;code&gt;partition-spec&lt;/code&gt;; both are deprecated from v2 onward in favor of&lt;br&gt;
&lt;code&gt;schemas[]&lt;/code&gt; and &lt;code&gt;partition-specs[]&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Partition field IDs were &lt;strong&gt;not tracked explicitly&lt;/strong&gt;. Implementations just&lt;br&gt;
assigned them sequentially starting at 1000. That caused real ambiguity when&lt;br&gt;
the same logical field got a different transform across specs - there was no&lt;br&gt;
stable identity to tie them together.&lt;/li&gt;
&lt;li&gt;A snapshot could &lt;strong&gt;embed its manifests inline&lt;/strong&gt; as a &lt;code&gt;manifests: [path, ...]&lt;/code&gt;&lt;br&gt;
list instead of pointing at a separate manifest-list file. Convenient, but it&lt;br&gt;
meant the per-snapshot bookkeeping that later inheritance rules depend on had&lt;br&gt;
nowhere to live.&lt;/li&gt;
&lt;li&gt;A few &lt;code&gt;data_file&lt;/code&gt; fields existed that nobody needed: &lt;code&gt;block_size_in_bytes&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;file_ordinal&lt;/code&gt;, &lt;code&gt;sort_columns&lt;/code&gt;. All removed in v2.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;None of this makes v1 wrong. It makes v1 &lt;em&gt;append-shaped&lt;/em&gt;. The entire push of v2&lt;br&gt;
is to make the table &lt;em&gt;mutable at the row&lt;/em&gt;, and to add the bookkeeping that makes&lt;br&gt;
that safe.&lt;/p&gt;
&lt;h3 id=&quot;v2-correctness-pushed-down-to-the-row&quot;&gt;v2: correctness pushed down to the row&lt;/h3&gt;
&lt;p&gt;v2 is the version most production tables ran on for years, because it's the one&lt;br&gt;
that turns Iceberg from an append log into a real transactional table. Three big&lt;br&gt;
ideas arrive together: &lt;strong&gt;row-level deletes&lt;/strong&gt;, &lt;strong&gt;sequence numbers&lt;/strong&gt;, and &lt;strong&gt;named&lt;br&gt;
references&lt;/strong&gt;.&lt;/p&gt;
&lt;h4 id=&quot;row-level-deletes-merge-on-read&quot;&gt;Row-level deletes: merge-on-read&lt;/h4&gt;
&lt;p&gt;Instead of rewriting a file to remove rows, v2 lets you write a small &lt;strong&gt;delete&lt;br&gt;
file&lt;/strong&gt; that says &quot;these rows in that data file are gone.&quot; The reader applies&lt;br&gt;
deletes on the fly at scan time. This is &lt;strong&gt;merge-on-read&lt;/strong&gt;, and it comes in two&lt;br&gt;
flavors:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Position deletes&lt;/strong&gt; - a list of &lt;code&gt;(file_path, position)&lt;/code&gt; tuples: &quot;row 42 and&lt;br&gt;
row 1007 of &lt;em&gt;this&lt;/em&gt; file are deleted.&quot; Precise, cheap to write, used by &lt;code&gt;DELETE&lt;/code&gt;&lt;br&gt;
and &lt;code&gt;MERGE&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Equality deletes&lt;/strong&gt; - a predicate on column values: &quot;any row where&lt;br&gt;
&lt;code&gt;id = 12345&lt;/code&gt; is deleted.&quot; These don't need to know &lt;em&gt;where&lt;/em&gt; the row lives, which&lt;br&gt;
makes them ideal for streaming upserts where you delete-then-insert without&lt;br&gt;
reading the old file first.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To make this work, the format needed a way to say whether a file holds data or&lt;br&gt;
deletes, and a way to order deletes against data. Both arrived in v2.&lt;/p&gt;
&lt;h4 id=&quot;the-content-discriminator-and-sequence-numbers&quot;&gt;The &lt;code&gt;content&lt;/code&gt; discriminator and sequence numbers&lt;/h4&gt;
&lt;p&gt;A v2 manifest, and each file it lists, now carries a &lt;code&gt;content&lt;/code&gt; field. On the&lt;br&gt;
manifest list it's &lt;code&gt;0 = data&lt;/code&gt; or &lt;code&gt;1 = deletes&lt;/code&gt;; a single manifest holds &lt;em&gt;either&lt;/em&gt;&lt;br&gt;
data files &lt;em&gt;or&lt;/em&gt; delete files, never both, so scan planning can load all the&lt;br&gt;
delete manifests first. On the data file itself, &lt;code&gt;content&lt;/code&gt; is &lt;code&gt;0 = DATA&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;1 = POSITION_DELETES&lt;/code&gt;, &lt;code&gt;2 = EQUALITY_DELETES&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The ordering problem is subtler. If a delete file says &quot;delete &lt;code&gt;id = 12345&lt;/code&gt;,&quot;&lt;br&gt;
&lt;em&gt;which&lt;/em&gt; inserts of that id does it kill - the ones before it, or also the ones&lt;br&gt;
after? v2 answers this with a monotonic &lt;strong&gt;sequence number&lt;/strong&gt; assigned at commit&lt;br&gt;
time and threaded through every layer:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The table metadata tracks &lt;code&gt;last-sequence-number&lt;/code&gt;, bumped on each commit.&lt;/li&gt;
&lt;li&gt;Each snapshot records its &lt;code&gt;sequence-number&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The manifest list records each manifest's &lt;code&gt;sequence_number&lt;/code&gt; and a&lt;br&gt;
&lt;code&gt;min_sequence_number&lt;/code&gt; (the smallest data sequence number among live files in&lt;br&gt;
it).&lt;/li&gt;
&lt;li&gt;Each manifest entry carries the file's &lt;code&gt;sequence_number&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The rule that falls out: an &lt;strong&gt;equality delete applies to a data file only when&lt;br&gt;
the delete's sequence number is greater than the file's&lt;/strong&gt; (and they share a&lt;br&gt;
partition). A position delete applies at equal-or-greater sequence. That's how&lt;br&gt;
&quot;delete then insert&quot; does the right thing - the new insert has a higher sequence&lt;br&gt;
number than the delete, so it survives.&lt;/p&gt;
&lt;h4 id=&quot;inheritance-why-this-is-cheap&quot;&gt;Inheritance: why this is cheap&lt;/h4&gt;
&lt;p&gt;Here's the piece that surprises people. A manifest entry can leave its&lt;br&gt;
&lt;code&gt;snapshot_id&lt;/code&gt;, &lt;code&gt;sequence_number&lt;/code&gt;, and &lt;code&gt;file_sequence_number&lt;/code&gt; &lt;strong&gt;null in the&lt;br&gt;
file&lt;/strong&gt;, and the reader fills them in from the manifest list. Why bother? Because&lt;br&gt;
it lets the &lt;em&gt;same manifest file&lt;/em&gt; be reused across optimistic-retry attempts. When&lt;br&gt;
a commit loses the compare-and-set race and has to retry with a new sequence&lt;br&gt;
number, only the small manifest list needs rewriting - the manifests and data&lt;br&gt;
files it points at are untouched.&lt;/p&gt;
&lt;h4 id=&quot;named-references-branches-and-tags&quot;&gt;Named references: branches and tags&lt;/h4&gt;
&lt;p&gt;v2 adds a &lt;code&gt;refs&lt;/code&gt; map to the table metadata - named branches and tags pointing at&lt;br&gt;
snapshots:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;&quot;refs&quot;: {
  &quot;main&quot;:        { &quot;snapshot-id&quot;: 8392648, &quot;type&quot;: &quot;branch&quot; },
  &quot;audit&quot;:       { &quot;snapshot-id&quot;: 8000000, &quot;type&quot;: &quot;branch&quot;,
                   &quot;min-snapshots-to-keep&quot;: 10, &quot;max-snapshot-age-ms&quot;: 604800000 },
  &quot;prod_release&quot;:{ &quot;snapshot-id&quot;: 7281001, &quot;type&quot;: &quot;tag&quot;,
                   &quot;max-ref-age-ms&quot;: 2592000000 }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;main&lt;/code&gt; always exists; if &lt;code&gt;refs&lt;/code&gt; is empty it implicitly points at&lt;br&gt;
&lt;code&gt;current-snapshot-id&lt;/code&gt;. Branches let you stage and validate writes off to the side&lt;br&gt;
(write-audit-publish); tags pin a snapshot so expiration won't garbage-collect&lt;br&gt;
it. Branches carry their own retention floor (&lt;code&gt;min-snapshots-to-keep&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;max-snapshot-age-ms&lt;/code&gt;); tags and non-main branches carry &lt;code&gt;max-ref-age-ms&lt;/code&gt;. &lt;code&gt;main&lt;/code&gt;&lt;br&gt;
never expires.&lt;/p&gt;
&lt;h4 id=&quot;the-v2-metadata-cleanup&quot;&gt;The v2 metadata cleanup&lt;/h4&gt;
&lt;p&gt;v2 also formalized a lot of Layer 1. These fields became &lt;strong&gt;required&lt;/strong&gt;:&lt;br&gt;
&lt;code&gt;last-sequence-number&lt;/code&gt;, &lt;code&gt;current-schema-id&lt;/code&gt;, &lt;code&gt;schemas&lt;/code&gt;, &lt;code&gt;default-spec-id&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;partition-specs&lt;/code&gt;, &lt;code&gt;last-partition-id&lt;/code&gt;, &lt;code&gt;default-sort-order-id&lt;/code&gt;, &lt;code&gt;sort-orders&lt;/code&gt;,&lt;br&gt;
and &lt;code&gt;table-uuid&lt;/code&gt; (a stable identity generated at create time, used as a&lt;br&gt;
refresh-time integrity check). The singular &lt;code&gt;schema&lt;/code&gt; and &lt;code&gt;partition-spec&lt;/code&gt; are&lt;br&gt;
deprecated, the inline snapshot &lt;code&gt;manifests&lt;/code&gt; list is gone, and partition field IDs&lt;br&gt;
are now &lt;strong&gt;explicit and unique across all specs&lt;/strong&gt; - fixing the v1 ambiguity.&lt;/p&gt;
&lt;p&gt;One nice compatibility property: &lt;strong&gt;a v1 file reads cleanly as v2.&lt;/strong&gt; A missing&lt;br&gt;
&lt;code&gt;sequence_number&lt;/code&gt; is read as 0, and a missing &lt;code&gt;content&lt;/code&gt; is read as 0 (data). So&lt;br&gt;
upgrading is a metadata-only operation; nothing has to be rewritten on day one.&lt;/p&gt;
&lt;p&gt;By the end of v2, Iceberg is a full transactional table: insert, delete, update,&lt;br&gt;
upsert, branch, tag, time-travel. So what's left for v3?&lt;/p&gt;
&lt;h3 id=&quot;v3-identity-efficient-deletes-and-richer-data&quot;&gt;v3: identity, efficient deletes, and richer data&lt;/h3&gt;
&lt;p&gt;If v2 made the &lt;em&gt;table&lt;/em&gt; transactional, v3 makes the &lt;em&gt;row&lt;/em&gt; a first-class citizen.&lt;br&gt;
It adds three things that don't fit neatly into v2's model: a stable identity for&lt;br&gt;
every row, a far more efficient delete mechanism, and a richer type and security&lt;br&gt;
surface.&lt;/p&gt;
&lt;h4 id=&quot;row-lineage-every-row-gets-a-stable-id&quot;&gt;Row lineage: every row gets a stable id&lt;/h4&gt;
&lt;p&gt;This is the headline feature, and it's genuinely clever because it touches three&lt;br&gt;
layers at once without storing an id per row anywhere. v3 mandates that every row&lt;br&gt;
has a stable &lt;code&gt;_row_id&lt;/code&gt; that survives compaction - so you can track a row across&lt;br&gt;
rewrites, build change feeds, and reason about lineage. It works by seeding,&lt;br&gt;
not storing:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Layer &lt;/th&gt;&lt;th&gt;Field &lt;/th&gt;&lt;th&gt;Set when &lt;/th&gt;&lt;th&gt;Used for&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;1 (table metadata) &lt;/td&gt;&lt;td&gt;&lt;code&gt;next-row-id&lt;/code&gt; &lt;/td&gt;&lt;td&gt;bumped per commit &lt;/td&gt;&lt;td&gt;seeds the next snapshot's first row id&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;2 (snapshot) &lt;/td&gt;&lt;td&gt;&lt;code&gt;first-row-id&lt;/code&gt;, &lt;code&gt;added-rows&lt;/code&gt; &lt;/td&gt;&lt;td&gt;at commit &lt;/td&gt;&lt;td&gt;starting &lt;code&gt;_row_id&lt;/code&gt; for the manifest list&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;2 (manifest list) &lt;/td&gt;&lt;td&gt;&lt;code&gt;first_row_id&lt;/code&gt; per manifest &lt;/td&gt;&lt;td&gt;at commit &lt;/td&gt;&lt;td&gt;starting &lt;code&gt;_row_id&lt;/code&gt; for files in that manifest&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;3 (manifest entry) &lt;/td&gt;&lt;td&gt;&lt;code&gt;data_file.first_row_id&lt;/code&gt; &lt;/td&gt;&lt;td&gt;at commit &lt;/td&gt;&lt;td&gt;starting &lt;code&gt;_row_id&lt;/code&gt; for rows in that file&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;4 (data file) &lt;/td&gt;&lt;td&gt;reserved fields &lt;code&gt;_row_id&lt;/code&gt;, &lt;code&gt;_last_updated_sequence_number&lt;/code&gt; &lt;/td&gt;&lt;td&gt;inherited at read &lt;/td&gt;&lt;td&gt;stable identity across compactions&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The reader computes a row's id with one formula:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;_row_id = data_file.first_row_id + row_position_in_file
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No per-row storage; the id is derived from where the row sits. If &lt;code&gt;first_row_id&lt;/code&gt;&lt;br&gt;
is null - say a v2-era file in a table that was upgraded to v3 - then &lt;code&gt;_row_id&lt;/code&gt;&lt;br&gt;
reads as null for those rows, which is exactly the honest answer. Equality&lt;br&gt;
deletes deliberately break lineage: an equality-delete update never reads the old&lt;br&gt;
row, so the replacement gets a fresh &lt;code&gt;_row_id&lt;/code&gt; rather than inheriting one it&lt;br&gt;
can't prove.&lt;/p&gt;
&lt;h4 id=&quot;deletion-vectors-position-deletes-done-right&quot;&gt;Deletion vectors: position deletes, done right&lt;/h4&gt;
&lt;p&gt;Position delete &lt;em&gt;files&lt;/em&gt; worked, but they had a scaling problem: lots of tiny&lt;br&gt;
delete files, each needing to be opened and merged. v3 replaces them with&lt;br&gt;
&lt;strong&gt;deletion vectors (DVs)&lt;/strong&gt; - a single compressed bitmap per data file, stored as&lt;br&gt;
a blob inside a Puffin file. One bitmap, one referenced data file, looked up by&lt;br&gt;
byte offset.&lt;/p&gt;
&lt;p&gt;The manifest entry for a DV reuses the position-delete &lt;code&gt;content&lt;/code&gt; code but adds&lt;br&gt;
three fields:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;manifest_entry {
  status = 1
  data_file {
    content              = 1                              // shares position-delete code
    file_path            = &quot;s3://warehouse/db/sales/data/dv-....puffin&quot;
    file_format          = &quot;puffin&quot;
    partition            = { country: &quot;IN&quot;, ts_day: 2025-06-15 }  // same partition as target
    record_count         = 3                              // number of deleted positions
    referenced_data_file = &quot;s3://.../00000-abc.parquet&quot;   // v3 NEW: which data file this DV covers
    content_offset       = 4                              // v3 NEW: byte offset of the blob in the Puffin file
    content_size_in_bytes= 23                             // v3 NEW: blob length
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;referenced_data_file&lt;/code&gt;, &lt;code&gt;content_offset&lt;/code&gt;, and &lt;code&gt;content_size_in_bytes&lt;/code&gt; are the new&lt;br&gt;
fields that let the reader jump straight to one bitmap. Position delete files are&lt;br&gt;
&lt;strong&gt;deprecated&lt;/strong&gt; in v3: writers can't create new ones, and existing ones get merged&lt;br&gt;
into DVs over time. The result is one delete artifact per data file instead of a&lt;br&gt;
pile of small files.&lt;/p&gt;
&lt;h4 id=&quot;new-types-and-column-defaults&quot;&gt;New types and column defaults&lt;/h4&gt;
&lt;p&gt;v3 broadens what a column can hold. New primitive types: &lt;code&gt;variant&lt;/code&gt; (semi-&lt;br&gt;
structured), &lt;code&gt;geometry&lt;/code&gt; and &lt;code&gt;geography&lt;/code&gt; (spatial), &lt;code&gt;unknown&lt;/code&gt; (a column whose type&lt;br&gt;
isn't known yet), and nanosecond timestamps &lt;code&gt;timestamp_ns&lt;/code&gt; / &lt;code&gt;timestamptz_ns&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;It also adds &lt;strong&gt;column defaults&lt;/strong&gt;, which finally make adding a non-null column&lt;br&gt;
sane:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;id&quot;: 5, &quot;name&quot;: &quot;country&quot;, &quot;type&quot;: &quot;string&quot;, &quot;required&quot;: false,
  &quot;initial-default&quot;: &quot;IN&quot;,   // value for rows in files written before this column existed
  &quot;write-default&quot;: &quot;IN&quot;      // value to fill when a writer omits the column
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;initial-default&lt;/code&gt; is the value that &lt;em&gt;existing&lt;/em&gt; rows get for a freshly added&lt;br&gt;
column, with no file rewrite - the reader synthesizes it. &lt;code&gt;write-default&lt;/code&gt; is what&lt;br&gt;
new writes use when the column is omitted. Together they make schema evolution a&lt;br&gt;
metadata change instead of a backfill.&lt;/p&gt;
&lt;p&gt;The type-promotion rules that keep schema evolution safe also expand in v3:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;From &lt;/th&gt;&lt;th&gt;v1 / v2 promotion &lt;/th&gt;&lt;th&gt;v3+ adds&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;unknown&lt;/code&gt; &lt;/td&gt;&lt;td&gt;- &lt;/td&gt;&lt;td&gt;promotable to &lt;em&gt;any&lt;/em&gt; type&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;int&lt;/code&gt; &lt;/td&gt;&lt;td&gt;&lt;code&gt;long&lt;/code&gt; &lt;/td&gt;&lt;td&gt;&lt;code&gt;long&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;date&lt;/code&gt; &lt;/td&gt;&lt;td&gt;- &lt;/td&gt;&lt;td&gt;&lt;code&gt;timestamp&lt;/code&gt;, &lt;code&gt;timestamp_ns&lt;/code&gt; (not the tz variants)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;float&lt;/code&gt; &lt;/td&gt;&lt;td&gt;&lt;code&gt;double&lt;/code&gt; &lt;/td&gt;&lt;td&gt;&lt;code&gt;double&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;decimal(P, S)&lt;/code&gt; &lt;/td&gt;&lt;td&gt;&lt;code&gt;decimal(P', S)&lt;/code&gt; with &lt;code&gt;P' &amp;gt; P&lt;/code&gt; &lt;/td&gt;&lt;td&gt;same&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4 id=&quot;partition-transforms-get-multi-argument&quot;&gt;Partition transforms get multi-argument&lt;/h4&gt;
&lt;p&gt;v3 adds &lt;code&gt;source-ids&lt;/code&gt; (plural) on partition fields, so a transform can take more&lt;br&gt;
than one source column. Single-argument transforms still write the old&lt;br&gt;
&lt;code&gt;source-id&lt;/code&gt;. The full set of allowed transforms is &lt;code&gt;identity&lt;/code&gt;, &lt;code&gt;bucket[N]&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;truncate[W]&lt;/code&gt;, &lt;code&gt;year&lt;/code&gt;, &lt;code&gt;month&lt;/code&gt;, &lt;code&gt;day&lt;/code&gt;, &lt;code&gt;hour&lt;/code&gt;, and &lt;code&gt;void&lt;/code&gt;. And a forward-&lt;br&gt;
compatibility rule lands: &lt;strong&gt;v3 readers must tolerate an unknown transform&lt;/strong&gt; and&lt;br&gt;
simply skip filter pushdown on it, rather than refusing to read. (v1/v2 only&lt;br&gt;
&lt;em&gt;should&lt;/em&gt;.) Writers, of course, still can't commit a transform they don't&lt;br&gt;
understand.&lt;/p&gt;
&lt;h4 id=&quot;encryption-arrives&quot;&gt;Encryption arrives&lt;/h4&gt;
&lt;p&gt;v3 adds table-level encryption with a three-place key model:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;&quot;encryption-keys&quot;: [
  { &quot;key-id&quot;: &quot;k1&quot;,
    &quot;encrypted-key-metadata&quot;: &quot;BASE64...&quot;,   // KMS-wrapped data encryption key
    &quot;encrypted-by-id&quot;: &quot;kms-master-2025&quot; }   // the logical key-encryption-key id
]
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;The table JSON holds &lt;code&gt;encryption-keys[]&lt;/code&gt; - data encryption keys (DEKs) each&lt;br&gt;
wrapped by a KMS-resident key-encryption-key (KEK).&lt;/li&gt;
&lt;li&gt;Each snapshot carries a &lt;code&gt;key-id&lt;/code&gt; naming which DEK protects that snapshot's&lt;br&gt;
manifest-list key metadata.&lt;/li&gt;
&lt;li&gt;Each file can carry per-file &lt;code&gt;key_metadata&lt;/code&gt; (this field already existed in&lt;br&gt;
v1/v2 on data files, but without a central registry).&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The DEK-to-KEK chain is opaque to the format; implementations plug into AWS KMS,&lt;br&gt;
GCP KMS, Vault, and so on via the wrapped bytes.&lt;/p&gt;
&lt;p&gt;By the end of v3, a row has an identity, deletes are a single bitmap, columns can&lt;br&gt;
default and hold variant or spatial data, and the table can be encrypted. The&lt;br&gt;
&lt;em&gt;user-visible&lt;/em&gt; feature set is essentially complete. Which is why v4 looks&lt;br&gt;
different from everything before it.&lt;/p&gt;
&lt;h3 id=&quot;v4-the-refactor-for-scale-and-portability&quot;&gt;v4: the refactor for scale and portability&lt;/h3&gt;
&lt;p&gt;v4 introduces &lt;strong&gt;no new user-visible types and no new delete mechanisms.&lt;/strong&gt; It is a&lt;br&gt;
metadata refactor aimed at three things: performance, portability, and richer&lt;br&gt;
per-file statistics. The changes are quieter, but two of them matter a lot in&lt;br&gt;
production.&lt;/p&gt;
&lt;h4 id=&quot;relative-paths-move-a-table-without-rewriting-it&quot;&gt;Relative paths: move a table without rewriting it&lt;/h4&gt;
&lt;p&gt;This is the biggest &lt;em&gt;invisible&lt;/em&gt; change in the whole format. In v1 through v3,&lt;br&gt;
every path stored inside metadata - &lt;code&gt;file_path&lt;/code&gt;, &lt;code&gt;manifest_path&lt;/code&gt;, the manifest&lt;br&gt;
list, &lt;code&gt;metadata-file&lt;/code&gt;, statistics paths - had to be &lt;strong&gt;absolute&lt;/strong&gt;, complete with a&lt;br&gt;
URI scheme like &lt;code&gt;s3://&lt;/code&gt; or &lt;code&gt;hdfs://&lt;/code&gt;. That meant the moment you wanted to move a&lt;br&gt;
table to a different bucket, every one of those absolute paths was wrong, and you&lt;br&gt;
had to rewrite the entire metadata tree to fix them.&lt;/p&gt;
&lt;p&gt;v4 allows paths to be &lt;strong&gt;relative to the table location&lt;/strong&gt;. The resolution rule is&lt;br&gt;
simple:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Format &lt;/th&gt;&lt;th&gt;Table location &lt;/th&gt;&lt;th&gt;Stored path &lt;/th&gt;&lt;th&gt;Resolves to&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;v4 &lt;/td&gt;&lt;td&gt;&lt;code&gt;s3://bucket/db/table&lt;/code&gt; &lt;/td&gt;&lt;td&gt;&lt;code&gt;data/00000.parquet&lt;/code&gt; &lt;/td&gt;&lt;td&gt;&lt;code&gt;s3://bucket/db/table/data/00000.parquet&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;v4 &lt;/td&gt;&lt;td&gt;&lt;code&gt;s3://bucket/db/table&lt;/code&gt; &lt;/td&gt;&lt;td&gt;&lt;code&gt;hdfs://wh/...&lt;/code&gt; &lt;/td&gt;&lt;td&gt;&lt;code&gt;hdfs://wh/...&lt;/code&gt; (absolute, used as-is)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;v3 and earlier &lt;/td&gt;&lt;td&gt;&lt;code&gt;s3://bucket/db/table&lt;/code&gt; &lt;/td&gt;&lt;td&gt;&lt;code&gt;s3://bucket/db/table/data/00000.parquet&lt;/code&gt; &lt;/td&gt;&lt;td&gt;unchanged&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;If a stored path has a URI scheme, it's absolute and used as-is. If it doesn't,&lt;br&gt;
the reader resolves it as &lt;code&gt;table_location + &quot;/&quot; + path&lt;/code&gt;. The writer rule:&lt;br&gt;
default to relative for files under the table location, use absolute for files&lt;br&gt;
outside it (say, a backfill from another bucket). Because location is now what&lt;br&gt;
ties relative paths together, &lt;code&gt;location&lt;/code&gt; in the table metadata JSON becomes&lt;br&gt;
&lt;strong&gt;optional&lt;/strong&gt; - the catalog can supply it.&lt;/p&gt;
&lt;p&gt;The operational payoff is the headline: moving a table from&lt;br&gt;
&lt;code&gt;s3://bucket-a/db/sales/&lt;/code&gt; to &lt;code&gt;s3://bucket-b/db/sales/&lt;/code&gt; needs only a catalog&lt;br&gt;
pointer update and, optionally, a new metadata.json with the new &lt;code&gt;location&lt;/code&gt;. &lt;strong&gt;No&lt;br&gt;
manifest list, no manifest, no data file gets rewritten.&lt;/strong&gt; Pre-v4 the same move&lt;br&gt;
required a full metadata rewrite.&lt;/p&gt;
&lt;h4 id=&quot;typed-content-stats-five-maps-become-one-struct&quot;&gt;Typed &lt;code&gt;content_stats&lt;/code&gt;: five maps become one struct&lt;/h4&gt;
&lt;p&gt;In v3 and earlier, per-column statistics on a data file were &lt;strong&gt;five parallel&lt;br&gt;
maps&lt;/strong&gt; keyed by field id: &lt;code&gt;value_counts&lt;/code&gt;, &lt;code&gt;null_value_counts&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;nan_value_counts&lt;/code&gt;, &lt;code&gt;lower_bounds&lt;/code&gt;, &lt;code&gt;upper_bounds&lt;/code&gt; (plus the on-disk&lt;br&gt;
&lt;code&gt;column_sizes&lt;/code&gt;). Five maps to keep in sync, all loosely typed (bounds were raw&lt;br&gt;
binary-encoded bytes).&lt;/p&gt;
&lt;p&gt;v4 replaces them with &lt;strong&gt;one typed struct&lt;/strong&gt;, &lt;code&gt;content_stats&lt;/code&gt;, whose layout is&lt;br&gt;
generated from the table schema itself. Each column reserves a block of ids and&lt;br&gt;
gets a typed sub-struct:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;146: optional struct content_stats {
  10_400: optional struct id (default null) {        // stats for table field 2 (int)
    10_401: optional int     lower_bound;
    10_402: optional int     upper_bound;
    10_403: optional boolean tight_bounds;
    10_404: optional long    value_count;
  }
  10_600: optional struct data (default null) {       // stats for table field 3 (string)
    10_601: optional string  lower_bound;
    10_602: optional string  upper_bound;
    10_603: optional boolean tight_bounds;            // v4 NEW: exact vs truncated min/max
    10_604: optional long    value_count;
    10_605: optional long    null_value_count;
    10_607: optional int     avg_value_size_in_bytes; // v4 NEW: variable-length sizing
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The id assignment is mechanical - each column reserves 200 ids - and two genuinely&lt;br&gt;
new pieces of information appear: &lt;code&gt;tight_bounds&lt;/code&gt;, a flag saying whether the&lt;br&gt;
min/max are exact or truncated (truncated bounds still prune, but you have to scan&lt;br&gt;
to confirm a match), and &lt;code&gt;avg_value_size_in_bytes&lt;/code&gt; for variable-length columns,&lt;br&gt;
which helps the planner estimate read cost. Spatial columns use typed&lt;br&gt;
&lt;code&gt;geo_lower&lt;/code&gt; / &lt;code&gt;geo_upper&lt;/code&gt; structs instead of opaque WKB bytes.&lt;/p&gt;
&lt;p&gt;The reassuring part: &lt;strong&gt;v3 and v4 statistics are equivalent.&lt;/strong&gt; A missing map key in&lt;br&gt;
v3 is the same as a missing-or-null sub-struct in v4. Nothing is lost in the&lt;br&gt;
translation; it's the same information, typed and consolidated.&lt;/p&gt;
&lt;h4 id=&quot;the-file-system-catalog-is-gone&quot;&gt;The file-system catalog is gone&lt;/h4&gt;
&lt;p&gt;v1 through v3 allowed a &quot;file-system table&quot;: sequential metadata filenames&lt;br&gt;
(&lt;code&gt;v1.metadata.json&lt;/code&gt;, &lt;code&gt;v2.metadata.json&lt;/code&gt;, ...) where a commit was an atomic file&lt;br&gt;
&lt;em&gt;rename&lt;/em&gt;. That only ever worked safely on HDFS, because object stores like S3&lt;br&gt;
don't offer atomic rename. v4 &lt;strong&gt;removes it entirely.&lt;/strong&gt; Every v4 table uses a real&lt;br&gt;
catalog (the metastore model), where a commit is a compare-and-set on the catalog&lt;br&gt;
pointer. This closes a long-standing source of silent corruption on object&lt;br&gt;
storage.&lt;/p&gt;
&lt;p&gt;That's the whole of v4: relative paths, optional &lt;code&gt;location&lt;/code&gt;, typed&lt;br&gt;
&lt;code&gt;content_stats&lt;/code&gt; with &lt;code&gt;tight_bounds&lt;/code&gt; and average value size, and the death of the&lt;br&gt;
file-system catalog. A refactor, not a feature release - and exactly the kind of&lt;br&gt;
change a format makes once its feature surface has settled.&lt;/p&gt;
&lt;h3 id=&quot;how-the-layers-earn-their-keep-scan-planning&quot;&gt;How the layers earn their keep: scan planning&lt;/h3&gt;
&lt;p&gt;The reason all this structure exists is to make a scan cheap, so it's worth&lt;br&gt;
watching a query actually use it. Given the current snapshot, a scan does&lt;br&gt;
&lt;strong&gt;three-level pruning&lt;/strong&gt; without ever needing a separate planner index:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Open the manifest list.&lt;/strong&gt; One read, roughly 100 KB.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Layer-2 pruning.&lt;/strong&gt; Drop any manifest whose partition summary can't match the&lt;br&gt;
query predicate. Whole manifests skipped without opening them.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Open the surviving delete manifests first&lt;/strong&gt;, then the data manifests. Delete&lt;br&gt;
manifests come first so the reader knows which deletes are in play before it&lt;br&gt;
decides what to emit.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Layer-3 pruning.&lt;/strong&gt; For each data file, check its &lt;code&gt;lower_bounds&lt;/code&gt; /&lt;br&gt;
&lt;code&gt;upper_bounds&lt;/code&gt;; if they rule the file out, skip it. Otherwise emit it as a scan&lt;br&gt;
task.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pair each scan task with the deletes that apply&lt;/strong&gt;, using the sequence-number&lt;br&gt;
rules from v2:&lt;ul&gt;
&lt;li&gt;a &lt;strong&gt;deletion vector&lt;/strong&gt; applies when its &lt;code&gt;referenced_data_file&lt;/code&gt; matches and its&lt;br&gt;
sequence is greater-or-equal, in the same partition;&lt;/li&gt;
&lt;li&gt;a &lt;strong&gt;position delete&lt;/strong&gt; applies by the same rule, but only when no DV is present;&lt;/li&gt;
&lt;li&gt;an &lt;strong&gt;equality delete&lt;/strong&gt; applies when its sequence is strictly greater than the&lt;br&gt;
data file's, in the same partition (or globally if unpartitioned).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Manifest list, then manifest, then file. Three reads narrow a petabyte table to&lt;br&gt;
the handful of files a query actually needs. That funnel is the entire reason the&lt;br&gt;
tree-of-files design beats a directory listing.&lt;/p&gt;
&lt;h3 id=&quot;reading-across-versions&quot;&gt;Reading across versions&lt;/h3&gt;
&lt;p&gt;A practical note that saves real debugging time: the format is designed so older&lt;br&gt;
files read correctly under newer rules.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;v1 read as v2:&lt;/strong&gt; a missing &lt;code&gt;sequence_number&lt;/code&gt; is 0; a missing &lt;code&gt;content&lt;/code&gt; is 0&lt;br&gt;
(data). Upgrading v1 to v2 is metadata-only.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;v2 read as v3:&lt;/strong&gt; files without &lt;code&gt;first_row_id&lt;/code&gt; simply report &lt;code&gt;_row_id&lt;/code&gt; as null;&lt;br&gt;
position delete files keep working but can't be created anew.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;v3 read as v4:&lt;/strong&gt; a missing stats map key equals a null typed sub-struct; absolute&lt;br&gt;
paths keep working unchanged alongside new relative ones.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And the one hard rule in the other direction: a reader refuses to open a table&lt;br&gt;
whose &lt;code&gt;format-version&lt;/code&gt; is higher than the reader supports. The version integer is&lt;br&gt;
a contract, not a hint.&lt;/p&gt;
&lt;h3 id=&quot;the-whole-format-on-one-page&quot;&gt;The whole format on one page&lt;/h3&gt;
&lt;p&gt;Here is the entire metadata surface, top to bottom, with the version each piece&lt;br&gt;
arrived in:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;catalog -&amp;gt; metadata.json
  |-- table level: format-version, schemas[], partition-specs[], sort-orders[],
  |               properties{}, last-*-id, last-sequence-number, encryption-keys[] (v3+)
  |-- refs{} -&amp;gt; main / branches / tags -&amp;gt; snapshot-id                            (v2+)
  |-- snapshots[] (whole history; expired entries pruned)
        |-- one snapshot:
              snapshot-id, parent-snapshot-id, sequence-number, summary{op,...},
              first-row-id + added-rows (v3+), key-id (v3+)
                |-- manifest list (Avro): [ manifest_file x N ]
                      manifest_file: path, len, spec-id, content (data|deletes),  (v2+)
                                     seq#, min_seq#, counts, partitions[],
                                     first_row_id (v3+)
                        |-- manifest (Avro): header{schema, spec, content} + [ entry x N ]
                              entry: status, snapshot_id, seq#, file_seq#,
                                     data_file {
                                       content, file_path, format, partition,
                                       record_count, size, sort_order_id,
                                       metrics maps (v1-v3)  -- OR --  content_stats struct (v4),
                                       equality_ids (v2+),
                                       referenced_data_file / content_offset / size (v3+),
                                       first_row_id (v3+), key_metadata
                                     }
                                |-- data file / delete file / Puffin DV blob
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And the version-by-version cheat sheet:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;&lt;/th&gt;&lt;th&gt;v1 &lt;/th&gt;&lt;th&gt;v2 &lt;/th&gt;&lt;th&gt;v3 &lt;/th&gt;&lt;th&gt;v4&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Core model&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;atomic snapshots on immutable files &lt;/td&gt;&lt;td&gt;+ row-level deletes, sequence numbers &lt;/td&gt;&lt;td&gt;+ stable row identity &lt;/td&gt;&lt;td&gt;metadata refactor only&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Deletes&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;rewrite whole file (copy-on-write) &lt;/td&gt;&lt;td&gt;position + equality delete files &lt;/td&gt;&lt;td&gt;deletion vectors (position deletes deprecated) &lt;/td&gt;&lt;td&gt;unchanged&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Schema/spec&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;singular &lt;code&gt;schema&lt;/code&gt; / &lt;code&gt;partition-spec&lt;/code&gt; &lt;/td&gt;&lt;td&gt;lists, explicit partition field ids &lt;/td&gt;&lt;td&gt;column defaults, new types, &lt;code&gt;source-ids&lt;/code&gt; &lt;/td&gt;&lt;td&gt;unchanged&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;References&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;- &lt;/td&gt;&lt;td&gt;&lt;code&gt;refs&lt;/code&gt;: branches + tags &lt;/td&gt;&lt;td&gt;unchanged &lt;/td&gt;&lt;td&gt;unchanged&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Row lineage&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;- &lt;/td&gt;&lt;td&gt;- &lt;/td&gt;&lt;td&gt;&lt;code&gt;next-row-id&lt;/code&gt; / &lt;code&gt;first-row-id&lt;/code&gt; / &lt;code&gt;_row_id&lt;/code&gt; &lt;/td&gt;&lt;td&gt;unchanged&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Types&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;base &lt;/td&gt;&lt;td&gt;+ sort orders &lt;/td&gt;&lt;td&gt;variant, geometry, geography, unknown, ns timestamps &lt;/td&gt;&lt;td&gt;unchanged&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Encryption&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;per-file key metadata only &lt;/td&gt;&lt;td&gt;per-file key metadata only &lt;/td&gt;&lt;td&gt;&lt;code&gt;encryption-keys&lt;/code&gt;, snapshot &lt;code&gt;key-id&lt;/code&gt; &lt;/td&gt;&lt;td&gt;unchanged&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Statistics&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;metrics maps &lt;/td&gt;&lt;td&gt;metrics maps &lt;/td&gt;&lt;td&gt;metrics maps &lt;/td&gt;&lt;td&gt;typed &lt;code&gt;content_stats&lt;/code&gt; + &lt;code&gt;tight_bounds&lt;/code&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Paths&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;absolute only &lt;/td&gt;&lt;td&gt;absolute only &lt;/td&gt;&lt;td&gt;absolute only &lt;/td&gt;&lt;td&gt;relative or absolute; &lt;code&gt;location&lt;/code&gt; optional&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Catalog&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;file-system or metastore &lt;/td&gt;&lt;td&gt;file-system or metastore &lt;/td&gt;&lt;td&gt;file-system or metastore &lt;/td&gt;&lt;td&gt;metastore only&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Every commit appends a new metadata.json plus new manifest-list, manifest, and&lt;br&gt;
data files. The old tree stays reachable through the metadata log for rollback&lt;br&gt;
and time travel, until snapshot expiration garbage-collects it. Nothing is ever&lt;br&gt;
mutated; the whole format is an append-only tree with one movable pointer at the&lt;br&gt;
root.&lt;/p&gt;
&lt;h3 id=&quot;the-throughline&quot;&gt;The throughline&lt;/h3&gt;
&lt;p&gt;Read the four versions back to back and the arc is clean. v1 made a table&lt;br&gt;
&lt;em&gt;atomic&lt;/em&gt; on immutable files. v2 pushed correctness down to the &lt;em&gt;row&lt;/em&gt; with deletes&lt;br&gt;
and sequence numbers, and added branches. v3 pushed &lt;em&gt;identity&lt;/em&gt; down to the row,&lt;br&gt;
made deletes a single bitmap, and broadened types and security. v4 turned inward&lt;br&gt;
and refactored the metadata so it can move and scale without rewrites.&lt;/p&gt;
&lt;p&gt;It's the same instinct applied at finer and finer grain: make the unit of change smaller, and make the metadata that tracks it cheaper. That's why a format that started as an append-only snapshot log can now back a streaming upsert table with row-level lineage across petabytes - without ever giving up the one property it started with, the single atomic pointer swap.&lt;/p&gt;
&lt;p&gt;If you want to build this understanding from the ground up - why data lakes broke,how snapshots and manifests really work, and the hands-on mechanics of deletes,branches, and compaction - that's the&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://petascalelabs.com/curriculum/open-table-formats/iceberg-foundations&quot;&gt;Iceberg Foundations&lt;/a&gt; track,&lt;br&gt;
part of the broader &lt;a rel=&quot;nofollow&quot; href=&quot;https://petascalelabs.com/curriculum/open-table-formats&quot;&gt;open table formats&lt;/a&gt;&lt;br&gt;
curriculum. The format rewards reading it as a story, because that's how it was written.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/21445/the-story-behind-apache-icebergs-format-version-v1-to-v4</guid>
<pubDate>Sat, 27 Jun 2026 09:08:20 +0000</pubDate>
</item>
<item>
<title>Not Worthless. Just Not Found Yet. - On verification work that arrives before anyone asks for it</title>
<link>https://coderlegion.com/21189/not-worthless-just-not-found-yet-on-verification-work-that-arrives-before-anyone-asks-for</link>
<description>&lt;h3 id=&quot;the-shape-of-the-work-right-now&quot;&gt;The shape of the work right now&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=14808488737358335165&quot; alt=&quot;1&quot;&gt;&lt;/p&gt;
&lt;p&gt;Zenodo views: 1. Downloads: 0. No external citations to the ledger to date.&lt;/p&gt;
&lt;p&gt;The code still runs.&lt;/p&gt;
&lt;p&gt;That is the uncomfortable shape of this work right now. A full Flamehaven cycle can take three weeks, sometimes six months or more: theory, repo, verification, result artifact, paper, ledger. No client asked for it. No deadline forced it. No market signal arrived to confirm that it mattered.&lt;/p&gt;
&lt;p&gt;So the question becomes simple, and not very flattering:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Is this verification work, or just elaborate work done in a room nobody enters?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;My answer is that those are different questions. Visibility asks whether people are watching. Provenance asks whether the work survives being checked later, by anyone, including a future version of us. In AI-era research, I think that second question is becoming the scarce one.&lt;/p&gt;
&lt;p&gt;This work is not valuable because someone is watching it.&lt;/p&gt;
&lt;p&gt;It is valuable because, when trust in AI-generated output becomes scarce, already-verifiable work becomes rare. Not worthless. Not yet found. Those are not the same thing, and the rest of this is about why the difference matters.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;how-one-cycle-actually-runs&quot;&gt;&lt;strong&gt;How one cycle actually runs&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=15232969967455637905&quot; alt=&quot;2&quot;&gt;&lt;/p&gt;
&lt;p&gt;From the outside it looks like &quot;wrote a paper.&quot; In practice, six stages chain together in sequence.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Theory&lt;/strong&gt; — Fix the boundary of the claim before anything else: what you will assert and, just as importantly, what you will not.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Repo&lt;/strong&gt; — Translate theory into code. The code doesn't prove the theory; it renders the theory into something falsifiable and checkable.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Verify&lt;/strong&gt; — The harness checks whether the code is internally self-consistent and actually respects the boundary defined upstream.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Result&lt;/strong&gt; — Numbers, pass/fail counts, degraded-pass flags, dropped into a file. Not a hand-written summary. An artifact produced by execution.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Paper&lt;/strong&gt; — Interprets the result. Does not stand in for it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Ledger&lt;/strong&gt; — Timestamps and keeps the trace of the entire cycle. When someone wants to re-verify the same theory later, the repo, result, and paper from that moment are still there, unchanged.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The reason these six stages run in strict order every time is simple: skip one, and the next stage proceeds unverified. Build the repo before fixing theory, and you end up writing code without knowing what it's supposed to test. Write the paper before running verify, and the paper ends up describing what the code was supposed to do rather than what it actually did. Skip ledger, and the next cycle has no way to audit the one before it.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;This scaffold isn't rebuilt from scratch for every project.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The same skeleton gets reused. QSOT2 is one instance of that skeleton applied to the quantum-channel domain.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-two-axis-problem-this-cycle-is-built-against&quot;&gt;&lt;strong&gt;The two-axis problem this cycle is built against&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=896739466489930502&quot; alt=&quot;3&quot;&gt;&lt;/p&gt;
&lt;p&gt;A result.json that took days to build, and nobody opens it. A claim_boundary field designed with real care, and nobody who'd recognize what it's doing. The time was real. Almost none of it registers as value from the outside.&lt;/p&gt;
&lt;p&gt;Answering that doubt requires reframing the question. Not who's watching, but whether this is the kind of problem worth watching in the first place. The evidence splits cleanly into two axes.&lt;/p&gt;
&lt;p&gt;The first is output contamination. In May 2026, arXiv began enforcing a one-year submission ban for papers found to contain incontrovertible evidence of unchecked generative AI use, including hallucinated references — leftover meta-commentary addressed to or from the model counts as well. [1]&lt;/p&gt;
&lt;p&gt;The scale behind that policy is documented: the rate of papers containing at least one fabricated reference went from roughly 1 in 2,828 in 2023 to 1 in 458 in 2025 to 1 in 277 in the first seven weeks of 2026, close to a twelvefold rise. A study co-authored by arXiv founder Paul Ginsparg audited 111 million references across arXiv, bioRxiv, SSRN, and PubMed Central, and found a conservative estimate of 146,932 hallucinated citations in 2025 alone. [2]&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Copy-pasted prompt residue, citations to papers that don't exist — this isn't an edge case anymore. It's a statistically detectable frequency.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The second is reproduction failure. OpenAI's PaperBench evaluated AI agents on the task of replicating 20 ICML 2024 papers from scratch, graded against 8,316 rubric items. [3] The best-performing tested agent scored 21.0%. Human ML PhDs, given 48 hours on a comparable subset, reached 41.4%.&lt;/p&gt;
&lt;p&gt;That doesn't mean AI agents are useless for research. It means fluent generation and successful reconstruction are still very different capabilities. On this benchmark, the strongest tested agent reached roughly half the human baseline on the comparable subset, and that gap matters precisely because the output can look competent before anyone re-runs it.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-the-cycle-answers-back-with&quot;&gt;&lt;strong&gt;What the cycle answers back with&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=10668546538095653306&quot; alt=&quot;4&quot;&gt;&lt;/p&gt;
&lt;p&gt;These are two stages of the same failure. The first is that submitted output often carries contamination straight through, unchecked. The second is that even output which clears that bar doesn't reproduce when someone actually re-runs it. Embedding claim_boundary directly in code is one response. Letting &lt;code&gt;DEGRADED_PASS&lt;/code&gt; fire automatically is another. Together, they turn the claim into something the repo has to keep surviving, not something written once and trusted forever.&lt;/p&gt;
&lt;p&gt;This isn't a concern unique to us. The broader research ecosystem is already circling the same problem from different angles: provenance, transparent reporting, dataset documentation, model cards, correction mechanisms, reproducibility. &lt;/p&gt;
&lt;p&gt;One academic paper argues that universities and research communities need to build provenance infrastructure that anchors AI-era scholarship, since generative AI has made it trivial to produce fluent, polished, but unsubstantiated output. [4] &lt;/p&gt;
&lt;p&gt;Elsevier's 2025 Researcher of the Future report found that 58% of researchers already use AI tools for work, while only 22% rate current GenAI tools as trustworthy. [5][6] 85% agreed that corrections and retractions help preserve the integrity of the scholarly record. Adoption and distrust are rising together, not trading off against each other.&lt;/p&gt;
&lt;p&gt;That is the gap Flamehaven is trying to work inside. Not as an institution. Not as a standard-setting body. Just as a small project asking one narrow question repeatedly:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Can the claim, code, result, paper, and ledger still point to each other after the attention has moved on?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3 id=&quot;not-a-system-a-small-pond-&quot;&gt;Not a system. A small pond.&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=12044609373809931736&quot; alt=&quot;5&quot;&gt;&lt;/p&gt;
&lt;p&gt;Looked at in isolation, QSOT2 can seem like overkill — this much scaffolding for a toy model with no new physics in it. That objection is fair. It's also the point. QSOT2 isn't presented as new physics. It's a controlled pond for testing whether the verification skeleton stays clear.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;claim_boundary&lt;/code&gt;, the &lt;code&gt;DEGRADED_PASS&lt;/code&gt; verdict logic, flagging patterns like riemann_is_toy — none of this is QSOT2-specific. Wrap the same harness around a different phenomenological model and the same tier of verification carries over unchanged. BioAI auditing, RExSyn-Nexus, CR-EP all share the same skeleton.&lt;/p&gt;
&lt;p&gt;Taken one at a time, each output looks worthless. Underneath, it isn't code rebuilt from zero each time. It's a structure built once and reused continuously — the important part is that the same skeleton is being grafted onto new domains, not redesigned for each one.&lt;/p&gt;
&lt;p&gt;Calling Flamehaven a system would overstate it.&lt;/p&gt;
&lt;p&gt;Right now it's closer to a small pond fed by a creek. The water is clear enough that you can see the bottom, and anything happening in it can be checked by anyone who looks. Murky water isn't a default we accept. Exposing riemann_is_toy instead of quietly dropping it is the same choice: keep the water clear rather than cloud it to look more impressive.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;A pond isn't a grand thing on its own. But a pond that stays clear and stable, over time, tends to attract life.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Frogs lay eggs. Pondweed takes root. Birds come to drink. None of this was designed top-down as an ecosystem from day one — it's what happens when one body of water is kept clear long enough that a small ecosystem accretes around it on its own.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-the-clarity-is-actually-worth&quot;&gt;What the clarity is actually worth&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=2039932667727696441&quot; alt=&quot;6&quot;&gt;&lt;/p&gt;
&lt;p&gt;The pond metaphor only works if “clear” stops being a mood and becomes something an outsider can inspect. In this context, clear means fewer hidden dependencies, fewer interpretive layers, and fewer places where the paper can make the result look cleaner than the execution actually was.&lt;/p&gt;
&lt;p&gt;That is why the technical details matter.&lt;/p&gt;
&lt;p&gt;Machine-epsilon-level CPTP verification runs on nothing but NumPy. 31 of the 35 core checks complete using pure NumPy, no PyTorch, no GPU. This does not make the work important by itself. It makes the work easier to re-run, easier to question, and harder to hide behind infrastructure.&lt;/p&gt;
&lt;p&gt;So &lt;strong&gt;QSOT2&lt;/strong&gt;'s view count and download count aren't the metric that matters here. The metric is how many other domains this skeleton has been grafted onto, and whether each graft required redesigning from scratch. That answer is already accumulating, one instance at a time.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;The useful test for any of this isn't whether someone agrees with the paper.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Agreement is cheap. The useful test is whether they can clone the repo, inspect the claim boundary, re-run the verification harness, compare result.json, and see the same ledgered output that existed before they arrived. That's the level where this work wants to be judged.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;living-with-worthlessness-rather-than-defeating-it&quot;&gt;Living with worthlessness, rather than defeating it&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=5711118409894117776&quot; alt=&quot;7&quot;&gt;&lt;/p&gt;
&lt;p&gt;The problem is that this is not the level where the visible market usually judges work.&lt;/p&gt;
&lt;p&gt;The visible market still rewards velocity and virality more than verification scaffolding. This isn't work the market has learned how to price yet, and that's a different problem than the work being wrong.&lt;/p&gt;
&lt;p&gt;So the task is not to defeat worthlessness with a louder story.&lt;/p&gt;
&lt;p&gt;It is to tolerate a state that looks worthless while refusing to cloud the water before a signal arrives confirming that judgment was wrong. Recognition arriving late and recognition never arriving are different outcomes. There's no way to distinguish between them right now. What's been available, so far, is to hold the form steady and wait.&lt;/p&gt;
&lt;p&gt;We have been waiting.&lt;/p&gt;
&lt;p&gt;Not for Flamehaven to be noticed. That would be the wrong story. What we were waiting for, although we could not have named it cleanly at the time, was for the pressure itself to become public: the sense that proof-shaped output can outrun disclosure, that claims can separate from the artifacts meant to hold them, and that human responsibility can become decorative after the machine has already produced something convincing.&lt;/p&gt;
&lt;p&gt;It arrived not as recognition, but as language.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;when-the-outside-world-starts-naming-the-same-problem&quot;&gt;&lt;strong&gt;When the outside world starts naming the same problem&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=15924688042470407975&quot; alt=&quot;8&quot;&gt;&lt;/p&gt;
&lt;p&gt;In June 2026, a working group of sixteen researchers from fifteen universities published the Leiden Declaration on Artificial Intelligence and Mathematics, drawn from a Lorentz Center workshop the previous September that had brought together around sixty researchers from ten countries. [7]&lt;/p&gt;
&lt;p&gt;The declaration is now officially endorsed by the International Mathematical Union. It names, in the language of mathematics, the same family of failure modes this piece has been circling. Automated techniques can produce results that look convincing but contain almost invisible errors, difficult to distinguish from genuine mathematical proofs, putting traditional standards of correctness, transparency, and independent verifiability under pressure.&lt;/p&gt;
&lt;p&gt;Results communicated through press releases or blog posts, ahead of peer review and without the disclosure needed for scientific evaluation, are named as a direct threat to proper evaluation.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;The Declaration's first recommendation for individual researchers is to disclose tool use explicitly, in a dedicated section, and to retain exclusive human responsibility for the correctness of any automated result.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That does not mean the Leiden Declaration validates Flamehaven. It means something narrower, and more useful: the failure mode Flamehaven was built around is no longer just an internal concern. Plausible-but-unreliable automated output, weak disclosure, missing provenance, and human accountability drifting away from machine-generated claims are now being named by the mathematical community itself, with institutional backing behind it.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;claim_boundary&lt;/code&gt; and &lt;code&gt;DEGRADED_PASS&lt;/code&gt; are not the Declaration in code. They are one small attempt to operationalize the same family of concern at the artifact level, built before that concern reached us as a public declaration with institutional backing.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=9790781015791710571&quot; alt=&quot;ledger&quot;&gt;&lt;/p&gt;
&lt;p&gt;Not because anyone here anticipated that working group, but because the underlying problem was already visible at small scale, one quiet result.json at a time.&lt;/p&gt;
&lt;p&gt;A working alignment note mapping the Flamehaven verification cycle against each of the Declaration's recommendations is now on the ledger, section by section, including the places where the cycle currently falls short of what the Declaration asks for. [8]&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;this-kind-of-naming-has-happened-before&quot;&gt;&lt;strong&gt;This kind of naming has happened before&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=6826480193413435580&quot; alt=&quot;9&quot;&gt;&lt;/p&gt;
&lt;p&gt;Once a field begins naming its own failure mode, the next question is not whether the language is elegant. The question is whether anyone changes behavior before an external authority forces them to.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;That is why the historical comparison matters.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;In 1975, recombinant DNA researchers gathered at Asilomar after parts of the field had already entered a voluntary pause, worried that the technology had outpaced anyone's ability to assess its risk. [9]&lt;/p&gt;
&lt;p&gt;Roughly 150 scientists debated guidelines from scratch over four days, and the NIH later used those recommendations as a template for its official rules on recombinant DNA research, shaping biosafety regulation for decades after.&lt;/p&gt;
&lt;p&gt;The point is not that AI mathematics is recombinant DNA. The point is narrower: sometimes a field begins to govern itself before formal institutions know what to do with the risk.&lt;/p&gt;
&lt;p&gt;Asilomar did not begin as regulation. It began as scientists deciding that some kinds of power should not be allowed to outrun the practices that make them inspectable.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;small-ponds-one-current&quot;&gt;Small ponds, one current&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=18232634446517114167&quot; alt=&quot;10&quot;&gt;&lt;/p&gt;
&lt;p&gt;This doesn't make the pond important on its own.&lt;/p&gt;
&lt;p&gt;It changes something smaller, but more useful: it changes what the pond can now be compared against.&lt;/p&gt;
&lt;p&gt;A year ago, a verification harness around toy quantum models could look like private overbuilding — too much claim_boundary, too many execution artifacts, too much ledger discipline for work almost nobody had asked to inspect.&lt;/p&gt;
&lt;p&gt;After the Leiden Declaration, the same habit sits inside a larger problem statement. The question is no longer only whether this particular pond matters. It is whether research claims, once AI makes them cheap to produce, can remain tied to artifacts sturdy enough to inspect.&lt;/p&gt;
&lt;p&gt;The Declaration is a mathematics-community document, not a software-verification framework. That boundary matters. Still, its core concern — automated output outrunning disclosure, review, and human responsibility — is the same pressure Flamehaven has been trying to make operational at the artifact level.&lt;/p&gt;
&lt;p&gt;Asilomar named a related pattern fifty years earlier, in a different field, with a different danger at stake: a technical capability began moving faster than the practices built to govern it, and the field responded before formal institutions had settled the rules.&lt;/p&gt;
&lt;p&gt;That is the only comparison I want to make.&lt;/p&gt;
&lt;p&gt;A declaration names the pressure from above. A ledger answers from below by keeping each claim tied to the code, result, paper, and timestamped record that supported it. Neither changes the whole field alone. But a watershed was never one current to begin with. It is what becomes visible after enough separate sources of clean water keep draining toward the same low ground.&lt;/p&gt;
&lt;p&gt;The trajectory still splits two ways. The trust gap around AI-generated research provenance could close on its own, market-driven and indifferent to anything built here. Or this kind of self-imposed, bottom-up correction — scientists and builders setting their own terms before regulation arrives — could become one of the ways it closes.&lt;/p&gt;
&lt;p&gt;Which branch is correct can't be known from here. The second branch is no longer just a hope, though. It now has a name, an institutional endorsement, a publication date, and a fifty-year precedent showing that field-led restraint can shape formal governance.&lt;/p&gt;
&lt;p&gt;That still does not make the work valuable in the market yet. The code still runs. The ledger still holds. The result still reproduces.&lt;/p&gt;
&lt;p&gt;We are not the ocean. We are not even the current. But a pond does not need to know it is part of a watershed to matter. It only has to stay clear, drain in the same direction as other clear water, and let the geography do the rest.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;references&quot;&gt;&lt;strong&gt;References&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;[1] Nature, &quot;arXiv to ban researchers for a year if they don't disclose AI use,&quot; May 2026&lt;/p&gt;
&lt;p&gt;[2] Ginsparg et al., &quot;Hallucinated References in Preprint Servers,&quot; arXiv, 2026&lt;/p&gt;
&lt;p&gt;[3] OpenAI, &quot;PaperBench: Evaluating AI's Ability to Replicate AI Research&quot;&lt;/p&gt;
&lt;p&gt;[4] &quot;Provenance Infrastructure for AI-Era Scholarship,&quot; arXiv, 2026&lt;/p&gt;
&lt;p&gt;[5] Elsevier, &quot;Shaping the Researcher of the Future: Confidence, Collaboration and AI,&quot; 2025&lt;/p&gt;
&lt;p&gt;[6] Elsevier, &quot;Researcher of the Future — Confidence in Research,&quot; 2025&lt;/p&gt;
&lt;p&gt;[7] Leiden Declaration on Artificial Intelligence and Mathematics, June 2026&lt;/p&gt;
&lt;p&gt;[7a] Leiden University, &quot;Leiden Declaration: AI is challenging the core values of mathematics,&quot; June 2026&lt;/p&gt;
&lt;p&gt;[8] Flamehaven Verification Ledger, &quot;Leiden Alignment Note&quot;&lt;/p&gt;
&lt;p&gt;[9] Embryo Project Encyclopedia, &quot;Asilomar Conference (1975)&quot;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/21189/not-worthless-just-not-found-yet-on-verification-work-that-arrives-before-anyone-asks-for</guid>
<pubDate>Tue, 23 Jun 2026 08:00:00 +0000</pubDate>
</item>
<item>
<title>Running Apache Airflow + Docker for Free Using GitHub Codespaces</title>
<link>https://coderlegion.com/21185/running-apache-airflow-docker-for-free-using-github-codespaces</link>
<description>&lt;p&gt;While building my ETL pipeline project, I ran into a common beginner problem:&lt;/p&gt;
&lt;p&gt;Running Apache Airflow locally on Windows with Docker was painful.&lt;/p&gt;
&lt;p&gt;Problems included:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Low disk space&lt;/li&gt;
&lt;li&gt;Docker setup issues&lt;/li&gt;
&lt;li&gt;Linux compatibility problems&lt;/li&gt;
&lt;li&gt;Environment debugging overhead&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;With only ~17GB free on my laptop, running multiple Airflow containers locally became difficult.&lt;/p&gt;
&lt;p&gt;So I moved the entire setup to GitHub Codespaces.&lt;/p&gt;
&lt;h3 id=&quot;what-codespaces-provided&quot;&gt;What Codespaces Provided&lt;/h3&gt;
&lt;p&gt;Out of the box:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Ubuntu Linux environment&lt;/li&gt;
&lt;li&gt;Docker pre-installed&lt;/li&gt;
&lt;li&gt;VS Code in browser&lt;/li&gt;
&lt;li&gt;Auto-cloned GitHub repo&lt;/li&gt;
&lt;li&gt;Port forwarding for Airflow UI&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;workflow&quot;&gt;Workflow&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker-compose up
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Open Airflow UI&lt;/li&gt;
&lt;li&gt;Trigger ETL DAG&lt;/li&gt;
&lt;li&gt;Verify successful execution ✔️&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Airflow was running in ~90 seconds.&lt;/p&gt;
&lt;h3 id=&quot;important-security-lesson&quot;&gt;Important Security Lesson&lt;/h3&gt;
&lt;p&gt;I accidentally committed my GCP service account key once.&lt;/p&gt;
&lt;p&gt;GitHub Secret Scanning blocked the push automatically.&lt;/p&gt;
&lt;p&gt;Immediately added:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-gitignore&quot;&gt;*.json
.env
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Never commit cloud credentials.&lt;/p&gt;
&lt;h3 id=&quot;why-this-setup-helps-beginners&quot;&gt;Why This Setup Helps Beginners&lt;/h3&gt;
&lt;p&gt;Codespaces removes a huge amount of local environment friction and lets you focus more on:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Airflow orchestration&lt;/li&gt;
&lt;li&gt;ETL pipelines&lt;/li&gt;
&lt;li&gt;Docker workflows&lt;/li&gt;
&lt;li&gt;Cloud integrations&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you'd like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Beginner-friendly walkthrough → check Medium&lt;/li&gt;
&lt;li&gt;Engineering-focused breakdown → check Hashnode&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Medium: &lt;a rel=&quot;nofollow&quot; href=&quot;https://medium.com/p/8832297e4d85/&quot;&gt;Link&lt;/a&gt;&lt;br&gt;
Hashnode: &lt;a rel=&quot;nofollow&quot; href=&quot;https://engineeringfriction.hashnode.dev/how-github-codespaces-helped-me-run-airflow-docker-free?utm_source=hashnode&amp;amp;utm_medium=feed&quot;&gt;Link&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Project Repo:&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/tanmaybhurkunde/ETL-Pipeline&quot;&gt;ETL Pipeline&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&quot;dataengineering-docker-apacheairflow-githubcodespaces&quot;&gt;DataEngineering #Docker #ApacheAirflow #GitHubCodespaces&lt;/h2&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/21185/running-apache-airflow-docker-for-free-using-github-codespaces</guid>
<pubDate>Tue, 23 Jun 2026 07:22:54 +0000</pubDate>
</item>
<item>
<title>Why I Stopped Treating Job Applications as My Only Career Strategy</title>
<link>https://coderlegion.com/21184/why-i-stopped-treating-job-applications-as-my-only-career-strategy</link>
<description>&lt;p&gt;Like many engineers, I started my job search with a simple idea:&lt;/p&gt;
&lt;p&gt;Apply to enough roles and eventually something will work out.&lt;/p&gt;
&lt;p&gt;The reality was more complicated.&lt;/p&gt;
&lt;p&gt;Some positions were already filled.&lt;br&gt;
Some never responded.&lt;br&gt;
Some required significantly more experience.&lt;br&gt;
Some disappeared before interviews even started.&lt;/p&gt;
&lt;p&gt;After a while, I realized something important:&lt;/p&gt;
&lt;p&gt;Applications are necessary, but they are not the only mechanism for creating opportunities.&lt;/p&gt;
&lt;p&gt;A Simple Probability Problem&lt;/p&gt;
&lt;p&gt;Imagine sending 100 applications.&lt;/p&gt;
&lt;p&gt;If the response rate is 2%, the expected number of responses is:&lt;/p&gt;
&lt;p&gt;100 × 0.02 = 2&lt;/p&gt;
&lt;p&gt;Now imagine spending part of that effort on:&lt;/p&gt;
&lt;p&gt;Building projects&lt;br&gt;
Writing technical articles&lt;br&gt;
Creating a portfolio&lt;br&gt;
Participating in engineering discussions&lt;/p&gt;
&lt;p&gt;None of these guarantee opportunities.&lt;/p&gt;
&lt;p&gt;But they increase the number of ways someone can discover your work.&lt;/p&gt;
&lt;p&gt;What I Decided to Build&lt;/p&gt;
&lt;p&gt;Instead of focusing exclusively on applications, I started working on:&lt;/p&gt;
&lt;p&gt;Payment Gateway Design&lt;/p&gt;
&lt;p&gt;Understanding transactions, idempotency, retries, and failure handling.&lt;/p&gt;
&lt;p&gt;Schema Design Portfolio&lt;/p&gt;
&lt;p&gt;Documenting database designs and architectural decisions.&lt;/p&gt;
&lt;p&gt;Data Engineering Journey&lt;/p&gt;
&lt;p&gt;Exploring Kafka, Spark, Airflow, and distributed systems.&lt;/p&gt;
&lt;p&gt;Technical Writing&lt;/p&gt;
&lt;p&gt;Sharing lessons learned while studying and building.&lt;/p&gt;
&lt;p&gt;The Hard Part Nobody Talks About&lt;/p&gt;
&lt;p&gt;The internet often makes personal branding sound easy.&lt;/p&gt;
&lt;p&gt;Reality looks more like this:&lt;/p&gt;
&lt;p&gt;Writing articles nobody reads.&lt;br&gt;
Publishing posts that get little engagement.&lt;br&gt;
Maintaining projects after the excitement wears off.&lt;br&gt;
Spending months before seeing meaningful results.&lt;/p&gt;
&lt;p&gt;There is no shortcut.&lt;/p&gt;
&lt;p&gt;The value comes from consistency.&lt;/p&gt;
&lt;p&gt;Final Thought&lt;/p&gt;
&lt;p&gt;I'm not abandoning job applications.&lt;/p&gt;
&lt;p&gt;I'm simply trying to build assets that continue working even when I'm not actively applying.&lt;/p&gt;
&lt;p&gt;Applications create opportunities one submission at a time.&lt;/p&gt;
&lt;p&gt;Projects and writing create opportunities that can compound over time.&lt;/p&gt;
&lt;p&gt;I'm curious how other engineers balance these two approaches.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/21184/why-i-stopped-treating-job-applications-as-my-only-career-strategy</guid>
<pubDate>Tue, 23 Jun 2026 07:22:20 +0000</pubDate>
</item>
<item>
<title>Managing Database Schema Like Code: The Quest for Database Version Control</title>
<link>https://coderlegion.com/20877/managing-database-schema-like-code-the-quest-for-database-version-control</link>
<description>&lt;p&gt;In modern software development, we take git for granted. We branch, we commit, we merge, and we roll back code seamlessly. But when it comes to the database layer, things get tricky. Databases have state—you can't just overwrite a production database with a new version without risking catastrophic data loss.&lt;/p&gt;
&lt;p&gt;So, is there a version control system for database structure changes? Yes, and it is a fundamental pillar of modern DevOps.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;How to Build a Database Version Control System&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;To create a reliable system for tracking database changes, you need to transition from manual, ad-hoc SQL execution to an automated, migration-based workflow.&lt;/p&gt;
&lt;p&gt;Baseline the existing database: You begin by taking a snapshot of your current database structure to serve as the ground-zero schema version.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Write incremental migration scripts:&lt;/strong&gt;&lt;br&gt;
 Every change—whether creating a table, adding a column, or modifying a constraint—is written as a separate, sequential SQL or code file.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Maintain a schema version table:&lt;/strong&gt; &lt;br&gt;
The database itself must track its own version using a dedicated metadata table that records which migration scripts have already run.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Automate via a migration engine:&lt;/strong&gt; &lt;br&gt;
Tools like Liquibase, Flyway, or framework-native tools (like Prisma, Alembic, or EF Core) are integrated into the CI/CD pipeline to automatically detect and apply missing migrations.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Roadblocks:&lt;/strong&gt;&lt;br&gt;
 Common Pitfalls in Database Version Control&lt;br&gt;
Transitioning databases to a version-controlled model introduces several friction points that do not exist with standard application code.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The state and data loss dilemma:&lt;/strong&gt; &lt;br&gt;
Unlike code files that can be easily replaced, database changes happen in-place and can easily destroy existing data if a destructive command (like dropping a column) is executed.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Merge conflicts in team environments:&lt;/strong&gt;&lt;br&gt;
 When multiple developers work on separate features simultaneously, they might generate conflicting migration sequences, leading to deployment failures.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The challenge of rollbacks:&lt;/strong&gt;&lt;br&gt;
 Reverting a code deployment is simple, but rolling back a database change that has already modified live data requires complex &quot;down&quot; scripts that may still result in data loss.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Production drift:&lt;/strong&gt;&lt;br&gt;
 If a developer or administrator manually alters the database schema directly in production, the version control system loses sync, causing subsequent automated deployments to fail.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Resolution:&lt;/strong&gt;&lt;br&gt;
 Best Practices for Seamless Database Migrations&lt;br&gt;
Overcoming these challenges requires strict engineering discipline and the right architectural approach.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Enforce a strict migration-only policy:&lt;/strong&gt;&lt;br&gt;
 Block direct schema modifications on all environments, ensuring that every single database change must go through code review and the CI/CD pipeline.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Utilize semantic or timestamped sequencing:&lt;/strong&gt;&lt;br&gt;
 Instead of using sequential integers (like 001, 002) which easily conflict, use UTC timestamps (e.g., 20260618120400_add_users_table) to allow concurrent migrations to coexist.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Adopt the expand and contract pattern:&lt;/strong&gt;&lt;br&gt;
 For zero-downtime deployments, split destructive changes into phases. First, add the new structure (expand), migrate the data safely, and only remove the old structure (contract) in a later release.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Incorporate automated schema validation:&lt;/strong&gt; &lt;br&gt;
Integrate linters and dry-run tests into your pull request workflow to catch syntax errors and unsafe operations before they ever touch a live database.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Sumita&lt;br&gt;
Web Developer&lt;/strong&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/20877/managing-database-schema-like-code-the-quest-for-database-version-control</guid>
<pubDate>Thu, 18 Jun 2026 06:08:16 +0000</pubDate>
</item>
<item>
<title>Supply Chain Sense: Merging Gemini AI and Math for Smart Retail Inventory</title>
<link>https://coderlegion.com/20494/supply-chain-sense-merging-gemini-ai-and-math-for-smart-retail-inventory</link>
<description>&lt;p&gt;In neighbourhood retail markets, local Kirana stores, and hyper-local fulfilment centres, inventory management isn't an administrative task—it's a high-stakes daily tightrope walk. If a shop owner over-orders fresh dairy, poultry, or vegetables, the items rot on the shelves, expiration windows slam shut, and cold-storage electricity bills quickly wipe out their slim profit margins.&lt;/p&gt;
&lt;p&gt;Conversely, if they under-order to play it safe, modern quick-commerce delivery grids and retail distributors issue immediate algorithmic visibility penalties, dropping their store placement and instantly handing premium shelf space to competitors. Traditional inventory tracking relies entirely on static, backward-looking spreadsheets that fail the moment a real-world disruption strikes. &lt;/p&gt;
&lt;p&gt;To solve this, I engineered Supply Chain Sense: an open-source, AI-driven inventory optimization suite that bridges the gap between unstructured real-world context and deterministic operations mathematics. By coupling classical Continuous Review Inventory Models with real-time risk contextualization powered by Google Gemini 2.5 Flash, the system translates unpredictable disruption narratives into live, mathematically precise supply chain parameters. &lt;/p&gt;
&lt;p&gt;Here is a look at the foundational constraints that inspired this project, the technical architecture, the cross-category mathematics, and how the application works end-to-end.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Genesis: The Small Business Inventory Paradox&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;When designing an inventory management system specifically for small business retailers and neighbourhood mini-marts, you quickly realize they operate on an incredibly tight rope. Unlike massive retail conglomerates with deep capital reserves, a small business manages a highly diverse product portfolio spanning everything from short-shelf-life dairy to slow-moving electronics. This diversity creates a brutal, high-stakes dual threat:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Risk of Over-Stocking: Spoilage vs. Frozen Capital&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;If a merchant over-orders fresh goods or raw materials, the consequences are immediate. Items rot on shelves, expiration windows slam shut, and the compounding overhead costs of cold-storage electricity and disposal completely obliterate their slim profit margins. On the other hand, if they over-stock high-unit-cost non-perishables (like electronic accessories or cosmetics), items don't rot, but their vital working capital is frozen in dead inventory that sits on a shelf for six months. In this sector, excess inventory is an active financial drain.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Risk of Under-Stocking: Brutal Platform Penalties&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Conversely, under-ordering is equally catastrophic. Modern quick-commerce apps, digital delivery grids, and large distributors enforce strict service-level agreements. If a merchant experiences a stockout on basic items, they face brutal immediate financial penalties, sudden downgrades in platform algorithm visibility, and immediate loss of premium shelf space to competitors. Customers who encounter empty shelves don't just wait for a backorder; they migrate to another provider forever.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Four Key Design Decisions for a Real Product Mix&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;We started this project on a core set of principles tailored specifically to handle a realistic convenience store ledger, shaping how our AI layer reasons about risk across different product categories:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Temporal Risk Window&lt;/strong&gt;: &lt;br&gt;
The ultimate question for fresh inventory isn't just &quot;how much volume do we order?&quot; but &quot;when does our risk window close?&quot; Demand calculation must be incredibly time-sensitive, because a 3-day miscalculation on fresh goods is catastrophic, resulting in a total inventory write-off.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Asymmetric Risk Mapping:&lt;/strong&gt; &lt;br&gt;
The engine recognizes that not all stockouts are created equal. Running out of critical daily staples (like fresh food and beverages) causes severe reputational damage and drops a store's customer retention rate instantly. In contrast, over-stocking a few extra shelf-stable household items is an acceptable, manageable holding cost. The system intentionally treats core staples with an asymmetric safety bias.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Hyper-Sensitivity to Weather &amp;amp; Seasonal Signals:&lt;/strong&gt;&lt;br&gt;
 Fast-moving consumer goods are hyper-reactive to external environmental shifts. Sudden monsoon deluges break transportation corridors while spiking grocery demand, local cultural festivals shift consumption patterns within hours, and high summer temperature spikes require instantaneous adjustments to cold-chain logistics capacity.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The &quot;Virtual COO&quot; Directive:&lt;/strong&gt; &lt;br&gt;
Small business owners do not have the time to decrypt complex academic models or interpret abstract data science jargon. The core output of the engine needed to act as an automated, domain-aware Chief Operating Officer—delivering tactical, empathetic, and category-specific advice that reads like real operational guidance, rather than generic corporate-speak.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Grounding the Operations in Classic Math&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;To maintain absolute structural integrity, the engine completely avoids arbitrary adjustments. Every initialized constraint is directly mapped to established operations research and financial risk theory using a continuous review model framework.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Target Service Levels (SL) and the Z-Score Matrix&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The Target Service Level represents the mathematical probability that a business will successfully fulfil customer orders without experiencing a stockout during a single vendor replenishment cycle. To convert this percentage into an active warehouse cushion, the code maps it onto a standard normal distribution curve using the Inverse Cumulative Distribution Function (also known as the Percent Point Function or Z-score function):&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;em&gt;Z = PPF(SL)&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The application initializes distinct service levels based on the financial and reputational cost of a stockout:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;em&gt;Food and Beverages (99% Service Level, Z approx. 2.33):&lt;/em&gt;&lt;/strong&gt; High protection threshold. If an essential grocery staple is unavailable, customers migrate immediately, inducing harsh penalties.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;em&gt;Electronic Accessories / Home &amp;amp; Lifestyle (85% Service Level, Z approx. 1.04):&lt;/em&gt;&lt;/strong&gt; &lt;br&gt;
Lower protection threshold. These high-value, slow-moving items tie up too much capital if over-stocked, and consumers are typically willing to tolerate minor backorder wait times.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Annual Carrying Cost Rates (I) and Daily Holding Costs (H)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Storing items involves overhead (rent, climate control, insurance, and spoilage). The Annual Carrying Cost Rate (I) represents this physical expense as a percentage of the item's unit price. The engine derives the Daily Holding Cost (H) per item using the following calculation:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;em&gt;H = (Unit Price x I) / 365&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The category rates directly correspond to product characteristics:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;em&gt;Food and Beverages (35% Carrying Rate):&lt;/em&gt;&lt;/strong&gt; High rate driven by extreme perishability, tight shelf-life limitations, and the steep energy costs required for refrigerated storage.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;em&gt;Fashion Accessories (25% Carrying Rate):&lt;/em&gt;&lt;/strong&gt; High rate driven by rapid style obsolescence and seasonal demand expiration.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;em&gt;Electronic Accessories (15% Carrying Rate):&lt;/em&gt;&lt;/strong&gt; Low rate due to high material stability, compact shelf volume, and long physical shelf-life profiles.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Elevating the Square Root of Lead Time&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;em&gt;The Core Mathematics: Why Variance Scales with sqrt(Lead Time)&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A common mistake in basic inventory systems is multiplying demand volatility (std_dev) linearly by the supplier lead time duration. Supply Chain Sense strictly avoids this by implementing standard statistical operations theory: standard deviation does not scale linearly over time; variance does.&lt;/p&gt;
&lt;p&gt;Because daily customer demand behaves like an independent random variable, demand variance aggregates linearly across successive days &lt;em&gt;(Var = std_dev^2 x Lead Time).&lt;/em&gt; To return this back into the physical unit dimension of a standard deviation cushion, we must extract the square root of the temporal window:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;em&gt;Adjusted Volatility Buffer = std_dev x sqrt(Lead Time adjusted)&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This mathematical choice is critical for risk exposure. If a supply chain transit window stretches from 2 days to 8 days (a 4x increase in delay), the actual risk exposure and required safety stock cushion do not quadruple—they double (2x). Utilizing a linear multiplier would cause small businesses to massively over-order, locking up thousands of dollars of vital working capital in redundant safety stock.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;System Architecture &amp;amp; Fail-Safe Design&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;To ensure true production-grade resilience for small businesses, the application handles data extraction and API calling defensively through two distinct mechanisms:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;em&gt;Deterministic Edge-Computed Heuristic Fallback:&lt;/em&gt;&lt;/strong&gt; In real-world retail environments, you cannot assume 100% cloud uptime. If a neighborhood merchant is facing a severe regional crisis—like a monsoon or a transport strike—cellular networks often experience heavy traffic spikes or blackouts. If our software relied entirely on a stable cloud connection to reach Google Gemini, a network drop would completely freeze the store's inventory pipeline. &lt;/p&gt;
&lt;p&gt;To prevent this single point of failure, the architecture includes a deterministic backup engine that executes completely offline on the edge, instantly parsing text logs using keyword tracking and lookahead loops if the cloud API becomes unreachable.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def intelligent_local_fallback(text_input, unique_categories):
    &quot;&quot;&quot;
    Edge-computed deterministic backup engine to handle cloud network traffic spikes.
    &quot;&quot;&quot;
    text = text_input.lower()
    detected_delay = 0
    words = text.split()
    
    # Extract temporal delay window from unstructured text
    for i, word in enumerate(words):
        if word in ['day', 'days', '-day'] and i &amp;gt; 0:
            clean_num = words[i-1].replace('-', '').strip()
            if clean_num.isdigit():
                detected_delay = int(clean_num)
                break

    # Default baseline delay handling for critical supply disruptions
    if detected_delay == 0 and any(k in text for k in ['strike', 'block', 'flood', 'storm', 'delay', 'cyclone']):
        detected_delay = 3

    food_shock = 1.00
    others_shock = 1.00
    
    # Asymmetric risk classification based on macro events
    if any(k in text for k in ['strike', 'block', 'protest', 'highway']):
        food_shock = 1.15
    elif any(k in text for k in ['cyclone', 'flood', 'storm', 'monsoon', 'weather']):
        food_shock = 1.60
        others_shock = 0.40
    elif any(k in text for k in ['holiday', 'festival', 'celebration', 'christmas']):
        food_shock = 1.30
        others_shock = 1.10

    adjustments = {}
    for cat in unique_categories:
        if any(f_word in cat.lower() for f_word in ['food', 'beverage', 'grocery', 'fresh', 'eat']):
            adjustments[cat] = { &quot;Demand_Shock_Factor&quot;: food_shock, &quot;Lead_Time_Delay_Days&quot;: detected_delay }
        else:
            adjustments[cat] = { &quot;Demand_Shock_Factor&quot;: others_shock, &quot;Lead_Time_Delay_Days&quot;: detected_delay }

    return {
        &quot;disruption_summary&quot;: f&quot;Local Fail-Safe Active: Inferred operational shifts with an estimated +{detected_delay} day transit bottleneck.&quot;,
        &quot;department_adjustments&quot;: adjustments
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;let-s-break-down-exactly-how-this-fallback-engine-isolates-critical-data-points-without-an-external-ai-model-&quot;&gt;Let's break down exactly how this fallback engine isolates critical data points without an external AI model:&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Text Tokenization &amp;amp; Temporal Search:&lt;/strong&gt; The code normalizes the user's input with &lt;em&gt;.lower()&lt;/em&gt; and splits it into individual words. It then searches for anchor terms like &lt;em&gt;['day', 'days', '-day']&lt;/em&gt;. When found, it utilizes a relative index lookahead (words[i-1]) to extract the preceding character, strips out hyphens, and runs &lt;em&gt;.isdigit()&lt;/em&gt; to safely convert natural language mentions of transit timelines &lt;em&gt;(e.g., &quot;5-day delay&quot;)&lt;/em&gt; into an integer.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Keyword Risk Mapping:&lt;/strong&gt; If no explicit day count is found but critical disruption keywords &lt;em&gt;(like strike&lt;/em&gt;, &lt;em&gt;flood, or cyclone)&lt;/em&gt; are present, the algorithm applies a defensive baseline delay of 3 days to keep safety parameters conservative.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Hardcoded Asymmetric Shocks:&lt;/strong&gt; The function mimics our primary &lt;em&gt;Gemini AI&lt;/em&gt; layer by applying uneven demand modifiers based on the crisis profile. For instance, if weather keywords like monsoon or flood are detected, it applies an aggressive 1.60 demand acceleration factor to essential groceries &lt;em&gt;(food_shock)&lt;/em&gt; due to panic buying patterns, while deliberately dialing down discretionary categories to 0.40 &lt;em&gt;(others_shock)&lt;/em&gt; to preserve store capital.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Schema Matching:&lt;/strong&gt; Finally, it iterates through the active store channels and structures its return payload as a dictionary that precisely mimics the JSON schema generated by &lt;em&gt;Gemini&lt;/em&gt;. This allows the core pandas continuous review loop to ingest the data seamlessly without knowing whether the parameters came from the cloud or the local edge.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Schema-Enforced Structural Reliability:&lt;/strong&gt; By forcing the model configuration parameters to return exact, structured JSON maps &lt;em&gt;(response_mime_type=&quot;application/json&quot;)&lt;/em&gt;, the system bypasses unpredictable conversational AI filler text, mapping narrative updates straight into pandas data manipulation streams without any risk of execution structural runtime crashes.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Real-World Simulation: Asymmetric Trade-Offs in Action&lt;/strong&gt;&lt;br&gt;
Let's evaluate how the system handles an intense multi-channel crisis scenario. Imagine an operator inputs the following unstructured update into the live feed:&lt;/p&gt;
&lt;p&gt;&lt;em&gt;&quot;Heavy storms and flash floods have blocked major highway entry points, delaying incoming cargo trucks by roughly 5 days. Local news is predicting intense panic buying on household groceries, while clothing and luxury retail spots expect massive drops in foot traffic.&quot;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The platform routes this alert to the cloud, executes the optimization models, and produces asymmetric parameter overrides:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Case A: The High-Risk Channel (Food &amp;amp; Beverages)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;AI Extraction:&lt;/strong&gt; Gemini reads the text and extracts a &lt;em&gt;Demand_Shock_Factor of 1.5 (50% velocity&lt;/em&gt; &lt;em&gt;acceleration)&lt;/em&gt; &lt;em&gt;and a Lead_Time_Delay_Days of +5.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Mathematics:&lt;/strong&gt; Total lead time expands from 2 days to 7 days. Simultaneously, average daily consumption &lt;em&gt;(avg_demand)&lt;/em&gt; and standard deviation &lt;em&gt;(std_dev)&lt;/em&gt; are scaled up by 1.5x to reflect panic-buying. Because consumption velocity and transit delays escalate together, the mathematical engine drives the Optimized Reorder Point upward significantly. This immediate surge creates a massive inventory buffer, keeping essential items on shelves throughout the storm and avoiding platform penalties.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=11722763222143481030&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Fig 1: The High-Risk Channel (Food &amp;amp; Beverages)&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Case B: The Low-Risk Channel (Electronic Accessories or Fashion)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;AI Extraction:&lt;/strong&gt; Gemini recognizes that discretionary segments will experience a steep decline in foot traffic as consumers focus purely on survival essentials, assigning a compressed &lt;em&gt;Demand_Shock_Factor of 0.5.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Mathematics&lt;/strong&gt;: Even though the regional transit delay pushes the supplier lead time window to 7 days, consumer buying activity drops by half. The engine balances these opposing metrics and drops the Optimized Reorder Point down safely.&lt;br&gt;
&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=18273870948296921422&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Fig 2: The Low-Risk Channel (Electronic Accessories or Fashion)&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Strategic Value&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This defensive suppression is where the system shines. By scaling down ordering triggers for durable, discretionary goods, the application &lt;strong&gt;safeguards the merchant's working capital&lt;/strong&gt;. The money saved by not ordering slow-moving items is automatically redirected to finance the critical safety stock required for high-demand groceries.&lt;/p&gt;
&lt;p&gt;The system then delivers customized &lt;em&gt;&quot;Virtual COO&quot;&lt;/em&gt; textual guidance, explaining the exact logic of these changes in clear, operational terms before generating a downloadable blueprint (.CSV) that can be fed directly into automated procurement software.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=16317371560269993307&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Fig 3: Downloadable Tailored Inventory Blueprint (.csv) available after analysis&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Enterprise-Grade Security: Simple Cloud Key Management&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;When deploying an open-source data application to public repositories like GitHub, security is a paramount concern. Hardcoding API keys or committing credential files (.env, .json) directly into a repository is an immediate security vulnerability that invites token theft and exploitation.&lt;/p&gt;
&lt;p&gt;However, forcing small business owners to set up complex local environment variables manually defeats the purpose of building a lean, accessible application. To solve this, the architecture abstracts credential handling completely away from the source code, deploying an encrypted, cloud-native runtime boundary check that integrates seamlessly with hosted environment secret managers.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if &quot;GEMINI_API_KEY&quot; in st.secrets:
    os.environ[&quot;GEMINI_API_KEY&quot;] = st.secrets[&quot;GEMINI_API_KEY&quot;]
    try:
        client = genai.Client()
    except Exception as e:
        st.error(f&quot;Failed to initialize Gemini Client: {e}&quot;)
else:
    st.error(&quot;**Security Token Missing!** Please add your `GEMINI_API_KEY` to your Streamlit Advanced Secrets panel.&quot;)
    st.stop()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Encrypted Sandbox Introspection:&lt;/strong&gt; The application uses Streamlit's native dictionary abstraction, &lt;em&gt;st.secrets,&lt;/em&gt; to check for the presence of the authentication token in the cloud's secure memory space. This completely removes the need to track, store, or manage sensitive string files within your Git tree.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Environment Variable Synchronization&lt;/strong&gt;: Once identified, the token is bound directly into the operating system's local memory environment using &lt;em&gt;os.environ[&quot;GEMINI_API_KEY&quot;]&lt;/em&gt;. This ensures the downstream Google GenAI SDK can implicitly fetch the credential without requiring explicit, plain-text string handling inside individual function parameters.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Defensive Try-Except Wrapping:&lt;/strong&gt; The instantiation call &lt;em&gt;genai.Client()&lt;/em&gt; is safely wrapped inside an execution monitoring block. If a network configuration issue, invalid credential structure, or handshake error occurs, the system catches the failure gracefully with except &lt;em&gt;Exception as e&lt;/em&gt; and prints a clean user-facing error message without causing a total application crash.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Assertive Streamlit Halting:&lt;/strong&gt; If the secret token is missing entirely from the cloud environment, the code displays a clear markdown warning message and immediately invokes &lt;em&gt;st.stop()&lt;/em&gt;. This acts as an assertive architectural boundary, completely freezing execution to prevent the optimization engine from crashing or attempting unauthorized operations further down the line.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Conclusion: Grounding Agri-Retail in Real-World Context&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;By anchoring the natural language comprehension of Large Language Models directly within rigorous statistical inventory control equations, we can build applications that are highly intuitive yet mathematically sound. Supply Chain Sense demonstrates its true value when dropped into highly volatile, localized ecosystems like the Indian Agri-retail corridor.&lt;/p&gt;
&lt;p&gt;In a sector where a sudden monsoon deluge halts transport logistics overnight, local wholesale mandis face sudden price shocks, and regional festivals cause hyper-local spikes in demand for dairy and perishables within hours, static software fails.&lt;/p&gt;
&lt;p&gt;Generative AI serves as a real-time logical reasoning layer that reads unstructured news alerts, interprets local environmental context, and immediately updates physical warehouse thresholds.&lt;/p&gt;
&lt;p&gt;This architecture proves that AI's greatest utility isn't generating open-ended conversational filler, but acting as a dynamic data-engineering bridge—shielding small merchants from stockouts, eliminating food spoilage, and protecting thin margins in an unpredictable world.&lt;/p&gt;
&lt;p&gt;The full repository is completely open-source, features a seamless downloadable blueprint generator for automated purchasing setups, and is ready for use.&lt;/p&gt;
&lt;p&gt;Check out the code and deploy it for your business here: &lt;a rel=&quot;nofollow&quot; href=&quot;http://github.com/Ishaa2004&quot;&gt;github.com/Ishaa2004&lt;/a&gt;&lt;br&gt;
Let's connect on LinkedIn: &lt;a rel=&quot;nofollow&quot; href=&quot;http://www.linkedin.com/in/isha-gupta12/&quot;&gt;linkedin.com/in/isha-gupta12&lt;/a&gt;Open to freelance projects: Need a custom dashboard, inventory model, or data pipeline for your business? I take on select freelance projects. [View my gigs on Fiverr][3] or reach out directly on LinkedIn.&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.fiverr.com/ishagupta102&quot;&gt;https://www.fiverr.com/ishagupta102&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/20494/supply-chain-sense-merging-gemini-ai-and-math-for-smart-retail-inventory</guid>
<pubDate>Sat, 13 Jun 2026 05:51:18 +0000</pubDate>
</item>
<item>
<title>Monitoring the Shift: How to Effectively Track SQL Server Table Changes</title>
<link>https://coderlegion.com/20397/monitoring-the-shift-how-to-effectively-track-sql-server-table-changes</link>
<description>&lt;p&gt;In modern data environments, knowing exactly when, how, and who changed your data is paramount for auditing, synchronization, and debugging. Implementing a reliable tracking system requires a solid blueprint, an understanding of potential roadblocks, and modern architectural solutions.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Initial Blueprint: How to Build It&lt;/strong&gt;&lt;br&gt;
To begin tracking modifications in an SQL Server database, developers traditionally lean on a few standard database-level mechanisms.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Audit Columns and Triggers:&lt;/strong&gt; The classic approach involves adding LastModifiedDate and ModifiedBy columns directly to your tables. You then deploy AFTER UPDATE and AFTER INSERT triggers to automatically populate these timestamps whenever a row is modified.&lt;/p&gt;
&lt;p&gt;Temporal Tables (System-Versioned): A more modern built-in approach where SQL Server automatically manages a parallel history table. Every time a row is updated or deleted, the old version is seamlessly archived with a validity period.&lt;/p&gt;
&lt;p&gt;The Real-World Roadblocks: What Will Go Wrong&lt;br&gt;
While the initial setup seems straightforward, deploying these methods in high-traffic production environments introduces significant engineering challenges.&lt;/p&gt;
&lt;p&gt;Severe Performance Degradation: Heavy use of synchronous triggers introduces massive transactional overhead. Every write operation is forced to wait for the trigger logic to execute, causing blocking, deadlocks, and slowed application response times.&lt;/p&gt;
&lt;p&gt;Storage Explosion: Tracking every single micro-change means your database size will balloon rapidly. Without an automated data-retention and purging strategy, you will quickly run out of disk space.&lt;/p&gt;
&lt;p&gt;Schema Evolution Fragility: Traditional tracking triggers are tightly coupled to the table structure. The moment a schema change occurs (like adding or dropping a column), your tracking scripts can break completely, leading to silent data loss or application crashes.&lt;/p&gt;
&lt;p&gt;The Modern Resolution: How We Solve These Problems&lt;br&gt;
To bypass the performance and maintenance nightmares of triggers, we must pivot toward asynchronous, engine-level features native to SQL Server.&lt;/p&gt;
&lt;p&gt;Implementing Change Data Capture (CDC): Instead of using triggers that intercept active queries, CDC reads the transaction log asynchronously. This completely eliminates runtime performance overhead on user queries while capturing full INSERT, UPDATE, and DELETE histories.&lt;/p&gt;
&lt;p&gt;Utilizing Change Tracking (CT): If you only need to know which rows changed (and not the historical values themselves), Change Tracking is the lightweight solution. It answers the question &quot;Has this row changed since version X?&quot; with minimal storage footprint, making it ideal for cache synchronization.&lt;/p&gt;
&lt;p&gt;Automated Partitioning and Offloading: To solve the storage crisis, history tables should be partitioned by date. Older audit logs should be automatically archived to cheaper cold storage (like Azure Blob Storage or an external data lake) to keep the primary transactional database lean and fast.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Sumita &lt;br&gt;
Web Developer&lt;/strong&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/20397/monitoring-the-shift-how-to-effectively-track-sql-server-table-changes</guid>
<pubDate>Fri, 12 Jun 2026 11:32:51 +0000</pubDate>
</item>
<item>
<title>Your Data Strategy Is Backwards. Here's the Pattern That Actually Works.</title>
<link>https://coderlegion.com/20321/your-data-strategy-is-backwards-heres-the-pattern-that-actually-works</link>
<description>&lt;p&gt;The numbers are hard to ignore. Eighty-seven percent of organizations are investing in data and analytics. Seventy-four percent are struggling to achieve and scale real value from it. The average Chief Data Officer lasts just 2.5 years in the role before moving on.&lt;/p&gt;
&lt;p&gt;Something is structurally wrong — and according to Nysa Zaran Evagelinos, Research Director at Info-Tech Research Group, the problem starts with how organizations think about data products.&lt;/p&gt;
&lt;p&gt;&quot;Why, given that we have so much data, given that we all do things with this data, is it always so challenging?&quot; she asked, opening the final day keynote at Info-Tech LIVE 2026 in Las Vegas. &quot;It's not a niche discipline. There's so much advice out there. Where do we keep falling short?&quot;&lt;/p&gt;
&lt;h3 id=&quot;the-data-product-problem&quot;&gt;The Data Product Problem&lt;/h3&gt;
&lt;p&gt;The current industry answer to that question is the data product — owned, trusted, reusable, discoverable. Vendors and analysts have been promoting the concept as the path to data-driven value for several years now.&lt;/p&gt;
&lt;p&gt;But the results tell a different story. According to a 2026 study of data engineering organizations, 70% of data product initiatives stall after 12 to 18 months.&lt;/p&gt;
&lt;p&gt;The reason, Zaran argued, is that most data products solve the wrong problem. They deliver real value — but only within the CIO and CDO portfolio. Faster time-to-incident-detection. Better time-to-resolution. Reduced data downtime. Quicker analytics deployment. These are efficiency gains for the data team, not enterprise value drivers.&lt;/p&gt;
&lt;p&gt;&quot;The results are limited to running a more efficient shop,&quot; she said. &quot;But the mission is to drive broader transformation — shift the culture, make a difference to the organization. Not just run a more efficient pipeline.&quot;&lt;/p&gt;
&lt;h3 id=&quot;the-data-value-chain&quot;&gt;The Data Value Chain&lt;/h3&gt;
&lt;p&gt;The framework Zaran introduced reframes the problem entirely. Instead of starting with data, she starts with value — and works backward.&lt;/p&gt;
&lt;p&gt;The Data Value Chain runs from left to right: Data → Insight → Decision → Action → Value. AI maps directly onto this chain, perceiving and learning on the left side, reasoning and planning in the middle, creating and acting on the right.&lt;/p&gt;
&lt;p&gt;Most organizations have invested heavily in the left side of that chain. They've built data infrastructure, established pipelines, created dashboards, and produced insights. What they've largely failed to do is own the right side — the leap from Decision to Action, and from Action to measurable Value. That leap, Zaran noted, is where data culture either exists or it doesn't.&lt;/p&gt;
&lt;p&gt;&quot;The business is left to own the rest of the chain,&quot; she said. &quot;And the gap to value remains.&quot;&lt;/p&gt;
&lt;h3 id=&quot;four-companies-that-closed-the-gap&quot;&gt;Four Companies That Closed the Gap&lt;/h3&gt;
&lt;p&gt;To illustrate what it looks like to own the full chain, Zaran walked through four case studies — each representing a different entry point on the value chain.&lt;/p&gt;
&lt;p&gt;Bloomberg Terminal anchored the left side. With 10 billion data points published daily across 8,000 curated datasets and 40,000 data fields, Bloomberg's product goal was straightforward: connect decision-makers to exceptional data they couldn't get anywhere else. The result is roughly $15 billion in annual revenue.&lt;/p&gt;
&lt;p&gt;Netflix moved further right. Rather than selling data, Netflix built decision intelligence — a feedback loop that uses viewing behavior to drive personalization, content strategy, and a low-friction experience. The system continuously collects data, predicts preferences, serves content, and refines itself. Revenue: $45.18 billion, with $3.4 billion invested annually in technology and development.&lt;/p&gt;
&lt;p&gt;UPS ORION pushed into action. The route optimization system — a copilot for drivers and logistics planners — took 10 years and $250 million to build. The payoff: $300 to $400 million in annual savings and 100,000 tonnes of carbon emissions avoided. The hardest part wasn't the algorithm. It was getting drivers to trust it enough to follow it.&lt;/p&gt;
&lt;p&gt;Waymo went all the way to autonomy. Five hundred thousand rides per week, 200 million miles logged, operating in 10 US cities. The data product isn't a dashboard or a recommendation engine — it's a system that perceives, decides, and acts without human intervention. Governance, Zaran noted, means running billions of simulations so the system never encounters a scenario it hasn't already seen.&lt;/p&gt;
&lt;p&gt;&quot;Each of these companies figured out a different way to use the data value chain,&quot; she said. &quot;They each stopped at a different point — and in doing so, they each had different problems to solve.&quot;&lt;/p&gt;
&lt;h3 id=&quot;the-winning-pattern&quot;&gt;The Winning Pattern&lt;/h3&gt;
&lt;p&gt;What separates these companies from the 74% struggling to scale isn't better data infrastructure. It's where they started.&lt;/p&gt;
&lt;p&gt;Most organizations start on the left: collect data, build pipelines, create products, and hope value follows. The winning pattern does the opposite — start on the right and backtrack.&lt;/p&gt;
&lt;p&gt;Four questions drive the approach: What outcomes does the organization actually care about? What actions and decisions drive those outcomes? Can data move the needle on those decisions? And do we have the data, or can we get it?&lt;/p&gt;
&lt;p&gt;&quot;A data product is almost a byproduct,&quot; Zaran said. &quot;It's a byproduct of the discipline of running an outcome end-to-end using data. You measure it by its purpose. You measure it by the downstream change it creates.&quot;&lt;/p&gt;
&lt;p&gt;The implication for AI is direct. Organizations racing to deploy AI tools, agents, and copilots face the same trap that data products fell into — investing heavily in capabilities without anchoring them to a specific outcome the organization cares about.&lt;/p&gt;
&lt;p&gt;&quot;This is how AI becomes a tool,&quot; Zaran said. &quot;Not your destination.&quot;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/20321/your-data-strategy-is-backwards-heres-the-pattern-that-actually-works</guid>
<pubDate>Thu, 11 Jun 2026 16:42:39 +0000</pubDate>
</item>
<item>
<title>The Hidden Cost of Distributed Systems</title>
<link>https://coderlegion.com/20293/the-hidden-cost-of-distributed-systems</link>
<description>&lt;p&gt;One of the most interesting contradictions in cloud architecture is that we spend a tremendous amount of effort trying to eliminate complexity, yet many of the systems we build seem to become more complex over time.&lt;/p&gt;
&lt;p&gt;A startup might begin with a single application, a single database, and a handful of users. Everything is relatively easy to understand. A request comes in, business logic is executed, data is stored, and a response is returned. The architecture is straightforward, deployments are simple, and troubleshooting usually involves looking in one place.&lt;/p&gt;
&lt;p&gt;Then growth happens.&lt;/p&gt;
&lt;p&gt;New customers arrive. New features are requested. More engineers join the team. Integrations are added. Reporting requirements expand. Mobile applications appear. Real-time processing becomes important. What was once a single application gradually evolves into a collection of services, databases, queues, and APIs.&lt;/p&gt;
&lt;p&gt;Without necessarily realizing it, the organization has started building a distributed system.&lt;/p&gt;
&lt;h3 id=&quot;what-exactly-is-a-distributed-system-&quot;&gt;What Exactly Is a Distributed System?&lt;/h3&gt;
&lt;p&gt;At its core, a distributed system is simply a group of independent components working together to provide what users perceive as a single application.&lt;/p&gt;
&lt;p&gt;Consider what happens when a customer places an order on an e-commerce platform. From the customer's perspective, the experience feels simple: click a button, receive a confirmation, and wait for the package to arrive.&lt;/p&gt;
&lt;p&gt;Behind the scenes, however, the request may travel through multiple services. One service records the order, another processes the payment, another updates inventory, while additional systems generate notifications, update dashboards, and trigger downstream workflows. Each component owns a specific responsibility, yet all of them collaborate to complete a single business transaction.&lt;/p&gt;
&lt;p&gt;This separation of responsibilities is one of the defining characteristics of distributed systems.&lt;/p&gt;
&lt;h3 id=&quot;why-do-we-build-them-&quot;&gt;Why Do We Build Them?&lt;/h3&gt;
&lt;p&gt;If distributed systems introduce additional complexity, a reasonable question follows:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why not keep everything inside a single application?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The answer is that distributed systems solve problems that become increasingly difficult to address as organizations grow.&lt;/p&gt;
&lt;p&gt;Imagine an online retailer preparing for Black Friday. The checkout process may suddenly experience a massive increase in traffic, while administrative reporting tools remain largely unchanged. In a monolithic architecture, scaling one component often means scaling the entire application. In a distributed architecture, services can scale independently based on their specific needs.&lt;/p&gt;
&lt;p&gt;The benefits extend beyond scalability. Distributed systems also help organizations isolate failures, allow teams to work more independently, and enable different technologies to be used for different workloads. As systems and engineering organizations grow, these advantages become increasingly valuable.&lt;/p&gt;
&lt;p&gt;This is why most modern cloud-native architectures eventually move in this direction.&lt;/p&gt;
&lt;p&gt;Not because distributed systems are fashionable.&lt;/p&gt;
&lt;p&gt;Because they solve real business and operational challenges.&lt;/p&gt;
&lt;h3 id=&quot;the-trade-offs-begin-the-moment-you-split-the-system&quot;&gt;The Trade-Offs Begin the Moment You Split the System&lt;/h3&gt;
&lt;p&gt;The problem is that complexity does not disappear.&lt;/p&gt;
&lt;p&gt;It simply moves.&lt;/p&gt;
&lt;p&gt;Inside a monolithic application, communication often happens through function calls within the same process. In a distributed system, communication happens over a network. That seemingly small change introduces an entirely new category of problems.&lt;/p&gt;
&lt;p&gt;Networks introduce latency. Requests time out. Connections fail. Messages arrive late. Sometimes they arrive twice. Occasionally they never arrive at all.&lt;/p&gt;
&lt;p&gt;Engineers must now design systems that can tolerate these realities.&lt;/p&gt;
&lt;p&gt;The architecture becomes more flexible, but it also becomes less predictable.&lt;/p&gt;
&lt;h3 id=&quot;data-consistency-becomes-a-design-problem&quot;&gt;Data Consistency Becomes a Design Problem&lt;/h3&gt;
&lt;p&gt;One of the biggest mindset shifts occurs when data is no longer stored in a single place.&lt;/p&gt;
&lt;p&gt;Imagine a payment service successfully charges a customer. Immediately afterward, the inventory service fails before reserving the product. The customer has paid, but the order cannot be fulfilled.&lt;/p&gt;
&lt;p&gt;What should happen next?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Should the payment be refunded automatically?&lt;/li&gt;
&lt;li&gt;Should the inventory operation be retried?&lt;/li&gt;
&lt;li&gt;Should the order remain pending until a human intervenes?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These questions rarely emerge in simple applications, but they appear regularly in distributed systems.&lt;/p&gt;
&lt;p&gt;As a result, engineers begin adopting concepts such as &lt;strong&gt;eventual consistency&lt;/strong&gt;, &lt;strong&gt;retries&lt;/strong&gt;, &lt;strong&gt;idempotency&lt;/strong&gt;, &lt;strong&gt;dead-letter queues&lt;/strong&gt;, and &lt;strong&gt;event-driven communication patterns&lt;/strong&gt;. These are not merely technical buzzwords. They are practical responses to the challenges created by distributing responsibilities across multiple services.&lt;/p&gt;
&lt;h3 id=&quot;choosing-complexity-carefully&quot;&gt;Choosing Complexity Carefully&lt;/h3&gt;
&lt;p&gt;One of the most valuable lessons in software architecture is that every decision involves trade-offs.&lt;/p&gt;
&lt;p&gt;Distributed systems provide scalability, resilience, and organizational flexibility. They also introduce operational overhead, additional failure modes, and significantly more complexity.&lt;/p&gt;
&lt;p&gt;For that reason, building a distributed system should never be the objective.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Solving a business problem should be the objective.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Sometimes that solution is a distributed architecture. Sometimes it is a well-designed monolith. The right answer depends entirely on the requirements, constraints, and growth trajectory of the system.&lt;/p&gt;
&lt;h3 id=&quot;final-thoughts&quot;&gt;Final Thoughts&lt;/h3&gt;
&lt;p&gt;Distributed systems power many of the applications we use every day. They enable organizations to scale beyond the limits of a single application and support levels of reliability that modern users have come to expect.&lt;/p&gt;
&lt;p&gt;At the same time, every service added to an architecture creates another relationship that must be understood, monitored, and maintained.&lt;/p&gt;
&lt;p&gt;That is the hidden cost of distributed systems.&lt;/p&gt;
&lt;p&gt;They give us the ability to scale, but they require us to manage a level of complexity that simply does not exist in smaller architectures.&lt;/p&gt;
&lt;p&gt;And that is why understanding distributed systems is not really about learning new technologies.&lt;/p&gt;
&lt;p&gt;It is about understanding the trade-offs that come with growth.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/20293/the-hidden-cost-of-distributed-systems</guid>
<pubDate>Thu, 11 Jun 2026 12:12:07 +0000</pubDate>
</item>
<item>
<title>go-intake v0.1.0: A Small Go Library for Messy Data Intake</title>
<link>https://coderlegion.com/20043/go-intake-v0-1-0-a-small-go-library-for-messy-data-intake</link>
<description>&lt;h2 id=&quot;go-intake-go-native-streaming-data-ingestion-toolkit&quot;&gt;go-intake: Go-Native Streaming Data Ingestion Toolkit&lt;/h2&gt;
&lt;h3 id=&quot;executive-summary&quot;&gt;Executive Summary&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;go-intake&lt;/strong&gt; is a minimalist, streaming-first ETL toolkit for Go developers that transforms messy data into validated, record-oriented output. With zero third-party dependencies and a focus on composition over configuration, it provides enterprise-grade data pipeline capabilities in a single, embeddable library.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;architecture-overview&quot;&gt;Architecture Overview&lt;/h3&gt;
&lt;h4 id=&quot;core-abstraction-layers&quot;&gt;Core Abstraction Layers&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Component &lt;/th&gt;&lt;th&gt;Interface &lt;/th&gt;&lt;th&gt;Responsibility &lt;/th&gt;&lt;th&gt;Pattern&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Source&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;3-method interface (&lt;code&gt;Open&lt;/code&gt;, &lt;code&gt;Read&lt;/code&gt;, &lt;code&gt;Close&lt;/code&gt;) &lt;/td&gt;&lt;td&gt;Data ingestion from CSV, JSONL &lt;/td&gt;&lt;td&gt;Stream until EOF&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Transformer&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;&lt;code&gt;Apply(ctx, Record) (Record, error)&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Field normalization, type parsing &lt;/td&gt;&lt;td&gt;Immutable, returns copy&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Validator&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;&lt;code&gt;Validate(ctx, Record) error&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Schema enforcement, business rules &lt;/td&gt;&lt;td&gt;Read-only&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Quarantine&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;&lt;code&gt;Write(ctx, InvalidRecord)&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Rejected record capture with context &lt;/td&gt;&lt;td&gt;Structured error logging&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Sink&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;3-method interface (&lt;code&gt;Open&lt;/code&gt;, &lt;code&gt;Write&lt;/code&gt;, &lt;code&gt;Close&lt;/code&gt;) &lt;/td&gt;&lt;td&gt;Output to CSV, JSONL &lt;/td&gt;&lt;td&gt;Streaming write&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4 id=&quot;design-principles&quot;&gt;Design Principles&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;┌─────────────┐    ┌──────────────┐    ┌─────────────┐
│   Source    │───▶│ Transformers │───▶│ Validators  │
└─────────────┘    └──────────────┘    └─────────────┘
                                            │
                               ┌────────────┴──────────┐
                               ▼                       ▼
                         ┌──────────┐           ┌───────────┐
                         │   Sink   │           │Quarantine  │
                         └──────────┘           └───────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;memory-usage-comparison&quot;&gt;Memory Usage Comparison&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;Memory (MB)
   500 │                      
       │    ●●●●●●●●●●●●●●●●●●  go-intake (constant)
       │                      
   100 │                      
       │                      
    50 │    ████ goflow grows with dataset
       │    ██████            
    25 │    ████████████      
       │    ████████████████  streamz grows
       │                       
     0 └──────────────────────
        0K    50K   100K   150K
           Records Processed
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;performance-benchmarks&quot;&gt;Performance Benchmarks&lt;/h3&gt;
&lt;h4 id=&quot;throughput-analysis&quot;&gt;Throughput Analysis&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Dataset Size &lt;/th&gt;&lt;th&gt;Records Processed &lt;/th&gt;&lt;th&gt;Time Elapsed &lt;/th&gt;&lt;th&gt;Throughput&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Small (10K) &lt;/td&gt;&lt;td&gt;10,000 &lt;/td&gt;&lt;td&gt;~40ms &lt;/td&gt;&lt;td&gt;~250,000/sec&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Medium (100K) &lt;/td&gt;&lt;td&gt;100,000 &lt;/td&gt;&lt;td&gt;~410ms &lt;/td&gt;&lt;td&gt;~243,902/sec&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Large (161K) &lt;/td&gt;&lt;td&gt;161,568 &lt;/td&gt;&lt;td&gt;643ms &lt;/td&gt;&lt;td&gt;&lt;strong&gt;251,000/sec&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;Memory Efficiency:&lt;/strong&gt; The streaming model ensures constant memory usage regardless of dataset size. Only one record exists in memory at any point during processing.&lt;/p&gt;
&lt;h4 id=&quot;comparative-performance&quot;&gt;Comparative Performance&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-mermaid&quot;&gt;graph LR
    A[go-intake&amp;lt;br/&amp;gt;251K/s] --&amp;gt; B[Memory: O(1)]
    C[Gopherize&amp;lt;br/&amp;gt;125K/s] --&amp;gt; D[Memory: O(n)]
    E[goflow&amp;lt;br/&amp;gt;180K/s] --&amp;gt; F[Memory: O(n)]
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;performance-scaling-chart&quot;&gt;Performance Scaling Chart&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;Records/sec
   300K │
        │
   250K │    ● (go-intake: 251K)
        │   ╱
   200K │  ╱
        │ ╱
   150K │● (goflow: 180K)
        │
   100K │● (streamz)
        │
    50K │● (Gopherize: 125K)
        │
     0K └─────────────────────
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;feature-matrix-vs-competition&quot;&gt;Feature Matrix vs Competition&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Feature &lt;/th&gt;&lt;th&gt;go-intake &lt;/th&gt;&lt;th&gt;Gopherize &lt;/th&gt;&lt;th&gt;goflow &lt;/th&gt;&lt;th&gt;streamz&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;✅ Zero Dependencies &lt;/td&gt;&lt;td&gt;✓ &lt;/td&gt;&lt;td&gt;✗ &lt;/td&gt;&lt;td&gt;✗ &lt;/td&gt;&lt;td&gt;✗&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;✅ Streaming Model &lt;/td&gt;&lt;td&gt;✓ &lt;/td&gt;&lt;td&gt;Partial &lt;/td&gt;&lt;td&gt;✓ &lt;/td&gt;&lt;td&gt;✓&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;✅ Structured Errors &lt;/td&gt;&lt;td&gt;✓ &lt;/td&gt;&lt;td&gt;✗ &lt;/td&gt;&lt;td&gt;Partial &lt;/td&gt;&lt;td&gt;✗&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;✅ Multi-validation Errors &lt;/td&gt;&lt;td&gt;✓ &lt;/td&gt;&lt;td&gt;✗ &lt;/td&gt;&lt;td&gt;✗ &lt;/td&gt;&lt;td&gt;✗&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;✅ Schema Discovery &lt;/td&gt;&lt;td&gt;✓ &lt;/td&gt;&lt;td&gt;✗ &lt;/td&gt;&lt;td&gt;✗ &lt;/td&gt;&lt;td&gt;✗&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;✅ Header Normalization &lt;/td&gt;&lt;td&gt;✓ &lt;/td&gt;&lt;td&gt;✓ &lt;/td&gt;&lt;td&gt;✗ &lt;/td&gt;&lt;td&gt;✗&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;✅ Quarantine Sinks &lt;/td&gt;&lt;td&gt;✓ &lt;/td&gt;&lt;td&gt;✗ &lt;/td&gt;&lt;td&gt;✗ &lt;/td&gt;&lt;td&gt;✗&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;✅ Context Cancellation &lt;/td&gt;&lt;td&gt;✓ &lt;/td&gt;&lt;td&gt;✗ &lt;/td&gt;&lt;td&gt;✓ &lt;/td&gt;&lt;td&gt;✓&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Lines of Code &lt;/td&gt;&lt;td&gt;~2,300 &lt;/td&gt;&lt;td&gt;~8,500 &lt;/td&gt;&lt;td&gt;~12,000 &lt;/td&gt;&lt;td&gt;~6,200&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h3 id=&quot;bundled-components&quot;&gt;Bundled Components&lt;/h3&gt;
&lt;h4 id=&quot;sources-1&quot;&gt;Sources&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;CSVSource&lt;/strong&gt;: RFC 4180 compliant, configurable delimiter, empty line skipping&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;JSONLSource&lt;/strong&gt;: Newline-delimited JSON, 16MB line buffer support&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;sinks&quot;&gt;Sinks&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;CSVSink&lt;/strong&gt;: Automatic header generation, deterministic column ordering&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;JSONLSink&lt;/strong&gt;: HTML-escaping disabled, one object per line&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;transformers&quot;&gt;Transformers&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Function &lt;/th&gt;&lt;th&gt;Purpose &lt;/th&gt;&lt;th&gt;Use Case&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;NormalizeHeaders(style)&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Case normalization &lt;/td&gt;&lt;td&gt;Column standardization&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;TrimStrings()&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Whitespace removal &lt;/td&gt;&lt;td&gt;Data cleaning&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;ParseFloat/Int/Bool/Date&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Type conversion &lt;/td&gt;&lt;td&gt;Schema enforcement&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;Rename/Drop/Keep/Copy&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Schema evolution &lt;/td&gt;&lt;td&gt;ETL pipeline stages&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;AddField&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Constant injection &lt;/td&gt;&lt;td&gt;Metadata enrichment&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;MapField&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Custom transformations &lt;/td&gt;&lt;td&gt;Business logic&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4 id=&quot;validators&quot;&gt;Validators&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Function &lt;/th&gt;&lt;th&gt;Type &lt;/th&gt;&lt;th&gt;Error Aggregation&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;Required(field)&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Presence &lt;/td&gt;&lt;td&gt;✅&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;Min/Max/Between&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Numeric range &lt;/td&gt;&lt;td&gt;✅&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;Regex(field, pattern)&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Pattern matching &lt;/td&gt;&lt;td&gt;✅&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;Enum(field, values...)&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Allowlist &lt;/td&gt;&lt;td&gt;✅&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;Email/URL&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Format validation &lt;/td&gt;&lt;td&gt;✅&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;NotFuture(field)&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Temporal constraint &lt;/td&gt;&lt;td&gt;✅&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h3 id=&quot;real-world-validation-results&quot;&gt;Real-World Validation Results&lt;/h3&gt;
&lt;h4 id=&quot;dataset-1-covid-19-aggregated-data-161-568-records-&quot;&gt;Dataset 1: COVID-19 Aggregated Data (161,568 records)&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Field Profiling Results:&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Field &lt;/th&gt;&lt;th&gt;Type &lt;/th&gt;&lt;th&gt;Confidence &lt;/th&gt;&lt;th&gt;Null %&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Confirmed &lt;/td&gt;&lt;td&gt;int &lt;/td&gt;&lt;td&gt;100% &lt;/td&gt;&lt;td&gt;0%&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Country &lt;/td&gt;&lt;td&gt;string &lt;/td&gt;&lt;td&gt;100% &lt;/td&gt;&lt;td&gt;0%&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Date &lt;/td&gt;&lt;td&gt;date &lt;/td&gt;&lt;td&gt;100% &lt;/td&gt;&lt;td&gt;0%&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Deaths &lt;/td&gt;&lt;td&gt;int &lt;/td&gt;&lt;td&gt;100% &lt;/td&gt;&lt;td&gt;0%&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Recovered &lt;/td&gt;&lt;td&gt;int &lt;/td&gt;&lt;td&gt;100% &lt;/td&gt;&lt;td&gt;0%&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;Pipeline Results:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Read: 161,568&lt;/li&gt;
&lt;li&gt;Written: 161,568&lt;/li&gt;
&lt;li&gt;Invalid: 0&lt;/li&gt;
&lt;li&gt;Failed: 0&lt;/li&gt;
&lt;li&gt;Runtime: 643ms&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;dataset-2-synthetic-messy-data-5-000-records-with-2-5-error-injection-&quot;&gt;Dataset 2: Synthetic Messy Data (5,000 records with 2-5% error injection)&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Statistical Validation:&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Metric &lt;/th&gt;&lt;th&gt;Value &lt;/th&gt;&lt;th&gt;Visualization&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Total Records &lt;/td&gt;&lt;td&gt;5,000 &lt;/td&gt;&lt;td&gt;██████████&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Valid Output &lt;/td&gt;&lt;td&gt;4,772 (95%) &lt;/td&gt;&lt;td&gt;████████░░&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Quarantined &lt;/td&gt;&lt;td&gt;228 (4.56%) &lt;/td&gt;&lt;td&gt;██░░░░░░░░&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Error Detection &lt;/td&gt;&lt;td&gt;100% &lt;/td&gt;&lt;td&gt;██████████&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;Pipeline Accuracy
100% │ ██████████
     │ ██████████  (100% records accounted for)
 95% │ ████████░░  (95% valid)
     │ ████░░░░░░  (4.56% quarantined)
  0% └────────────────
     0%       100%  Accuracy
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Error Distribution in Quarantine:&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Error Type &lt;/th&gt;&lt;th&gt;Count &lt;/th&gt;&lt;th&gt;Description &lt;/th&gt;&lt;th&gt;Distribution&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;required &lt;/td&gt;&lt;td&gt;96 &lt;/td&gt;&lt;td&gt;Missing required fields &lt;/td&gt;&lt;td&gt;████░░░░░░ (29%)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;min &lt;/td&gt;&lt;td&gt;58 &lt;/td&gt;&lt;td&gt;Negative numeric values &lt;/td&gt;&lt;td&gt;██░░░░░░░░ (18%)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;regex &lt;/td&gt;&lt;td&gt;172 &lt;/td&gt;&lt;td&gt;Email format mismatch &lt;/td&gt;&lt;td&gt;██████░░░░ (53%)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;326 &lt;/td&gt;&lt;td&gt;Captured validation errors &lt;/td&gt;&lt;td&gt;██████████ (100%)&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h3 id=&quot;use-cases&quot;&gt;Use Cases&lt;/h3&gt;
&lt;h4 id=&quot;1-data-migration-pipelines&quot;&gt;1. Data Migration Pipelines&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;p := intake.New().
    From(source.CSV(&quot;legacy_export.csv&quot;)).
    Transform(transform.NormalizeHeaders(transform.SnakeCase)).
    Validate(validate.Required(&quot;id&quot;)).
    To(sink.JSONL(&quot;normalized.jsonl&quot;))
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;2-api-data-validation&quot;&gt;2. API Data Validation&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;p := intake.New().
    From(source.JSONL(&quot;api_incoming.jsonl&quot;)).
    Transform(transform.TrimStrings()).
    Validate(
        validate.Email(&quot;email&quot;),
        validate.URL(&quot;website&quot;),
        validate.Min(&quot;age&quot;, 18),
    ).
    OnInvalid(quarantine.JSONL(&quot;rejected.jsonl&quot;)).
    To(sink.CSV(&quot;clean.csv&quot;))
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;3-etl-for-analytics&quot;&gt;3. ETL for Analytics&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;p := intake.New().
    From(source.CSV(&quot;raw_events.csv&quot;)).
    Transform(
        transform.NormalizeHeaders(transform.SnakeCase),
        transform.ParseDate(&quot;timestamp&quot;, time.RFC3339),
        transform.ParseFloat(&quot;value&quot;),
    ).
    Validate(validate.Required(&quot;event_id&quot;)).
    OnInvalid(quarantine.JSONL(&quot;bad_events.jsonl&quot;)).
    To(sink.JSONL(&quot;analytics_ready.jsonl&quot;))
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;4-data-quality-inspection&quot;&gt;4. Data Quality Inspection&lt;/h4&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;src := source.CSV(&quot;unknown_dataset.csv&quot;)
profile, err := discover.InspectSource(ctx, src, discover.Options{
    SampleSize: 10000,
})

for _, f := range profile.Fields {
    fmt.Printf(&quot;%s: %s (confidence %.2f)\n&quot;, 
        f.Name, f.Type, f.TypeConfidence)
}
for _, issue := range profile.Issues {
    fmt.Printf(&quot;[%s] %s\n&quot;, issue.Severity, issue.Message)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;data-transformation-flow&quot;&gt;Data Transformation Flow&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;    ┌─────────────────┐
    │  Raw CSV/JSONL  │
    │   (Messy Data)  │
    └────────┬────────┘
             │
             ▼
    ┌─────────────────┐     ┌───────────────┐
    │   Discover      │────▶│ Field types   │
    │   Inspect       │     │ Null ratios   │
    └─────────────────┘     └───────────────┘
             │
             ▼
    ┌─────────────────┐
    │  Normalize      │
    │  Headers        │
    │  Trim Strings   │
    └────────┬────────┘
             │
             ▼
    ┌─────────────────┐
    │   Parse Types   │
    │  (int, float,  │
    │   date, bool)   │
    └────────┬────────┘
             │
             ▼
    ┌─────────────────┐     ┌───────────────┐
    │   Validate      │────▶│ Valid Output  │
    │   Required      │     │   (Sink)      │
    │   Min/Max       │     └───────────────┘
    │   Regex/Enum    │             │
    └────────┬────────┘             │
             │                      │
             ▼                      ▼
    ┌─────────────────┐     ┌───────────────┐
    │   Quarantine    │     │   Stats:      │
    │   (errors +    │     │ Read/Written/ │
    │   record data)  │     │ Invalid/Failed│
    └─────────────────┘     └───────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;competitor-comparison-matrix&quot;&gt;Competitor Comparison Matrix&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Toolkit &lt;/th&gt;&lt;th&gt;Dependencies &lt;/th&gt;&lt;th&gt;Streaming &lt;/th&gt;&lt;th&gt;Discovery &lt;/th&gt;&lt;th&gt;Quarantine &lt;/th&gt;&lt;th&gt;Lines of Code&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;go-intake&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;&lt;strong&gt;Zero&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;✅ &lt;/td&gt;&lt;td&gt;✅ &lt;/td&gt;&lt;td&gt;✅ &lt;/td&gt;&lt;td&gt;~2,300&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Gopherize &lt;/td&gt;&lt;td&gt;12 &lt;/td&gt;&lt;td&gt;Partial &lt;/td&gt;&lt;td&gt;❌ &lt;/td&gt;&lt;td&gt;❌ &lt;/td&gt;&lt;td&gt;~8,500&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;goflow &lt;/td&gt;&lt;td&gt;18 &lt;/td&gt;&lt;td&gt;✅ &lt;/td&gt;&lt;td&gt;❌ &lt;/td&gt;&lt;td&gt;❌ &lt;/td&gt;&lt;td&gt;~12,000&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;streamz &lt;/td&gt;&lt;td&gt;8 &lt;/td&gt;&lt;td&gt;✅ &lt;/td&gt;&lt;td&gt;❌ &lt;/td&gt;&lt;td&gt;❌ &lt;/td&gt;&lt;td&gt;~6,200&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;etl-go &lt;/td&gt;&lt;td&gt;5 &lt;/td&gt;&lt;td&gt;❌ &lt;/td&gt;&lt;td&gt;❌ &lt;/td&gt;&lt;td&gt;❌ &lt;/td&gt;&lt;td&gt;~4,500&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h3 id=&quot;technical-specifications&quot;&gt;Technical Specifications&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Specification &lt;/th&gt;&lt;th&gt;Value&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Go Version &lt;/td&gt;&lt;td&gt;1.23+&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;License &lt;/td&gt;&lt;td&gt;MIT&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Dependencies &lt;/td&gt;&lt;td&gt;Zero (stdlib only)&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Memory Model &lt;/td&gt;&lt;td&gt;Streaming (O(1))&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Concurrency &lt;/td&gt;&lt;td&gt;Context-aware cancellation&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Test Coverage &lt;/td&gt;&lt;td&gt;100% on public API&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Race Detector &lt;/td&gt;&lt;td&gt;Clean&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h3 id=&quot;non-goals-explicit-design-decisions-&quot;&gt;Non-Goals (Explicit Design Decisions)&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;❌ CLI/REPL interface (library-first)&lt;/li&gt;
&lt;li&gt;❌ DAG engine or scheduler&lt;/li&gt;
&lt;li&gt;❌ Distributed execution&lt;/li&gt;
&lt;li&gt;❌ DataFrame abstraction&lt;/li&gt;
&lt;li&gt;❌ Airflow/Airbyte clone&lt;/li&gt;
&lt;li&gt;❌ Connector marketplace&lt;/li&gt;
&lt;li&gt;❌ PDF/ML features&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;installation&quot;&gt;Installation&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;go get github.com/firfircelik/go-intake
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h3 id=&quot;quality-gate&quot;&gt;Quality Gate&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;go test -count=1 ./...
go vet ./...
go test -race -count=1 ./...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;All tests pass. Race detector clean. Zero external dependencies.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/20043/go-intake-v0-1-0-a-small-go-library-for-messy-data-intake</guid>
<pubDate>Tue, 09 Jun 2026 10:05:39 +0000</pubDate>
</item>
<item>
<title>Snowflake Summit 26: What Developers, Engineers, and Architects Need to Know</title>
<link>https://coderlegion.com/19368/snowflake-summit-26-what-developers-engineers-and-architects-need-to-know</link>
<description>&lt;p&gt;Snowflake's biggest user conference yet kicked off June 1 in San Francisco, and the company arrived with a full slate of product announcements aimed squarely at builders. Here's what matters most if you're writing code, managing data infrastructure, or designing systems on Snowflake.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;first-two-new-names&quot;&gt;First, Two New Names&lt;/h3&gt;
&lt;p&gt;Before getting into the announcements, note two product rebranding moves. Cortex Code is now officially &lt;strong&gt;Snowflake Coco&lt;/strong&gt;. The informal name had already taken hold with customers, so Snowflake made it official. Snowflake Intelligence is now &lt;strong&gt;Snowflake Cowork&lt;/strong&gt; — a rename that reflects how the product has grown from a next-gen BI tool into a broader productivity platform for knowledge workers.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;coco-gets-a-lot-more-surface-area&quot;&gt;Coco Gets a Lot More Surface Area&lt;/h3&gt;
&lt;p&gt;The biggest news for developers is how many places Coco now lives. Snowflake is introducing a &lt;strong&gt;VS Code extension&lt;/strong&gt;, an &lt;strong&gt;Excel plugin&lt;/strong&gt;, &lt;strong&gt;Slack integration&lt;/strong&gt;, and a standalone &lt;strong&gt;Coco Desktop app&lt;/strong&gt;. Claude Code support is also on the list — useful for any team already using Anthropic's CLI in their workflow.&lt;/p&gt;
&lt;p&gt;On the capability side, Coco can now handle &lt;strong&gt;long-running autonomous tasks&lt;/strong&gt; via Cloud Agents, create &lt;strong&gt;reusable workflows through Skills&lt;/strong&gt;, and support &lt;strong&gt;automations&lt;/strong&gt; for recurring monitoring and validation tasks. New integrations with &lt;strong&gt;Retool, Superblocks, and Vercel&lt;/strong&gt; let developers turn a prompt into a deployed app on Snowflake's App Runtime.&lt;/p&gt;
&lt;p&gt;Bala Kasiviswanathan, Snowflake's VP of Developer and AI Experiences, framed the shift this way: &quot;A year ago, building on enterprise data meant writing code, managing infrastructure, and stitching together disconnected tools for orchestration, streaming, governance, and deployment. Today, Snowflake CoCo is collapsing that complexity into a single AI-native development experience.&quot;&lt;/p&gt;
&lt;p&gt;CEO Sridhar Ramaswamy added that Coco can already outperform frontier models on operations within Snowflake, and that partners are shifting from time-and-material billing to outcome-based engagements because of it.&lt;/p&gt;
&lt;p&gt;The customer numbers are concrete. Fanatics reduced pipeline troubleshooting from days to hours. Thomson Reuters modernized legacy systems and delivered insights in days instead of weeks across more than 37,500 governed tables.&lt;/p&gt;
&lt;p&gt;Coco also works across data sources beyond Snowflake — including &lt;strong&gt;AWS Glue, dbt, Apache Airflow, Databricks, and Postgres&lt;/strong&gt;.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;snowflake-datastream-managed-streaming-built-in&quot;&gt;Snowflake Datastream: Managed Streaming, Built In&lt;/h3&gt;
&lt;p&gt;For data engineers managing streaming infrastructure, Snowflake introduced &lt;strong&gt;Datastream&lt;/strong&gt; — a fully managed, Kafka wire protocol compatible streaming service built natively into the platform. Existing Kafka clients and applications can point to it without rewriting integrations. Under the hood, it's pure Snowflake technology, not a Kafka wrapper.&lt;/p&gt;
&lt;p&gt;The practical appeal: streaming data routes directly into Snowflake tables with low latency, and it automatically inherits existing governance, access controls, masking policies, and lineage. No separate broker to manage. Datastream enters private preview shortly after Summit.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;apache-iceberg-v3-goes-ga-and-open-data-gets-serious&quot;&gt;Apache Iceberg v3 Goes GA — and Open Data Gets Serious&lt;/h3&gt;
&lt;p&gt;Snowflake announced several moves on the open data front. &lt;strong&gt;Apache Iceberg v3 support is now generally available&lt;/strong&gt;, with the broadest feature set on the market — expanded data types, cross-system change tracking, and improved semi-structured data performance.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Snowflake Storage for Apache Iceberg Tables&lt;/strong&gt; is also GA, with bi-directional read/write access to Snowflake-managed Iceberg tables from external engines via Horizon Catalog, powered by Apache Polaris. The &lt;strong&gt;Iceberg REST Scan Plan API&lt;/strong&gt; ensures security and masking policies travel with data even when accessed by external tools.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Zero-copy integrations&lt;/strong&gt; for SAP, Salesforce, and Workday mean direct access to that data without replication. The architectural goal: work on a single, governed copy of data wherever it lives, without moving it.&lt;/p&gt;
&lt;p&gt;Two new context layers sit on top of all of this. &lt;strong&gt;Horizon Context&lt;/strong&gt; brings in explicit, customer-defined metadata — including from the Select Star acquisition, which ingests metadata from Postgres, SQL Server, Tableau, and Power BI. &lt;strong&gt;Cortex Sense&lt;/strong&gt; builds implicit context automatically from usage patterns, feeding both Coco and Cowork without requiring manual configuration.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;natoma-acquisition-governing-what-agents-actually-do&quot;&gt;Natoma Acquisition: Governing What Agents Actually Do&lt;/h3&gt;
&lt;p&gt;This is the announcement that architects should pay the most attention to. Snowflake announced its intent to acquire &lt;strong&gt;Natoma&lt;/strong&gt;, an enterprise Model Context Protocol (MCP) platform. The significance: Natoma extends governance beyond data access to cover AI agent &lt;em&gt;actions&lt;/em&gt; — sending emails, summarizing Slack threads, updating Jira tickets, checking calendars — all from within Snowflake's governed environment with policy enforcement and full audit trails.&lt;/p&gt;
&lt;p&gt;For anyone designing agentic systems, this signals where Snowflake is heading. The goal is to be the control plane not just for what data agents can see, but for what they can do across enterprise systems.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;ai-security-three-new-layers&quot;&gt;AI Security: Three New Layers&lt;/h3&gt;
&lt;p&gt;Four security announcements are worth noting for teams building agentic workloads:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Agent Identity&lt;/strong&gt; gives every agent a verified identity with role-based permissions and a full audit trail. &lt;strong&gt;Prompt Injection Protection&lt;/strong&gt; uses ML-driven detection to block jailbreak attempts. The &lt;strong&gt;Trust Center&lt;/strong&gt; is getting an AI security package with codified best practices for agent configuration, credential usage, and data access. And &lt;strong&gt;multi-party authorization&lt;/strong&gt; requires a second approver for sensitive operations — protecting against both insider threats and runaway agents.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;a-few-more-things-worth-noting&quot;&gt;A Few More Things Worth Noting&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Snowflake Apps&lt;/strong&gt; now supports native React development alongside AI-powered workflows, moving from prompt to a production-ready app deployed inside the security perimeter.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Adaptive Compute&lt;/strong&gt; goes GA at Summit. It automatically determines the right mix of compute resources in real time — no manual warehouse sizing — with reported gains of up to 3.5x over first-generation warehouses.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cowork&lt;/strong&gt; is adding automations, background agents, and a next-generation artifacts and dashboards layer. For architects: Cowork is the governed, managed experience for everyone in the organization. Coco is the power tool for technical users who know what they're doing.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/19368/snowflake-summit-26-what-developers-engineers-and-architects-need-to-know</guid>
<pubDate>Tue, 02 Jun 2026 13:00:00 +0000</pubDate>
</item>
<item>
<title>Standardizing the Ingestion Interface: OpenBrokerCSV and the MIT Importer</title>
<link>https://coderlegion.com/18825/standardizing-the-ingestion-interface-openbrokercsv-and-the-mit-importer</link>
<description>&lt;h2 id=&quot;standardizing-the-ingestion-interface-openbrokercsv-and-the-mit-importer&quot;&gt;Standardizing the Ingestion Interface: OpenBrokerCSV and the MIT Importer&lt;/h2&gt;
&lt;p&gt;We open-source a &lt;strong&gt;financial interaction workflow&lt;/strong&gt; — not a bag of UI widgets. The concrete artifact is &lt;code&gt;@pocket-portfolio/importer&lt;/code&gt; (&lt;code&gt;packages/importer&lt;/code&gt;, &lt;strong&gt;MIT&lt;/strong&gt; on npm). It normalises broker-specific chaos into a &lt;strong&gt;&lt;code&gt;Trade&lt;/code&gt; model&lt;/strong&gt; the app and SDK consumers can reason about.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;package-reality&quot;&gt;Package reality&lt;/h3&gt;
&lt;p&gt;From &lt;code&gt;packages/importer/package.json&lt;/code&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Name:&lt;/strong&gt; &lt;code&gt;@pocket-portfolio/importer&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;License:&lt;/strong&gt; MIT&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Surface:&lt;/strong&gt; TypeScript adapters (Robinhood, Fidelity, eToro, IBKR Flex, Koinly, TurboTax, Ghostfolio, Sharesight, and more) + &lt;strong&gt;universal&lt;/strong&gt; inference for unknown CSV shapes.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Schema SSOT in-repo:&lt;/strong&gt; &lt;code&gt;packages/importer/README.md&lt;/code&gt;, &lt;code&gt;packages/importer/src/universal/types.ts&lt;/code&gt;, adapter registry in &lt;code&gt;src/registry.ts&lt;/code&gt;. A dedicated &lt;code&gt;SCHEMA.md&lt;/code&gt; is on the roadmap; until then, cite TypeScript types and README — do not invent a phantom file path in external copy.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;client-side-parse-by-design&quot;&gt;Client-side parse by design&lt;/h3&gt;
&lt;p&gt;Broker CSV parsing runs &lt;strong&gt;in the browser&lt;/strong&gt; for the product architecture we ship — the file does not need to land on an app-server disk for transformation. That supports:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Inspectable transforms&lt;/strong&gt; (clone the repo, read the adapter),&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reduced central warehouse temptation&lt;/strong&gt; for raw exports,&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Adversarial testing&lt;/strong&gt; in &lt;code&gt;src/universal/adversarial.ts&lt;/code&gt; without multi-tenant DB blast radius.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;universal-mapping-truncated-preview-only&quot;&gt;Universal mapping: truncated preview only&lt;/h3&gt;
&lt;p&gt;For unknown columns, &lt;code&gt;docs/IP-TECHNICAL-MECHANISMS.md&lt;/code&gt; documents &lt;strong&gt;truncated payload&lt;/strong&gt; for mapping assistance: headers + sample rows — &lt;strong&gt;full file stays client-side&lt;/strong&gt;. This mirrors the Ask AI instinct (Part 2): send the &lt;strong&gt;minimum&lt;/strong&gt; structure needed for the next step.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;openbrokercsv-narrative&quot;&gt;OpenBrokerCSV narrative&lt;/h3&gt;
&lt;p&gt;Open Portfolio surfaces OpenBrokerCSV as the &lt;strong&gt;procurement-facing name&lt;/strong&gt; for the normalised interchange (&lt;code&gt;app/open/openbrokercsv/page.tsx&lt;/code&gt;). Pocket exercises the same package under retail UX pressure — that is the &lt;strong&gt;harness&lt;/strong&gt; relationship (Part 6).&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;what-we-do-not-promise&quot;&gt;What we do not promise&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;plugin marketplace&lt;/strong&gt; runtime in &lt;code&gt;app/&lt;/code&gt; — not shipped.&lt;/li&gt;
&lt;li&gt;“Zero server” after import for signed-in users — Firebase still holds authoritative trades (Part 4).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Do&lt;/strong&gt; point engineers to &lt;strong&gt;GitHub&lt;/strong&gt;, &lt;strong&gt;Discussions&lt;/strong&gt;, and npm — the real contributor loop (&lt;code&gt;README-OSS-WORKFLOW.md&lt;/code&gt;).&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Part 5 of &lt;strong&gt;Sovereign Ingestion &amp;amp; Stateless Inference&lt;/strong&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Read the full &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.pocketportfolio.app/book/sovereign-intelligence&quot;&gt;Sovereign Intelligence&lt;/a&gt; book, explore &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.openportfolio.co.uk&quot;&gt;Open Portfolio&lt;/a&gt;, or &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.pocketportfolio.app&quot;&gt;try Pocket Portfolio&lt;/a&gt;.&lt;/strong&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/18825/standardizing-the-ingestion-interface-openbrokercsv-and-the-mit-importer</guid>
<pubDate>Fri, 29 May 2026 08:00:00 +0000</pubDate>
</item>
<item>
<title>AI-Powered DLQ Triage with Amazon Bedrock</title>
<link>https://coderlegion.com/17500/ai-powered-dlq-triage-with-amazon-bedrock</link>
<description>&lt;h2 id=&quot;ai-assisted-operational-intelligence-for-sqs-dlq-triage-with-amazon-bedrock&quot;&gt;AI-Assisted Operational Intelligence for SQS DLQ Triage with Amazon Bedrock&lt;/h2&gt;
&lt;h3 id=&quot;overview&quot;&gt;Overview&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;bedrock-dlq-triage&lt;/code&gt; turns a dead-letter queue from a passive failure sink into an operational intelligence layer. When a message lands in an SQS DLQ, an AWS Lambda triage processor sends the payload to Amazon Bedrock, classifies the failure, assigns a severity, suggests a remediation path, and stores the result in DynamoDB for operator review. A separate replay flow lets operators re-inject only approved messages back into the source queue with idempotency safeguards.&lt;/p&gt;
&lt;p&gt;This pattern is useful when teams handle high message volumes, heterogeneous failure modes, and repetitive investigation work. Instead of reading raw payloads one by one, operators get a structured record that highlights likely root cause, urgency, and next action. The system also creates an audit trail that supports compliance, incident review, and safer operational replay.&lt;/p&gt;
&lt;h3 id=&quot;problem-statement&quot;&gt;Problem Statement&lt;/h3&gt;
&lt;p&gt;DLQs are excellent at preserving failed messages, but they do not explain why a message failed. In real production systems, a single DLQ may contain transient integration errors, schema mismatches, downstream timeouts, poison messages, and malformed payloads, all mixed together. That forces engineers to spend time triaging failures manually before they can decide whether to fix code, replay data, or quarantine the message.&lt;/p&gt;
&lt;p&gt;The goal of this design is to reduce mean time to understanding by using generative AI as a triage assistant. The model does not replace engineering judgment; it standardizes the first pass, applies a consistent rubric, and produces structured output that downstream workflows can consume. That makes DLQ handling faster, more repeatable, and easier to monitor.&lt;/p&gt;
&lt;h3 id=&quot;architecture&quot;&gt;Architecture&lt;/h3&gt;
&lt;p&gt;The core flow is simple: SQS DLQ events invoke a Lambda function named &lt;code&gt;triage_processor&lt;/code&gt;, which calls Amazon Bedrock for classification and recommendation generation. The Lambda then writes a normalized triage record into DynamoDB, emits structured logs and custom metrics to CloudWatch, and archives prompt artifacts and raw message snapshots in S3 when needed. A DynamoDB stream triggers a second Lambda, &lt;code&gt;replay_handler&lt;/code&gt;, which performs controlled replays back into the source SQS queue.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;SQS DLQ ──► Lambda (triage_processor)
                │
                ├──► Amazon Bedrock (Claude / Titan)
                │         └── classification + severity + remediation
                │
                ├──► DynamoDB (triage_results)
                │         └── audit trail, TTL, idempotency key
                │
                └──► CloudWatch (structured logs + custom metrics)

DynamoDB Stream ──► Lambda (replay_handler)
                         └──► Source SQS Queue (controlled replay)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;All compute runs inside a VPC, IAM follows least privilege, secrets live in AWS Secrets Manager, and data at rest is protected with AWS KMS across SQS, DynamoDB, and S3. DynamoDB Streams provides near-real-time change propagation for replay workflows and retains stream records for up to 24 hours. [page:1]&lt;/p&gt;
&lt;h3 id=&quot;how-triage-works&quot;&gt;How Triage Works&lt;/h3&gt;
&lt;p&gt;The triage Lambda should normalize every message into a consistent envelope before calling Bedrock. A good prompt usually includes the message body, metadata such as queue name and receive count, recent error context if available, and a strict output schema like JSON. Amazon Bedrock supports Claude text completion and messages APIs, which makes it suitable for structured inference calls from Lambda. [page:2]&lt;/p&gt;
&lt;p&gt;A practical output schema might include &lt;code&gt;failure_type&lt;/code&gt;, &lt;code&gt;severity&lt;/code&gt;, &lt;code&gt;confidence&lt;/code&gt;, &lt;code&gt;recommended_action&lt;/code&gt;, &lt;code&gt;replay_safe&lt;/code&gt;, and &lt;code&gt;operator_notes&lt;/code&gt;. The severity score can be rule-based, model-based, or a hybrid of both; for example, authentication failures may be marked high severity, while temporary downstream throttling may be low or medium severity. The most useful implementations also include a compact rationale so operators understand why the model reached its conclusion.&lt;/p&gt;
&lt;p&gt;To keep results actionable, the Lambda should validate the model output before writing to DynamoDB. If the response is malformed, the function can fall back to a deterministic classification path or mark the record as &lt;code&gt;needs_human_review&lt;/code&gt;. That keeps the system resilient even when the model output is incomplete or ambiguous.&lt;/p&gt;
&lt;h3 id=&quot;replay-flow&quot;&gt;Replay Flow&lt;/h3&gt;
&lt;p&gt;Replay should be separated from triage so that analysis and action do not happen in the same execution path. When a triage result is approved, the DynamoDB stream emits the change event, and the replay Lambda can check the replay state, idempotency key, and operator decision before sending the original payload back to the source queue. DynamoDB Streams is designed for this kind of near-real-time change capture and preserves item-level changes in order within the stream. [page:1]&lt;/p&gt;
&lt;p&gt;The replay path should be intentionally conservative. Only messages that meet explicit criteria should be eligible for re-injection, such as &lt;code&gt;replay_safe = true&lt;/code&gt;, a matching approved status, and a message-level deduplication key that prevents duplicates. This design is especially important when the same DLQ message may be inspected, edited, and replayed multiple times by different operators.&lt;/p&gt;
&lt;h3 id=&quot;data-model&quot;&gt;Data Model&lt;/h3&gt;
&lt;p&gt;A DynamoDB table such as &lt;code&gt;triage_results&lt;/code&gt; can store the operational state of each failure. A useful partition key is the DLQ message ID or a deterministic hash of message body plus queue metadata, while a sort key can represent the triage attempt or replay version. TTL can be used to automatically expire stale records after the operational retention window, which helps keep the table small and reduces noise.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Attribute &lt;/th&gt;&lt;th&gt;Purpose&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;message_id&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Primary identifier for the failed message.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;idempotency_key&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Prevents duplicate triage and replay actions.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;failure_type&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Model-generated or rule-assisted classification.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;severity&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Operational urgency, such as low, medium, high, or critical.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;recommended_action&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Suggested remediation step for operators.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;replay_safe&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Indicates whether controlled replay is allowed.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;status&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Triage state, such as new, reviewed, approved, replayed, or closed.&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;ttl&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Expiration time for automatic cleanup.&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Because DynamoDB Streams can emit the full before-and-after image of a record, it works well for audit and replay triggers when you need traceability around operator decisions. [page:1]&lt;/p&gt;
&lt;h3 id=&quot;observability&quot;&gt;Observability&lt;/h3&gt;
&lt;p&gt;CloudWatch should capture both machine-readable logs and operational metrics. Useful metrics include triage volume, Bedrock invocation latency, classification confidence, replay attempts, replay success rate, and the number of messages escalated for human review.  Alarms can trigger when triage failures spike, when Bedrock latency degrades, or when replay activity exceeds a safe threshold.&lt;/p&gt;
&lt;p&gt;Structured logs are especially valuable because they let engineers correlate one DLQ message with its classification, Bedrock prompt version, model response, and replay outcome. That observability makes the system debuggable and also gives teams a feedback loop for improving prompt design and classification rules over time.&lt;/p&gt;
&lt;h3 id=&quot;security-and-governance&quot;&gt;Security And Governance&lt;/h3&gt;
&lt;p&gt;The system should use least-privilege IAM roles for each Lambda function, and Bedrock permissions should be scoped to the specific model ARNs in use. Secrets and tokens belong in AWS Secrets Manager rather than environment variables, and KMS should protect data in SQS, DynamoDB, and S3. Keeping the workload inside a VPC can help align the design with enterprise network controls and inspection requirements.&lt;/p&gt;
&lt;p&gt;Governance also benefits from storing prompt templates, model versions, and replay approvals alongside the triage record. That gives teams a full audit trail for incident response and makes it easier to reproduce a decision later. In regulated environments, this history is often as important as the classification itself.&lt;/p&gt;
&lt;h3 id=&quot;implementation-notes&quot;&gt;Implementation Notes&lt;/h3&gt;
&lt;p&gt;A strong implementation starts with a narrow prompt and a fixed response schema. The model should be asked to classify the failure, assign severity, recommend an action, and state whether replay is safe, all in machine-readable JSON. The Lambda should retry cautiously, enforce timeouts, and reject responses that do not match the expected schema.&lt;/p&gt;
&lt;p&gt;Idempotency is critical for both triage and replay. The safest approach is to derive an idempotency key from stable message attributes and record processing state in DynamoDB before any replay action occurs. That protects the system from duplicate deliveries, operator retries, and stream reprocessing.&lt;/p&gt;
&lt;h3 id=&quot;why-it-matters&quot;&gt;Why It Matters&lt;/h3&gt;
&lt;p&gt;This architecture changes DLQ handling from manual troubleshooting into an AI-assisted operational workflow. Engineers spend less time reading raw failure payloads and more time addressing the actual root cause. Operators also gain a consistent, auditable decision trail for every failure that enters the DLQ.&lt;/p&gt;
&lt;p&gt;The best result is not just automation, but better judgment at scale. Bedrock supplies the first-pass reasoning, Lambda orchestrates the workflow, DynamoDB preserves state, and CloudWatch keeps the system observable.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/17500/ai-powered-dlq-triage-with-amazon-bedrock</guid>
<pubDate>Mon, 11 May 2026 23:56:56 +0000</pubDate>
</item>
<item>
<title>Eradicating Operational Drag: Architecting a Resilient Data Ingestion Pipeline</title>
<link>https://coderlegion.com/16853/eradicating-operational-drag-architecting-a-resilient-data-ingestion-pipeline</link>
<description>&lt;p&gt;&lt;img src=&quot;https://cdn.jaiswalankit.com.np/data-ingestion/eraser-io-architecture.png&quot; alt=&quot;Hand-drawn architecture diagram of an asynchronous data ingestion pipeline&quot;&gt;&lt;/p&gt;
&lt;p&gt;In the lifecycle of a scaling technology company, manual data entry is a silent killer of engineering velocity. &lt;/p&gt;
&lt;p&gt;I was recently tasked with solving a massive operational bottleneck for a mobile telecommunications client. Their core product relied on a perpetually up-to-date, deeply categorized database of thousands of mobile devices encompassing highly specific hardware specs, varying image URLs, and user metadata. &lt;/p&gt;
&lt;p&gt;Historically, this required human operators spending days manually copying data from centralized industry hubs. It was slow, wildly prone to human error, and completely unscalable. Every time a manufacturer announced a new lineup, the client's operations team was paralyzed by manual data entry.&lt;/p&gt;
&lt;p&gt;The directive was clear: automate it. However, as a senior engineer, you know that writing a quick Python or Node.js scraping script is easy; building a &lt;strong&gt;resilient, scalable data ingestion pipeline&lt;/strong&gt; that survives network timeouts, DOM mutations, and rate limits is a complex architectural challenge.&lt;/p&gt;
&lt;p&gt;Here is a case study on my thought process, the engineering hurdles, and how I architected a robust ingestion engine that permanently eliminated this operational drag.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-architectural-challenge-the-web-is-hostile&quot;&gt;The Architectural Challenge: The Web is Hostile&lt;/h3&gt;
&lt;p&gt;When transitioning from manual collection to automated ingestion, you are not just writing a script you are building a distributed system that must interact with external, highly volatile environments. &lt;/p&gt;
&lt;p&gt;Before writing any logic, I mapped out the three primary failure vectors of web-based data extraction:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;The Fragility of the DOM:&lt;/strong&gt; HTML is not a reliable API. Websites redesign their UIs, class names change, and tables nest unpredictably. A brittle parser will crash the entire pipeline the moment a target website updates its CSS.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Network Hostility &amp;amp; Rate Limiting:&lt;/strong&gt; Target servers do not want you scraping thousands of pages concurrently. If you run a massive &lt;code&gt;for-loop&lt;/code&gt; of HTTP requests, your IP will be aggressively banned within seconds.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;State Management &amp;amp; Idempotency:&lt;/strong&gt; If the pipeline runs twice, or crashes halfway through, it cannot create duplicate database entries or corrupt existing data. The system must be deterministic.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;phase-1-engineering-a-resilient-parsing-matrix&quot;&gt;Phase 1: Engineering a Resilient Parsing Matrix&lt;/h3&gt;
&lt;p&gt;The first problem to solve was the extraction of unstructured data. Mobile phone specifications are often buried inside deeply nested, inconsistent HTML tables.&lt;/p&gt;
&lt;p&gt;Instead of hardcoding DOM traversal logic directly into the execution flow, I separated the &quot;Fetching&quot; from the &quot;Parsing.&quot; I designed the system to fetch the raw HTML payload and pass it to an isolated, stateless parsing module using Cheerio (a high-performance, server-side implementation of core jQuery logic).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Thought Process:&lt;/strong&gt; To protect against UI changes, I avoided tying the parser to brittle visual CSS classes (like &lt;code&gt;.red-text-bold&lt;/code&gt;). Instead, I targeted semantic data attributes (e.g., &lt;code&gt;span[data-spec=battery]&lt;/code&gt;). I structured the parser as a &quot;Configuration Matrix&quot;, a simple mapping dictionary that linked our internal database schema fields to specific HTML selectors. &lt;/p&gt;
&lt;p&gt;If the target website updated its UI, the core engine wouldn't break; we simply updated a single line in the configuration matrix. This transformed a fragile web scraper into a robust, maintainable data transformer.&lt;/p&gt;
&lt;h3 id=&quot;phase-2-respectful-concurrency-and-the-worker-queue&quot;&gt;Phase 2: Respectful Concurrency and the Worker Queue&lt;/h3&gt;
&lt;p&gt;The most common mistake junior engineers make with automation is aggressive concurrency. Firing 5,000 HTTP GET requests simultaneously will trigger DDoS protection (like Cloudflare) on the target server, resulting in immediate IP blacklisting.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Thought Process:&lt;/strong&gt;&lt;br&gt;
To ensure high availability and prevent network bans, the ingestion engine had to be &quot;polite&quot; but persistent. I architected the flow using an asynchronous queue system.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;The Indexer:&lt;/strong&gt; The master process performs a single, lightweight pass over the target's brand directory, identifying pagination limits and gathering the specific URLs for all 5,000+ devices.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The Queue:&lt;/strong&gt; Instead of fetching these URLs immediately, the indexer pushes them into a task queue.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Throttled Workers:&lt;/strong&gt; A fleet of worker nodes pulls URLs from the queue at a strictly controlled rate. I implemented a &lt;strong&gt;Token Bucket algorithm&lt;/strong&gt; to enforce a maximum outbound request limit (e.g., 5 requests per second). &lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Exponential Backoff:&lt;/strong&gt; If a worker receives a &lt;code&gt;429 Too Many Requests&lt;/code&gt; or &lt;code&gt;503 Service Unavailable&lt;/code&gt; error, it doesn't crash. It relies on exponential backoff waiting 2 seconds, then 4, then 8 before retrying.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This respectful, queued concurrency guaranteed that we could ingest massive amounts of data continuously without ever triggering hostile network defenses.&lt;/p&gt;
&lt;h3 id=&quot;phase-3-guaranteeing-idempotent-database-writes&quot;&gt;Phase 3: Guaranteeing Idempotent Database Writes&lt;/h3&gt;
&lt;p&gt;The final, and arguably most critical, piece of the architecture was ensuring data integrity. The ingestion pipeline runs as a scheduled cron job (e.g., every 24 hours) to catch newly released devices. &lt;/p&gt;
&lt;p&gt;If the pipeline grabs data for a phone that already exists in our database, performing a blind &lt;code&gt;INSERT&lt;/code&gt; operation would result in thousands of duplicated rows.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Thought Process:&lt;/strong&gt;&lt;br&gt;
I engineered the final stage of the pipeline to be strictly &lt;strong&gt;idempotent&lt;/strong&gt;. &lt;/p&gt;
&lt;p&gt;Before committing data, the worker generates a unique hash based on the device's brand and model name. It queries our primary database for this hash. &lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;If the hash does not exist, it executes an &lt;code&gt;INSERT&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;If the hash exists, it runs a differential check. It compares the newly scraped specifications against the existing database record. If the target website updated a specification or added a new image URL, the worker executes a surgical &lt;code&gt;UPDATE&lt;/code&gt; on only the modified fields.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This delta-update logic drastically reduced database write-load and guaranteed that running the crawler 100 times yielded the exact same perfect state as running it once.&lt;/p&gt;
&lt;hr&gt;
&lt;h4 id=&quot;the-engineering-impact&quot;&gt;The Engineering Impact&lt;/h4&gt;
&lt;p&gt;The true value of software engineering is measured in business leverage. By replacing a human workflow with a structurally sound, event-driven ingestion microservice, the impact was profound.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Eradicated Operational Drag:&lt;/strong&gt; A process that previously paralyzed the operations team for days was reduced to a silent background job that autonomously updates the platform while everyone sleeps.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Flawless Data Integrity:&lt;/strong&gt; We eliminated human transcription errors entirely. The database became a mathematically accurate reflection of the source data.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Architectural Scalability:&lt;/strong&gt; Because the engine decoupled fetching, parsing, and state management, onboarding a completely new data source simply requires creating a new CSS parsing matrix, rather than rewriting the core infrastructure.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Automation is not merely writing scripts to mimic human clicks; it is about architecting resilient, deterministic pipelines that empower humans to stop acting like machines and focus entirely on high-value business logic.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Dealing with massive data orchestration bottlenecks or looking to architect resilient automated pipelines? Let's Connect!&lt;/strong&gt;&lt;br&gt;
&lt;em&gt;I am Ankit Jaiswal, a Senior Full Stack AI Engineer specializing in system design, distributed architectures, and building robust, cloud-agnostic SaaS platforms.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/16853/eradicating-operational-drag-architecting-a-resilient-data-ingestion-pipeline</guid>
<pubDate>Wed, 06 May 2026 12:24:56 +0000</pubDate>
</item>
<item>
<title>How to Connect Power BI to a SQL (PostgreSQL) Database and Build a Unified Dashboard</title>
<link>https://coderlegion.com/16637/how-to-connect-power-bi-to-a-sql-postgresql-database-and-build-a-unified-dashboard</link>
<description>&lt;h3 id=&quot;introduction-4&quot;&gt;Introduction&lt;/h3&gt;
&lt;p&gt;Power BI is a business intelligence (BI) and data visualization tool from Microsoft. It enables analysts and business users to transform raw data into interactive dashboards and reports. Companies use Power BI to analyze sales, customer behavior, inventory trends, and other critical business metrics.&lt;/p&gt;
&lt;p&gt;SQL databases, like PostgreSQL, are widely used to store and manage structured data. Connecting Power BI to SQL databases allows analysts to retrieve, clean, transform, and model data efficiently, creating a single source of truth for dashboards and KPIs.&lt;/p&gt;
&lt;p&gt;In this article, we’ll walk through:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Connecting Power BI to local and cloud PostgreSQL databases&lt;/li&gt;
&lt;li&gt;Loading raw data and applying transformations&lt;/li&gt;
&lt;li&gt;Combining datasets from multiple sources&lt;/li&gt;
&lt;li&gt;Adding a Date Dimension table (DimDate) for KPIs and filtering&lt;/li&gt;
&lt;li&gt;Preparing a dashboard-ready model for GitHub and reporting&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;what-you-need-before-connecting&quot;&gt;What You Need Before Connecting&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;Power BI Desktop installed&lt;/li&gt;
&lt;li&gt;PostgreSQL database connection details (server, port, database, username, password) &lt;/li&gt;
&lt;li&gt;For cloud connections: SSL certificate (CA certificate)&lt;/li&gt;
&lt;li&gt;Model view showing relationships across cloud and local tables&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;connecting-to-a-local-postgresql-database&quot;&gt;Connecting to a Local PostgreSQL Database&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Open Power BI Desktop.&lt;/li&gt;
&lt;li&gt;Click &lt;code&gt;Get Data → PostgreSQL database&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/muja5c62us91l6vuhe3r.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/20klv8w166r4cc41iml0.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Enter your connection info:&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;Server: &lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;localhost:5432 (replace 5432 with your PostgreSQL port)
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;Database: Name of your database&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mutt8pxcsxiq4dxb8ug2.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Enter your username and password.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/z8u5wi3x3dhg6fzipr7w.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Click OK to connect.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The &lt;code&gt;localhost:port &lt;/code&gt; syntax ensures Power BI connects to the correct PostgreSQL server port.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;In the Navigator window, select tables to load, then click Load (or Transform Data).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qhaeme1626hoc82bzloi.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/oighsih3wdlxiipqr02w.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;connecting-to-a-cloud-postgresql-database-aiven-or-similar-&quot;&gt;Connecting to a Cloud PostgreSQL Database (Aiven or Similar)&lt;/h3&gt;
&lt;p&gt;Cloud databases require secure SSL connections.&lt;/p&gt;
&lt;h4 id=&quot;guide-to-aiven-connection&quot;&gt;Guide to Aiven Connection&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;From your cloud provider, obtain:&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;Host&lt;/li&gt;
&lt;li&gt;Port&lt;/li&gt;
&lt;li&gt;Database name&lt;/li&gt;
&lt;li&gt;Username and password&lt;/li&gt;
&lt;li&gt;SSL certificate (CA certificate)&lt;/li&gt;
&lt;li&gt;Install the CA certificate on your system:&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Open the downloaded .crt or .pem file.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Install it in the trusted root certificate store:&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;Windows: Right-click → Install Certificate → Place in “Trusted Root Certification Authorities”
&lt;/code&gt;&lt;br&gt;
&lt;code&gt;Linux/macOS: Follow OS instructions to add to the system or user trust store&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Installing the CA certificate allows Power BI to validate the server’s identity and establish a secure connection.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;In Power BI Desktop:&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;Go to &lt;code&gt;Get Data → PostgreSQL database&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Enter Server&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;your-cloud-host:5432
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt; and Database&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/i8woqdjcyo7gbuqy2j30.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Check Use SSL certificate and select the installed certificate&lt;/li&gt;
&lt;li&gt;Enter your username and password&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zw3s255xopek538qzdyz.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;If successful, it opens your aiven cloud database as shown below&lt;br&gt;
&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/njvy6d7xhlcn0hudte0r.png&quot; alt=&quot;Image description&quot;&gt;&lt;/li&gt;
&lt;li&gt;Choose tables from your database&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/e3406ibiobq3temr53iy.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Click OK and load the tables&lt;/li&gt;
&lt;/ul&gt;
&lt;h5 id=&quot;load-raw-data-first&quot;&gt;Load Raw Data First&lt;/h5&gt;
&lt;p&gt;It’s important to load raw data before applying transformations:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Load all tables (customers, products, sales, inventory) first.&lt;/li&gt;
&lt;li&gt;Review the raw data to assess missing values, incorrect or inconsistent formats and unnecessary columns&lt;/li&gt;
&lt;li&gt;This helps you understand which fields need cleaning and transformation before building dashboards.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;filtering-and-transformation&quot;&gt;Filtering and Transformation&lt;/h3&gt;
&lt;p&gt;Use Power BI’s Power Query Editor for data preparation:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Remove unnecessary columns – keep only fields needed for analysis.&lt;/li&gt;
&lt;li&gt;Rename columns – make names descriptive.&lt;/li&gt;
&lt;li&gt;Filter rows – e.g., remove canceled sales or test records.&lt;/li&gt;
&lt;li&gt;Change data types – ensure numeric and date fields are correct.&lt;/li&gt;
&lt;li&gt;Create calculated columns – e.g., total sales = quantity × price.&lt;/li&gt;
&lt;li&gt;Handle missing values – fill, replace, or remove nulls.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;creating-relationships-between-tables&quot;&gt;Creating Relationships Between Tables&lt;/h3&gt;
&lt;p&gt;Once data is cleaned:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Switch to Model View.&lt;/li&gt;
&lt;li&gt;Create relationships:&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;`sales.customer_id → customers.customer_id&lt;/p&gt;
&lt;p&gt;sales.product_id → products.product_id&lt;/p&gt;
&lt;p&gt;inventory.product_id → products.product_id`&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/oz1v03qw42h4pz9g8cfh.png&quot; alt=&quot;Image description&quot;&gt;&lt;br&gt;
Validate relationships for correct cardinality and cross-filtering.&lt;/p&gt;
&lt;p&gt;Relationships ensure Power BI can accurately summarize data across multiple tables.&lt;/p&gt;
&lt;h3 id=&quot;data-modeling-basics&quot;&gt;Data Modeling Basics&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Star schema&lt;br&gt;
 Fact table (sales) connected to dimension tables (customers, products, inventory).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Cardinality&lt;br&gt;
One-to-many or many-to-one relationships.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Filter directions&lt;br&gt;
 Define single or bidirectional filters depending on your analysis needs.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Proper modeling ensures dashboards are interactive and accurate.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8s7yqhb8frrf01gcvxyj.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;why-sql-skills-matter-for-power-bi-analysts&quot;&gt;Why SQL Skills Matter for Power BI Analysts&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;Data extraction: Retrieve exactly the data you need.&lt;/li&gt;
&lt;li&gt;Filtering &amp;amp; aggregation: Pre-process data for better performance.&lt;/li&gt;
&lt;li&gt;Data cleaning &amp;amp; transformation: Prepare data before loading it into Power BI.&lt;/li&gt;
&lt;li&gt;Performance optimization: Efficient queries improve dashboard responsiveness.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;By combining SQL knowledge with Power BI, analysts can build powerful, reliable dashboards that drive business decisions.&lt;/p&gt;
&lt;h3 id=&quot;conclusion-5&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;Connecting PostgreSQL to Power BI empowers analysts to turn structured data into actionable insights. Whether using local or cloud databases, establishing secure and reliable connections ensures that the data you analyze is accurate and up-to-date. By first loading raw data, applying necessary transformations, and creating relationships, including a Date table for time-based analysis, you build a robust data model ready for interactive dashboards.&lt;/p&gt;
&lt;p&gt;Mastering this workflow not only simplifies KPI calculations and trend analysis but also highlights the importance of SQL skills for preparing and managing data. Ultimately, integrating PostgreSQL with Power BI provides a seamless bridge between data storage and visualization, enabling smarter, data-driven business decisions.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/16637/how-to-connect-power-bi-to-a-sql-postgresql-database-and-build-a-unified-dashboard</guid>
<pubDate>Mon, 04 May 2026 13:29:57 +0000</pubDate>
</item>
<item>
<title>Market Data Integrator: Consuming Real-Time Data with Python, MongoDB, and Kafka</title>
<link>https://coderlegion.com/16542/market-data-integrator-consuming-real-time-data-with-python-mongodb-and-kafka</link>
<description>&lt;p&gt;Hey folks!&lt;/p&gt;
&lt;p&gt;Continuing the &lt;strong&gt;My Broker B3&lt;/strong&gt; series, today we'll build the first microservice in the ecosystem: the &lt;strong&gt;trading-broker-market-data&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;This service has a simple and well-defined responsibility: &lt;strong&gt;fetch real quotes for Brazilian assets from the Brapi API and distribute them to the ecosystem&lt;/strong&gt; — persisting the history in MongoDB and publishing updated prices to Kafka for other services to consume.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;-this-service-s-role-in-the-ecosystem&quot;&gt;️ This Service's Role in the Ecosystem&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;[brapi.dev] ◀── HTTP ── [broker-market-data-api]
                                  │
                    ┌─────────────┴──────────────┐
                    ▼                             ▼
             MongoDB                           Kafka
        (price_history)          (trading-assets-market-data-v1)
                                               │
                                    [broker-asset-api] (consumer)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It's the entry point for real data into the system. Without it, the ecosystem operates on static prices.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;mvp-focus&quot;&gt;MVP Focus&lt;/h2&gt;
&lt;p&gt;The goal here is clear: &lt;strong&gt;have real quotes available in Kafka&lt;/strong&gt; for other services to react to. In this phase, I prioritized:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Scheduled ingestion of 50 assets (Blue Chips, REITs and ETFs)&lt;/li&gt;
&lt;li&gt;Historical persistence in MongoDB for audit&lt;/li&gt;
&lt;li&gt;Kafka event publishing for downstream consumers&lt;/li&gt;
&lt;li&gt;Rate limit protection for the Brapi API (free plan)&lt;/li&gt;
&lt;li&gt;Immediate execution on service startup&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&quot;-tech-stack&quot;&gt;️ Tech Stack&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;Technology &lt;/th&gt;&lt;th align=&quot;left&quot;&gt;Usage&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;Python 3.12&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Service runtime&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;requests&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;HTTP client for Brapi API&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;pymongo&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;MongoDB persistence&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;confluent-kafka&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Kafka producer&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;schedule&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Periodic scheduling&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;&lt;strong&gt;python-dotenv&lt;/strong&gt; &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;Environment variables&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id=&quot;-the-implementation&quot;&gt;️ The Implementation&lt;/h2&gt;
&lt;h3 id=&quot;the-ingestion-loop&quot;&gt;The Ingestion Loop&lt;/h3&gt;
&lt;p&gt;The heart of the service is the &lt;code&gt;run_ingestion()&lt;/code&gt; function, which iterates over the watchlist, fetches each asset's price and distributes the data:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def run_ingestion():
    &quot;&quot;&quot;Fetches market data from Brapi, persists to MongoDB and publishes to Kafka.&quot;&quot;&quot;
    logger.info(&quot;Market data ingestion job started&quot;)

    token = os.getenv(&quot;BRAPI_TOKEN&quot;)
    if not token:
        logger.error(&quot;BRAPI_TOKEN not set. Aborting.&quot;)
        return

    # Producer created per run to avoid crash on import if Kafka is unavailable
    producer = Producer({
        'bootstrap.servers': os.getenv(&quot;KAFKA_BOOTSTRAP_SERVERS&quot;, &quot;localhost:9092&quot;),
        'client.id': 'broker-market-data-api'
    })

    mongo = MongoRepository()
    success_count = 0
    error_count = 0

    for ticker in WATCHLIST:
        try:
            url = f&quot;https://brapi.dev/api/quote/{ticker}?token={token}&quot;
            response = requests.get(url, timeout=10)
            response.raise_for_status()

            result = response.json()[&quot;results&quot;][0]
            payload = {
                &quot;ticker&quot;: result.get(&quot;symbol&quot;),
                &quot;name&quot;: result.get(&quot;longName&quot;),
                &quot;short_name&quot;: result.get(&quot;shortName&quot;),
                &quot;price&quot;: result.get(&quot;regularMarketPrice&quot;),
                &quot;volume&quot;: result.get(&quot;regularMarketVolume&quot;),
                &quot;updated_at&quot;: result.get(&quot;regularMarketTime&quot;)
            }

            # 1. Persist historical record in MongoDB
            mongo.save(payload.copy())

            # 2. Publish event to Kafka
            producer.produce(
                topic=TOPIC_NAME,
                key=ticker,
                value=json.dumps(payload).encode('utf-8'),
                callback=delivery_report
            )
            producer.poll(0)

            success_count += 1
            time.sleep(0.5)  # Rate limiting — respect Brapi free plan

        except Exception as e:
            error_count += 1
            logger.error(f&quot;Error processing ticker {ticker}: {e}&quot;)

    producer.flush()
    logger.info(f&quot;Ingestion completed: {success_count} succeeded, {error_count} failed&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Important design decisions:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why create the Producer inside the function?&lt;/strong&gt; If it were at module level, an unavailable Kafka at &lt;code&gt;import&lt;/code&gt; time would crash the script before executing any logic. Creating it inside the function means a Kafka failure doesn't prevent execution — only that run fails.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why &lt;code&gt;try/catch&lt;/code&gt; per ticker?&lt;/strong&gt; If one asset fails (timeout, invalid ticker), the loop continues for the rest. The &lt;code&gt;error_count&lt;/code&gt; at the end shows how many failed without stopping the work.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The &lt;code&gt;time.sleep(0.5)&lt;/code&gt;&lt;/strong&gt; — respects the rate limit of Brapi's free plan. For 50 assets, the full run takes ~25 seconds.&lt;/p&gt;
&lt;h3 id=&quot;the-scheduling&quot;&gt;The Scheduling&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;if __name__ == &quot;__main__&quot;:
    logger.info(f&quot;Starting — sync every {SYNC_INTERVAL_MINUTES} minutes&quot;)

    # Run immediately on startup, then on schedule
    run_ingestion()

    schedule.every(SYNC_INTERVAL_MINUTES).minutes.do(run_ingestion)

    while True:
        schedule.run_pending()
        time.sleep(1)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Why run immediately on startup?&lt;/strong&gt; If the service restarts, we don't want to wait 30 minutes for data in Kafka. The first execution is immediate, subsequent ones follow the schedule.&lt;/p&gt;
&lt;h3 id=&quot;the-kafka-payload&quot;&gt;The Kafka Payload&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;ticker&quot;: &quot;PETR4&quot;,
  &quot;name&quot;: &quot;Petróleo Brasileiro S.A. - Petrobras&quot;,
  &quot;short_name&quot;: &quot;PETROBRAS PN&quot;,
  &quot;price&quot;: 35.50,
  &quot;volume&quot;: 15234567,
  &quot;updated_at&quot;: 1714234567
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Topic:&lt;/strong&gt; &lt;code&gt;trading-assets-market-data-v1&lt;/code&gt;&lt;br&gt;
&lt;strong&gt;Key:&lt;/strong&gt; asset symbol (e.g. &lt;code&gt;PETR4&lt;/code&gt;) — ensures events for the same asset go to the same partition&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;the-watchlist&quot;&gt;The Watchlist&lt;/h2&gt;
&lt;p&gt;50 assets covering the main B3 segments:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;WATCHLIST = [
    # Blue Chips
    &quot;PETR4&quot;, &quot;PETR3&quot;, &quot;VALE3&quot;, &quot;ITUB4&quot;, &quot;BBDC4&quot;, &quot;BBAS3&quot;, ...
    # REITs / FIIs
    &quot;MXRF11&quot;, &quot;HGLG11&quot;, &quot;KNIP11&quot;, &quot;XPLG11&quot;, ...
    # ETFs
    &quot;BOVA11&quot;, &quot;IVVB11&quot;
]
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2 id=&quot;security-token-out-of-code&quot;&gt;Security: Token Out of Code&lt;/h2&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;# .env — never committed
BRAPI_TOKEN=your_token_here
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;# application — no fallback, required variable
token = os.getenv(&quot;BRAPI_TOKEN&quot;)
if not token:
    logger.error(&quot;BRAPI_TOKEN not set. Aborting.&quot;)
    return
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Never expose credentials as default values in public repositories.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;-validating-the-execution&quot;&gt;✅ Validating the Execution&lt;/h2&gt;
&lt;p&gt;With the service running locally:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;✅ First run executes immediately on startup&lt;/li&gt;
&lt;li&gt;✅ MongoDB receives documents with &lt;code&gt;created_at&lt;/code&gt; automatically&lt;/li&gt;
&lt;li&gt;✅ Kafka receives messages with key = ticker&lt;/li&gt;
&lt;li&gt;✅ Scheduler waits 30 minutes for the next round&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&quot;what-s-next-&quot;&gt;What's Next?&lt;/h2&gt;
&lt;p&gt;With &lt;code&gt;broker-market-data-api&lt;/code&gt; publishing quotes to Kafka, the next step is &lt;code&gt;b3-market-sync-api&lt;/code&gt; — the Java service that consumes these events and synchronizes prices to Redis, feeding the Matching Engine with real-time data.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;about-the-series&quot;&gt;About the Series&lt;/h2&gt;
&lt;p&gt;⬅️ Previous Post: &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/rvneto/infrastructure-as-code-deploying-a-financial-ecosystem-with-docker-compose-2p3d&quot;&gt;Infrastructure as Code&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;➡️ Next Post: Syncing the Real Market: Consuming Brapi and Feeding Redis with Spring Boot&lt;/p&gt;
&lt;p&gt; Series Index: &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/rvneto/series-roadmap-building-a-stock-brokerage-simulator-with-microservices-kgh&quot;&gt;Series Roadmap&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Links:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.linkedin.com/in/roberto-de-vargas/&quot;&gt;LinkedIn&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/rvneto/trading-broker-market-data&quot;&gt;GitHub&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/16542/market-data-integrator-consuming-real-time-data-with-python-mongodb-and-kafka</guid>
<pubDate>Sun, 03 May 2026 20:33:47 +0000</pubDate>
</item>
<item>
<title>How to Build a Serverless Data Lake Foundation with AWS Glue</title>
<link>https://coderlegion.com/16401/how-to-build-a-serverless-data-lake-foundation-with-aws-glue</link>
<description>&lt;h4 id=&quot;1-introduction&quot;&gt;1. Introduction&lt;/h4&gt;
&lt;p&gt;Welcome to this comprehensive tutorial on building a Serverless Data Lake Foundation using AWS Glue. By the end of this guide, you will be able to design and implement a robust, automated pipeline that extracts raw data from Amazon S3, transforms it into an optimized analytics-ready format, and makes it available for querying via Amazon Athena. This architectural pattern is highly useful because it completely eliminates the need to manage underlying servers, clusters, or infrastructure, allowing you to focus entirely on your core data logic. Furthermore, it automatically scales out alongside your data volume, ensuring long-term cost-effectiveness since you only pay for the exact compute resources consumed during the active transformation process. Whether you are dealing with daily batch processing operations or aggregating vast amounts of historical data, mastering this foundational architectural pattern is a critical and necessary milestone in modern data engineering.&lt;/p&gt;
&lt;h4 id=&quot;2-prerequisites&quot;&gt;2. Prerequisites&lt;/h4&gt;
&lt;p&gt;Before starting this step-by-step implementation, ensure you have the following tools and foundational knowledge prepared:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;AWS Account:&lt;/strong&gt; An active Amazon Web Services account with administrative or sufficient permissions to create S3 buckets, configure IAM roles, and deploy AWS Glue jobs.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Basic Python or PySpark Knowledge:&lt;/strong&gt; Familiarity with basic data manipulation concepts, variables, and data structures using Python or Apache Spark.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Understanding of Cloud Storage:&lt;/strong&gt; General knowledge of how object storage systems function, specifically Amazon S3 concepts including buckets and prefixes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;IAM Fundamentals:&lt;/strong&gt; A foundational understanding of Identity and Access Management principles to allow distinct AWS services to communicate securely.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;3-step-by-step&quot;&gt;3. Step-by-Step&lt;/h4&gt;
&lt;h5 id=&quot;step-1-architecting-the-serverless-data-lake-foundation&quot;&gt;Step 1: Architecting the Serverless Data Lake Foundation&lt;/h5&gt;
&lt;p&gt;&lt;strong&gt;What to do:&lt;/strong&gt; Review the end-to-end data flow and understand the specific roles of each AWS service before provisioning any cloud resources. The architecture requires Amazon S3 for storage, Amazon EventBridge for orchestration, AWS Glue for processing and metadata management, and Amazon Athena for consumption.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why do it:&lt;/strong&gt; Establishing a clear mental model and a visual blueprint prevents configuration errors later in the process. It ensures that all AWS services are logically connected, network paths are understood, and the separation between storage and compute is maintained throughout the pipeline design.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Example:&lt;/strong&gt; &lt;br&gt;
&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5jot29rybvf2vq8dr4uh.png&quot; alt=&quot;Solution Diagram&quot;&gt;&lt;/p&gt;
&lt;h5 id=&quot;step-2-configuring-amazon-s3-storage-zones&quot;&gt;Step 2: Configuring Amazon S3 Storage Zones&lt;/h5&gt;
&lt;p&gt;&lt;strong&gt;What to do:&lt;/strong&gt; Access the Amazon S3 console and create a new S3 bucket with a unique name. Inside this bucket, create two distinct folders (prefixes). Name the first folder &lt;code&gt;raw/&lt;/code&gt; to represent the landing zone for your incoming, unprocessed data. Name the second folder &lt;code&gt;curated/&lt;/code&gt; to represent the destination for your clean, transformed data.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why do it:&lt;/strong&gt; Segregating data by its processing state is a fundamental best practice in data engineering. It prevents accidental overwrites of source data, simplifies access control policies, and optimizes query performance by keeping raw, unoptimized files strictly separated from the analytical query engine.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Example:&lt;/strong&gt; &lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-hcl&quot;&gt;# 1. Define the core S3 Data Lake Bucket
resource &quot;aws_s3_bucket&quot; &quot;datalake_production&quot; {
  bucket = &quot;enterprise-datalake-production&quot;

  tags = {
    Environment = &quot;Production&quot;
    Purpose     = &quot;Serverless Data Lake Foundation&quot;
    ManagedBy   = &quot;Terraform&quot;
  }
}

# 2. Enable Bucket Versioning (Recommended Data Lake Best Practice)
resource &quot;aws_s3_bucket_versioning&quot; &quot;datalake_versioning&quot; {
  bucket = aws_s3_bucket.datalake_production.id
  
  versioning_configuration {
    status = &quot;Enabled&quot;
  }
}

# 3. Create the 'raw' subdirectory (Landing Zone)
resource &quot;aws_s3_object&quot; &quot;raw_zone&quot; {
  bucket = aws_s3_bucket.datalake_production.id
  key    = &quot;raw/&quot;
  
  # Defines the object as a directory rather than a standard file
  content_type = &quot;application/x-directory&quot;
}

# 4. Create the 'curated' subdirectory (Gold/Analytics Zone)
resource &quot;aws_s3_object&quot; &quot;curated_zone&quot; {
  bucket = aws_s3_bucket.datalake_production.id
  key    = &quot;curated/&quot;
  
  content_type = &quot;application/x-directory&quot;
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h5 id=&quot;step-3-setting-up-iam-permissions&quot;&gt;Step 3: Setting Up IAM Permissions&lt;/h5&gt;
&lt;p&gt;&lt;strong&gt;What to do:&lt;/strong&gt; Navigate to the AWS Identity and Access Management console and create a new Service Role specifically assigned to AWS Glue. Attach the AWS managed policy named &lt;code&gt;AWSGlueServiceRole&lt;/code&gt;. Additionally, create and attach an inline policy that explicitly grants &lt;code&gt;s3:GetObject&lt;/code&gt; and &lt;code&gt;s3:PutObject&lt;/code&gt; permissions targeted strictly at the S3 bucket you created in the previous step.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why do it:&lt;/strong&gt; AWS services operate under the strict principle of least privilege. AWS Glue cannot access your raw data, write processed files to S3, or publish execution logs to Amazon CloudWatch without explicit, cryptographically secure permissions defined by an IAM Role.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Example:&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-hcl&quot;&gt;# 1. Define the Trust Relationship (Assume Role Policy) for AWS Glue
data &quot;aws_iam_policy_document&quot; &quot;glue_assume_role_policy&quot; {
  statement {
    actions = [&quot;sts:AssumeRole&quot;]
    
    principals {
      type        = &quot;Service&quot;
      identifiers = [&quot;glue.amazonaws.com&quot;]
    }
  }
}

# 2. Create the IAM Role
resource &quot;aws_iam_role&quot; &quot;glue_execution_role&quot; {
  name               = &quot;GlueDataLakeExecutionRole&quot;
  assume_role_policy = data.aws_iam_policy_document.glue_assume_role_policy.json

  tags = {
    Environment = &quot;Production&quot;
    Purpose     = &quot;Serverless Data Lake Foundation&quot;
    ManagedBy   = &quot;Terraform&quot;
  }
}

# 3. Attach the core AWS Managed Policy for Glue Service operations
# This grants necessary permissions for CloudWatch logging, ENI creation (if VPC bounded), etc.
resource &quot;aws_iam_role_policy_attachment&quot; &quot;glue_service_managed_policy&quot; {
  role       = aws_iam_role.glue_execution_role.name
  policy_arn = &quot;arn:aws:iam::aws:policy/service-role/AWSGlueServiceRole&quot;
}

# 4. Define a restrictive inline policy for the specific Data Lake S3 bucket
data &quot;aws_iam_policy_document&quot; &quot;s3_datalake_access_policy&quot; {
  statement {
    sid    = &quot;AllowDataLakeReadWrite&quot;
    effect = &quot;Allow&quot;
    
    actions = [
      &quot;s3:GetObject&quot;,
      &quot;s3:PutObject&quot;
    ]
    
    resources = [
      # Restrict access exclusively to the specific bucket and defined prefixes
      &quot;arn:aws:s3:::enterprise-datalake-production/raw/*&quot;,
      &quot;arn:aws:s3:::enterprise-datalake-production/curated/*&quot;
    ]
  }
}

# 5. Create and attach the inline S3 policy to the IAM Role
resource &quot;aws_iam_role_policy&quot; &quot;s3_datalake_inline_policy&quot; {
  name   = &quot;RestrictedS3DataLakeAccess&quot;
  role   = aws_iam_role.glue_execution_role.id
  policy = data.aws_iam_policy_document.s3_datalake_access_policy.json
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h5 id=&quot;step-4-orchestrating-the-pipeline-execution-flow&quot;&gt;Step 4: Orchestrating the Pipeline Execution Flow&lt;/h5&gt;
&lt;p&gt;&lt;strong&gt;What to do:&lt;/strong&gt; Define the exact sequence of automated events that will trigger and execute your ETL process. Configure an Amazon EventBridge rule that listens for an &lt;code&gt;ObjectCreated&lt;/code&gt; event in your S3 &lt;code&gt;raw/&lt;/code&gt; prefix. Set the target of this rule to invoke your AWS Glue Job automatically whenever a new file finishes uploading.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why do it:&lt;/strong&gt; Automation entirely removes the need for manual intervention and scheduling guesswork. By structurally mapping the sequence, you ensure that compute resources are only utilized when new data is actually present, maximizing efficiency and minimizing idle costs.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Example:&lt;/strong&gt; &lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4ofkeynmdsjdzn9kxqes.png&quot; alt=&quot;Sequence Diagram&quot;&gt;&lt;/p&gt;
&lt;h5 id=&quot;step-5-authoring-the-aws-glue-transformation-job&quot;&gt;Step 5: Authoring the AWS Glue Transformation Job&lt;/h5&gt;
&lt;p&gt;&lt;strong&gt;What to do:&lt;/strong&gt; Open the AWS Glue Studio visual editor and create a new Spark ETL job. Configure the source node to read from your S3 &lt;code&gt;raw/&lt;/code&gt; path. Add a transformation node to clean the data, such as dropping null values, mapping column names to standard conventions, and casting data types correctly. Finally, configure the target node to write the data back to your S3 &lt;code&gt;curated/&lt;/code&gt; path, explicitly selecting Parquet as the output format.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why do it:&lt;/strong&gt; The transformation phase is where raw information becomes valuable data. Writing the output in Apache Parquet format is critical. Parquet is a highly optimized, columnar storage format that compresses data heavily, dramatically reduces long-term S3 storage costs, and exponentially speeds up subsequent analytical queries by allowing engines to skip irrelevant columns.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Example (Terraform):&lt;/strong&gt; &lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-hcl&quot;&gt;resource &quot;aws_glue_job&quot; &quot;datalake_curation_job&quot; {
  name     = &quot;raw-to-curated-parquet-job&quot;
  role_arn = aws_iam_role.glue_execution_role.arn # From the previous IAM step

  # Defines the execution engine and script location
  command {
    name            = &quot;glueetl&quot;
    script_location = &quot;s3://enterprise-datalake-production/scripts/etl_job.py&quot;
    python_version  = &quot;3&quot;
  }

  default_arguments = {
    &quot;--job-language&quot;                     = &quot;python&quot;
    &quot;--enable-metrics&quot;                   = &quot;true&quot;
    &quot;--enable-continuous-cloudwatch-log&quot; = &quot;true&quot;
  }

  # Cost and performance optimization
  glue_version      = &quot;4.0&quot;
  worker_type       = &quot;G.1X&quot;
  number_of_workers = 2

  tags = {
    Environment = &quot;Production&quot;
    Purpose     = &quot;Serverless Data Lake Foundation&quot;
    ManagedBy   = &quot;Terraform&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Example (Glue Job):&lt;/strong&gt; &lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import sys
from awsglue.transforms import *
from awsglue.utils import getResolvedOptions
from pyspark.context import SparkContext
from awsglue.context import GlueContext
from awsglue.job import Job

sc = SparkContext.getOrCreate()
glueContext = GlueContext(sc)
spark = glueContext.spark_session
job = Job(glueContext)

# ==============================================================================
# Node 1: S3 Source (Reads raw CSV data)
# ==============================================================================
S3Source_node1 = glueContext.create_dynamic_frame.from_options(
    format_options={&quot;quoteChar&quot;: '&quot;', &quot;withHeader&quot;: True, &quot;separator&quot;: &quot;,&quot;},
    connection_type=&quot;s3&quot;,
    format=&quot;csv&quot;,
    connection_options={
        &quot;paths&quot;: [&quot;s3://enterprise-datalake-production/raw/&quot;], 
        &quot;recurse&quot;: True
    },
    transformation_ctx=&quot;S3Source_node1&quot;
)

# ==============================================================================
# Node 2: Transform - ApplyMapping (Cleans and maps data types)
# ==============================================================================
ApplyMapping_node2 = ApplyMapping.apply(
    frame=S3Source_node1,
    mappings=[
        (&quot;id&quot;, &quot;string&quot;, &quot;user_id&quot;, &quot;string&quot;),
        (&quot;name&quot;, &quot;string&quot;, &quot;full_name&quot;, &quot;string&quot;),
        (&quot;timestamp&quot;, &quot;string&quot;, &quot;event_time&quot;, &quot;timestamp&quot;)
    ],
    transformation_ctx=&quot;ApplyMapping_node2&quot;
)

# ==============================================================================
# Node 3: S3 Target (Writes optimized Parquet data)
# ==============================================================================
S3Target_node3 = glueContext.write_dynamic_frame.from_options(
    frame=ApplyMapping_node2,
    connection_type=&quot;s3&quot;,
    format=&quot;glueparquet&quot;,
    connection_options={
        &quot;path&quot;: &quot;s3://enterprise-datalake-production/curated/&quot;, 
        &quot;partitionKeys&quot;: []
    },
    format_options={&quot;compression&quot;: &quot;snappy&quot;},
    transformation_ctx=&quot;S3Target_node3&quot;
)

job.commit()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Example (Glue Studio):&lt;/strong&gt; &lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ndzf8ajjlbo5jmp62ee1.png&quot; alt=&quot;Glue Nodes&quot;&gt;&lt;/p&gt;
&lt;h5 id=&quot;step-6-cataloging-the-data-with-aws-glue-crawlers&quot;&gt;Step 6: Cataloging the Data with AWS Glue Crawlers&lt;/h5&gt;
&lt;p&gt;&lt;strong&gt;What to do:&lt;/strong&gt; Navigate to the Crawlers section within the AWS Glue console. Create a new crawler and configure its target repository to point directly at your &lt;code&gt;curated/&lt;/code&gt; S3 folder. Assign the IAM role created in Step 3 to the crawler. Set the crawler to run on a daily schedule or to be triggered immediately after the AWS Glue transformation job completes successfully.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why do it:&lt;/strong&gt; Data sitting in S3 is useless to business analysts without structural context. The Crawler automatically inspects your Parquet files, infers the underlying schema including column names and data types, and registers this structural definition as a logical table within the centralized AWS Glue Data Catalog.&lt;/p&gt;
&lt;h5 id=&quot;step-7-querying-the-transformed-data-with-amazon-athena&quot;&gt;Step 7: Querying the Transformed Data with Amazon Athena&lt;/h5&gt;
&lt;p&gt;&lt;strong&gt;What to do:&lt;/strong&gt; Navigate to the Amazon Athena console. Ensure you have set up an Athena query result location in a separate S3 bucket. Select the database created by your Glue Crawler from the dropdown menu. Write and execute standard SQL queries against your newly cataloged table to analyze the data.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why do it:&lt;/strong&gt; This final step validates the structural integrity of your entire pipeline. Athena uses the metadata schema stored in the Data Catalog to seamlessly execute SQL queries directly against the compressed Parquet files residing in S3. This proves that your serverless architecture is fully operational and ready for business intelligence workloads.&lt;/p&gt;
&lt;h4 id=&quot;4-common-troubleshooting&quot;&gt;4. Common Troubleshooting&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Problem 1: Out of Memory Errors in Glue Jobs&lt;/strong&gt;&lt;br&gt;
This issue frequently occurs due to data skew, where one worker node attempts to process significantly more data than others, or when attempting to process thousands of excessively small files simultaneously. &lt;br&gt;
&lt;strong&gt;Solution:&lt;/strong&gt; Resolve this by navigating to the Glue Job details and increasing the worker type from the standard configuration to &lt;code&gt;G.1X&lt;/code&gt; or &lt;code&gt;G.2X&lt;/code&gt;, providing more memory per executor. Alternatively, utilize Spark's &lt;code&gt;.repartition()&lt;/code&gt; command within your script to evenly distribute the workload across the entire serverless cluster before executing wide transformations.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Problem 2: Glue Crawler Creates Multiple Unwanted Tables&lt;/strong&gt;&lt;br&gt;
If your curated S3 data utilizes a deeply nested, highly partitioned, or logically inconsistent folder structure, the AWS Glue Crawler might misinterpret the subfolders as entirely separate tables instead of partitions of a single dataset.&lt;br&gt;
&lt;strong&gt;Solution:&lt;/strong&gt; To rectify this, edit the Crawler configuration settings. Locate the section labeled &quot;Grouping behavior for S3 data&quot; and explicitly check the option to &quot;Create a single schema for each S3 path&quot;. This forces the crawler to treat all files under the root prefix as part of the same table.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Problem 3: Amazon Athena Queries Return Zero Records&lt;/strong&gt;&lt;br&gt;
If the table successfully appears in the Data Catalog but standard &lt;code&gt;SELECT&lt;/code&gt; queries return absolutely empty results, the S3 data is highly likely partitioned, but the Data Catalog remains unaware of the new partition directories.&lt;br&gt;
&lt;strong&gt;Solution:&lt;/strong&gt; Execute the &lt;code&gt;MSCK REPAIR TABLE your_table_name;&lt;/code&gt; command directly within the Athena query editor. This command scans the S3 path, loads the missing partition metadata into the Data Catalog, and immediately makes the data queryable.&lt;/p&gt;
&lt;h4 id=&quot;5-conclusion&quot;&gt;5. Conclusion&lt;/h4&gt;
&lt;p&gt;Congratulations on successfully architecting and understanding the Serverless Data Lake Foundation. You have learned how to decouple storage from compute layers, automate data transformations using event-driven principles, and seamlessly bridge raw information into a highly optimized, queryable state. By mastering these core components, you now possess the foundational engineering skills required to build scalable, enterprise-grade data platforms. As a logical next step, consider exploring AWS Step Functions to orchestrate more complex, multi-job dependencies, or experiment with Apache Iceberg within your AWS Glue jobs to introduce robust transactional capabilities and time-travel querying to your data lake.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/16401/how-to-build-a-serverless-data-lake-foundation-with-aws-glue</guid>
<pubDate>Sat, 02 May 2026 12:41:31 +0000</pubDate>
</item>
<item>
<title>Data's Expiration Date</title>
<link>https://coderlegion.com/16216/datas-expiration-date</link>
<description>&lt;p&gt;Here's something nobody tells you when you start in data engineering: &lt;em&gt;data goes bad&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Not like &quot;error 500&quot; bad. More like &quot;left the milk out&quot; bad. &lt;strong&gt;Silent. Subtle. Dangerous.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;I learned this the hard way. Built a beautiful pipeline once: clean transformations, perfect schema, the works. Proud of myself. Real proud. Then, two weeks later, predictions started drifting. Turns out the upstream API had quietly started serving cached responses after an update. My data looked fresh. Smelled fresh. But it was last week's data wearing a new timestamp like a cheap disguise.&lt;/p&gt;
&lt;p&gt;That's the thing about data. It has a shelf life. And we keep pretending it doesn't.&lt;/p&gt;
&lt;p&gt;Your customer segmentation from Q2? &lt;em&gt;Adorable&lt;/em&gt;. Your users moved on in Q3. That fraud detection model from last year? &lt;em&gt;Cute&lt;/em&gt;. Fraudsters iterate faster than your sprint cycle.&lt;/p&gt;
&lt;p&gt;So now I check expiration dates like I'm grocery shopping. When was this actually generated? Has the schema shifted? Is the distribution what my model expects? Can I trace it back to source?&lt;/p&gt;
&lt;p&gt;Because here's the truth: &lt;strong&gt;data without a valid date isn't data&lt;/strong&gt;. It's a story you're telling yourself. And in production, self-deception is the most expensive bug you'll ship.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/16216/datas-expiration-date</guid>
<pubDate>Thu, 30 Apr 2026 01:42:18 +0000</pubDate>
</item>
<item>
<title>Stop Treating Your Data Pipeline Like a Script - Treat It Like a Product</title>
<link>https://coderlegion.com/16007/stop-treating-your-data-pipeline-like-a-script-treat-it-like-a-product</link>
<description>&lt;p&gt;I learned this the hard way after watching a &lt;strong&gt;&lt;em&gt;simple&lt;/em&gt;&lt;/strong&gt; ETL job torch our weekend.&lt;/p&gt;
&lt;p&gt;When I started in data engineering, I thought my job was writing scripts that moved data from A to B. Clean, logical, done. &lt;strong&gt;I was wrong&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The difference between a pipeline that works and one that &lt;em&gt;survives&lt;/em&gt;? Three things nobody told me:&lt;/p&gt;
&lt;p&gt; &lt;strong&gt;Observability first, logic second&lt;/strong&gt; - If you can't see what's happening inside your pipeline, you're flying blind. Dashboards aren't optional; they're infrastructure.&lt;/p&gt;
&lt;p&gt; &lt;strong&gt;Data contracts over hope&lt;/strong&gt; - Assume your upstream source will silently betray you. Schema changes, null explosions, timestamp format switches at 2am. Code defensively or suffer.&lt;/p&gt;
&lt;p&gt; &lt;strong&gt;Idempotency is non-negotiable&lt;/strong&gt; - Rerunning yesterday's job shouldn't duplicate records or corrupt state. Build for reruns, not just first runs.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The mindset shift:&lt;/strong&gt; Your pipeline isn't finished when it runs. It's finished when it runs &lt;em&gt;reliably&lt;/em&gt; while you're sleeping.&lt;/p&gt;
&lt;p&gt;What's one lesson you learned after your first production failure? Drop it below. &lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/16007/stop-treating-your-data-pipeline-like-a-script-treat-it-like-a-product</guid>
<pubDate>Mon, 27 Apr 2026 19:04:09 +0000</pubDate>
</item>
<item>
<title>I Built a Real-Time Crypto Analytics Pipeline for $0.01/Month — Here's the Full Architecture</title>
<link>https://coderlegion.com/15962/i-built-a-real-time-crypto-analytics-pipeline-for-0-01-month-heres-the-full-architecture</link>
<description>&lt;p&gt;&lt;em&gt;How I combined Apache Flink, Redpanda, Airflow, dbt Cloud, and Grafana to track Bitcoin, Ethereum, Solana, BNB, and Cardano in real time — all running on Google Cloud for less than a cup of coffee per month.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;If you've been learning data engineering, you've probably built pipelines that move CSV files from A to B. Nothing wrong with that — but real data engineering interviews are starting to ask a different question: &lt;strong&gt;Can you handle data that never stops arriving?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This article walks through CoinPulse, a project I built from scratch to answer that question. It's a hybrid streaming and batch crypto analytics pipeline that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Consumes live trade events from Binance's WebSocket feed at ~1 tick per second per coin&lt;/li&gt;
&lt;li&gt;Processes them with Apache Flink using 1-minute tumbling windows&lt;/li&gt;
&lt;li&gt;Enriches the streaming data daily with CoinGecko market metadata via Airflow&lt;/li&gt;
&lt;li&gt;Transforms everything with dbt Cloud&lt;/li&gt;
&lt;li&gt;Serves a live, auto-refreshing Grafana dashboard — publicly accessible&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Total cloud cost: &lt;strong&gt;~$0.01/month.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Here's how I built it.&lt;/p&gt;
&lt;h3 id=&quot;the-problem-i-was-solving&quot;&gt;The Problem I Was Solving&lt;/h3&gt;
&lt;p&gt;Crypto market data has two distinct tempos:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Real-time (milliseconds to seconds):&lt;/strong&gt; Price ticks from exchanges. You need stream processing here — you can't batch-load ticks after the fact and call it &quot;live.&quot; The data volume is too high to dump raw into a warehouse row by row.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Daily (slow-moving context):&lt;/strong&gt; Market cap, trading volume, rankings, OHLC candlesticks. These change once a day. You don't need Flink for this — a simple scheduled API call works fine.&lt;/p&gt;
&lt;p&gt;The interesting engineering challenge is: &lt;strong&gt;how do you join these two tempos cheaply and reliably?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The naive answer — stream everything into BigQuery using the Streaming Insert API — costs money at scale. The smarter answer is what I built: stream to GCS first, batch-load to BigQuery for free, and join the two lanes in dbt.&lt;/p&gt;
&lt;h3 id=&quot;architecture-overview-1&quot;&gt;Architecture Overview&lt;/h3&gt;
&lt;p&gt;Before diving in, here's the full picture:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;STREAMING LANE
Binance WebSocket → Python Producer → Redpanda → PyFlink → GCS → BigQuery
                    (real-time ticks)  (Kafka)  (windows)  (JSONL) (free load)

BATCH LANE  
CoinGecko API → Airflow DAG (7 tasks) → GCS → BigQuery
                (daily @ 06:00 UTC)    (Parquet)  (free load)

TRANSFORMATION LAYER
BigQuery → dbt Cloud → crypto_staging.* → crypto_mart.*
           (daily @ 07:00 UTC)

VISUALIZATION
BigQuery → Grafana Cloud → 6 panels, auto-refresh 30s
           (BigQuery plugin, free)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Everything runs locally in Docker except the data warehouse (BigQuery) and transformation/visualization layers (dbt Cloud + Grafana Cloud). This is the &quot;local-to-cloud&quot; pattern — keep compute on your machine, use GCP only for storage and analytics.&lt;/p&gt;
&lt;h3 id=&quot;the-streaming-lane-binance-redpanda-pyflink-gcs&quot;&gt;The Streaming Lane: Binance → Redpanda → PyFlink → GCS&lt;/h3&gt;
&lt;h4 id=&quot;why-binance-instead-of-coincap-&quot;&gt;Why Binance Instead of CoinCap?&lt;/h4&gt;
&lt;p&gt;I originally planned to use CoinCap's WebSocket feed. Halfway through the build, I discovered CoinCap now requires an API key even for the free tier. Binance's WebSocket, however, is completely open — no account, no key, no rate limits on the trade stream.&lt;/p&gt;
&lt;p&gt;The URL pattern is:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;wss://stream.binance.com:9443/stream?streams=btcusdt@trade/ethusdt@trade/solusdt@trade/bnbusdt@trade/adausdt@trade
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each message looks like:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;stream&quot;: &quot;btcusdt@trade&quot;,
  &quot;data&quot;: {
    &quot;s&quot;: &quot;BTCUSDT&quot;,
    &quot;p&quot;: &quot;76034.12&quot;,
    &quot;q&quot;: &quot;0.00150000&quot;,
    &quot;T&quot;: 1776875894886
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;the-python-producer&quot;&gt;The Python Producer&lt;/h4&gt;
&lt;p&gt;The producer is a Docker container running &lt;code&gt;websockets&lt;/code&gt; + &lt;code&gt;confluent-kafka&lt;/code&gt;. It connects to Binance, maps Binance pair names to readable coin names (&lt;code&gt;BTCUSDT → bitcoin&lt;/code&gt;), and publishes JSON messages to a Redpanda topic:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;message = {
    &quot;symbol&quot;: &quot;bitcoin&quot;,
    &quot;price_usd&quot;: 76034.12,
    &quot;event_timestamp&quot;: &quot;2026-04-23T18:31:34+00:00&quot;
}
producer.produce(topic=&quot;crypto-prices&quot;, key=&quot;bitcoin&quot;, value=json.dumps(message))
&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;why-redpanda-instead-of-kafka-&quot;&gt;Why Redpanda Instead of Kafka?&lt;/h4&gt;
&lt;p&gt;Kafka requires Zookeeper. In a local Docker setup, that's three containers just for the message broker (Zookeeper, Kafka broker, Schema Registry if you want it). Redpanda is Kafka-API compatible — meaning PyFlink's Kafka connector works with it unchanged — but runs as a &lt;strong&gt;single binary, no Zookeeper&lt;/strong&gt;. It starts in under 3 seconds and uses ~512MB RAM instead of Kafka's 2-3GB.&lt;/p&gt;
&lt;h4 id=&quot;the-pyflink-job&quot;&gt;The PyFlink Job&lt;/h4&gt;
&lt;p&gt;The Flink job does the heavy lifting. It consumes from Redpanda, applies a &lt;strong&gt;1-minute tumbling event-time window&lt;/strong&gt; per coin symbol, and computes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;avg_price&lt;/code&gt; — average price within the window&lt;/li&gt;
&lt;li&gt;&lt;code&gt;min_price&lt;/code&gt; / &lt;code&gt;max_price&lt;/code&gt; — price range&lt;/li&gt;
&lt;li&gt;&lt;code&gt;price_stddev&lt;/code&gt; — standard deviation (volatility proxy)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;open_price&lt;/code&gt; / &lt;code&gt;close_price&lt;/code&gt; — first and last tick&lt;/li&gt;
&lt;li&gt;&lt;code&gt;record_count&lt;/code&gt; — number of ticks received&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Event-time windows (as opposed to processing-time) are important here. They use the timestamp embedded in the Binance message, so late-arriving messages (within a 10-second watermark) land in the correct window rather than the current one.&lt;/p&gt;
&lt;p&gt;The output of each completed window is a JSONL file written to GCS:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;gs://coinpulse-data-lake/streaming/2026/04/23/18/stream_bitcoin_20260423_183134.jsonl
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A scheduled BigQuery Load Job (free) then loads these files into &lt;code&gt;crypto_raw.stream_prices&lt;/code&gt;, partitioned by HOUR on &lt;code&gt;event_timestamp&lt;/code&gt; and clustered by &lt;code&gt;symbol&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The key insight here:&lt;/strong&gt; I never use BigQuery's Streaming Insert API, which charges ~$0.01 per 200MB. Load Jobs are completely free. The data is 1 minute old in the warehouse instead of 0 seconds old — but for a dashboard refreshing every 30 seconds, nobody notices.&lt;/p&gt;
&lt;h3 id=&quot;the-batch-lane-coingecko-airflow-gcs-bigquery&quot;&gt;The Batch Lane: CoinGecko → Airflow → GCS → BigQuery&lt;/h3&gt;
&lt;h4 id=&quot;the-airflow-dag&quot;&gt;The Airflow DAG&lt;/h4&gt;
&lt;p&gt;The batch pipeline runs as a 7-task Airflow DAG at 06:00 UTC daily with two parallel branches:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-plaintext&quot;&gt;fetch_coingecko → transform_to_parquet → upload_to_gcs → load_to_bigquery
fetch_ohlc      → upload_ohlc_to_gcs   → load_ohlc_to_bigquery
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Markets branch&lt;/strong&gt; hits CoinGecko's &lt;code&gt;/coins/markets&lt;/code&gt; endpoint for all 5 coins in a single call, getting current price, market cap, 24h volume, price change percentage, rank, and fully diluted valuation.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;OHLC branch&lt;/strong&gt; hits &lt;code&gt;/coins/{id}/ohlc?days=1&lt;/code&gt; for each coin, getting candlestick arrays &lt;code&gt;[timestamp, open, high, low, close]&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Both branches serialize data to &lt;strong&gt;typed Parquet&lt;/strong&gt; (using explicit schema definitions, not autodetect — more on why this matters below) and upload to GCS before loading to BigQuery.&lt;/p&gt;
&lt;h4 id=&quot;the-schema-war-and-how-i-won-it-&quot;&gt;The Schema War (And How I Won It)&lt;/h4&gt;
&lt;p&gt;This took me longer than I'd like to admit. The root cause: PyArrow (used by Pandas &lt;code&gt;.to_parquet()&lt;/code&gt;) infers Python &lt;code&gt;datetime&lt;/code&gt; objects as &lt;code&gt;TIMESTAMP&lt;/code&gt; in Parquet metadata. But when BigQuery's autodetect reads a &lt;code&gt;datetime.isoformat()&lt;/code&gt; string, it creates the column as &lt;code&gt;STRING&lt;/code&gt;. When the same column arrives as a proper &lt;code&gt;datetime&lt;/code&gt; object in the next run, BigQuery rejects the load because &lt;code&gt;TIMESTAMP&lt;/code&gt; ≠ &lt;code&gt;STRING&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The permanent fix: define explicit schemas in the BigQuery Load Job config and force all string columns to &lt;code&gt;str&lt;/code&gt; dtype in Pandas before writing Parquet:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;df[&quot;snapshot_date&quot;] = df[&quot;snapshot_date&quot;].astype(str)
df[&quot;ingested_at&quot;]   = df[&quot;ingested_at&quot;].astype(str)

job_config = bigquery.LoadJobConfig(
    source_format=bigquery.SourceFormat.PARQUET,
    schema=MARKETS_SCHEMA,  # explicit, no autodetect
    write_disposition=bigquery.WriteDisposition.WRITE_APPEND,
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once I stopped fighting autodetect and defined schemas explicitly, the pipeline ran clean for weeks.&lt;/p&gt;
&lt;h3 id=&quot;the-transformation-layer-dbt-cloud&quot;&gt;The Transformation Layer: dbt Cloud&lt;/h3&gt;
&lt;h4 id=&quot;why-dbt-cloud-over-dbt-core&quot;&gt;Why dbt Cloud Over dbt Core&lt;/h4&gt;
&lt;p&gt;I already had Redpanda, Flink, Airflow, and PostgreSQL running locally — approximately 13GB RAM. Adding a dbt Core Docker container would push that higher for no functional gain. dbt Cloud's free developer plan gives a managed scheduler, visual lineage graph, and test runner at zero cost and zero local RAM.&lt;/p&gt;
&lt;h4 id=&quot;the-model-structure&quot;&gt;The Model Structure&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Staging layer&lt;/strong&gt; (views, zero storage cost):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;stg_stream_prices&lt;/code&gt; — cleans and type-casts streaming data, filters nulls and zero prices&lt;/li&gt;
&lt;li&gt;&lt;code&gt;stg_coingecko_markets&lt;/code&gt; — adds &lt;code&gt;market_cap_category&lt;/code&gt; (large/mid/small/micro cap) derived column&lt;/li&gt;
&lt;li&gt;&lt;code&gt;stg_ohlc_candles&lt;/code&gt; — computes &lt;code&gt;candle_range&lt;/code&gt;, &lt;code&gt;candle_change_pct&lt;/code&gt;, and &lt;code&gt;candle_direction&lt;/code&gt; (bullish/bearish)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Mart layer&lt;/strong&gt; (incremental tables, insert_overwrite):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;mart_crypto_prices&lt;/code&gt; — joins streaming aggregations with latest daily market metadata&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mart_volatility&lt;/code&gt; — computes hourly &lt;code&gt;composite_volatility_score&lt;/code&gt; combining streaming stddev with OHLC candle metrics&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The mart models use &lt;code&gt;insert_overwrite&lt;/code&gt; with hour-level partitioning and an incremental filter that scans only the last 2 hours of streaming data per run. This means dbt never full-scans the streaming table — it touches only the partitions it needs to update.&lt;/p&gt;
&lt;h4 id=&quot;the-dbt-deploy-job&quot;&gt;The dbt Deploy Job&lt;/h4&gt;
&lt;p&gt;A scheduled dbt Cloud production environment job runs daily at &lt;strong&gt;07:00 UTC&lt;/strong&gt; — one hour after Airflow — ensuring mart tables always have fresh data before the dashboard refreshes.&lt;/p&gt;
&lt;p&gt;After some initial failures (a common dbt Fusion syntax issue with &lt;code&gt;accepted_values&lt;/code&gt; requiring &lt;code&gt;arguments:&lt;/code&gt; nesting), the deploy job has run cleanly every day since April 19, 2026. Each run completes in under 45 seconds.&lt;/p&gt;
&lt;h3 id=&quot;the-dashboard-grafana-cloud&quot;&gt;The Dashboard: Grafana Cloud&lt;/h3&gt;
&lt;h4 id=&quot;why-grafana-over-looker-studio&quot;&gt;Why Grafana Over Looker Studio&lt;/h4&gt;
&lt;p&gt;Looker Studio is the natural choice for BigQuery — it has a native connector and no setup friction. But it has one fatal flaw for streaming analytics: &lt;strong&gt;it doesn't auto-refresh&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Grafana Cloud's free tier includes the BigQuery plugin (one-click install, no credit card), supports 30-second auto-refresh, has proper time series panels with logarithmic Y-axis (essential when plotting BTC at $76K alongside ADA at $0.25 on the same chart), and produces significantly more polished visualizations.&lt;/p&gt;
&lt;p&gt;My prior projects (Sovereign Debt Observatory, Tech Ecosystem Observatory) both used Looker Studio, so using Grafana here also demonstrates range across tooling.&lt;/p&gt;
&lt;h4 id=&quot;the-6-dashboard-panels&quot;&gt;The 6 Dashboard Panels&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Panel 1 — Live Crypto Prices (Streaming)&lt;/strong&gt;&lt;br&gt;
A treemap showing the latest price for each coin from &lt;code&gt;crypto_raw.stream_prices&lt;/code&gt;, colour-coded green/red by price direction. BTC at $76K, ETH at $2.3K, SOL at $86, BNB at $630, ADA at $0.249.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3en0m48tmf3aw9cu1v1o.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Panel 2 — Price Trend Over Time (Streaming)&lt;/strong&gt;&lt;br&gt;
A time series chart with logarithmic Y-axis showing &lt;code&gt;avg_price&lt;/code&gt; per 1-minute window for all coins. The log scale handles the 300,000x price spread between BTC and ADA.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6hrj4gfo8movdpezaim6.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Panel 3 — Hourly Volatility Score (Streaming)&lt;/strong&gt;&lt;br&gt;
A gradient bar chart (green → red) showing the &lt;code&gt;composite_volatility_score&lt;/code&gt; per coin from &lt;code&gt;mart_volatility&lt;/code&gt;. Cardano leads at 0.566 (most volatile relative to its price), Binance Coin lowest at 0.264.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qxn1hhyvnnfg967copky.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Panel 4 — Market Cap Rankings (Batch)&lt;/strong&gt;&lt;br&gt;
BTC dominates at $1.57T, ETH at $289B. The scale difference is immediately visible and gives context to the streaming data.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zcyugt3ejy117zwracvj.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Panel 5 — Daily OHLC Candles (Batch)&lt;/strong&gt;&lt;br&gt;
A table panel showing raw candle data from &lt;code&gt;coingecko_ohlc_candles&lt;/code&gt; with &lt;code&gt;candle_direction&lt;/code&gt; colour-coded — bearish rows highlighted in red.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5fuy7m5zxxv2o5g0nny9.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Panel 6 — 24h Price Change % (Batch)&lt;/strong&gt;&lt;br&gt;
ADA leads at +0.563%, ETH is the only negative at -0.132%.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cypoq4bqotrzp1ffp2y1.png&quot; alt=&quot;Image description&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Public URL:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://derrickryangiggs.grafana.net/public-dashboards/81560968e15140f08f65b52d78a4b252&quot;&gt;https://derrickryangiggs.grafana.net/public-dashboards/81560968e15140f08f65b52d78a4b252&lt;/a&gt;&lt;/p&gt;
&lt;h3 id=&quot;infrastructure-terraform-for-everything&quot;&gt;Infrastructure: Terraform for Everything&lt;/h3&gt;
&lt;p&gt;All GCP resources are defined in Terraform — nothing clicked in the console. Running &lt;code&gt;terraform apply&lt;/code&gt; provisions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;GCP project with billing account linked&lt;/li&gt;
&lt;li&gt;Required APIs (BigQuery, BigQuery Storage, Cloud Storage, Resource Manager)&lt;/li&gt;
&lt;li&gt;Service account with least-privilege IAM roles (BQ Data Editor, BQ Job User, GCS Object Admin)&lt;/li&gt;
&lt;li&gt;GCS bucket with 90-day lifecycle rule&lt;/li&gt;
&lt;li&gt;BigQuery datasets and tables with explicit schemas, partitioning, and clustering&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Nothing is hardcoded.&lt;/strong&gt; All values flow from &lt;code&gt;.env&lt;/code&gt; via &lt;code&gt;TF_VAR_&lt;/code&gt; prefixed variables. This is non-negotiable — hardcoded project IDs in &lt;code&gt;.tf&lt;/code&gt; files are a code review failure waiting to happen.&lt;/p&gt;
&lt;h3 id=&quot;the-cost-breakdown-for-real-&quot;&gt;The Cost Breakdown (For Real)&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Service &lt;/th&gt;&lt;th&gt;Details &lt;/th&gt;&lt;th&gt;Monthly Cost&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Redpanda &lt;/td&gt;&lt;td&gt;Local Docker &lt;/td&gt;&lt;td&gt;$0.00&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;PyFlink &lt;/td&gt;&lt;td&gt;Local Docker &lt;/td&gt;&lt;td&gt;$0.00&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Apache Airflow &lt;/td&gt;&lt;td&gt;Local Docker &lt;/td&gt;&lt;td&gt;$0.00&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Google Cloud Storage &lt;/td&gt;&lt;td&gt;~200MB of Parquet + JSONL &lt;/td&gt;&lt;td&gt;~$0.01&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;BigQuery &lt;/td&gt;&lt;td&gt;&amp;lt;1GB data, Load Jobs only &lt;/td&gt;&lt;td&gt;~$0.00&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;dbt Cloud &lt;/td&gt;&lt;td&gt;Developer plan &lt;/td&gt;&lt;td&gt;$0.00&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Grafana Cloud &lt;/td&gt;&lt;td&gt;Free tier &lt;/td&gt;&lt;td&gt;$0.00&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt; &lt;/td&gt;&lt;td&gt;&lt;/td&gt;&lt;td&gt;&lt;strong&gt;~$0.01/month&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The $0.01 is literally the GCS storage bill. Everything else is free.&lt;/p&gt;
&lt;p&gt;The three decisions that make this possible:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Use GCS + Load Jobs instead of BigQuery Streaming Inserts.&lt;/strong&gt; Streaming Inserts charge per row. Load Jobs are free. The trade-off is 1-minute data freshness instead of real-time — acceptable for a dashboard refreshing every 30 seconds.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Run compute locally.&lt;/strong&gt; Cloud Composer (managed Airflow) starts at $300/month. Dataproc (managed Flink) is similarly expensive. Running on your own machine costs nothing except electricity.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Use free tiers everywhere else.&lt;/strong&gt; dbt Cloud developer plan, Grafana Cloud free tier, CoinGecko Demo API free tier, Binance WebSocket (no auth required). Every external service in this pipeline has a genuinely free tier sufficient for this workload.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;hard-won-lessons&quot;&gt;Hard-Won Lessons&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;1. Flink's &lt;code&gt;WindowFunction.apply()&lt;/code&gt; signature changed in 2.2.0&lt;/strong&gt;&lt;br&gt;
In PyFlink 1.18, &lt;code&gt;apply(self, key, window, inputs, collector)&lt;/code&gt; uses a collector argument. In 2.2.0, the signature is &lt;code&gt;apply(self, key, window, inputs)&lt;/code&gt; and you use &lt;code&gt;yield&lt;/code&gt; instead. This isn't documented prominently. I lost an afternoon to this.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. Read env vars inside functions, not at module import time&lt;/strong&gt;&lt;br&gt;
My Airflow DAG originally read &lt;code&gt;GCS_BUCKET_NAME = os.environ[&quot;GCS_BUCKET_NAME&quot;]&lt;/code&gt; at the module level. When Airflow's scheduler reloads the module after a container restart, &lt;code&gt;GCS_BUCKET_NAME&lt;/code&gt; is empty — the env var isn't available at import time in the scheduler process. Moving all &lt;code&gt;os.environ&lt;/code&gt; reads inside the task functions fixed this permanently.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. Never use autodetect for BigQuery Load Jobs if you care about schema stability&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;autodetect=True&lt;/code&gt; infers schema from the first file it sees. If that file has a &lt;code&gt;datetime&lt;/code&gt; object, BQ creates a &lt;code&gt;TIMESTAMP&lt;/code&gt; column. If the next file has an &lt;code&gt;.isoformat()&lt;/code&gt; string, BQ rejects it as a &lt;code&gt;STRING&lt;/code&gt; / &lt;code&gt;TIMESTAMP&lt;/code&gt; mismatch. Define explicit schemas in the &lt;code&gt;LoadJobConfig&lt;/code&gt; and cast DataFrame columns explicitly before writing Parquet.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4. The Flink TaskManager won't connect if &lt;code&gt;jobmanager.rpc.address&lt;/code&gt; in &lt;code&gt;config.yaml&lt;/code&gt; doesn't match your Docker container name&lt;/strong&gt;&lt;br&gt;
The baked-in &lt;code&gt;config.yaml&lt;/code&gt; in the PyFlink image has &lt;code&gt;jobmanager.rpc.address: jobmanager&lt;/code&gt;. My container is named &lt;code&gt;coinpulse-flink-jobmanager&lt;/code&gt;. The &lt;code&gt;FLINK_PROPERTIES&lt;/code&gt; env var doesn't always override &lt;code&gt;config.yaml&lt;/code&gt;. The fix: add a &lt;code&gt;sed&lt;/code&gt; command in the Dockerfile to rewrite the address in &lt;code&gt;config.yaml&lt;/code&gt; directly.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;5. Redpanda's &lt;code&gt;rpk topic create&lt;/code&gt; command arguments must be on one line&lt;/strong&gt;&lt;br&gt;
Multi-line bash in Docker Compose &lt;code&gt;entrypoint&lt;/code&gt; with &lt;code&gt;&amp;gt;&lt;/code&gt; YAML block scalar doesn't handle line continuations the way you'd expect. The &lt;code&gt;--brokers&lt;/code&gt;, &lt;code&gt;--partitions&lt;/code&gt;, and &lt;code&gt;--replicas&lt;/code&gt; flags ended up being interpreted as separate commands. Fix: use &lt;code&gt;[&quot;bash&quot;, &quot;-c&quot;, &quot;single long command string&quot;]&lt;/code&gt; format instead.&lt;/p&gt;
&lt;h3 id=&quot;what-i-d-add-next-the-circuit-breaker-enhancements-&quot;&gt;What I'd Add Next (The &quot;Circuit Breaker&quot; Enhancements)&lt;/h3&gt;
&lt;p&gt;After sharing the project, I got feedback suggesting three production-grade improvements. Here's my assessment:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. dbt-expectations data contracts&lt;/strong&gt;&lt;br&gt;
Add guardrails so the pipeline fails loudly if bad data arrives — negative prices, null coin symbols, impossible price spikes. In dbt, this is two lines per test in your schema YAML. The &lt;code&gt;dbt-expectations&lt;/code&gt; package brings &lt;code&gt;expect_column_values_to_be_between&lt;/code&gt; and similar assertions. Highly recommended, zero cost.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. BigQuery ML ARIMA_PLUS price forecasting&lt;/strong&gt;&lt;br&gt;
Train a time series model directly in BigQuery SQL to forecast the next 7 days of prices per coin. The first 10GB of &lt;code&gt;CREATE MODEL&lt;/code&gt; data processed monthly is free — both projects' datasets are well under that threshold, making this effectively free. It turns the dashboard from &quot;here's what happened&quot; into &quot;here's what might happen.&quot;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. GitHub Actions CI/CD for Terraform&lt;/strong&gt;&lt;br&gt;
A &lt;code&gt;terraform plan&lt;/code&gt; on every PR to the repo, ensuring infrastructure changes are reviewed before applying. Standard practice in production environments, takes about 20 lines of GitHub Actions YAML to implement.&lt;/p&gt;
&lt;h3 id=&quot;the-full-stack-summarised&quot;&gt;The Full Stack, Summarised&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Layer &lt;/th&gt;&lt;th&gt;Tool &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;IaC &lt;/td&gt;&lt;td&gt;Terraform &lt;/td&gt;&lt;td&gt;Reproducible, version-controlled infrastructure&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Message Broker &lt;/td&gt;&lt;td&gt;Redpanda &lt;/td&gt;&lt;td&gt;Kafka-compatible, no Zookeeper, runs in one container&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Stream Processing &lt;/td&gt;&lt;td&gt;PyFlink 2.2.0 &lt;/td&gt;&lt;td&gt;Stateful windowed aggregations, event-time semantics&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Orchestration &lt;/td&gt;&lt;td&gt;Apache Airflow &lt;/td&gt;&lt;td&gt;Full end-to-end DAG, retry logic, monitoring&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Object Storage &lt;/td&gt;&lt;td&gt;GCS &lt;/td&gt;&lt;td&gt;Staging layer between compute and warehouse&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Data Warehouse &lt;/td&gt;&lt;td&gt;BigQuery &lt;/td&gt;&lt;td&gt;Serverless, free Load Jobs, columnar, partitioned&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Transformations &lt;/td&gt;&lt;td&gt;dbt Cloud &lt;/td&gt;&lt;td&gt;Incremental models, lineage graph, scheduled runs&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Visualization &lt;/td&gt;&lt;td&gt;Grafana Cloud &lt;/td&gt;&lt;td&gt;Auto-refresh, BigQuery plugin free on all tiers&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&quot;try-it-yourself&quot;&gt;Try It Yourself&lt;/h3&gt;
&lt;p&gt;The full project is open source:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Derrick-Ryan-Giggs/coinpulse&quot;&gt;https://github.com/Derrick-Ryan-Giggs/coinpulse&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Live Dashboard:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://derrickryangiggs.grafana.net/public-dashboards/81560968e15140f08f65b52d78a4b252&quot;&gt;https://derrickryangiggs.grafana.net/public-dashboards/81560968e15140f08f65b52d78a4b252&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The README has step-by-step reproduction instructions — clone, fill in &lt;code&gt;.env&lt;/code&gt;, run &lt;code&gt;terraform apply&lt;/code&gt;, start the Docker stacks, and you have a running pipeline within about 20 minutes. All you need is a GCP account (free tier works), a CoinGecko Demo API key (free, no card), dbt Cloud account (free developer plan), and Grafana Cloud account (free forever).&lt;/p&gt;
&lt;h3 id=&quot;what-this-project-taught-me&quot;&gt;What This Project Taught Me&lt;/h3&gt;
&lt;p&gt;Building this reinforced something I suspected but didn't fully appreciate: &lt;strong&gt;the hardest problems in data engineering aren't the algorithms — they're the plumbing.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Getting PyFlink to connect to a Redpanda broker with the right container hostname was a day of debugging. Getting the Airflow DAG to read env vars correctly on scheduled runs (not just manual triggers) was another. Getting BigQuery to accept the same schema consistently across runs required understanding how PyArrow serialises Python types to Parquet column metadata.&lt;/p&gt;
&lt;p&gt;None of these problems are glamorous. But solving them is what separates a &quot;pipeline that worked once on my laptop&quot; from a &quot;pipeline that runs reliably every day.&quot;&lt;/p&gt;
&lt;p&gt;That's what CoinPulse is — a pipeline that runs reliably every day, for $0.01/month.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;If you found this useful or have questions about any part of the implementation, feel free to reach out. I write regularly about data engineering, cloud infrastructure, and GCP on &lt;a rel=&quot;nofollow&quot; href=&quot;https://medium.com/@derrickryangiggs&quot;&gt;Medium&lt;/a&gt;, &lt;a rel=&quot;nofollow&quot; href=&quot;https://dev.to/derrickryangiggs&quot;&gt;Dev.to&lt;/a&gt;, and &lt;a rel=&quot;nofollow&quot; href=&quot;https://ryan-giggs.hashnode.dev&quot;&gt;Hashnode&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/15962/i-built-a-real-time-crypto-analytics-pipeline-for-0-01-month-heres-the-full-architecture</guid>
<pubDate>Mon, 27 Apr 2026 09:35:03 +0000</pubDate>
</item>
<item>
<title>Your Serverless Data Lake is Lying to You: Add Observability or Lose Data (AWS)</title>
<link>https://coderlegion.com/15300/your-serverless-data-lake-is-lying-to-you-add-observability-or-lose-data-aws</link>
<description>&lt;h3 id=&quot;tl-dr-1&quot;&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Serverless Data Lakes are scalable and cost-efficient—but they can fail silently&lt;/li&gt;
&lt;li&gt;Without observability, you risk making decisions on incomplete data&lt;/li&gt;
&lt;li&gt;Adding a Dead Letter Queue (DLQ) ensures no data is lost&lt;/li&gt;
&lt;li&gt;Monitoring with Amazon CloudWatch + alerts via Amazon SNS gives you real visibility&lt;/li&gt;
&lt;li&gt;Trade-off: slightly more complexity for significantly higher reliability&lt;/li&gt;
&lt;li&gt;The Moment Everything “Worked”… But Was Wrong&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It was 2 AM.&lt;/p&gt;
&lt;p&gt;The pipeline had completed successfully.&lt;br&gt;
Amazon Athena was returning results.&lt;/p&gt;
&lt;p&gt;Dashboards were updating.&lt;/p&gt;
&lt;p&gt;And yet… the numbers didn’t match.&lt;/p&gt;
&lt;p&gt;Walking through logs in Amazon CloudWatch, I found the issue:&lt;br&gt;
messages sitting in a queue that no one was monitoring.&lt;/p&gt;
&lt;p&gt;No alarms.&lt;br&gt;
No failures reported.&lt;br&gt;
Just silently missing data.&lt;/p&gt;
&lt;p&gt;That’s the uncomfortable truth about serverless systems:&lt;/p&gt;
&lt;p&gt;They don’t crash. They drift.&lt;/p&gt;
&lt;p&gt;**&lt;/p&gt;
&lt;h2 id=&quot;the-typical-clean-architecture&quot;&gt;The Typical “Clean” Architecture&lt;/h2&gt;
&lt;p&gt;**&lt;/p&gt;
&lt;p&gt;Most Serverless Data Lakes follow a familiar pattern:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Amazon S3 as the storage layer&lt;/li&gt;
&lt;li&gt;AWS Glue for schema and transformation&lt;/li&gt;
&lt;li&gt;Amazon Athena for analytics&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It’s elegant:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;No servers&lt;/li&gt;
&lt;li&gt;Pay-per-use&lt;/li&gt;
&lt;li&gt;Infinite scale&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;But it assumes something dangerous:&lt;/p&gt;
&lt;p&gt;That success = correctness&lt;/p&gt;
&lt;p&gt;What Was Missing: Observability&lt;/p&gt;
&lt;p&gt;The issue wasn’t the architecture.&lt;/p&gt;
&lt;p&gt;It was the lack of visibility into:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;What failed&lt;/li&gt;
&lt;li&gt;What was retried&lt;/li&gt;
&lt;li&gt;What never got processed&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Without that, your data lake becomes a black box.&lt;/p&gt;
&lt;p&gt;And black boxes are risky in data engineering.&lt;/p&gt;
&lt;p&gt;**&lt;/p&gt;
&lt;h3 id=&quot;the-fix-designing-for-failure-on-purpose-&quot;&gt;The Fix: Designing for Failure (On Purpose)&lt;/h3&gt;
&lt;p&gt;**&lt;/p&gt;
&lt;p&gt;For an E-commerce analytics workshop, I reworked the architecture with a different mindset:&lt;/p&gt;
&lt;p&gt;Assume everything will fail—and make it visible when it does.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. Decoupling Ingestion&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Instead of triggering jobs directly from S3 events:&lt;/p&gt;
&lt;p&gt;Amazon S3 emits events&lt;br&gt;
Amazon SQS captures and buffers them&lt;/p&gt;
&lt;p&gt;This introduces control:&lt;/p&gt;
&lt;p&gt;Retry mechanisms&lt;br&gt;
Backpressure handling&lt;br&gt;
Event durability&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. Dead Letter Queue (DLQ): No Data Left Behind&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Failures are inevitable. Silent failures are optional.&lt;/p&gt;
&lt;p&gt;By adding a DLQ:&lt;/p&gt;
&lt;p&gt;Failed messages are isolated after retries&lt;br&gt;
Nothing is lost&lt;br&gt;
You can inspect and replay events&lt;/p&gt;
&lt;p&gt;This turns “unknown issues” into actionable signals.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. Lightweight Orchestration&lt;/strong&gt;&lt;br&gt;
AWS Lambda polls SQS&lt;br&gt;
Triggers AWS Glue jobs&lt;/p&gt;
&lt;p&gt;Simple, decoupled, and reactive.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4. Optimized Storage for Analytics&lt;/strong&gt;&lt;br&gt;
Raw data lands in S3 (CSV/JSON)&lt;br&gt;
Transformed into Parquet&lt;br&gt;
Partitioned by date&lt;/p&gt;
&lt;p&gt;This reduces cost and improves query performance in Amazon Athena.&lt;/p&gt;
&lt;p&gt;**&lt;/p&gt;
&lt;h2 id=&quot;observability-where-things-get-real&quot;&gt;Observability: Where Things Get Real&lt;/h2&gt;
&lt;p&gt;**&lt;/p&gt;
&lt;p&gt;This is the layer most diagrams ignore—and the one that matters most in production.&lt;/p&gt;
&lt;p&gt;Metrics That Tell the Truth&lt;/p&gt;
&lt;p&gt;Using Amazon CloudWatch:&lt;/p&gt;
&lt;p&gt;Queue depth (are messages piling up?)&lt;br&gt;
DLQ size (are failures accumulating?)&lt;br&gt;
Glue job status&lt;br&gt;
Lambda error rates&lt;br&gt;
Alerts That Don’t Wait&lt;/p&gt;
&lt;p&gt;With Amazon SNS:&lt;/p&gt;
&lt;p&gt;DLQ &amp;gt; 0 → immediate alert&lt;br&gt;
Glue job failure → alert&lt;br&gt;
Pipeline inactivity → alert&lt;/p&gt;
&lt;p&gt;No need to “check dashboards.”&lt;br&gt;
You get notified when something breaks.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A Simple Rule&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;If your system fails and you don’t know immediately:&lt;/p&gt;
&lt;p&gt;You don’t have observability.&lt;/p&gt;
&lt;p&gt;**&lt;/p&gt;
&lt;h2 id=&quot;trade-offs-because-nothing-is-free-&quot;&gt;Trade-offs (Because Nothing Is Free)&lt;/h2&gt;
&lt;p&gt;**&lt;/p&gt;
&lt;p&gt;Adding observability and DLQs improves reliability—but it’s not without cost.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What You Gain&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Data reliability&lt;/li&gt;
&lt;li&gt;Failure traceability&lt;/li&gt;
&lt;li&gt;Faster incident response&lt;/li&gt;
&lt;li&gt;Confidence in your analytics&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;What You Pay&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;More components (SQS, DLQ, Lambda)&lt;/li&gt;
&lt;li&gt;Slightly higher operational complexity&lt;/li&gt;
&lt;li&gt;Additional monitoring configuration&lt;/li&gt;
&lt;li&gt;Small increase in cost (messages, metrics, alerts)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;The Real Trade-off&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;You’re not choosing between “simple vs complex.”&lt;/p&gt;
&lt;p&gt;You’re choosing between:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Simple system that hides failures&lt;/li&gt;
&lt;li&gt;Slightly more complex system that exposes them&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In production, that’s an easy decision.&lt;/p&gt;
&lt;p&gt;**&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Final Thought&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;**&lt;/p&gt;
&lt;p&gt;Serverless is powerful. It removes infrastructure, reduces costs, and accelerates development.&lt;/p&gt;
&lt;p&gt;But it also removes friction—and friction is often what makes failures visible.&lt;/p&gt;
&lt;p&gt;If you don’t design for observability, your system will fail quietly.&lt;br&gt;
And quiet failures are the most expensive ones.&lt;/p&gt;
&lt;p&gt;How are you handling failures in your data pipelines today?&lt;/p&gt;
&lt;p&gt;Are you capturing them… or hoping they don’t happen?&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/15300/your-serverless-data-lake-is-lying-to-you-add-observability-or-lose-data-aws</guid>
<pubDate>Mon, 20 Apr 2026 12:55:14 +0000</pubDate>
</item>
<item>
<title>Oracle GoldenGate 23ai: Powering Distributed AI with Real-Time Data Replication</title>
<link>https://coderlegion.com/15117/oracle-goldengate-23ai-powering-distributed-ai-with-real-time-data-replication</link>
<description>&lt;p&gt;The database world has always needed a reliable bridge between operational systems and analytical or AI workloads. For decades, &lt;strong&gt;Oracle GoldenGate&lt;/strong&gt; has served that role — silently moving terabytes of transactional changes across databases, clouds, and continents with minimal latency and near-zero impact on source systems. With the release of &lt;strong&gt;GoldenGate 23ai&lt;/strong&gt; in 2024, that bridge now carries a new payload: &lt;strong&gt;AI vector embeddings&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;This post covers GoldenGate's core use cases, the Microservices Architecture that underpins modern deployments, and the powerful new distributed AI capabilities that make GoldenGate 23ai a critical component of enterprise GenAI pipelines.&lt;/p&gt;
&lt;h3 id=&quot;what-is-oracle-goldengate-&quot;&gt;What Is Oracle GoldenGate?&lt;/h3&gt;
&lt;p&gt;At its heart, GoldenGate is a &lt;strong&gt;Change Data Capture (CDC)&lt;/strong&gt; and real-time replication engine. Rather than querying the source database directly — which would impose load — GoldenGate reads the database's &lt;strong&gt;redo/transaction logs&lt;/strong&gt;, captures only the changed deltas (inserts, updates, deletes), and delivers them to one or more target systems with sub-second latency.&lt;/p&gt;
&lt;p&gt;This log-based approach makes GoldenGate extremely low-impact on production systems while supporting highly demanding replication topologies across heterogeneous environments: Oracle to PostgreSQL, Oracle to Kafka, MySQL to BigQuery, and many more.&lt;/p&gt;
&lt;h3 id=&quot;common-goldengate-use-cases&quot;&gt;Common GoldenGate Use Cases&lt;/h3&gt;
&lt;p&gt;GoldenGate's flexibility makes it applicable across a broad range of enterprise scenarios:&lt;/p&gt;
&lt;h4 id=&quot;1-multi-active-high-availability-and-cross-region-deployments&quot;&gt;1. Multi-Active, High Availability, and Cross-Region Deployments&lt;/h4&gt;
&lt;p&gt;GoldenGate enables &lt;strong&gt;active-active (bidirectional) replication&lt;/strong&gt;, where multiple database instances in different regions accept both reads and writes simultaneously. Built-in conflict detection and resolution ensures data consistency across sites. This architecture powers mission-critical applications that require zero planned downtime and regional failover capabilities — with GoldenGate's &lt;strong&gt;ExaPortMon&lt;/strong&gt;-style awareness ensuring continuous replication even under network disruptions.&lt;/p&gt;
&lt;h4 id=&quot;2-data-offloading-and-data-hub&quot;&gt;2. Data Offloading and Data Hub&lt;/h4&gt;
&lt;p&gt;Rather than running analytical queries against production OLTP databases, GoldenGate streams &lt;strong&gt;only the changed data&lt;/strong&gt; to a dedicated analytics database, data warehouse, or data lake in real time. This eliminates the traditional &quot;nightly CSV batch&quot; pattern and gives analysts access to data that is seconds, not hours, behind production.&lt;/p&gt;
&lt;h4 id=&quot;3-migrations-and-upgrades&quot;&gt;3. Migrations and Upgrades&lt;/h4&gt;
&lt;p&gt;GoldenGate is the gold standard for &lt;strong&gt;zero-downtime database migrations&lt;/strong&gt; — moving from on-premises to cloud, upgrading Oracle Database versions, or switching from Oracle to PostgreSQL. The approach: synchronize old and new systems in parallel using GoldenGate, then cut over the application connection once replication lag reaches zero. Even migrations of hundreds of terabytes are reduced to a switchover window measured in minutes.&lt;/p&gt;
&lt;h4 id=&quot;4-analytics-data-feeds&quot;&gt;4. Analytics Data Feeds&lt;/h4&gt;
&lt;p&gt;GoldenGate feeds real-time change streams into analytics platforms — data warehouses, Apache Kafka topics, Oracle Streaming Service, Confluent Cloud, and cloud object stores. This underpins event-driven architectures and real-time dashboards that would be impossible with batch-based ETL.&lt;/p&gt;
&lt;h4 id=&quot;5-stream-analytics&quot;&gt;5. Stream Analytics&lt;/h4&gt;
&lt;p&gt;With &lt;strong&gt;GoldenGate Stream Analytics&lt;/strong&gt;, change events flowing through GoldenGate can be enriched, filtered, and analyzed in motion — before they land in a target system. Integrations with &lt;strong&gt;Oracle Machine Learning (OML)&lt;/strong&gt; and &lt;strong&gt;ONNX (Open Neural Network Exchange)&lt;/strong&gt; enable actionable AI/ML directly from streaming pipelines, turning raw CDC events into predictive signals.&lt;/p&gt;
&lt;h3 id=&quot;goldengate-microservices-architecture&quot;&gt;GoldenGate Microservices Architecture&lt;/h3&gt;
&lt;p&gt;When Oracle introduced the &lt;strong&gt;Microservices Architecture (MA)&lt;/strong&gt; in &lt;strong&gt;GoldenGate 12.3 (August 2017)&lt;/strong&gt;, it represented a fundamental modernization of how GoldenGate is deployed, managed, and integrated. The classic command-line-driven architecture (GGSCI) was complemented by a fully API-driven, web-based platform.&lt;/p&gt;
&lt;h4 id=&quot;five-core-components&quot;&gt;Five Core Components&lt;/h4&gt;
&lt;p&gt;The Microservices Architecture is built around five independently manageable services:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Service Manager&lt;/strong&gt; — acts as a watchdog, managing and monitoring all other services within a deployment&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Administration Server&lt;/strong&gt; — the central control entity for managing Extract, Replicat, and all replication processes&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Distribution Server&lt;/strong&gt; — a high-performance data distribution agent that routes trail files to one or more targets over HTTPS/WebSockets&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Receiver Server&lt;/strong&gt; — handles all incoming trail files from remote Distribution Servers&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Performance Metrics Server&lt;/strong&gt; — collects and exposes real-time performance data for all GoldenGate processes via REST API or JSON&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;key-benefits-of-microservices-architecture&quot;&gt;Key Benefits of Microservices Architecture&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;REST API-driven management&lt;/strong&gt;: Every GoldenGate operation — starting a replicat, checking lag, configuring a pipeline — is exposed via a standardized REST API, enabling automation with Python, Terraform, and CI/CD pipelines.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Renewed web UI in 23ai&lt;/strong&gt;: GoldenGate 23ai ships with a &lt;strong&gt;completely redesigned graphical interface&lt;/strong&gt;, with more logical, guided assistant steps for setting up integrations — a marked improvement over the previous look and feel.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TLS encryption and OAuth 2.0 authentication&lt;/strong&gt; out of the box, with integration into external identity providers.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Simpler patching and upgrades&lt;/strong&gt;: Each microservice can be updated with minimal disruption. Upgrading from GoldenGate 21c to 23ai, for example, requires only downloading the new software and updating the &lt;code&gt;OGG_HOME&lt;/code&gt; directory parameters.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Deployment flexibility&lt;/strong&gt;: Runs on-premises, as a fully managed service in OCI (OCI GoldenGate), or on third-party clouds including AWS, Azure, and GCP.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;goldengate-version-upgrade-path&quot;&gt;GoldenGate Version Upgrade Path&lt;/h4&gt;
&lt;p&gt;For teams still running older GoldenGate versions, the supported upgrade sequence through the modern microservices era is:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;12.1 → 12.2 → 12.3 (Microservices introduced) → 18c → 19c → 21c → 23ai → 26ai (next LTS)
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: Classic Architecture support for Oracle Database is deprecated in 23ai. All new deployments should use Microservices Architecture. GoldenGate &lt;strong&gt;26ai&lt;/strong&gt; has been announced as the next Long-Term Support (LTS) release, with further AI and Data Mesh enhancements planned.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;distributed-ai-processing-with-vector-replication&quot;&gt;Distributed AI Processing with Vector Replication&lt;/h3&gt;
&lt;p&gt;This is where GoldenGate 23ai breaks genuinely new ground. The release adds first-class support for the &lt;strong&gt;Oracle Database 23ai &lt;code&gt;VECTOR&lt;/code&gt; data type&lt;/strong&gt; — the foundational building block of AI Vector Search and RAG (Retrieval-Augmented Generation) pipelines.&lt;/p&gt;
&lt;h4 id=&quot;what-is-a-vector-embedding-&quot;&gt;What Is a Vector Embedding?&lt;/h4&gt;
&lt;p&gt;When AI models process documents, images, or other unstructured data, they convert that content into &lt;strong&gt;vector embeddings&lt;/strong&gt; — high-dimensional numerical representations that encode semantic meaning. These vectors are stored in vector databases and searched using similarity algorithms (cosine distance, dot product, Euclidean distance) to retrieve contextually relevant content.&lt;/p&gt;
&lt;p&gt;Keeping vector stores synchronized with the operational databases that hold the source data is a significant engineering challenge — and exactly what GoldenGate 23ai solves.&lt;/p&gt;
&lt;h4 id=&quot;key-vector-replication-capabilities&quot;&gt;Key Vector Replication Capabilities&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Migrate Vectors into Oracle Vector Database Without Downtime&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;GoldenGate can migrate vector embeddings stored in one database into Oracle Database 23ai (or Oracle AI Database 26ai) without any service interruption. This enables seamless transitions from on-premises environments to OCI, or between cloud providers, while keeping AI-powered applications running continuously.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Replicate and Consolidate Vector Changes in Real Time&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;As source data changes — a customer record is updated, a product description is revised — GoldenGate captures those changes and propagates updated vectors to all target vector stores in real time. This ensures that RAG pipelines are always querying fresh, consistent embeddings rather than stale snapshots.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Heterogeneous Vector Replication&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;GoldenGate 23ai can replicate vectors both homogeneously (Oracle to Oracle) and heterogeneously across different vector stores, provided the &lt;strong&gt;embedding algorithm, type, and dimension are consistent&lt;/strong&gt; across source and target. If the source and target use different embedding algorithms, GoldenGate can replicate the &lt;strong&gt;raw source data&lt;/strong&gt; instead, allowing the target system to re-vectorize it with its own model.&lt;/p&gt;
&lt;p&gt;Support extends beyond Oracle: GoldenGate 23ai also supports capture and delivery of the &lt;strong&gt;pgvector extension&lt;/strong&gt; for PostgreSQL and its derivatives, as well as the &lt;strong&gt;VECTOR datatype for MySQL 9.0+&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Multi-Cloud, Multi-Active Oracle Vector Database&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;GoldenGate enables &lt;strong&gt;active-active vector database architectures&lt;/strong&gt; spanning multiple cloud regions or providers. AI applications in different regions can write to and read from local vector stores while GoldenGate keeps all instances synchronized — enabling both low-latency AI inference and high availability for vector workloads.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Stream Changes to Search Engines&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;GoldenGate can stream vector and document changes in real time to &lt;strong&gt;Elasticsearch or OpenSearch compatible search indexes&lt;/strong&gt;, enabling hybrid semantic + keyword search architectures without manual synchronization pipelines.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;GoldenGate Data Streams: Pub/Sub for Database Events&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A major new capability in GoldenGate 23ai is the &lt;strong&gt;Data Streams service&lt;/strong&gt;, which provides simplified, event-driven access to database change records via &lt;strong&gt;AsyncAPI over WebSocket connections&lt;/strong&gt;. This enables developers to subscribe to database change events — including vector updates — in a pub/sub pattern, without needing to manage trail files or configure traditional Replicat processes. Combined with &lt;strong&gt;Oracle Database 23ai's JSON Relational Duality views&lt;/strong&gt;, this means document-side changes can be captured and propagated as structured events in real time.&lt;/p&gt;
&lt;h3 id=&quot;generative-ai-with-your-own-business-data&quot;&gt;Generative AI with Your Own Business Data&lt;/h3&gt;
&lt;p&gt;GoldenGate 23ai directly enables three patterns for building AI applications on private enterprise data:&lt;/p&gt;
&lt;h4 id=&quot;1-from-scratch-llms-custom-foundation-models-&quot;&gt;1. From-Scratch LLMs (Custom Foundation Models)&lt;/h4&gt;
&lt;p&gt;Organizations with truly unique proprietary datasets — medical records, legal case files, financial transaction histories — may opt to train custom Large Language Models from the ground up on that data. GoldenGate ensures the training datasets fed into these models are continuously fresh and complete, sourced in real time from operational databases rather than periodic exports.&lt;/p&gt;
&lt;p&gt;This is an advanced, resource-intensive approach suited to well-resourced organizations with data assets that are sufficiently distinctive to justify the investment.&lt;/p&gt;
&lt;h4 id=&quot;2-fine-tuned-llms-domain-adaptation-&quot;&gt;2. Fine-Tuned LLMs (Domain Adaptation)&lt;/h4&gt;
&lt;p&gt;A more practical approach for most enterprises: take a pre-trained foundational LLM (such as Llama, Mistral, or an OCI Generative AI model) and &lt;strong&gt;fine-tune it on a private domain dataset&lt;/strong&gt;. GoldenGate replicates the relevant operational data into a training pipeline, keeping the fine-tuned model aligned with current business reality as data evolves over time.&lt;/p&gt;
&lt;h4 id=&quot;3-rag-retrieval-augmented-generation-the-most-common-pattern&quot;&gt;3. RAG (Retrieval-Augmented Generation) — The Most Common Pattern&lt;/h4&gt;
&lt;p&gt;RAG is currently the dominant architecture for enterprise GenAI. Instead of baking private knowledge into model weights (which requires expensive retraining), RAG retrieves relevant documents at query time and injects them into the LLM's prompt as context.&lt;/p&gt;
&lt;p&gt;GoldenGate 23ai is purpose-built to power better RAG pipelines:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Keeps vector stores current&lt;/strong&gt;: As source data changes, GoldenGate propagates those changes to the vector index within seconds — not hours.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Enables a consolidated vector hub&lt;/strong&gt;: Rather than maintaining separate, potentially inconsistent vector stores for each application, GoldenGate can consolidate vectors from multiple source databases into a single Oracle AI Database vector hub.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reduces hallucination risk&lt;/strong&gt;: Stale vector indexes are one of the primary causes of LLM hallucinations in RAG systems. Fresh data = more accurate, grounded responses.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Supports heterogeneous sources&lt;/strong&gt;: GoldenGate can pull data from Oracle, PostgreSQL, MySQL, SQL Server, DB2, and more — vectorize it, and deliver it to Oracle AI Database, all in a unified pipeline.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;why-goldengate-23ai-for-ai-the-summary&quot;&gt;Why GoldenGate 23ai for AI: The Summary&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Capability &lt;/th&gt;&lt;th&gt;Impact&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Real-time vector replication &lt;/td&gt;&lt;td&gt;RAG pipelines query fresh embeddings, not stale snapshots&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Zero-downtime vector migration &lt;/td&gt;&lt;td&gt;Move AI workloads to cloud without service interruption&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Multi-cloud active-active vectors &lt;/td&gt;&lt;td&gt;Low-latency AI inference globally with high availability&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Heterogeneous source support &lt;/td&gt;&lt;td&gt;Unify vectors from Oracle, PostgreSQL, MySQL, and more&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Data Streams (AsyncAPI) &lt;/td&gt;&lt;td&gt;Event-driven AI pipelines without complex trail file management&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Stream to search engines &lt;/td&gt;&lt;td&gt;Elasticsearch/OpenSearch integration for hybrid search&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Fine-tuning data pipelines &lt;/td&gt;&lt;td&gt;Keep domain-adapted LLMs current with live business data&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The underlying thesis is the same one Oracle argues for Exadata AI Storage: &lt;strong&gt;bring AI to the data, not data to the AI&lt;/strong&gt;. GoldenGate 23ai extends this philosophy to the replication layer — ensuring that wherever your AI application runs, it has access to consistent, real-time data from your authoritative operational systems.&lt;/p&gt;
&lt;p&gt;As enterprise AI moves from experimentation to production, the quality of data pipelines will increasingly determine the quality of AI outcomes. GoldenGate 23ai is Oracle's answer to that challenge.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Are you using GoldenGate for AI pipelines or RAG architectures? What challenges have you run into keeping vector stores synchronized? Share in the comments below.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/15117/oracle-goldengate-23ai-powering-distributed-ai-with-real-time-data-replication</guid>
<pubDate>Sat, 18 Apr 2026 11:13:01 +0000</pubDate>
</item>
<item>
<title>Building the Sovereign Debt Observatory: An End-to-End ELT Pipeline on World Bank Debt Data for Low</title>
<link>https://coderlegion.com/14319/building-the-sovereign-debt-observatory-end-end-elt-pipeline-world-bank-debt-data-for-low</link>
<description>&lt;h3 id=&quot;introduction-5&quot;&gt;Introduction&lt;/h3&gt;
&lt;p&gt;Global sovereign debt is one of the most consequential datasets in existence. It shapes foreign policy, determines credit ratings, drives IMF bailout decisions, and affects the daily lives of billions of people in developing countries. The World Bank publishes this data openly — 130+ countries, 27 years of history, updated quarterly — yet there is no ready-made analytical layer on top of it.&lt;/p&gt;
&lt;p&gt;If you want to answer a question like &quot;which African countries have the highest ratio of private nonguaranteed debt to total external debt, and how has that changed since 2010?&quot;, you have to manually download Excel files from multiple World Bank portals, clean inconsistent column names, handle missing values, and stitch everything together in a spreadsheet. Every time the data updates, you do it again.&lt;/p&gt;
&lt;p&gt;That is the problem this project solves.&lt;/p&gt;
&lt;p&gt;The Sovereign Debt Observatory is an end-to-end ELT pipeline that ingests World Bank external debt data, lands it in a cloud data lake, transforms it in BigQuery using dbt Cloud, orchestrates everything quarterly with Apache Airflow, and surfaces the answers in a Looker Studio dashboard.&lt;/p&gt;
&lt;p&gt;This article walks through every layer of the pipeline — the architecture decisions, the technical challenges, and how I solved them.&lt;/p&gt;
&lt;h3 id=&quot;the-five-questions-this-pipeline-answers&quot;&gt;The Five Questions This Pipeline Answers&lt;/h3&gt;
&lt;p&gt;Before writing a single line of code, I defined the analytical questions the pipeline needed to answer. This kept every decision grounded in purpose rather than technology for its own sake.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;How is gross external debt distributed across public, publicly guaranteed, private nonguaranteed, and multilateral sectors per country?&lt;/li&gt;
&lt;li&gt;Which countries carry the highest short-term external debt exposure and how has that changed since 2010?&lt;/li&gt;
&lt;li&gt;What share of external debt is foreign-currency denominated and where is that ratio worsening?&lt;/li&gt;
&lt;li&gt;How has regional external debt stock evolved from 1998 to 2025 across Africa, Latin America, East Asia, South Asia, Europe and Central Asia, and the Middle East?&lt;/li&gt;
&lt;li&gt;Which countries face the heaviest debt service pressure relative to their total debt position?&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;architecture-overview-2&quot;&gt;Architecture Overview&lt;/h3&gt;
&lt;p&gt;The pipeline follows a modern ELT pattern — extract and load first, transform inside the warehouse.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;World Bank API (IDS source 2)
        |
        v
PySpark job (Docker container)
        |
        v
Google Cloud Storage — raw Parquet, partitioned by extracted_date
        |
        v
BigQuery external tables (raw dataset)
        |
        v
dbt Cloud — staging views + mart tables
        |
        v
Looker Studio dashboard
        |
Orchestrated by Apache Airflow on Docker Compose
Infrastructure provisioned by Terraform
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Why ELT and not ETL?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;In ETL, transformation happens before loading — your Spark job does the cleaning, aggregation, and business logic before writing to the warehouse. In ELT, raw data lands untransformed and the warehouse does all the heavy lifting.&lt;/p&gt;
&lt;p&gt;For this project, ELT is the right choice for three reasons. First, raw data is preserved in GCS indefinitely — if analytical requirements change six months from now, I just write a new dbt model without re-running the ingestion layer. Second, BigQuery is optimized for analytical SQL transformations at scale — it is far better at this than PySpark running in a Docker container on a local machine. Third, dbt gives us version-controlled, tested, documented transformations that are readable by anyone with SQL knowledge.&lt;/p&gt;
&lt;p&gt;PySpark's job in this pipeline is purely mechanical: hit the API, paginate, write Parquet. No business logic. No aggregations. Pure extract and load.&lt;/p&gt;
&lt;h3 id=&quot;data-sources&quot;&gt;Data Sources&lt;/h3&gt;
&lt;h4 id=&quot;international-debt-statistics-ids-world-bank-source-2&quot;&gt;International Debt Statistics (IDS) — World Bank source 2&lt;/h4&gt;
&lt;p&gt;The IDS database is the flagship World Bank debt dataset. It covers external debt stocks and flows for low and middle income countries, with annual data going back to 1998. I access it through the &lt;code&gt;wbgapi&lt;/code&gt; Python library, which wraps the World Bank Indicators API v2.&lt;/p&gt;
&lt;p&gt;I ingest nine series:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Series Code &lt;/th&gt;&lt;th&gt;Description&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;DT.DOD.DECT.CD &lt;/td&gt;&lt;td&gt;Total external debt stocks&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;DT.DOD.DLXF.CD &lt;/td&gt;&lt;td&gt;Long-term external debt&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;DT.DOD.DPNG.CD &lt;/td&gt;&lt;td&gt;Private nonguaranteed debt&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;DT.DOD.MIBR.CD &lt;/td&gt;&lt;td&gt;PPG IBRD loans&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;DT.DOD.DPPG.CD &lt;/td&gt;&lt;td&gt;Public and publicly guaranteed debt&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;DT.DOD.DIMF.CD &lt;/td&gt;&lt;td&gt;IMF credit&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;DT.DOD.PVLX.CD &lt;/td&gt;&lt;td&gt;Present value of external debt&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;DT.DOD.MWBG.CD &lt;/td&gt;&lt;td&gt;IBRD loans and IDA credits&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;DT.DOD.MIDA.CD &lt;/td&gt;&lt;td&gt;PPG IDA loans&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;One important lesson from building this: the World Bank has multiple API source databases, and not all of them support the standard &lt;code&gt;wbgapi&lt;/code&gt; query format. Sources 22 (QEDS SDDS), 23 (QEDS GDDS), and 54 (JEDH) all return JSON decode errors when queried programmatically — they use a separate DataBank backend. Only source 2 (IDS) reliably supports the Indicators API. This cost me several hours of debugging.&lt;/p&gt;
&lt;h4 id=&quot;quarterly-external-debt-statistics-sdds-qeds-&quot;&gt;Quarterly External Debt Statistics SDDS (QEDS)&lt;/h4&gt;
&lt;p&gt;QEDS provides quarterly debt payment schedule data broken down by sector and maturity. Unlike IDS, QEDS does not support programmatic API access in the standard format. The World Bank provides bulk Excel downloads for each supplementary table instead.&lt;/p&gt;
&lt;p&gt;I download five Excel files directly:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Table 1.5 — Net external debt position by sector&lt;/li&gt;
&lt;li&gt;Table 3 — Debt service payment schedule by sector&lt;/li&gt;
&lt;li&gt;Table 3.2 — Debt service by sector and instrument&lt;/li&gt;
&lt;li&gt;Table 2.1 — Foreign currency and domestic currency debt&lt;/li&gt;
&lt;li&gt;Table 1.6 — Reconciliation of positions and flows&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;infrastructure-as-code-with-terraform&quot;&gt;Infrastructure as Code with Terraform&lt;/h3&gt;
&lt;p&gt;Every GCP resource in this project is provisioned by Terraform. The three core resources are a GCS bucket for the data lake and three BigQuery datasets — raw, staging, and mart.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-hcl&quot;&gt;resource &quot;google_storage_bucket&quot; &quot;data_lake&quot; {
  name          = var.gcs_bucket_name
  location      = var.region
  force_destroy = true

  lifecycle_rule {
    action { type = &quot;Delete&quot; }
    condition { age = 90 }
  }

  versioning { enabled = true }
}

resource &quot;google_bigquery_dataset&quot; &quot;raw&quot; {
  dataset_id                 = &quot;raw&quot;
  location                   = var.location
  delete_contents_on_destroy = true
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The 90-day lifecycle rule on GCS automatically deletes old partitions, keeping storage costs near zero. The entire GCP footprint for this project costs less than $0.05 per month — BigQuery's free tier covers 1 TB of queries and 10 GB of storage, which is far more than this dataset requires.&lt;/p&gt;
&lt;p&gt;A &lt;code&gt;setup_gcp.sh&lt;/code&gt; script handles the one-time bootstrap — creating the GCP project, enabling APIs, creating the service account, and granting IAM roles. The billing account ID is passed as an environment variable so it never appears in version-controlled files.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;BILLING_ACCOUNT=your-billing-id bash scripts/setup_gcp.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;ingestion-layer-pyspark-on-docker&quot;&gt;Ingestion Layer — PySpark on Docker&lt;/h3&gt;
&lt;h4 id=&quot;jedh-ids-ingestion&quot;&gt;JEDH / IDS Ingestion&lt;/h4&gt;
&lt;p&gt;The IDS ingestion script uses &lt;code&gt;wbgapi&lt;/code&gt; to fetch all nine series across all available countries from 1998 to 2025. The API returns data in wide format — one row per country per series, with year columns as separate fields. PySpark writes this to GCS as Snappy-compressed Parquet, partitioned by extraction date.&lt;/p&gt;
&lt;p&gt;One critical issue I hit: the World Bank API returns year columns as bare integers (&lt;code&gt;1998&lt;/code&gt;, &lt;code&gt;1999&lt;/code&gt;, etc.). BigQuery rejects column names that start with numbers. The fix was to prefix all year columns with &lt;code&gt;year_&lt;/code&gt; before writing:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;combined.columns = [
    f&quot;year_{col}&quot; if str(col).isdigit() else col
    for col in combined.columns
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This produces clean column names like &lt;code&gt;year_1998&lt;/code&gt;, &lt;code&gt;year_1999&lt;/code&gt; that BigQuery accepts without complaint.&lt;/p&gt;
&lt;h4 id=&quot;qeds-ingestion&quot;&gt;QEDS Ingestion&lt;/h4&gt;
&lt;p&gt;The QEDS ingestion downloads five Excel files from World Bank DataBank, reads all sheets from each file using &lt;code&gt;pandas.read_excel&lt;/code&gt;, and concatenates them into a single DataFrame.&lt;/p&gt;
&lt;p&gt;The Excel files have messy column names — spaces, brackets, special characters. A &lt;code&gt;clean_column_name&lt;/code&gt; function normalizes everything:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def clean_column_name(col):
    col = str(col)
    col = re.sub(r'\s*\[.*?\]', '', col)  # remove [YR2021Q4] suffixes
    col = col.strip()
    col = re.sub(r'[^a-zA-Z0-9_]', '_', col)
    col = re.sub(r'_+', '_', col)
    col = col.strip('_')
    if col and col[0].isdigit():
        col = 'q_' + col  # prefix quarter columns: q_2021q4
    return col.lower()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I also hit an OOM error trying to write the QEDS data through PySpark — 76,835 rows with 90+ columns across 214 sheets was too much for the JVM heap in the Docker container. The fix was to bypass Spark entirely for QEDS and write directly to GCS using the &lt;code&gt;google-cloud-storage&lt;/code&gt; Python client:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;buffer = io.BytesIO()
df.to_parquet(buffer, index=False, engine=&quot;pyarrow&quot;)
buffer.seek(0)

client = storage.Client()
bucket = client.bucket(GCS_BUCKET)
blob = bucket.blob(output_path)
blob.upload_from_file(buffer, content_type=&quot;application/octet-stream&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No JVM, no Spark executor, no OOM. The lesson: use the right tool for the job. PySpark is excellent for large distributed datasets. For a 76K-row DataFrame from an Excel file, plain pandas and the GCS Python client is simpler and more reliable.&lt;/p&gt;
&lt;h4 id=&quot;docker-image&quot;&gt;Docker Image&lt;/h4&gt;
&lt;p&gt;The ingestion image is built on &lt;code&gt;eclipse-temurin:17-jdk-jammy&lt;/code&gt; rather than a plain Python image. This is because PySpark requires Java, and the &lt;code&gt;python:3.11-slim&lt;/code&gt; base image uses Debian Trixie which does not carry &lt;code&gt;openjdk-17-jdk&lt;/code&gt; in its default repositories. The Temurin image ships Java 17 out of the box, which is exactly what Spark 3.5.1 needs.&lt;/p&gt;
&lt;p&gt;The GCS connector JAR is downloaded at build time:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-dockerfile&quot;&gt;RUN curl --progress-bar -L \
    https://storage.googleapis.com/hadoop-lib/gcs/gcs-connector-hadoop3-latest.jar \
    -o ${SPARK_HOME}/jars/gcs-connector-hadoop3-latest.jar
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Credentials are never baked into the image. They are mounted at runtime as a volume:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;docker run --rm \
  -v /path/to/key.json:/app/credentials/key.json \
  -e GOOGLE_APPLICATION_CREDENTIALS=/app/credentials/key.json \
  sovereign-debt-ingestion:v1 python3 extract_jedh.py
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;orchestration-with-apache-airflow&quot;&gt;Orchestration with Apache Airflow&lt;/h3&gt;
&lt;p&gt;Airflow runs on Docker Compose using the official &lt;code&gt;apache/airflow:2.9.2&lt;/code&gt; image. The stack includes a Celery executor with Redis as the message broker and Postgres as the metadata database.&lt;/p&gt;
&lt;p&gt;The DAG runs quarterly — on the 1st of January, April, July, and October at 06:00 UTC:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;schedule_interval=&quot;0 6 1 1,4,7,10 *&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Task flow:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;extract_load_jedh &amp;gt;&amp;gt; extract_load_qeds
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Both tasks use the &lt;code&gt;DockerOperator&lt;/code&gt; to spin up the ingestion container on the host machine, which means Airflow itself does not need PySpark, Java, or any data dependencies — it just tells Docker to run the job.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Docker socket problem&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;DockerOperator&lt;/code&gt; communicates with the host Docker daemon through the Unix socket at &lt;code&gt;/var/run/docker.sock&lt;/code&gt;. This requires two things: the socket must be mounted into the Airflow worker container, and the worker must have permission to use it.&lt;/p&gt;
&lt;p&gt;The socket mount goes in &lt;code&gt;docker-compose.yml&lt;/code&gt; under the common volumes section:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;volumes:
  - /var/run/docker.sock:/var/run/docker.sock
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The permission fix requires adding the Docker group ID to the Airflow worker. On my machine the Docker group ID is 984:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;group_add:
  - &quot;984&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Without the group add, the worker can see the socket but gets &lt;code&gt;PermissionError(13, 'Permission denied')&lt;/code&gt;. This is a common Airflow + Docker-in-Docker gotcha that took several debugging sessions to resolve.&lt;/p&gt;
&lt;h3 id=&quot;transformation-with-dbt-cloud&quot;&gt;Transformation with dbt Cloud&lt;/h3&gt;
&lt;h4 id=&quot;staging-layer&quot;&gt;Staging Layer&lt;/h4&gt;
&lt;p&gt;The staging models are materialized as views — no storage cost, no latency, just a SQL lens over the raw external tables.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;stg_jedh&lt;/code&gt; does the heavy lifting: it unpivots the wide year-column format into long format using BigQuery's &lt;code&gt;UNPIVOT&lt;/code&gt; operator:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;unpivot(
    value for year in (
        year_1998, year_1999, year_2000, ..., year_2025
    )
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then extracts the year integer from the column name:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;cast(replace(year, 'year_', '') as int64) as year
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And adds a human-readable series description via a CASE statement, turning &lt;code&gt;DT.DOD.DECT.CD&lt;/code&gt; into &lt;code&gt;Total external debt stocks&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The result is a clean long-format table: one row per country per series per year.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;stg_qeds&lt;/code&gt; is simpler — it selects the clean columns, uses &lt;code&gt;SAFE_CAST&lt;/code&gt; to convert quarter values to float64, and filters out null countries and series codes. &lt;code&gt;SAFE_CAST&lt;/code&gt; is preferable to &lt;code&gt;CAST&lt;/code&gt; here because it returns NULL on failure rather than throwing an error, which is the right behaviour for messy Excel data.&lt;/p&gt;
&lt;h4 id=&quot;mart-layer&quot;&gt;Mart Layer&lt;/h4&gt;
&lt;p&gt;The mart models are materialized as tables with partitioning and clustering for query efficiency.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;mart_debt_stocks&lt;/code&gt; is partitioned by year and clustered by &lt;code&gt;country_code&lt;/code&gt; and &lt;code&gt;series_code&lt;/code&gt;. It enriches the staging data with YoY percentage change and debt as a percentage of total external debt per country per year — both computed using window functions:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;lag(b.debt_value_usd) over (
    partition by b.country_code, b.series_code
    order by b.year
) as prev_year_value,

safe_divide(
    b.debt_value_usd - lag(b.debt_value_usd) over (
        partition by b.country_code, b.series_code
        order by b.year
    ),
    lag(b.debt_value_usd) over (
        partition by b.country_code, b.series_code
        order by b.year
    )
) * 100 as yoy_change_pct
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;mart_regional_debt&lt;/code&gt; assigns countries to six World Bank regions using a CASE statement on ISO3 country codes, then aggregates total, average, max, and min debt stocks per region per series per year. This powers the regional trajectory time series on the dashboard.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;mart_debt_service&lt;/code&gt; computes total annual debt payments and average quarterly payments per country from the QEDS payment schedule data, enabling debt service pressure analysis.&lt;/p&gt;
&lt;h4 id=&quot;dbt-tests&quot;&gt;dbt Tests&lt;/h4&gt;
&lt;p&gt;All models have &lt;code&gt;not_null&lt;/code&gt; tests on primary dimension columns. Running &lt;code&gt;dbt test&lt;/code&gt; after every model change ensures data quality is enforced at the transformation layer rather than discovered downstream in the dashboard.&lt;/p&gt;
&lt;h3 id=&quot;the-dashboard&quot;&gt;The Dashboard&lt;/h3&gt;
&lt;p&gt;The Looker Studio dashboard has two pages, both connected directly to the BigQuery mart tables.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Page 1 — Global Debt Overview&lt;/strong&gt; answers Q4 at a glance. A time series chart using &lt;code&gt;mart_regional_debt&lt;/code&gt; shows six regional debt trajectories from 1998 to 2024. Africa's trajectory is notably steeper post-2010. A bar chart shows the top 20 countries by total external debt for the selected year. A scorecard shows total global external debt.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Page 2 — Country Deep-Dive&lt;/strong&gt; answers Q1, Q2, and Q5. A stacked bar chart shows debt composition by sector over time for any selected country. A line chart shows short-term vulnerability trends. A table sorted by total 2021 payments shows which countries face the most acute debt service pressure.&lt;/p&gt;
&lt;p&gt;Live dashboard: &lt;a rel=&quot;nofollow&quot; href=&quot;https://lookerstudio.google.com/reporting/7fc18e9e-a5c6-4616-b920-b5b4bddf2264&quot;&gt;https://lookerstudio.google.com/reporting/7fc18e9e-a5c6-4616-b920-b5b4bddf2264&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;/home/derrrick-ryan-giggs/Pictures/Screenshots/Screenshot From 2026-04-09 12-08-05.png&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=1550071413390411053&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;/home/derrrick-ryan-giggs/Pictures/Screenshots/Screenshot From 2026-04-09 12-08-28.png&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=2618517427269534522&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;/home/derrrick-ryan-giggs/Pictures/Screenshots/Screenshot From 2026-04-09 12-08-51.png&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=5694517971391333576&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;p&gt;/home/derrrick-ryan-giggs/Pictures/Screenshots/Screenshot From 2026-04-09 12-09-33.png&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=3224913193265563604&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;key-technical-lessons&quot;&gt;Key Technical Lessons&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;1. Know which World Bank API sources support programmatic access.&lt;/strong&gt; Only source 2 (IDS) reliably supports the Indicators API v2 via &lt;code&gt;wbgapi&lt;/code&gt;. Sources 22, 23, and 54 use a different DataBank backend and return JSON decode errors. For QEDS data, use the bulk Excel downloads instead.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. BigQuery rejects column names starting with numbers.&lt;/strong&gt; Prefix them before writing Parquet. A simple list comprehension handles this: &lt;code&gt;f&quot;year_{col}&quot; if str(col).isdigit() else col&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. Use the right tool for the data size.&lt;/strong&gt; PySpark is excellent for large datasets but overkill for a 76K-row Excel file. The GCS Python client with pandas and PyArrow is simpler, faster, and doesn't OOM.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4. Docker-in-Docker with Airflow requires explicit socket mounting and group permissions.&lt;/strong&gt; Mount &lt;code&gt;/var/run/docker.sock&lt;/code&gt; and add the Docker group ID to the worker container. Without the group add, you get a silent permission error.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;5. ELT separates concerns cleanly.&lt;/strong&gt; When the analytical questions evolved during development, I only needed to update dbt models — never the ingestion layer. This separation is the most valuable architectural decision in the project.&lt;/p&gt;
&lt;h3 id=&quot;what-i-would-do-differently&quot;&gt;What I Would Do Differently&lt;/h3&gt;
&lt;p&gt;If I were building this again, I would use the World Bank DataBank bulk download API to get historical QEDS time series data instead of point-in-time Excel files. The current QEDS data only has a handful of quarters because the Excel files are snapshots of the latest publication. A proper time series would require downloading and archiving each quarterly release.&lt;/p&gt;
&lt;p&gt;I would also add a &lt;code&gt;load_to_bigquery&lt;/code&gt; step that loads Parquet directly into native BigQuery tables rather than using external tables. External tables work well but they require manually updating the source URI list each time a new partition is added. A native table with partitioning handles this automatically.&lt;/p&gt;
&lt;h3 id=&quot;conclusion-6&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;The Sovereign Debt Observatory took the World Bank's raw debt data from scattered Excel files and API endpoints to a fully automated, tested, and documented analytical pipeline. Every component is reproducible — Terraform provisions the infrastructure, Docker packages the ingestion environment, Airflow schedules the runs, and dbt documents and tests the transformations.&lt;/p&gt;
&lt;p&gt;The full source code is on GitHub: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Derrick-Ryan-Giggs/sovereign-debt-observatory&quot;&gt;https://github.com/Derrick-Ryan-Giggs/sovereign-debt-observatory&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;If you have questions about any part of the implementation, drop them in the comments. I am happy to go deeper on any layer.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Ryan Derrick Giggs is a data engineering practitioner and technical writer based in Nairobi, Kenya. He is currently completing the DataTalksClub Data Engineering Zoomcamp 2026.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;LinkedIn: &lt;a rel=&quot;nofollow&quot; href=&quot;https://linkedin.com/in/ryan-giggs-a19330265&quot;&gt;https://linkedin.com/in/ryan-giggs-a19330265&lt;/a&gt;&lt;/em&gt;&lt;br&gt;
&lt;em&gt;GitHub: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Derrick-Ryan-Giggs&quot;&gt;https://github.com/Derrick-Ryan-Giggs&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/14319/building-the-sovereign-debt-observatory-end-end-elt-pipeline-world-bank-debt-data-for-low</guid>
<pubDate>Thu, 09 Apr 2026 10:02:56 +0000</pubDate>
</item>
<item>
<title>Pandas 3.0's PyArrow String Revolution: A Deep Dive into Memory and Performance</title>
<link>https://coderlegion.com/14212/pandas-3-0s-pyarrow-string-revolution-a-deep-dive-into-memory-and-performance</link>
<description>&lt;h3 id=&quot;introduction-6&quot;&gt;Introduction&lt;/h3&gt;
&lt;p&gt;Pandas 3.0 made a game-changing decision: &lt;strong&gt;PyArrow-backed strings are now the default&lt;/strong&gt;. Instead of storing strings as Python objects (the old &lt;code&gt;object&lt;/code&gt; dtype), pandas now uses Apache Arrow's columnar format with the new &lt;code&gt;string[pyarrow]&lt;/code&gt; dtype.&lt;/p&gt;
&lt;p&gt;But here's the question that matters: &lt;strong&gt;How much does this new string dtype actually improve performance and memory usage in real-world scenarios?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;To find out, I ran comprehensive benchmarks across diverse datasets and common string operations. The results? &lt;strong&gt;51.8% memory savings&lt;/strong&gt; on average, with operations running &lt;strong&gt;2-27x faster&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;This isn't a theoretical improvement, it's a fundamental shift in how pandas handles string data.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-results-summary-dashboard&quot;&gt;The Results: Summary Dashboard&lt;/h3&gt;
&lt;p&gt;Let me start with the headline numbers, then we'll dive into how I got them.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/robot010/Dev-Post-Code/blob/main/pandas-memory-saving/visualizations/summary_dashboard.png?raw=true&quot; alt=&quot;Result Summary&quot;&gt;&lt;/p&gt;
&lt;h4 id=&quot;the-four-key-metrics&quot;&gt;The Four Key Metrics&lt;/h4&gt;
&lt;h4 id=&quot;1-51-8-memory-savings&quot;&gt;1. 51.8% Memory Savings&lt;/h4&gt;
&lt;p&gt;Across all test datasets, the new PyArrow string dtype used half the memory of the old object dtype. This isn't a marginal improvement, it's transformative for memory-constrained environments.&lt;/p&gt;
&lt;h4 id=&quot;2-6-17x-average-operation-speedup&quot;&gt;2. 6.17x Average Operation Speedup&lt;/h4&gt;
&lt;p&gt;String operations aren't just more memory-efficient: they're dramatically faster. On average, operations like &lt;code&gt;str.lower()&lt;/code&gt;, &lt;code&gt;str.contains()&lt;/code&gt;, and &lt;code&gt;str.len()&lt;/code&gt; run 6x faster with PyArrow strings.&lt;/p&gt;
&lt;p&gt;Some operations are even more impressive:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;str.len()&lt;/code&gt;: 27x faster&lt;/li&gt;
&lt;li&gt;&lt;code&gt;str.startswith()&lt;/code&gt;: 16x faster&lt;/li&gt;
&lt;li&gt;&lt;code&gt;str.endswith()&lt;/code&gt;: 15x faster&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;3-889-mb-total-memory-saved&quot;&gt;3. 889 MB Total Memory Saved&lt;/h4&gt;
&lt;p&gt;Across our test datasets (totaling 645 MB on disk), we saved nearly &lt;strong&gt;1 GB of RAM&lt;/strong&gt; in memory. For a real data pipeline processing dozens of datasets, this compounds quickly.&lt;/p&gt;
&lt;h4 id=&quot;4-memory-overhead-the-game-changer&quot;&gt;4. Memory Overhead: The Game Changer&lt;/h4&gt;
&lt;p&gt;The bottom chart reveals something crucial about how pandas handles strings:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Old string dtype (object):&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;CSV files on disk: 645 MB&lt;/li&gt;
&lt;li&gt;Loaded into pandas: 1,714 MB&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Memory overhead: 165.7%&lt;/strong&gt; (more than doubles!)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;New string dtype (PyArrow):&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;CSV files on disk: 645 MB&lt;/li&gt;
&lt;li&gt;Loaded into pandas: 825 MB&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Memory overhead: 27.9%&lt;/strong&gt; (minimal overhead)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;What does this mean?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;When pandas reads a CSV file, it doesn't just store the raw bytes: it creates in-memory data structures for fast operations. The old object dtype was incredibly inefficient, essentially duplicating string data multiple times. The new PyArrow string dtype keeps overhead minimal with a smarter memory layout.&lt;/p&gt;
&lt;p&gt;This is the difference between pandas 2's Python-object approach and pandas 3's columnar Arrow approach.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-methodology-why-5-different-datasets-&quot;&gt;The Methodology: Why 5 Different Datasets?&lt;/h3&gt;
&lt;p&gt;Now that you've seen the results, let me explain how I tested this. Real-world data comes in many shapes and sizes. A single benchmark on one type of data wouldn't tell the whole story.&lt;/p&gt;
&lt;p&gt;That's why I created &lt;strong&gt;5 distinct datasets&lt;/strong&gt;, each representing common patterns you'll encounter in production:&lt;/p&gt;
&lt;h4 id=&quot;1-low-cardinality-dataset-1m-rows-&quot;&gt;1. &lt;strong&gt;Low Cardinality Dataset&lt;/strong&gt; (1M rows)&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;What it is:&lt;/strong&gt; Repeated categorical values like product categories, status codes, regions, and priorities.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why it matters:&lt;/strong&gt; This is typical of business data - think order statuses, customer segments, or department codes. The same values repeat millions of times.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Example columns:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;category&lt;/code&gt;: &quot;Electronics&quot;, &quot;Clothing&quot;, &quot;Food&quot; (10 unique values)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;status&lt;/code&gt;: &quot;pending&quot;, &quot;completed&quot;, &quot;failed&quot; (4 unique values)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;2-high-cardinality-dataset-1m-rows-&quot;&gt;2. &lt;strong&gt;High Cardinality Dataset&lt;/strong&gt; (1M rows)&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;What it is:&lt;/strong&gt; Mostly unique strings like user IDs, email addresses, and session tokens.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why it matters:&lt;/strong&gt; When every row is different (like customer emails or transaction IDs), pandas can't use simple optimizations. This tests worst-case scenarios.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Example columns:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;user_id&lt;/code&gt;: &quot;USER_00000001&quot;, &quot;USER_00000002&quot;... (1M unique)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;email&lt;/code&gt;: &quot;user123@example45.com&quot; (1M unique)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;3-mixed-string-lengths-dataset-1m-rows-&quot;&gt;3. &lt;strong&gt;Mixed String Lengths Dataset&lt;/strong&gt; (1M rows)&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;What it is:&lt;/strong&gt; A combination of short codes (2-5 chars), medium names (20-50 chars), and long descriptions (100-300 chars).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why it matters:&lt;/strong&gt; Real data isn't uniform. You might have product codes next to customer addresses next to order notes. This tests how pandas handles variable-length strings.&lt;/p&gt;
&lt;h4 id=&quot;4-dataset-with-nulls-1m-rows-&quot;&gt;4. &lt;strong&gt;Dataset With Nulls&lt;/strong&gt; (1M rows)&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;What it is:&lt;/strong&gt; Data with missing values (10-33% nulls in different columns).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why it matters:&lt;/strong&gt; Messy data is reality. How does pandas 3.0 handle missing string data compared to pandas 2?&lt;/p&gt;
&lt;h4 id=&quot;5-large-dataset-10m-rows-&quot;&gt;5. &lt;strong&gt;Large Dataset&lt;/strong&gt; (10M rows)&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;What it is:&lt;/strong&gt; A scaled-up version to test performance at scale.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why it matters:&lt;/strong&gt; Memory savings that look good at 1M rows might behave differently at 10M rows. This validates the findings scale linearly.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;memory-savings-by-dataset-type&quot;&gt;Memory Savings by Dataset Type&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/robot010/Dev-Post-Code/blob/04343be535204b9093df487d1bcfb921d9d00f2e/pandas-memory-saving/visualizations/memory_comparison.png?raw=true&quot; alt=&quot;Memory Consumption Comparison&quot;&gt;&lt;/p&gt;
&lt;p&gt;The memory savings from PyArrow strings vary significantly by dataset characteristics:&lt;/p&gt;
&lt;h4 id=&quot;best-case-low-cardinality-data--71-6-&quot;&gt;&lt;strong&gt;Best Case: Low Cardinality Data (-71.6%)&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;When data has &lt;strong&gt;repeated values&lt;/strong&gt; (like categories), PyArrow strings shine:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Object dtype: 219 MB&lt;/li&gt;
&lt;li&gt;PyArrow string dtype: 62 MB&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Savings: 71.6%&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;worst-case-mixed-string-lengths--30-6-&quot;&gt;&lt;strong&gt;Worst Case: Mixed String Lengths (-30.6%)&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;Variable-length strings see smaller (but still significant) savings:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Object dtype: 383 MB&lt;/li&gt;
&lt;li&gt;PyArrow string dtype: 266 MB&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Savings: 30.6%&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&quot;the-pattern&quot;&gt;&lt;strong&gt;The Pattern&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;Notice how savings correlate with &lt;strong&gt;data characteristics&lt;/strong&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Repeated values&lt;/strong&gt; (low cardinality) → Best savings (64-72%)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Unique values&lt;/strong&gt; (high cardinality) → Good savings (53-55%)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Variable length&lt;/strong&gt; (mixed sizes) → Moderate savings (31%)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Takeaway:&lt;/strong&gt; PyArrow strings help everywhere, but they're &lt;em&gt;especially&lt;/em&gt; powerful for categorical-like data.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;performance-operation-specific-speedups&quot;&gt;Performance: Operation-Specific Speedups&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/robot010/Dev-Post-Code/blob/main/pandas-memory-saving/visualizations/operation_speedup_heatmap.png?raw=true&quot; alt=&quot;Operation Speedup Heatmap&quot;&gt;&lt;/p&gt;
&lt;p&gt;This heatmap shows how much &lt;strong&gt;faster&lt;/strong&gt; PyArrow strings are compared to object dtype for common string operations (values &amp;gt; 1.0 mean PyArrow is faster).&lt;/p&gt;
&lt;h4 id=&quot;the-fastest-operations&quot;&gt;&lt;strong&gt;The Fastest Operations&lt;/strong&gt;&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;str.len()&lt;/code&gt;: 10-27x faster&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;str.startswith()&lt;/code&gt; and &lt;code&gt;str.endswith()&lt;/code&gt;: 11-18x faster&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;str.contains()&lt;/code&gt;: 3-5x faster&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;str.split()&lt;/code&gt;: 1-8x faster&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 id=&quot;the-pattern-1&quot;&gt;&lt;strong&gt;The Pattern&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;Read operations (like &lt;code&gt;len()&lt;/code&gt;, &lt;code&gt;startswith()&lt;/code&gt;) → Massive speedups (10-27x)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;These operations just examine existing data without modification&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Transform operations (like &lt;code&gt;replace()&lt;/code&gt;, &lt;code&gt;split()&lt;/code&gt;) → Good speedups (2-5x)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;These operations create new data, which limits the performance gains&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;the-trade-off-csv-loading-time&quot;&gt;The Trade-off: CSV Loading Time&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://github.com/robot010/Dev-Post-Code/blob/main/pandas-memory-saving/visualizations/load_time_comparison.png?raw=true&quot; alt=&quot;Load Time Comparison&quot;&gt;&lt;/p&gt;
&lt;p&gt;There's no such thing as a free lunch. While PyArrow strings save memory and run operations faster, &lt;strong&gt;loading CSV files is 9%-61% slower&lt;/strong&gt;.&lt;/p&gt;
&lt;h4 id=&quot;why-the-slowdown-&quot;&gt;Why the Slowdown?&lt;/h4&gt;
&lt;p&gt;When pandas reads a CSV with PyArrow strings enabled:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;It parses the text (same as before)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;It converts strings to PyArrow's columnar format&lt;/strong&gt; (extra step)&lt;/li&gt;
&lt;li&gt;This conversion involves building dictionary encodings and optimized memory structures&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Pandas is doing &lt;strong&gt;more work upfront&lt;/strong&gt; to enable better performance downstream.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Real-world impact:&lt;/strong&gt; On our 10M row dataset, the difference is &lt;strong&gt;1.63s vs 2.02s&lt;/strong&gt;, an extra 0.4 seconds for 10 million rows. For many data pipelines, this upfront cost might be negligible compared to the 2-27x speedup in subsequent operations.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;pros-and-cons-should-you-adopt-pyarrow-strings-&quot;&gt;Pros and Cons: Should You Adopt PyArrow Strings?&lt;/h3&gt;
&lt;h4 id=&quot;benefits-of-pyarrow-string-dtype&quot;&gt;&lt;strong&gt;Benefits of PyArrow String Dtype&lt;/strong&gt;&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Massive Memory Savings (30-72%)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Dramatically Faster String Operations (2-27x)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Minimal Memory Overhead (28% vs 166%)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Modern Data Ecosystem Integration&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 id=&quot;trade-offs-to-consider&quot;&gt;&lt;strong&gt;Trade-offs to Consider&lt;/strong&gt;&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Slower CSV Loading (9-61% slower)&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Initial data ingestion takes longer&lt;/li&gt;
&lt;li&gt;May impact workflows that repeatedly load small files&lt;/li&gt;
&lt;li&gt;The trade-off: slower start, much faster operations&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Behavioral Changes&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;String dtype behaves differently from object dtype in edge cases&lt;/li&gt;
&lt;li&gt;Need to update code that explicitly checks for &lt;code&gt;object&lt;/code&gt; dtype&lt;/li&gt;
&lt;li&gt;Testing required for migration&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 id=&quot;the-recommendation&quot;&gt;&lt;strong&gt;The Recommendation&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;For most data workflows, &lt;strong&gt;PyArrow strings are a clear win&lt;/strong&gt;. The memory and performance benefits far outweigh the trade-offs.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Consider staying with object dtype if:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You rarely work with string columns&lt;/li&gt;
&lt;li&gt;Your datasets easily fit in memory&lt;/li&gt;
&lt;li&gt;Load time is critical and you rarely perform string operations&lt;/li&gt;
&lt;li&gt;You have legacy code that's deeply coupled to object dtype behavior&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Definitely adopt PyArrow strings if:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You process large datasets with text data&lt;/li&gt;
&lt;li&gt;String operations are a significant part of your workflow&lt;/li&gt;
&lt;li&gt;Memory is a constraint in your environment&lt;/li&gt;
&lt;li&gt;You're building production data pipelines&lt;/li&gt;
&lt;li&gt;You work with modern data tools (Parquet, Arrow, DuckDB, etc.)&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3 id=&quot;conclusion-7&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;Our comprehensive analysis across &lt;strong&gt;5 diverse datasets&lt;/strong&gt; and &lt;strong&gt;15+ string operations&lt;/strong&gt; conclusively shows that PyArrow-backed strings deliver transformative improvements:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;51.8% average memory savings&lt;/strong&gt; across all dataset types&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;6.17x average operation speedup&lt;/strong&gt; for string operations&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Minimal memory overhead&lt;/strong&gt; (28% vs 166% with Python objects)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;PyArrow strings aren't just an incremental improvement, they're a fundamental reimagining of how pandas handles text data. By adopting Apache Arrow's proven columnar format, pandas has joined the modern data ecosystem while delivering massive performance and memory improvements.&lt;/p&gt;
&lt;p&gt;For most data practitioners working with text data, the question isn't &quot;Should I use PyArrow strings?&quot; but rather &quot;How quickly can I migrate?&quot;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Questions or feedback? Feel free to open an issue or contribute to this analysis! The code we used in this analysis has been uploaded to this &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/robot010/Dev-Post-Code/tree/04343be535204b9093df487d1bcfb921d9d00f2e/pandas-memory-saving&quot;&gt;repo&lt;/a&gt;. &lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/14212/pandas-3-0s-pyarrow-string-revolution-a-deep-dive-into-memory-and-performance</guid>
<pubDate>Wed, 08 Apr 2026 06:01:21 +0000</pubDate>
</item>
<item>
<title>Stop Managing Clusters: Building a Scalable Serverless Data Lake on AWS</title>
<link>https://coderlegion.com/14105/stop-managing-clusters-building-a-scalable-serverless-data-lake-on-aws</link>
<description>&lt;p&gt;Building data pipelines shouldn't feel like babysitting servers. If you’ve ever managed a dedicated cluster just to run a few SQL queries, you know the pain: capacity planning, idle costs, and the &quot;fun&quot; of scaling infrastructure at 3 AM.&lt;/p&gt;
&lt;p&gt;As a Data Engineer, I always follow a simple mantra: Design, then exist. (Or in this case: Design serverless, then relax.)&lt;/p&gt;
&lt;p&gt;Today, we’re breaking down how to centralize your fragmented data into a Serverless Data Lake using the &quot;Big Three&quot; of AWS: S3, Glue, and Athena.&lt;/p&gt;
&lt;p&gt;⚠️ &lt;strong&gt;The Problem: The &quot;Data Swamps&quot;&lt;/strong&gt;&lt;br&gt;
When an organization grows, its data ends up in silos—CSV reports in one bucket, application logs in another, and transactional exports scattered everywhere.&lt;/p&gt;
&lt;p&gt;The result? Data is hard to find, and your analytical queries require heavy ETL before you can even start.&lt;/p&gt;
&lt;p&gt;️ &lt;strong&gt;The Architecture: Storage ≠ Compute&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The beauty of a serverless approach is the decoupling of storage from compute. You only pay for what you store and what you process.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Amazon S3 (The Backbone): This is your central repository. We don't just &quot;dump&quot; data here; we organize it into Layers:&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Raw Layer: The &quot;Source of Truth.&quot; Data exactly as it arrived.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Curated Layer: Cleaned, partitioned, and optimized data (think Parquet format).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;AWS Glue (The Librarian): You don't want to manually define schemas. Glue Crawlers scan your S3 buckets, infer the schema, and populate the Glue Data Catalog.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Amazon Athena (The Engine): A serverless query engine that lets you run standard SQL directly against your files in S3. No clusters to spin up.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;️ &lt;strong&gt;Mini-Demo: From S3 to SQL in 3 Steps&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Ingest: Drop your dataset (CSV or JSON) into your raw S3 bucket.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Catalog: Run a Glue Crawler. It will &quot;detect&quot; your columns and create a table in the metadata catalog.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Query: Head over to the Athena Console and run a query like this:&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;SQL&lt;br&gt;
-- Aggregating sales data directly from S3 files&lt;br&gt;
SELECT &lt;/p&gt;
&lt;pre&gt;&lt;code&gt;region, 
SUM(amount) as total_sales
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;FROM &quot;data_lake_db&quot;.&quot;sales_curated&quot;&lt;br&gt;
GROUP BY region&lt;br&gt;
ORDER BY total_sales DESC;&lt;/p&gt;
&lt;p&gt; &lt;strong&gt;Pro-Tips for the Community&lt;/strong&gt;&lt;br&gt;
If you're building this for production, keep these two things in mind:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Friends don't let friends use CSV for Analytics: Convert your data to Apache Parquet. Because it’s a columnar format, Athena only reads the columns you need. This can reduce your query costs by up to 90%.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Partitioning is King: Organize your S3 paths by date (e.g., s3://my-bucket/year=2026/month=04/). This limits the amount of data Athena has to scan, making your queries lightning-fast.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Final Thoughts&lt;/strong&gt;&lt;br&gt;
Serverless Data Lakes allow us to experiment fast. You can build a proof-of-concept in an afternoon and scale it to petabytes without ever touching a Linux terminal.&lt;/p&gt;
&lt;p&gt;I’m curious—what’s your biggest headache when managing data at scale? Are you team &quot;Serverless All The Way&quot; or do you still prefer dedicated clusters for specific workloads?&lt;/p&gt;
&lt;p&gt;Let’s discuss in the comments! &lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/14105/stop-managing-clusters-building-a-scalable-serverless-data-lake-on-aws</guid>
<pubDate>Mon, 06 Apr 2026 20:52:19 +0000</pubDate>
</item>
<item>
<title>Tech Ecosystem Observatory: How I Built a Cloud-Native Data Pipeline to Track Global Tech Layoffs</title>
<link>https://coderlegion.com/13681/tech-ecosystem-observatory-how-built-cloud-native-data-pipeline-track-global-tech-layoffs</link>
<description>&lt;p&gt;Just completed my DEZ Zoomcamp 2026 capstone project — the Tech Ecosystem Observatory&lt;/p&gt;
&lt;p&gt;Built a full cloud-native batch data pipeline from scratch that answers: which industries are shedding the most jobs, and how does that correlate with YC startup activity?&lt;/p&gt;
&lt;p&gt;Here's what the pipeline looks like end to end:&lt;/p&gt;
&lt;p&gt;✅ Terraform — provisioned GCS bucket and BigQuery datasets as infrastructure as code&lt;/p&gt;
&lt;p&gt;✅ Docker — containerized all ingestion scripts into a portable image&lt;/p&gt;
&lt;p&gt;✅ Kestra — orchestrated a 4-task batch DAG running weekly every Monday 6AM UTC&lt;/p&gt;
&lt;p&gt;✅ Google Cloud Storage — raw JSONL data lake storing layoffs and YC company data&lt;/p&gt;
&lt;p&gt;✅ BigQuery — partitioned by date (monthly) and clustered by industry/country for optimized queries&lt;/p&gt;
&lt;p&gt;✅ dbt Cloud — built staging views and mart tables (mart_monthly_layoffs + mart_tech_ecosystem)&lt;/p&gt;
&lt;p&gt;✅ Looker Studio — 2-page interactive dashboard with layoff trends, geo maps, ecosystem stress ratios&lt;/p&gt;
&lt;p&gt; Data: 4,317 layoff events (2023–2024) + 5,690 YC-backed companies&lt;/p&gt;
&lt;p&gt; Live dashboard: &lt;a rel=&quot;nofollow&quot; href=&quot;https://lookerstudio.google.com/reporting/b1620cae-97cb-4911-82b8-dd0c46ee8acb&quot;&gt;https://lookerstudio.google.com/reporting/b1620cae-97cb-4911-82b8-dd0c46ee8acb&lt;/a&gt;&lt;/p&gt;
&lt;p&gt; GitHub: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Derrick-Ryan-Giggs/tech-ecosystem-observatory&quot;&gt;https://github.com/Derrick-Ryan-Giggs/tech-ecosystem-observatory&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Huge thanks to @DataTalksClub and Alexey Grigorev for building and maintaining this incredible free course. If you're serious about data engineering, this is where you start &lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/DataTalksClub/data-engineering-zoomcamp/&quot;&gt;https://github.com/DataTalksClub/data-engineering-zoomcamp/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Who else is building data pipelines? Drop your projects below &lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/13681/tech-ecosystem-observatory-how-built-cloud-native-data-pipeline-track-global-tech-layoffs</guid>
<pubDate>Mon, 30 Mar 2026 07:55:31 +0000</pubDate>
</item>
<item>
<title>Streaming with PyFlink and Redpanda</title>
<link>https://coderlegion.com/12943/streaming-with-pyflink-and-redpanda</link>
<description>&lt;p&gt;Week 7 of Data Engineering Zoomcamp by @DataTalksClub complete&lt;/p&gt;
&lt;p&gt;Just finished Module 7 - Streaming with PyFlink. Learned how to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Set up Redpanda as a Kafka replacement&lt;/li&gt;
&lt;li&gt;Build Kafka producers and consumers in Python&lt;/li&gt;
&lt;li&gt;Create tumbling and session windows in Flink&lt;/li&gt;
&lt;li&gt;Analyze real-time taxi trip data with stream processing&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here's my homework solution: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Derrick-Ryan-Giggs/Streaming-Homework&quot;&gt;https://github.com/Derrick-Ryan-Giggs/Streaming-Homework&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;You can sign up here: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/DataTalksClub/data-engineering-zoomcamp/&quot;&gt;https://github.com/DataTalksClub/data-engineering-zoomcamp/&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/12943/streaming-with-pyflink-and-redpanda</guid>
<pubDate>Mon, 16 Mar 2026 13:34:43 +0000</pubDate>
</item>
<item>
<title>Batch Processing with Apache Spark</title>
<link>https://coderlegion.com/12456/batch-processing-with-apachespark</link>
<description>&lt;p&gt; Week 6 of Data Engineering Zoomcamp by @DataTalksClub complete&lt;/p&gt;
&lt;p&gt;Just finished Module 6 - Batch Processing with Spark. Learned how to:&lt;/p&gt;
&lt;p&gt;✅ Set up PySpark and create Spark sessions&lt;/p&gt;
&lt;p&gt;✅ Read and process Parquet files at scale&lt;/p&gt;
&lt;p&gt;✅ Repartition data for optimal performance&lt;/p&gt;
&lt;p&gt;✅ Analyze millions of taxi trips with DataFrames&lt;/p&gt;
&lt;p&gt;✅ Use Spark UI for monitoring jobs&lt;/p&gt;
&lt;p&gt;Processing 4M+ taxi trips with Spark - distributed computing is powerful&lt;/p&gt;
&lt;p&gt;Here's my homework solution: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Derrick-Ryan-Giggs/pyspark-homework&quot;&gt;https://github.com/Derrick-Ryan-Giggs/pyspark-homework&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Following along with this amazing free course - who else is learning data engineering?&lt;/p&gt;
&lt;p&gt;You can sign up here: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/DataTalksClub/data-engineering-zoomcamp/&quot;&gt;https://github.com/DataTalksClub/data-engineering-zoomcamp/&lt;/a&gt;&lt;/p&gt;
</description>
<category>Tutorials</category>
<guid isPermaLink="true">https://coderlegion.com/12456/batch-processing-with-apachespark</guid>
<pubDate>Sat, 07 Mar 2026 13:04:56 +0000</pubDate>
</item>
<item>
<title>Why Most Data Projects Fail Before the First Model Is Built</title>
<link>https://coderlegion.com/12407/why-most-data-projects-fail-before-the-first-model-is-built</link>
<description>&lt;p&gt;&lt;strong&gt;Many organizations invest in AI, analytics, and dashboards — yet most data projects fail before the first model is even built.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;When people think about data projects, they often imagine machine learning models, predictive algorithms, and complex pipelines.&lt;/p&gt;
&lt;p&gt;But in reality, most data initiatives fail long before any model is trained.&lt;/p&gt;
&lt;p&gt;Not because the algorithms are weak.&lt;/p&gt;
&lt;p&gt;But because the foundation is broken.&lt;/p&gt;
&lt;h3 id=&quot;the-data-illusion&quot;&gt;The Data Illusion&lt;/h3&gt;
&lt;p&gt;Organizations today generate enormous amounts of data.&lt;/p&gt;
&lt;p&gt;They store logs, transactions, operational records, and performance metrics.&lt;/p&gt;
&lt;p&gt;On paper, everything looks ready for analytics.&lt;/p&gt;
&lt;p&gt;But when teams actually start working with the data, they quickly encounter problems:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Missing values&lt;/li&gt;
&lt;li&gt;Inconsistent formats&lt;/li&gt;
&lt;li&gt;Conflicting sources&lt;/li&gt;
&lt;li&gt;Undefined metrics&lt;/li&gt;
&lt;li&gt;Poor documentation&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Suddenly, the project shifts from analysis to data archaeology.&lt;/p&gt;
&lt;h3 id=&quot;data-science-starts-with-data-reliability&quot;&gt;Data Science Starts with Data Reliability&lt;/h3&gt;
&lt;p&gt;Before any meaningful analysis can begin, teams must answer fundamental questions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;What is the source of truth?&lt;/li&gt;
&lt;li&gt;Who owns the dataset?&lt;/li&gt;
&lt;li&gt;How frequently is it updated?&lt;/li&gt;
&lt;li&gt;What transformations are applied?&lt;/li&gt;
&lt;li&gt;Are definitions consistent across departments?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Without clear answers, even the most advanced models produce misleading insights.&lt;/p&gt;
&lt;h3 id=&quot;the-hidden-cost-of-poor-data-foundations&quot;&gt;The Hidden Cost of Poor Data Foundations&lt;/h3&gt;
&lt;p&gt;Many organizations invest heavily in analytics tools, dashboards, and AI platforms.&lt;/p&gt;
&lt;p&gt;But without strong data foundations, these investments create an illusion of intelligence.&lt;/p&gt;
&lt;p&gt;Dashboards become visually impressive but operationally misleading.&lt;/p&gt;
&lt;p&gt;Models generate predictions, but the inputs themselves are unstable.&lt;/p&gt;
&lt;p&gt;This leads to one of the most dangerous outcomes in data work:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;False confidence.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Decisions start relying on numbers that appear precise but are fundamentally unreliable.&lt;/p&gt;
&lt;h3 id=&quot;data-engineering-is-the-real-backbone&quot;&gt;Data Engineering Is the Real Backbone&lt;/h3&gt;
&lt;p&gt;In practice, the majority of effort in data projects is not modeling.&lt;/p&gt;
&lt;p&gt;It is:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Data cleaning&lt;/li&gt;
&lt;li&gt;Validation&lt;/li&gt;
&lt;li&gt;Schema alignment&lt;/li&gt;
&lt;li&gt;Pipeline reliability&lt;/li&gt;
&lt;li&gt;Monitoring data quality&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That is why experienced teams often say:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;“80% of data science is data preparation.”&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;And the better the data infrastructure, the faster meaningful insights appear.&lt;/p&gt;
&lt;h3 id=&quot;a-simple-rule-for-data-teams&quot;&gt;A Simple Rule for Data Teams&lt;/h3&gt;
&lt;p&gt;Before building any model, ask three questions:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Is the data trustworthy?&lt;/li&gt;
&lt;li&gt;Is the definition of the metric consistent?&lt;/li&gt;
&lt;li&gt;Can the pipeline reproduce the same dataset tomorrow?&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If the answer to any of these is unclear, the problem is not analytical.&lt;/p&gt;
&lt;p&gt;It is architectural.&lt;/p&gt;
&lt;h3 id=&quot;final-thought-1&quot;&gt;Final Thought&lt;/h3&gt;
&lt;p&gt;Good data teams do not start with models.&lt;/p&gt;
&lt;p&gt;They start with reliability.&lt;/p&gt;
&lt;p&gt;Because in data systems, accuracy is not created by algorithms.&lt;/p&gt;
&lt;p&gt;It is created by architecture.&lt;/p&gt;
&lt;p&gt;If you're interested in systems thinking, data architecture, and enterprise optimization, feel free to connect.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;LinkedIn:&lt;/em&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.linkedin.com/in/fadydesokysaeedabdelaziz&quot;&gt;https://www.linkedin.com/in/fadydesokysaeedabdelaziz&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;GitHub:&lt;/em&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/fadydesoky&quot;&gt;https://github.com/fadydesoky&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/12407/why-most-data-projects-fail-before-the-first-model-is-built</guid>
<pubDate>Thu, 05 Mar 2026 18:45:16 +0000</pubDate>
</item>
<item>
<title>From APIs to Warehouses: AI-Assisted Data Ingestion with dlt</title>
<link>https://coderlegion.com/12288/from-apis-to-warehouses-ai-assisted-data-ingestion-withdlt</link>
<description>&lt;p&gt; dlt Workshop of Data Engineering Zoomcamp by @DataTalksClub complete&lt;/p&gt;
&lt;p&gt;Just finished the Data Ingestion workshop with @dltHub. Learned how to:&lt;br&gt;
✅ Build REST API data pipelines with dlt&lt;br&gt;
✅ Use AI-assisted development with dlt MCP Server&lt;br&gt;
✅ Load paginated API data into DuckDB&lt;br&gt;
✅ Inspect pipeline data with dlt Dashboard and marimo notebooks&lt;/p&gt;
&lt;p&gt;Built a full NYC taxi data pipeline from a custom API - AI-assisted data engineering is the future&lt;/p&gt;
&lt;p&gt;Here's my homework solution: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Derrick-Ryan-Giggs/-my-dlt-taxi-pipeline&quot;&gt;https://github.com/Derrick-Ryan-Giggs/-my-dlt-taxi-pipeline&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Following along with this amazing free course - who else is learning data engineering?&lt;/p&gt;
&lt;p&gt;You can sign up here: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/DataTalksClub/data-engineering-zoomcamp/&quot;&gt;https://github.com/DataTalksClub/data-engineering-zoomcamp/&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/12288/from-apis-to-warehouses-ai-assisted-data-ingestion-withdlt</guid>
<pubDate>Sun, 01 Mar 2026 11:45:47 +0000</pubDate>
</item>
<item>
<title>The Day BI Died in Real Time: What MCP Integration With a Semantic Layer Means for Data Engineers</title>
<link>https://coderlegion.com/12253/the-day-bi-died-real-time-what-mcp-integration-with-semantic-layer-means-for-data-engineers</link>
<description>&lt;p&gt;On December 12, 2025, Strategy CPO Saurabh Abhyankar watched Claude Code connect to Mosaic through the Model Context Protocol (MCP) and build a complete analytics application. No dashboard tool. No BI platform. Just an AI coding agent querying a semantic layer and generating a working app.&lt;/p&gt;
&lt;p&gt;At Strategy World 2026, Abhyankar told a room of industry analysts and practitioners that this was the moment he knew BI was dead. Not dying. Dead. Twenty years of building BI tools, he said — billions of dollars of engineering — and an AI coding agent replicated it in minutes.&lt;/p&gt;
&lt;p&gt;That's a bold claim from a CPO whose company sells BI tools. But the technical architecture underneath it is worth understanding, regardless of whether you buy the marketing. Because what's actually happening at the protocol level — MCP connecting AI agents to governed semantic layers — changes how data engineers and developers will build analytics for the foreseeable future.&lt;/p&gt;
&lt;h3 id=&quot;the-abstraction-gap-that-explains-everything&quot;&gt;The Abstraction Gap That Explains Everything&lt;/h3&gt;
&lt;p&gt;Abhyankar's keynote built on a technical argument that developers and data engineers should find familiar: the history of productivity in software is the history of abstraction layers.&lt;/p&gt;
&lt;p&gt;Physical circuits gave way to assembly language, which delivered roughly a 50x productivity improvement. High-level languages and eventually Python and AI-assisted coding have pushed that number to approximately 22,000x over six decades. Each layer of abstraction let engineers work at a higher level while the toolchain handled the complexity underneath.&lt;/p&gt;
&lt;p&gt;Data engineering followed a different trajectory. The move from flat files to relational databases and SQL delivered about a 600x improvement. And then it stopped. The Big Data era actually regressed — MapReduce and Java-based processing were less productive than SQL for most analytical workloads. Modern lakehouse platforms like Snowflake and Databricks abstracted compute from storage, which was a meaningful architectural advance, but the query interface is still SQL. The abstraction layer didn't move up.&lt;/p&gt;
&lt;p&gt;That's the gap. Software engineering productivity improved 22,000x. Data engineering productivity improved 600x and stalled. Abhyankar's argument is that this gap explains why data engineering projects take so long relative to software development, why data teams are perpetually backlogged, and why the BI industry has been building incrementally better versions of the same thing for 30 years.&lt;/p&gt;
&lt;p&gt;Mosaic, in this framing, is the next abstraction layer. Instead of working at the SQL and table level, data engineers work at the business semantics level — defining what entities, metrics, and relationships mean. The platform handles the SQL generation, table creation, and query optimization underneath, the same way a compiler handles the conversion from high-level code to machine instructions.&lt;/p&gt;
&lt;h3 id=&quot;how-mcp-changes-the-architecture&quot;&gt;How MCP Changes the Architecture&lt;/h3&gt;
&lt;p&gt;The Model Context Protocol is what makes the December 12 demo more than a parlor trick. MCP provides a standardized way for AI agents to discover and interact with external tools and data sources. When Claude Code connected to Mosaic via MCP, it didn't need to know the underlying database schema, the SQL dialect, or the table relationships. It queried the semantic layer, which translated business-level questions into the correct technical operations.&lt;/p&gt;
&lt;p&gt;This is architecturally significant for three reasons.&lt;/p&gt;
&lt;p&gt;First, the AI agent works at the same level of abstraction as a business user. It asks for revenue by region or inventory by store, and the semantic layer handles the joins, filters, and aggregations. The agent doesn't generate raw SQL against source tables. It generates queries against governed business definitions. That means the output is consistent regardless of which agent, which LLM, or which interface is asking.&lt;/p&gt;
&lt;p&gt;Second, governance travels with the data. Because the agent queries through the semantic layer, every access control, metric definition, and business rule applies automatically. An AI agent building an inventory management app gets the same governed data as a dashboard, a notebook, or a human asking questions through conversational BI. The governance isn't bolted on after the fact. It's baked into the query path.&lt;/p&gt;
&lt;p&gt;Third, the application layer becomes disposable. If an AI coding tool can generate a working analytics app from a requirements document in minutes, the traditional build cycle — requirements gathering, development, testing, deployment, maintenance — collapses. The app itself stops being the valuable artifact. The semantic layer underneath it becomes the durable asset.&lt;/p&gt;
&lt;p&gt;Abhyankar demonstrated this live on stage with three different AI platforms. Gemini pulled data from Mosaic to create a product management workflow across CRM and ticketing systems. Claude generated multi-sheet Excel analyses with visualizations and built executive presentations from the same governed data source. OpenAI's Codex took a requirements document and built a complete inventory management application that worked on iPad and iPhone — from specification to working app in minutes.&lt;/p&gt;
&lt;p&gt;The common thread: all three used Mosaic as the data layer through MCP. None of them needed to know anything about the underlying databases.&lt;/p&gt;
&lt;h3 id=&quot;open-semantic-interchange-and-git-integration-what-matters-to-engineers&quot;&gt;Open Semantic Interchange and Git Integration: What Matters to Engineers&lt;/h3&gt;
&lt;p&gt;Abhyankar announced that Strategy has joined the Open Semantic Interchange initiative. The practical implication: semantic models defined in Mosaic can be exported and imported in a common YAML format. On paper, this means enterprises aren't locked into a single semantic layer vendor. They can move definitions between tools or run multiple semantic layers that stay synchronized.&lt;/p&gt;
&lt;p&gt;More immediately useful for engineering teams: Mosaic now supports Git integration. Semantic models can be version-controlled, run through CI/CD pipelines, and managed with standard diff and merge workflows. If you're a data engineer who's been maintaining metric definitions in documentation or tribal knowledge, this is the shift toward treating semantic models as code — versioned, tested, reviewed, and deployed through the same pipelines as application code.&lt;/p&gt;
&lt;p&gt;Abhyankar was candid that enterprises will end up with multiple semantic layers. Companies acquire other companies, different teams adopt different tools, and the notion of a single universal layer across a large enterprise is aspirational. The OSI standard and Git integration are Strategy's answer to that reality: if you're going to have multiple semantic layers, at least make them interoperable and manageable through standard engineering workflows.&lt;/p&gt;
&lt;h3 id=&quot;from-descriptive-semantics-to-business-ontology&quot;&gt;From Descriptive Semantics to Business Ontology&lt;/h3&gt;
&lt;p&gt;The most technically ambitious part of the Mosaic roadmap is the expansion from a semantic layer into a full business ontology. Traditional semantic layers are descriptive: they define what entities and metrics mean. Mosaic is adding verbs, rules, subtypes, workflows, and operational descriptions.&lt;/p&gt;
&lt;p&gt;In concrete terms, that means encoding not just that a &quot;customer&quot; entity exists, but that a customer buys products, returns products, and must buy before returning. Premium customers inherit from the base customer entity with additional properties. Stores operate according to defined workflows. Factory material flows follow specific sequences.&lt;/p&gt;
&lt;p&gt;During the analyst briefing, one industry researcher drew the distinction between descriptive and prescriptive semantics. A descriptive layer tells you what your data looks like. A prescriptive layer tells agents what to do with it. That distinction matters for the AI use case. If you want an agent to take action on behalf of the business — not just answer questions but execute workflows — it needs prescriptive rules, not just descriptive metadata.&lt;/p&gt;
&lt;p&gt;Abhyankar described a system where Mosaic can suggest relationships automatically when it sees entities. If it encounters a customer entity and a product entity, it can propose that the relationship is &quot;customer buys product&quot; based on common patterns. Companies that have existing business process documentation can feed that into Mosaic to accelerate the ontology build. And over time, metadata from existing Strategy deployments across industries can be aggregated into vertical templates — a default semantic layer for financial services, retail, healthcare, and so on.&lt;/p&gt;
&lt;p&gt;This is where the vision gets ambitious and the execution risk increases. Encoding an entire business into a semantic ontology is a massive undertaking. It's also where AI has real limitations.&lt;/p&gt;
&lt;h3 id=&quot;what-production-looks-like-today&quot;&gt;What Production Looks Like Today&lt;/h3&gt;
&lt;p&gt;At the same conference, Sachin Bhatta, BI Director at the North Texas Tollway Authority, described what it's actually like to run Mosaic in production. NTTA processes over 300 billion toll transactions and moves 1.7 terabytes of data daily. They were one of the earliest Mosaic pilot customers and are now live in dark mode alongside their existing infrastructure.&lt;/p&gt;
&lt;p&gt;Bhatta's experience offers useful data points for engineering teams evaluating the platform.&lt;/p&gt;
&lt;p&gt;On the data warehouse question: Bhatta is keeping his BigQuery environment. He sees the direction Mosaic is heading, but he's waiting for the technology to prove out at his data volumes before migrating. For new projects and new data sources, he starts in Mosaic. For existing infrastructure, he runs both in parallel.&lt;/p&gt;
&lt;p&gt;On AI limitations: Bhatta offered a practical corrective that developers need to hear. AI handles textual queries well, but it still struggles with mathematical calculations and computations. That's why the metrics layer in Mosaic matters separately from the LLM integration. Calculated metrics need to be defined and computed by the engine, not generated by a language model that might hallucinate the math. Mosaic addresses this with two types of agents — one that operates on the semantic model and one that operates on dashboards — keeping computation separate from natural language interpretation.&lt;/p&gt;
&lt;p&gt;On integration with existing Strategy environments: this is the gap Bhatta flagged most directly. Organizations that have invested years in building semantic layers with thousands of attributes and metrics on Strategy's traditional Intelligence Server platform need a bridge to Mosaic. That bridge doesn't fully exist yet. New projects work well in Mosaic. Migrating legacy semantic models is still a work in progress.&lt;/p&gt;
&lt;p&gt;On preparatory work: Bhatta was blunt about what has to happen before Mosaic or AI delivers value. Your data dictionary, data catalog, and naming conventions need to be in order. Mosaic reads your metadata. If the metadata is inconsistent, the LLM answers will be inconsistent. No amount of AI sophistication fixes dirty metadata.&lt;/p&gt;
&lt;p&gt;NTTA also provided a useful cost comparison. Before Mosaic, Bhatta's team ran customer clustering through Google Vertex AI — 32 separate processes involving knowledge graphs and ML models on BigQuery. Mosaic's built-in clustering feature could have handled much of that work within a single tool at significantly lower cost. For engineering teams doing cost-benefit analysis, that's a concrete data point.&lt;/p&gt;
&lt;h3 id=&quot;what-data-engineers-should-pay-attention-to&quot;&gt;What Data Engineers Should Pay Attention To&lt;/h3&gt;
&lt;p&gt;Strip away the conference marketing and three technical developments from Strategy World 2026 are worth tracking.&lt;/p&gt;
&lt;p&gt;The first is MCP as the standard interface between AI agents and governed data layers. This isn't specific to Strategy. Any semantic layer or data platform that exposes its capabilities through MCP becomes accessible to any AI agent that supports the protocol. The competitive question isn't which semantic layer is best. It's which one your AI toolchain can talk to most effectively.&lt;/p&gt;
&lt;p&gt;The second is semantic models as code. Git integration, CI/CD pipelines, version control, diff and merge — these are the workflows that data engineers already use for everything else. Bringing semantic model management into the same toolchain removes a category of manual governance work and makes semantic definitions testable and auditable in the same way application code is.&lt;/p&gt;
&lt;p&gt;The third is the shift from descriptive to prescriptive semantics. If your organization is planning for agentic AI — agents that don't just answer questions but execute business workflows — your semantic layer needs to express business rules, relationships, and constraints, not just metric definitions. That's a significantly larger modeling effort than what most organizations have undertaken, and it's worth starting the planning now even if the tooling isn't fully mature.&lt;/p&gt;
&lt;p&gt;Abhyankar gave the room a timeline for the data warehouse transition: some elements in six months, substantial capability within five years. That's an honest range from a CPO who watched his own product category get replicated by an AI agent in real time.&lt;/p&gt;
&lt;p&gt;For data engineers, the practical move is the same one NTTA is already making: start new projects in the semantic layer, keep existing infrastructure running, invest in metadata quality, and watch how MCP integration with your AI toolchain evolves over the next two quarters. The architecture is shifting. The timeline depends on your data volumes, your governance maturity, and how much of your current pipeline is adding value versus just moving bytes.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/12253/the-day-bi-died-real-time-what-mcp-integration-with-semantic-layer-means-for-data-engineers</guid>
<pubDate>Fri, 27 Feb 2026 18:32:35 +0000</pubDate>
</item>
<item>
<title>Data Platforms with Bruin</title>
<link>https://coderlegion.com/12125/data-platforms-with-bruin</link>
<description>&lt;p&gt;Week 5 of Data Engineering Zoomcamp by @DataTalksClub complete&lt;/p&gt;
&lt;p&gt;Just finished Module 5 - Data Platforms with Bruin. Learned how to:&lt;/p&gt;
&lt;p&gt;✅ Build end-to-end ELT pipelines with Bruin&lt;br&gt;
✅ Configure environments and connections&lt;br&gt;
✅ Use materialization strategies for incremental processing&lt;br&gt;
✅ Add data quality checks to ensure data integrity&lt;br&gt;
✅ Deploy pipelines from local to cloud (BigQuery)&lt;/p&gt;
&lt;p&gt;Modern data platforms in a single CLI tool - no vendor lock-in&lt;/p&gt;
&lt;p&gt;Here's my homework solution: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Derrick-Ryan-Giggs/bruin-taxi-pipeline-homework&quot;&gt;https://github.com/Derrick-Ryan-Giggs/bruin-taxi-pipeline-homework&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Following along with this amazing free course - who else is learning data engineering?&lt;/p&gt;
&lt;p&gt;You can sign up here: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/DataTalksClub/data-engineering-zoomcamp/&quot;&gt;https://github.com/DataTalksClub/data-engineering-zoomcamp/&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/12125/data-platforms-with-bruin</guid>
<pubDate>Tue, 24 Feb 2026 09:04:57 +0000</pubDate>
</item>
<item>
<title>Analytics Engineering</title>
<link>https://coderlegion.com/11683/analytics-engineering</link>
<description>&lt;p&gt;Week 4 of Data Engineering Zoomcamp by @DataTalksClub complete&lt;/p&gt;
&lt;p&gt;Just finished Module 4 - Analytics Engineering with dbt. Learned how to:&lt;/p&gt;
&lt;p&gt;✅ Build transformation models with dbt&lt;br&gt;
✅ Create staging, intermediate, and fact tables&lt;br&gt;
✅ Write tests to ensure data quality&lt;br&gt;
✅ Understand lineage and model dependencies&lt;br&gt;
✅ Analyze revenue patterns across NYC zones&lt;/p&gt;
&lt;p&gt;Transforming raw data into analytics-ready models - the T in ELT&lt;/p&gt;
&lt;p&gt;Here's my homework solution: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Derrick-Ryan-Giggs/Analytics-Engineering&quot;&gt;https://github.com/Derrick-Ryan-Giggs/Analytics-Engineering&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Following along with this amazing free course - who else is learning data engineering?&lt;/p&gt;
&lt;p&gt;You can sign up here: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/DataTalksClub/data-engineering-zoomcamp/&quot;&gt;https://github.com/DataTalksClub/data-engineering-zoomcamp/&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/11683/analytics-engineering</guid>
<pubDate>Sun, 15 Feb 2026 11:51:43 +0000</pubDate>
</item>
<item>
<title>Data Is Not a Department — It’s a Decision Architecture</title>
<link>https://coderlegion.com/11594/data-is-not-a-department-its-a-decision-architecture</link>
<description>&lt;p&gt;Most organizations say they want to be “&lt;strong&gt;data-driven&lt;/strong&gt;.”&lt;/p&gt;
&lt;p&gt;They invest in dashboards.&lt;br&gt;
They build reporting systems.&lt;br&gt;
They hire analysts.&lt;/p&gt;
&lt;p&gt;Yet somehow, decisions still rely on intuition, hierarchy, or urgency.&lt;/p&gt;
&lt;p&gt;The issue is rarely the absence of data.&lt;/p&gt;
&lt;p&gt;It’s the absence of decision architecture.&lt;/p&gt;
&lt;h3 id=&quot;the-illusion-we-need-more-data-&quot;&gt;The Illusion: “We Need More Data&quot;&lt;/h3&gt;
&lt;p&gt;In many enterprise environments, the default reaction to underperformance is:&lt;/p&gt;
&lt;p&gt;“&lt;strong&gt;We need better data.&lt;/strong&gt;”&lt;/p&gt;
&lt;p&gt;But when you look closely, the organization often already has:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Historical performance metrics&lt;/li&gt;
&lt;li&gt;Operational KPIs&lt;/li&gt;
&lt;li&gt;Real-time dashboards&lt;/li&gt;
&lt;li&gt;Financial summaries&lt;/li&gt;
&lt;li&gt;Compliance reports&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The problem isn’t collection.&lt;/p&gt;
&lt;p&gt;It’s &lt;strong&gt;translation&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Data exists — but decision flow does not.&lt;/p&gt;
&lt;h3 id=&quot;where-data-quietly-fails&quot;&gt;Where Data Quietly Fails&lt;/h3&gt;
&lt;p&gt;Over time, I’ve noticed a recurring pattern in large systems.&lt;/p&gt;
&lt;p&gt;Reports are generated. Dashboards are circulated. Metrics are reviewed.&lt;/p&gt;
&lt;p&gt;But very few questions are clearly defined:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Who owns this metric?&lt;/li&gt;
&lt;li&gt;What threshold triggers action?&lt;/li&gt;
&lt;li&gt;What happens when it deviates?&lt;/li&gt;
&lt;li&gt;Who is accountable for response?&lt;/li&gt;
&lt;li&gt;Is there feedback after the decision?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Without these layers, data becomes informative — not operational.&lt;/p&gt;
&lt;p&gt;And informative systems do not scale well.&lt;/p&gt;
&lt;h3 id=&quot;data-without-structure-becomes-noise&quot;&gt;Data Without Structure Becomes Noise&lt;/h3&gt;
&lt;p&gt;Imagine a KPI dashboard that shows:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Turnaround time&lt;/li&gt;
&lt;li&gt;Error rate&lt;/li&gt;
&lt;li&gt;Processing backlog&lt;/li&gt;
&lt;li&gt;Resource utilization&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;All useful.&lt;/p&gt;
&lt;p&gt;But unless the organization defines:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A decision threshold&lt;/li&gt;
&lt;li&gt;A response protocol&lt;/li&gt;
&lt;li&gt;An escalation path&lt;/li&gt;
&lt;li&gt;A measurable follow-up&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The dashboard becomes a &lt;strong&gt;passive artifact&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;It looks analytical. It feels analytical. But it doesn’t change behavior.&lt;/p&gt;
&lt;p&gt;That’s not a data problem.&lt;/p&gt;
&lt;p&gt;That’s an architecture problem.&lt;/p&gt;
&lt;h3 id=&quot;what-decision-architecture-actually-means&quot;&gt;What Decision Architecture Actually Means&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Decision architecture is not a tool&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;It’s a structured design that connects:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Metrics&lt;/li&gt;
&lt;li&gt;Ownership&lt;/li&gt;
&lt;li&gt;Triggers&lt;/li&gt;
&lt;li&gt;Actions&lt;/li&gt;
&lt;li&gt;Feedback loops&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;In mature systems, every critical metric answers:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;When does this require action?&lt;/li&gt;
&lt;li&gt;Who initiates the action?&lt;/li&gt;
&lt;li&gt;What is the defined response?&lt;/li&gt;
&lt;li&gt;How is impact measured afterward?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That is when data becomes operational.&lt;/p&gt;
&lt;h3 id=&quot;a-practical-enterprise-lens&quot;&gt;A Practical Enterprise Lens&lt;/h3&gt;
&lt;p&gt;In large organizations, complexity multiplies quickly:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Multiple regions&lt;/li&gt;
&lt;li&gt;Multiple policy versions&lt;/li&gt;
&lt;li&gt;Multiple approval layers&lt;/li&gt;
&lt;li&gt;Multiple reporting lines&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Without structured decision architecture, each layer interprets the same data differently.&lt;/p&gt;
&lt;p&gt;Over time, this creates:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Fragmented responses&lt;/li&gt;
&lt;li&gt;Delayed interventions&lt;/li&gt;
&lt;li&gt;Manual overrides&lt;/li&gt;
&lt;li&gt;Invisible technical debt&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And eventually, performance issues are treated as isolated incidents instead of systemic misalignments.&lt;/p&gt;
&lt;h3 id=&quot;the-human-element&quot;&gt;The Human Element&lt;/h3&gt;
&lt;p&gt;There’s also a subtle human factor.&lt;/p&gt;
&lt;p&gt;When decision rules are unclear:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Teams hesitate.&lt;/li&gt;
&lt;li&gt;Accountability blurs.&lt;/li&gt;
&lt;li&gt;Escalation becomes personal instead of structural.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Clear architecture removes &lt;strong&gt;ambiguity&lt;/strong&gt;. It replaces &lt;strong&gt;emotion&lt;/strong&gt; with &lt;strong&gt;logic&lt;/strong&gt;. And it reduces &lt;strong&gt;friction&lt;/strong&gt; across teams.&lt;/p&gt;
&lt;p&gt;Data alone cannot do that.&lt;/p&gt;
&lt;p&gt;Design can.&lt;/p&gt;
&lt;h3 id=&quot;from-data-collection-to-data-responsibility&quot;&gt;From Data Collection to Data Responsibility&lt;/h3&gt;
&lt;p&gt;Organizations that truly operate on data do something different.&lt;/p&gt;
&lt;p&gt;They don’t just measure.&lt;/p&gt;
&lt;p&gt;They define:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Ownership boundaries&lt;/li&gt;
&lt;li&gt;Trigger conditions&lt;/li&gt;
&lt;li&gt;Decision latency limits&lt;/li&gt;
&lt;li&gt;Review cycles&lt;/li&gt;
&lt;li&gt;Iteration mechanisms&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;They treat data as &lt;strong&gt;infrastructure&lt;/strong&gt; — not decoration.&lt;/p&gt;
&lt;h3 id=&quot;final-thought-2&quot;&gt;Final Thought&lt;/h3&gt;
&lt;p&gt;Good organizations collect data.&lt;/p&gt;
&lt;p&gt;Mature organizations analyze it.&lt;/p&gt;
&lt;p&gt;High-performing organizations design decisions around it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Data is not a department&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;It is an architectural responsibility.&lt;/p&gt;
&lt;p&gt;And when that architecture is clear, performance becomes a byproduct — not a struggle.&lt;/p&gt;
&lt;p&gt;Finally, If you’re interested in systems thinking, enterprise optimization, and designing responsible decision frameworks, feel free to connect.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;LinkedIn&lt;/strong&gt;: &lt;a rel=&quot;nofollow&quot; href=&quot;https://www.linkedin.com/in/fadydesokysaeedabdelaziz&quot;&gt;https://www.linkedin.com/in/fadydesokysaeedabdelaziz&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/fadydesoky&quot;&gt;https://github.com/fadydesoky&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/11594/data-is-not-a-department-its-a-decision-architecture</guid>
<pubDate>Fri, 13 Feb 2026 12:47:57 +0000</pubDate>
</item>
<item>
<title>Data Warehousing &amp; BigQuery</title>
<link>https://coderlegion.com/11258/data-warehousing-bigquery</link>
<description>&lt;p&gt;Week 3 of Data Engineering Zoomcamp by @DataTalksClub complete!&lt;br&gt;
Just finished Module 3 - Data Warehousing with BigQuery. Learned how to:&lt;br&gt;
 Create external tables from GCS bucket data&lt;br&gt;
 Build materialized tables in BigQuery&lt;br&gt;
 Partition and cluster tables for performance&lt;br&gt;
 Understand columnar storage and query optimization&lt;br&gt;
 Analyze NYC taxi data at scale&lt;br&gt;
Working with 20M+ records and learning how partitioning reduces query costs!&lt;/p&gt;
&lt;p&gt;Here's my homework solution: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/Derrick-Ryan-Giggs/Module-3-Data-Warehousing-BigQuery-homework&quot;&gt;https://github.com/Derrick-Ryan-Giggs/Module-3-Data-Warehousing-BigQuery-homework&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Following along with this amazing free course - who else is learning data engineering?&lt;br&gt;
You can sign up here: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/DataTalksClub/data-engineering-zoomcamp/&quot;&gt;https://github.com/DataTalksClub/data-engineering-zoomcamp/&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/11258/data-warehousing-bigquery</guid>
<pubDate>Sun, 08 Feb 2026 11:13:57 +0000</pubDate>
</item>
</channel>
</rss>