Blog Details


Oops image is missing

Posted On Aug. 26, 2026, 1:29 p.m.

Why you should profile a warehouse's live schema, row counts, and cardinality with T-SQL before writing a single data agent instruction.

By the time you're ready to write data source instructions, it's tempting to work from memory: you know this warehouse, you built half of it. That's exactly the assumption worth checking before you write anything down for the agent to rely on, because "I know this schema" and "this is what the schema actually contains right now" drift apart faster than anyone expects, especially on a warehouse that's been in production for a year with three people touching it.

Why profiling comes before instructions, not after

Every later part of this series (business terms, joins, time intelligence, example queries) is you writing down claims about the data: this column means X, these tables join on Y, dates in this table only go back to Z. Every one of those claims is either grounded in something you actually queried, or it's a guess wearing the confidence of a fact. Profiling is the step where you replace guesses with queries, and it's cheap: an hour of SELECT statements against INFORMATION_SCHEMA catches problems that would otherwise surface as a data agent confidently returning a wrong number.

Start with the schema itself

INFORMATION_SCHEMA is standard T-SQL and works the same way here as anywhere else. Two views cover most of what you need to start:

SELECT table_schema, table_name, table_type
FROM INFORMATION_SCHEMA.TABLES
ORDER BY table_schema, table_name;


SELECT table_name, column_name, data_type, is_nullable
FROM INFORMATION_SCHEMA.COLUMNS
WHERE table_name = 'FactSales'
ORDER BY ordinal_position;


This alone answers questions people usually guess at: which tables actually exist, what schema they live in, what's nullable, and what the real data types are. varchar columns holding what should be numbers, or a datetime2 column you assumed was just a date, show up here before they show up as a confusing agent answer three weeks from now.

It's also the fastest way to catch a mismatch between your mental model and the current state of the warehouse: a table someone renamed, a column someone added last sprint, or a table you thought was still in use that's actually been superseded by a newer one nobody dropped. None of that is visible from memory. All of it is visible from two SELECT statements.

Row counts and freshness, per table

Once you know what tables exist, get a row count and a date range for anything with a date column. There's no universal shortcut here that's safe to assume across every engine and configuration, so the reliable version is also the boring version: query each table directly.

SELECT COUNT(*) AS row_count
FROM FactSales;

 

SELECT
    MIN(order_date) AS earliest_date,
    MAX(order_date) AS latest_date
FROM FactSales;

Run this across every fact table you're planning to expose. You're looking for two kinds of surprises: tables with far fewer rows than you expected (a load that's been silently failing), and date ranges that don't start or end where you assumed (a table that only has two years of history, or one that stopped updating four months ago). Both are the kind of thing a data agent will cheerfully compute a confident, wrong answer against, because a query engine has no way to know your data is stale, only that it ran successfully.

Cardinality: the check that catches join problems early

Cardinality, how many distinct values a column has relative to its row count, is the profiling step people skip most often and need most. A quick pass:

SELECT
       COUNT(*) AS total_rows,
    COUNT(DISTINCT customer_id) AS distinct_customers,
    COUNT(DISTINCT order_id) AS distinct_orders
FROM FactSales;

If distinct_orders is meaningfully lower than total_rows on a table you assumed was grain-level-one-row-per-order, you've just found a fan-out waiting to happen. That's the exact failure mode covered in Part 12: a join that silently multiplies rows, and every measure built on top of it, by however many duplicate rows exist per key. Catching it here, with a COUNT(DISTINCT ...), costs one query. Catching it after the agent is live costs a very awkward conversation about why last quarter's revenue suddenly doubled.

Diagram (flow)

·         Row counts +\ndate ranges per table  →  Cardinality checks\n(COUNT DISTINCT vs COUNT *

·         Cardinality checks\n(COUNT DISTINCT vs COUNT *  →  Grain matches\nwhat you assumed?

·         Grain matches\nwhat you assumed?  →  Fix the model, or document\nthe real grain honestly   [No]

·         Grain matches\nwhat you assumed?  →  Write data source\ninstructions in Part 5   [Yes]

Null rates deserve their own check

A column being nullable in INFORMATION_SCHEMA.COLUMNS tells you it's allowed to contain NULL. It doesn't tell you how often it actually does, and that gap matters a lot once an agent starts aggregating the column.

SELECT
   COUNT(*) AS total_rows,
   COUNT(discount_pct) AS non_null_rows,
   COUNT(*) - COUNT(discount_pct) AS null_rows
FROM FactSales;

A column that's NULL for 40% of rows behaves very differently in an AVG() than one that's NULL for 1%, since AVG() silently ignores NULLs rather than treating them as zero. If a business term in Part 6 is going to be defined as an average or a rate over a column like this, you want to know the null rate before you write that definition, not after someone asks why the average discount looks suspiciously generous.

A minimal profiling checklist

·         Every table you plan to expose has a known row count, not an assumed one.

·         Every table with a date column has a checked MIN/MAX, not an assumed range.

·         Every table's assumed grain (one row per what?) is confirmed with a COUNT(DISTINCT ...) check, not just a table or column name that sounds right.

·         Any column you plan to describe as a key, a flag, or a category has had its actual distinct values checked at least once (SELECT DISTINCT status FROM ...), since a "status" column with five undocumented values you've never seen is a common surprise.

·         Anything that surprised you during profiling is written down somewhere, even informally, before you move on. You will not remember it three parts from now.

Ground it in reality, not in your mental model

The uncomfortable trade-off here is time: profiling properly takes real effort on a warehouse with dozens of tables, and it's tempting to profile only the two or three tables you're sure you'll need first. That's a reasonable scope decision, as long as it's a decision and not an accident. Profile the tables you're actually going to describe to the agent in Part 5, thoroughly, rather than spreading a thin, incomplete pass across everything in the warehouse.

Key takeaways

·         Profiling replaces assumptions about the schema with queries you actually ran, and every later part of this series depends on those queries being right.

·         INFORMATION_SCHEMA.TABLES and .COLUMNS are the starting point for what actually exists, not what you remember building.

·         Row counts and date ranges catch stale or silently-failing loads before the agent surfaces them as a confident, wrong answer.

·         Cardinality checks (COUNT(DISTINCT ...) versus COUNT(*)) catch grain mismatches and fan-out joins while they're a one-query fix, not a production incident.

·         Scope profiling to the tables you'll actually describe to the agent next, and do that scope thoroughly rather than everything shallowly.

What's next

Next in the series: Writing a Data Source Description That Routes Correctly. With a profiled, ground-truthed schema in hand, we'll write the description that tells a multi-source agent when this warehouse is the right place to look.

0 comments

No comments yet. Be the first to comment!

Leave a comment

All Categories

Related Post


img missing

Posted On : Aug. 26, 2026, 1:29 p.m.

Read More
img missing

Posted On : Aug. 26, 2026, 11:54 a.m.

Read More
img missing

Posted On : July 25, 2026, 8:37 p.m.

Read More