02 · SELECT Basics & Data Types¶
🎥 Video walkthrough¶
SELECT retrieves data. Almost everything you do in SQL revolves around
shaping and refining a SELECT statement, so getting comfortable with its
basic anatomy now pays off for the rest of the course.
We'll use this books table for every example below:
CREATE TABLE books (
id INTEGER PRIMARY KEY,
title TEXT NOT NULL,
author TEXT NOT NULL,
price REAL,
published_year INTEGER
);
INSERT INTO books (title, author, price, published_year) VALUES
('Dune', 'Frank Herbert', 9.99, 1965),
('Foundation', 'Isaac Asimov', 8.50, 1951),
('Neuromancer', 'William Gibson', 7.25, 1984),
('The Left Hand of Darkness', 'Ursula K. Le Guin', 8.99, 1969),
('Snow Crash', 'Neal Stephenson', 10.50, 1992);
Selecting specific columns¶
title author
-------------------------- ------------------
Dune Frank Herbert
Foundation Isaac Asimov
Neuromancer William Gibson
The Left Hand of Darkness Ursula K. Le Guin
Snow Crash Neal Stephenson
Listing exact columns (instead of SELECT *) is good practice in real
applications: it's explicit about what the query needs, and it doesn't break
if someone adds a column to the table later.
SELECT * — all columns¶
Fine for quick exploration in the shell; avoid it in application code, reports, or views, where an unexpected new column can silently change behavior downstream.
Column aliases with AS¶
AS is optional (SELECT title book_title works too) but including it makes
queries easier to read. Aliases are essential once you introduce expressions,
since the raw expression makes an ugly column header:
Expressions in SELECT¶
You can compute values, not just read stored columns:
SELECT title,
price,
price * 0.9 AS discounted_price,
published_year,
2024 - published_year AS years_old
FROM books;
title price discounted_price published_year years_old
----------- ----- ---------------- -------------- ---------
Dune 9.99 8.991 1965 59
Foundation 8.5 7.65 1951 73
...
String concatenation uses || in SQLite/Postgres (MySQL uses CONCAT()
instead):
DISTINCT — removing duplicates¶
DISTINCT applies to the whole selected row, not just one column — so
SELECT DISTINCT author, published_year removes only rows that are duplicates
across both columns together.
Data types in SQLite¶
SQLite uses type affinity rather than the strict, fixed column types you may know from other databases. A column is given a preferred affinity, but SQLite will still store whatever type of value you insert. The five storage classes:
| Storage class | Example | Roughly equivalent to |
|---|---|---|
NULL |
NULL |
Absence of a value |
INTEGER |
42 |
int, bigint |
REAL |
9.99 |
float, double |
TEXT |
'Dune' |
varchar, char, string |
BLOB |
binary data | bytea, binary |
CREATE TABLE demo (a INTEGER, b TEXT, c REAL, d BLOB, e NUMERIC);
INSERT INTO demo VALUES (1, 'hello', 3.14, x'0102', 'still works');
SELECT typeof(a), typeof(b), typeof(c), typeof(d), typeof(e) FROM demo;
typeof(a) typeof(b) typeof(c) typeof(d) typeof(e)
--------- --------- --------- --------- ---------
integer text real blob text
This flexibility is SQLite-specific. Postgres, MySQL, and SQL Server
enforce column types strictly — inserting text into an INTEGER column
raises an error there, whereas SQLite will accept it under most affinities.
Don't rely on this leniency in code meant to be portable; declare types as if
they were enforced, and validate data in your application.
CAST — explicit conversion¶
SELECT CAST('42' AS INTEGER) AS as_int,
CAST(42 AS TEXT) AS as_text,
CAST(9.99 AS INTEGER) AS truncated;
CAST is portable across nearly all SQL databases and is the safest way to
convert types explicitly rather than relying on implicit coercion.
How It Actually Works¶
SQLite uses type affinity, not rigid static typing like most databases.
Each column has a declared type (INTEGER, TEXT, REAL, BLOB, NUMERIC)
but that's a hint the storage engine uses to decide how to coerce a value
before storing it — the value itself still carries its own storage class
tag on disk. A column declared TEXT can still hold an integer if you insert
one and it doesn't cleanly convert. Under the hood, every row is stored as a
record: a header of varint-encoded serial types (one per column,
describing exactly how many bytes and what class each value occupies) followed
by the raw payload bytes, packed with no padding. This is why SQLite databases
are often smaller than the equivalent Postgres/MySQL table — there's no fixed
per-row slot size to pad out. When you SELECT a column, the VDBE walks the
row's header varints to compute byte offsets, then reads only the bytes for
the columns you asked for — it never deserializes columns you didn't request.
🔀 See this in another language¶
Exercise¶
Using the books table above:
- Select
titleandprice, aliasingpriceascost. - Write a query that returns
titleand a computed columnprice_in_cents(price multiplied by 100, cast toINTEGER). - Select the distinct list of
authorvalues. - Write a query producing a single readable string per row like
"Dune (1965) — $9.99"using||concatenation.