SQL • DATA TRANSFORMATION

Creating Flags & Buckets — Create flags and buckets from numeric or date fields

Transform raw continuous data into categorical labels that power segmentation, reporting, and feature engineering.

Historical Context & Motivation

Long before SQL existed, analysts and statisticians faced a fundamental challenge: continuous numeric data—revenue figures, timestamps, sensor readings—is difficult to summarize, compare, and communicate in its raw form. The practice of discretization, or converting continuous values into discrete categories, has roots in early statistical analysis where researchers binned observations into frequency tables and histograms. As relational databases matured throughout the 1980s and 1990s, the need to perform this categorization inside the database engine—rather than exporting data and manipulating it in application code—became critical for performance and consistency. Two dominant patterns emerged: flags (binary indicators) and buckets (multi-level categories), both expressible through standard SQL constructs.

1970
Codd's Relational Model
Edgar F. Codd publishes the relational model, establishing that data should be stored in tables with typed columns—numeric, date, and character—laying the groundwork for in-database transformations.
1986
SQL-86 Standard
The first ANSI SQL standard formalizes basic query syntax, but lacks conditional expressions like CASE, forcing developers to use application-layer logic for flagging and bucketing.
1992
SQL-92 & the CASE Expression
SQL-92 introduces the CASE expression, providing a portable, declarative way to create flags and buckets directly in SELECT statements. This is the cornerstone construct for in-query data transformation.
2003
SQL:2003 & WIDTH_BUCKET
The SQL:2003 standard introduces the WIDTH_BUCKET function, enabling equal-width histogram bucketing as a first-class SQL function—significantly simplifying numeric discretization.
2010s
Analytics & Feature Engineering Era
Modern analytics warehouses (BigQuery, Snowflake, Redshift) make flags and buckets essential for dashboards, cohort analysis, and machine-learning feature pipelines executed entirely in SQL.

The central question this lesson addresses is straightforward yet powerful: given a table with numeric columns (prices, scores, counts) or date columns (order dates, signup timestamps), how do we write SQL that produces meaningful categorical labels—binary flags like is_high_value or multi-level buckets like age_group—directly within a query, without leaving the database?

Core Principles & Definitions

Before diving into syntax, it is important to establish a precise vocabulary for the two transformation patterns. A flag is a binary (0/1 or TRUE/FALSE) derived column that answers a yes-or-no question about each row. A bucket (also called a bin or tier) is a derived column that maps a continuous value into one of several mutually exclusive, collectively exhaustive categories. Both constructs share a common structural principle: they use conditional expressions to evaluate each row and assign a label. The distinction is cardinality: flags yield exactly two outcomes, while buckets yield three or more.

1

Flags (Binary Indicators)

A flag answers a single Boolean question per row. Typically implemented with CASE WHEN ... THEN 1 ELSE 0 END or the shorter IIF() in some dialects. Output cardinality: exactly 2.
2

Buckets (Multi-Level Bins)

Buckets partition a continuous domain into N ≥ 3 labeled segments. A searched CASE expression evaluates conditions top-to-bottom, assigning the first matching label. Alternatively, WIDTH_BUCKET() automates equal-width binning.
3

Numeric vs. Date Fields

Numeric fields (INT, DECIMAL, FLOAT) use arithmetic comparisons for thresholds. Date/timestamp fields require date functions such as EXTRACT(), DATEDIFF(), or DATE_TRUNC() to derive a numeric proxy before applying conditions.
4

MECE Principle

Buckets must be Mutually Exclusive and Collectively Exhaustive (MECE). Every row should fall into exactly one bucket. Always include an ELSE clause or a catch-all condition to handle NULLs and edge cases—otherwise rows silently receive NULL labels.
5

Evaluation Order

SQL's searched CASE evaluates WHEN clauses sequentially and returns the result of the first TRUE condition. This means the order of conditions is semantically significant; overlapping ranges will silently resolve to the first match.
KEY TAKEAWAY
Think of flags and buckets like sorting mail. A flag is a simple yes/no stamp—"Does this letter go international?"—while a bucket is a set of labeled bins on a sorting shelf: Local, Regional, National, International. In both cases, you inspect a continuous attribute (the destination distance) and assign a discrete label. The SQL CASE expression is your sorting rule sheet.

Visual Explanation — Flags & Buckets Flow

The diagram shows a source column of continuous order totals flowing through two transformation paths. The top path demonstrates a flag that produces a binary 0 or 1 based on a single threshold (≥ 100). The bottom path shows a bucket that partitions the same values into four labeled tiers using cascading CASE conditions.

