In the previous lesson you returned data exactly as it's stored. But a database doesn't store the price with VAT, nor a product's margin, nor the amount of an order line: it stores the minimal pieces and everything else is computed at query time. In this lesson you'll learn to build those calculated columns with arithmetic and text expressions, to give them a readable name with AS, and to understand —using the logical execution order from 02-01— why that name can be used in some parts of the query and not in others. This is also where the most important expression of the whole course shows up: quantity * unit_price * (1 - discount).

Contents

  1. Column aliases with AS
  2. When an alias needs double quotes
  3. Table aliases
  4. Calculated columns: arithmetic over columns
  5. The star expression: the amount of an order line
  6. Text concatenation: || and CONCAT
  7. ROUND for presenting money
  8. Why an alias doesn't work in WHERE but does in ORDER BY
  9. Common Mistakes and Tips
  10. Exercises
  11. Conclusion

  1. Column aliases with AS

An alias is the name you want a column to have in the result. You declare it with AS:

SELECT name  AS product,
       price AS rrp
FROM products;

First of all, let's see why it's needed. Without an alias, an expression has no name:

SELECT name, price * 1.21
FROM products;
name ?column?
Extra virgin olive oil 500 ml 15.1250
... ...

?column? is PostgreSQL's way of saying "this has no name". A report with that header is useless, and a program accessing the result by column name will find nothing. With an alias:

SELECT name                 AS product,
       price                AS price_without_vat,
       price * 1.21         AS price_with_vat
FROM products;
product price_without_vat price_with_vat
Extra virgin olive oil 500 ml 12.50 15.1250
Organic brown rice 1 kg 3.90 4.7190
Raw orange blossom honey 500 g 9.75 11.7975
Spelt pasta 500 g 2.80 3.3880
Organic crushed tomato 400 g 1.95 2.3595
Aloe vera face cream 50 ml 18.90 22.8690
Rosemary solid shampoo 80 g 8.40 10.1640
Almond body oil 200 ml 14.25 17.2425
Calendula lip balm 15 ml 4.60 5.5660
Concentrated eco laundry detergent 1 L 11.20 13.5520

(First 10 of 20 rows.)

The word AS is optional for columns. These two lines are equivalent:

SELECT price * 1.21 AS price_with_vat FROM products;
SELECT price * 1.21    price_with_vat FROM products;

Even so, always write it. Without AS, forgetting a comma turns one column into the alias of the previous one and the query runs with no error, giving a wrong result (you saw it in 01-03 and in 02-01):

SELECT name price FROM products;   -- a single column, called "price",
                                   -- holding the NAMES of the products
price
Extra virgin olive oil 500 ml
...

  1. When an alias needs double quotes

An alias is an identifier, so it follows the rules from 01-03: unquoted it folds to lowercase and only accepts letters, digits and underscores.

Alias Does it need quotes? Result
AS price_with_vat No Column price_with_vat
AS PriceWithVAT No, but... The column will be called pricewithvat (folded to lowercase)
AS "Price with VAT" Yes (it has spaces) Column Price with VAT
AS "Margin %" Yes (it has a %) Column Margin %
AS order Yes (order is a reserved word) Error without quotes
AS 'Price' Never Error: single quotes are for strings
SELECT name            AS "Product",
       price           AS "Price (€)",
       price * 1.21    AS "Price with VAT (€)"
FROM products;
Product Price (€) Price with VAT (€)
Ceremonial matcha green tea 30 g 22.00 26.6200
... ... ...

This is the only legitimate use of double quotes in this course: presentation aliases meant for a report or an export. For any alias you're going to reuse inside the query, use lowercase snake_case and save yourself the quotes.

Dialect note: MySQL and SQLite also let you use backticks (`Price with VAT`) or single quotes as an alias delimiter. PostgreSQL doesn't: double quotes or nothing. Always write the standard form.

  1. Table aliases

