LEARN TO CODE | SQL | 02 RETRIEVING DATA

02.4 DISTINCT and calculated columns

Remove duplicate result combinations and calculate useful output columns while leaving the stored data unchanged.

01 | FROM STORED VALUES TO USEFUL OUTPUT

The database stores each club’s category, but an organiser may want a list of category choices without repetition. It stores fees in cents, but a report may need a human-readable amount.

DISTINCT controls duplicate result rows. Expressions calculate values for each result row. Neither needs to change the original table.

Use this section’s expanded Clubs table. It is a separate retrieval dataset , not a migration of the Section 01 database. All examples use SQLite. The queries below read data without changing stored records.

Set up the shared dataset and run queries in VS Code

Create sql_retrieval.py in VS Code and paste this complete Python runner. With your Python interpreter and Python extension selected, click Run Python File. Standard Python installations normally include sqlite3.

Change only the SQL inside query = """...""" for each exercise. Keep setup_sql unchanged. Click Run again to rebuild the same in-memory dataset and print the new query result in the terminal. Python prints None for SQL NULL and quotes empty strings as ''. No data persists after the program ends.

import sqlite3

connection = sqlite3.connect(":memory:")
setup_sql = """
CREATE TABLE Clubs (
    club_id INTEGER PRIMARY KEY,
    club_name TEXT NOT NULL,
    category TEXT NOT NULL,
    capacity INTEGER NOT NULL,
    room TEXT,
    fee_cents INTEGER NOT NULL,
    session_minutes INTEGER NOT NULL
);

INSERT INTO Clubs
    (club_id, club_name, category, capacity, room, fee_cents, session_minutes)
VALUES
    (1, 'Robotics', 'Technology', 12, 'AIR Lab', 1500, 90),
    (2, 'Game Design', 'Technology', 16, 'Room 12', 0, 60),
    (3, 'Art', 'Creative', 12, 'Studio', 800, 60),
    (4, 'Film', 'Creative', 20, NULL, 1000, 90),
    (5, 'Chess', 'Strategy', 16, 'Room 12', 0, 45),
    (6, 'Coding', 'Technology', 20, 'AIR Lab', 0, 60),
    (7, 'Drama', 'Creative', 24, NULL, 1200, 90),
    (8, 'Robotics Junior', 'Technology', 10, '', 500, 45);
"""

query = """
SELECT club_id, club_name, category, capacity, room, fee_cents, session_minutes
FROM Clubs
ORDER BY club_id;
"""

try:
    connection.executescript(setup_sql)
    cursor = connection.execute(query)
    print(" | ".join(column[0] for column in cursor.description))
    for row in cursor.fetchall():
        print(row)
except sqlite3.Error as error:
    print("Database error:", error)
finally:
    connection.close()

02 | SHARED REFERENCE DATA

Expected result on the shared dataset
club_id club_name category capacity room fee_cents session_minutes
1 Robotics Technology 12 AIR Lab 1500 90
2 Game Design Technology 16 Room 12 0 60
3 Art Creative 12 Studio 800 60
4 Film Creative 20 NULL 1000 90
5 Chess Strategy 16 Room 12 0 45
6 Coding Technology 20 AIR Lab 0 60
7 Drama Creative 24 NULL 1200 90
8 Robotics Junior Technology 10 (empty text) 500 45

Money is stored as integer cents to avoid using binary floating point as the stored monetary amount. fee_cents = 0 means free, not unknown. Film and Drama have NULL rooms; Robotics Junior has an empty text room, deliberately included so you can compare the two situations.

03 | DISTINCT REMOVES DUPLICATE RESULT ROWS

SELECT DISTINCT category
FROM Clubs
ORDER BY category;
Expected result on the shared dataset
category
Creative
Strategy
Technology

Technology appears in several source records but only once in this result. DISTINCT does not delete duplicate source records; it removes duplicate combinations from the selected output.

Order is still a separate concern. DISTINCT alone does not guarantee an alphabetical result.

04 | TWO COLUMNS MEAN DISTINCT PAIRS

SELECT DISTINCT category, capacity
FROM Clubs
ORDER BY category, capacity;
Expected result on the shared dataset
category capacity
Creative 12
Creative 20
Creative 24
Strategy 16
Technology 10
Technology 12
Technology 16
Technology 20

DISTINCT applies to the complete selected combination. Technology can appear several times when paired with different capacities.

Adding a unique club_id would make every selected row distinct, so it would not produce one row per category. Choose the columns that match the question.

05 | AS NAMES A RESULT COLUMN

SELECT club_name AS name, capacity AS maximum_members
FROM Clubs
ORDER BY club_id;
Expected result on the shared dataset
name maximum_members
Robotics 12
Game Design 16
Art 12
Film 20
Chess 16
Coding 20
Drama 24
Robotics Junior 10

AS supplies an alias, a heading used in the query result. It does not rename the stored database column.

Clear aliases help when an expression would otherwise produce a long heading. Simple names with underscores are easy to reuse in ORDER BY.

06 | CALCULATE A PER-ROW VALUE

SELECT club_name, session_minutes,
       session_minutes * 4 AS four_session_minutes
FROM Clubs
ORDER BY club_id;
Expected result on the shared dataset
club_name session_minutes four_session_minutes
Robotics 90 360
Game Design 60 240
Art 60 240
Film 90 360
Chess 45 180
Coding 60 240
Drama 90 360
Robotics Junior 45 180

