There is a moment in every analyst’s life when SELECT * feels like freedom. Every column arrives. Nothing is forgotten. Then the result is 80 columns wide, the CSV chokes email, and a stakeholder asks why the export includes three internal surrogate keys and a debug flag from 2019.
This is Part 3 of the SQL series on Analytics Made Simple. We stay on reading data, but we make SELECT intentional: named columns, aliases that make sense in a slide deck, DISTINCT when duplicates are noise, and LIMIT when you are exploring. The toy tables are still customers and orders. Series hub: SQL series.
What you will learn
- How SELECT lists columns instead of hauling everything
- When
SELECT *is fine, and when it is lazy in a harmful way - How aliases rename columns in the result without changing the table
- How DISTINCT collapses duplicate result rows
- How LIMIT caps how many rows you fetch while exploring
- How these tools combine for clean handoffs to Sheets, BI, or Python
SELECT is a shopping list
The SELECT clause is not decoration. It is the shopping list of values you want back. Everything after it shapes which rows and in what order, but the list decides what each result row contains.
Minimal reminder from earlier parts:
SELECT
name,
region
FROM customers;Two columns only. No email, no id, unless you asked. That is already a product decision: what does this handoff need?
Name your columns
Prefer explicit column lists for anything you share, schedule, or put behind a dashboard.
SELECT
customer_id,
name,
region,
email
FROM customers
ORDER BY customer_id;Example output:

Why explicit lists win:
- Stability. If someone adds a huge text column to the table tomorrow, your extract does not suddenly balloon.
- Clarity. Readers see the contract: these are the fields this query cares about.
- Safety. You are less likely to leak columns you did not mean to export.
- Performance (sometimes). Wide tables with heavy columns cost more to ship over the network. You do not need every byte for every question.
When SELECT * is still OK
- Private exploration on a small practice table
- Quick “what columns even exist?” moments in a client that does not show a schema browser
- Throwaway debugging you will not save as a production query
A good habit: start with * for thirty seconds, then rewrite with names before you paste the query into a shared doc.
Expressions in the select list
You are not limited to raw columns. You can compute light expressions. Keep them readable.
SELECT
order_id,
amount,
amount * 1.08 AS amount_with_tax_estimate
FROM orders
WHERE status = 'paid';That tax example is toy math, not tax advice. The point is structural: the result can include derived values. Heavy business logic often belongs in documented models, not in one-off mystery expressions, but small transforms are normal.
Aliases: rename for humans
An alias renames a column (or expression) in the result. The underlying table stays the same. Syntax uses AS (recommended for clarity).
SELECT
customer_id AS id,
name AS customer_name,
region AS sales_region
FROM customers
ORDER BY id;Example output:

Aliases help when:
- Database names are cryptic (
cust_nm) and humans need English - Two tables share a column name after a join (Part 6) and you must disambiguate
- You compute
COUNT(*)and wantAS order_countinstead of a blank or ugly default label
Style tips:
- Prefer
snake_casealiases for data work; they travel well into Python and files. - Avoid spaces in aliases unless your team standard requires quoted titles for a BI tool.
- Do not alias everything just to feel busy. Alias when the default name is unclear.
DISTINCT: unique result rows
DISTINCT removes duplicate rows from the result after the select list is built. It is not a magic dedupe for bad source data. It is a way to ask, “What unique combinations appear?”
Which regions appear in the customer table?
SELECT DISTINCT
region
FROM customers
ORDER BY region;Example output:

