DataLens

Aliases and CASE expressions

Name things clearly, label rows with if/then logic, and count several things in one pass with conditional aggregation.

Level: Intermediate · 10 min read

Practise this module

The questions for this lesson run in your browser and check your answer straight away.

The idea

Two tools in this module make queries read like reports:

Column aliases

You've used AS to name calculated columns. A few more details:

SELECT name AS product,
       price AS "Unit Price (Rp)",
       price - cost AS margin
FROM products
ORDER BY margin DESC
LIMIT 3;
 product          | Unit Price (Rp) | margin
------------------+-----------------+--------
 USB-C Hub        |          289000 | 119000
 Pour-over Kettle |          320000 | 110000
 Desk Lamp LED    |          245000 | 105000
(3 rows)

Where can you use an alias? Only in clauses that run after SELECT:

ClauseRunsCan use a SELECT alias?
WHEREbefore SELECTno
HAVINGbefore SELECTno
ORDER BYafter SELECTyes

(PostgreSQL also lets GROUP BY use an alias as a convenience, but many databases don't, so this track repeats the expression there.)

Table aliases

You can also give a table a short name and use it to say which table a column comes from:

SELECT p.name, p.price
FROM products AS p
WHERE p.category = 'Home';
 name           | price
----------------+--------
 Scented Candle |  75000
 Throw Pillow   | 120000
 Bamboo Plant   |  60000
(3 rows)

With one table it's optional. In the next module, when queries combine several tables that share column names, table aliases become essential.

CASE: if/then for each row

CASE checks conditions in order and returns the value of the first one that's true:

SELECT name,
       price,
       CASE
           WHEN price < 100000 THEN 'budget'
           WHEN price < 200000 THEN 'mid-range'
           ELSE 'premium'
       END AS price_band
FROM products
ORDER BY price;
 name                 | price  | price_band
----------------------+--------+------------
 Gel Pen Set          |  38000 | budget
 Notebook A5 Dotted   |  45000 | budget
 Ceramic Mug          |  55000 | budget
 Bamboo Plant         |  60000 | budget
 Robusta Lampung 250g |  65000 | budget
 Scented Candle       |  75000 | budget
 Desk Organizer       |  89000 | budget
 Arabica Gayo 250g    |  95000 | budget
 Toraja Kalosi 250g   | 110000 | mid-range
 Throw Pillow         | 120000 | mid-range
 Wireless Mouse       | 175000 | mid-range
 French Press 600ml   | 185000 | mid-range
 Desk Lamp LED        | 245000 | premium
 USB-C Hub            | 289000 | premium
 Pour-over Kettle     | 320000 | premium
(15 rows)

Because the first match wins, order matters. Watch what happens if the broader condition comes first:

SELECT name,
       price,
       CASE
           WHEN price >= 100000 THEN 'mid-range'
           WHEN price >= 200000 THEN 'premium'
           ELSE 'budget'
       END AS price_band
FROM products
WHERE price >= 200000;
 name             | price  | price_band
------------------+--------+------------
 Pour-over Kettle | 320000 | mid-range
 USB-C Hub        | 289000 | mid-range
 Desk Lamp LED    | 245000 | mid-range
(3 rows)

A Rp 320.000 kettle is labelled "mid-range", because price >= 100000 was true first, and the premium line is never reached. Order conditions from most specific to least specific (or use ranges that can't overlap).

Grouping by a CASE

A CASE creates a new column, and you can group by it like any other:

SELECT CASE
           WHEN price < 100000 THEN 'budget'
           WHEN price < 200000 THEN 'mid-range'
           ELSE 'premium'
       END AS price_band,
       COUNT(*) AS products
FROM products
GROUP BY 1
ORDER BY products DESC;
 price_band | products
------------+----------
 budget     |        8
 mid-range  |        4
 premium    |        3
(3 rows)

GROUP BY 1 means "group by the first column of the SELECT". It saves repeating a long CASE, and it's common in analytics work.

Conditional aggregation

This is one of the most useful patterns in analytics SQL. Count several things in one pass by putting a CASE inside an aggregate:

SELECT channel,
       COUNT(*) AS orders,
       SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) AS completed,
       SUM(CASE WHEN status = 'cancelled' THEN 1 ELSE 0 END) AS cancelled
FROM orders
GROUP BY channel
ORDER BY channel;
 channel | orders | completed | cancelled
---------+--------+-----------+-----------
 app     |     12 |        10 |         2
 web     |     28 |        16 |         8
(2 rows)

Each row adds 1 to completed if it's completed, 0 otherwise, so the SUM counts completed orders. You've turned status values into columns, just like a pivot table.

PostgreSQL has a neater spelling for the same thing, FILTER:

SELECT channel,
       COUNT(*) FILTER (WHERE status = 'completed') AS completed,
       ROUND(100.0 * COUNT(*) FILTER (WHERE status = 'cancelled') / COUNT(*), 1) AS cancel_rate
FROM orders
GROUP BY channel
ORDER BY channel;
 channel | completed | cancel_rate
---------+-----------+-------------
 app     |        10 |        16.7
 web     |        16 |        28.6
(2 rows)

The SUM(CASE ...) version works in every database. FILTER is shorter and reads better, but it's specific to PostgreSQL (and a few others).

Why this matters for clean code

CASE puts business rules in one visible place. "Premium means Rp 200.000 and up" lives in the query, not in someone's memory or a hidden Excel formula. When the rule changes, you change one line. Pair it with clear aliases and your query becomes the documentation of how a number is calculated.

Recap

Practise this module

The questions for this lesson run in your browser and check your answer straight away.