Tables take aliases too, and here AS is equally optional:

SELECT p.id,
       p.name,
       p.price
FROM products AS p;

With a single table it looks like a whim: p.name adds nothing over name. Its value shows up in module 3, when a query combines several tables that have columns with the same name:

-- A preview of module 3, DON'T run it yet
SELECT p.name AS product,
       c.name AS category
FROM products AS p
JOIN categories AS c ON p.category_id = c.id;

Without the aliases p and c, name would be ambiguous and PostgreSQL would answer column reference "name" is ambiguous. That's why in professional SQL almost every query carries table aliases.

Useful conventions for table aliases:

Practice Example Comment
Initial or short abbreviation products AS p, order_lines AS ol The most widespread
A meaningful name when there's ambiguity employees AS manager, customers AS referrer Essential when joining a table to itself (03-06)
Prefix every column if there's an alias p.name, p.price Avoids surprises when you add tables later
A cryptic single-letter alias products AS x Avoid: unreadable in long queries

One difference that catches people out: if you give the table an alias, the original name stops being available. This fails:

SELECT products.name
FROM products AS p;
ERROR:  invalid reference to FROM-clause entry for table "products"
HINT:  Perhaps you meant to reference the table alias "p".

  1. Calculated columns: arithmetic over columns

A calculated column is an expression in the SELECT list. It's evaluated row by row, with that row's values.

4.1. Gross margin

SELECT name,
       price,
       cost,
       price - cost AS margin
FROM products;
name price cost margin
Extra virgin olive oil 500 ml 12.50 7.80 4.70
Organic brown rice 1 kg 3.90 2.10 1.80
Raw orange blossom honey 500 g 9.75 5.40 4.35
Spelt pasta 500 g 2.80 1.35 1.45
Organic crushed tomato 400 g 1.95 0.90 1.05
Aloe vera face cream 50 ml 18.90 9.50 9.40
Rosemary solid shampoo 80 g 8.40 3.60 4.80
Almond body oil 200 ml 14.25 7.10 7.15
Calendula lip balm 15 ml 4.60 1.80 2.80
Concentrated eco laundry detergent 1 L 11.20 6.00 5.20

(First 10 of 20 rows.)

In subtraction the result's scale is the larger of the two, so the margin comes out with two clean decimals. In multiplication it won't.

4.2. Percentage margin

SELECT name,
       price,
       cost,
       (price - cost) / price * 100 AS margin_pct
FROM products;
name price cost margin_pct
Extra virgin olive oil 500 ml 12.50 7.80 37.6000000000000000
Organic brown rice 1 kg 3.90 2.10 46.1538461538461538
Raw orange blossom honey 500 g 9.75 5.40 44.6153846153846154
Spelt pasta 500 g 2.80 1.35 51.7857142857142857
Organic crushed tomato 400 g 1.95 0.90 53.8461538461538462
Aloe vera face cream 50 ml 18.90 9.50 49.7354497354497354

(First 6 of 20 rows.)

Three things to learn from this:

  1. The parentheses are mandatory. Without them, price - cost / price * 100 would evaluate as price - ((cost / price) * 100), because * and / have higher priority than - (precedence table from 01-03). The result for product 1 would be 12.50 - 62.4 = -49.90. It raises no error: it gives an absurd number.
  2. The division is decimal, not integer, because price is NUMERIC and not INTEGER. If the columns were integers, (price - cost) / price would give 0 for every row and then 0 * 100 = 0. It's the classic failure when computing percentages; you solve it by casting one operand to NUMERIC with CAST (module 6).
  3. The tail of decimals is ugly. PostgreSQL computes the NUMERIC division with generous precision. To present it you need ROUND (section 7).

4.3. Other useful expressions over a single table

SELECT name,
       stock,
       price,
       stock * price AS inventory_value_rrp,
       stock * cost  AS inventory_value_cost
