Mixed-grain tables: diagnose and fix wrong agent answers with a GRAIN dimension and verified queries¶
An agent double-counts a table that holds daily and monthly rows. Diagnose it from wrong answers, fix the semantic view with a GRAIN enum, rules, and verified queries, re-test.
| Stage | Build, Evaluate |
| Topics | Semantic Views, Cortex Agents |
| Status | GA |
| Time to complete | 30min |
| Edition | Any edition with semantic views and Cortex Agents |
| Regions | Regions where Cortex Agents and Cortex Analyst are available |
| Privileges | ACCOUNTADMIN for the account statements in 01_setup.sql and for teardown; everything else runs as COOKBOOK_MIXED_GRAIN_AND_VERIFIED_QUERIES_ROLE |
| Related skills | semantic-view-patterns (Snowflake-Labs/coco-skills), agent-studio (bundled with Cortex Code) |
| Last verified | 2026-10-04 |
| Authors | Bala Krishnamoorthy |
| Code | View all files or download mixed-grain-and-verified-queries.zip, which includes this page as README.md. Teardown: sql/99_teardown.sql |
This recipe is a reference implementation: review the code and adapt it to your environment before production use.
Many summary tables store more than one grain side by side: daily rows next to monthly roll-ups, districts next to their region totals. Point a semantic view at one of these without saying so, and a Cortex Agent returns fluent, confident answers that are exactly twice the truth. This recipe reproduces that failure on a small fictional dataset, scores the agent on four questions (it gets 0 or 1 right), traces the wrong numbers to the SQL it wrote, and fixes the semantic view, not the agent and not the data, until the same agent gets all four right.
When to use this¶
- An agent or Cortex Analyst answer is off by a clean multiple (2x, 3x), or an average is skewed high.
- A table mixes row types (daily and monthly, detail and subtotal, account and account group) and the rows share keys and dates.
- You want a repeatable before and after test for a semantic view change instead of spot-checking in a chat.
When not to use it:
- You can change the data model. Splitting the grains into separate tables (or a view per grain) removes the trap entirely; do that when you can.
- The wrong numbers come from joins (fan traps, multiple paths). That is a relationship problem; see the
sv_diagnosticssnippet under Related skills.
How it works¶
- The trap.
STORE_SALES_SUMMARYhas 360 daily rows and 12 monthly rows that each equal the sum of their month. OnlyPERIOD_START = PERIOD_ENDtells them apart. A first-draft semantic view that callsPERIOD_START“sale date” invitesSUM(net_sales) WHERE sale_date BETWEEN ..., which adds every sale twice. - Test through the agent, score against the data. Expected answers are computed from the daily rows in setup.
The harness asks the agent each question with
DATA_AGENT_RUN, stores the answer and the SQL the agent wrote, and scores it: numbers within 0.5 percent, or anAI_COMPLETEjudge for the “which stores” question. - Fix the semantic view only. A
GRAINdimension withIS_ENUMnames the row types, grain rules inAI_SQL_GENERATIONsay which grain answers which kind of question, and a few verified queries show the right shape. The agent spec is never touched. - Verified queries that differ from the test. The verified queries use other stores and months than the test questions, so a passing re-test shows the rules generalize rather than that a stored query was replayed.
CREATE OR ALTERkeeps grants. The fix is applied in place; the analyst role’sSELECTgrant on the view is still there afterwards.
Prerequisites¶
| Requirement | Notes |
|---|---|
| Cortex Agents and Cortex Analyst | The recipe role gets SNOWFLAKE.CORTEX_USER. |
| ACCOUNTADMIN | Only for the account statements at the top of 01_setup.sql and for 99_teardown.sql. |
| Default role | Agents run with the calling user’s default role. Make sure it inherits COOKBOOK_MIXED_GRAIN_AND_VERIFIED_QUERIES_ROLE (setup grants the role to you and to SYSADMIN). |
| Judge model | claude-sonnet-4-6 scores the one list question (MODEL in 04 and 07). |
| Orchestration model | auto (AGENT_MODEL in 03). The script checks the name against a pattern before it goes into the agent specification. |
Quickstart¶
Run in order. Each script sets its own role, warehouse, and database, so separate snow sql -f calls work:
- sql/01_setup.sql: roles, warehouse, database, the mixed-grain table, and the test questions.
- sql/02_semantic_view_before.sql: the first-draft semantic view and an analyst grant.
- sql/03_agent_and_test_harness.sql: the agent and
RUN_AGENT_TESTS. - sql/04_test_before.sql: score the agent (a few minutes).
- sql/05_diagnose.sql: find the cause.
- sql/06_fix_semantic_view.sql: apply the fix with
CREATE OR ALTER. - sql/07_test_after.sql: re-score the same agent and compare.
- sql/99_teardown.sql.
Walkthrough¶
1. The questions and the right answers¶
| ID | Question | Correct answer |
|---|---|---|
| Q1 | Total net sales for Harbor Street in February 2026 | 35,713.00 |
| Q2 | Total net sales across all stores in Q1 2026 | 439,348.35 |
| Q3 | Average daily net sales for Canal Park in January 2026 | 1,543.90 |
| Q4 | Which stores missed their March 2026 sales target | Elm Square, Mill Road |
2. The symptom¶
Against the first-draft view, two test runs scored 1 of 4 and 0 of 4. Q1 and Q2 were wrong the same way both times; Q3 is from the first run and Q4 from the second:
| ID | Agent answer | Ratio to correct |
|---|---|---|
| Q1 | $71,426.00 | 2.00 |
| Q2 | $878,696.70 | 2.00 |
| Q3 | $2,991.30 | 1.94 |
| Q4 | “No stores missed their March 2026 sales target. All four stores exceeded their targets.” | wrong |
Nothing in the answers signals a problem. Q4 is the most damaging: doubled sales against single-counted targets make every store look like it beat plan. In the first run the agent noticed an empty result on Q4, investigated, and got it right, which is why a scored test beats a single spot check.
3. The diagnosis¶
05_diagnose.sql goes from symptom to cause. The ratios above are the first clue: a SUM that is off by exactly 2.00
is two grains added together. The SQL the agent wrote confirms it, with a date filter and no grain filter:
And the table itself shows the two grains, each summing to the full total:
| IMPLICIT_GRAIN | ROWS | NET_SALES | ROWS_WITH_TARGET |
|---|---|---|---|
| DAILY | 360 | 439,348.35 | 0 |
| MONTHLY | 12 | 439,348.35 | 12 |
4. The fix¶
06_fix_semantic_view.sql replaces the definition in place. The parts that matter for grain:
Also in the fix: the misleading sale_date name becomes period_start plus period_end, every fact and metric
comment states which grain it is valid in, and store and region get IS_ENUM sample values. VERIFIED_BY names a
team contact, not a person. Synonyms are limited to the two terms users actually use for the grain.
5. The re-test¶
The same agent, the same four questions, both test runs: 4 of 4. The agent’s SQL now filters on grain every time,
and picks the grain the rules call for:
None of the test questions matched a verified query word for word.
Verify¶
07_test_after.sql ends with a summary:
| RUN_LABEL | CORRECT | QUESTIONS |
|---|---|---|
| before | 0 or 1 | 4 |
| after | 4 | 4 |
and the side-by-side table shows each answer and the agent’s SQL. Also check that the grant survived:
Agent answers vary between runs, so treat a single run as a sample. The “after” result was 4 of 4 in every run while this recipe was built.
Gotchas¶
| Symptom | Cause | Fix |
|---|---|---|
| Answers off by exactly 2x (or Nx) | Two (or N) grains summed together | Add a GRAIN enum and rules; better, split the grains in the data model |
| Averages skewed high but not by a round number | Roll-up rows averaged together with detail rows | Rules: per-day averages use the detail grain only |
| “All stores beat target” | Doubled actuals compared with single-counted targets | Targets live on one grain; say so in the column comment and the rules |
| Fix passes only the exact verified questions | Verified queries doing the work, not the rules | Keep test questions different from verified queries, as here |
DATA_AGENT_RUN in a procedure fails with “needs to be constant” | The request body was built inline | Build the body into a variable, then pass :body |
| Agent calls fail with privilege errors | Agents use the user’s default role | Grant the recipe role to the default role |
| Grants disappear after a fix | CREATE OR REPLACE without COPY GRANTS | Use CREATE OR ALTER SEMANTIC VIEW |
Teardown¶
Adapt this recipe¶
Keep these, so a customized version keeps the recipe’s security model:
- Least privilege:
ACCOUNTADMINonly for the scripts listed under Privileges; everything else runs as the recipe’s own role. - No grants to
PUBLIC. - Caller’s rights: procedures run as
EXECUTE AS CALLER, so each user’s own grants apply. - Credentials: create keys, tokens, and secrets yourself, in a separate terminal, and read them from environment variables. Never put them in a script or give them to an agent.
- Dynamic SQL, if you add any, uses bind variables or an allowlist of identifiers, never text concatenated from user or model input.
Change these to fit your use case:
- Object names. Rename them in every script, including teardown, then check that teardown still removes everything.
- Roles, personas, and users: map them to your own, each granted no more than the recipe grants.
- Data: point the scripts at your tables instead of the sample data, then run Verify again.
- Models and warehouse sizes.
To have a coding agent adapt it:
Run it with a coding agent¶
Related skills¶
semantic-view-patterns(coco-skills): theai_metadatasnippet teachesAI_SQL_GENERATION,AI_QUESTION_CATEGORIZATION, andAI_VERIFIED_QUERIESsyntax; thesv_diagnosticssnippet covers other ways semantic views produce wrong numbers (fan traps, multiple paths). Apply mode adapts either to your own view.agent-studio(bundled): creating, auditing, and optimizing semantic views, verified queries, and agents.