An anti-join is a pattern for retrieving records that “exist in A but not in B.” It uses a WHERE clause to filter on the property that rows without a LEFT JOIN match have NULL in the joined key column.
-- Basic anti-join pattern SELECT a.* FROM table_a a LEFT JOIN table_b b ON a.key = b.key -- A missing match makes the b columns NULL WHERE b.key IS NULL; -- NULL leaves only rows absent from B
NOT IN (subquery) has a trap: if the subquery result contains NULL, every row is excluded. LEFT JOIN + IS NULL is NULL-safe and generally gives stable performance, making it more dependable in production.Among the markets entered by the three competitors, list every “market gap” your company has not entered (our_markets), including both company name and market ID. Return company_name, market_id, market_name, sorted by company_name ascending and then market_id ascending.
| market_id | market_name |
|---|---|
| 1 | Domestic ERP |
| 2 | Domestic CRM |
| 3 | AI/ML |
| 4 | IoT |
| 5 | Data Analytics |
| market_id |
|---|
| 1 |
| 2 |
| company_name | market_id |
|---|---|
| BetaSoft | 1 |
| BetaSoft | 2 |
| BetaSoft | 3 |
| GammaSys | 2 |
| GammaSys | 4 |
| GammaSys | 5 |
| DeltaNet | 3 |
| DeltaNet | 4 |
Expected output (company_name ascending → market_id ascending):
| company_name | market_id | market_name |
|---|---|---|
| BetaSoft | 3 | AI/ML |
| DeltaNet | 3 | AI/ML |
| DeltaNet | 4 | IoT |
| GammaSys | 4 | IoT |
| GammaSys | 5 | Data Analytics |
Rows matching markets your company has entered (market_id 1,2) are excluded. The three markets AI/ML, IoT, and Data Analytics emerge as competitive market gaps.
SELECT cm.company_name, m.market_id, m.market_name FROM competitor_markets cm JOIN markets m USING (market_id) -- Attach the market name LEFT JOIN our_markets om USING (market_id) -- Left-join markets your company has entered WHERE om.market_id IS NULL -- A NULL row means your company has not entered the market ORDER BY cm.company_name, m.market_id; /* Execution order (logical SQL evaluation order): 1. FROM competitor_markets cm → Read the rows 2. JOIN markets m → Join matching rows 3. LEFT JOIN our_markets om → Join while preserving every left-side row 4. WHERE om.market_id IS NULL → Filter the rows 5. SELECT → Evaluate the columns 6. ORDER BY company_name, market_id → Sort and return the result */
LEGEND
① FROM + JOIN markets
FROM competitor_markets cm JOIN markets m USING (market_id)Use the eight rows in competitor_markets as the starting point and INNER JOIN markets to attach market names. All eight rows remain, with market_name added to each row.| company_name | market_id | market_name |
|---|---|---|
| BetaSoft | 1 | Domestic ERP |
| BetaSoft | 2 | Domestic CRM |
| BetaSoft | 3 | AI/ML |
| GammaSys | 2 | Domestic CRM |
| GammaSys | 4 | IoT |
| GammaSys | 5 | Data Analytics |
| DeltaNet | 3 | AI/ML |
| DeltaNet | 4 | IoT |
WHERE market_id NOT IN (SELECT market_id FROM our_markets) can produce the same result, but if a NULL row enters our_markets, NOT IN excludes every row. LEFT JOIN + IS NULL is NULL-safe and robust against dirty production data.WHERE NOT EXISTS (SELECT 1 FROM our_markets om WHERE om.market_id = cm.market_id) is equivalent and can be easier for the query planner to optimize on large tables. LEFT JOIN + IS NULL is often favored for readability, while NOT EXISTS is favored for explicit intent.WHERE market_id NOT IN (SELECT market_id FROM our_markets) excludes every row if our_markets.market_id contains even one NULL row, because comparisons with NULL are UNKNOWN. Prefer LEFT JOIN + IS NULL over NOT IN for production data.Adding a window frame (ROWS BETWEEN) to OVER(ORDER BY ...) controls the range of rows to aggregate. It is essential for implementing moving averages.
AVG(revenue) OVER ( PARTITION BY company_name ORDER BY quarter ROWS BETWEEN 2 PRECEDING AND CURRENT ROW -- Average of the latest 3 rows )
| Frame specification | Meaning |
|---|---|
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW | Current row + two preceding rows (3 rows total) |
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW | First row through current row (cumulative) |
ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING | One preceding row, current row, and one following row (3 rows total) |
RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW, which also includes rows tied with the current row. For a moving average, always specify ROWS BETWEEN N PRECEDING AND CURRENT ROW explicitly.Using quarterly revenue data for three companies, calculate each company's three-quarter moving average (moving_avg_3q, rounded to one decimal place). Return company_name, quarter, revenue, moving_avg_3q, sorted by company_name ascending and then quarter ascending.
| company_name | quarter | revenue |
|---|---|---|
| AlphaTech | 2023Q1 | 300 |
| AlphaTech | 2023Q2 | 360 |
| AlphaTech | 2023Q3 | 280 |
| AlphaTech | 2023Q4 | 400 |
| BetaSoft | 2023Q1 | 250 |
| BetaSoft | 2023Q2 | 220 |
| BetaSoft | 2023Q3 | 290 |
| BetaSoft | 2023Q4 | 310 |
| GammaSys | 2023Q1 | 120 |
| GammaSys | 2023Q2 | 150 |
| GammaSys | 2023Q3 | 170 |
| GammaSys | 2023Q4 | 160 |
revenue unit: JPY 100 million
Expected output (company_name ascending → quarter ascending):
| company_name | quarter | revenue | moving_avg_3q |
|---|---|---|---|
| AlphaTech | 2023Q1 | 300 | 300.0 |
| AlphaTech | 2023Q2 | 360 | 330.0 |
| AlphaTech | 2023Q3 | 280 | 313.3 |
| AlphaTech | 2023Q4 | 400 | 346.7 |
| BetaSoft | 2023Q1 | 250 | 250.0 |
| BetaSoft | 2023Q2 | 220 | 235.0 |
| BetaSoft | 2023Q3 | 290 | 253.3 |
| BetaSoft | 2023Q4 | 310 | 273.3 |
| GammaSys | 2023Q1 | 120 | 120.0 |
| GammaSys | 2023Q2 | 150 | 135.0 |
| GammaSys | 2023Q3 | 170 | 146.7 |
| GammaSys | 2023Q4 | 160 | 160.0 |
At the first and second rows, the window contracts to the rows that exist (Q1 averages one row; Q2 averages two rows). AlphaTech Q4 has the highest moving average at 346.7.
SELECT company_name, quarter, revenue, ROUND( AVG(revenue) OVER ( PARTITION BY company_name -- An independent window for each company ORDER BY quarter -- Ascending quarter order defines the “current row” ROWS BETWEEN 2 PRECEDING AND CURRENT ROW -- Cover the latest 3 quarters ), 1 ) AS moving_avg_3q FROM quarterly_revenue ORDER BY company_name, quarter; /* Execution order (logical SQL evaluation order): 1. FROM quarterly_revenue → Read the rows 2. AVG(revenue) OVER (...) → Evaluate the window function while preserving the row count 3. SELECT → Evaluate the columns (moving_avg_3q) 4. ORDER BY company_name, quarter → Sort and return the result */
LEGEND
① FROM quarterly_revenue
FROM quarterly_revenueRead the 12 rows in quarterly_revenue (three companies × four quarters). PARTITION BY will next divide them into company windows, and ROWS BETWEEN will calculate the moving averages.| company_name | quarter | revenue |
|---|---|---|
| AlphaTech | 2023Q1 | 300 |
| AlphaTech | 2023Q2 | 360 |
| AlphaTech | 2023Q3 | 280 |
| AlphaTech | 2023Q4 | 400 |
| BetaSoft | 2023Q1 | 250 |
| BetaSoft | 2023Q2 | 220 |
| BetaSoft | 2023Q3 | 290 |
| BetaSoft | 2023Q4 | 310 |
| GammaSys | 2023Q1 | 120 |
| GammaSys | 2023Q2 | 150 |
| GammaSys | 2023Q3 | 170 |
| GammaSys | 2023Q4 | 160 |
ROWS BETWEEN determines the frame by a physical row count. RANGE BETWEEN determines it by a value range and may include tied rows in the same frame. Always use ROWS for a row-based moving average. RANGE can unintentionally combine multiple rows for the same quarter.2 PRECEDING: two preceding rows plus the current row equals three rows. For a 12-month moving average over monthly data, specify 11 PRECEDING. Remember that “an N-row moving average” means “(N-1) PRECEDING AND CURRENT ROW.”OVER (PARTITION BY company_name ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) has no ORDER BY, so the “current row” order is indeterminate and the moving-average result is indeterminate. Always pair ROWS BETWEEN with ORDER BY.UNBOUNDED PRECEDING AND CURRENT ROW, from the first row through the current row. A moving average and a cumulative aggregate are entirely different calculations. Always verify that the output is not an unintended cumulative value.ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING frame, symmetric around the current row, can also provide median-like smoothing and support before-and-after comparisons of event effects.HAVING filters aggregate results after GROUP BY. WHERE is a row-level filter applied before aggregation, whereas HAVING is a group-level filter applied after aggregation.
SELECT company_name, SUM(revenue) FROM data WHERE year = 2024 -- ① Row-level filter before aggregation GROUP BY company_name HAVING SUM(revenue) > 1000 -- ② Group-level filter after aggregation
From quarterly performance data for three competitors, select the competitors that satisfy all three conditions: “annual total revenue of at least JPY 100 billion,” “average churn rate of at most 5.0%,” and “at least 500,000 new customers acquired during the year.” Return company_name, total_revenue, avg_churn_pct, total_new_customers, sorted by total_revenue descending.
| company_name | quarter | revenue | new_customers | churn_rate_pct |
|---|---|---|---|---|
| AlphaTech | 2023Q1 | 320 | 15 | 3.2 |
| AlphaTech | 2023Q2 | 360 | 18 | 2.8 |
| AlphaTech | 2023Q3 | 280 | 12 | 4.1 |
| AlphaTech | 2023Q4 | 400 | 20 | 3.5 |
| BetaSoft | 2023Q1 | 250 | 10 | 6.2 |
| BetaSoft | 2023Q2 | 220 | 8 | 7.0 |
| BetaSoft | 2023Q3 | 290 | 11 | 5.8 |
| BetaSoft | 2023Q4 | 310 | 13 | 6.5 |
| GammaSys | 2023Q1 | 120 | 8 | 4.5 |
| GammaSys | 2023Q2 | 150 | 10 | 3.8 |
| GammaSys | 2023Q3 | 170 | 12 | 3.2 |
| GammaSys | 2023Q4 | 160 | 9 | 4.0 |
Expected output (total_revenue descending):
| company_name | total_revenue | avg_churn_pct | total_new_customers |
|---|---|---|---|
| AlphaTech | 1360 | 3.4 | 65 |
BetaSoft fails because avg_churn=6.4% (>5.0). GammaSys fails because total_revenue=600 (<1000) and total_new_customers=39 (<50). Only AlphaTech passes every condition.
SELECT company_name, SUM(revenue) AS total_revenue, ROUND(AVG(churn_rate_pct), 1) AS avg_churn_pct, SUM(new_customers) AS total_new_customers FROM competitor_metrics GROUP BY company_name HAVING SUM(revenue) >= 1000 -- Condition ①: annual revenue of at least JPY 100 billion AND AVG(churn_rate_pct) <= 5.0 -- Condition ②: average churn rate of at most 5.0% AND SUM(new_customers) >= 50 -- Condition ③: at least 500,000 new customers during the year ORDER BY total_revenue DESC; /* Execution order (logical SQL evaluation order): 1. FROM competitor_metrics → Read the rows 2. GROUP BY company_name → Form the groups 3. SUM/AVG → Evaluate the aggregate functions 4. HAVING (3 conditions) → Filter the groups 5. SELECT → Evaluate the columns 6. ORDER BY total_revenue DESC → Sort and return the result */
LEGEND
① FROM competitor_metrics (12 rows)
FROM competitor_metricsRead the 12 rows in competitor_metrics (three companies × four quarters). GROUP BY will aggregate them into three company groups, and HAVING will then apply the condition filters.| company_name | quarter | revenue | new_customers | churn_rate_pct |
|---|---|---|---|---|
| AlphaTech | 2023Q1 | 320 | 15 | 3.2 |
| AlphaTech | 2023Q2 | 360 | 18 | 2.8 |
| AlphaTech | 2023Q3 | 280 | 12 | 4.1 |
| AlphaTech | 2023Q4 | 400 | 20 | 3.5 |
| BetaSoft | 2023Q1 | 250 | 10 | 6.2 |
| BetaSoft | 2023Q2 | 220 | 8 | 7 |
| BetaSoft | 2023Q3 | 290 | 11 | 5.8 |
| BetaSoft | 2023Q4 | 310 | 13 | 6.5 |
| GammaSys | 2023Q1 | 120 | 8 | 4.5 |
| GammaSys | 2023Q2 | 150 | 10 | 3.8 |
| GammaSys | 2023Q3 | 170 | 12 | 3.2 |
| GammaSys | 2023Q4 | 160 | 9 | 4 |
HAVING total_revenue >= 1000 raises an error. Write the aggregate expression directly in HAVING, or put the aggregation in a subquery or CTE and filter its alias with an outer WHERE.WHERE SUM(revenue) >= 1000 is a SQL syntax error. Aggregate functions cannot be used in WHERE. Always put post-aggregation conditions in HAVING.CROSS JOIN generates every combination of rows from its left and right tables: the Cartesian product. Combined with LEFT JOIN, it is useful when you need a complete matrix with no missing combinations, such as “all companies × all categories.”
SELECT c.company_name, cat.category_name, COALESCE(sd.revenue, 0) AS revenue -- Replace NULL with 0 FROM companies c CROSS JOIN categories cat -- Generate every combination (3 companies × 3 categories = 9 rows) LEFT JOIN sales_data sd ON sd.company_id = c.company_id AND sd.category_id = cat.category_id -- No recorded result produces NULL
COALESCE(expr1, expr2, ...) evaluates from left to right and returns the first non-NULL value. COALESCE(sd.annual_revenue, 0) returns 0 when annual_revenue is NULL, meaning the company has not entered the category, converting it to a comparable number.Using the revenue records for three companies, generate an all-company × all-category matrix and fill categories with no entry with 0. Return company_name, category_name, annual_revenue, using 0 for a missing value, and sort by company_name ascending and then category_id ascending.
| company_id | company_name |
|---|---|
| 1 | AlphaTech |
| 2 | BetaSoft |
| 3 | GammaSys |
| category_id | category_name |
|---|---|
| 1 | Cloud |
| 2 | Security |
| 3 | AI/ML |
| company_id | category_id | annual_revenue |
|---|---|---|
| 1 | 1 | 4200 |
| 1 | 2 | 1800 |
| 2 | 1 | 2800 |
| 2 | 3 | 1500 |
| 3 | 2 | 900 |
| 3 | 3 | 650 |
annual_revenue unit: JPY 100 million / 3 missing combinations (AlphaTech-AI/ML, BetaSoft-Security, GammaSys-Cloud)
Expected output (company_name ascending → category_id ascending):
| company_name | category_name | annual_revenue |
|---|---|---|
| AlphaTech | Cloud | 4200 |
| AlphaTech | Security | 1800 |
| AlphaTech | AI/ML | 0 |
| BetaSoft | Cloud | 2800 |
| BetaSoft | Security | 0 |
| BetaSoft | AI/ML | 1500 |
| GammaSys | Cloud | 0 |
| GammaSys | Security | 900 |
| GammaSys | AI/ML | 650 |
The three rows whose annual_revenue is 0 are unentered categories filled by COALESCE. The six recorded rows expand to nine rows: all companies × all categories.
SELECT c.company_name, cat.category_name, COALESCE(sd.annual_revenue, 0) AS annual_revenue -- Replace NULL with 0 FROM companies c CROSS JOIN categories cat -- Every one of 3 companies × 3 categories = 9 combinations LEFT JOIN sales_data sd ON sd.company_id = c.company_id AND sd.category_id = cat.category_id -- Match on both keys ORDER BY c.company_name, cat.category_id; /* Execution order (logical SQL evaluation order): 1. FROM companies c → Read the rows 2. CROSS JOIN categories cat → Join with a Cartesian product 3. LEFT JOIN sales_data sd → Join while preserving every left-side row 4. SELECT → Evaluate the columns and format annual_revenue 5. ORDER BY company_name, category_id → Sort and return the result */
LEGEND
① CROSS JOIN companies × categories → All 9 combinations
FROM companies c CROSS JOIN categories catCROSS JOIN companies (three rows) and categories (three rows). This generates all 3×3=9 combinations. Every (company_id, category_id) pair exists as a row whether or not a result was recorded.| company_id | company_name | category_id | category_name |
|---|---|---|---|
| 1 | AlphaTech | 1 | Cloud |
| 1 | AlphaTech | 2 | Security |
| 1 | AlphaTech | 3 | AI/ML |
| 2 | BetaSoft | 1 | Cloud |
| 2 | BetaSoft | 2 | Security |
| 2 | BetaSoft | 3 | AI/ML |
| 3 | GammaSys | 1 | Cloud |
| 3 | GammaSys | 2 | Security |
| 3 | GammaSys | 3 | AI/ML |
COALESCE(a, b, c) returns the first non-NULL value from left to right, a→b→c. COALESCE(sd.annual_revenue, 0) means “use the recorded value when present; otherwise use 0.” Using standard SQL COALESCE instead of ISNULL, a SQL Server dialect feature, or NVL, an Oracle dialect feature, improves portability.FROM companies, categories, sales_data produces a three-table Cartesian product of 3×3×6=54 rows. Always specify a join condition for an ordinary JOIN.AVG(NULL, NULL, 900) = 900, which differs from treating the missing values as 0. Decide whether to convert explicitly to 0 before aggregation according to the business requirement.A running total is implemented by giving SUM() OVER (ORDER BY ...) the window frame ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW. Cumulative share in descending value order is the foundation of Pareto analysis.
SUM(revenue) OVER ( ORDER BY revenue DESC ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW -- Cumulative total from the first row through the current row ) AS cumulative_revenue
SUM(revenue) OVER (), with neither PARTITION BY nor ORDER BY, treats every row as one window and returns the grand total. Use it as the denominator to calculate cumulative value ÷ grand total × 100.Using annual revenue for six IT competitors, calculate cumulative revenue (cumulative_revenue) and cumulative share (cumulative_share_pct, rounded to one decimal place) in descending revenue order. This Pareto analysis visualizes what percentage of the market is held by the top N companies. Return company_name, annual_revenue, cumulative_revenue, cumulative_share_pct, sorted by annual_revenue descending.
| company_name | annual_revenue |
|---|---|
| AlphaTech | 4200 |
| BetaSoft | 2800 |
| GammaSys | 1500 |
| DeltaNet | 500 |
| EpsilonSys | 300 |
| ZetaCloud | 200 |
annual_revenue unit: JPY 100 million / Market total: JPY 950 billion
Expected output (annual_revenue descending):
| company_name | annual_revenue | cumulative_revenue | cumulative_share_pct |
|---|---|---|---|
| AlphaTech | 4200 | 4200 | 44.2 |
| BetaSoft | 2800 | 7000 | 73.7 |
| GammaSys | 1500 | 8500 | 89.5 |
| DeltaNet | 500 | 9000 | 94.7 |
| EpsilonSys | 300 | 9300 | 97.9 |
| ZetaCloud | 200 | 9500 | 100.0 |
The top two companies, AlphaTech and BetaSoft, hold a cumulative 73.7% share. The top three hold 89.5%, so the third company is where the cumulative share reaches 80%.
SELECT company_name, annual_revenue, SUM(annual_revenue) OVER ( ORDER BY annual_revenue DESC ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW -- Cumulative total from the first row through the current row ) AS cumulative_revenue, ROUND( SUM(annual_revenue) OVER ( ORDER BY annual_revenue DESC ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW ) * 100.0 / SUM(annual_revenue) OVER (), -- Denominator: grand total (OVER() = all rows) 1 ) AS cumulative_share_pct FROM competitor_revenue ORDER BY annual_revenue DESC; /* Execution order (logical SQL evaluation order): 1. FROM competitor_revenue → Read the rows 2. SUM(...) OVER () → Attach the grand total to every row 3. SUM(...) OVER (...) → Calculate the running total 4. Cumulative value / grand total → Calculate cumulative share (%) 5. SELECT → Select the columns 6. ORDER BY annual_revenue DESC → Sort and return the result */
LEGEND
① FROM competitor_revenue
FROM competitor_revenueRead the six rows in competitor_revenue. SUM() OVER () will attach the grand total, while SUM() OVER (ORDER BY DESC ROWS...) will attach the cumulative total to each row.| company_name | annual_revenue |
|---|---|
| AlphaTech | 4200 |
| BetaSoft | 2800 |
| GammaSys | 1500 |
| DeltaNet | 500 |
| EpsilonSys | 300 |
| ZetaCloud | 200 |
SUM(x) OVER () attaches the fixed grand total to every row. SUM(x) OVER (ORDER BY x DESC ROWS UNBOUNDED...) returns a different cumulative value for each row. Combining them as the former denominator and the latter numerator, the cumulative value, calculates cumulative share in a single query.RANGE, which includes rows tied with the current row. Writing ROWS makes the intent to accumulate by physical row explicit and reduces the risk of misreading the frame when equal revenue values exist.SUM(annual_revenue) OVER (ORDER BY annual_revenue DESC) does produce a cumulative value, but with tied records its RANGE behavior includes every row tied with the current row in the current frame, which can produce an unintended cumulative value. Specify ROWS BETWEEN explicitly so that RANGE and ROWS are not confused.FROM t a JOIN t b ON b.revenue >= a.revenue or multiple subqueries, greatly degrading performance. A window function is the first choice for a cumulative aggregate.cumulative_share_pct and the preceding row's value in the outer query to detect the crossing boundary.