<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Shorts on EXPLAIN ANALYZE</title><link>https://explainanalyze.com/tags/shorts/</link><description>Recent content in Shorts on EXPLAIN ANALYZE</description><generator>Hugo -- gohugo.io</generator><language>en-us</language><lastBuildDate>Sat, 11 Jul 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://explainanalyze.com/tags/shorts/index.xml" rel="self" type="application/rss+xml"/><item><title>Cross-Database Joins: The Coupling You Can't See Until You Split</title><link>https://explainanalyze.com/p/cross-database-joins-the-coupling-you-cant-see-until-you-split/</link><pubDate>Sat, 11 Jul 2026 00:00:00 +0000</pubDate><guid>https://explainanalyze.com/p/cross-database-joins-the-coupling-you-cant-see-until-you-split/</guid><description>&lt;img src="https://explainanalyze.com/" alt="Featured image of post Cross-Database Joins: The Coupling You Can't See Until You Split" /&gt;&lt;div class="tldr-box"&gt;
 &lt;strong&gt;TL;DR&lt;/strong&gt;
 &lt;div&gt;A join across two schemas on the same MySQL server costs nothing to write, which is exactly why nobody counts it. Every one of those joins is a hard coupling that pins both schemas to one instance, so the day you need to split for scale, the joins are the wall. And the referential integrity you assumed the boundary carried was never enforced to begin with.&lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;Somewhere in the codebase, since roughly the first month, there is a query like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;li&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;li&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sku&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;li&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;qty&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;inv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;inv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;amount_due&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;line_items&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;li&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;JOIN&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;billing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;invoices&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;inv&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;ON&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;inv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;li&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;li&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;2026-07-01&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Two schemas, &lt;code&gt;orders&lt;/code&gt; and &lt;code&gt;billing&lt;/code&gt;, one MySQL server, single-digit milliseconds for three years. In that time roughly four hundred more queries have grown the same shape: application code, a dozen dbt models, cron reports, the admin dashboard. Nobody wrote a design doc for any of them, because MySQL lets you qualify a table with &lt;code&gt;schema.table&lt;/code&gt; and join across the boundary as if it weren&amp;rsquo;t there. Then the primary crosses 90% on the capacity dashboard, the obvious relief is to lift &lt;code&gt;billing&lt;/code&gt; onto its own instance, and the migration plan dies in review on one line: four hundred joins stop compiling the moment the schemas live on different servers.&lt;/p&gt;
&lt;h2 id="the-fdw-that-looks-like-a-fix"&gt;The FDW that looks like a fix
&lt;/h2&gt;&lt;p&gt;The escape hatch is obvious: split anyway and put a foreign data wrapper between the instances so the joins keep working across the wire. The syntax survives the split. The performance does not. Cross-server joins don&amp;rsquo;t push down; &lt;code&gt;postgres_fdw&lt;/code&gt; only sends a join to the far side when both tables live on the same foreign server (&lt;a class="link" href="https://www.postgresql.org/docs/current/postgres-fdw.html" target="_blank" rel="noopener"
 &gt;the docs are explicit&lt;/a&gt;), so across two instances the join executes locally, pulling rows over the network first. Svix &lt;a class="link" href="https://www.svix.com/blog/fdw-pitfalls/" target="_blank" rel="noopener"
 &gt;hit exactly this in October 2025&lt;/a&gt;: a join that failed to push down pulled both tables in their entirety, moved millions of rows where thousands were expected, and timed out.&lt;/p&gt;
&lt;p&gt;So the FDW gives you back the &lt;code&gt;SELECT&lt;/code&gt; and takes away the reason you split. Every cross-boundary query drags its working set back over the network to be joined next to the primary anyway.&lt;/p&gt;
&lt;div class="warning-box"&gt;
 &lt;strong&gt;An FDW join across instances is a distributed join, not a local one&lt;/strong&gt;
 &lt;div&gt;If &lt;code&gt;EXPLAIN (VERBOSE)&lt;/code&gt; shows the join in the local plan rather than in the &lt;code&gt;Remote SQL&lt;/code&gt;, the far table is shipping over in full. Treat every cross-instance FDW join as a network transfer sized by the larger table, not by your &lt;code&gt;WHERE&lt;/code&gt; clause.&lt;/div&gt;
