SQL Fundamentals

Lesson 1 of 5

SELECT Basics

"The select list is where you say what you mean. Everything else in the query is about which rows; this is about which facts." — a database teacher's first rule

In Part I you met SELECT the way you meet a city from a train window — enough to recognize it, not enough to live there. You ran SELECT * FROM customers and saw rows appear. That was the tease. This chapter is the depth: SELECT is the single most-used statement in all of SQL, the one you will type ten thousand times in your career, and almost every engineer uses only a fraction of what it can do.

The heart of SELECT is one deceptively small idea — the select list, the comma-separated things you ask for between SELECT and FROM. It answers which columns and computed values do I want back? Filtering rows, sorting, grouping — those come later and get their own chapters. Here we stay laser-focused on the projection: naming columns, renaming them, computing new ones, and stripping duplicates. Master this and every later query reads more clearly, runs a little leaner, and breaks less often in production.

By the end you'll know why SELECT * is a code smell in shipped systems, how to build derived columns with expressions, why SQLite spells string concatenation || and not +, and the neat trick of running SELECT with no table at all as a pocket calculator.

The big question

A query returns rows and columns — WHERE decides the rows, but what decides the columns, and how much can that part of the query actually do?

Projection: choosing your columns

The formal name for "which columns come back" is projection. When you write the select list, you are projecting the table down to just the fields you care about. Start with the most common form — naming columns explicitly:

SQL — Try it Yourself
Loading editor…
Result

Click Run to execute the query.

Two columns, five rows. You asked for name and city, and that is exactly what came back — not id, not age. The select list is a filter for columns the same way WHERE is a filter for rows.

The order you list columns is the order they appear in the result. The table stores them as id, name, city, age, but the query decides the layout:

SQL — Try it Yourself
Loading editor…
Result

Click Run to execute the query.

city now leads, then name, then age. The stored order is irrelevant; the select list is the contract for what the caller receives.

Analogy A table is a full pantry; the select list is your shopping list. SELECT * is walking out with the entire pantry every time you want a snack. Naming columns is taking the two things you actually came for — faster, lighter, and nobody trips over the crate of flour you didn't need.

Why SELECT * is a smell in production

SELECT * is perfect for exploring at a prompt — quick, no typing, shows you everything. In application code it's a liability, and reviewers flag it for good reasons:

Problem with SELECT * Why it bites you later
Fragile to schema changes Add or reorder a column and your code silently receives different data; positional access (row[2]) now points at the wrong field.
Wasted I/O and memory You pull every column — including a TEXT blob you never read — over the wire and into RAM.
Breaks covering indexes An index that could answer SELECT id, name from the index alone is useless once you demand every column; the engine must visit the table.
Hides intent A reader can't tell what the code needs. Explicit columns document the query's purpose.
Ambiguity in joins Two joined tables each with an id column produce duplicate names and confusion.

The rule most teams settle on: SELECT * at the console, explicit columns in code. Type the names. Your future self, debugging at 2 a.m., will thank you.

Renaming columns with AS

By default a column comes back labeled with its source name. Often you want a friendlier or clearer label — especially for computed columns, which otherwise get an ugly auto-generated name. That's what aliasing with AS is for:

SQL — Try it Yourself
Loading editor…
Result

Click Run to execute the query.

The data is identical; only the headers change. Aliases matter more than cosmetics: they name computed values (next section), they resolve clashes when two joined tables share a column name, and they become the handle your application code reads by (row["customer"]).

The AS keyword is optional — name customer works too — but write it. SELECT name city FROM customers looks like a typo for SELECT name, city; the explicit AS removes all doubt. If an alias contains spaces, wrap it in double quotes: AS "Full Name".

Computed and derived columns

Here is where the select list stops being a mere picker and becomes an expression engine. Anything that evaluates to a value can go in the select list — arithmetic, function calls, literals, concatenations. The result is a derived column: it isn't stored anywhere, it's computed per row at query time.

Our orders.amount is stored as an INTEGER in kobo (the smallest currency unit — a common real-world practice, since floating-point money is a bug factory). To display naira, divide by 100:

SQL — Try it Yourself
Loading editor…
Result

Click Run to execute the query.

Notice the 100.0, not 100. SQLite does integer division when both operands are integers: amount / 100 would throw away the fractional part. Writing 100.0 forces real-number division. This is a classic trap — 250000 / 100 and 250000 / 100.0 both look right until a value like 3050 kobo reveals the truncation.

You can chain operations and round the result:

SQL — Try it Yourself
Loading editor…
Result

Click Run to execute the query.

Each row gets its own computed naira and a 7.5% VAT figure — none of it stored, all of it derived on the fly. The select list just did real work.

Worked technique Derived columns keep money logic in one place. If every service computes naira from kobo its own way, they'll disagree. Better still, a query like SELECT product, amount / 100.0 AS naira can later graduate into a database view (Part IV) so every consumer reads the same derived column by name — the expression lives once, in the schema.