FROM products;
name stock price inventory_value_rrp inventory_value_cost
Extra virgin olive oil 500 ml 120 12.50 1500.00 936.00
Organic brown rice 1 kg 200 3.90 780.00 420.00
Raw orange blossom honey 500 g 80 9.75 780.00 432.00
Spelt pasta 500 g 150 2.80 420.00 202.50
Organic crushed tomato 400 g 300 1.95 585.00 270.00
Soy wax candles (pack of 2) 0 13.75 0.00 0.00

(A selection of rows out of the 20.)

Here stock is INTEGER and price is NUMERIC(10,2): when types mix, PostgreSQL promotes the integer to NUMERIC and the result keeps two decimals.

Careful with division by zero. If instead of stock * price you computed something / stock, product 13 (stock 0) would blow up the whole query with ERROR: division by zero. You protect against it with NULLIF, covered in module 6.

  1. The star expression: the amount of an order line

In 01-06 it was settled that orders has no total column and that a line's amount is:

quantity * unit_price * (1 - discount)

This expression is going to show up in practically every remaining module. Let's write it for the first time:

SELECT id,
       order_id,
       product_id,
       quantity,
       unit_price,
       discount,
       quantity * unit_price * (1 - discount) AS amount
FROM order_lines;
id order_id product_id quantity unit_price discount amount
1 1 1 2 11.95 0.00 23.9000
2 1 2 3 3.90 0.00 11.7000
3 1 14 2 3.25 0.00 6.5000
4 2 6 1 17.50 0.00 17.5000
5 2 9 2 4.60 0.00 9.2000
6 3 5 6 1.95 0.10 10.5300
7 3 4 4 2.80 0.00 11.2000
8 3 2 2 3.90 0.00 7.8000
9 4 15 1 22.00 0.00 22.0000
10 4 3 1 9.75 0.00 9.7500
11 5 10 1 11.20 0.00 11.2000
12 5 11 2 5.50 0.00 11.0000

(First 12 of 47 rows.)

Let's take the expression apart, because every piece matters:

Piece Why it's there
quantity * Units of that product sold in that order
unit_price The price at the moment of the sale, not the current one. That's why line 1 uses €11.95 and not the €12.50 product 1 costs today
(1 - discount) discount is a fraction: 0.10 = 10 %. 1 - 0.10 = 0.90, that is, "90 % is charged"
The parentheses Without them, quantity * unit_price * 1 - discount would subtract €0.10 from the total instead of applying 10 %

Check line 6 by hand: 6 units × €1.95 = €11.70; with a 10 % discount, 11.70 × 0.90 = €10.53. It matches.

And notice the four decimals: 10.5300, not 10.53. The quantity is an integer and contributes no scale, unit_price contributes 2 and (1 - discount) another 2, so the result comes out with 4. Arithmetically it's correct and for adding up it's what you want, but to show it to somebody you have to round.

Six of the 47 lines carry a discount; these are their exact amounts:

id order_id quantity unit_price discount amount
6 3 6 1.95 0.10 10.5300
18 8 3 12.50 0.05 35.6250
24 10 2 18.90 0.10 34.0200
27 11 8 1.95 0.15 13.2600
39 16 2 11.20 0.05 21.2800
45 19 6 4.95 0.10 26.7300

There's a revealing detail there: line 24 is worth €34.0200 and the return for order 10 recorded in returns is €34.02. The data is consistent across tables, and checking that with a query will be one of module 3's exercises.

Frequent error: writing quantity * unit_price * discount. That computes the discounted amount, not the amount charged. For line 6 it would give €1.17 instead of €10.53.

  1. Text concatenation: || and CONCAT

The standard concatenation operator in SQL is ||:

SELECT name || ' ' || last_name AS customer,
       city,
       country