&lt;/div&gt;

&lt;h2 id="free-to-write-fake-underneath"&gt;Free to write, fake underneath
&lt;/h2&gt;&lt;p&gt;A MySQL &amp;ldquo;database&amp;rdquo; and a MySQL &amp;ldquo;schema&amp;rdquo; are the same object: a namespace on one server. There is no cross-database join inside one server, only a fully-qualified one, and the qualifier costs a few keystrokes. Postgres draws the line elsewhere. Schemas in one database join freely; separate databases can&amp;rsquo;t be joined at all without FDW or &lt;code&gt;dblink&lt;/code&gt;, so a team that split into Postgres databases hits the wall on day one, where at least it&amp;rsquo;s visible.&lt;/p&gt;
&lt;p&gt;The worse fact: MySQL foreign keys can cross schemas. &lt;code&gt;REFERENCES billing.invoices(order_id)&lt;/code&gt; is legal DDL, and almost nobody writes it, because the schemas belong to different teams and nobody wants their writes gated on another team&amp;rsquo;s table. So the boundary those four hundred joins cross has no enforced referential integrity and never did. The failure modes are the ones you&amp;rsquo;d predict from &lt;a class="link" href="https://explainanalyze.com/p/foreign-keys-are-not-optional/" &gt;dropping a foreign key&lt;/a&gt;, except no FK was ever declared: orphaned line items with no cascade to catch them, ID-type drift that turns the join into a silent cast (&lt;code&gt;BIGINT&lt;/code&gt; on one side, &lt;code&gt;CHAR(36)&lt;/code&gt; UUID after a migration on the other), backfills that assume the far side exists.&lt;/p&gt;
&lt;div class="note-box"&gt;
 &lt;strong&gt;This is a same-server, cross-namespace problem&lt;/strong&gt;
 &lt;div&gt;Cross-schema joins inside one Postgres database, or on a MySQL server you never intend to split, are not the anti-pattern. The coupling bites when scale forces the namespaces onto different instances. If that day never comes, the free join is genuinely free; the trap is assuming that&amp;rsquo;s your situation when the growth curve says otherwise.&lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;Recovery has the same hole. Each database backs up independently, two &lt;code&gt;--single-transaction&lt;/code&gt; dumps, each consistent, each catching a different instant, and the skew surfaces only when you restore one side. Roll &lt;code&gt;billing&lt;/code&gt; back to 04:00 while &lt;code&gt;orders&lt;/code&gt; keeps its afternoon of writes and every cross-schema join now compares two timelines. No constraint exists to fail, so nothing errors; the afternoon&amp;rsquo;s orders point at invoices the restore erased. (Postgres PITR replays WAL for the whole cluster, never one database, so an independent restore was never on offer.) The honest unit of &lt;a class="link" href="https://explainanalyze.com/p/before-the-bad-write/" &gt;backup and restore&lt;/a&gt; is the set of databases your queries treat as one, and four hundred joins say that set has two members.&lt;/p&gt;