Unique region and status pairs from a join-like question on orders alone (status only here):
SELECT DISTINCT
status
FROM orders
ORDER BY status;DISTINCT across multiple columns treats the whole combination as the uniqueness key:
SELECT DISTINCT
customer_id,
status
FROM orders
ORDER BY customer_id, status;That answers: which statuses has each customer ever had on an order? It does not answer how many times. For counts, you want aggregates (Part 5), not DISTINCT alone.
DISTINCT is not a substitute for keys
If your customer table has two rows for the same person with different ids, DISTINCT on email might hide a data quality problem instead of fixing it. When duplicates are a reliability issue, fix upstream or document a merge rule. See the data quality series for uniqueness as a dimension, not only a SELECT keyword.
LIMIT: take a sample, not the ocean
LIMIT caps how many rows return. Perfect for exploration, smoke tests, and “show me an example.”
SELECT
order_id,
customer_id,
amount,
status
FROM orders
ORDER BY order_date DESC
LIMIT 5;Important: without ORDER BY, which rows you get as “the first five” can be engine-defined and unstable. If the sample must be meaningful (latest orders, highest amounts), sort first, then limit.
Some engines use FETCH FIRST or TOP instead of LIMIT. The idea is the same. This series uses LIMIT because SQLite and many warehouses accept it or a close cousin.
LIMIT and pagination (concept)
Apps often combine LIMIT with an offset to page results. As a data analyst, you more often want better filters than deep offsets. If you need rows 10,000 through 10,020 of an arbitrary order, ask whether a keyset filter would be clearer. For now: LIMIT is for sampling and head checks, not for pretending you loaded the whole warehouse.
Putting it together
A clean extract pattern for a marketing list draft:
SELECT DISTINCT
name AS customer_name,
email AS email_address,
region AS sales_region
FROM customers
WHERE email IS NOT NULL
ORDER BY customer_name
LIMIT 100;What each piece does:
- Explicit columns and aliases for a human-facing file
- DISTINCT in case your real table is messier than the toy
- A filter so you do not hand blank emails to a mail tool (NULL details expand in Part 4)
- ORDER BY for a stable reading order
- LIMIT as a safety cap while you validate the logic
Worked example: shape an ops glance
Question: “Show me a compact view of recent paid orders with friendly names.”
SELECT
order_id AS id,
customer_id AS customer,
order_date AS ordered_on,
amount AS order_amount,
status AS order_status
FROM orders
WHERE status = 'paid'
ORDER BY order_date DESC
LIMIT 10;You still do not need every column that might exist on a real orders table (shipping JSON, fraud scores, raw payloads). You need the glance fields. When you later join customer names (Part 6), you will add name AS customer_name the same way.
Second task: unique paid customers by id from the orders table alone.
SELECT DISTINCT
customer_id
FROM orders
WHERE status = 'paid'
ORDER BY customer_id;That list is a set of ids, not a count. For “how many unique paid customers,” Part 5’s COUNT(DISTINCT customer_id) is the cleaner answer. Notice how Part 3 tools set up those later questions without replacing them.
Handoffs: Sheets, BI, and Python
Intentional SELECT lists make every handoff kinder:
- Sheets: fewer columns means less horizontal scrolling and fewer accidental sorts on the wrong field.
- BI tools: aliases become friendlier field pickers when the tool reflects SQL names.
- Python: reading a narrow result into pandas keeps memory and mental load down. See the Python series when you bridge tools.
If you still live mostly in grids, pair these habits with spreadsheets to data. For a map of site paths, use Learn.
Column lists as documentation
A shared query with an explicit SELECT list is a tiny contract. Anyone reading it can answer: what fields does this extract promise? That matters when you hand work to a contractor, schedule a job, or paste SQL into a ticket. SELECT * says “whatever the table happens to be this week,” which is a moving target.
In regulated or privacy-sensitive environments, explicit lists also reduce accidental leakage. You are less likely to ship a column that should never leave the warehouse if the column is not named in the query. That is not a substitute for proper access control. It is still a useful second line of defense for analysts who export samples.
DISTINCT, LIMIT, and honesty in sampling
When leadership asks for “a quick sample of customers,” decide whether they need random-ish examples, latest signups, highest value buyers, or unique emails. LIMIT 20 after ORDER BY date gives latest. LIMIT 20 with no ORDER BY gives whatever the engine returns first, which is a poor story in a meeting. DISTINCT on email before LIMIT gives unique addresses, but it can hide that your table has duplicate person rows that still need a quality fix.
Write the sample intent in a comment in your notes:
SELECT
customer_id,
name,
region
FROM customers
ORDER BY customer_id
LIMIT 10;
-- Intent: stable first ten ids for a screenshot, not a statistical sampleHonesty about sampling prevents fake confidence. A LIMIT query is not a census.
Common mistakes
- Shipping SELECT * to production jobs. Exploration leftover becomes a silent contract.
- Alias typos that break downstream dashboards. Rename carefully when something is already in use.
- DISTINCT as a panic button. If you need DISTINCT to make a join “look right,” you may have a join grain problem (Part 6).
- LIMIT without ORDER BY when you believe you are looking at “the top” anything.
- Assuming DISTINCT reorders rows. Add ORDER BY when order matters.
- Selecting columns you do not understand. If you cannot explain a field, do not put it in a executive CSV.
Practice
- Rewrite any
SELECT *in your notes into an explicit list. - Create a customer extract with aliases suitable for a non-technical reader.
- List DISTINCT statuses and DISTINCT regions. Compare row counts to the full tables.
- Fetch the five highest
amountorders using ORDER BY and LIMIT. - Write one query that uses a column list, an alias, DISTINCT or a clear reason you did not need it, and a LIMIT for safety while testing.
Quick recap
- SELECT columns are a contract: ask only for what you need.
SELECT *is for private exploration, not default production style.- Aliases rename results for humans and downstream tools.
- DISTINCT returns unique result rows; it does not fix root-cause duplication.
- LIMIT samples; pair it with ORDER BY when “top” matters.
- Clean select lists make Sheets, BI, and Python handoffs less painful.
Next: filters and sorting. WHERE, AND/OR, IN, BETWEEN, careful LIKE, ORDER BY, and NULL basics. That is Part 4.
FAQ: SELECT craft
Does column order in SELECT matter?
For correctness of most queries, no. For human reading and CSV handoffs, yes. Put identifiers first, then descriptive fields, then measures. Keep the same order across related extracts so stakeholders can compare files without hunting columns.
Should I SELECT DISTINCT by default?
No. Default to exact rows. Add DISTINCT when the question is about unique combinations, or when you have proven duplicates and understand why they exist. Habitual DISTINCT is a smell that the grain or join is wrong, especially once you reach Part 6.
Is LIMIT 1 a good way to check existence?
It can be a quick smoke test (“is there at least one paid order?”), but for existence in serious SQL you will later prefer patterns like EXISTS (Part 7). For now, LIMIT 1 with a clear WHERE is acceptable in interactive exploration.
Sources
References for SELECT-list behavior:
