How do you test data quality in SQL Server without writing T-SQL by hand?

Generate the tests from a data profile instead of writing them. A tool such as DataOps TestGen reads every column in your SQL Server tables, records what normal looks like, and writes the checks itself: nulls, uniqueness, valid values, patterns, ranges, and freshness. You review them and add only the business rules no profile can infer.

Key points

  • Hand-written T-SQL checks don't scale: in DataKitchen's benchmark, covering 20 tables and 1,000 columns came to 2,526 tests, about 1,250 hours of work at 30 minutes per hand-written test.
  • DataOps TestGen profiles 55 characteristics per SQL Server column and generates tests from 34 test types, setting each threshold from the profile.
  • Business rules a profile can't see, such as 'ship date is on or after order date', come from 19 business-rule and custom SQL test types that you configure without writing the surrounding test harness.
  • Every generated test runs as T-SQL inside SQL Server through a read-only SQL login; TestGen never runs INSERT, UPDATE, DELETE, CREATE, ALTER, or DROP.
  • Open-source TestGen is free for one user, one connection, and one project, with unlimited tables.

The usual way to test SQL Server data is a folder of T-SQL scripts. Someone writes SELECT COUNT(*) WHERE CustomerID IS NULL for the three tables that broke last quarter, schedules them in SQL Server Agent, and the other 300 tables go untested.

The scripts aren’t wrong. They just never get finished.

Why hand-written checks stall

DataKitchen’s Purview benchmark counted what full coverage takes: 20 tables and 1,000 columns came to 2,526 tests at about 2.5 per column. At 30 minutes per hand-written test, that’s about 1,250 hours. Most SQL Server databases are wider than 20 tables.

Hand-written checks also only test what someone thought of. A column that has always been 100% populated has no null check, because nobody expected it to break.

Generate tests from a data profile

The alternative is to let the data tell you what normal is:

  1. Data profiling. Read every column once: null share, distinct values, min and max, lengths, patterns, frequent values.
  2. Generation. Turn each observed fact into a test with a threshold. A column that was never null gets a null test. A column with five distinct codes gets a valid-value test.
  3. Review. Deactivate what doesn’t matter, tighten what does.
  4. Add business rules. Write only what the data can’t show.

Here’s one column, both ways:

Hand-writtenGenerated
Null check on Orders.CustomerIDSELECT COUNT(*) FROM dbo.Orders WHERE CustomerID IS NULL; plus a threshold, a schedule, and an alertRequired Entry test, generated because the profile found no nulls
Valid Status codesWHERE Status NOT IN ('O','S','C','R'), updated by hand when a code is addedValue Match test from the profiled list
Duplicate ordersA GROUP BY ... HAVING COUNT(*) > 1 queryDuplicate Rows test on columns the profile picks as the key

A whole table, before and after

Before: the checks a careful engineer writes for a dbo.Orders table. Each query returns rows only when something is wrong.

-- Required customer
SELECT OrderID FROM dbo.Orders WHERE CustomerID IS NULL;

-- Unique order numbers
SELECT OrderNumber, COUNT(*) AS copies
FROM dbo.Orders
GROUP BY OrderNumber
HAVING COUNT(*) > 1;

-- Valid status codes
SELECT OrderID, Status FROM dbo.Orders
WHERE Status IS NULL OR Status NOT IN ('O', 'S', 'C', 'R');

-- No negative totals
SELECT OrderID, TotalDue FROM dbo.Orders WHERE TotalDue < 0;

-- Every order has a customer
SELECT o.OrderID
FROM dbo.Orders AS o
LEFT JOIN dbo.Customers AS c ON c.CustomerID = o.CustomerID
WHERE o.CustomerID IS NOT NULL AND c.CustomerID IS NULL;

-- Loaded in the last day
SELECT MAX(OrderDate) AS latest_order
FROM dbo.Orders
HAVING MAX(OrderDate) < DATEADD(day, -1, SYSDATETIME());

That’s six checks for one table, before the scheduling, history, and alerting around them. They cover the columns someone thought about.

After: one data profiling run on the same table, then test generation. TestGen generates tests like these when the profile supports them:

Hand-written checkGenerated test typeGenerated when the profile shows
Required customerRequired EntryNo nulls in CustomerID
Unique order numbersUnique ValuesEvery OrderNumber distinct, in a table over 500 rows
Valid status codesValue MatchA short list of Status values
No negative totalsMinimum ValueThe lowest TotalDue in the profile, used as the baseline
Loaded in the last dayRecencyRecent dates in OrderDate
Every order has a customerReference MatchNot generated: you pick the two tables and columns

Review what comes back. Minimum Value fails below the profiled minimum, so if the profile already found a negative total, set the threshold to 0 yourself. The columns nobody wrote a check for get tests too, such as Pattern Match on a formatted order number or Percent Missing on an optional column. The orphaned-customer check stays yours, because a profile of one table can’t know which other table it should match.

How DataOps TestGen does it

TestGen connects to SQL Server with a read-only SQL login, profiles 55 characteristics per column, and flags 32 kinds of hygiene issue, such as placeholder blanks and dates stored as text. Test generation picks from 34 test types and sets thresholds from the latest profile. Every test runs as T-SQL inside SQL Server; you never see it unless you open the query.

For rules a profile can’t infer, TestGen has 19 business-rule and custom SQL types. Reference Match checks that values exist in another table; Aggregate Balance checks that grouped totals match a reference table. You fill in the tables and columns. The test harness, history, and alerting already exist.

Then schedule it. Runs can start on a TestGen schedule or from SSIS, SQL Server Agent, or any scheduler through the REST API. See adding checks after an SSIS package.

The SQL Server setup answer has the grants and a walkthrough. Open source is free for one user, one connection, and one project. Start at the SQL Server install page.

Frequently asked questions

Short answers to the follow-up questions people ask next.

What tests can be generated from a SQL Server data profile?

Anything a profile can measure. In TestGen that includes required entries, unique values, duplicate rows, valid value lists, patterns, minimum and maximum values and dates, text length ranges, recency, and shifts in averages or distinct-value share. Thresholds come from the profile, so a column that has never been null gets a test that it stays that way.

Which tests do I still have to write?

The ones that encode business knowledge. A profile can't know that a ship date must fall on or after its order date, or that two tables must balance. TestGen covers those with 19 business-rule and custom SQL test types, such as Reference Match and Aggregate Balance, which you configure by filling in columns and tables.

Will generated tests be too noisy?

Some will, at first. Review the generated suite before you schedule it, deactivate tests on columns that legitimately vary, and loosen thresholds where the profile caught an unusual day. Generated tests are a starting suite you edit, not a verdict. Re-profile after big loads so thresholds follow the data.

Does generated testing work on large SQL Server tables?

Yes, with care on busy servers. TestGen runs aggregate queries in SQL Server, and data profiling can sample large tables for pattern and frequent-value analysis. DataKitchen's hotel reservation case study, on a 1.2 TB SQL Server database, set TestGen to one or two threads and raised the count once runs were stable.

Try it on your own data

DataOps TestGen is free and open source. Profile a table and generate its tests this afternoon.