&lt;h2 id="find-the-joins-before-you-need-the-split"&gt;Find the joins before you need the split
&lt;/h2&gt;&lt;p&gt;The coupling lives in query text, not the catalog: no &lt;code&gt;pg_constraint&lt;/code&gt; row, no &lt;code&gt;KEY_COLUMN_USAGE&lt;/code&gt; entry. But it always leaves a fingerprint, because at least one table in the query has to be namespace-qualified. On a running MySQL server, the digest table preserves the qualifiers:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;div class="chroma"&gt;
&lt;table class="lntable"&gt;&lt;tr&gt;&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code&gt;&lt;span class="lnt"&gt;1
&lt;/span&gt;&lt;span class="lnt"&gt;2
&lt;/span&gt;&lt;span class="lnt"&gt;3
&lt;/span&gt;&lt;span class="lnt"&gt;4
&lt;/span&gt;&lt;span class="lnt"&gt;5
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class="lntd"&gt;
&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;SCHEMA_NAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;DIGEST_TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;COUNT_STAR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;SUM_ROWS_EXAMINED&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;performance_schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;events_statements_summary_by_digest&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;SCHEMA_NAME&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;orders&amp;#39;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AND&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;DIGEST_TEXT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;LIKE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;%billing.%&amp;#39;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;ORDER&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;BY&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;COUNT_STAR&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Two caveats: &lt;code&gt;DIGEST_TEXT&lt;/code&gt; truncates at &lt;code&gt;performance_schema_max_digest_length&lt;/code&gt; (1024 bytes by default), and the digest table resets on restart, so it shows recent traffic, not history. Pair it with a grep of the repos (application code, ORM models, dbt sources) for the &lt;code&gt;billing.&lt;/code&gt; qualifier. Datadog ran this inventory across 30-plus teams when they &lt;a class="link" href="https://www.datadoghq.com/blog/engineering/unwinding-shared-database/" target="_blank" rel="noopener"
 &gt;unwound a shared database in 2025&lt;/a&gt;; mapping the cross-boundary queries came before any carving. In Postgres the hunt is easier: the cross-instance dependency is already wearing a &lt;code&gt;postgres_fdw&lt;/code&gt; label, listed in &lt;code&gt;pg_foreign_table&lt;/code&gt;, with query text in &lt;code&gt;pg_stat_statements&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="what-the-fixes-cost"&gt;What the fixes cost
&lt;/h2&gt;&lt;p&gt;The cheapest move is writing the coupling down where a machine can read it: &lt;code&gt;COMMENT 'references billing.invoices.order_id, service-owned, no FK across instances'&lt;/code&gt; on the column. Readable by humans and by &lt;a class="link" href="https://explainanalyze.com/p/foreign-keys-are-not-optional/" &gt;schema-reading assistants&lt;/a&gt;, enforced by nothing. If the boundary has to actually hold, a scheduled reconciliation job anti-joins the two sides and reports the orphans the missing FK would have rejected at write time (&lt;a class="link" href="https://explainanalyze.com/p/scale-the-pattern-not-the-instance/" &gt;the scale-the-class fix&lt;/a&gt; applied to integrity). Eventually-consistent by construction, and someone has to own the alert. For the analytics joins, a consolidated replica (logical replication or CDC into one reporting instance) makes the join legal again, at the price of a pipeline to run and a lag window to reason about. For the transactional joins, the durable answer is the one Datadog and the &lt;a class="link" href="https://docs.aws.amazon.com/prescriptive-guidance/latest/database-decomposition/joins.html" target="_blank" rel="noopener"
 &gt;AWS decomposition guidance&lt;/a&gt; both land on: replace the query with a service call. That&amp;rsquo;s the real unwind and the expensive one, a rewrite of every call site the join touched.&lt;/p&gt;
&lt;p&gt;When the split itself arrives, replication makes it survivable, and the order of operations matters more than the tooling. Write-path joins and cross-schema transactions get rewritten first, while everything still shares a server; after cutover there is no atomicity across the boundary. Then the future cluster comes up as a replica of the whole instance, &lt;code&gt;billing&lt;/code&gt; reads migrate at leisure, and writes cut over in a short freeze once GTIDs match. &lt;a class="link" href="https://github.blog/engineering/infrastructure/partitioning-githubs-relational-databases-scale/" target="_blank" rel="noopener"
 &gt;GitHub ran this playbook in 2021&lt;/a&gt;: SQL linters to stop new cross-boundary queries, then a write-cutover that moved 130 tables with sub-second downtime. Read joins stay gradual. A multi-source channel keeps feeding &lt;code&gt;orders&lt;/code&gt; into the new cluster read-only, so stragglers join against a lagged copy while the digest counts drain to zero; give that channel a deadline, it&amp;rsquo;s the weld on life support. One trap: a read join that feeds a write decision (check invoice status, then update the order) is a cross-schema transaction wearing a SELECT, and the lagged copy turns it into a race. The digest table won&amp;rsquo;t flag it; only the call sites do.&lt;/p&gt;