The diagram above illustrates the fundamental duality of this lesson. A single source column can produce both a flag and a bucket in the same SELECT statement—these are independent derived columns. Notice how the bucket version requires carefully ordered thresholds: because CASE evaluates top-to-bottom, placing WHEN total < 500 before WHEN total < 200 would incorrectly classify a $150 order as 'High' instead of 'Medium'. This sequential evaluation behavior is the most common source of bucketing bugs in production SQL code.

SQL Constructs — CASE, IIF, and WIDTH_BUCKET

The Searched CASE Expression

The searched CASE expression is the workhorse for creating both flags and buckets. Unlike the simple CASE form that matches a single expression against literal values, the searched CASE evaluates arbitrary Boolean predicates, giving it the flexibility to handle range-based bucketing and complex flag logic involving multiple columns. Each WHEN clause is evaluated in declaration order; the first clause whose predicate returns TRUE determines the result, and all subsequent clauses are skipped. If no predicate matches, the ELSE clause fires—or NULL is returned if ELSE is omitted.

SEARCHED CASE SYNTAX
CASE WHEN predicate₁ THEN result₁ WHEN predicate₂ THEN result₂ … ELSE default_result END
predicateₙ = any Boolean expression (e.g., amount >= 100); resultₙ = scalar value or expression returned when predicateₙ is the first TRUE match.

Flag Pattern (Binary Output)

FLAG IDIOM
CASE WHEN condition THEN 1 ELSE 0 END AS flag_name
Returns INTEGER 1 when the condition holds, 0 otherwise. Some dialects support IIF(condition, 1, 0) as syntactic sugar (SQL Server, SQLite). PostgreSQL and MySQL allow direct (condition)::INT casting.

Bucket Pattern (Multi-Level Output)

BUCKET IDIOM
CASE WHEN value < t₁ THEN 'Bucket A' WHEN value < t₂ THEN 'Bucket B' … ELSE 'Bucket N' END AS bucket_name
t₁ < t₂ < … < tₙ₋₁ are the boundary thresholds. Because evaluation is sequential and earlier conditions are already FALSE when later ones are reached, each WHEN implicitly represents the range [tₖ₋₁, tₖ).

WIDTH_BUCKET for Equal-Width Binning

WIDTH_BUCKET SYNTAX
WIDTH_BUCKET(expression, min_bound, max_bound, num_buckets)
Returns an INTEGER from 0 to num_buckets + 1. Bucket 0 catches values below min_bound; bucket num_buckets + 1 catches values ≥ max_bound. Each interior bucket k covers the range [min_bound + (k−1) × w, min_bound + k × w) where w = (max_bound − min_bound) ÷ num_buckets.
NULL Handling
Both CASE and WIDTH_BUCKET return NULL when the input expression is NULL. In flag columns, a NULL is semantically different from 0 (FALSE)—it means "unknown." Always decide upfront whether NULLs should map to 0 (via COALESCE(column, default)) or remain NULL. This choice affects downstream aggregations like SUM(is_high_value) and AVG(is_high_value).

Date-Based Flags & Buckets

Date and timestamp fields require an additional step compared to plain numeric columns: you must first extract or compute a numeric proxy from the date, then apply the flag or bucket logic to that proxy. Common proxies include the day-of-week (for weekday/weekend flags), the month or quarter (for seasonal buckets), and the days elapsed since a reference date (for recency or tenure buckets). The extract-then-bucket pipeline is a two-stage transformation that composes naturally because SQL allows nesting expressions.

Date-based transformations follow a two-stage pipeline: first extract a numeric proxy (day-of-week, month, days elapsed) from the date field, then apply standard flag or bucket logic to that numeric value. The bottom panel lists dialect-specific extraction functions.
Common date-based flag and bucket patterns
Use CaseDate ProxyTypeExample Label(s)
Weekend ordersEXTRACT(DOW FROM order_date)Flagis_weekend → 0 | 1
Fiscal quarterEXTRACT(QUARTER FROM order_date)BucketQ1, Q2, Q3, Q4
Customer tenureDATEDIFF(day, signup_date, CURRENT_DATE)BucketNew, Active, Aging, Dormant
Business hoursEXTRACT(HOUR FROM event_ts)Flagis_business_hours → 0 | 1
SeasonEXTRACT(MONTH FROM order_date)BucketWinter, Spring, Summer, Fall

Worked Example — E-Commerce Order Analysis

Consider an orders table with columns order_id INT, customer_id INT, order_total DECIMAL(10,2), order_date DATE, and item_count INT. We want to derive: (1) a flag is_bulk_order for orders with ≥ 10 items, (2) a bucket revenue_tier segmenting order totals, and (3) a date-based bucket season.