For each source row, multiply the duration by four. A 90-minute session gives 360 minutes across four equal sessions.

This is a row calculation, not a total across all clubs. Aggregates such as SUM are taught in the next section. No new stored column has been created.

07 | DIVISION IN SQLITE NEEDS CARE

SELECT club_name, fee_cents / 100 AS truncated_amount,
       fee_cents / 100.0 AS amount
FROM Clubs
ORDER BY club_id;
Expected result on the shared dataset
club_name truncated_amount amount
Robotics 15 15.0
Game Design 0 0.0
Art 8 8.0
Film 10 10.0
Chess 0 0.0
Coding 0 0.0
Drama 12 12.0
Robotics Junior 5 5.0

SQLite uses integer division when both operands are integers, truncating towards zero. Our sample fees happen to be multiples of 100, which can hide that problem.

SELECT 1550 / 100 AS integer_result,
       1550 / 100.0 AS decimal_result;
Expected result on the shared dataset
integer_result decimal_result
15 15.5

Using 100.0 forces real-number arithmetic here: 15.5 rather than 15. Monetary values remain stored as integer cents; a display calculation is not a reason to store exact money as binary floating point.

08 | PARENTHESES AND MEANING

SELECT club_name,
       (fee_cents + 200) / 100.0 AS amount_with_surcharge
FROM Clubs
ORDER BY club_id;
Expected result on the shared dataset
club_name amount_with_surcharge
Robotics 17.0
Game Design 2.0
Art 10.0
Film 12.0
Chess 2.0
Coding 2.0
Drama 14.0
Robotics Junior 7.0

Add a fictional 200-cent surcharge before converting to units. Without the parentheses, division would apply to 200 first and produce a different calculation.

Use units to check reasoning: fee_cents + 200 is still cents, then dividing by 100 converts the display amount. Do not combine minutes and cents just because both are numeric.

09 | CONCATENATE TEXT

SELECT club_name || ' (' || category || ')' AS club_label
FROM Clubs
ORDER BY club_id;
Expected result on the shared dataset
club_label
Robotics (Technology)
Game Design (Technology)
Art (Creative)
Film (Creative)
Chess (Strategy)
Coding (Technology)
Drama (Creative)
Robotics Junior (Technology)

In SQLite, || concatenates text. The spaces and parentheses inside the quoted strings become part of the output.

Do not assume + concatenates SQL text; in SQLite it is an arithmetic operator. String-concatenation syntax varies by database product.

10 | HANDLE MISSING VALUES DELIBERATELY

SELECT club_name,
       COALESCE(room, 'To be confirmed') AS room_display
FROM Clubs
ORDER BY club_id;
Expected result on the shared dataset
club_name room_display
Robotics AIR Lab
Game Design Room 12
Art Studio
Film To be confirmed
Chess Room 12
Coding AIR Lab
Drama To be confirmed
Robotics Junior (empty text)

COALESCE returns the first non-NULL argument. It substitutes a display label for Film and Drama, but leaves Robotics Junior’s empty string unchanged.

Arithmetic or text concatenation involving NULL usually produces NULL. For example, a club name concatenated with an unknown room produces NULL unless you deliberately supply a replacement. Do not silently turn unknown numeric values into zero unless that matches their meaning.

11 | FILTER SOURCE FACTS; SORT THE RESULT

SELECT club_name, fee_cents / 100.0 AS amount
FROM Clubs
WHERE fee_cents >= 500
ORDER BY amount DESC, club_id ASC;
Expected result on the shared dataset
club_name amount
Robotics 15.0
Drama 12.0
Film 10.0
Art 8.0
Robotics Junior 5.0

Use source columns in WHERE for clear, portable filtering. Use the alias in ORDER BY to sort the displayed calculation. SQLite supports some extra alias behaviour, but relying on it can make queries less portable.

TRY IT YOURSELF

Show distinct non-NULL room values in ascending order. Explain why one looks empty.

Reveal a possible query and result
SELECT DISTINCT room
FROM Clubs
WHERE room IS NOT NULL
ORDER BY room;
Expected result on the shared dataset
room
(empty text)
AIR Lab
Room 12
Studio

The empty string is distinct and non-NULL, so it remains. Use an additional blank-value condition if the question requires usable room names.

12 | REPORT CHALLENGE

TRY IT YOURSELF

For Technology clubs only, show the name, fee in units and minutes for four sessions. Sort by fee descending, then club_id ascending.

Reveal a possible query and result
SELECT club_name, fee_cents / 100.0 AS amount,
       session_minutes * 4 AS four_session_minutes
FROM Clubs
WHERE category = 'Technology'
ORDER BY amount DESC, club_id ASC;
Expected result on the shared dataset
club_name amount four_session_minutes
Robotics 15.0 360
Robotics Junior 5.0 180
Game Design 0.0 240
Coding 0.0 240

Filtering, calculation and sorting have different roles. The query changes presentation, not stored fees or durations.

You are ready for the section challenges. Later, joins and aggregates will let us combine related tables and summarise several records at once.

KEEP LEARNING

Previous page 02 Retrieving data: challenges

Reference: SQLite SELECT · SQLite expressions and filters