&lt;p&gt;One more consumer of the missing metadata: an AI assistant scoped to one connection (the default MCP posture) either declares the column unjoinable or, handed a shared server, cheerfully reproduces the free cross-schema join with no signal that it just crossed a service boundary. The column comment is the only place the relationship exists in a form the model reads.&lt;/p&gt;
&lt;p&gt;The join that costs nothing to write is the one that costs the most to remove. Every &lt;code&gt;schema.table&lt;/code&gt; qualifier typed without a second thought is a vote to keep both schemas on one server forever, cast by someone who had no idea there was an election.&lt;/p&gt;</description></item><item><title>How Teams Actually Finish What They Start, Part V: The Sprint as a Working Set</title><link>https://explainanalyze.com/p/how-teams-actually-finish-what-they-start-part-v-the-sprint-as-a-working-set/</link><pubDate>Tue, 21 Apr 2026 00:00:00 +0000</pubDate><guid>https://explainanalyze.com/p/how-teams-actually-finish-what-they-start-part-v-the-sprint-as-a-working-set/</guid><description>&lt;img src="https://explainanalyze.com/" alt="Featured image of post How Teams Actually Finish What They Start, Part V: The Sprint as a Working Set" /&gt;&lt;div class="tldr-box"&gt;
 &lt;strong&gt;TL;DR&lt;/strong&gt;
 &lt;div&gt;For teams whose week is shaped by inbound work, the sprint should hold only what is being worked on now plus what gets pulled next. No forward estimates, no velocity commitments. Priority lives in labels; the team pulls from the labeled backlog as in-flight work completes.&lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;Tuesday at 2pm. Sprint planning. The team has been here for ninety minutes. Eighteen tickets on the board, points being argued about (was this a 5 or an 8 last quarter?). A senior engineer flags they have to leave for an interview at 3. The product manager wants to commit to 42 points so the velocity curve in the leadership deck stays smooth. Three operational tickets came in during the meeting itself. Nobody has touched code today. The sprint will start tomorrow with eighteen tickets the team has not properly looked at, plus the three that arrived during planning, plus whatever arrives over the next two weeks. The planning meeting was the work today.&lt;/p&gt;
&lt;h2 id="better-grooming-doesnt-fix-it"&gt;Better grooming doesn&amp;rsquo;t fix it
&lt;/h2&gt;&lt;p&gt;The standard fixes target the planning meeting: better grooming, T-shirt sizing instead of points, async estimation in Slack. Each saves twenty minutes and leaves the underlying mistake intact. The mistake is the sprint trying to be a committed plan for two weeks of work the team has not done yet, on a team whose two weeks are not predictable. No amount of grooming makes the unpredictable predictable. The fix is a sprint that admits it.&lt;/p&gt;
&lt;h2 id="the-sprint-as-a-working-set"&gt;The sprint as a working set
&lt;/h2&gt;&lt;p&gt;A different mode: the sprint holds only what is being worked on right now, plus the highest-priority items the team will pull next. That is it. The sprint stops being a two-week forecast or a velocity commitment. The board reflects reality rather than narrating it.&lt;/p&gt;
&lt;p&gt;The mechanics follow. The backlog holds everything, labeled by priority. The manager keeps the labels current, and high-priority items rise to the top of the filtered view. The sprint holds only tickets currently in progress plus immediate next pulls. Engineers pull from the labeled backlog into the sprint as their current work completes. There is no forward estimation at planning, because points are written post-hoc (see &lt;a class="link" href="https://explainanalyze.com/p/how-teams-actually-finish-what-they-start-part-iv-point-after-the-fact/" &gt;Part IV&lt;/a&gt;). Planning becomes a short check-in: the team confirms priorities, surfaces blockers, and returns to work.&lt;/p&gt;
&lt;p&gt;Engineers file their own tickets when they discover work. A bug found while shipping a feature. A refactor that surfaces during code review. A dependency that needs chasing. The IC who found it writes the ticket. &amp;ldquo;Someone will write this up later&amp;rdquo; becomes nobody, and the work disappears from the tracker without disappearing from reality. The team&amp;rsquo;s tracker has to hold the team&amp;rsquo;s actual work; if the work is not in the tracker, the work does not exist for planning purposes.&lt;/p&gt;
&lt;p&gt;The responder rotation absorbs incoming interruption tickets (&lt;a class="link" href="https://explainanalyze.com/p/how-teams-actually-finish-what-they-start-part-iii-a-working-responder-rotation/" &gt;Part III&lt;/a&gt;) so the sprint is not churned by every Slack message and every cross-team request. The sprint is what the team is building. The responder column is what arrives. The two queues stay separate, and the sprint stays small.&lt;/p&gt;
&lt;div class="warning-box"&gt;
 &lt;strong&gt;The sprint is not a copy of the backlog&lt;/strong&gt;
 &lt;div&gt;The pressure to grow the sprint is constant. Leadership wants velocity numbers, the team wants to look ambitious, every new priority feels like it should land &amp;ldquo;in this sprint.&amp;rdquo; It should not. The sprint is what the team is doing now plus what they will pull next. If the sprint contains tickets nobody has looked at, the discipline has slipped, and the velocity that comes out the other side is fiction.&lt;/div&gt;
