A production orders table can hold 40 columns and half a billion rows — but the report your manager wants needs exactly three columns. SELECT is how you carve precisely what you need out of a mountain of data, and it's the single most-typed word in any data career. By the end of this lesson you'll be writing queries that pick columns, compute new ones, rename them cleanly, and strip out duplicates.
What you'll learn
What
SELECT names the columns you want back; FROM names the table to read them from. Together they form the smallest complete SQL query. Aliases (AS) rename columns in your output, and DISTINCT removes duplicate rows from the result.
Why
Pulling every column of every row wastes time, memory, and money — cloud warehouses literally bill by data scanned. Choosing exactly the columns you need (called projection) makes queries faster, cheaper, and easier to read.
Where it's used
Every dashboard tile, every report, every machine-learning training set starts with a SELECT. It is the verb of data analysis.
Where this runs in production
Netflix analysts query viewing-history tables with billions of rows, selecting just the columns a model needs — profile_id, title_id, watch_seconds — rather than dragging entire rows across the network.
Airbnb's internal dashboards run SELECT queries against listings data, projecting price, rating, and location columns and aliasing computed fields like nightly_revenue for clean chart labels.
DoorDash measures marketplace health with queries like SELECT DISTINCT store_id FROM deliveries — collapsing millions of delivery rows into the unique set of stores that fulfilled at least one order.
Querying a table is like ordering at a restaurant. FROM is choosing the restaurant (which table), and SELECT is your order (which dishes/columns). Saying SELECT * is telling the kitchen 'bring me everything on the menu' — occasionally useful for exploring, wasteful and slow as a habit. The analogy breaks down in one way: the database never runs out of stock — it returns every matching row, every time.
-- customers(customer_id, name, email, country, signup_date) SELECT name, country FROM customers;
This asks for exactly two columns from the customers table. The result is itself a table — same number of rows as customers, but only two columns wide. Column order in your output follows the order you list them in SELECT, not their order in the table.
Every SELECT returns rows and columns — a temporary result table. This is why SQL composes so well: anything that produces a table can feed something that consumes one. That idea powers subqueries and CTEs later in the course.
-- order_items(order_item_id, order_id, product_id, quantity, unit_price) SELECT order_id, quantity * unit_price AS line_revenue FROM order_items;
quantity * unit_price is computed per row. AS line_revenue gives the computed column a readable name — an alias. Without the alias, PostgreSQL would label the column '?column?', which helps nobody reading your report. The AS keyword itself is optional (quantity * unit_price line_revenue also works), but writing it makes the intent unmissable.
SELECT DISTINCT country FROM customers returns each country once, no matter how many customers live there. DISTINCT applies to the whole selected row: SELECT DISTINCT country, city de-duplicates on the combination — two rows survive if they differ in either column.
SELECT * is fine for exploring a new table interactively. In saved queries, dashboards, and pipelines it's a liability: it scans and transfers columns you don't need (cloud warehouses like BigQuery bill by bytes scanned), and if someone adds a column to the table, your downstream code silently receives a different shape of data. List your columns explicitly.
Click each stage to see what it contributes. The query flows from the full stored table to your trimmed, renamed, de-duplicated result.
The marketing team wants a simple list of customer names and countries for a newsletter segment.
SELECT lists the two columns marketing asked for, separated by a comma. FROM names the table. The semicolon ends the statement.
Your task
You're an analyst at an online electronics shop. The products table holds the catalog. Build a price sheet: product name (labeled 'product'), the unit price, and a new column 'price_with_vat' that adds 20% VAT (unit_price * 1.20). Fill in the blanks.
product | unit_price | price_with_vat ----------------+------------+---------------- Wireless Mouse | 25.00 | 30.0000 USB-C Hub | 49.50 | 59.4000 Laptop Stand | 32.00 | 38.4000 (3 rows)
Write your solution in the editor on the right, then hit Run.
What does 'projection' mean in SQL?
Which query returns each customer's name under the output column header 'client'?
The orders table has 1,000 rows from 340 different customers. How many rows does SELECT DISTINCT customer_id FROM orders; return?
What is the root cause, and the correct fix?
Using order_items(order_item_id, order_id, product_id, quantity, unit_price), write a query returning order_id and a computed column named line_total (quantity multiplied by unit_price), for all rows.
customers has rows: (Egypt, Cairo), (Egypt, Cairo), (Egypt, Alexandria) in its (country, city) columns. What does SELECT DISTINCT country, city FROM customers; return?
Walk me through what SELECT and FROM do in a SQL query.
FROM names the table the database reads, and SELECT lists which columns come back in the result — an operation called projection. The result of any query is itself a table of rows and columns, which is what makes SQL composable. SELECT can also compute new columns with expressions, like quantity * unit_price, and rename any output column with an AS alias. A query as small as SELECT name FROM customers; is complete and useful — those two clauses are the foundation everything else builds on.
Why is SELECT * discouraged in production queries, and when is it acceptable?
SELECT * fetches every column, which creates three problems in production. First, cost and speed: columnar warehouses bill and perform based on the columns you scan, so pulling 90 columns when you need 3 can multiply cost dramatically. Second, fragility: when someone adds columns to the table, every SELECT * query silently changes shape, which can break downstream code, inflate dashboards, or leak newly added sensitive columns. Third, readability: an explicit column list documents exactly what the query depends on. SELECT * is perfectly acceptable for interactive exploration — peeking at an unfamiliar table's contents — and in a few idioms like EXISTS subqueries where the column list is ignored. My rule: star while exploring, explicit columns in anything saved or scheduled.
How does DISTINCT behave with multiple columns, and what performance consideration comes with it on large tables?
DISTINCT operates on the entire selected row, not any individual column: SELECT DISTINCT country, city keeps every unique combination of the pair, so two rows survive if they differ in either value. A frequent misconception is that DISTINCT binds to the first column it appears next to — it doesn't; it's a modifier on the whole SELECT. Performance-wise, the database must determine uniqueness across all returned rows, typically by hashing or sorting the full result set, which can be expensive on tables with hundreds of millions of rows. If I only need distinct values of grouped data I'm aggregating anyway, GROUP BY expresses that intent directly. I also treat an unexpected need for DISTINCT as a smell: in join-heavy queries it often papers over accidental row duplication from a fan-out, and the right fix is usually the join logic, not de-duplication at the end.
Common Mistakes to Avoid
1) Forgetting commas between column names in SELECT — SELECT name country silently treats country as an alias for name, giving you one mislabeled column instead of two. 2) Using single quotes for aliases or column names — in standard SQL, 'text' in single quotes is a string value; identifiers with spaces need double quotes: AS "Customer Name". 3) Assuming DISTINCT applies only to the column written right after it — it de-duplicates the entire selected row. 4) Expecting integer division to keep decimals — 5400 / 3600 is 1 in PostgreSQL; write 3600.0 to force decimal division. 5) Shipping SELECT * in saved queries and dashboards, which bloats cost and breaks silently when the table gains columns.
Ask the AI Tutor
Try these prompts in the AI Tutor panel: • 'ELI5: what does SELECT actually do, using a menu analogy different from the lesson's?' • 'Quiz me: five questions about aliases and DISTINCT.' • 'Show me a case where forgetting a comma in the SELECT list produces a wrong-but-running query.' • 'Explain why SELECT is written first but evaluated near the end.' • 'Interview mode: ask me why SELECT * is risky in production and grade my answer.'
Glossary
SELECT — the clause listing which columns (or expressions) a query returns. FROM — the clause naming the table to read. Projection — returning a chosen subset of columns. Result set — the table of rows and columns a query produces. Expression — a computation in the SELECT list, like quantity * unit_price. Alias — an output name assigned with AS to a column or expression. Identifier — the name of a table or column; quoted with double quotes when it contains spaces. String literal — a text value in single quotes, like 'Egypt'. DISTINCT — a modifier that removes duplicate rows from the result. SELECT * — shorthand for 'all columns'; fine for exploration, risky in production. Integer division — dividing two integers yields an integer in PostgreSQL; use a decimal like 3600.0 to keep fractions.
Recommended Resources
• Docs: PostgreSQL manual — 'Queries: Select Lists' covers projection, expressions, and aliasing from the source. • Read: your warehouse vendor's pricing page (e.g. BigQuery on-demand pricing) to see why bytes scanned — and therefore SELECT * — is a real money question. • Practice: on any sample database, write five queries that each return exactly the columns needed for an imaginary stakeholder request — no more. • Next in DSM: WHERE & Filtering, where you stop returning every row and start choosing which rows qualify.
Recap
✓ SELECT lists the columns to return; FROM names the table — together they form a complete query. ✓ Projection (choosing columns) changes the width of the result, never the row count. ✓ SELECT can compute derived columns per row with expressions like quantity * unit_price. ✓ AS gives columns and expressions readable output names; double quotes protect aliases with spaces. ✓ DISTINCT removes duplicate rows, comparing the entire selected row — not just one column. ✓ Prefer explicit column lists over SELECT * in anything saved or scheduled: cheaper, faster, and stable as schemas grow. Next up: WHERE & Filtering. You can now choose your columns — but every query so far returned all rows. Next you'll write predicates that keep only the rows you want: orders over $100, customers in Egypt, emails matching a pattern — and you'll meet SQL's famously tricky NULL logic.
Run your code to see the output here.