Understanding Null Semantics In Sql: 2026 Guide & Examples
Master three-valued logic and traps by understanding null semantics in sql. Learn NOT IN pitfalls, WHERE vs HAVING filtering, and safe COALESCE math.
Mastering relational databases requires understanding null semantics in sql, where missing data is governed by three-valued logic rather than binary booleans. In production environments, unhandled NULL values silently invalidate financial summaries, corrupt aggregate metrics, and cause take-home interview queries to fail without throwing a single syntax error. When a query returns zero rows instead of hundreds, or when an executive dashboard displays an inflated average, NULL semantics are almost always the hidden culprit.
This understanding null semantics in sql practical guide breaks down the theoretical foundations and daily engineering challenges of SQL NULL handling. We explore three-valued logic (3VL), demystify how WHERE and HAVING treat missing data differently across execution phases, expose the deadly NOT IN subquery trap, and demonstrate safe handling strategies using verified datasets from our live PostgreSQL environment. For complementary database optimization patterns, review our SQL JOIN Fan-Out Guide, SQL CTE Guide, and execution order breakdown in WHERE vs HAVING in SQL.
1. The Core Philosophy: NULL Represents Missing Information, Not Empty State
In E.F. Codd's relational database model, NULL is not a data value; it is a placeholder or marker signaling that a value is missing, unknown, or not applicable. Treating NULL as an ordinary value is the root cause of the most common SQL bugs.
To write reliable analytical queries, you must distinguish between four distinct states of "emptiness":
| State | Data Type | Meaning | How to Test |
|---|---|---|---|
NULL | Marker (Any Type) | Information does not exist or is unknown | col IS NULL |
Empty String ('') | String (VARCHAR/TEXT) | Known, recorded string with zero characters | col = '' |
Zero (0) | Numeric (INT/NUMERIC) | Known numeric quantity representing none | col = 0 |
Whitespace (' ') | String (VARCHAR/TEXT) | Known string containing space characters | TRIM(col) = '' |
An empty string is a value you have that happens to contain zero characters. A NULL is a value you do not have at all. While both render as empty cells in most database GUI clients (like DBeaver, pgAdmin, or DataGrip), they respond to completely different comparison logic.
Spotting Missing Values in Practice
In our verified e-commerce dataset containing 500 customer records, 443 customers provided an email address during onboarding, while 57 records have no email recorded. You can float missing records to the top of your inspection results using boolean evaluation in ORDER BY:
-- Surface missing customer contact data first
SELECT customer_id, first_name, email, phone, city
FROM customers
ORDER BY (email IS NULL) DESC, (phone IS NULL) DESC, customer_id ASC
LIMIT 10;In PostgreSQL, the boolean expression (email IS NULL) evaluates to TRUE (1) for missing rows and FALSE (0) for complete rows. Sorting in descending order guarantees that missing contact records appear immediately for data auditing.
2. Three-Valued Logic (3VL): Truth Tables for AND, OR, and NOT
Standard computer logic is two-valued (Boolean): an expression is either TRUE or FALSE. Relational databases, however, operate on Three-Valued Logic (3VL) comprising TRUE, FALSE, and UNKNOWN.
When an attribute is missing, the database cannot definitively assert whether a comparison is true or false. For example, if employee Sarah's salary is NULL, the statement salary > 50000 cannot be confirmed as TRUE, nor can it be proven FALSE. The only logically sound answer is UNKNOWN.
How Comparisons Yield UNKNOWN
Any standard comparison operator (=, !=, <>, >, <, <=, >=) applied to a NULL operand evaluates to UNKNOWN:
-- Every single one of these expressions evaluates to UNKNOWN:
NULL = 500
NULL != 500
NULL > 500
NULL < 500
NULL = NULL
NULL <> NULLThe NULL = NULL Interview Trap
One of the most frequent technical interview questions asks: "What does SELECT * FROM table WHERE NULL = NULL; return?" It returns zero rows. Even two unknown values cannot be considered equal, because two unknown quantities in the real world might represent completely different unknown numbers.
The 3VL Truth Tables
Mastering understanding null semantics in sql examples requires understanding how UNKNOWN interacts with logical operators (AND, OR, NOT). SQL evaluates composite predicates using the following formal truth tables:
Logical AND Truth Table
| Operands | TRUE | FALSE | UNKNOWN |
|---|---|---|---|
| TRUE | TRUE | FALSE | UNKNOWN |
| FALSE | FALSE | FALSE | FALSE |
| UNKNOWN | UNKNOWN | FALSE | UNKNOWN |
Notice that FALSE AND UNKNOWN resolves to FALSE. Why? Because regardless of what the unknown value actually turns out to be, an AND condition with one FALSE branch can never be true. However, TRUE AND UNKNOWN must evaluate to UNKNOWN.
Logical OR Truth Table
| Operands | TRUE | FALSE | UNKNOWN |
|---|---|---|---|
| TRUE | TRUE | TRUE | TRUE |
| FALSE | TRUE | FALSE | UNKNOWN |
| UNKNOWN | TRUE | UNKNOWN | UNKNOWN |
Notice that TRUE OR UNKNOWN resolves to TRUE. Because one operand is already proven true, the unknown branch cannot change the satisfied outcome. Conversely, FALSE OR UNKNOWN remains UNKNOWN.
Logical NOT Truth Table
| Operand | Result of NOT |
|---|---|
| TRUE | FALSE |
| FALSE | TRUE |
| UNKNOWN | UNKNOWN |
Inverting an unknown proposition does not produce certainty; NOT (UNKNOWN) remains UNKNOWN.
The Law of Excluded Middle Trap
In classical Boolean logic, the law of the excluded middle dictates that for any proposition P, P OR (NOT P) is always true. In SQL 3VL, this law does not hold. Consider this innocent query intended to audit all employees:
-- Trap: Does NOT return all employees!
SELECT employee_id, salary
FROM employees
WHERE salary > 50000 OR salary <= 50000;If an employee has a NULL salary:
salary > 50000evaluates toUNKNOWN.salary <= 50000evaluates toUNKNOWN.UNKNOWN OR UNKNOWNevaluates toUNKNOWN.
Because the WHERE clause retains rows only if the final condition evaluates strictly to TRUE, every employee with a missing salary is silently omitted from the results. To include them, you must explicitly handle nullability:
-- Safe: Explicitly accounts for NULL values
SELECT employee_id, salary
FROM employees
WHERE salary > 50000 OR salary <= 50000 OR salary IS NULL;Alternatively, modern SQL dialects supporting PostgreSQL and SQLite offer the null-safe comparison operator IS DISTINCT FROM:
-- Evaluates to TRUE when one side is NULL and the other is not
WHERE status IS DISTINCT FROM 'archived';3. The NOT IN With NULL Trap: The Deadliest Subquery Pitfall
Among all SQL pitfalls, none causes more severe production incidents than evaluating NOT IN against a dataset or subquery containing NULLs.
The Business Scenario
Imagine your marketing team wants a list of all customers who have never placed an order, so they can send a promotional onboarding voucher. Our database has 500 customers and 2,000 orders. An analyst writes the following query:
-- THE TRAP QUERY: Returns 0 rows if any order has a NULL customer_id
SELECT c.customer_id, c.first_name, c.email
FROM customers AS c
WHERE c.customer_id NOT IN (
SELECT o.customer_id
FROM orders AS o
);If even a single row in the orders table has a NULL customer_id (for example, guest checkouts, abandoned cart syncs, or legacy migrated transactions), this query returns exactly 0 rows. The entire marketing campaign is blocked, yet no database error is raised.
Deconstructing the Mathematical Failure
To understand why this happens, apply De Morgan's laws to how SQL compiles the IN and NOT IN operators.
The expression x IN (1, 2, NULL) expands to:
(x = 1) OR (x = 2) OR (x = NULL)The negated expression x NOT IN (1, 2, NULL) expands to:
NOT ((x = 1) OR (x = 2) OR (x = NULL))Distributing the negation across the disjunction yields a conjunction of inequalities:
(x != 1) AND (x != 2) AND (x != NULL)Now evaluate this expression for customer ID 5 (who has never ordered):
5 != 1evaluates toTRUE.5 != 2evaluates toTRUE.5 != NULLevaluates toUNKNOWN.TRUE AND TRUE AND UNKNOWNresolves toUNKNOWN.
Because the final predicate is UNKNOWN, the row is rejected. If you test a customer ID that has ordered (e.g. 1), the expression evaluates to FALSE AND ... which is FALSE.
Thus, for every row in the table, the condition evaluates to either FALSE or UNKNOWN. It can never evaluate to TRUE. The query returns an empty set every single time.
The Three Production-Grade Solutions
Here is how senior data engineers write robust anti-joins that remain immune to NULL values:
Fix 1: Correlated NOT EXISTS (Recommended)
EXISTS checks whether the subquery produces one or more rows. It operates on existence rather than scalar value comparison:
-- Fix 1: Bulletproof against NULLs and highly optimized by query planners
SELECT c.customer_id, c.first_name, c.email
FROM customers AS c
WHERE NOT EXISTS (
SELECT 1
FROM orders AS o
WHERE o.customer_id = c.customer_id
);When an order row has customer_id IS NULL, the join predicate o.customer_id = c.customer_id evaluates to UNKNOWN. The subquery discards that row and returns nothing for that comparison, allowing NOT EXISTS to evaluate cleanly to TRUE for unpurchased customers.
Fix 2: Explicit NULL Filtering in Subquery
If your team insists on using NOT IN, you must explicitly strip NULLs from the subquery candidate list:
-- Fix 2: Safe NOT IN with explicit NULL exclusion
SELECT c.customer_id, c.first_name, c.email
FROM customers AS c
WHERE c.customer_id NOT IN (
SELECT o.customer_id
FROM orders AS o
WHERE o.customer_id IS NOT NULL
);Fix 3: LEFT JOIN Anti-Join Pattern
Join the child table and test for the absence of the primary key:
-- Fix 3: Traditional outer join anti-pattern
SELECT c.customer_id, c.first_name, c.email
FROM customers AS c
LEFT JOIN orders AS o ON c.customer_id = o.customer_id
WHERE o.order_id IS NULL;Notice that we filter on o.order_id IS NULL (the primary key of orders), never on the joined foreign key.
| Feature / Criteria |
|---|
Master Advanced SQL Joins & Subqueries
Level up from basic queries to enterprise-grade analytics engineering with 100+ hands-on challenges in our Data Analyst Career Track.
Explore Career Track4. NULL Filtering in WHERE vs HAVING: Execution Order & Aggregation Mechanics
A foundational concept in SQL query architecture is how NULLs are filtered before versus after aggregation. Understanding the SQL logical execution pipeline prevents catastrophic performance regressions and inaccurate analytical metrics:
1. FROM & JOIN --> Build virtual Cartesian product & join tables
2. WHERE --> Filter raw base rows (discards FALSE and UNKNOWN)
3. GROUP BY --> Form summary groups (NULLs grouped into one bucket)
4. HAVING --> Filter aggregated group summaries
5. SELECT --> Compute projected columns, scalar expressions & aliases
6. ORDER BY --> Sort final presentation rows
Row-Level Filtering: The WHERE Clause
WHERE filters individual rows from the source tables before any grouping occurs. If a row's predicate evaluates to FALSE or UNKNOWN, that single row is eliminated immediately.
-- Filters out customers with missing cities BEFORE grouping
SELECT city, COUNT(*) AS customer_count
FROM customers
WHERE city IS NOT NULL
GROUP BY city;Because this occurs in step 2 of execution:
- The database engine can utilize B-tree indexes on
cityto skip NULL rows without reading table heap blocks. - Memory consumption during the subsequent
GROUP BYhash or sort operation is minimized because unneeded rows were already discarded.
Group-Level Filtering: How GROUP BY & HAVING Treat NULLs
In the ANSI SQL standard, all NULL values in a grouping column are treated as mutually equivalent for the purpose of aggregation. They are clustered into a single summary group:
-- Groups all 57 missing emails into a single NULL bucket
SELECT email, COUNT(*) AS row_count
FROM customers
GROUP BY email;The resulting table contains one row where email is NULL, with a row_count of 57.
Once groups are formed, HAVING filters group summaries based on aggregate metrics. If you evaluate a condition against an aggregate that returns NULL, HAVING drops that entire group because the condition evaluates to UNKNOWN:
-- Compare how WHERE and HAVING evaluate missing values
SELECT
p.category,
COUNT(*) AS total_items,
AVG(oi.unit_price) AS avg_item_price
FROM order_items AS oi
JOIN products AS p ON oi.product_id = p.product_id
GROUP BY p.category
HAVING AVG(oi.unit_price) > 50.00;If a newly created product category has zero priced items (all unit prices are NULL), AVG(oi.unit_price) returns NULL. The comparison NULL > 50.00 evaluates to UNKNOWN, and the entire product category is pruned from the report.
The WHERE vs HAVING Performance Trap
A frequent mistake in analytical queries is placing row-level column filters into the HAVING clause instead of WHERE:
-- WRONG & SLOW: Groups all rows before filtering
SELECT department_id, AVG(salary) AS avg_sal
FROM employees
GROUP BY department_id, status
HAVING status = 'active';
-- CORRECT & FAST: Filters base rows before grouping
SELECT department_id, AVG(salary) AS avg_sal
FROM employees
WHERE status = 'active'
GROUP BY department_id;Filtering in WHERE prunes inactive employee rows prior to hashing, preserving memory and CPU bandwidth. Filtering in HAVING forces the engine to aggregate every historical record before discarding the computed summaries.
5. The COUNT Trap: Counting Rows vs. Counting Values
One of the most frequently asked interview questions tests candidate understanding of aggregate counting semantics.
The Experiment
Consider our 500-customer dataset where 57 customers lack an email address:
-- Demonstrating the aggregate counting discrepancy
SELECT
COUNT(*) AS count_star,
COUNT(1) AS count_one,
COUNT(email) AS count_column,
COUNT(DISTINCT email) AS count_distinct_email
FROM customers;The Output
| count_star | count_one | count_column | count_distinct_email |
|---|---|---|---|
| 500 | 500 | 443 | 443 |
Why the Counts Differ
COUNT(*)andCOUNT(1)count rows. They evaluate whether a row exists in the relation. Even if every single column in a row contains NULL,COUNT(*)counts that row. Both return 500.COUNT(column)counts non-NULL values. It inspects the specific column expression and skips every row where that column is NULL. It returns 443.- The arithmetic gap (
500 - 443 = 57) represents the number of uncollected email addresses.
Aggregate Function Behavior Matrix
Every standard aggregate function in SQL follows strict ANSI null-handling rules:
| Function | Ignores NULLs? | Return Value if All Values are NULL | Example in 800-Review Dataset |
|---|---|---|---|
COUNT(*) | No | 0 (on empty set) | Returns 800 |
COUNT(rating) | Yes | 0 | Returns 714 (86 NULL ratings skipped) |
SUM(rating) | Yes | NULL | Sums 714 active ratings |
AVG(rating) | Yes | NULL | Computes average of 714 active ratings (2.941) |
MIN(rating) | Yes | NULL | Minimum of 714 active ratings |
MAX(rating) | Yes | NULL | Maximum of 714 active ratings |
The Dangerous Average Skew
Notice how AVG handles missing ratings. If 86 customers purchased an item but never left a rating:
AVG(rating)evaluates to2.941. This represents the average rating among customers who actually rated the item.AVG(COALESCE(rating, 0))evaluates to2.625. This assumes every non-rater intended to submit a score of zero.
Unless business specifications explicitly dictate that unsubmitted feedback is equivalent to zero stars, using COALESCE(rating, 0) inside an average calculation distorts product satisfaction metrics and misinforms business stakeholders.
6. NULL Poisons Arithmetic: Safe Calculations with COALESCE and NULLIF
In SQL arithmetic expressions, NULL acts as a logical poison pill. Any standard mathematical operation involving a NULL operand yields NULL:
-- All of the following yield NULL:
100 + NULL
100 - NULL
100 * NULL
100 / NULLArithmetic Contamination in Financial Reporting
In our verified order_items table containing 5,000 line items, 169 records have a missing quantity due to unfulfilled backorders. Calculating line-item revenue directly results in catastrophic data loss:
-- Broken Query: Yields NULL for 169 line items
SELECT
item_id,
quantity,
unit_price,
quantity * unit_price AS raw_line_amount
FROM order_items
WHERE quantity IS NULL
LIMIT 5;| item_id | quantity | unit_price | raw_line_amount |
|---|---|---|---|
| 3 | NULL | 89.99 | NULL |
| 17 | NULL | 45.00 | NULL |
| 42 | NULL | 129.50 | NULL |
| 68 | NULL | 14.95 | NULL |
| 91 | NULL | 210.00 | NULL |
Even though unit_price contains valid numeric data, the missing quantity forces raw_line_amount to evaluate to NULL. When passed to an outer SUM(), those missing amounts contribute $0.00 to ledger totals without notifying the reporting system.
The Safety Net: COALESCE
The ANSI SQL COALESCE(val1, val2, ...) function accepts a variable list of arguments and returns the first non-NULL expression:
-- Clean Query: Restores financial integrity
SELECT
item_id,
COALESCE(quantity, 0) AS clean_quantity,
COALESCE(quantity, 0) * unit_price AS clean_line_amount
FROM order_items
WHERE quantity IS NULL
LIMIT 5;Now clean_quantity defaults to 0, and clean_line_amount computes safely to 0.00 instead of NULL.
Preventing Division by Zero with NULLIF
The counterpart to COALESCE is NULLIF(arg1, arg2). It compares two arguments and returns NULL if they are equal; otherwise, it returns arg1.
Data analysts frequently combine COALESCE and NULLIF to prevent division-by-zero crashes when calculating conversion rates or profit margins:
-- Safe division pattern: returns NULL instead of raising a DB exception
SELECT
campaign_id,
conversions,
clicks,
ROUND(conversions::numeric / NULLIF(clicks, 0) * 100, 2) AS conversion_rate
FROM marketing_campaigns;If clicks is 0, NULLIF(clicks, 0) returns NULL. Dividing by NULL returns NULL rather than crashing the scheduled ETL job with a division by zero database error.
7. Interactive Practice Sandbox: Solve Anti-Joins Live
Mastering three-valued logic and subquery null mechanics requires executing queries against realistic relational databases. You can test both the broken subquery trap and the verified NOT EXISTS anti-join solution directly in our interactive PostgreSQL sandbox on Tracks That Have Never Been Purchased (Anti-Join).
This sandbox question runs against a production-grade digital media store database containing 3,503 catalog tracks and 2,240 invoice line items. Exactly 1,984 tracks have been purchased at least once, leaving 1,519 unpurchased tracks that the catalog merchandising team needs to identify.
Sandbox Query 1: The NOT IN Trap With an Injected NULL
Execute this query in the sandbox editor to observe how a single NULL destroys an anti-join:
-- Sandbox Query 1: The NOT IN NULL collapse
SELECT COUNT(*) AS unpurchased_track_count
FROM track
WHERE trackid NOT IN (
SELECT trackid FROM invoiceline
UNION ALL
SELECT NULL::int -- Simulating an unlinked transaction or guest line item
);Sandbox Output: 0 rows
Because the subquery returns an injected NULL, every track row evaluates to UNKNOWN in the WHERE clause. Despite 1,519 unpurchased items existing in the table, the query reports zero results.
Sandbox Query 2: The Clean Correlated NOT EXISTS Anti-Join
Now execute the industry-standard NOT EXISTS solution:
-- Sandbox Query 2: Verified NULL-safe correlated anti-join
SELECT t.trackid, t.name
FROM track AS t
WHERE NOT EXISTS (
SELECT 1
FROM invoiceline AS il
WHERE il.trackid = t.trackid
)
ORDER BY t.trackid ASC;Sandbox Output: 1,519 rows
| trackid | name |
|---|---|
| 7 | Let's Get It Up |
| 11 | C.O.D. |
| 17 | Let There Be Rock |
| 18 | Bad Boy Boogie |
| 22 | Whole Lotta Rosie |
The correlated subquery checks for the existence of matching rows. Even if invoiceline contains rows with NULL foreign keys, the join condition il.trackid = t.trackid evaluates to UNKNOWN, returning no rows to EXISTS. Consequently, NOT EXISTS correctly evaluates to TRUE, accurately surfacing all 1,519 unpurchased tracks.
To master relational data modeling and end-to-end analytics engineering, explore our comprehensive Data Analyst Career Track.
8. Syntax & Behavior Cheatsheet for SQL NULLs
Use this quick-reference table to audit queries for potential NULL evaluation bugs:
| Scenario / Operation | Recommended Syntax | Risky / Incorrect Syntax | Failure Mode |
|---|---|---|---|
| Test for Missing Data | WHERE col IS NULL | WHERE col = NULL | Evaluates to UNKNOWN; returns 0 rows |
| Test for Existing Data | WHERE col IS NOT NULL | WHERE col <> NULL | Evaluates to UNKNOWN; returns 0 rows |
| Test for Empty String | WHERE col = '' | WHERE col IS NULL | Missing strings and empty text are distinct types |
| Unmatched Records (Anti-Join) | WHERE NOT EXISTS (...) | WHERE col NOT IN (...) | Subquery with single NULL returns 0 rows |
| Aggregate Row Counts | COUNT(*) | COUNT(nullable_col) | Column count skips missing values |
| Compute Additive Sums | SUM(COALESCE(qty, 0)) | SUM(qty) | Safe if missing quantities represent zero additions |
| Compute Metric Averages | AVG(rating) | AVG(COALESCE(rating, 0)) | Coalescing to zero artificially depresses user averages |
| Prevent Division by Zero | val / NULLIF(denom, 0) | val / denom | Crashes query with zero-division error |
| Row-Level Pre-Filtering | Put in WHERE clause | Put in HAVING clause | Severe memory bloat and hash aggregation overhead |
9. Next Steps & Interactive Practice
How to use understanding null semantics in sql effectively comes down to disciplined defensive coding: always verify whether missing values mean "empty" or "unknown," prefer NOT EXISTS over NOT IN, and guard arithmetic with COALESCE and NULLIF.
Practice SQL NULL Handling & Anti-Joins Live
Master correlated subqueries, avoid NOT IN traps, and solve realistic company interview problems in our PostgreSQL browser sandbox.
Open Practice Sandbox FreeFrequently Asked Questions
What is three-valued logic in SQL?
SQL three-valued logic (3VL) evaluates logical expressions to TRUE, FALSE, or UNKNOWN. Because NULL represents missing data rather than a fixed value, direct comparisons like NULL = NULL or column = 100 evaluate to UNKNOWN. Conditions in WHERE and HAVING clauses only retain rows that evaluate strictly to TRUE.
Why does NOT IN return zero rows when a subquery contains NULL?
The expression col NOT IN (SELECT other_col) expands logically to a chain of inequality checks joined by AND. If the subquery contains even one NULL, the comparison `col <> NULL` evaluates to UNKNOWN. Because TRUE AND UNKNOWN yields UNKNOWN, the entire condition can never be TRUE, causing SQL to return zero rows.
How does NULL filtering differ between WHERE and HAVING?
The WHERE clause filters individual base rows before grouping and aggregation, discarding any row where conditions evaluate to FALSE or UNKNOWN. The HAVING clause filters grouped summary rows after aggregation functions have already processed and skipped row-level NULLs, evaluating conditions on the resulting group aggregates.
Why does COUNT(*) return more rows than COUNT(column)?
COUNT(*) counts every row in the dataset regardless of whether individual columns contain NULL values. In contrast, COUNT(column) evaluates the specific column and skips every row where that column is NULL. The difference between the two counts reveals the exact number of NULL values in that column.
What is the difference between NULL and an empty string in SQL?
An empty string ('') is a known, zero-length string value present in memory, whereas NULL represents an absence of value or unknown data. Equality checks with empty strings (= '') match empty text but return UNKNOWN for NULLs. NULL rows can only be matched using IS NULL or IS NOT NULL operators.
What is the safest way to handle NULL in SQL arithmetic?
Use COALESCE(column, default_value) to replace NULLs before arithmetic calculations. Because any standard mathematical operation with NULL yields NULL (e.g. price * NULL = NULL), wrapping nullable columns in COALESCE(column, 0) prevents missing numbers from poisoning the calculation.

Written by
Founder at Topfolio with 6+ years in data & analytics across JPMC, Ultrahuman, and high-growth startups. Sat on hiring panels, reviewed 500+ resumes, and writes practical SQL & data guides.
Related Articles
Full Outer Join In Sql: 2026 Guide & Examples
Master the full outer join in sql with practical examples, billing reconciliation queries, syntax rules, and NULL handling for data analysts.
SQL Date Functions Guide: Practical Examples (2026)
Master sql date functions with practical examples. Learn DATE_TRUNC, EXTRACT, interval rolling windows, and how to avoid the timestamp BETWEEN trap.
Window Functions in SQL: Practical Guide & Examples
Master window functions in sql with practical examples. Learn OVER, PARTITION BY, running totals, rankings, and lead lag calculations step by step.