FROM customers;
customer city country
Lucía Martínez Soler Valencia Spain
Carlos Ferrer Ibáñez Valencia Spain
Marta Sanchis Gil Castellón Spain
Javier Ortega Ruiz Madrid Spain
Ana Belmonte Roca Barcelona Spain
Pau Llorens Vidal Valencia Spain
Sofia Moreira Costa Lisbon Portugal
Tiago Almeida Nunes Porto Portugal
Camille Dubois Lyon France
Julien Moreau Paris France
Elena Navarro Puig Alicante Spain
Diego Ramos Herrera Seville Spain
Núria Bosch Ferrer Barcelona Spain
Hugo Iglesias Pardo Zaragoza Spain
Inés Carrasco Vega Valencia Spain

You can concatenate text with numbers: PostgreSQL converts the number to text automatically.

SELECT name,
       'Ref. ' || id || ' - ' || price || ' EUR' AS label
FROM products;
name label
Extra virgin olive oil 500 ml Ref. 1 - 12.50 EUR
Organic brown rice 1 kg Ref. 2 - 3.90 EUR
Raw orange blossom honey 500 g Ref. 3 - 9.75 EUR

(First 3 of 20 rows.)

6.1. The || trap with NULL

Concatenating anything with NULL gives NULL. All the text is lost. This is one of SQL's most expensive surprises, and GreenStore has the data to demonstrate it: seven customers have referred_by_id set to NULL.

SELECT name,
       referred_by_id,
       'Referred by customer ' || referred_by_id AS source
FROM customers;
name referred_by_id source
Lucía (null) (null)
Carlos 1 Referred by customer 1
Marta 1 Referred by customer 1
Javier (null) (null)
Ana 2 Referred by customer 2
Pau (null) (null)
Sofia (null) (null)
Tiago 7 Referred by customer 7
Camille (null) (null)
Julien 9 Referred by customer 9
Elena 6 Referred by customer 6
Diego (null) (null)
Núria 5 Referred by customer 5
Hugo (null) (null)
Inés 1 Referred by customer 1

Seven rows with an empty source, not seven rows with the text and a gap. The literal 'Referred by customer ' vanishes completely, because the rule for nulls is that any operation with an unknown value produces an unknown value.

6.2. CONCAT treats nulls differently

The CONCAT function ignores NULLs and replaces them with an empty string:

SELECT name,
       referred_by_id,
       CONCAT('Referred by customer ', referred_by_id) AS source
FROM customers;
name referred_by_id source
Lucía (null) Referred by customer
Carlos 1 Referred by customer 1
Javier (null) Referred by customer
Ana 2 Referred by customer 2

(A selection of rows out of the 15.)

The text isn't lost any more, but the result isn't right either: it says "Referred by customer" without saying which one. The real solution is to replace the null with sensible text using COALESCE, studied in lesson 06-04:

-- A preview of module 6
SELECT name,
       COALESCE('Referred by customer ' || referred_by_id, 'Arrived on their own') AS source
FROM customers;

A comparison to keep handy:

| Aspect | a || b | CONCAT(a, b) | |---|---|---| | SQL standard | Yes | No (but very widespread) | | With NULL | Returns NULL | Treats the NULL as '' | | In MySQL | By default it doesn't concatenate: || is logical OR | It's the usual form | | In SQL Server | Doesn't exist; + is used | Available since 2012 | | In SQLite | Yes | Doesn't exist |

Important dialect note: in MySQL, 'a' || 'b' returns 0, because there || means OR. It only behaves as concatenation if the server has PIPES_AS_CONCAT mode enabled. If you write SQL that has to work on both MySQL and PostgreSQL, use CONCAT.

  1. ROUND for presenting money

ROUND(expression, decimals) rounds to the number of decimals you specify:

SELECT name,
       price,
       ROUND(price * 1.21, 2)                  AS price_with_vat,
       ROUND((price - cost) / price * 100, 2)  AS margin_pct