String concatenation — mind the dialect

Joining text is where SQL dialects openly disagree, and it trips up engineers moving between databases. In SQLite (and PostgreSQL, and the SQL standard) the concatenation operator is ||:

SQL — Try it Yourself
Loading editor…
Result

Click Run to execute the query.

Ada from Lagos, Ada (30) — and note SQLite happily coerces the integer age into text inside the concatenation. That convenience is not portable, so be deliberate.

The dialect differences are worth memorizing, because using the wrong one is an instant error at work:

Dialect How you concatenate Notes
SQLite a || b Standard operator; auto-coerces numbers to text.
PostgreSQL a || b Same operator; stricter about types.
Oracle a || b Also supports CONCAT(a, b) (two args only).
MySQL CONCAT(a, b, ...) || means logical OR by default! + does arithmetic, not text.
SQL Server a + b or CONCAT(...) + concatenates strings; CONCAT is safer with NULLs.

The one that burns people most: in MySQL, || is boolean OR, and 'a' + 'b' tries to add the strings as numbers (yielding 0). Reach for CONCAT() there. Because our playground is SQLite, every runnable block here uses ||.

Literals and functions in the select list

The select list can hold plain constant values (literals) alongside real columns. A literal produces the same value in every row — handy for tagging rows or shaping output for the caller:

SQL — Try it Yourself
Loading editor…
Result

Click Run to execute the query.

status and country are constants stamped onto every row; is_senior is a boolean expression — SQLite has no real boolean type, so it returns 1 for true and 0 for false (a type-affinity quirk worth remembering).

Scalar functions transform a value per row. A few you'll use constantly:

SQL — Try it Yourself
Loading editor…
Result

Click Run to execute the query.

UPPER('Ada') → ADA, LENGTH('Ada') → 3, SUBSTR('Lagos', 1, 3) → Lag. These are scalar functions: one input row, one output value. (Contrast with aggregate functions like COUNT and SUM, which collapse many rows into one — those get their own chapter.)

DISTINCT: removing duplicate rows

Sometimes you want the set of values, not every occurrence. DISTINCT removes duplicate rows from the result. Our five customers live in only three cities:

SQL — Try it Yourself
Loading editor…
Result

Click Run to execute the query.

Three rows back — Lagos, Abuja, Kano — even though two customers each live in Lagos and Abuja. The key subtlety: DISTINCT dedupes across the entire select list, not one column. It looks at the whole projected row and drops exact repeats. Two orders are Book/3000 and two are Pen/500, so:

SQL — Try it Yourself
Loading editor…
Result

Click Run to execute the query.

Six orders collapse to four distinct (product, amount) pairs. Add another column and rows that were duplicates may become unique again:

SQL — Try it Yourself
Loading editor…
Result

Click Run to execute the query.

Now just the product names — Book, Pen, Laptop, Chair — four rows, because the distinctness is over product alone; the two Book orders and two Pen orders each fold into one. The deduping key is exactly the columns you selected. Change the select list, and you change what "duplicate" means.

In production DISTINCT is a quiet performance cost — the engine must compare or sort rows to find duplicates. When a query shouldn't return duplicates in the first place, reaching for DISTINCT is often a band-aid over a wrong join or a missing key. Seasoned reviewers see SELECT DISTINCT and ask "why are there duplicates to remove?" before accepting it. Sometimes the honest answer is "there aren't — I added it defensively," and that's the smell.

SELECT with no table: SQL as a calculator

A SELECT doesn't actually need a FROM. Without one, it evaluates the select-list expressions exactly once and returns a single row — a scratchpad for computation, testing a function, or checking your engine's behavior:

SQL — Try it Yourself
Loading editor…
Result

Click Run to execute the query.

One row: 4, 3, 3.3333…, 3.1429. This is the fastest way to prove a dialect quirk to yourself — the 10 / 3 versus 10 / 3.0 gap above is the integer-division trap made visible in two adjacent columns. Use SELECT sans FROM whenever you want to sanity-check an expression before dropping it into a real query.

Projection vs. the rest of the query

Keep the mental model clean. A full query has clauses, and the select list is only the projection step:

Clause Question it answers Covered
SELECT Which columns / computed values? (projection) this chapter
FROM From which table(s)? Part II–III
WHERE Which rows? (filter before grouping) next chapters
GROUP BY Collapse rows into groups? aggregation
HAVING Which groups? aggregation
ORDER BY In what order? next chapters
LIMIT How many rows? next chapters

Everything in this chapter lives in that first row. The select list decides what each surviving row looks like; the other clauses decide which rows survive and in what order. Confusing the two is the root of many beginner mistakes — like trying to filter with the select list, or expecting DISTINCT to change which rows exist rather than which are duplicated.

