When module 3 closed, two halves were left pending: sharpening the filtering and learning to aggregate. This lesson opens the first. Up to now, whenever you've filtered text you've done it with =, that is, demanding an exact, character-by-character match. That works for country = 'Portugal', where the value is a closed code, but it's no use at all for anything a business really asks: "the products carrying the word organic", "the customers with a Portuguese email", "the job titles that end in manager".
That's what LIKE exists for: an operator that compares a string against a pattern instead of against a value. In this lesson you'll learn its two wildcards, the case trap and its three solutions, how to search for a % that's a genuine percentage and not a wildcard, PostgreSQL's regular expressions for the cases LIKE doesn't cover, and —very importantly— why a search that begins with a wildcard can wreck performance on a large table.
Contents
- From exact equality to patterns
LIKEandNOT LIKE: syntax and wildcards- Pattern table: what matches and what doesn't
- Examples over GreenStore
- Upper and lower case:
ILIKE,LOWERand collations - Searching for a literal
%or_:ESCAPE SIMILAR TOand regular expressions- Performance: why
'%text%'can't use an index - Common Mistakes and Tips
- Exercises
- Conclusion
- From exact equality to patterns
The problem with = is that it admits no shades. If management asks for "the listing of organic products", WHERE name = 'Organic' returns zero rows: no product is called exactly that, the word is inside the name.
LIKE solves this by comparing against a pattern, where certain characters stop standing for themselves and start meaning "anything at all".
| id | name | category_id | price |
|---|---|---|---|
| 2 | Organic brown rice 1 kg | 1 | 3.90 |
| 5 | Organic crushed tomato 400 g | 1 | 1.95 |
| 14 | Organic chamomile tea 20 bags | 4 | 3.25 |
3 rows. Notice two details of the pattern. First, the capital O: that's exactly how the word appears in the catalogue, and LIKE is case-sensitive in PostgreSQL (section 5 gives you three ways round that). Second, the pattern is deliberately short: a well-chosen fragment is enough, and cutting the pattern before the part of a word that varies is an elementary and effective trick. '%bag%', for instance, matches both Reusable cotton bags and Organic chamomile tea 20 bags, singular or plural.
LIKE and NOT LIKE: syntax and wildcards
LIKE and NOT LIKE: syntax and wildcardsThe general form is:
The pattern is an ordinary string in which two characters have a special meaning:
| Wildcard | Meaning | Analogy |
|---|---|---|
% |
Any sequence of zero or more characters | The * of operating-system filenames |
_ |
Exactly one character, whatever it is | The ? of filenames |
Any other character in the pattern is compared literally. And there's a consequence that surprises a lot of people: a pattern with no wildcards is equivalent to =.
LIKE's result is a boolean, so it can be combined with AND, OR, NOT and parentheses exactly like the comparisons from 02-03. NOT LIKE is its negation:
SELECT id,
name,
last_name,
job_title
FROM employees
WHERE job_title NOT LIKE '%manager'
ORDER BY id;| id | name | last_name | job_title |
|---|---|---|---|
| 4 | Óscar | Peris Blasco | Sales rep |
| 5 | Laia | Puig Sanchis | Sales rep |
| 6 | Marc | Estévez Roig | Customer support |
| 7 | Irene | Salvador Mira | Warehouse operator |
| 8 | Daniel | Vercher Lluch | Data analyst |
5 rows out of 8. Left out are Rosa Alcázar Vives (General manager), Andrés Company Talens (Sales manager) and Beatriz Nadal Ripoll (Logistics manager).
A null warning, right now:
NOT LIKEdrags along the same problem as<>in 02-03. If the column allowsNULL, those rows don't appear withLIKEor withNOT LIKE, because the comparison returns unknown in both cases. Herejob_titleisNOT NULLand there's no risk, but withcustomers.citythere would be. Lesson 04-03 explains it in depth.
- Pattern table: what matches and what doesn't
The four canonical patterns, with the name each one gets in practice:
| Pattern | Name | Matches | Doesn't match |
|---|---|---|---|
'Organic%' |
Prefix (starts with) | Organic brown rice…, Organic crushed tomato… |
Almond body oil…, organic… (lower case) |
'%ml' |
Suffix (ends in) | …extra 500 ml |
…20 bags, …1 kg |
'%vera%' |
Contains | Aloe vera face cream 50 ml |
Rosemary solid shampoo 80 g |
'____' |
Exact length (4 characters) | Rosa, Marc, Laia |
Óscar (5), Ana (3) |
And the ones that mix both wildcards:
| Pattern | Matches | Comment |
|---|---|---|
'B%a' |
Barcelona (in customers.city) |
Starts with B and ends in a; it doesn't match Bosch Ferrer |
'%(pack of _)' |
…scrubber (pack of 3), …bags (pack of 5), …candles (pack of 2) |
The _ stands in for the digit |
'A_a' |
Ana |
Three letters, first A, last a |
Let's check the second one from that table against real data:
| id | name | price |
|---|---|---|
| 11 | Loofah scrubber (pack of 3) | 5.50 |
| 12 | Reusable cotton bags (pack of 5) | 9.90 |
| 13 | Soy wax candles (pack of 2) | 13.75 |
3 rows, the three products sold in multipack format. The _ has matched 3, 5 and 2 respectively. If we'd written '%(pack of %)' we'd have got the same thing, but (pack of 12) or (pack of assorted) would match too: _ is stricter and therefore more precise when you know a single character goes there.
The _ wildcard is easy to forget
An instructive case with orders.payment_method:
| payment_method |
|---|
| cash_on_delivery |
It matches. But not because the underscores in the pattern are underscores: each _ matched whatever character sat in that position, which here happens to be an underscore. The very same pattern would also match cash-on-delivery or cashXonYdelivery. It's the mistake that makes WHERE column LIKE 'unit_price' find things you weren't expecting when you're searching for column names in a metadata catalogue. Section 6 shows you how to avoid it.
- Examples over GreenStore
4.1. Products by format: the suffix trap
The catalogue encodes the format at the end of the name. Small liquid containers end in ml:
| id | name | category_id | price |
|---|---|---|---|
| 1 | Extra virgin olive oil 500 ml | 1 | 12.50 |
| 6 | Aloe vera face cream 50 ml | 2 | 18.90 |
| 8 | Almond body oil 200 ml | 2 | 14.25 |
| 9 | Calendula lip balm 15 ml | 2 | 4.60 |
| 16 | Ginger kombucha 750 ml | 4 | 4.95 |
5 rows. Now the ones sold by weight in grams:
-- ⚠️ INCORRECT: '%g' also matches 'kg'
SELECT id, name
FROM products
WHERE name LIKE '%g'
ORDER BY id;| id | name |
|---|---|
| 2 | Organic brown rice 1 kg |
| 3 | Raw orange blossom honey 500 g |
| 4 | Spelt pasta 500 g |
| 5 | Organic crushed tomato 400 g |
| 7 | Rosemary solid shampoo 80 g |
| 15 | Ceremonial matcha green tea 30 g |
| 19 | Natural stick deodorant 50 g |
7 rows, and one of them shouldn't be there: the rice is sold in kilos, not grams. '%g' means "ends in the letter g", and kg ends in g. The fix is to include the space in the pattern:
| id | name | price |
|---|---|---|
| 3 | Raw orange blossom honey 500 g | 9.75 |
| 4 | Spelt pasta 500 g | 2.80 |
| 5 | Organic crushed tomato 400 g | 1.95 |
| 7 | Rosemary solid shampoo 80 g | 8.40 |
| 15 | Ceremonial matcha green tea 30 g | 22.00 |
| 19 | Natural stick deodorant 50 g | 7.80 |
6 rows. The space in front of the g is the difference between a correct report and one that mixes units. It's exactly the kind of mistake that gives no warning at all.
4.2. Customers by email domain
GreenStore sells to three countries and the sample emails reflect each one's domain. The Portuguese customers:
SELECT id,
name || ' ' || last_name AS customer,
email,
city,
country
FROM customers
WHERE email LIKE '%.pt'
ORDER BY id;| id | customer | city | country | |
|---|---|---|---|---|
| 7 | Sofia Moreira Costa | [email protected] | Lisbon | Portugal |
| 8 | Tiago Almeida Nunes | [email protected] | Porto | Portugal |
And the French ones, combining two patterns with OR:
SELECT id,
name || ' ' || last_name AS customer,
email,
country
FROM customers
WHERE email LIKE '%.fr'
OR email LIKE '%.pt'
ORDER BY country, id;| id | customer | country | |
|---|---|---|---|
| 9 | Camille Dubois | [email protected] | France |
| 10 | Julien Moreau | [email protected] | France |
| 7 | Sofia Moreira Costa | [email protected] | Portugal |
| 8 | Tiago Almeida Nunes | [email protected] | Portugal |
4 rows: the four international customers. The other 11 use example.com.
Watch what you're actually measuring. Filtering by the email domain is not the same as filtering by
country. They coincide here because the data set was built that way, but in a real database there are Portuguese people with@gmail.comand Spaniards with@example.fr. If the business question is "customers from Portugal", the correct answer isWHERE country = 'Portugal', not aLIKEover the email.LIKEis powerful, and that's exactly why it's easy to use it to answer a similar but different question.
4.3. Job titles in the org chart
SELECT id,
name || ' ' || last_name AS employee,
job_title,
salary
FROM employees
WHERE job_title LIKE '%manager'
ORDER BY id;| id | employee | job_title | salary |
|---|---|---|---|
| 1 | Rosa Alcázar Vives | General manager | 62000.00 |
| 2 | Andrés Company Talens | Sales manager | 41000.00 |
| 3 | Beatriz Nadal Ripoll | Logistics manager | 39500.00 |
3 rows: everyone with management responsibility in the hierarchy you saw in 03-06 — the general manager and the two area managers.
Notice the shape of the pattern. In English the management roles end in manager rather than starting with a common prefix, so this is a suffix pattern ('%manager') and not a prefix one. That has a very practical consequence you'll see in section 8: a suffix pattern can't take advantage of an index, while a prefix pattern —'Sales%', for instance, which would bring back the Sales manager and the two Sales reps— can. When you get to design searches over large tables, whether the fixed part of the pattern sits at the beginning or at the end stops being a stylistic question.
- Upper and lower case:
ILIKE, LOWER and collations
ILIKE, LOWER and collationsIn 02-03 you saw that country = 'spain' returns zero rows. LIKE inherits exactly the same behaviour: in PostgreSQL, LIKE is case-sensitive.
| id | name |
|---|---|
| 2 | Organic brown rice 1 kg |
| 5 | Organic crushed tomato 400 g |
| 14 | Organic chamomile tea 20 bags |
And this is a real problem, because whoever types into a search box doesn't reach for the shift key. There are three solutions.
Solution 1: ILIKE (a PostgreSQL extension)
| id | name | price |
|---|---|---|
| 2 | Organic brown rice 1 kg | 3.90 |
| 5 | Organic crushed tomato 400 g | 1.95 |
| 14 | Organic chamomile tea 20 bags | 3.25 |
The I is for insensitive. Its negation is NOT ILIKE. It's the most readable option and the one we'll use in the course when working on PostgreSQL, with one caveat: it isn't standard SQL, so a query with ILIKE isn't portable.
Solution 2: LOWER on both sides
It returns the same 3 rows. It's portable to any engine, which is why it's the form you'll see in code that has to work on several databases.
Two important details:
LOWERgoes on both sides. If you writeLOWER(name) LIKE '%Organic%'nothing will ever match, because the column has been lowercased but the pattern hasn't.- Applying a function to the column prevents a normal index from being used. It's the same warning from 02-03 about
WHERE price - cost > 5. The solution (an index on the expressionLOWER(name)) is module 8 material.
LOWER and UPPER are studied in depth, along with the rest of the string functions, in lesson 06-01. Here we're only using them as a tool.
Solution 3: a case-insensitive collation
PostgreSQL 12 and later let you define non-deterministic collations, which make the comparison itself ignore case (and even accents). They're declared once and affect =, LIKE and ORDER BY without touching the queries:
It's the cleanest solution when an entire column has to be compared that way, but it's a schema design decision (module 5) and it has trade-offs: with a non-deterministic collation, LIKE over that column can't use the usual indexes.
Behaviour by engine
| Engine | Is LIKE case-sensitive? |
Idiomatic insensitive form |
|---|---|---|
| PostgreSQL | Yes, always | ILIKE, or LOWER(col) LIKE LOWER(pat), or a non-deterministic collation |
| MySQL / MariaDB | No by default: it depends on the collation, and the usual one (utf8mb4_0900_ai_ci) is insensitive to case and to accents |
It already is; to make it sensitive, LIKE ... COLLATE utf8mb4_0900_as_cs or BINARY |
| SQLite | No for ASCII, yes for everything else: 'a' LIKE 'A' is true, but 'á' LIKE 'Á' is false |
LOWER(col) LIKE LOWER(pat), with the same ASCII limitation |
| SQL Server | It depends on the column's collation; typical installations are insensitive (_CI_) |
LOWER(...) or an explicit COLLATE ..._CI_AS |
| Oracle | Yes | LOWER(...), or the session parameters NLS_COMP/NLS_SORT |
Practical consequence: a
LIKEquery that works perfectly in MySQL can return zero rows when ported to PostgreSQL, with nothing having changed in the data. It's one of the dialect differences that wastes the most time in migrations. When you write SQL that has to travel, useLOWER()on both sides and don't rely on anybody's default behaviour.
- Searching for a literal
% or _: ESCAPE
% or _: ESCAPEAnd what if what you're looking for is a percent sign? Since % means "anything", the pattern '%%%' doesn't search for a percentage: it matches absolutely everything.
The solution is to escape the wildcard, marking it with a character that strips its power. In PostgreSQL (and in MySQL) the default escape character is the backslash \:
SELECT message
FROM (VALUES ('20% off drinks'),
('Free shipping over 50 EUR'),
('2-for-1 pack on cosmetics')) AS t(message)
WHERE message LIKE '%\%%';| message |
|---|
| 20% off drinks |
Read the pattern '%\%%' from left to right: % (anything) + \% (a literal percent sign) + % (anything).
If the backslash strikes you as unreadable —or if your data contains backslashes— the ESCAPE clause lets you choose another character:
SELECT message
FROM (VALUES ('20% off drinks'),
('Free shipping over 50 EUR'),
('2-for-1 pack on cosmetics')) AS t(message)
WHERE message LIKE '%!%%' ESCAPE '!';| message |
|---|
| 20% off drinks |
The same result, a more readable pattern. ESCAPE works the same way for the underscore. Coming back to section 3's case, this is how you search for a genuine _:
| Query | What it really searches for |
|---|---|
LIKE 'cash_on_delivery' |
cash + any character + on + any character + delivery → matches cash_on_delivery, but also cash-on-delivery |
LIKE 'cash\_on\_delivery' |
The literal string cash_on_delivery, underscores included |
LIKE 'unit\_price' |
The exact column name, with its underscore |
Dialect note: the SQL standard doesn't define a default escape character; it requires you to declare one with
ESCAPE. PostgreSQL and MySQL do have one (\); SQL Server and Oracle don't, so thereLIKE '%\%%'literally searches for a backslash followed by anything and you have to writeESCAPE '\'explicitly. Always writing an explicitESCAPEis the portable option.
SIMILAR TO and regular expressions
SIMILAR TO and regular expressionsLIKE falls short as soon as the question involves alternatives ("ends in ml, g, kg, L, units or bags"), repetitions ("two or more digits") or character classes ("a letter followed by a number"). PostgreSQL offers two more families.
7.1. SIMILAR TO
It's standard SQL and it's a hybrid: it uses LIKE's wildcards (% and _) plus some regular-expression operators (|, *, +, ?, (), [], {}).
| id | name |
|---|---|
| 1 | Extra virgin olive oil 500 ml |
| 2 | Organic brown rice 1 kg |
| 6 | Aloe vera face cream 50 ml |
| 8 | Almond body oil 200 ml |
| 9 | Calendula lip balm 15 ml |
| 16 | Ginger kombucha 750 ml |
6 rows: the 5 products in millilitres plus the rice in kilos. With LIKE you'd have needed two conditions joined by OR.
In practice SIMILAR TO is rarely used: whoever needs that power usually prefers full regular expressions, and whoever doesn't sticks with LIKE. It's worth knowing about because it shows up in legacy code and because it's the only one of the three families that, along with LIKE, is part of the standard.
7.2. POSIX regular expressions: ~, ~*, !~, !~*
These are PostgreSQL's native operators and they use the regular-expression syntax you already know from any programming language:
| Operator | Meaning |
|---|---|
~ |
Matches the regular expression, case-sensitively |
~* |
Matches, case-insensitively |
!~ |
Does not match, case-sensitively |
!~* |
Does not match, case-insensitively |
A fundamental difference from LIKE: a regular expression matches anywhere in the string by default, not from start to end. That's why ^ (start) and $ (end) are needed when you want to anchor it.
Example 1: validating an email address's format.
SELECT id,
name || ' ' || last_name AS customer,
email
FROM customers
WHERE email !~ '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$'
ORDER BY id;Zero rows, which here is the desired answer: all 15 GreenStore emails have a valid format. This pattern —searching for what doesn't comply— is the usual way of auditing data quality before a migration or an email campaign.
Example 2: products whose name ends in a unit of measurement.
| id | name | price |
|---|---|---|
| 11 | Loofah scrubber (pack of 3) | 5.50 |
| 12 | Reusable cotton bags (pack of 5) | 9.90 |
| 13 | Soy wax candles (pack of 2) | 13.75 |
| 18 | Bamboo toothbrush | 3.50 |
4 rows: the only four products in the catalogue that don't state a size at the end. The other 16 do. This is a genuine catalogue quality check: if tomorrow the content team adds a reference with no format, this query gives it away. With LIKE you'd have to chain six conditions with OR and even then you couldn't demand that a number sat in front of the unit.
7.3. The three families compared
LIKE / ILIKE |
SIMILAR TO |
~ (POSIX) |
|
|---|---|---|---|
| Standard SQL | Yes | Yes | No (PostgreSQL) |
| Wildcards | %, _ |
%, _ + regex operators |
Pure regex syntax (., *, +, ?, [], (), |) |
| Anchoring | Always to the whole string | Always to the whole string | No, unless you use ^ and $ |
Alternatives (a or b) |
No | Yes, (a|b) |
Yes, a|b |
| Repetitions | No | Yes, {2,4} |
Yes, {2,4} |
| Case-insensitive | ILIKE |
Not directly | ~* |
| Can use a B-tree index | Yes, if the pattern starts with fixed text | No | Only with ^ anchoring and planner analysis |
| Readability | Very high | Medium | Low for anyone who isn't fluent in regex |
| When to use it | 90 % of cases | Almost never | Complex validations and extractions |
Dialect note: regular expressions vary a lot between engines. MySQL uses
REGEXP/RLIKE(andREGEXP_LIKEsince 8.0), Oracle usesREGEXP_LIKE, and SQLite has theREGEXPoperator but the function that implements it isn't bundled in: you have to register it from the application.SIMILAR TOpractically only exists in PostgreSQL. If your SQL has to be portable,LIKEis the only safe bet.
- Performance: why
'%text%' can't use an index
'%text%' can't use an indexThis is the point that separates a query that works over 20 rows from one that works over 20 million.
A B-tree index —the normal index, the one you'll study in module 8— stores values sorted alphabetically, like a dictionary. And that determines what it can and can't speed up:
flowchart TD
A["LIKE 'Organic%'"] --> B["The index is sorted.<br/>Everything starting with 'Organic'<br/>sits together, in one contiguous stretch"]
B --> C["✅ Jump straight to that stretch.<br/>Index used"]
D["LIKE '%organic%'"] --> E["Anything containing 'organic' in the middle<br/>can sit at any point<br/>of the alphabetical order"]
E --> F["❌ You have to read and check<br/>EVERY row: a full scan"]
Think of it with a paper dictionary: finding all the words that start with organic means opening at O and reading one stretch. Finding all the ones that contain organic forces you to read the whole dictionary.
| Pattern | Type | B-tree index? |
|---|---|---|
LIKE 'Organic%' |
Fixed prefix | Yes |
LIKE 'Organic%rice%' |
Fixed prefix + rest | Yes, it uses the prefix to narrow down |
LIKE '%organic' |
Suffix | No |
LIKE '%organic%' |
Contains | No |
LIKE '_rganic%' |
Starts with a wildcard | No |
ILIKE 'Organic%' |
Case-insensitive | No with a normal index |
LOWER(name) LIKE 'organic%' |
Function over the column | No with a normal index, yes with an index on the expression |
There are two nuances worth knowing from now on:
- For
LIKE 'prefix%'to use the index in PostgreSQL, the index has to use a special operator class (text_pattern_ops) unless the database is in theCcollation. It's a detail explained in module 8, but it explains why sometimes "I have the index and it isn't using it". - For
'%text%'searches there's a specific solution: thepg_trgmextension, which breaks the text into trigrams (groups of three characters) and lets you create GIN or GiST indexes capable of speeding upLIKE '%text%'andILIKE '%text%'. It's enabled withCREATE EXTENSION pg_trgm;and it's also covered in module 8.
And when LIKE stops being the tool
LIKE compares characters, not words. It doesn't know that candle and candles are the same word, it doesn't ignore accents, it doesn't rank results by relevance and it doesn't understand that somebody searching for "olive oil" wants the same rows as somebody searching for "oil olive".
That's what full-text search exists for, which PostgreSQL ships with out of the box through the tsvector and tsquery types and the @@ operator. It turns text into a list of lexical roots, ignores stop words and supports very efficient GIN indexes. It's outside this course, but it's worth knowing it exists: if you catch yourself chaining five ILIKE '%…%' conditions with OR to build a search box, LIKE is no longer the right tool.
Common Mistakes and Tips
- Using
=when you meantLIKE.WHERE name = '%ml'raises no error: it literally searches for the string%mland returns zero rows. - Forgetting that
LIKEis case-sensitive in PostgreSQL. Zero rows and no clue as to why. If you're coming from MySQL, it's the first surprise you'll get. - Applying
LOWERto only one side.LOWER(name) LIKE '%Organic%'never matches anything. Both sides. - Writing
'%g'when you meant'% g'. The 1 kg rice sneaks into the listing of products sold by the gram. Anchor the pattern with the right separator. - Trusting that
_is a literal underscore. It's a wildcard. To search for the character,\_orESCAPE. - Searching for a
%without escaping it.LIKE '%%%'returns every row in the table. - Assuming
NOT LIKEreturns "everything else". Rows withNULLcome out on neither side. Check that the two halves add up to the total (04-03). - Using
LIKEover the email to deduce the country. It answers a similar question, not the same one. Use the column that models the fact. - Putting a wildcard at the start on a large table.
'%text%'forces a full scan. With 20 rows you won't notice; with 20 million, your query takes minutes. - Tip: always start with the loosest pattern and tighten it up. Fire off
ILIKE '%organ%', look at what comes out, and only then refine. It's quicker than guessing the exact pattern first time. - Tip: count the rows of the condition and of its negation. If
LIKEgives 6 andNOT LIKEgives 14 over a table of 20, the logic closes. If it doesn't close, there are nulls. - Tip: if the pattern has to be portable, write
LOWER(col) LIKE LOWER(pat) ESCAPE '\'. It's more verbose, but it works identically on all five engines.
Exercises
Exercise 1
The content team wants to review how the catalogue is written. Write three queries over products:
- The products whose name contains the word natural (in any position and regardless of case).
- The products whose name starts with the letter
C. - The products that don't carry the word Organic in the name but do belong to category 1 (Food).
In each case state how many rows come out and why you chose that pattern.
Exercise 2
Marketing is preparing an email campaign and needs to segment. Over customers:
- Count how many customers have an email in the
example.comdomain, usingLIKE. - Write the query that returns the customers whose first name has exactly 3 letters.
- A colleague proposes
WHERE email LIKE '%@example.com%'for the first part. Does it return the same thing? Is it equivalent? Which would you prefer, and why?
Exercise 3
Management wants a catalogue audit. Write a single query that returns, for each product, its id, its name and a format column holding:
- the name itself if it does not end in a unit of measurement (the four you saw in section 7.2),
- and nothing else: only those four products should appear.
Solve it first with regular expressions and then answer: could you have done it with LIKE alone? What would you be missing?
Solutions
Solution 1
1. Contains natural, regardless of case:
| id | name | category_id | price |
|---|---|---|---|
| 19 | Natural stick deodorant 50 g | 5 | 7.80 |
1 row. ILIKE is used because the word opens the name with a capital N: LIKE '%natural%' in lower case would return zero rows, and LIKE '%Natural%' would work today but would be fragile against a future reference where the word appeared mid-name in lower case.
2. Starts with C:
| id | name | price |
|---|---|---|
| 9 | Calendula lip balm 15 ml | 4.60 |
| 10 | Concentrated eco laundry detergent 1 L | 11.20 |
| 15 | Ceremonial matcha green tea 30 g | 22.00 |
| 17 | Cold-pressed orange juice 1 L | 5.40 |
4 rows. Here LIKE is preferable to ILIKE: product names start with a capital by convention, and ILIKE 'c%' would add nothing while preventing an index from being used.
3. Food without the word Organic:
SELECT id, name, category_id, price
FROM products
WHERE category_id = 1
AND name NOT LIKE '%Organic%'
ORDER BY id;| id | name | category_id | price |
|---|---|---|---|
| 1 | Extra virgin olive oil 500 ml | 1 | 12.50 |
| 3 | Raw orange blossom honey 500 g | 1 | 9.75 |
| 4 | Spelt pasta 500 g | 1 | 2.80 |
3 rows. The consistency check: category 1 has 5 products, two of them (2 and 5) carry Organic in the name, and 5 − 2 = 3. It closes. That it closes matters precisely because name is NOT NULL; if it allowed nulls, NOT LIKE would have discarded them and the arithmetic wouldn't add up.
Solution 2
1.
| com_domain_customers |
|---|
| 11 |
11 customers, the 15 minus the 2 Portuguese and the 2 French ones.
2. A first name of exactly 3 letters: three underscores and nothing else.
| id | name | last_name | city |
|---|---|---|---|
| 5 | Ana | Belmonte Roca | Barcelona |
| 6 | Pau | Llorens Vidal | Valencia |
2 rows. With LIKE '___' (three _) you demand exactly three characters, because the pattern is anchored to the whole string. If you'd written '___%' you'd get every name of three letters or more, that is, almost the whole table.
3. '%@example.com%' returns the same thing here (11 rows), but it isn't equivalent. The trailing % means "and then anything", so it would also match [email protected] or [email protected]. It's a looser pattern that answers "the email contains @example.com", not "the email ends in @example.com".
'%@example.com' is preferable: it says exactly what we want, it drags in no false positives and —no minor detail— a pattern that ends without a wildcard still can't use an index, but at least it doesn't invite logical errors. The general rule: don't put in wildcards you don't need, each one silently widens the set of answers.
Solution 3
SELECT id,
name AS format
FROM products
WHERE name !~ '[0-9]+ ?(ml|kg|g|L|units|bags)$'
ORDER BY id;| id | format |
|---|---|
| 11 | Loofah scrubber (pack of 3) |
| 12 | Reusable cotton bags (pack of 5) |
| 13 | Soy wax candles (pack of 2) |
| 18 | Bamboo toothbrush |
4 rows. A breakdown of the pattern:
| Fragment | Meaning |
|---|---|
[0-9]+ |
One or more digits |
? |
An optional space |
(ml|kg|g|L|units|bags) |
Any of the six units |
$ |
Anchored to the end of the string |
!~ |
Returns the rows that do not match |
Could you do it with LIKE? Only halfway. You could write:
-- An approximation with LIKE: incomplete
WHERE name NOT LIKE '%ml'
AND name NOT LIKE '% g'
AND name NOT LIKE '% kg'
AND name NOT LIKE '% L'
AND name NOT LIKE '% units'
AND name NOT LIKE '% bags'and over this data it would give the same 4 rows. But you'd be missing three things LIKE can't express:
- Demanding that a number sits in front of the unit. A product called "Hand soap ml" —a perfectly possible typo— would be classified as "has a format", because
LIKE '%ml'only looks at the last two letters. The regular expression demands[0-9]+in front and would catch it. - Tolerating case variations. "Shower gel 500 mL" doesn't match
'%ml'in PostgreSQL; with regex it's enough to swap~for~*. - Maintaining it. Six conditions chained with
ANDbecome eight the moment the catalogue addsclandoz; the regular expression only adds two alternatives inside the parentheses.
That's the criterion for choosing: LIKE while the condition is a single fixed shape; regex as soon as alternatives, repetitions or character classes show up.
Conclusion
You now know how to search by pattern and, above all, when you shouldn't:
LIKEcompares against a pattern, not against a value, and returns a boolean you can combine withAND,ORandNOT.NOT LIKEis its negation, with the same blindness toNULLas<>.- The two wildcards are
%(zero or more characters) and_(exactly one). A pattern with no wildcards is equivalent to=, and the pattern is always anchored to the whole string. - In PostgreSQL
LIKEis case-sensitive. The three ways out areILIKE(convenient but non-standard),LOWER(col) LIKE LOWER(pat)(portable) and a non-deterministic collation (a schema decision). In MySQL the default behaviour is the opposite, and in SQLite it only applies to ASCII. - To search for a literal
%or_you have to escape them:\%by default in PostgreSQL, or the character you declare withESCAPE, which is the portable form. - When the condition involves alternatives, repetitions or character classes,
LIKEfalls short: in comeSIMILAR TO(standard, little used) and above all PostgreSQL's regular expressions~,~*,!~,!~*, with which you've validated the 15 emails and spotted the 4 products with no size stated. - Performance depends on the pattern's first character:
'text%'can use a B-tree index;'%text%'forces the whole table to be read. For that case there'spg_trgm(module 8), and when what you need is a real search engine, full-text search.
In the next lesson, the IN and BETWEEN operators, you'll go on sharpening the filtering but along a different road: instead of text patterns, lists of values and ranges. You'll see how IN replaces an endless chain of ORs, how BETWEEN compacts a range always including both endpoints, and you'll run into one of the most expensive mistakes in all of SQL: NOT IN with a list containing a NULL doesn't return "the rest", it returns exactly zero rows.
SQL Course
Module 1: Introduction to SQL
- What is SQL?
- Setting up your SQL environment
- Basic SQL syntax
- Understanding databases and tables
- The relational model: primary and foreign keys
- The course database: GreenStore
Module 2: Basic SQL queries
- The SELECT statement
- Aliases, expressions and calculated columns
- Filtering data with WHERE
- DISTINCT and removing duplicates
- Sorting data with ORDER BY
- Limiting results with LIMIT
Module 3: Working with multiple tables
- JOIN operations
- INNER JOIN
- LEFT JOIN
- RIGHT JOIN
- FULL OUTER JOIN
- SELF JOIN and CROSS JOIN
- Set operations: UNION, INTERSECT and EXCEPT
Module 4: Advanced data filtering
- Using LIKE for pattern matching
- The IN and BETWEEN operators
- NULL values and IS NULL
- Aggregate functions: COUNT, SUM, AVG, MIN and MAX
- Aggregating data with GROUP BY
- The HAVING clause
Module 5: Data manipulation
- Creating tables and constraints with CREATE TABLE
- The INSERT statement
- The UPDATE statement
- The DELETE statement
- The UPSERT (MERGE) statement
- Changing the schema: ALTER TABLE and safe migrations
Module 6: Advanced SQL functions
- String functions
- Numeric functions
- Date and time functions
- Type conversion and handling NULL: CAST and COALESCE
- Conditional expressions
Module 7: Subqueries and nested queries
- Introduction to subqueries
- Correlated subqueries
- EXISTS and NOT EXISTS
- Using subqueries in SELECT, FROM and WHERE
- Subquery or JOIN: which one to choose
Module 8: Indexes and performance tuning
- Understanding indexes
- Creating and managing indexes
- Index types and when not to index
- Query optimization techniques
- Analyzing query performance
Module 9: Transactions and concurrency
- Introduction to transactions
- ACID properties
- Transaction control statements
- Isolation levels and concurrency anomalies
- Handling concurrency: locks and deadlocks
Module 10: Advanced topics
- Views
- Common table expressions (CTEs)
- Window functions
- Stored procedures
- Triggers
- JSON and semi-structured data
Module 11: SQL in practice
- Real-world use cases
- Best practices
- Security: SQL injection, permissions and roles
- SQL for data analysis
- SQL in web development