&lt;/div&gt;

&lt;h2 id="in-the-tracker"&gt;In the tracker
&lt;/h2&gt;&lt;p&gt;Jira gives you priority fields, labels, components, ranks, epics, and themes. The working-set sprint needs three things from the tracker: a backlog the manager can prioritize, a way to see the labeled top of the backlog, and a sprint board that shows what the team is doing right now. The rest is decoration.&lt;/p&gt;
&lt;p&gt;A workable setup: priority lives on a single field or a single label, picked once and used consistently. A saved filter (JQL or board view) shows the labeled high-priority backlog, and that filter is the team&amp;rsquo;s entry point when their current ticket closes. The sprint board shows in-progress and next-up tickets only. Standups walk that board ticket by ticket. Estimation columns are optional; if used, they are filled in after the ticket closes, not before.&lt;/p&gt;
&lt;div class="note-box"&gt;
 &lt;strong&gt;Use one priority mechanism, not three&lt;/strong&gt;
 &lt;div&gt;Jira lets you mix priority field, labels, components, and rank order. Pick one. A label like &lt;code&gt;priority:p0&lt;/code&gt; works. So does the built-in priority field. Mixing them means engineers pull from one filter while the manager updates another, and the team works on the wrong tickets while the tracker says everything is fine.&lt;/div&gt;
&lt;/div&gt;

&lt;h2 id="when-forward-sprints-work"&gt;When forward sprints work
&lt;/h2&gt;&lt;p&gt;A team with a stable, well-scoped backlog and predictable interruptions can run forward sprints with point commitments. Some maintenance teams have this. Some platform teams whose remit has been narrowed and frozen do too. For everyone else, the working-set sprint is the one that matches reality. Part IV&amp;rsquo;s measurement discipline is how the team finds out which side it is on; the working-set sprint is what the team does with the answer.&lt;/p&gt;
&lt;h2 id="what-changes"&gt;What changes
&lt;/h2&gt;&lt;p&gt;Sprint planning gets short. Velocity stops being a fiction the leadership deck has been running on. With the board reflecting actual work, the rest of the cadence gets honest too. Standups walk the board ticket by ticket and finish in fifteen minutes. Retros talk about what actually happened, not about the gap between estimate and reality. And reviews compare engineers on the same shapes of work, with the data Part IV produced. The tracker stops being a stage for the planning ritual and becomes a tool for getting work done.&lt;/p&gt;</description></item><item><title>How Teams Actually Finish What They Start, Part IV: Point After the Fact</title><link>https://explainanalyze.com/p/how-teams-actually-finish-what-they-start-part-iv-point-after-the-fact/</link><pubDate>Fri, 03 Apr 2026 00:00:00 +0000</pubDate><guid>https://explainanalyze.com/p/how-teams-actually-finish-what-they-start-part-iv-point-after-the-fact/</guid><description>&lt;img src="https://explainanalyze.com/" alt="Featured image of post How Teams Actually Finish What They Start, Part IV: Point After the Fact" /&gt;&lt;div class="tldr-box"&gt;
 &lt;strong&gt;TL;DR&lt;/strong&gt;
 &lt;div&gt;Forward estimation breaks for any team whose week is shaped by work that arrives. The discipline that scales is to point after the fact: when the ticket closes, the person who did the work writes down what it took. Over a quarter the team has real data about where time actually goes.&lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;Standup Monday. A 5-point ticket lands in the engineer&amp;rsquo;s column. Wednesday the engineer is still on it, two production fires deep, the original scope half-discovered. By Friday it ships. The retro looks at velocity. The 5 stays a 5, the team&amp;rsquo;s data says the engineer did 5 points of work, and the next sprint&amp;rsquo;s planning uses that data to size the next ticket of the same shape. The bug compounds.&lt;/p&gt;