FROM products;
name price price_with_vat margin_pct
Extra virgin olive oil 500 ml 12.50 15.13 37.60
Organic brown rice 1 kg 3.90 4.72 46.15
Raw orange blossom honey 500 g 9.75 11.80 44.62
Spelt pasta 500 g 2.80 3.39 51.79
Organic crushed tomato 400 g 1.95 2.36 53.85
Aloe vera face cream 50 ml 18.90 22.87 49.74
Rosemary solid shampoo 80 g 8.40 10.16 57.14
Almond body oil 200 ml 14.25 17.24 50.18
Calendula lip balm 15 ml 4.60 5.57 60.87
Concentrated eco laundry detergent 1 L 11.20 13.55 46.43
Loofah scrubber (pack of 3) 5.50 6.66 60.00
Reusable cotton bags (pack of 5) 9.90 11.98 56.57
Soy wax candles (pack of 2) 13.75 16.64 49.82
Organic chamomile tea 20 bags 3.25 3.93 56.92
Ceremonial matcha green tea 30 g 22.00 26.62 43.18
Ginger kombucha 750 ml 4.95 5.99 53.54
Cold-pressed orange juice 1 L 5.40 6.53 51.85
Bamboo toothbrush 3.50 4.24 65.71
Natural stick deodorant 50 g 7.80 9.44 57.69
Spirulina capsules 120 units 16.40 19.84 46.95

Now that's a presentable table. And with it you can already answer a business question: the product with the highest percentage margin is the bamboo toothbrush (65.71 %), and the one with the lowest, the extra virgin olive oil (37.60 %) —the catalogue's flagship product is, precisely, the one that leaves the thinnest relative margin.

The same for the line amount:

SELECT id,
       order_id,
       ROUND(quantity * unit_price * (1 - discount), 2) AS amount
FROM order_lines;
id order_id amount
6 3 10.53
18 8 35.63
24 10 34.02
27 11 13.26
39 16 21.28
45 19 26.73

(The 6 lines with a discount, out of the 47.)

Look at line 18: the exact value is 35.6250 and ROUND leaves it at 35.63. On NUMERIC types, PostgreSQL rounds "half away from zero", which is what any accountant expects.

Course rule: compute with the full expression and round only at the end, for presentation. If you round each line and then add up, the total can differ by cents from the rounded total. The numeric functions (ROUND, CEIL, FLOOR, TRUNC, ABS) are covered thoroughly in lesson 06-02.

  1. Why an alias doesn't work in WHERE but does in ORDER BY

This is where the logical order from 02-01 stops being theory. Try filtering by a calculated column:

SELECT name,
       price * 1.21 AS price_with_vat
FROM products
WHERE price_with_vat > 20;
ERROR:  column "price_with_vat" does not exist
LINE 4: WHERE price_with_vat > 20;
              ^

The table exists, the alias is written right above, and yet the engine says that column doesn't exist. The explanation is in the diagram:

flowchart LR
    A["1 · FROM<br/>products"] --> B["2 · WHERE<br/>❌ the alias does NOT exist yet"]
    B --> C["3 · SELECT<br/>✅ aliases are born here"]
    C --> D["4 · ORDER BY<br/>✅ the alias already exists"]
    D --> E["5 · LIMIT"]

When WHERE is evaluated, the SELECT step hasn't run yet, so the name price_with_vat hasn't been created. When ORDER BY is evaluated, it has.

The two ways to filter by an expression:

-- Option A: repeat the expression in the WHERE (always works)
SELECT name,
       price * 1.21 AS price_with_vat
FROM products
WHERE price * 1.21 > 20;
name price_with_vat
Aloe vera face cream 50 ml 22.8690
Ceremonial matcha green tea 30 g 26.6200
-- Option B: wrap the query in a subquery (module 7)
SELECT name, price_with_vat
FROM (SELECT name, price * 1.21 AS price_with_vat FROM products) AS t
WHERE price_with_vat > 20;

And in ORDER BY the alias just works (lesson 02-05):

