Batting 6th

281, THEN 35, THEN 44

Where does the fan-out hide?

You know the fan-out. Two tables, a join on the wrong grain, and suddenly revenue's triple what it should be. Nobody can say why the dashboard total doesn't match the report.

Tableau's relationship model was built to stop that. A semantic model in Data 360 is the same idea with agents standing on top, and it fanned out on me anyway. Three times. Three different numbers.

Data 360 Snowflake Databricks Google Cloud Storage Salesforce CLI

The game. Tuesday, July 9, 2002. The All-Star Game in Milwaukee. Both managers tried to get everybody in, and by the 11th both bullpens were empty: ten pitchers for the National League, nine for the American. 7–7. The commissioner called it a tie, and 41,871 people booed. I kept score. Ten future Hall of Famers played that night, the most of any game I've seen: six hitters and four pitchers. Count them the way my first measure did, batters only, and you get six. Which players count is the whole question here, and it's the pitchers who go missing.

Data 360 Semantic Model Builder: the Keeping Score model, seven objects and five relationships
The Keeping_Score model in Data 360: seven objects, five relationships. Plays reaches the Hall of Fame only through Lahman People, joined on the batter, so pitchers never get in: that's the 35. HOF Sightings stands alone, and it already knows batters and pitchers both count: that's the 44.

#Three answers

The question was “how many Hall of Famers have I seen play?” The contract's answer is 44: every inducted batter or pitcher who appeared in a game I attended. The first measure I wrote in Data 360 joined People to the Hall of Fame table and came back 281. Every inducted player in history. Nothing in the formula forced the path through my games, so the engine didn't walk it. A measure's query joins only the objects its expression names.

Add Plays to the formula and it drops to 35. Better, and still wrong. The graph has one edge from People to Plays, the batter, so pitchers never get in. The 44 comes from a pre-built sightings table that already knows batters and pitchers both count. I kept the 35 in the model as its own measure, right next to the 44, so the lesson stays measurable. Corollary found the same day: two metrics from unrelated fact tables can't share one query. The engine throws NO_PATH, and Tableau Next inherits the same limit, because it's the same model.

The same cross-table formula that Data 360 fanned out to 281, Tableau Cloud refused outright. “Only one upstream base table.” Different engines make different bets about whether to stop you or trust you. Neither one knows what a Hall of Famer sighting is. That's yours.

#How it works

The whole layer was built into a fresh scratch org from scripts: two connections, Snowflake by key-pair and Databricks by token, seven data streams reading the data where it sits, one model with seven objects and five relationships and the seven measures. Verified seven of seven through the semantic engine on 2026-09-04. The docs said the connectors needed the browser. They didn't. Two Setup toggles were the only clicks in the sprint.

Then the third cloud. BigQuery's connector is walled in that org by an entitlement no API would name, so the marts were exported to Parquet in Google Cloud Storage and ingested through the storage connector: seven more streams, a second model. Ingest, not zero-copy. Two zero-copy legs and one copy. Three clouds in one org.

The ingest leg found its own bug. A total-replace load keyed on player id kept one Hall of Fame ballot per player, 1,543 of 6,426 rows, and the graph measure silently read 24 instead of 35. Fix: a composite ballot key in the export. The number that told me was the parity harness, which is the only reason the wrong 24 lasted a day instead of a quarter.

#From the shoebox

Ticket: the 2002 All-Star Game, A.L. vs. N.L. at Miller Park, Tuesday July 9, 2002, 7pm, section 436, row 21, seat 24, $125
Section 436, row 21, seat 24. $125, tax included. The scorecard from this night spent years in a frame and is somewhere it shouldn't be; the ticket is the receipt that I was in the building for the tie.
2002 All-Star Game official program, Robin Yount on the cover, $10
The official program. Robin Yount, the first Brewers Hall of Famer, on the cover. A Hall of Famer who wasn't on the field that night, which is exactly the kind of thing the 281 counted.

#Try this

Before you trust a count from a semantic model, ask it for one you know is wrong: every inducted player, not just the ones you saw. If both come back the same, the path isn't being walked.

#What it taught me

  1. The fan-out hides in the path the engine chose not to walk. 281 wasn't a join that doubled rows; it was a join that never happened. Your relationship-model instincts transfer exactly. What's new is that an agent will state the 281 in a full sentence, and nobody downstream is looking at a join diagram.
  2. Keep the wrong numbers. The 35 still sits in the model next to the 44, on purpose. A lesson you can't reproduce is just a story.
  3. The GUI floor wasn't a floor. Connections, streams, objects, model, all from the CLI. When a doc says something needs the browser, measure it.

Next, batting 7th: Do the numbers survive the move? →