&lt;h2 id="better-estimates-dont-fix-it"&gt;Better estimates don&amp;rsquo;t fix it
&lt;/h2&gt;&lt;p&gt;The obvious move is to estimate better: planning poker, three-point estimates, finer story points, more grooming up front. None of it works on a team whose week is shaped by inbound work. The variance is not in how the engineer reads the ticket. It is in what arrives between Monday and Friday. A ticket scoped honestly Monday gets eaten by an unrelated incident Wednesday. A 5-point ticket stays 5 points until the dependency the engineer didn&amp;rsquo;t know about turns it into 13. Forward estimation is trying to predict the team&amp;rsquo;s week, and the team&amp;rsquo;s week is not predictable.&lt;/p&gt;
&lt;h2 id="point-what-you-did"&gt;Point what you did
&lt;/h2&gt;&lt;p&gt;The fix is mechanical. When the ticket closes, the person who did the work writes down what it took. Over a quarter the team has real data: which categories of work consume the most time, which engineers carry which kinds of load, where the same shape of problem keeps eating a day each time. The cost is near zero (the ticket is closed; the person who did the work is sitting there). Bottlenecks surface fast: a category that always takes three times what its siblings take is a place to invest, and the data makes it visible without anyone having to argue for it.&lt;/p&gt;
&lt;p&gt;The rule has to be load-bearing in the workflow, not aspirational. A ticket cannot move to Done without a points value. Without that constraint, the data has gaps, and the gaps are not the random kind.&lt;/p&gt;
&lt;div class="warning-box"&gt;
 &lt;strong&gt;Half-pointed data is worse than no data&lt;/strong&gt;
 &lt;div&gt;Without a load-bearing constraint, easy tickets get pointed and hard tickets get closed in a rush. The long-tail work the team most needs to see disappears from the record. The team trusts the partial data anyway and reaches the wrong conclusions about its own capacity.&lt;/div&gt;
&lt;/div&gt;

&lt;h2 id="what-the-data-enables"&gt;What the data enables
&lt;/h2&gt;&lt;p&gt;Operation-heavy teams can drop forward sprint commitments entirely. The manager sets priorities through labels on tickets and epics. The team pulls from the top of the labeled backlog as engineers free up. After-the-fact points accumulate over the quarter and give the team a real baseline: capacity, distribution, ticket-shape patterns. Forecasting becomes a quarter-long view rather than a sprint-long commitment.&lt;/p&gt;
&lt;p&gt;The same data benchmarks people. Recurring work converges in shape over a quarter. Responder rotations cluster around the same handful of incident types. Refactors pattern-match to a few common shapes. When ten engineers have closed the same kind of ticket, the spread of points across people becomes visible. Reviews stop being a debate about effort and become a comparison against work everyone has done. The conversation shifts from &amp;ldquo;I think Alice ships fast&amp;rdquo; to &amp;ldquo;Alice closes the same shape of ticket in 6 points where the team median is 8.&amp;rdquo;&lt;/p&gt;
&lt;div class="note-box"&gt;
 &lt;strong&gt;Compare same shapes, not raw velocity&lt;/strong&gt;
 &lt;div&gt;The benchmark only works for like-for-like work. A frontend ticket and a database investigation are not on the same scale. Group tickets by shape (incident type, refactor pattern, investigation category) before comparing engineers, and ignore raw point totals across the team.&lt;/div&gt;
&lt;/div&gt;

&lt;h2 id="when-forward-estimation-works"&gt;When forward estimation works
&lt;/h2&gt;&lt;p&gt;Forward estimation works when the team&amp;rsquo;s work is genuinely stable: same shape every week, predictable interruptions, no unscoped depth. Some maintenance teams have this. Most product teams and most operational teams do not. The point of measuring after the fact is to find out which side your team is actually on, and to design the cadence around the answer.&lt;/p&gt;
&lt;h2 id="what-changes-over-a-quarter"&gt;What changes over a quarter
&lt;/h2&gt;&lt;p&gt;A team that points after the fact has a quarter&amp;rsquo;s worth of evidence about itself: which work consumes time, who is faster on what, where the bottlenecks live. A team that points before the fact has a quarter&amp;rsquo;s worth of guesses, refined and re-litigated each sprint. Retros become data-driven instead of opinion-driven. Reviews compare engineers against the same shapes of work. And when the team asks for headcount, the ask is grounded in a category that has been swallowing unbudgeted time, not a hunch.&lt;/p&gt;</description></item><item><title>The Hello-World Procurement Problem: Why LLM Tooling Gets Bought Wrong</title><link>https://explainanalyze.com/p/the-hello-world-procurement-problem-why-llm-tooling-gets-bought-wrong/</link><pubDate>Sun, 21 Dec 2025 00:00:00 +0000</pubDate><guid>https://explainanalyze.com/p/the-hello-world-procurement-problem-why-llm-tooling-gets-bought-wrong/</guid><description>&lt;img src="https://explainanalyze.com/" alt="Featured image of post The Hello-World Procurement Problem: Why LLM Tooling Gets Bought Wrong" /&gt;&lt;div class="tldr-box"&gt;
 &lt;strong&gt;TL;DR&lt;/strong&gt;
 &lt;div&gt;A CTO declares &amp;ldquo;full agentic&amp;rdquo; off a vendor demo. Without an SME watching the rollout, corruption ships and surfaces a year later when a customer reports a wrong number. With an SME, the work is information infrastructure first (so agents have enough context to make high-probability decisions) and guardrails for the cases where context isn&amp;rsquo;t enough.&lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;A CTO sits through a vendor demo. A sales engineer types &amp;ldquo;show me the top ten customers by revenue last quarter&amp;rdquo; into a prompt and a working SQL query materializes in 30 seconds, runs against a sample dataset, returns plausible numbers. The CTO declares the company is going full agentic. Procurement closes the contract by Friday.&lt;/p&gt;