SELECT name,
       price - cost AS margin
FROM products
ORDER BY margin DESC;
name margin
Ceremonial matcha green tea 30 g 9.50
Aloe vera face cream 50 ml 9.40
Spirulina capsules 120 units 7.70
Almond body oil 200 ml 7.15
Soy wax candles (pack of 2) 6.85

(First 5 of 20 rows.)

A summary worth memorising:

Clause Can you use a SELECT alias? Reason
WHERE No It runs before SELECT
GROUP BY Yes in PostgreSQL (an extension), no in the standard Module 4
HAVING No Module 4
ORDER BY Yes It runs after SELECT

Common Mistakes and Tips

  • Forgetting AS and losing a comma. SELECT name price FROM products doesn't fail: it returns the names under the header price. Always count the result's columns.
  • An alias in single quotes. AS 'Price' is a syntax error in PostgreSQL. Double quotes for identifiers, single ones for strings.
  • Expecting AS PriceVAT to keep the capitals. Unquoted it folds to pricevat.
  • Using an alias in WHERE. column "..." does not exist. Repeat the expression or use a subquery.
  • Forgetting the parentheses in the percentage margin. price - cost / price * 100 raises no error and returns meaningless numbers.
  • Confusing the discount with the discounted amount. The amount charged is quantity * unit_price * (1 - discount); without the 1 - you compute exactly the opposite.
  • Concatenating with || over columns that can be null. You lose the whole string. Use CONCAT or, better, COALESCE (module 6).
  • Rounding too early. Round only in the presentation layer; intermediate calculations keep full precision.
  • Tip: name your aliases like real columns. snake_case, lowercase, descriptive: price_with_vat, margin_pct, amount. "Pretty" aliases with quotes are only for the final report.
  • Tip: validate the expression with SELECT and no FROM. SELECT 8 * 1.95 * (1 - 0.15); confirms in a second whether the formula is what you think.

Exercises

Exercise 1

Write a query on products that returns, with readable aliases: the product's name, its price, its unit margin in euros and its percentage margin rounded to one decimal. Explain why the parentheses in the percentage margin are essential.

Exercise 2

On employees, build a single text column called profile with this exact format: Last name, First name (Job title) - City. For example: Alcázar Vives, Rosa (General manager) - Valencia.

Exercise 3

On order_lines, return the line's id, the order it belongs to, the gross amount (before the discount), the discount in euros and the final amount charged, all three rounded to two decimals. Check your results against the six lines that have a discount.

Solutions

Solution 1

SELECT name                                    AS product,
       price,
       price - cost                            AS margin_eur,
       ROUND((price - cost) / price * 100, 1)  AS margin_pct
FROM products;
product price margin_eur margin_pct
Extra virgin olive oil 500 ml 12.50 4.70 37.6
Organic brown rice 1 kg 3.90 1.80 46.2
Raw orange blossom honey 500 g 9.75 4.35 44.6
Spelt pasta 500 g 2.80 1.45 51.8
Organic crushed tomato 400 g 1.95 1.05 53.8
Aloe vera face cream 50 ml 18.90 9.40 49.7
Rosemary solid shampoo 80 g 8.40 4.80 57.1
Almond body oil 200 ml 14.25 7.15 50.2
Calendula lip balm 15 ml 4.60 2.80 60.9
Concentrated eco laundry detergent 1 L 11.20 5.20 46.4

(First 10 of 20 rows.)

The reasoning about the parentheses. The operator precedence from 01-03 says * and / are evaluated before -. Without parentheses, price - cost / price * 100 means price - ((cost / price) * 100). For product 1: 12.50 - ((7.80/12.50) * 100) = 12.50 - 62.40 = -49.90. A margin of -49.90 on a product that earns €4.70 a unit. The query runs without any warning: it's a silent error, the worst kind.

Solution 2