Building Flags & Buckets on the orders Table
1
Step 1 — Define the flag predicateWe want a binary indicator for bulk orders. The predicate is straightforward: item_count >= 10. Using the flag idiom: CASE WHEN item_count >= 10 THEN 1 ELSE 0 END AS is_bulk_order. We use ELSE 0 rather than omitting ELSE so that NULLs in item_count produce 0, not NULL—assuming the business rule treats missing counts as non-bulk.
CASE WHEN item_count >= 10 THEN 1 ELSE 0 END AS is_bulk_order
2
Step 2 — Define the revenue bucket thresholdsThe business defines four revenue tiers: Micro (< $25), Standard ($25–$99.99), Premium ($100–$499.99), and Enterprise (≥ $500). We write the CASE with ascending thresholds, exploiting sequential evaluation: once order_total < 25 is FALSE, we know order_total >= 25, so the next clause only needs < 100 to represent the range [25, 100).
CASE WHEN order_total < 25 THEN 'Micro' WHEN order_total < 100 THEN 'Standard' WHEN order_total < 500 THEN 'Premium' ELSE 'Enterprise' END AS revenue_tier
3
Step 3 — Extract month and build the season bucketTo derive a season bucket, we first extract the month number using EXTRACT(MONTH FROM order_date). Then we map month ranges to seasons. Note that the month extraction can be nested directly inside the CASE: CASE WHEN EXTRACT(MONTH FROM order_date) IN (12, 1, 2) THEN 'Winter' .... The IN operator keeps the code readable when a bucket corresponds to a set of discrete values rather than a continuous range.
CASE WHEN EXTRACT(MONTH FROM order_date) IN (12,1,2) THEN 'Winter' WHEN EXTRACT(MONTH FROM order_date) IN (3,4,5) THEN 'Spring' WHEN EXTRACT(MONTH FROM order_date) IN (6,7,8) THEN 'Summer' ELSE 'Fall' END AS season
4
Step 4 — Compose the full SELECT statementWe combine all three derived columns alongside the original fields into a single query. Each CASE expression is an independent column alias. The query engine evaluates them independently per row—there is no performance penalty for multiple CASE expressions in a single SELECT, as each is computed in a single pass over the data.
SELECT order_id, customer_id, order_total, order_date, item_count, CASE WHEN item_count >= 10 THEN 1 ELSE 0 END AS is_bulk_order, CASE WHEN order_total < 25 THEN 'Micro' WHEN order_total < 100 THEN 'Standard' WHEN order_total < 500 THEN 'Premium' ELSE 'Enterprise' END AS revenue_tier, CASE WHEN EXTRACT(MONTH FROM order_date) IN (12,1,2) THEN 'Winter' WHEN EXTRACT(MONTH FROM order_date) IN (3,4,5) THEN 'Spring' WHEN EXTRACT(MONTH FROM order_date) IN (6,7,8) THEN 'Summer' ELSE 'Fall' END AS season FROM orders;
5
Step 5 — Verify with aggregate validationA critical post-step is validating that the buckets are MECE. Wrap the query in a CTE and compute GROUP BY revenue_tier with COUNT(*) and MIN(order_total), MAX(order_total) to confirm each bucket's range boundaries match your intent. Also check for NULL in the bucket column: WHERE revenue_tier IS NULL should return zero rows if your ELSE clause is correct.
WITH tagged AS ( ...full query above... ) SELECT revenue_tier, COUNT(*) AS cnt, MIN(order_total) AS min_val, MAX(order_total) AS max_val FROM tagged GROUP BY revenue_tier ORDER BY min_val;

CASE vs. WIDTH_BUCKET vs. Application Logic

Multiple strategies exist for creating flags and buckets, each with distinct strengths. The searched CASE expression is the most portable and flexible approach, while WIDTH_BUCKET offers terser syntax for uniform-width binning. Application-layer bucketing (e.g., in Python or Java after querying raw data) provides the greatest programmatic flexibility but pushes more data across the network and duplicates logic across codebases.

Comparison of bucketing strategies across key dimensions
CriterionSearched CASEWIDTH_BUCKETApplication Layer
PortabilityAll SQL dialects (SQL-92+)PostgreSQL, Oracle, DB2; limited in MySQL/SQL ServerLanguage-dependent, not portable across SQL clients
Custom thresholdsFully custom, non-uniform widths supportedEqual-width only; custom requires post-mappingFully custom
ConcisenessVerbose for many bucketsSingle function callVaries (pandas.cut is terse)
PerformanceComputed in-engine, single pass per rowComputed in-engine, potentially optimized internallyRequires transferring raw data; adds latency
Label controlReturns any scalar (string, int, etc.)Returns integer bucket ID; needs CASE to map to labelFull programmatic control
ComposabilityCan reference other columns, subqueries, functionsSingle numeric expression onlyUnlimited
WHEN TO USE WHICH
In practice, the searched CASE expression is the default tool because it handles both flags and non-uniform buckets with full label control. Reserve WIDTH_BUCKET for exploratory histogram analysis where equal-width bins are appropriate, and defer to application-layer logic only when the bucketing rules depend on runtime configuration or machine-learning model outputs that change per request.

