SQL SELECT: Querying a Database
BeginnerLesson 2 of 6 in SQL Basics Course
What you will learn
- Write queries that read chosen columns from one table, understand the result set they return, and list the different values in a column with DISTINCT.
- Write a basic SELECT queryName the columns to show with SELECT and the table to read with FROM, follow the basic syntax rules, and know what the result looks like.
- List different values with DISTINCTRemove repeated rows from a result with SELECT DISTINCT and predict how many rows remain, including when several columns or missing values are involved.
- Write a basic SELECT query
- Keep only the rows that answer a question by writing conditions with comparison operators on numbers, text and dates, and by combining conditions with AND, OR and NOT.
- Compare numbers with comparison operatorsUse WHERE with =, <>, <, >, <= and >= to select rows by numeric values, and choose the right operator for the words of a question.
- Compare text and dates correctlyWrite text and date values in the correct form, predict how case, alphabetical order and date formats affect a comparison, and avoid the usual quoting and date mistakes.
- Combine conditions with AND, OR and NOTJoin several conditions with AND and OR, reverse a condition with NOT, and use parentheses so the query tests exactly what the question asks.
- Compare numbers with comparison operators
- Shorten and sharpen conditions with IN for lists of values, BETWEEN for ranges and LIKE for text patterns, including their NOT forms and their usual traps.
- Match a list of values with INUse IN and NOT IN to keep rows whose value is one of several listed values, and know how lists interact with AND and with missing values.
- Select a range with BETWEENUse BETWEEN and NOT BETWEEN for ranges of numbers, dates and text, knowing that both ends are included and where that causes mistakes.
- Search text patterns with LIKEFind text that starts with, ends with or contains given characters using LIKE with % and _, and handle case sensitivity, literal wildcards and missing values.
- Match a list of values with IN
- Recognize NULL as a missing value, find it with IS NULL and IS NOT NULL, and predict how missing values change the result of every other kind of condition.
- Find missing values with IS NULLTell NULL apart from zero and empty text, and use IS NULL and IS NOT NULL to find rows where a value is or is not recorded.
- Predict how NULL affects conditionsApply three-valued logic (true, false, unknown) to comparisons, AND, OR, NOT, IN, BETWEEN and LIKE, and decide whether rows with missing values belong in an answer.
- Find missing values with IS NULL
- Put result rows in a meaningful order with ORDER BY, sort by several columns while handling ties and missing values, and return the top rows or one page of rows with LIMIT and OFFSET.
- Sort rows with ORDER BYSort a result by one column in ascending or descending order and predict how numbers, dates and text are ordered.
- Sort by several columns and handle ties and NULLsSort by more than one column with a direction for each, add tie-breakers for a repeatable order, and control where missing values appear.
- Return top rows and pages with LIMIT and OFFSETReturn the first few rows of a sorted result with LIMIT, skip rows with OFFSET to show later pages, and write the same request in other SQL dialects.
- Sort rows with ORDER BY
- Combine all the clauses into correct, readable queries, understand the order in which a query is processed, and turn everyday questions into queries whose results you check.
- Put the clauses in order and know how a query runsWrite SELECT, FROM, WHERE, ORDER BY and LIMIT in the required order and use the logical processing order to predict what each clause can see.
- Turn a question into a query and check the answerTranslate an everyday question into a single-table query step by step, then test the result for boundaries, missing values, case and ties.
- Put the clauses in order and know how a query runs
SQL SELECT study guide
1Choose the data to display
A useful query starts by choosing the table and the columns that answer your question. The result can show every record, a few fields from each record, or a list of different values. You will work with two small tables so you can trace each result back to the stored data. Reading the column names carefully and separating column order from row order will make later filters and sorts easier to understand.
1.1 SELECT, FROM, and the result table
A query asks a database for data. A plain SELECT query reads the stored table without changing it. SELECT names the columns to display, and FROM names the table to read.
The text examples use ordinary case-sensitive equality in SQLite and PostgreSQL. Collation settings can change how text compares and sorts.
Two tables to work with
The employees table has these eight rows. salary is yearly pay in dollars; hire_date is text in YYYY-MM-DD form. NULL marks a missing value.
| employee_id | first_name | last_name | department | city | salary | hire_date | phone |
|---|---|---|---|---|---|---|---|
| 1 | Ana | Lopez | Sales | Chicago | 52000 | 2019-03-15 | 312-555-0147 |
| 2 | Ben | Carter | IT | Denver | 71000 | 2021-07-01 | NULL |
| 3 | Chloe | Nguyen | Sales | Chicago | 48000 | 2023-01-09 | 312-555-0198 |
| 4 | David | Kim | Marketing | Boston | 60000 | 2020-11-23 | 617-555-0110 |
| 5 | Emma | Brown | Sales | Denver | 65000 | 2018-05-30 | NULL |
| 6 | Farid | Haddad | IT | Chicago | 83000 | 2017-09-12 | 312-555-0175 |
| 7 | Grace | Okafor | NULL | Boston | 45000 | 2024-02-19 | 617-555-0133 |
| 8 | Hugo | O'Neill | Marketing | Chicago | 58000 | 2022-08-05 | NULL |
The products table records prices in dollars and stock in warehouse units.
| product_id | name | category | price | stock |
|---|---|---|---|---|
| 1 | Desk Lamp | Lighting | 24.99 | 40 |
| 2 | Floor Lamp | Lighting | 89.00 | 12 |
| 3 | Lamp Shade | Lighting | 15.50 | 0 |
| 4 | Office Chair | Furniture | 149.00 | 8 |
| 5 | Standing Desk | Furniture | 349.00 | 5 |
| 6 | Desk Organizer | Storage | 18.75 | 60 |
| 7 | Bookshelf | Furniture | 120.00 | NULL |
| 8 | Paper Tray | Storage | 9.99 | 25 |
Read the query's shape
SELECT city, first_name
FROM employees;
The result set is another table: eight rows and two columns, with city first. No filter is present, so every employee contributes a row. SQL gives no guaranteed row order until you add ORDER BY.
SELECT * FROM products; displays every column in the table's defined order. The star is useful for exploring. In a saved query, naming the columns keeps the result focused and prevents a newly added table column from changing its shape.
Make the syntax readable
Separate columns with commas, with no trailing comma before FROM. End the statement with a semicolon. Keywords ignore case, so select works, but capitals make clauses easy to find. Spaces and line breaks separate words; they do not change this query's meaning. A -- comment continues to the end of its line.
Names must resolve to the schema. If frist_name causes a missing-column error, compare the spelling with first_name. Standard SQL uses double quotes for names containing spaces or reserved words, such as "order date". PostgreSQL folds unquoted names to lowercase, while "City" asks for that exact capitalized name. MySQL normally uses backticks; double quotes identify names in its ANSI_QUOTES mode. SQL Server also accepts square brackets and accepts double quotes when QUOTED_IDENTIFIER is on, its default.
Key Takeaways
SELECTchooses displayed columns;FROMidentifies their table.- The result columns follow the SELECT list; row order needs
ORDER BY. SELECT *displays all columns; explicit names keep saved queries focused.- Use commas between columns and a semicolon to end a statement.
- Check names against the schema and use the database’s identifier quoting rules.
1.2 Different values and different combinations
A repeated value in a query result is often useful. Several employees can live in the same city, and each employee still needs a row. Use DISTINCT when the question asks for different values or different combinations.
Choose what should count as a duplicate
SELECT DISTINCT city
FROM employees;
The result contains Chicago, Denver, and Boston, with no guaranteed order. The plain SELECT city would return eight rows because each employee supplies a city. Adding DISTINCT reduces that display to three different values. It leaves all eight employee records in storage.
The comparison includes every selected column. Consider:
SELECT DISTINCT department, city
FROM employees;
Ana and Chloe both contribute (Sales, Chicago), so that pair appears once. Emma contributes (Sales, Denver), which is different and stays. The other pairs are (IT, Denver), (Marketing, Boston), (IT, Chicago), (NULL, Boston), and (Marketing, Chicago). There are seven result rows. Repeated departments remain because a department paired with another city is a different result row.
Write DISTINCT once, directly after SELECT. It applies to the complete selected row. Placing it between column names, as in SELECT department, DISTINCT city, is a syntax error.
Watch which columns you include
If you add employee_id to the selected columns, every row becomes different. SELECT DISTINCT employee_id, city FROM employees; still returns eight rows: each ID is unique, even where the city repeats. To answer “Which cities have employees?”, select the city alone. To answer “Which employees live in each city?”, keep identifying columns and expect repeated cities.
For this duplicate-removal step, repeated NULLs are treated as duplicates. SELECT DISTINCT department FROM employees; returns Sales, IT, Marketing, and one NULL row. The NULL row records that a department is missing; it is not a fourth department name. This duplicate rule differs from NULL = NULL, which gives unknown in a filter.
Text comparisons follow a collation, the database's rules for matching and ordering text. With the ordinary case-sensitive comparisons used for these SQLite and PostgreSQL examples, Chicago and chicago are different values. A DISTINCT list can expose inconsistent capitalization. Removing repeated displays does not repair spelling in the stored data.
Combine DISTINCT with a sorted shortlist
SELECT DISTINCT city
FROM employees
ORDER BY city
LIMIT 2;
First, duplicate cities disappear. Next, the three cities are sorted. Finally, two result rows remain: Boston and Chicago, in that order. Applying the limit to employee rows first would answer a different question.
For a DISTINCT query, keep the sort columns in the SELECT list. PostgreSQL requires this; SQLite permits some queries that sort on an unselected column, but choosing among different hidden values makes their ordering hard to interpret. If a result unexpectedly repeats, inspect which columns you selected before deciding that DISTINCT belongs in the query.
Key Takeaways
DISTINCTremoves duplicate complete result rows without changing stored rows.- Adding columns changes which rows count as duplicates; a unique key keeps every row different.
- DISTINCT groups repeated NULLs together.
- Duplicates are removed before the sorted result is limited.
2Keep the rows that answer the question
Filtering turns a table into an answer about particular records: a pay range, a city, or a group meeting several conditions. Small choices matter, including whether an endpoint counts and whether two conditions must both hold. Numeric comparisons, text and date values, and grouped logic each have their own rules. A query can run successfully and still ask the wrong question, so you will trace which rows pass each test.
2.1 Numbers and boundary values
The WHERE clause keeps a row only when its condition is true. It follows FROM and can test any column in the table, including a column you do not display.
Match the words to the operator
| Request | Operator | Example condition |
|---|---|---|
| Equal to | = | stock = 0 |
| Different from | <> | stock <> 0 |
| More than | > | salary > 60000 |
| At least | >= | salary >= 60000 |
| Less than | < | price < 20.50 |
| At most | <= | price <= 20.50 |
<> is standard SQL for “not equal.” The five databases discussed here also accept !=. Use one equals sign for equality. SQLite accepts ==, but PostgreSQL rejects that form.
Trace a boundary row
SELECT first_name, salary
FROM employees
WHERE salary > 60000;
Ben has 71000, Emma has 65000, and Farid has 83000. Those are the three qualifying rows, with no stated order. David earns exactly 60000. His row fails > 60000 because “more than” excludes equality.
Change only the operator to >=. David now joins the result, giving four rows. This is how “at least 60000” differs from “more than 60000.” A useful boundary check asks what happens to a value exactly equal to the threshold; values far above it cannot reveal that mistake.
For “at most 20.50,” a price of 20.50 passes and a price of 20.51 fails. When a request gives both a lower and an upper bound, check each end separately before joining the tests.
Write numeric values as numbers
Use plain digits in SQL conditions: 60000, with no thousands comma or currency sign. A comma separates SQL items; WHERE salary > 60,000 is a syntax error. Decimal values use a point, as in WHERE price < 20.50. Salaries and prices belong in numeric columns so that comparisons use their numeric size.
Text that looks numeric follows text rules when compared as text. The text value '9' sorts after '10' because its first character comes later. That tells you about characters, not about nine being greater than ten. Read the column's data type before interpreting an unexpected comparison.
Filter without displaying the tested column
SELECT first_name
FROM employees
WHERE salary < 50000;
Chloe and Grace pass. Their salaries are used to test the rows but are absent from the result. You do not need to add salary to SELECT to make the filter work. While checking a query, showing it temporarily can help you inspect the reason each row passed.
Equality on a primary key provides a direct record lookup: WHERE employee_id = 6 finds Farid. A primary key identifies one row, so that equality cannot return several employees with the same ID.
Missing data needs separate care. WHERE stock <> 0 returns six products. Lamp Shade is left out because its known stock is zero. Bookshelf is also left out: its stock is NULL, so the comparison is unknown and WHERE does not keep it. A “not equal” test does not add missing values to its result.
Key Takeaways
- WHERE retains only rows whose condition is true.
>and<exclude their thresholds;>=and<=include them.- Write numeric literals without currency signs, quotes, or thousands separators.
- A filter can test a column that is not displayed.
- Use a key equality to look up one record.
- Ordinary comparisons with NULL do not pass a WHERE filter.
2.2 Text, quotes, and complete dates
Text and date conditions need values in a form the database can interpret. A misspelled value may quietly return no rows, while a missing quote can turn that value into a column name.
Distinguish a value from a name
SELECT first_name
FROM employees
WHERE city = 'Chicago';
The single quotes make 'Chicago' a text value. The result contains Ana, Chloe, Farid, and Hugo. Without quotes, Chicago is read as a name, so the database looks for a column with that name. Standard double quotes also mark names. PostgreSQL rejects city = "Chicago" when no such column exists. Some SQLite configurations accept double-quoted strings as a legacy feature; use single quotes for values consistently.
An apostrophe inside a value is doubled:
SELECT first_name
FROM employees
WHERE last_name = 'O''Neill';
That finds Hugo. The two adjacent single quotes represent one apostrophe inside the value; the outer quotes mark its start and end. Standard SQL does not use a backslash to escape that apostrophe.
Check the comparison rules
A collation controls text equality and sorting, including case and accented letters. In the ordinary case-sensitive SQLite and PostgreSQL comparisons used here, 'Chicago' and 'chicago' differ. WHERE city = 'chicago' returns no rows. MySQL and SQL Server commonly use case-insensitive collations, so their usual settings can match both spellings.
Text range comparisons also follow collation. With these names, WHERE last_name < 'M' returns Brown, Carter, Haddad, Kim, and Lopez. Values are compared from the left. A longer value beginning with the same characters comes after its prefix: 'Martin' is after 'M'. A cutoff of 'M' is not the same as “all names beginning with M.”
Put the year first
The example hire_date column stores full dates as text: year, month, then day, with fixed widths. This makes text order agree with calendar order.
SELECT first_name, hire_date
FROM employees
WHERE hire_date >= '2022-01-01';
Chloe, Grace, and Hugo pass. PostgreSQL also has a real DATE type that accepts this literal. Its standard typed spelling is DATE '2022-01-01'; SQLite does not use that typed-literal syntax.
A local date string such as '03/04/2024' is ambiguous in a date column because settings can make it March 4 or April 3. Stored as text, month-first dates compare by month before year. For example, '12/01/2021' sorts after '03/15/2024' even though the first calendar date is earlier.
Translate the full date boundary
“Hired after 2022” means starting with '2023-01-01'. Comparing text to '2022' instead also admits Hugo's '2022-08-05': the longer text sorts after the matching prefix.
If a column contains times, equality with a date alone does not select every time on that day. To include March 31, 2024, use ordered_at >= '2024-03-31' AND ordered_at < '2024-04-01'. This keeps every time from the start of March 31 up to, but excluding, the start of April 1.
Key Takeaways
- Use single quotes for text values; double an apostrophe inside a value.
- Collation determines how text case and characters compare.
- Use complete
YYYY-MM-DDdates so fixed-width text dates sort chronologically. - To cover an entire day with times, include its start and exclude the next day’s start.
2.3 AND, OR, NOT, and grouped conditions
A row may need to meet several tests. AND requires both conditions to be true. OR requires at least one to be true. NOT reverses true and false; when a condition is unknown because of NULL, NOT leaves it unknown.
Decide whether both tests must hold
SELECT first_name
FROM employees
WHERE department = 'Sales' AND city = 'Chicago';
Ana and Chloe pass both tests. Emma works in Sales but lives in Denver, so she fails this AND. Changing AND to OR admits Emma, Farid, and Hugo as well: the five qualifying employees satisfy one test or both.
Adding an AND condition can keep or reduce the result. Adding an OR condition can keep or expand it. This gives you a quick check when building a filter in stages.
Every comparison must be complete. Write department = 'IT' OR department = 'Marketing', repeating the column and operator. department = 'IT' OR 'Marketing' does not express the second comparison. PostgreSQL rejects it; SQLite treats the text on its own as false and returns only IT staff. Likewise, write salary > 50000 AND salary < 70000, with salary on both sides of AND.
Translate groups of alternatives
“Employees in Boston and Denver” usually asks for people in either city. Each row has one city, so use city = 'Boston' OR city = 'Denver'. That returns Ben, David, Emma, and Grace. An AND between these equalities asks one city value to be both places at once and returns no rows.
When operators mix, NOT is evaluated before AND, and AND before OR. Without parentheses:
WHERE department = 'Sales'
OR department = 'Marketing' AND city = 'Chicago'
This means Sales staff anywhere, plus Marketing staff in Chicago. Emma passes because the Sales comparison is enough. To restrict both departments to Chicago, group the alternatives:
SELECT first_name
FROM employees
WHERE (department = 'Sales' OR department = 'Marketing')
AND city = 'Chicago';
Now Ana, Chloe, and Hugo pass. The parentheses put the two department choices under one shared city requirement. Use them to make a mixed AND/OR filter clear even when the default order would already be right.
Reverse a whole group
NOT (city = 'Chicago' OR city = 'Denver') returns David and Grace in Boston. Neither listed city is allowed. It is equivalent to city <> 'Chicago' AND city <> 'Denver' for these values.
Using OR between those two “not equal” tests is a common trap. A Chicago row differs from Denver, and a Denver row differs from Chicago. Every employee with a recorded city passes at least one test, so the OR fails to exclude either city.
Build a complex condition one piece at a time. Check the rows after the first test, then after adding the next. You can swap the two sides of an AND, or the two sides of an OR, without changing the answer; choose the arrangement that makes the groups easiest to read.
Key Takeaways
- AND needs both conditions; OR needs at least one.
- Write a complete comparison on each side of a logical operator.
- NOT is evaluated before AND, then OR; parentheses make the intended grouping explicit.
- Alternatives for one column usually need OR, even when the request says “and.”
- To exclude either of two matches, negate the OR group or join the two exclusions with AND.
3Match lists, ranges, and text patterns
Some questions call for several exact choices, others for a stretch of values or part of a name. IN, BETWEEN, and LIKE give each kind of request a clear form. Choosing between them starts with what a match means: a whole value, a value between endpoints, or text fitting a pattern. You will also check missing values and the details that change an answer, including inclusive endpoints, wildcard placement, and case sensitivity.
3.1 Lists of exact choices with IN
Use IN when one column may match any value in a list. It shortens a group of equalities joined with OR while keeping their meaning.
Build a list of alternatives
SELECT first_name
FROM employees
WHERE department IN ('IT', 'Marketing');
Ben, David, Farid, and Hugo pass. Each has one of the two department values. The same test could be written as department = 'IT' OR department = 'Marketing'.
Put list items inside parentheses, separated by commas. Quote text; leave numeric values unquoted. For example, employee_id IN (2, 4, 6) selects Ben, David, and Farid. The order of list entries does not sort the result. Repeating an entry does not repeat a matching row: IN (6, 2, 2, 4) selects the same records. Use ORDER BY if you need a result order.
IN matches whole values under the same comparison rules as equality. city IN ('Chi') does not match Chicago. Under the ordinary case-sensitive comparisons used here, city IN ('chicago') does not match it either. A fragment search needs LIKE, while case behavior depends on collation.
Apply another rule to every choice
SELECT first_name
FROM employees
WHERE department IN ('IT', 'Marketing')
AND city = 'Chicago';
Farid and Hugo pass. IN is one condition, so AND applies the city test to the entire department choice. In the expanded OR version, put parentheses around the two department comparisons. Without them, department = 'IT' OR department = 'Marketing' AND city = 'Chicago' also admits Ben, because AND binds more tightly and the city rule applies only to Marketing.
Exclude the listed values
WHERE city NOT IN ('Chicago', 'Denver') returns David and Grace. A recorded city passes only when it differs from both entries. This is a useful form for several exclusions and avoids the mistake of joining “not equal” conditions with OR.
Missing values are not a match for either form. Grace has a NULL department, so department NOT IN ('Sales') does not include her. The database cannot establish that the missing department differs from Sales. To include unassigned staff as well, write department NOT IN ('Sales') OR department IS NULL.
A NULL inside a NOT IN list creates a broader problem. department NOT IN ('Sales', NULL) requires both department <> 'Sales' and department <> NULL. The second comparison is always unknown, so the combined test is never true and WHERE returns no rows. Keep missing-value tests separate from the value list.
With IN, a real match still succeeds even if another list entry is NULL. department IN ('Sales', NULL) keeps the three Sales staff, but it does not add Grace. Use IS NULL to request missing values explicitly.
An ordinary SQL IN list needs at least one item. PostgreSQL rejects IN (); SQLite accepts it as false. If a program builds a list from user choices, handle an empty list before sending the query so the behavior is deliberate across databases.
Key Takeaways
- IN matches any whole value in its parenthesized, comma-separated list.
- List order and repeated entries do not control result order or duplicate matching rows.
- IN combines with AND as one condition.
- NOT IN excludes listed values but does not include missing values.
- A NULL inside a NOT IN list prevents any row from passing the WHERE test.
3.2 Ranges and included endpoints
BETWEEN tests a range with both ends included. Use it when “from” and “to” include the named boundary values; use explicit comparisons when either boundary must be excluded.
Check the two ends
SELECT name, price
FROM products
WHERE price BETWEEN 15.50 AND 89.00;
Desk Lamp, Floor Lamp, Lamp Shade, and Desk Organizer pass. Floor Lamp costs exactly 89.00 and Lamp Shade exactly 15.50, so both remain. The condition means price >= 15.50 AND price <= 89.00.
Put the lower value first. price BETWEEN 89.00 AND 15.50 asks for a price at least 89.00 and at most 15.50. It is valid syntax but has no matches. Ordinary BETWEEN does not reorder the endpoints for you. PostgreSQL offers BETWEEN SYMMETRIC to do that, but use the lower-first form for a query shared with SQLite.
For “more than 15.50 and less than 89.00,” write price > 15.50 AND price < 89.00. Only Desk Lamp and Desk Organizer pass. That difference is about the question's endpoints, not the way the result is displayed.
Outside a range
price NOT BETWEEN 20 AND 100 means price < 20 OR price > 100. It returns Lamp Shade, Office Chair, Standing Desk, Desk Organizer, Bookshelf, and Paper Tray. Values exactly 20 or 100 would be inside the original range and would fail NOT BETWEEN.
A missing tested value is unknown in either test. For stock, BETWEEN 5 AND 40 selects five products and NOT BETWEEN selects two: Lamp Shade with 0 and Desk Organizer with 60. Bookshelf has NULL stock and belongs to neither result. A range and its negation therefore need not cover every stored row.
Dates and times
Complete date strings in YYYY-MM-DD order support date ranges. hire_date BETWEEN '2020-01-01' AND '2022-12-31' selects Ben, David, and Hugo, covering all three calendar years in this date-only column.
A column containing times needs a different upper boundary for a whole month. A date-only endpoint in a timestamp column means midnight at the start of that day. In text, even '2024-03-31 00:00:00' sorts after the shorter prefix '2024-03-31'. To include all March times, use:
WHERE ordered_at >= '2024-03-01'
AND ordered_at < '2024-04-01'
This includes the beginning of March and stops just before April, without guessing the final fraction of a second on March 31.
Text ranges and the two uses of AND
Text endpoints are complete values. last_name BETWEEN 'A' AND 'C' includes Brown but excludes Carter, since Carter sorts after its prefix C. Under these text rules, last_name >= 'A' AND last_name < 'D' covers names beginning A, B, or C and returns Brown and Carter.
In price BETWEEN 10 AND 100 AND category = 'Lighting', the first AND separates range endpoints; the second joins the range test to the category test. The matching products are Desk Lamp, Floor Lamp, and Lamp Shade. Line breaks can make the two roles easier to see.
Key Takeaways
- BETWEEN includes both endpoints and expects the lower endpoint first.
- NOT BETWEEN selects values below the low end or above the high end.
- Explicit comparisons let you exclude one or both endpoints.
- For a whole month with times, include its start and stop before the next month.
- A text endpoint is a whole value, not every value with that starting letter.
- NULL in the tested column passes neither range test.
3.3 Text patterns with LIKE
LIKE tests text against a pattern. Use it to search for a beginning, an ending, or a fragment when equality would ask for a whole exact value.
Place the wildcards
| Pattern | Meaning | Matching product names |
|---|---|---|
'Lamp%' | Starts with Lamp | Lamp Shade |
'%Lamp' | Ends with Lamp | Desk Lamp, Floor Lamp |
'%Lamp%' | Contains Lamp | Desk Lamp, Floor Lamp, Lamp Shade |
'Lamp' | Exactly Lamp | None |
The pattern must match the whole value. % can represent zero or more characters, including spaces. In '%Lamp%', either percent sign may match nothing, so the text can begin or end with Lamp as well as contain it in the middle.
_ stands for exactly one character. last_name LIKE '_im' finds Kim. Four underscores in first_name LIKE '____' find Emma and Hugo, the first names with exactly four letters. The underscore is not an optional character.
SELECT first_name, phone
FROM employees
WHERE phone LIKE '312-%';
Ana, Chloe, and Farid pass because their recorded phone text starts with 312-. The percent sign covers every remaining character.
Choose the operator and case rule
Wildcards belong to LIKE patterns. name = '%Lamp%' looks for literal percent signs and returns no products here. The file-search wildcards * and ? have no special meaning in LIKE; name LIKE 'Desk*' asks for the literal asterisk after Desk.
SQLite's LIKE normally ignores case for ASCII letters A–Z, while PostgreSQL's ordinary LIKE is case-sensitive. name LIKE '%desk%' therefore finds Desk Lamp, Standing Desk, and Desk Organizer in SQLite, but none in PostgreSQL under case-sensitive collation. PostgreSQL also provides the case-insensitive ILIKE. MySQL's behavior follows collation.
For the ASCII product names used here, LOWER(name) LIKE '%desk%' gives the same matches in SQLite and PostgreSQL: LOWER converts the name to lowercase before matching. Case conversion for other writing systems depends on database and language rules. SQLite's equality is also different from its LIKE: city = 'chicago' fails to find Chicago under the default binary collation even though city LIKE 'chicago' finds it.
Search for a real wildcard character
To match an underscore literally, choose an escape character:
WHERE code LIKE 'SPRING!_%' ESCAPE '!'
In a discount-code table, this matches 'SPRING_10' but not 'SPRINGS10'. The !_ is a real underscore; the last % still matches a sequence. Use the same approach with !% for a literal percent sign. Writing ESCAPE explicitly avoids relying on database defaults; PostgreSQL normally uses a backslash and SQLite has no default escape character.
Negation and search size
name NOT LIKE '%Lamp%' returns the other five products. Missing text passes neither LIKE nor NOT LIKE. Of the employees, three phones match '312-%', two recorded phones do not, and the three NULL phones appear in neither result.
A leading % gives an ordinary index no fixed starting text to narrow down, so a large part of the table may be scanned. A fixed start such as 'Desk%' can help an eligible index. For dates, use date ranges: LIKE on these text dates works, but PostgreSQL requires conversion before applying LIKE to a true DATE column.
Key Takeaways
- LIKE matches the whole value;
%covers any number of characters and_exactly one. - Put
%at the start, end, or both to control where a fragment can occur. - Case behavior differs by database and collation; LOWER helps with these ASCII names.
- ESCAPE lets a wildcard represent its literal character.
- NULL matches neither LIKE nor NOT LIKE.
- Leading wildcards can make a search scan more data; use ranges for dates.
4Account for missing values
Missing data changes which records a filter can find. A zero stock count, an empty phone string, and an unrecorded value tell you different things, even if a query tool makes some cells look alike. You will use explicit missing-value tests and trace the third truth value, unknown. These rules explain why a filter and its opposite may leave out the same rows, and how to include those rows when the question calls for them.
4.1 NULL, zero, and empty text
NULL marks a missing value: a value may be unknown, not recorded, or inapplicable. It is different from a known zero or known text with no characters.
Read what a missing value means
Lamp Shade has stock = 0. Its recorded quantity is zero, so it is out of stock. Bookshelf has stock NULL: the quantity was not recorded. The database cannot tell you that it has zero units or any other number.
The distinction changes the question you can answer:
| Question | Condition | Result |
|---|---|---|
| Which products are out of stock? | stock = 0 | Lamp Shade |
| Which stock quantities are unknown? | stock IS NULL | Bookshelf |
| Which quantities are recorded? | stock IS NOT NULL | The other seven products |
A blank-looking cell in a query tool is not enough to distinguish these states. Some tools show NULL as an empty cell; others print NULL. Test the stored value directly.
Use the missing-value tests
SELECT first_name
FROM employees
WHERE phone IS NULL;
Ben, Emma, and Hugo have no recorded phone. IS NULL gives true for their missing values. To find the five employees with a recorded phone, use phone IS NOT NULL: Ana, Chloe, David, Farid, and Grace.
Do not write phone = NULL to find missing phones. An ordinary comparison involving NULL gives unknown, even when both sides are NULL. WHERE keeps true rows only, so = NULL returns none. <> NULL has the same problem. IS NULL is a test for the missing marker and gives a definite true or false answer for these single-column values.
The form NOT phone IS NULL also finds present phones in SQLite and PostgreSQL, but phone IS NOT NULL makes the intended test easier to read.
Separate three kinds of text
| SQL expression | What it represents |
|---|---|
NULL | A missing value |
'' | A known text value with zero characters |
'NULL' | Four ordinary letters |
SQLite, PostgreSQL, MySQL, and SQL Server distinguish empty text from NULL. phone = '' would find empty strings, not Ben's, Emma's, or Hugo's missing phones. If a column can contain both, use phone IS NULL OR phone = '' to find either state. Oracle treats an empty text value as NULL, so this distinction changes when you move databases.
Apply the wording and the schema
“Missing,” “not recorded,” and “not assigned” point to IS NULL. department IS NULL finds Grace. Her missing department does not mean the department text is the word 'NULL', nor does it establish which department she belongs to.
The table definition can mark a column NOT NULL, meaning that a stored row cannot have NULL in that column. An IS NULL query on such a column returns no rows. Check this rule when inspecting a schema: columns that permit missing data need special attention when you build exclusions or combine conditions.
Key Takeaways
- NULL is a missing marker; zero and empty text are known values.
- IS NULL finds missing values, and IS NOT NULL finds present values.
= NULLand<> NULLcannot find rows through ordinary comparisons.- Quoted
'NULL'is text, and Oracle treats empty text differently from the other databases shown. - A NOT NULL column cannot store NULL.
4.2 True, false, and unknown
A condition in SQL can be true, false, or unknown. Missing values make the third state necessary: without a recorded quantity, a database cannot establish whether that quantity is above a threshold.
A condition and its opposite
For Bookshelf, stock > 10 is unknown because stock is NULL. So is stock <= 10. WHERE keeps only true conditions and drops both false and unknown ones.
In the products table, stock > 10 returns four rows and stock <= 10 returns three. Together they cover seven of the eight products. Bookshelf is absent from both. Applying NOT does not restore it: NOT unknown is unknown, so NOT (stock > 10) still selects only the three known counts at most 10.
This differs from a value that fails a condition. Lamp Shade's known zero makes stock > 10 false, so NOT turns it true. A missing count supplies no such definite comparison to reverse.
Combine unknown with another test
| Combination | Result |
|---|---|
| Unknown AND false | False |
| Unknown AND true | Unknown |
| Unknown AND unknown | Unknown |
| Unknown OR true | True |
| Unknown OR false | Unknown |
| Unknown OR unknown | Unknown |
Bookshelf costs 120.00. In stock > 10 OR price > 100, the price condition is true. OR needs only one true part, so the row is kept even though the stock comparison is unknown. In stock > 10 AND price > 100, the stock condition remains unresolved, so the result is unknown and the row is dropped.
Include missing values deliberately
SELECT first_name
FROM employees
WHERE department <> 'Sales'
OR department IS NULL;
This returns Ben, David, Farid, Hugo, and Grace. The comparison alone would return four, leaving Grace out. The IS NULL branch explicitly includes staff whose department is unassigned. Whether such staff belong in “outside Sales” is a question about the intended answer; the query must express that choice.
A NULL value in a NOT IN list can block the entire result. department NOT IN ('Sales', NULL) means that the department must differ from Sales and differ from NULL. The latter comparison is unknown for every row, so the full condition is never true. IN still keeps real matches: department IN ('Sales', NULL) selects the three Sales employees, but does not find Grace.
A missing tested value also produces unknown for LIKE and for an ordinary BETWEEN range. IS NULL and IS NOT NULL give definite answers for these scalar columns. Check missing values with those tests separately when a row count seems surprising.
Know when NULLs are grouped
NULL = NULL remains unknown; two missing phone numbers are not known to be the same number. DISTINCT, however, treats repeated missing values as duplicates, and ORDER BY places them together. For a comparison that deliberately treats two NULLs as equal, SQLite and PostgreSQL also provide IS NOT DISTINCT FROM.
Key Takeaways
- WHERE keeps true rows and removes false and unknown rows.
- NOT leaves unknown unchanged; negated filters can still miss NULL rows.
- A true OR branch can keep a row with another unknown branch.
- Add IS NULL explicitly when missing values should belong in the answer.
- A NULL in a NOT IN list means no row can pass that exclusion filter.
- Duplicate removal groups NULLs even though ordinary NULL equality is unknown.
5Sort results and choose a page
A row limit becomes meaningful only after you decide which rows should come first. Sorting can arrange numbers, text, or dates, and later sort columns settle ties in earlier ones. Missing values and tied rows need attention when a shortlist or a page must be repeatable. You will build complete sort orders, apply limits after filtering, and recognize the different row-limit syntax used by common database systems.
5.1 Ascending and descending order
ORDER BY sets the order of result rows. Without it, SQL makes no promise about which row appears first, even when a small table seems to return the same order every time.
Choose the direction
| Direction | Numbers | Complete dates | Text |
|---|---|---|---|
ASC | Smallest first | Earliest first | Collation order |
DESC | Largest first | Latest first | Reverse collation order |
ASC is the default. ORDER BY salary and ORDER BY salary ASC ask for the same direction. “Highest paid first” calls for ORDER BY salary DESC.
SELECT first_name, salary
FROM employees
ORDER BY salary DESC;
Read the result from highest to lowest: Farid 83000, Ben 71000, Emma 65000, David 60000, Hugo 58000, Ana 52000, Chloe 48000, and Grace 45000. The database rearranges the result rows; this query does not change the stored table.
Filter before sorting
SELECT name, price
FROM products
WHERE category = 'Lighting'
ORDER BY price;
Only the three Lighting rows enter this result. Their ascending prices produce Lamp Shade 15.50, Desk Lamp 24.99, then Floor Lamp 89.00. ORDER BY follows WHERE in the statement. Reversing those clauses is a syntax error.
The sort column need not be displayed. SELECT first_name FROM employees ORDER BY salary DESC; gives the same name order without showing salaries. PostgreSQL restricts this when DISTINCT is present: a DISTINCT query must select its sort columns.
Read the stored data type
Numeric columns sort by numeric size. Text columns sort by characters under their collation. The text values '10', '9', and '100' sort as '10', '100', '9' under ordinary character ordering. This is not a correct numeric ranking, so check the column type when amounts look out of order.
The example hire_date values are complete fixed-width text dates with the year first. ORDER BY hire_date starts with Farid on 2017-09-12 and ends with Grace on 2024-02-19. DESC reverses that for newest hires first. Month-first text dates such as MM/DD/YYYY compare by month before year and cannot give reliable calendar order across years.
Text order depends on collation
SQLite's default binary collation puts uppercase ASCII letters before lowercase letters. Under that rule, 'Zebra' precedes 'apple', and 'é' comes after 'z'. Many PostgreSQL databases use language-aware collations that produce another order. Consistent capitalization helps, but the database's comparison rules still decide the order of accented and other characters.
Use column names for a clear sort specification. ORDER BY 2 DESC means the second item in the SELECT list, so moving columns can silently change the sort. ORDER BY 'salary' names a text constant, not the salary column: SQLite provides no useful ordering from it, and PostgreSQL rejects it. Write ORDER BY salary.
Key Takeaways
- ORDER BY determines row order; ASC is the default and DESC reverses the direction.
- WHERE filters rows before they are sorted.
- Numeric size, date representation, and text collation determine what “ascending” means.
- Name sort columns explicitly to avoid quoted constants and changing column positions.
5.2 Several sort columns, ties, and missing values
A second sort column matters only when rows tie on the first. Think of a sorted staff list: group by city first, then put the names within each city in order.
Follow the sort columns from left to right
SELECT last_name, city
FROM employees
ORDER BY city, last_name;
The result begins with Kim and Okafor in Boston. Then come Haddad, Lopez, Nguyen, and O'Neill in Chicago. Brown and Carter in Denver are last. last_name does not compete with city; it decides the order only within a shared city.
Reversing the columns changes the main order. ORDER BY last_name, city mainly sorts by last name and consults city only when last names tie. Use one ORDER BY clause with commas between its sort columns. A second ORDER BY clause is invalid.
Give each column its direction
ORDER BY department, salary DESC means department ascending, then salary descending within each department. DESC belongs only to salary. To make both descending, write department DESC, salary DESC.
In SQLite's default NULL ordering, the first form places Grace's missing department first. IT follows with Farid then Ben; Marketing has David then Hugo; Sales has Emma, Ana, then Chloe. You are reading one hierarchy of comparisons, not first sorting every row by salary and then ignoring that order.
Finish a tie with a unique value
Rows equal on every sort column can appear in either order. ORDER BY city does not settle which Chicago employee comes first. A complete sort order adds a last column that uniquely identifies each row:
SELECT first_name, city
FROM employees
ORDER BY city, employee_id;
The IDs resolve every city tie. For unchanged data, this makes a later LIMIT or page select a repeatable part of the ordered list. A unique tie-breaker fixes which rows appear at a cutoff; it does not include extra tied rows.
Position missing values deliberately
Database defaults differ:
| Database | NULL in ASC order | NULL in DESC order |
|---|---|---|
| SQLite, MySQL, SQL Server | First | Last |
| PostgreSQL, Oracle | Last | First |
So the department query puts Grace last in PostgreSQL under its default, even though it puts her first in SQLite. This difference is separate from the salary direction.
SELECT first_name, department, salary
FROM employees
ORDER BY department NULLS LAST, salary DESC;
SQLite 3.30 or later, PostgreSQL, and Oracle support NULLS FIRST and NULLS LAST. Here, they put Grace after all recorded departments while preserving salary order within each department. MySQL and SQL Server do not accept those clauses.
Translate a requested hierarchy in the same sequence it is spoken: “by department, highest paid first within each department” gives department, salary DESC. “Alphabetically by last name, then first name” gives last_name, first_name. Add an ID last when you need repeatable order among people with identical names.
Key Takeaways
- Later ORDER BY columns break ties in earlier ones.
- ASC or DESC applies to its own column; omitted directions default to ASC.
- Adding a unique final sort column makes the order complete for unchanged data.
- NULL placement depends on the database; supported NULLS FIRST/LAST clauses set it explicitly.
- A tie-breaker settles row choice without expanding a LIMIT to include all ties.
5.3 Top results and pages with LIMIT
LIMIT keeps at most a chosen number of result rows. It does not decide which rows deserve to be first. Use ORDER BY to give a shortlist a meaning such as highest paid, newest, or cheapest.
Sort, then cut
SELECT first_name, salary
FROM employees
ORDER BY salary DESC
LIMIT 3;
The sorted salaries begin with Farid 83000, Ben 71000, and Emma 65000, so those three rows remain. Without ORDER BY, LIMIT 3 returns some three rows, with no promise that they are the highest earners.
WHERE works before the limit. WHERE city = 'Boston' ORDER BY salary DESC LIMIT 5 returns only David and Grace because just two employees qualify. LIMIT is a maximum, not a request to invent extra rows. LIMIT 0 returns none.
Skip rows for a later page
OFFSET skips rows before LIMIT counts the rows to return. ORDER BY salary DESC LIMIT 3 OFFSET 3 skips Farid, Ben, and Emma, then returns David, Hugo, and Ana: positions four through six.
For page k with n rows per page, skip (k − 1) × n rows. Page three at three rows per page uses LIMIT 3 OFFSET 6. Only Chloe and Grace remain. The first page has offset zero, not one.
“Second row” and “second different value” are different requests. ORDER BY salary DESC LIMIT 1 OFFSET 1 returns Ben with 71000 here. If two employees shared the top salary, that query would return another top earner. To find the second different salary, select DISTINCT salary before sorting and skipping.
Settle the cutoff
ORDER BY city LIMIT 3 includes the two Boston employees and one Chicago employee, but does not specify which Chicago employee. ORDER BY city, employee_id LIMIT 3 selects David, Grace, and Ana. The unique ID resolves the cutoff.
To keep everyone tied at the final sort value, PostgreSQL 13 or later and Oracle support FETCH FIRST 3 ROWS WITH TIES. With ORDER BY city, it keeps six rows: both Boston rows and all four Chicago rows. If the ORDER BY also includes a unique ID, rows cannot tie on the complete sort key.
Recognize the database's syntax
| Database | A three-row limit |
|---|---|
| SQLite, PostgreSQL, MySQL | ORDER BY salary DESC LIMIT 3 |
| PostgreSQL, Oracle 12c or later | ORDER BY salary DESC FETCH FIRST 3 ROWS ONLY |
| SQL Server | SELECT TOP (3) first_name, salary FROM employees ORDER BY salary DESC; |
Standard SQL uses OFFSET and FETCH, not LIMIT. PostgreSQL and Oracle accept OFFSET 3 ROWS FETCH FIRST 3 ROWS ONLY. SQL Server uses ORDER BY salary DESC OFFSET 3 ROWS FETCH NEXT 3 ROWS ONLY; ORDER BY is required for this paging form.
SQLite and MySQL also accept LIMIT 6, 3, with the offset first. Prefer LIMIT 3 OFFSET 6 so the roles are explicit. Paging runs a query again for each page. A complete sort order keeps pages consistent while data stays unchanged; inserts or deletions between requests can still make pages repeat or skip records.
Key Takeaways
- ORDER BY decides which rows come first; LIMIT keeps at most the requested number.
- OFFSET skips rows before LIMIT counts; the first page uses offset zero.
- Select DISTINCT first when ranking different values instead of individual rows.
- A unique tie-breaker fixes a cutoff; WITH TIES can include extra rows tied on the full sort key.
- LIMIT, TOP, and OFFSET/FETCH are database-specific forms of row limiting.
- Changed data can shift page boundaries between requests.
6Build and check a complete query
A workplace question usually combines several choices: which records count, which details to show, and which rows come first. Planning those choices separately helps you write a query whose parts agree. The written clause order and the logical order explain why a filter can use a hidden column and why a limit applies to qualifying rows. You will also check boundaries, missing data, and ties before trusting a plausible-looking result.
6.1 Written order and logical processing
SQL clauses have a fixed written order. Their logical order explains the result as a sequence of decisions, even when the database uses a different internal execution plan.
Write the statement in clause order
SELECT DISTINCT city
FROM employees
WHERE department = 'Sales'
ORDER BY city
LIMIT 2 OFFSET 0;
SELECT and FROM identify the requested columns and source table. The other clauses are optional, but when present they keep this order: WHERE, ORDER BY, LIMIT, then OFFSET in this LIMIT form. DISTINCT sits directly after SELECT.
Use one WHERE clause with several joined conditions. A second WHERE is a syntax error. The same applies to ORDER BY: put several sort columns in one clause, separated by commas. Moving WHERE below ORDER BY does not ask for a different processing order; it produces invalid SQL.
Trace the logical decisions
| Logical stage | What happens in the example |
|---|---|
| FROM | Read employee rows. |
| WHERE | Keep Ana, Chloe, and Emma in Sales. |
| SELECT | Keep their city values: Chicago, Chicago, Denver. |
| DISTINCT | Reduce those values to Chicago and Denver. |
| ORDER BY | Put Chicago before Denver. |
| OFFSET and LIMIT | Skip none and return at most two rows. |
The result is Chicago, then Denver. The repeated city disappears before the limit counts result rows. You are not taking two employees and then asking whether they live in different cities.
The optimizer may use a different physical execution plan that produces the same result as this logical sequence.
Use columns where they are available
WHERE tests the source rows before SELECT chooses displayed columns:
SELECT first_name
FROM employees
WHERE salary > 60000;
This works without salary in the output. The qualifying names are Ben, Emma, and Farid. Salary is still a column of each source row when the test is made.
Likewise, filtering before limiting is essential for a top result within a group:
SELECT first_name
FROM employees
WHERE department = 'Sales'
ORDER BY salary DESC
LIMIT 1;
The answer is Emma. Sales staff qualify first; their salaries are then ranked. Taking the company's top earner first and testing that single row for Sales would miss the intended answer, because Farid belongs to IT.
Make the structure visible
Write one clause per line and place longer conditions on separate indented lines. For “Furniture products below 200 dollars, highest price first,” use:
SELECT name, price
FROM products
WHERE category = 'Furniture'
AND price < 200
ORDER BY price DESC;
Office Chair at 149.00 comes before Bookshelf at 120.00. The two conditions share one WHERE, the numeric threshold has no currency sign, and ORDER BY sorts the rows that passed. Checking each line against its role makes both syntax errors and wrong interpretations easier to locate.
Key Takeaways
- Write SELECT, FROM, WHERE, ORDER BY, and LIMIT in their required clause order.
- Logically read the table, filter, select columns, remove duplicates, sort, then slice rows.
- WHERE can test source columns that SELECT does not display.
- Filtering before ranking finds the top result within the requested group.
6.2 From a request to a checked answer
Before writing SQL, identify the separate decisions hidden in an everyday request. Each decision belongs to a clause, and each clause can be checked against the intended answer.
Make six decisions
- Which table contains the records? Choose FROM.
- Which records qualify? Build WHERE.
- Which fields answer the question? Choose SELECT.
- Are repeated result values useful? Decide on DISTINCT.
- What should come first? Build ORDER BY.
- Is there a maximum or page? Add LIMIT and OFFSET.
For “at least,” include equality. “One of” suggests IN, “contains” suggests a LIKE pattern with percent signs at both ends, “missing” suggests IS NULL, and “different values” suggests DISTINCT. A top-n request needs both a sort direction and a limit.
Build a staff shortlist
“Show the names and hire dates of the three newest hires outside Sales who have a phone on file, newest first.” Include unassigned staff in this request's meaning of outside Sales.
Start with employees. The qualifying department rule is department <> 'Sales' OR department IS NULL. Group that choice so the phone rule applies to both branches. Show names and dates, then rank the qualifying rows:
SELECT first_name, last_name, hire_date
FROM employees
WHERE (department <> 'Sales' OR department IS NULL)
AND phone IS NOT NULL
ORDER BY hire_date DESC
LIMIT 3;
Grace Okafor on 2024-02-19 is first, David Kim on 2020-11-23 second, and Farid Haddad on 2017-09-12 third. Removing the IS NULL branch loses Grace and leaves only two results. For an unclear request, settle how missing departments should count and state the choice with the answer.
Combine category, price, and stock
“Which Lighting or Storage products cost less than 20 dollars and are in stock, cheapest first?” Use IN for the two category choices and require a known positive quantity:
SELECT name, price
FROM products
WHERE category IN ('Lighting', 'Storage')
AND price < 20
AND stock > 0
ORDER BY price;
Paper Tray at 9.99 comes before Desk Organizer at 18.75. Lamp Shade is cheap enough and in a chosen category, but its stock is zero, so it fails the final test. A missing stock quantity would also fail because it cannot establish that stock is positive.
Check the rows that could expose a mistake
Begin with SELECT * FROM products LIMIT 5; when exploring the schema and a few records. Add filters one at a time, checking changes in the result count and temporarily displaying tested columns when useful.
Test exact boundaries, missing values, text capitalization, and ties at the limit. NOT shows rows where a condition was false; it does not reveal rows where the condition was unknown. Run IS NULL tests separately. Check that your output has the requested columns and that ORDER BY resolves any ties that affect the answer.
When moving the query to another database, check row-limit syntax, equality and LIKE case rules, identifier quoting, date handling, and NULL placement. The examples use LIMIT in SQLite and PostgreSQL; standard SQL's row-limit form is OFFSET with FETCH FIRST.
Key Takeaways
- Plan the table, qualifying rows, output columns, duplicates, ordering, and row count separately.
- Write the missing-value choice explicitly when an exclusion should include unassigned records.
- Check boundaries and individual filter effects against the question.
- Inspect NULLs separately because NOT does not turn unknown into true.
- Recheck case, dates, names, NULL ordering, and row limits when changing databases.
SQL SELECT examples
1From employee records to a city list
11 steps
- 1IntroductionOne table, two kinds of answers
A staff list and a list of staff cities use the same employee records, but they need different result shapes. A basic SELECT chooses the fields to display. DISTINCT can then reduce repeated selected values. We will first build a two-column employee result, then change the question to different cities and trace what disappears.
- 2ExplanationChoose the displayed columns
Start with a specific request: show each employee’s city followed by their first name. Write:
SELECT city, first_name FROM employees;The SELECT list sets the output column order, so city comes first. FROM supplies the table. There is no filter, so all eight employees contribute a row; this plain query leaves the stored records unchanged.
- 3ExampleRead the result shape
Ana’s record contributes Chicago and Ana; Ben’s contributes Denver and Ben. Each remaining employee contributes the same two fields. The result therefore has eight rows and two columns. Repeated cities stay because those rows still describe different people. No ORDER BY is present, so these examples identify rows without promising their display order.
- 4ChallengeCheck a changed SELECT list
What changes if the query becomes
SELECT first_name FROM employees;? State its row count and column count.---reveal---
It still returns eight rows, now with one column, first_name. Removing city from SELECT removes a displayed field, not employee records. A filter or row limit would be needed to reduce the rows.
- 5TipKeep syntax errors local
Separate column names with commas, but leave no trailing comma before FROM. Use a semicolon to end the statement;
--begins a comment to the end of the line. If the database reports an unknown column, compare the spelling with the schema.SELECT *is useful for a first look, while explicit names keep a saved result focused. - 6ExplanationAsk for different cities
Now ask which cities have staff. First names no longer belong in that answer:
SELECT DISTINCT city FROM employees;The eight city entries reduce to Chicago, Denver, and Boston, in no guaranteed order. DISTINCT removes repeats from the selected result, not from the stored employee table. Each city appears once even though several people may live there.
- 7ExampleCompare complete selected rows
Add department to the SELECT list. Ana and Chloe both contribute the pair Sales-Chicago, which collapses to one row. Emma contributes Sales-Denver, which stays because the city differs. The complete
SELECT DISTINCT department, city FROM employees;result has seven rows. DISTINCT is written once after SELECT and compares every selected field together. - 8ChallengeA unique ID changes duplicate removal
How many rows would
SELECT DISTINCT employee_id, city FROM employees;return, and why?---reveal---
All eight rows remain. Every employee_id is unique, so each ID-city pair differs from every other pair. Repeated cities alone are insufficient to make complete selected rows duplicate.
- 9Key InsightDecide what a NULL row says
A DISTINCT department list contains Sales, IT, Marketing, and one NULL row for Grace. Repeated missing markers would also collapse to one. That row says a department is unrecorded; it is not another named department. Duplicate grouping treats NULLs together even though an ordinary comparison
NULL = NULLis unknown. - 10ChallengeFinish with a sorted shortlist
Predict
SELECT DISTINCT city FROM employees ORDER BY city LIMIT 2;. Give the values in their returned order and explain what the limit counts.---reveal---
Boston, then Chicago. DISTINCT first reduces the city values to three; ORDER BY sorts those values and LIMIT keeps two. It counts different result cities after duplicate removal, not two employee records.
- 11RecapMatch the output to the question
A staff result keeps identifying fields; a city inventory selects the city alone with DISTINCT. Selected columns determine duplicate comparisons, while ORDER BY determines row order. When changing a query, check both the output fields and what one result row represents.
2Build a filter that means what you intend
13 steps
Save this lesson to follow every Walkthrough step by step.
- 1IntroductionEvery filter asks a precise question
- 2ExplanationTranslate the pay threshold
- 3ChallengeInclude the boundary when needed
- 4TipKeep numeric literals plain
- 5ExplanationQuote a text value
- 6ExampleKeep an apostrophe inside the value
- 7ExplanationUse a complete date boundary
- 8ChallengeCheck a date with a time
- 9ExplanationJoin the conditions
- 10ExampleApply one city rule to alternatives
- 11ChallengeReverse the group
- 12ChallengeCombine a date and a department group
- 13RecapTrace a revealing row
3Choose an exact list, a range, or a pattern
15 steps
Save this lesson to follow every Walkthrough step by step.
- 1IntroductionThree meanings of a match
- 2ExplanationStart with whole category choices
- 3ExampleApply a stock rule to the list
- 4ChallengePredict an exclusion
- 5ExplanationPlace a price inside a range
- 6ChallengeRemove the endpoints deliberately
- 7TipCover a whole month with times
- 8ExplanationDecide where the fragment belongs
- 9ExampleCount one character with an underscore
- 10ExampleMake the wildcard literal
- 11ChallengeCheck a pattern at its edges
- 12ConnectionCheck database case behavior
- 13ChallengeAccount for a missing phone
- 14ChallengeCombine all three kinds of matching
- 15RecapChoose the form from the meaning
4Find and reason about missing data
10 steps
Save this lesson to follow every Walkthrough step by step.
- 1IntroductionAn unrecorded quantity is unresolved
- 2ExampleSeparate zero from missing
- 3ExplanationTest presence directly
- 4ChallengeCheck empty text
- 5ExplanationFollow a comparison and its opposite
- 6ExampleLet a true OR branch decide
- 7ChallengeA false AND branch can settle it
- 8Key InsightAvoid NULL inside an exclusion list
- 9ChallengeInclude missing departments, require phones
- 10RecapInspect missing data separately
5Build a repeatable sorted page
15 steps
Save this lesson to follow every Walkthrough step by step.
- 1IntroductionA page is a slice of an ordered answer
- 2ExampleChoose highest first
- 3ChallengeSort only qualifying products
- 4TipKnow whether the sort is numeric
- 5ExplanationRead sort priorities from the left
- 6ChallengeGive each sort column its direction
- 7ExplanationResolve the remaining ties
- 8ConnectionPlace missing values explicitly
- 9ExampleKeep the first three ranked rows
- 10ExampleMove past the first page
- 11Key InsightA row rank is not a distinct-value rank
- 12ChallengeInspect a tied cutoff
- 13TipRecognize another row-limit form
- 14ChallengeKeep the second city page
- 15RecapState the ranking before the slice
6Translate and check a workplace request
12 steps
Save this lesson to follow every Walkthrough step by step.
- 1IntroductionSeparate the decisions in a request
- 2ExplanationWrite valid clause order
- 3ExampleTrace the logical model
- 4ChallengeFilter before choosing a winner
- 5ExplanationIdentify the staff request
- 6ExampleMake the department rule explicit
- 7ExampleApply the phone requirement
- 8ExampleRank the qualifying records
- 9ChallengeCheck the missing-value decision
- 10ChallengeConnect the clauses to one pipeline
- 11TipChoose checks that expose mistakes
- 12RecapRead the answer back into the request
SQL SELECT practice questions
Set 1: Lists, ranges, and character patterns10 Practice Questions
Question 1 of 10
temperature column contains 4, 9, 14, 19, and NULL. What values pass WHERE temperature NOT BETWEEN 9 AND 14?Show answerHide answer
Question 2 of 10
promo_code column contains 'VIP_2', 'VIPA2', 'VIP_', and 'VIP2'. What set matches WHERE promo_code LIKE 'VIP!_%' ESCAPE '!'?Show answerHide answer
Question 3 of 10
fee column has values 12, 18, 24, and 30. What values pass WHERE fee BETWEEN 18 AND 30?Show answerHide answer
Question 4 of 10
report?Show answerHide answer
Question 5 of 10
requests table has (request_id, region) rows (31, 'South'), (32, 'East'), (33, 'South'). What set of IDs passes WHERE region IN ('South', 'South')?Show answerHide answer
Question 6 of 10
labels table, what set of values matches label LIKE '__7%': 'AB7', 'A7', 'AB70', and 'ABC7'?Show answerHide answer
Question 7 of 10
appointments table stores time-bearing text in fixed-width YYYY-MM-DD HH:MM:SS form. What condition selects every appointment in February 2028?Show answerHide answer
Question 8 of 10
word LIKE 'map%' from 'Maple', 'map', 'roadmap', and 'MAPS'?Show answerHide answer
Question 9 of 10
members table has (member_id, tier) rows (41, 'Gold'), (42, 'Silver'), (43, NULL). What set of IDs passes WHERE tier IN ('Silver', NULL)?Show answerHide answer
Question 10 of 10
shipments table records one port per row. What condition excludes both Oakland and Tacoma when all port values are recorded?Show answerHide answer
Set 2: Columns, comparisons, and clear conditions10 Practice Questions
Save this lesson to see every answer and practice every set.
Question 1 of 10
rooms table has 12 rows. What is the shape of the result of SELECT floor, room_name FROM rooms;?Show answerHide answer
Save this lesson to see this answer.
Question 2 of 10
parcels table stores numeric weight_kg. What condition keeps parcels weighing at most 8 kilograms?Show answerHide answer
Save this lesson to see this answer.
Question 3 of 10
D'Arcy in a surname column?Show answerHide answer
Save this lesson to see this answer.
Question 4 of 10
tickets table has status and numeric priority. What condition selects open or pending tickets, each with priority at least 4?Show answerHide answer
Save this lesson to see this answer.
Question 5 of 10
visits table contains (site, day) pairs ('East', 'Mon'), ('East', 'Mon'), ('East', 'Tue'), and ('West', 'Mon'). How many rows does SELECT DISTINCT site, day FROM visits; return?Show answerHide answer
Save this lesson to see this answer.
Question 6 of 10
team = 'Blue', city = 'Austin', and score = 62. Does it pass WHERE team = 'Blue' OR city = 'Miami' AND score >= 80, and why?Show answerHide answer
Save this lesson to see this answer.
Question 7 of 10
YYYY-MM-DD. What condition selects dates after the calendar year 2025?Show answerHide answer
Save this lesson to see this answer.
Question 8 of 10
zone value is either North or West?Show answerHide answer
Save this lesson to see this answer.
Question 9 of 10
loans table has numeric days_late values 0, 2, 5, and NULL. How many rows pass WHERE days_late <> 0?Show answerHide answer
Save this lesson to see this answer.
Question 10 of 10
stockrooms table has (room_id, building) rows (1, 'C'), (2, 'A'), (3, 'A'), (4, 'B'), (5, 'C'). What ordered values result from SELECT DISTINCT building FROM stockrooms ORDER BY building DESC LIMIT 2;?Show answerHide answer
Save this lesson to see this answer.
Set 3: Missing values and combined filters10 Practice Questions
Save this lesson to see every answer and practice every set.
Question 1 of 10
load values: 0, 3, 7, NULL, NULL. Two queries use WHERE load > 3 and WHERE NOT (load > 3). What are their row counts, in that order?Show answerHide answer
Save this lesson to see this answer.
Question 2 of 10
deliveries table has district values 'Hill', NULL, 'Hill', NULL, and 'Lake'. How many rows does SELECT DISTINCT district FROM deliveries; return?Show answerHide answer
Save this lesson to see this answer.
Question 3 of 10
WHERE rating >= 4 and then checks excluded rows with WHERE NOT (rating >= 4). Some records appear in neither result. What additional test reveals records whose rating was not recorded?Show answerHide answer
Save this lesson to see this answer.
Question 4 of 10
messages table has subjects 'Note A', 'Alert', NULL, and 'Note B'. How many rows pass WHERE subject NOT LIKE 'Note%'?Show answerHide answer
Save this lesson to see this answer.
Question 5 of 10
contacts table stores phone values NULL, '', 'NULL', and '555-0188'. How many rows pass WHERE phone IS NULL?Show answerHide answer
Save this lesson to see this answer.
Question 6 of 10
devices row has battery_pct = NULL and status = 'ready'. What happens under WHERE battery_pct > 50 OR status = 'ready'?Show answerHide answer
Save this lesson to see this answer.
Question 7 of 10
email value, including empty text if it is stored as a value in PostgreSQL?Show answerHide answer
Save this lesson to see this answer.
Question 8 of 10
jobs table contains (job_id, hours, urgent) rows (51, NULL, 'yes'), (52, 6, 'no'), (53, 2, 'yes'), (54, NULL, 'no'). What set passes WHERE NOT (hours > 4) OR urgent = 'yes'?Show answerHide answer
Save this lesson to see this answer.
Question 9 of 10
files table has folder values 'Archive', 'Active', and NULL. What condition includes Active and the missing folder, while excluding Archive?Show answerHide answer
Save this lesson to see this answer.
Question 10 of 10
routes table has (route_id, depot) rows (61, 'A'), (62, 'B'), (63, NULL). What set of IDs passes WHERE depot NOT IN ('A', NULL) OR depot IS NULL?Show answerHide answer
Save this lesson to see this answer.
Set 4: Ranking, ties, and later pages10 Practice Questions
Save this lesson to see every answer and practice every set.
Question 1 of 10
duration_minutes column has values 16, 4, and 23. What is their order under ORDER BY duration_minutes?Show answerHide answer
Save this lesson to see this answer.
Question 2 of 10
racers table has (racer_id, group_name, points) rows (71, 'A', 8), (72, 'B', 12), (73, 'A', 15). What ID order results from ORDER BY group_name, points DESC?Show answerHide answer
Save this lesson to see this answer.
Question 3 of 10
Show answerHide answer
Save this lesson to see this answer.
Question 4 of 10
orders table has unique order_id values, and several rows share both order_date and total. What ORDER BY gives newest dates first, highest totals within a date, and a complete order?Show answerHide answer
Save this lesson to see this answer.
Question 5 of 10
offers table has (offer_id, rate) rows (81, 40), (82, 40), (83, 35), (84, 30). What rate results from SELECT DISTINCT rate FROM offers ORDER BY rate DESC LIMIT 1 OFFSET 1;?Show answerHide answer
Save this lesson to see this answer.
Question 6 of 10
queues table contains names 'Delta', NULL, and 'Alpha'. Under the default ORDER BY queue_name ASC, where is the NULL row in SQLite and PostgreSQL?Show answerHide answer
Save this lesson to see this answer.
Question 7 of 10
sales table has (sale_id, amount) rows (91, 70), (92, 70), (93, 55), (94, 55), (95, 20). In PostgreSQL, how many rows does SELECT sale_id, amount FROM sales ORDER BY amount DESC FETCH FIRST 3 ROWS WITH TIES; return?Show answerHide answer
Save this lesson to see this answer.
Question 8 of 10
'6', '12', and '100' under binary collation. What is their ascending order?Show answerHide answer
Save this lesson to see this answer.
Question 9 of 10
ORDER BY item_id LIMIT 4 OFFSET 4 on unchanged rows with unique IDs 1 through 12. Before the next page query, row 2 is deleted. What IDs does LIMIT 4 OFFSET 8 now return under the same sort?Show answerHide answer
Save this lesson to see this answer.
Question 10 of 10
tasks table has (task_id, owner, hours) rows (101, 'Lee', 9), (102, 'Ari', 8), (103, 'Lee', 5), (104, 'Lee', 3). What ID results from SELECT task_id FROM tasks WHERE owner = 'Lee' ORDER BY hours DESC LIMIT 1 OFFSET 1;?Show answerHide answer
Save this lesson to see this answer.
Set 5: Complete queries and result checks10 Practice Questions
Save this lesson to see every answer and practice every set.
Question 1 of 10
Show answerHide answer
Save this lesson to see this answer.
Question 2 of 10
courses table has title, numeric fee, and numeric places_left. What query shows the titles and fees of courses under 75 dollars with available places, cheapest first?Show answerHide answer
Save this lesson to see this answer.
Question 3 of 10
cases table has (case_id, team, opened_on, phone) rows (111, 'Red', '2026-06-12', 'x'), (112, NULL, '2026-06-20', 'y'), (113, 'Blue', '2026-06-18', NULL), (114, 'Blue', '2026-06-10', 'z'). What ordered IDs give the two newest cases outside Red, including unassigned teams, with a recorded phone?Show answerHide answer
Save this lesson to see this answer.
Question 4 of 10
rentals row has branch = 'East' and returned = 'yes'. A query should show unreturned rentals from East or West, but this row appears. The filter is branch = 'East' OR branch = 'West' AND returned = 'no'. What correction applies the return rule to both branches?Show answerHide answer
Save this lesson to see this answer.
Question 5 of 10
catalog table has ASCII product names. What condition finds names containing steel without ASCII case differences in both SQLite and PostgreSQL?Show answerHide answer
Save this lesson to see this answer.
Question 6 of 10
'G', 'G1', 'H2', 'J', and 'J1' under binary collation. What set passes WHERE code BETWEEN 'G' AND 'J' AND code NOT LIKE 'H%'?Show answerHide answer
Save this lesson to see this answer.
Question 7 of 10
logs table contains (region, channel) rows ('East', 'web'), ('East', 'web'), ('East', 'store'), ('West', 'web'), ('West', 'store'). What ordered rows result from SELECT DISTINCT region, channel FROM logs WHERE channel = 'web' ORDER BY region DESC LIMIT 1;?Show answerHide answer
Save this lesson to see this answer.
Question 8 of 10
bids table has (bid_id, amount) rows (121, 90), (122, 90), (123, 80), (124, 80). How many rows does SELECT bid_id, amount FROM bids ORDER BY amount DESC, bid_id FETCH FIRST 3 ROWS WITH TIES; return?Show answerHide answer
Save this lesson to see this answer.
Question 9 of 10
sensors table contains (sensor_id, reading, enabled) rows (131, NULL, 'yes'), (132, 8, 'yes'), (133, 2, 'no'), (134, NULL, 'no'). What set passes WHERE NOT (reading > 5 AND enabled = 'yes')?Show answerHide answer
Save this lesson to see this answer.
Question 10 of 10
fee > 50, and the only available test records have fees 20 and 80. What extra test fee most directly exposes the error?Show answerHide answer
Save this lesson to see this answer.
Stuck on your own question? Homework Help
Key Terms
A
AND
ASC
price ASC puts the cheapest lighting product first and the most expensive last.B
BETWEEN
price BETWEEN 15.50 AND 89.00 includes products priced at exactly 15.50 or 89.00, the two named endpoints.Boundary check
C
Clause order
Collation
'Chicago' from 'chicago', so a filter for 'chicago' misses rows stored as Chicago.Comparison operator
>= includes an employee paid exactly 60000 in salary >= 60000 because that pay equals the threshold.Complete sort order
ORDER BY city, employee_id settles every employee’s position through the unique ID.D
DATE type
DATE '2022-01-01'; SQLite does not use this typed-literal syntax.DESC
ORDER BY hire_date DESC puts an employee hired in 2024 before one hired in 2023 in the employee list.DISTINCT
SELECT DISTINCT city FROM employees; lists just those three cities.Duplicate result row
E
Empty text
phone = '' checks for empty text and does not find employee phone values that are missing.Endpoint
BETWEEN 15.50 AND 89.00 is included, so products priced at exactly 15.50 or 89.00 both qualify.ESCAPE clause
LIKE 'SPRING!_%' ESCAPE '!' makes the underscore after SPRING a literal character.F
FROM clause
SELECT name, price FROM products; identifies products as the source of both fields.H
Half-open date range
'2024-04-01', preserving late times on the final day.I
Identifier
hire_date names a column, while the quoted text '2024-02-19' represents a value.ILIKE
name ILIKE '%desk%' can match product names containing the capitalized text Desk.IN
department IN ('IT', 'Marketing') selects every IT or Marketing employee without repeating a department comparison.IN list
(6, 2, 2, 4) matches the same employee records as (2, 4, 6), despite its order and repetition.Inclusive threshold
salary >= 60000.Incremental query building
IS NOT DISTINCT FROM
IS NOT NULL
phone IS NOT NULL keeps an employee whose phone is recorded even when that employee’s department is missing.IS NULL
phone IS NULL finds the employees whose records have no phone value recorded.ISO date form
hire_date >= '2023-01-01' includes employees hired on or after that date in calendar order comparisons.L
LIKE
name LIKE '%Lamp%' finds every product name containing Lamp, whether Lamp occurs at the start, middle, or end.LIMIT
Logical processing order
Lower bound
price BETWEEN 89.00 AND 15.50 exceeds the upper bound, leaving no matches.N
NOT
stock > 10 does not include a product with missing stock, because that row still gives an unknown condition.NOT BETWEEN
stock NOT BETWEEN 5 AND 40 keeps a product with zero stock and one with sixty, but excludes missing stock.NOT IN
city NOT IN ('Chicago', 'Denver') keeps Boston staff because their cities differ from both entries.NULL
NULLS LAST
department NULLS LAST puts employees with no recorded department after recorded departments in current SQLite and PostgreSQL queries.Numeric literal
20.50 in price < 20.50 has no quotes, thousands separator, or currency sign.O
OFFSET
Operator precedence
OR
ORDER BY
ORDER BY salary DESC places the highest-paid employee first without changing any stored employee record.P
Page
Percent wildcard
'Lamp%' permits any suffix after Lamp, including no suffix for the value Lamp.Primary sort column
ORDER BY city, last_name, keeping Boston’s names ahead of Chicago’s.Q
Query
SELECT first_name FROM employees; asks the database for the first-name field of each employee.Query optimizer
R
Result check
Result set
SELECT city FROM employees; has eight rows, with repeated city values allowed.S
SELECT clause
SELECT city, first_name FROM employees; puts city before the employee’s first name.Selected combination
Sort column
SQL comment
-- can explain why a filter includes unassigned staff without changing its conditions.SQL dialect
T
Text literal
'O''Neill' matches the surname O’Neill because its doubled quote represents one apostrophe.Three-valued logic
price > 100 branch is true.Tie-breaker
TOP
SELECT TOP (3) can replace a three-row LIMIT shortlist when the query is written for SQL Server.U
Underscore wildcard
Unknown truth value
stock > 10, so that row fails the filter.W
WHERE clause
WHERE salary < 50000 keeps employees paid under 50000, even when the output hides their salaries.WITH TIES
About this lesson
Make your own Study Materials
Turn your notes, slides and documents into Study Guides, Practice Questions, Flashcards and 6 more tools.