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
- Column aliases with
AS - When an alias needs double quotes
- Table aliases
- Calculated columns: arithmetic over columns
- The star expression: the amount of an order line
- Text concatenation:
||andCONCAT ROUNDfor presenting money- Why an alias doesn't work in
WHEREbut does inORDER BY - Common Mistakes and Tips
- Exercises
- Conclusion
- Column aliases with
AS
ASAn alias is the name you want a column to have in the result. You declare it with AS:
First of all, let's see why it's needed. Without an alias, an expression has no name:
| 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:
| 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 |
| ... |
- 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 |
| 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.
- Table aliases
Tables take aliases too, and here AS is equally optional:
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:
ERROR: invalid reference to FROM-clause entry for table "products" HINT: Perhaps you meant to reference the table alias "p".
- 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
| 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
| 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:
- The parentheses are mandatory. Without them,
price - cost / price * 100would evaluate asprice - ((cost / price) * 100), because*and/have higher priority than-(precedence table from 01-03). The result for product 1 would be12.50 - 62.4 = -49.90. It raises no error: it gives an absurd number. - The division is decimal, not integer, because
priceisNUMERICand notINTEGER. If the columns were integers,(price - cost) / pricewould give0for every row and then0 * 100 = 0. It's the classic failure when computing percentages; you solve it by casting one operand toNUMERICwithCAST(module 6). - The tail of decimals is ugly. PostgreSQL computes the
NUMERICdivision with generous precision. To present it you needROUND(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 * priceyou computedsomething / stock, product 13 (stock 0) would blow up the whole query withERROR: division by zero. You protect against it withNULLIF, covered in module 6.
- 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:
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.
- Text concatenation:
|| and CONCAT
|| and CONCATThe standard concatenation operator in SQL is ||:
| 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.
| 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.
| 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'returns0, because there||meansOR. It only behaves as concatenation if the server hasPIPES_AS_CONCATmode enabled. If you write SQL that has to work on both MySQL and PostgreSQL, useCONCAT.
ROUND for presenting money
ROUND for presenting moneyROUND(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:
| 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.
- Why an alias doesn't work in
WHERE but does in ORDER BY
WHERE but does in ORDER BYThis is where the logical order from 02-01 stops being theory. Try filtering by a calculated column:
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):
| 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
ASand losing a comma.SELECT name price FROM productsdoesn't fail: it returns the names under the headerprice. 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 PriceVATto keep the capitals. Unquoted it folds topricevat. - 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 * 100raises no error and returns meaningless numbers. - Confusing the discount with the discounted amount. The amount charged is
quantity * unit_price * (1 - discount); without the1 -you compute exactly the opposite. - Concatenating with
||over columns that can be null. You lose the whole string. UseCONCATor, 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
SELECTand noFROM.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
| 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
ASnames any column or expression; without one you'll see?column?. WriteASalways, 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: anyNULLoperand wipes out the whole string.CONCATavoids 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
WHEREand does inORDER 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
- What is SQL?
- Setting up your SQL environment
- Basic SQL syntax
- Understanding databases and tables
- The relational model: primary and foreign keys
- The course database: GreenStore
Module 2: Basic SQL queries
- The SELECT statement
- Aliases, expressions and calculated columns
- Filtering data with WHERE
- DISTINCT and removing duplicates
- Sorting data with ORDER BY
- Limiting results with LIMIT
Module 3: Working with multiple tables
- JOIN operations
- INNER JOIN
- LEFT JOIN
- RIGHT JOIN
- FULL OUTER JOIN
- SELF JOIN and CROSS JOIN
- Set operations: UNION, INTERSECT and EXCEPT
Module 4: Advanced data filtering
- Using LIKE for pattern matching
- The IN and BETWEEN operators
- NULL values and IS NULL
- Aggregate functions: COUNT, SUM, AVG, MIN and MAX
- Aggregating data with GROUP BY
- The HAVING clause
Module 5: Data manipulation
- Creating tables and constraints with CREATE TABLE
- The INSERT statement
- The UPDATE statement
- The DELETE statement
- The UPSERT (MERGE) statement
- Changing the schema: ALTER TABLE and safe migrations
Module 6: Advanced SQL functions
- String functions
- Numeric functions
- Date and time functions
- Type conversion and handling NULL: CAST and COALESCE
- Conditional expressions
Module 7: Subqueries and nested queries
- Introduction to subqueries
- Correlated subqueries
- EXISTS and NOT EXISTS
- Using subqueries in SELECT, FROM and WHERE
- Subquery or JOIN: which one to choose
Module 8: Indexes and performance tuning
- Understanding indexes
- Creating and managing indexes
- Index types and when not to index
- Query optimization techniques
- Analyzing query performance
Module 9: Transactions and concurrency
- Introduction to transactions
- ACID properties
- Transaction control statements
- Isolation levels and concurrency anomalies
- Handling concurrency: locks and deadlocks
Module 10: Advanced topics
- Views
- Common table expressions (CTEs)
- Window functions
- Stored procedures
- Triggers
- JSON and semi-structured data
Module 11: SQL in practice
- Real-world use cases
- Best practices
- Security: SQL injection, permissions and roles
- SQL for data analysis
- SQL in web development