&lt;p&gt;Procurement closes on Friday. The first agents reach production Tuesday. By the end of the quarter, three internal dashboards are running LLM-generated SQL, two of them against the company&amp;rsquo;s order data. The SQL passes the lint check (&lt;code&gt;EXPLAIN&lt;/code&gt; runs, the result set has the right columns), and the dashboards display plausible numbers. Whether the numbers match what they should be is a question nobody on the team is positioned to answer.&lt;/p&gt;
&lt;h2 id="without-smes"&gt;Without SMEs
&lt;/h2&gt;&lt;p&gt;If the agent-generated SQL looks like gold to everyone in the room, the Rounders rule applies: if you can&amp;rsquo;t spot the sucker in your first half hour at the table, you are the sucker. Without someone in the room who&amp;rsquo;d catch the polysemic &lt;code&gt;tier&lt;/code&gt; column or the undocumented soft-delete convention buried in three tables, the team is approving plausibility on a system optimized to produce it.&lt;/p&gt;
&lt;p&gt;If the produced code looks good to you, you&amp;rsquo;re probably not the SME.&lt;/p&gt;
&lt;p&gt;The corruption rate observed in the demo is a lower bound for what the tool produces against real data, &lt;a class="link" href="https://explainanalyze.com/p/corruption-is-a-feature-not-a-bug-why-llms-corrupt-by-design/" &gt;often by a multiple&lt;/a&gt;. The realities catalogued in &lt;a class="link" href="https://explainanalyze.com/p/what-ai-gets-wrong-about-your-database/" &gt;What AI Gets Wrong About Your Database&lt;/a&gt; (undocumented conventions, polysemic columns, business logic in tribal knowledge, ten-year-old codebases with three &amp;ldquo;current&amp;rdquo; patterns) are exactly the regions of input space where the model&amp;rsquo;s training distribution is sparse and contradictory. Demos run in the dense-distribution sweet spot. Production runs the inverse on every axis.&lt;/p&gt;
&lt;p&gt;With nobody positioned to measure the gap, nothing flags it. Corruption is silent by construction. It doesn&amp;rsquo;t surface as one identifiable bug; it surfaces as drift across many places at once, traced back to LLM-generated code or queries whose authors can&amp;rsquo;t reconstruct what the model meant. By the time the rate is visible, the corruption has been propagating for weeks or months. The team has too many simultaneous issues to triage one at a time. Backups have rolled past the worst of the window.&lt;/p&gt;
&lt;p&gt;The detection mode is external. A customer reports a number that doesn&amp;rsquo;t match what they expected. An analyst running LLM-powered queries on the company&amp;rsquo;s data publishes a report that contradicts internal numbers. A regulator asks a question and the answer doesn&amp;rsquo;t match the previous quarter&amp;rsquo;s filing. Whatever surfaces it, the failure is now a public one, and the team learning the failure mode is the same team trying to contain it.&lt;/p&gt;
&lt;h2 id="with-smes"&gt;With SMEs
&lt;/h2&gt;&lt;p&gt;The CTO&amp;rsquo;s declaration doesn&amp;rsquo;t change. The job changes. With an SME watching the rollout, the work is infrastructure first.&lt;/p&gt;
&lt;p&gt;Agents make high-probability decisions when their inputs are dense. That means the schema is documented, polysemic columns are tagged, conventions are written down somewhere the model can reach, the dataset the agent runs against mirrors production rather than a curated subset. The realities that make a mature codebase mature (patterns evolved over years, decisions encoded in column names, exceptions buried in tribal knowledge) are exactly the inputs the agent doesn&amp;rsquo;t have unless someone puts them there. The SME&amp;rsquo;s first job is documenting what currently lives in heads. Without that, the agent operates in the sparse regions of its training distribution, and the floor on its corruption rate stays high regardless of how the harness is tuned.&lt;/p&gt;
&lt;p&gt;Guardrails are the second piece, for the cases where dense inputs still aren&amp;rsquo;t enough. Decompose work into chunks small enough to verify. Route checkpoints between chunks to the SME whose domain it is. Audits produce a failure-rate number against ground truth, not a yes/no. Recovery drills test rolling back six months of LLM-generated changes, because that&amp;rsquo;s the realistic detection horizon for silent corruption. The point is to catch the cases where the agent&amp;rsquo;s confidence and its accuracy are decoupled, which is where most of the corruption lives.&lt;/p&gt;
&lt;p&gt;Both pieces have to be in place before the deployment goes wide. Once the rate is visible from outside, the SME bench is already triaging incidents instead of building infrastructure, and the architecture won&amp;rsquo;t grow either piece on its own.&lt;/p&gt;
&lt;h2 id="when-this-doesnt-apply"&gt;When this doesn&amp;rsquo;t apply
&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Small teams.&lt;/strong&gt; The buyer is the SME, or one degree away. The infrastructure question gets answered by the same person making the rollout call.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Bounded, low-stakes use cases.&lt;/strong&gt; Personal productivity tooling, draft generation, internal-only knowledge work where corruption is recoverable.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Mature vendor categories.&lt;/strong&gt; Office suites, established CI/CD platforms, well-trodden CRM tooling. The failure modes are known and the buyer has reference points. New categories are where the asymmetry lives, and that&amp;rsquo;s exactly where LLM tooling sits in 2026.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="the-bill-arrives-later"&gt;The bill arrives later
&lt;/h2&gt;&lt;p&gt;The productivity dividend the CTO booked off the demo is real, in the sense that the deal closed and the harness shipped. The bill arrives a quarter or two later when a customer surfaces a number that doesn&amp;rsquo;t match the books, the auditor asks how the model arrived at it, and the team learns the failure mode in public instead of in QA.&lt;/p&gt;</description></item></channel></rss>