Common Mistakes

  • SELECT * in application code. Great at the console, fragile in production — schema drift, wasted I/O, dead covering indexes. Name the columns you need.
  • Integer division surprise. amount / 100 truncates; write amount / 100.0 (or CAST(amount AS REAL) / 100) when you want a fraction.
  • Wrong concat operator across dialects. || in SQLite/Postgres, but CONCAT() in MySQL (where || is OR) and + in SQL Server. Using the wrong one fails loudly or, worse, silently returns 0.
  • Forgetting to alias computed columns. amount / 100.0 with no AS gets an ugly auto-name that your code can't reliably reference. Always alias derived columns.
  • Misreading DISTINCT. It dedupes the whole projected row, not a single column. SELECT DISTINCT a, b keeps every unique (a, b) pair.
  • Using DISTINCT to paper over a bad join. If duplicates appear, understand why before sweeping them away.

Think about it Predict, without running, how many rows each of these returns against the seed: (a) SELECT city FROM customers; (b) SELECT DISTINCT city FROM customers; (c) SELECT DISTINCT city, age FROM customers;. Then run all three. If (c) surprised you, you've just internalized the most important thing about DISTINCT: adding a column can increase the distinct-row count, because it changes the definition of "duplicate."

Interview Corner — the same idea at three levels. Answer yours; recognise the one above it.
Warm-up
What's the difference between `SELECT
Model answer — and listing columns, and why prefer the latter in code? — returns every column in stored order; naming columns returns exactly those, in your order. Explicit columns are resilient to schema changes, cheaper on I/O, keep covering indexes usable, and document intent. *` is fine for ad-hoc exploration.
Intermediate
Write a query that returns each customer's name and a single label like "Ada — Lagos (30)", and explain the concat operator.
Model answer — SELECT name || ' — ' || city || ' (' || age || ')' AS label FROM customers;. In SQLite/Postgres the operator is ||; in MySQL you'd use CONCAT(name, ' — ', city, ...) because || means OR there, and in SQL Server +.
Advanced
Does SELECT DISTINCT dedupe per-column or per-row, and what does that imply for performance and correctness?
Model answer — Per-row across the full select list, so adding columns can raise the distinct count. It forces the engine to sort or hash the result to detect duplicates, which costs CPU and memory; on large results that's significant. Correctness-wise, DISTINCT masking join-induced duplicates hides a modeling bug — the fix is usually the join or a key, not DISTINCT.
Company lens — how the same topic gets asked
micro1the AI SQL screen frequently opens with projection basics — "select these columns, alias them, add a computed field" — before ramping up; fluency with AS and simple expressions is table stakes.
Googlemay probe why SELECT * hurts at scale (I/O, covering indexes, schema coupling) rather than just whether you avoid it.
Metaits fast-paced SQL rounds reward writing clean, explicit select lists with derived columns quickly and correctly under time pressure.
Amazonexpect a "Dive Deep" follow-up such as how integer vs. real division or dialect-specific concatenation could silently corrupt a report.

Hands-on Build one polished result set. Starting from the block below, produce for each order: the product, an aliased price_naira derived from kobo (rounded to 2 places), and a single label column like "Book — ₦30.00" using ||. Then wrap the whole thing in SELECT DISTINCT and see how many rows collapse — and why.

SQL — Try it Yourself
Loading editor…
Result

Click Run to execute the query.

Summary

  • The select list (the part between SELECT and FROM) performs projection — it decides which columns and computed values come back, and in what order.
  • SELECT * is fine for exploring but a smell in production code: fragile to schema changes, wasteful, index-defeating, and intent-hiding. Name your columns.
  • AS aliases rename columns and, crucially, name computed ones; always alias derived values.
  • The select list is an expression engine: arithmetic, literals, boolean tests, scalar functions, and concatenation all produce derived columns computed per row.
  • SQLite concatenates with || — MySQL uses CONCAT() (its || is OR), SQL Server uses +. Watch integer division too: use / 100.0, not / 100.
  • DISTINCT dedupes across the entire projected row, not one column; treat it as a signal to check for a deeper cause of duplicates.
  • SELECT needs no FROM — a one-row scratchpad for computing and proving expressions.

Next we add the other half of a query: the WHERE clause, where you stop taking every row and start asking precise questions of your data — comparisons, ranges, IN, LIKE, and the three-valued logic of NULL.

Editorial reflection The select list is the smallest, most-overlooked part of SQL, and it's where good engineers quietly separate themselves. Anyone can write SELECT *; the craftsperson names exactly the facts the caller needs, shapes them with the right expression, and labels them so the next reader understands at a glance. A query is a sentence about your data — the select list is where you choose your words. Choose them precisely, and everything downstream, from performance to the person maintaining your code, gets easier.

Q&A · 0

Enrol to ask questions and join the discussion.

No questions yet — be the first to ask.