SELECT last_name || ', ' || name || ' (' || job_title || ') - ' || city AS profile
FROM employees;
profile
Alcázar Vives, Rosa (General manager) - Valencia
Company Talens, Andrés (Sales manager) - Valencia
Nadal Ripoll, Beatriz (Logistics manager) - Valencia
Peris Blasco, Óscar (Sales rep) - Valencia
Puig Sanchis, Laia (Sales rep) - Castellón
Estévez Roig, Marc (Customer support) - Valencia
Salvador Mira, Irene (Warehouse operator) - Valencia
Vercher Lluch, Daniel (Data analyst) - Valencia

The reasoning. Columns and text literals alternate: the literals go in single quotes and carry the format's spaces, commas and parentheses inside them. All 8 rows came out because in this table none of the four columns used is null. If city were —and it could be, since the column allows nulls— that row would return NULL in its entirety. In a query destined for production it would be wise to shield it with COALESCE(city, 'Unassigned'), which you'll see in 06-04.

Solution 3

SELECT id,
       order_id,
       ROUND(quantity * unit_price, 2)                    AS gross_amount,
       ROUND(quantity * unit_price * discount, 2)         AS discount_eur,
       ROUND(quantity * unit_price * (1 - discount), 2)   AS final_amount
FROM order_lines;

The six lines with a discount, out of the 47 the query returns:

id order_id gross_amount discount_eur final_amount
6 3 11.70 1.17 10.53
18 8 37.50 1.88 35.63
24 10 37.80 3.78 34.02
27 11 15.60 2.34 13.26
39 16 22.40 1.12 21.28
45 19 29.70 2.97 26.73

The reasoning. The three columns share the base quantity * unit_price; what changes is what's done with discount: multiplying by discount gives what the customer saves, multiplying by (1 - discount) gives what they pay. The check that the formula is right is that discount_eur + final_amount = gross_amount on every row. On line 18 there's an apparent one-cent discrepancy (1.88 + 35.63 = 37.51 ≠ 37.50): it's the effect of rounding each column separately, since the exact values are 1.8750 and 35.6250. It's exactly why you round at the end and not at every intermediate step.

Conclusion

Now your queries don't just read: they compute.

  • An alias with AS names any column or expression; without one you'll see ?column?. Write AS always, even though it's optional.
  • Double quotes in an alias are only necessary if it contains spaces, symbols or capitals you want to keep: use them only for presentation aliases.
  • Table aliases look superfluous with a single table and will be mandatory in module 3.
  • You know how to build calculated columns: price with VAT, margin in euros and percentage margin, with the parentheses in the right place and without falling into integer division.
  • You've got a grip on the expression that holds up the whole course: quantity * unit_price * (1 - discount), and you understand why the discount is a fraction and why the result comes out with four decimals.
  • You concatenate text with || and you know its trap: any NULL operand wipes out the whole string. CONCAT avoids it, COALESCE (module 6) solves it properly.
  • You present amounts with ROUND(expression, 2), rounding only at the end.
  • And, above all, you understand from the logical execution order why an alias doesn't exist yet in WHERE and does in ORDER BY.

In the next lesson, Filtering Data with WHERE, you'll add step 2 of the diagram: you'll stop fetching all 20 rows of products or all 47 of order_lines and start asking only for the ones that meet a condition. With comparisons over numbers, text and dates, and with AND, OR, NOT and a few parentheses that, once again, will make the difference between a correct result and a silently wrong one.

SQL Course

Module 1: Introduction to SQL

Module 2: Basic SQL queries

Module 3: Working with multiple tables

Module 4: Advanced data filtering

Module 5: Data manipulation

Module 6: Advanced SQL functions

Module 7: Subqueries and nested queries

Module 8: Indexes and performance tuning

Module 9: Transactions and concurrency

Module 10: Advanced topics

Module 11: SQL in practice

Module 12: Final project

© Copyright 2026. All rights reserved