How to analyze an A/B test with SQL
Max Musing
Max MusingFounder and CEO of Basedash
· August 21, 2026

Max Musing
Max MusingFounder and CEO of Basedash
· August 21, 2026

To analyze an A/B test with SQL, you need three things: one row per user per variant, a clear conversion definition, and a significance check. Count exposed users and conversions per variant, compare the conversion rates to get the lift, then run a two-proportion z-test to decide whether the difference is real or just noise. If the absolute z-score is above 1.96, the result is significant at the 95% confidence level. You do not need a dedicated experimentation platform to do this, but you do need to fix your sample size in advance and resist stopping the test the moment it looks like a winner.
This guide is for product managers, growth engineers, and analysts who have experiment data sitting in Postgres, BigQuery, or a warehouse and want to read the result themselves instead of waiting on a stats tool or a data scientist. It covers the exact SQL to get conversion rates and lift, how to check significance without Python or R, how to think about sample size, the mistakes that quietly turn noise into a “win,” and when a BI tool is enough versus when you should reach for a purpose-built experimentation platform.
Every A/B test analysis rests on two facts per user: which variant they saw, and whether they did the thing you care about. In warehouse terms that usually means two tables:
variant column (control or treatment) and a timestamp. This is the source of truth for who was in the test.The single most important rule is that the denominator is exposed users, not total users and not events. You measure the conversion rate among people who actually entered the experiment, and you count each user once. If you divide conversions by the wrong population, every number after that is wrong no matter how careful the statistics are.
Two data-quality checks belong here, before any math:
Users often have multiple assignment rows (repeated page loads, retries). Collapse them to a single exposure, keeping the first one, and flag anyone who was assigned to more than one variant so you can exclude them.
with assignments as (
select
user_id,
variant,
row_number() over (partition by user_id order by assigned_at) as rn,
count(distinct variant) over (partition by user_id) as variant_count
from experiment_assignments
where experiment_key = 'checkout_v2'
)
select user_id, variant
from assignments
where rn = 1
and variant_count = 1 -- drop users who landed in both arms
This CTE (exposed) is the population for the rest of the analysis. Everything downstream joins back to it.
Now join exposed users to conversions and aggregate. Use a left join so non-converters stay in the denominator, and dedupe conversions to one per user.
with exposed as (
-- the CTE from step 1
),
converted as (
select distinct user_id
from conversions
where converted_at >= timestamp '2026-08-01'
)
select
e.variant,
count(*) as users,
count(c.user_id) as conversions,
round(100.0 * count(c.user_id) / count(*), 2) as conversion_rate_pct
from exposed e
left join converted c on c.user_id = e.user_id
group by e.variant
order by e.variant;
You now have something like:
| Variant | Users | Conversions | Conversion rate |
|---|---|---|---|
| control | 8,000 | 640 | 8.00% |
| treatment | 8,000 | 720 | 9.00% |
The relative lift is (treatment_rate - control_rate) / control_rate. Here that is (9.00 - 8.00) / 8.00 = 12.5%. The absolute lift is one percentage point. Report both. A 12.5% relative lift sounds impressive, but if the absolute change is 0.1 points on a metric that barely moves the business, the framing matters.
A different conversion rate is not the same as a better variant. With 16,000 users split evenly, an eight-versus-nine-percent gap could easily be luck. The standard test for comparing two conversion rates is the two-proportion z-test. You can compute it in plain SQL.
with variant_results as (
-- the query from step 2, aliased so we can pivot it
select variant, users, conversions from ab_results
),
stats as (
select
max(case when variant = 'control' then users end) as n_c,
max(case when variant = 'control' then conversions end) as x_c,
max(case when variant = 'treatment' then users end) as n_t,
max(case when variant = 'treatment' then conversions end) as x_t
from variant_results
)
select
x_c::float / n_c as p_control,
x_t::float / n_t as p_treatment,
(x_t::float / n_t - x_c::float / n_c)
/ sqrt(
((x_c + x_t)::float / (n_c + n_t))
* (1 - (x_c + x_t)::float / (n_c + n_t))
* (1.0 / n_c + 1.0 / n_t)
) as z_score
from stats;
Read the z-score with two thresholds:
For the numbers above the z-score is about 2.2, so treatment beats control at the 95% level. If z had come back at 1.1, you would not have enough evidence to call it, no matter how much you like the new design.
Two caveats. This z-test is built for a binary metric (converted or not) with a reasonably large sample. For a continuous metric like revenue per user, use a t-test or a bootstrap instead, because a few big spenders can distort a mean in ways proportions never show. And a z-score is not a substitute for setting the sample size in advance, which is the next step.
Significance depends on three inputs you decide before the test starts: your baseline conversion rate, the minimum detectable effect (MDE, the smallest lift worth caring about), and the statistical power (conventionally 80%) and significance level (conventionally 95%). Smaller effects and lower baseline rates need dramatically more traffic.
The practical consequence: you cannot pick a sample size after looking at the data. Decide up front how many users per arm you need to detect your MDE, run until you hit it, then analyze once. Evan Miller’s sample size calculator is a reliable, widely used way to get that number without doing the power calculation by hand. If the required sample is larger than the traffic you can realistically gather in a few weeks, that is a signal the test is not worth running, not a reason to squint at an underpowered result.
Most bad A/B calls come from process errors, not arithmetic. Watch for these:
Experiments are not free, and some decisions do not benefit from one:
Run through this before you write “treatment wins” in a doc:
If any item fails, the honest answer is “we don’t know yet,” not “close enough.”
Analyzing a test in SQL is the right call when you run a handful of experiments, already trust your warehouse, and want full control over how metrics are defined. A dedicated experimentation platform earns its keep once you are running many concurrent tests and need randomized assignment, automatic significance, and guardrail metrics handled for you.
| Capability | BI tool + SQL | Experimentation platform |
|---|---|---|
| Randomized assignment | You build it (feature flags, hashing) | Built in |
| Sample size and power | Manual or external calculator | Built in |
| Continuous monitoring | Not safe without extra methods | Sequential testing handled |
| Guardrail and SRM checks | You write the queries | Automated alerts |
| Metric definitions | Full control, defined in SQL | Config-driven, less flexible |
| Ties directly to your warehouse | Yes | Varies by vendor |
| Best for | Occasional tests, custom metrics | High-volume experimentation programs |
If your experiment data lives in a production database or warehouse, a modern BI tool is often enough to run the full analysis. Basedash connects to Postgres, MySQL, Snowflake, and BigQuery, and lets you write the queries above or ask for the same numbers in plain English, then share the result as a dashboard the whole team can read. That keeps experiment analysis next to the rest of your product analytics instead of in a separate silo.
Yes. For a binary conversion metric, the two-proportion z-test is simple arithmetic you can express directly in SQL, as shown above. Compare the absolute z-score against 1.96 for 95% confidence or 2.58 for 99%. For continuous metrics like revenue per user, SQL alone is weaker, and you are better off exporting to a stats library or a purpose-built tool that can run a t-test or bootstrap.
It depends on your baseline conversion rate, the minimum lift you want to detect, and your target power and confidence. Lower baselines and smaller effects need much more traffic. Decide the number before the test using a power calculation or a sample size calculator, then run until you hit it. Choosing a sample size after seeing the data invalidates the significance test.
Because checking repeatedly and stopping at the first significant reading inflates your false positive rate far beyond the 5% you think you are accepting. Results that look significant early often revert to noise. Fix the sample size and analyze once, or use a sequential testing method built for continuous monitoring.
Relative lift is (treatment_rate - control_rate) / control_rate. Absolute lift is the raw difference in percentage points. A one-point move from 8% to 9% is a 12.5% relative lift but a one-point absolute lift. Report both, since relative numbers can make small absolute changes sound larger than they are.
Sample ratio mismatch (SRM) is when the actual split between variants differs meaningfully from what you configured, for example 53/47 when you intended 50/50. It usually means assignment, logging, or filtering is broken, which biases the comparison in ways the significance test cannot fix. Check the ratio first; if it fails, debug the pipeline before reading any results.
If you run occasional tests and your data is in a warehouse, a BI tool and SQL can handle the full analysis with full control over metric definitions. If you run many concurrent experiments and need randomized assignment, automatic significance, guardrail metrics, and continuous monitoring handled for you, a dedicated experimentation platform is worth the cost. Many teams start in SQL and adopt a platform only when experiment volume grows.
Written by

Founder and CEO of Basedash
Max Musing is the founder and CEO of Basedash, an AI-native business intelligence platform designed to help teams explore analytics and build dashboards without writing SQL. His work focuses on applying large language models to structured data systems, improving query reliability, and building governed analytics workflows for production environments.
Basedash lets you build charts, dashboards, and reports in seconds using all your data.