Connection to Advanced Techniques

Flags and buckets are foundational building blocks that connect to several advanced SQL and data engineering topics. Understanding these connections helps you recognize when a simple CASE expression evolves into a more powerful construct. Window-based flags combine CASE with window functions to create relative flags—for instance, flagging whether a row's value exceeds the group median. Quantile buckets use NTILE(n) to partition rows into equal-count groups rather than equal-width ranges. Feature engineering in machine-learning pipelines frequently uses bucketed columns as categorical features for models trained on SQL-derived feature tables.

From basic flags & buckets to advanced SQL patterns
Basic TechniqueAdvanced ExtensionKey Difference
CASE flag (absolute threshold)CASE + window function (relative threshold)Threshold is computed per partition (e.g., per customer segment) rather than hardcoded
WIDTH_BUCKET (equal-width)NTILE(n) OVER (ORDER BY col)NTILE creates equal-count buckets; WIDTH_BUCKET creates equal-width buckets
Inline CASE in SELECTMaterialized/computed columns or viewsPersisted flags avoid recomputation; useful when the CASE logic is expensive or frequently reused
Static date bucketsDynamic cohort analysis with date arithmeticCohorts are computed relative to each row's anchor date (e.g., days since first purchase)

As you advance, you will encounter scenarios where bucket boundaries are not known at query-authoring time—they may come from a lookup table, a percentile calculation, or even a configuration service. In these cases, joining against a range table (sometimes called a bins table) using inequality joins replaces the CASE expression entirely, yielding a data-driven bucketing strategy that requires no query modifications when thresholds change. This pattern is central to data warehousing methodologies like the Kimball model, where flags and buckets often live as attributes in slowly changing dimension tables.

Practice Problems

PROBLEM 1CONCEPTUAL
Explain why the order of WHEN clauses in a searched CASE expression matters when constructing buckets. What would happen if you wrote CASE WHEN score < 90 THEN 'B' WHEN score < 60 THEN 'F' ELSE 'A' END to grade students?
PROBLEM 2BASIC CALCULATION
Given a table products(product_id INT, price DECIMAL(8,2)), write a SQL query that adds a flag column is_expensive (1 if price ≥ 100, else 0) and a bucket column price_band with labels: 'Budget' (< $25), 'Mid-range' ($25–$99.99), 'Premium' ($100–$499.99), 'Luxury' (≥ $500).
PROBLEM 3INTERMEDIATE
You have a table logins(user_id INT, login_ts TIMESTAMP). Write a query that produces, for each login, (1) a flag is_weekend_login and (2) a bucket time_of_day with values 'Morning' (6–11), 'Afternoon' (12–17), 'Evening' (18–22), 'Night' (23–5). Assume PostgreSQL syntax.
PROBLEM 4APPLIED
A SaaS company stores subscription data in subscriptions(sub_id INT, start_date DATE, monthly_revenue DECIMAL(10,2), status VARCHAR(20)). Write a query that computes the number of days since each subscription started (tenure), creates a tenure bucket ('Trial' ≤ 14 days, 'Onboarding' 15–90, 'Established' 91–365, 'Veteran' > 365), creates a flag is_high_value for monthly_revenue ≥ 500, and then aggregates to show count and average revenue per tenure_bucket and is_high_value combination.
PROBLEM 5CRITICAL THINKING
A colleague proposes replacing all CASE-based buckets with WIDTH_BUCKET for simplicity. They also suggest that flags are unnecessary because a bucket with two levels is equivalent. Critically evaluate both claims. Under what conditions is each claim valid, and when does it break down? Provide a concrete example where WIDTH_BUCKET cannot replicate CASE behavior and another where a two-level bucket is semantically different from a flag.

Lesson Summary

This lesson introduced two essential SQL data transformation patterns: flags (binary indicators derived from a single Boolean predicate) and buckets (multi-level categories created by partitioning continuous domains into labeled segments). The primary SQL construct for both patterns is the searched CASE expression, which evaluates WHEN clauses in sequential order—making the ordering of conditions semantically critical. For date fields, a two-stage pipeline first extracts a numeric proxy (month, day-of-week, days elapsed) using functions like EXTRACT() or DATEDIFF(), then applies standard CASE logic.

The alternative WIDTH_BUCKET function handles equal-width numeric binning concisely but lacks support for non-uniform thresholds and descriptive labels. All bucket designs should satisfy the MECE principle (mutually exclusive, collectively exhaustive)—ensured by proper threshold ordering and an ELSE clause. Advanced extensions include window-based relative flags, quantile bucketing with NTILE(), and data-driven range-table joins that externalize bucket boundaries for configurability.

Varsity Tutors • SQL • Creating Flags & Buckets