Find customers who have placed at least one order
A support engineer needs a list of paying customers. In Chinook, a completed purchase is represented by an invoice. The question sounds simple: “Which customers have at least one invoice?” But should the report contain one row per customer, or one row per invoice? That choice—the result grain—matters more than choosing a favourite SQL keyword.
A JOIN combines rows from two table expressions using a relationship. A subquery is a query nested inside another SQL statement. They are not opposing categories: a JOIN can join a subquery. Use a join when you need related columns, and consider EXISTS when you only need to know whether a related row exists.
The examples show why three queries identify the same customers while another returns 412 invoice matches for 59 customers. They also show why “joins are always faster” is not a useful rule. SQL semantics determine correctness; the engine’s plan and measurements determine performance.
Prerequisite: basic SELECT and WHERE; GROUP BY foundations helps with summaries. Related: Aggregation · Filtering · Windows. Practice: this lesson’s exercises. Follow the SQL Learning Hub path.
The Chinook sample database
These examples use the complete Chinook music-store database available in the DataTug demo: 59 customers and 412 invoices, with every original table and value retained. All 59 customers have invoices. Queries that focus on one customer or qualifying invoices show that condition explicitly in the SQL.
The relationship is Customer.CustomerId → Invoice.CustomerId: one customer can have many invoices; each invoice belongs to one customer. InvoiceId is the invoice’s unique key. InvoiceDate is ISO-sortable text; Total is a numeric amount in the dataset’s units. This source uses dates from 2021–2025; other Chinook releases differ. Invoice IDs are not necessarily chronological.
Results are shown as a preview for clarity. Tables with more than 10 records initially show the first 10. Choose Show all to reveal every matching record, or Run to execute the exact displayed SQL in the DataTug editor against the same Chinook database. No query is saved to the project. The preview does not add a WHERE or LIMIT clause. Synthetic examples define their own rows with VALUES in the displayed query.
Download the complete Chinook SQLite database and open it with SQLite 3.25.0 or later, then run any query below. No account or network database is required. Query downloads contain every displayed example and exercise solution. Frozen result tables are checked against this database; each code header states its minimum SQLite version.
Chinook upstream and schema · Chinook licence · Source attribution. SQLite uses binary floating-point for many decimal operations; examples round money for display. Production finance needs an explicit exact-decimal or integer-minor-unit policy.
Customer source rows
Who are the 59 customers?
SELECT CustomerId, FirstName, LastName, Country
FROM Customer ORDER BY CustomerId;Show all 59 recordsShow first 10 records
| CustomerId | FirstName | LastName | Country |
|---|---|---|---|
| 1 | Luís | Gonçalves | Brazil |
| 2 | Leonie | Köhler | Germany |
| 3 | François | Tremblay | Canada |
| 4 | Bjørn | Hansen | Norway |
| 5 | František | Wichterlová | Czech Republic |
| 6 | Helena | Holý | Czech Republic |
| 7 | Astrid | Gruber | Austria |
| 8 | Daan | Peeters | Belgium |
| 9 | Kara | Nielsen | Denmark |
| 10 | Eduardo | Martins | Brazil |
| 11 | Alexandre | Rocha | Brazil |
| 12 | Roberto | Almeida | Brazil |
| 13 | Fernanda | Ramos | Brazil |
| 14 | Mark | Philips | Canada |
| 15 | Jennifer | Peterson | Canada |
| 16 | Frank | Harris | USA |
| 17 | Jack | Smith | USA |
| 18 | Michelle | Brooks | USA |
| 19 | Tim | Goyer | USA |
| 20 | Dan | Miller | USA |
| 21 | Kathy | Chase | USA |
| 22 | Heather | Leacock | USA |
| 23 | John | Gordon | USA |
| 24 | Frank | Ralston | USA |
| 25 | Victor | Stevens | USA |
| 26 | Richard | Cunningham | USA |
| 27 | Patrick | Gray | USA |
| 28 | Julia | Barnett | USA |
| 29 | Robert | Brown | Canada |
| 30 | Edward | Francis | Canada |
| 31 | Martha | Silk | Canada |
| 32 | Aaron | Mitchell | Canada |
| 33 | Ellie | Sullivan | Canada |
| 34 | João | Fernandes | Portugal |
| 35 | Madalena | Sampaio | Portugal |
| 36 | Hannah | Schneider | Germany |
| 37 | Fynn | Zimmermann | Germany |
| 38 | Niklas | Schröder | Germany |
| 39 | Camille | Bernard | France |
| 40 | Dominique | Lefebvre | France |
| 41 | Marc | Dubois | France |
| 42 | Wyatt | Girard | France |
| 43 | Isabelle | Mercier | France |
| 44 | Terhi | Hämäläinen | Finland |
| 45 | Ladislav | Kovács | Hungary |
| 46 | Hugh | O'Reilly | Ireland |
| 47 | Lucas | Mancini | Italy |
| 48 | Johannes | Van der Berg | Netherlands |
| 49 | Stanisław | Wójcik | Poland |
| 50 | Enrique | Muñoz | Spain |
| 51 | Joakim | Johansson | Sweden |
| 52 | Emma | Jones | United Kingdom |
| 53 | Phil | Hughes | United Kingdom |
| 54 | Steve | Murray | United Kingdom |
| 55 | Mark | Taylor | Australia |
| 56 | Diego | Gutiérrez | Argentina |
| 57 | Luis | Rojas | Chile |
| 58 | Manoj | Pareek | India |
| 59 | Puja | Srivastava | India |
Invoice source rows
Which invoices belong to customer 1?
SELECT InvoiceId, CustomerId, InvoiceDate, Total
FROM Invoice WHERE CustomerId = 1
ORDER BY InvoiceDate, InvoiceId;| InvoiceId | CustomerId | InvoiceDate | Total |
|---|---|---|---|
| 98 | 1 | 2022-03-11 00:00:00 | 3.98 |
| 121 | 1 | 2022-06-13 00:00:00 | 3.96 |
| 143 | 1 | 2022-09-15 00:00:00 | 5.94 |
| 195 | 1 | 2023-05-06 00:00:00 | 0.99 |
| 316 | 1 | 2024-10-27 00:00:00 | 1.98 |
| 327 | 1 | 2024-12-07 00:00:00 | 13.86 |
| 382 | 1 | 2025-08-07 00:00:00 | 8.91 |
JOINs describe relationships, not deduplication
An INNER JOIN emits a row for each pair satisfying its condition. A customer with seven invoices participates in seven matching pairs. A customer with none participates in none. Selecting only the customer’s name does not change how many pairs exist.
One customer, many matching invoices
- 1Customer 1: Luís (one source row)
- 2Invoice.CustomerId = 1: seven source rows
- 3INNER JOIN: seven rows with CustomerId 1
INNER JOIN: every matching invoice
Which customers have invoices? Inspect every match.
SELECT c.CustomerId, c.FirstName, i.InvoiceId
FROM Customer AS c
INNER JOIN Invoice AS i ON i.CustomerId = c.CustomerId
ORDER BY c.CustomerId, i.InvoiceId;Show all 412 recordsShow first 10 records
| CustomerId | FirstName | InvoiceId |
|---|---|---|
| 1 | Luís | 98 |
| 1 | Luís | 121 |
| 1 | Luís | 143 |
| 1 | Luís | 195 |
| 1 | Luís | 316 |
| 1 | Luís | 327 |
| 1 | Luís | 382 |
| 2 | Leonie | 1 |
| 2 | Leonie | 12 |
| 2 | Leonie | 67 |
| 2 | Leonie | 196 |
| 2 | Leonie | 219 |
| 2 | Leonie | 241 |
| 2 | Leonie | 293 |
| 3 | François | 99 |
| 3 | François | 110 |
| 3 | François | 165 |
| 3 | François | 294 |
| 3 | François | 317 |
| 3 | François | 339 |
| 3 | François | 391 |
| 4 | Bjørn | 2 |
| 4 | Bjørn | 24 |
| 4 | Bjørn | 76 |
| 4 | Bjørn | 197 |
| 4 | Bjørn | 208 |
| 4 | Bjørn | 263 |
| 4 | Bjørn | 392 |
| 5 | František | 77 |
| 5 | František | 100 |
| 5 | František | 122 |
| 5 | František | 174 |
| 5 | František | 295 |
| 5 | František | 306 |
| 5 | František | 361 |
| 6 | Helena | 46 |
| 6 | Helena | 175 |
| 6 | Helena | 198 |
| 6 | Helena | 220 |
| 6 | Helena | 272 |
| 6 | Helena | 393 |
| 6 | Helena | 404 |
| 7 | Astrid | 78 |
| 7 | Astrid | 89 |
| 7 | Astrid | 144 |
| 7 | Astrid | 273 |
| 7 | Astrid | 296 |
| 7 | Astrid | 318 |
| 7 | Astrid | 370 |
| 8 | Daan | 3 |
| 8 | Daan | 55 |
| 8 | Daan | 176 |
| 8 | Daan | 187 |
| 8 | Daan | 242 |
| 8 | Daan | 371 |
| 8 | Daan | 394 |
| 9 | Kara | 56 |
| 9 | Kara | 79 |
| 9 | Kara | 101 |
| 9 | Kara | 153 |
| 9 | Kara | 274 |
| 9 | Kara | 285 |
| 9 | Kara | 340 |
| 10 | Eduardo | 25 |
| 10 | Eduardo | 154 |
| 10 | Eduardo | 177 |
| 10 | Eduardo | 199 |
| 10 | Eduardo | 251 |
| 10 | Eduardo | 372 |
| 10 | Eduardo | 383 |
| 11 | Alexandre | 57 |
| 11 | Alexandre | 68 |
| 11 | Alexandre | 123 |
| 11 | Alexandre | 252 |
| 11 | Alexandre | 275 |
| 11 | Alexandre | 297 |
| 11 | Alexandre | 349 |
| 12 | Roberto | 34 |
| 12 | Roberto | 155 |
| 12 | Roberto | 166 |
| 12 | Roberto | 221 |
| 12 | Roberto | 350 |
| 12 | Roberto | 373 |
| 12 | Roberto | 395 |
| 13 | Fernanda | 35 |
| 13 | Fernanda | 58 |
| 13 | Fernanda | 80 |
| 13 | Fernanda | 132 |
| 13 | Fernanda | 253 |
| 13 | Fernanda | 264 |
| 13 | Fernanda | 319 |
| 14 | Mark | 4 |
| 14 | Mark | 133 |
| 14 | Mark | 156 |
| 14 | Mark | 178 |
| 14 | Mark | 230 |
| 14 | Mark | 351 |
| 14 | Mark | 362 |
| 15 | Jennifer | 36 |
| 15 | Jennifer | 47 |
| 15 | Jennifer | 102 |
| 15 | Jennifer | 231 |
| 15 | Jennifer | 254 |
| 15 | Jennifer | 276 |
| 15 | Jennifer | 328 |
| 16 | Frank | 13 |
| 16 | Frank | 134 |
| 16 | Frank | 145 |
| 16 | Frank | 200 |
| 16 | Frank | 329 |
| 16 | Frank | 352 |
| 16 | Frank | 374 |
| 17 | Jack | 14 |
| 17 | Jack | 37 |
| 17 | Jack | 59 |
| 17 | Jack | 111 |
| 17 | Jack | 232 |
| 17 | Jack | 243 |
| 17 | Jack | 298 |
| 18 | Michelle | 112 |
| 18 | Michelle | 135 |
| 18 | Michelle | 157 |
| 18 | Michelle | 209 |
| 18 | Michelle | 330 |
| 18 | Michelle | 341 |
| 18 | Michelle | 396 |
| 19 | Tim | 15 |
| 19 | Tim | 26 |
| 19 | Tim | 81 |
| 19 | Tim | 210 |
| 19 | Tim | 233 |
| 19 | Tim | 255 |
| 19 | Tim | 307 |
| 20 | Dan | 113 |
| 20 | Dan | 124 |
| 20 | Dan | 179 |
| 20 | Dan | 308 |
| 20 | Dan | 331 |
| 20 | Dan | 353 |
| 20 | Dan | 405 |
| 21 | Kathy | 16 |
| 21 | Kathy | 38 |
| 21 | Kathy | 90 |
| 21 | Kathy | 211 |
| 21 | Kathy | 222 |
| 21 | Kathy | 277 |
| 21 | Kathy | 406 |
| 22 | Heather | 91 |
| 22 | Heather | 114 |
| 22 | Heather | 136 |
| 22 | Heather | 188 |
| 22 | Heather | 309 |
| 22 | Heather | 320 |
| 22 | Heather | 375 |
| 23 | John | 5 |
| 23 | John | 60 |
| 23 | John | 189 |
| 23 | John | 212 |
| 23 | John | 234 |
| 23 | John | 286 |
| 23 | John | 407 |
| 24 | Frank | 92 |
| 24 | Frank | 103 |
| 24 | Frank | 158 |
| 24 | Frank | 287 |
| 24 | Frank | 310 |
| 24 | Frank | 332 |
| 24 | Frank | 384 |
| 25 | Victor | 17 |
| 25 | Victor | 69 |
| 25 | Victor | 190 |
| 25 | Victor | 201 |
| 25 | Victor | 256 |
| 25 | Victor | 385 |
| 25 | Victor | 408 |
| 26 | Richard | 70 |
| 26 | Richard | 93 |
| 26 | Richard | 115 |
| 26 | Richard | 167 |
| 26 | Richard | 288 |
| 26 | Richard | 299 |
| 26 | Richard | 354 |
| 27 | Patrick | 39 |
| 27 | Patrick | 168 |
| 27 | Patrick | 191 |
| 27 | Patrick | 213 |
| 27 | Patrick | 265 |
| 27 | Patrick | 386 |
| 27 | Patrick | 397 |
| 28 | Julia | 71 |
| 28 | Julia | 82 |
| 28 | Julia | 137 |
| 28 | Julia | 266 |
| 28 | Julia | 289 |
| 28 | Julia | 311 |
| 28 | Julia | 363 |
| 29 | Robert | 48 |
| 29 | Robert | 169 |
| 29 | Robert | 180 |
| 29 | Robert | 235 |
| 29 | Robert | 364 |
| 29 | Robert | 387 |
| 29 | Robert | 409 |
| 30 | Edward | 49 |
| 30 | Edward | 72 |
| 30 | Edward | 94 |
| 30 | Edward | 146 |
| 30 | Edward | 267 |
| 30 | Edward | 278 |
| 30 | Edward | 333 |
| 31 | Martha | 18 |
| 31 | Martha | 147 |
| 31 | Martha | 170 |
| 31 | Martha | 192 |
| 31 | Martha | 244 |
| 31 | Martha | 365 |
| 31 | Martha | 376 |
| 32 | Aaron | 50 |
| 32 | Aaron | 61 |
| 32 | Aaron | 116 |
| 32 | Aaron | 245 |
| 32 | Aaron | 268 |
| 32 | Aaron | 290 |
| 32 | Aaron | 342 |
| 33 | Ellie | 27 |
| 33 | Ellie | 148 |
| 33 | Ellie | 159 |
| 33 | Ellie | 214 |
| 33 | Ellie | 343 |
| 33 | Ellie | 366 |
| 33 | Ellie | 388 |
| 34 | João | 28 |
| 34 | João | 51 |
| 34 | João | 73 |
| 34 | João | 125 |
| 34 | João | 246 |
| 34 | João | 257 |
| 34 | João | 312 |
| 35 | Madalena | 126 |
| 35 | Madalena | 149 |
| 35 | Madalena | 171 |
| 35 | Madalena | 223 |
| 35 | Madalena | 344 |
| 35 | Madalena | 355 |
| 35 | Madalena | 410 |
| 36 | Hannah | 29 |
| 36 | Hannah | 40 |
| 36 | Hannah | 95 |
| 36 | Hannah | 224 |
| 36 | Hannah | 247 |
| 36 | Hannah | 269 |
| 36 | Hannah | 321 |
| 37 | Fynn | 6 |
| 37 | Fynn | 127 |
| 37 | Fynn | 138 |
| 37 | Fynn | 193 |
| 37 | Fynn | 322 |
| 37 | Fynn | 345 |
| 37 | Fynn | 367 |
| 38 | Niklas | 7 |
| 38 | Niklas | 30 |
| 38 | Niklas | 52 |
| 38 | Niklas | 104 |
| 38 | Niklas | 225 |
| 38 | Niklas | 236 |
| 38 | Niklas | 291 |
| 39 | Camille | 105 |
| 39 | Camille | 128 |
| 39 | Camille | 150 |
| 39 | Camille | 202 |
| 39 | Camille | 323 |
| 39 | Camille | 334 |
| 39 | Camille | 389 |
| 40 | Dominique | 8 |
| 40 | Dominique | 19 |
| 40 | Dominique | 74 |
| 40 | Dominique | 203 |
| 40 | Dominique | 226 |
| 40 | Dominique | 248 |
| 40 | Dominique | 300 |
| 41 | Marc | 106 |
| 41 | Marc | 117 |
| 41 | Marc | 172 |
| 41 | Marc | 301 |
| 41 | Marc | 324 |
| 41 | Marc | 346 |
| 41 | Marc | 398 |
| 42 | Wyatt | 9 |
| 42 | Wyatt | 31 |
| 42 | Wyatt | 83 |
| 42 | Wyatt | 204 |
| 42 | Wyatt | 215 |
| 42 | Wyatt | 270 |
| 42 | Wyatt | 399 |
| 43 | Isabelle | 84 |
| 43 | Isabelle | 107 |
| 43 | Isabelle | 129 |
| 43 | Isabelle | 181 |
| 43 | Isabelle | 302 |
| 43 | Isabelle | 313 |
| 43 | Isabelle | 368 |
| 44 | Terhi | 53 |
| 44 | Terhi | 182 |
| 44 | Terhi | 205 |
| 44 | Terhi | 227 |
| 44 | Terhi | 279 |
| 44 | Terhi | 400 |
| 44 | Terhi | 411 |
| 45 | Ladislav | 85 |
| 45 | Ladislav | 96 |
| 45 | Ladislav | 151 |
| 45 | Ladislav | 280 |
| 45 | Ladislav | 303 |
| 45 | Ladislav | 325 |
| 45 | Ladislav | 377 |
| 46 | Hugh | 10 |
| 46 | Hugh | 62 |
| 46 | Hugh | 183 |
| 46 | Hugh | 194 |
| 46 | Hugh | 249 |
| 46 | Hugh | 378 |
| 46 | Hugh | 401 |
| 47 | Lucas | 63 |
| 47 | Lucas | 86 |
| 47 | Lucas | 108 |
| 47 | Lucas | 160 |
| 47 | Lucas | 281 |
| 47 | Lucas | 292 |
| 47 | Lucas | 347 |
| 48 | Johannes | 32 |
| 48 | Johannes | 161 |
| 48 | Johannes | 184 |
| 48 | Johannes | 206 |
| 48 | Johannes | 258 |
| 48 | Johannes | 379 |
| 48 | Johannes | 390 |
| 49 | Stanisław | 64 |
| 49 | Stanisław | 75 |
| 49 | Stanisław | 130 |
| 49 | Stanisław | 259 |
| 49 | Stanisław | 282 |
| 49 | Stanisław | 304 |
| 49 | Stanisław | 356 |
| 50 | Enrique | 41 |
| 50 | Enrique | 162 |
| 50 | Enrique | 173 |
| 50 | Enrique | 228 |
| 50 | Enrique | 357 |
| 50 | Enrique | 380 |
| 50 | Enrique | 402 |
| 51 | Joakim | 42 |
| 51 | Joakim | 65 |
| 51 | Joakim | 87 |
| 51 | Joakim | 139 |
| 51 | Joakim | 260 |
| 51 | Joakim | 271 |
| 51 | Joakim | 326 |
| 52 | Emma | 11 |
| 52 | Emma | 140 |
| 52 | Emma | 163 |
| 52 | Emma | 185 |
| 52 | Emma | 237 |
| 52 | Emma | 358 |
| 52 | Emma | 369 |
| 53 | Phil | 43 |
| 53 | Phil | 54 |
| 53 | Phil | 109 |
| 53 | Phil | 238 |
| 53 | Phil | 261 |
| 53 | Phil | 283 |
| 53 | Phil | 335 |
| 54 | Steve | 20 |
| 54 | Steve | 141 |
| 54 | Steve | 152 |
| 54 | Steve | 207 |
| 54 | Steve | 336 |
| 54 | Steve | 359 |
| 54 | Steve | 381 |
| 55 | Mark | 21 |
| 55 | Mark | 44 |
| 55 | Mark | 66 |
| 55 | Mark | 118 |
| 55 | Mark | 239 |
| 55 | Mark | 250 |
| 55 | Mark | 305 |
| 56 | Diego | 119 |
| 56 | Diego | 142 |
| 56 | Diego | 164 |
| 56 | Diego | 216 |
| 56 | Diego | 337 |
| 56 | Diego | 348 |
| 56 | Diego | 403 |
| 57 | Luis | 22 |
| 57 | Luis | 33 |
| 57 | Luis | 88 |
| 57 | Luis | 217 |
| 57 | Luis | 240 |
| 57 | Luis | 262 |
| 57 | Luis | 314 |
| 58 | Manoj | 120 |
| 58 | Manoj | 131 |
| 58 | Manoj | 186 |
| 58 | Manoj | 315 |
| 58 | Manoj | 338 |
| 58 | Manoj | 360 |
| 58 | Manoj | 412 |
| 59 | Puja | 23 |
| 59 | Puja | 45 |
| 59 | Puja | 97 |
| 59 | Puja | 218 |
| 59 | Puja | 229 |
| 59 | Puja | 284 |
412 invoice matches across 59 customers. Most customers have seven invoices; one has six. Repeated customer identifiers are the correct one-to-many result, not duplicate invoices.
A LEFT JOIN also keeps a left-side row when no right-side match exists. Right-side columns become NULL. It does not promise one row per left record: multiple matches still multiply rows. Use it when unmatched records belong in the report.
LEFT JOIN: preserve unmatched customers
Who has an invoice over 20 units, while keeping every customer visible?
SELECT c.CustomerId, c.FirstName, i.InvoiceId, i.Total
FROM Customer AS c
LEFT JOIN Invoice AS i
ON i.CustomerId = c.CustomerId AND i.Total > 20
ORDER BY c.CustomerId, i.InvoiceId;Show all 59 recordsShow first 10 records
| CustomerId | FirstName | InvoiceId | Total |
|---|---|---|---|
| 1 | Luís | NULL | NULL |
| 2 | Leonie | NULL | NULL |
| 3 | François | NULL | NULL |
| 4 | Bjørn | NULL | NULL |
| 5 | František | NULL | NULL |
| 6 | Helena | 404 | 25.86 |
| 7 | Astrid | NULL | NULL |
| 8 | Daan | NULL | NULL |
| 9 | Kara | NULL | NULL |
| 10 | Eduardo | NULL | NULL |
| 11 | Alexandre | NULL | NULL |
| 12 | Roberto | NULL | NULL |
| 13 | Fernanda | NULL | NULL |
| 14 | Mark | NULL | NULL |
| 15 | Jennifer | NULL | NULL |
| 16 | Frank | NULL | NULL |
| 17 | Jack | NULL | NULL |
| 18 | Michelle | NULL | NULL |
| 19 | Tim | NULL | NULL |
| 20 | Dan | NULL | NULL |
| 21 | Kathy | NULL | NULL |
| 22 | Heather | NULL | NULL |
| 23 | John | NULL | NULL |
| 24 | Frank | NULL | NULL |
| 25 | Victor | NULL | NULL |
| 26 | Richard | 299 | 23.86 |
| 27 | Patrick | NULL | NULL |
| 28 | Julia | NULL | NULL |
| 29 | Robert | NULL | NULL |
| 30 | Edward | NULL | NULL |
| 31 | Martha | NULL | NULL |
| 32 | Aaron | NULL | NULL |
| 33 | Ellie | NULL | NULL |
| 34 | João | NULL | NULL |
| 35 | Madalena | NULL | NULL |
| 36 | Hannah | NULL | NULL |
| 37 | Fynn | NULL | NULL |
| 38 | Niklas | NULL | NULL |
| 39 | Camille | NULL | NULL |
| 40 | Dominique | NULL | NULL |
| 41 | Marc | NULL | NULL |
| 42 | Wyatt | NULL | NULL |
| 43 | Isabelle | NULL | NULL |
| 44 | Terhi | NULL | NULL |
| 45 | Ladislav | 96 | 21.86 |
| 46 | Hugh | 194 | 21.86 |
| 47 | Lucas | NULL | NULL |
| 48 | Johannes | NULL | NULL |
| 49 | Stanisław | NULL | NULL |
| 50 | Enrique | NULL | NULL |
| 51 | Joakim | NULL | NULL |
| 52 | Emma | NULL | NULL |
| 53 | Phil | NULL | NULL |
| 54 | Steve | NULL | NULL |
| 55 | Mark | NULL | NULL |
| 56 | Diego | NULL | NULL |
| 57 | Luis | NULL | NULL |
| 58 | Manoj | NULL | NULL |
| 59 | Puja | NULL | NULL |
Only customers 6, 26, 45 and 46 have an invoice over 20. The other 55 customers receive a null-extended row. Keeping the amount condition in ON preserves them; moving it to WHERE removes them.
Conditions in ON decide which invoice can match. A condition on i.Total in the outer WHERE runs after null extension; unknown comparisons are discarded. Moving that condition can remove customers without qualifying invoices. That is a correctness change, not a formatting preference.
A subquery can return a value, a set, or a table
A scalar subquery supplies one value, usable in a selected expression or comparison. A non-correlated subquery does not refer to outer-query columns. A correlated subquery does: its meaning depends on the current outer row. These describe dependencies, not a guarantee that the engine physically reruns a query for every row.
Correlated EXISTS: a per-customer question
- 1Read the outer customer identifier
- 2Ask whether any invoice has that CustomerId
- 3Keep the customer once if yes
EXISTS: test for a match
Which customers have at least one invoice?
SELECT c.CustomerId, c.FirstName
FROM Customer AS c
WHERE EXISTS (
SELECT 1 FROM Invoice AS i
WHERE i.CustomerId = c.CustomerId
)
ORDER BY c.CustomerId;Show all 59 recordsShow first 10 records
| CustomerId | FirstName |
|---|---|
| 1 | Luís |
| 2 | Leonie |
| 3 | François |
| 4 | Bjørn |
| 5 | František |
| 6 | Helena |
| 7 | Astrid |
| 8 | Daan |
| 9 | Kara |
| 10 | Eduardo |
| 11 | Alexandre |
| 12 | Roberto |
| 13 | Fernanda |
| 14 | Mark |
| 15 | Jennifer |
| 16 | Frank |
| 17 | Jack |
| 18 | Michelle |
| 19 | Tim |
| 20 | Dan |
| 21 | Kathy |
| 22 | Heather |
| 23 | John |
| 24 | Frank |
| 25 | Victor |
| 26 | Richard |
| 27 | Patrick |
| 28 | Julia |
| 29 | Robert |
| 30 | Edward |
| 31 | Martha |
| 32 | Aaron |
| 33 | Ellie |
| 34 | João |
| 35 | Madalena |
| 36 | Hannah |
| 37 | Fynn |
| 38 | Niklas |
| 39 | Camille |
| 40 | Dominique |
| 41 | Marc |
| 42 | Wyatt |
| 43 | Isabelle |
| 44 | Terhi |
| 45 | Ladislav |
| 46 | Hugh |
| 47 | Lucas |
| 48 | Johannes |
| 49 | Stanisław |
| 50 | Enrique |
| 51 | Joakim |
| 52 | Emma |
| 53 | Phil |
| 54 | Steve |
| 55 | Mark |
| 56 | Diego |
| 57 | Luis |
| 58 | Manoj |
| 59 | Puja |
The correlation links invoices to the current outer customer. The value 1 is irrelevant: EXISTS tests the presence of a row.
EXISTS ignores selected values: even a selected NULL establishes existence if a row exists. The correlation predicate does the essential work. IN tests whether an expression belongs to a single-column subquery result. Both are useful in WHERE; choose the one making the relationship clearest.
IN: membership
Which customer IDs occur in Invoice?
SELECT CustomerId, FirstName
FROM Customer
WHERE CustomerId IN (SELECT CustomerId FROM Invoice)
ORDER BY CustomerId;Show all 59 recordsShow first 10 records
| CustomerId | FirstName |
|---|---|
| 1 | Luís |
| 2 | Leonie |
| 3 | François |
| 4 | Bjørn |
| 5 | František |
| 6 | Helena |
| 7 | Astrid |
| 8 | Daan |
| 9 | Kara |
| 10 | Eduardo |
| 11 | Alexandre |
| 12 | Roberto |
| 13 | Fernanda |
| 14 | Mark |
| 15 | Jennifer |
| 16 | Frank |
| 17 | Jack |
| 18 | Michelle |
| 19 | Tim |
| 20 | Dan |
| 21 | Kathy |
| 22 | Heather |
| 23 | John |
| 24 | Frank |
| 25 | Victor |
| 26 | Richard |
| 27 | Patrick |
| 28 | Julia |
| 29 | Robert |
| 30 | Edward |
| 31 | Martha |
| 32 | Aaron |
| 33 | Ellie |
| 34 | João |
| 35 | Madalena |
| 36 | Hannah |
| 37 | Fynn |
| 38 | Niklas |
| 39 | Camille |
| 40 | Dominique |
| 41 | Marc |
| 42 | Wyatt |
| 43 | Isabelle |
| 44 | Terhi |
| 45 | Ladislav |
| 46 | Hugh |
| 47 | Lucas |
| 48 | Johannes |
| 49 | Stanisław |
| 50 | Enrique |
| 51 | Joakim |
| 52 | Emma |
| 53 | Phil |
| 54 | Steve |
| 55 | Mark |
| 56 | Diego |
| 57 | Luis |
| 58 | Manoj |
| 59 | Puja |
Repeated IDs inside IN do not multiply outer rows. This positive membership test selects the same customers here; both keys are non-null.
For “show each customer and their spending”, a scalar aggregate expresses one summary per customer. The aggregate guarantees one result row. Beware scalar queries simply selecting an invoice amount: a customer can have many invoices, and engines differ in handling multi-row scalar results.
Scalar subquery
Show each customer’s total spending.
SELECT c.CustomerId, c.FirstName,
COALESCE((SELECT ROUND(SUM(i.Total), 2)
FROM Invoice AS i WHERE i.CustomerId = c.CustomerId), 0) AS Spending
FROM Customer AS c
ORDER BY c.CustomerId;Show all 59 recordsShow first 10 records
| CustomerId | FirstName | Spending |
|---|---|---|
| 1 | Luís | 39.62 |
| 2 | Leonie | 37.62 |
| 3 | François | 39.62 |
| 4 | Bjørn | 39.62 |
| 5 | František | 40.62 |
| 6 | Helena | 49.62 |
| 7 | Astrid | 42.62 |
| 8 | Daan | 37.62 |
| 9 | Kara | 37.62 |
| 10 | Eduardo | 37.62 |
| 11 | Alexandre | 37.62 |
| 12 | Roberto | 37.62 |
| 13 | Fernanda | 37.62 |
| 14 | Mark | 37.62 |
| 15 | Jennifer | 38.62 |
| 16 | Frank | 37.62 |
| 17 | Jack | 39.62 |
| 18 | Michelle | 37.62 |
| 19 | Tim | 38.62 |
| 20 | Dan | 39.62 |
| 21 | Kathy | 37.62 |
| 22 | Heather | 39.62 |
| 23 | John | 37.62 |
| 24 | Frank | 43.62 |
| 25 | Victor | 42.62 |
| 26 | Richard | 47.62 |
| 27 | Patrick | 37.62 |
| 28 | Julia | 43.62 |
| 29 | Robert | 37.62 |
| 30 | Edward | 37.62 |
| 31 | Martha | 37.62 |
| 32 | Aaron | 37.62 |
| 33 | Ellie | 37.62 |
| 34 | João | 39.62 |
| 35 | Madalena | 37.62 |
| 36 | Hannah | 37.62 |
| 37 | Fynn | 43.62 |
| 38 | Niklas | 37.62 |
| 39 | Camille | 38.62 |
| 40 | Dominique | 38.62 |
| 41 | Marc | 37.62 |
| 42 | Wyatt | 39.62 |
| 43 | Isabelle | 40.62 |
| 44 | Terhi | 41.62 |
| 45 | Ladislav | 45.62 |
| 46 | Hugh | 45.62 |
| 47 | Lucas | 37.62 |
| 48 | Johannes | 40.62 |
| 49 | Stanisław | 37.62 |
| 50 | Enrique | 37.62 |
| 51 | Joakim | 38.62 |
| 52 | Emma | 37.62 |
| 53 | Phil | 37.62 |
| 54 | Steve | 37.62 |
| 55 | Mark | 37.62 |
| 56 | Diego | 37.62 |
| 57 | Luis | 46.62 |
| 58 | Manoj | 38.62 |
| 59 | Puja | 36.64 |
All 59 customers have invoices here, so every spending total is positive. SUM without GROUP BY always returns one row; for no matches its value is NULL, converted to zero by COALESCE. SQLite takes the first row of a multi-row scalar subquery; many engines reject it. Use a query guaranteed to produce at most one value.
A subquery in FROM, a derived table, can establish the grain before a join. The inner grouping creates one row per customer, so the outer join preserves that summary grain. This is often clearer when you need several aggregates from the same input.
Subquery in FROM
Summarize invoices before joining customers.
SELECT c.CustomerId, c.FirstName, COALESCE(s.Spending, 0) AS Spending
FROM Customer AS c
LEFT JOIN (
SELECT CustomerId, ROUND(SUM(Total), 2) AS Spending
FROM Invoice GROUP BY CustomerId
) AS s ON s.CustomerId = c.CustomerId
ORDER BY c.CustomerId;Show all 59 recordsShow first 10 records
| CustomerId | FirstName | Spending |
|---|---|---|
| 1 | Luís | 39.62 |
| 2 | Leonie | 37.62 |
| 3 | François | 39.62 |
| 4 | Bjørn | 39.62 |
| 5 | František | 40.62 |
| 6 | Helena | 49.62 |
| 7 | Astrid | 42.62 |
| 8 | Daan | 37.62 |
| 9 | Kara | 37.62 |
| 10 | Eduardo | 37.62 |
| 11 | Alexandre | 37.62 |
| 12 | Roberto | 37.62 |
| 13 | Fernanda | 37.62 |
| 14 | Mark | 37.62 |
| 15 | Jennifer | 38.62 |
| 16 | Frank | 37.62 |
| 17 | Jack | 39.62 |
| 18 | Michelle | 37.62 |
| 19 | Tim | 38.62 |
| 20 | Dan | 39.62 |
| 21 | Kathy | 37.62 |
| 22 | Heather | 39.62 |
| 23 | John | 37.62 |
| 24 | Frank | 43.62 |
| 25 | Victor | 42.62 |
| 26 | Richard | 47.62 |
| 27 | Patrick | 37.62 |
| 28 | Julia | 43.62 |
| 29 | Robert | 37.62 |
| 30 | Edward | 37.62 |
| 31 | Martha | 37.62 |
| 32 | Aaron | 37.62 |
| 33 | Ellie | 37.62 |
| 34 | João | 39.62 |
| 35 | Madalena | 37.62 |
| 36 | Hannah | 37.62 |
| 37 | Fynn | 43.62 |
| 38 | Niklas | 37.62 |
| 39 | Camille | 38.62 |
| 40 | Dominique | 38.62 |
| 41 | Marc | 37.62 |
| 42 | Wyatt | 39.62 |
| 43 | Isabelle | 40.62 |
| 44 | Terhi | 41.62 |
| 45 | Ladislav | 45.62 |
| 46 | Hugh | 45.62 |
| 47 | Lucas | 37.62 |
| 48 | Johannes | 40.62 |
| 49 | Stanisław | 37.62 |
| 50 | Enrique | 37.62 |
| 51 | Joakim | 38.62 |
| 52 | Emma | 37.62 |
| 53 | Phil | 37.62 |
| 54 | Steve | 37.62 |
| 55 | Mark | 37.62 |
| 56 | Diego | 37.62 |
| 57 | Luis | 46.62 |
| 58 | Manoj | 38.62 |
| 59 | Puja | 36.64 |
One summary row per CustomerId makes this a many-to-one join. The derived table is non-correlated: it never refers to c.
Compare equivalent lists and different row counts
The customer-only DISTINCT JOIN, EXISTS and positive IN return all 59 customer IDs. Their equivalence depends on selected columns and the question. The raw join includes InvoiceId and has 412 rows. Switch approaches and highlight a customer to follow multiplication. Without JavaScript, all alternatives remain visible.
Same population, different result grain
INNER JOIN: every matching invoice
Which customers have invoices? Inspect every match.
SELECT c.CustomerId, c.FirstName, i.InvoiceId
FROM Customer AS c
INNER JOIN Invoice AS i ON i.CustomerId = c.CustomerId
ORDER BY c.CustomerId, i.InvoiceId;Show all 412 recordsShow first 10 records
| CustomerId | FirstName | InvoiceId |
|---|---|---|
| 1 | Luís | 98 |
| 1 | Luís | 121 |
| 1 | Luís | 143 |
| 1 | Luís | 195 |
| 1 | Luís | 316 |
| 1 | Luís | 327 |
| 1 | Luís | 382 |
| 2 | Leonie | 1 |
| 2 | Leonie | 12 |
| 2 | Leonie | 67 |
| 2 | Leonie | 196 |
| 2 | Leonie | 219 |
| 2 | Leonie | 241 |
| 2 | Leonie | 293 |
| 3 | François | 99 |
| 3 | François | 110 |
| 3 | François | 165 |
| 3 | François | 294 |
| 3 | François | 317 |
| 3 | François | 339 |
| 3 | François | 391 |
| 4 | Bjørn | 2 |
| 4 | Bjørn | 24 |
| 4 | Bjørn | 76 |
| 4 | Bjørn | 197 |
| 4 | Bjørn | 208 |
| 4 | Bjørn | 263 |
| 4 | Bjørn | 392 |
| 5 | František | 77 |
| 5 | František | 100 |
| 5 | František | 122 |
| 5 | František | 174 |
| 5 | František | 295 |
| 5 | František | 306 |
| 5 | František | 361 |
| 6 | Helena | 46 |
| 6 | Helena | 175 |
| 6 | Helena | 198 |
| 6 | Helena | 220 |
| 6 | Helena | 272 |
| 6 | Helena | 393 |
| 6 | Helena | 404 |
| 7 | Astrid | 78 |
| 7 | Astrid | 89 |
| 7 | Astrid | 144 |
| 7 | Astrid | 273 |
| 7 | Astrid | 296 |
| 7 | Astrid | 318 |
| 7 | Astrid | 370 |
| 8 | Daan | 3 |
| 8 | Daan | 55 |
| 8 | Daan | 176 |
| 8 | Daan | 187 |
| 8 | Daan | 242 |
| 8 | Daan | 371 |
| 8 | Daan | 394 |
| 9 | Kara | 56 |
| 9 | Kara | 79 |
| 9 | Kara | 101 |
| 9 | Kara | 153 |
| 9 | Kara | 274 |
| 9 | Kara | 285 |
| 9 | Kara | 340 |
| 10 | Eduardo | 25 |
| 10 | Eduardo | 154 |
| 10 | Eduardo | 177 |
| 10 | Eduardo | 199 |
| 10 | Eduardo | 251 |
| 10 | Eduardo | 372 |
| 10 | Eduardo | 383 |
| 11 | Alexandre | 57 |
| 11 | Alexandre | 68 |
| 11 | Alexandre | 123 |
| 11 | Alexandre | 252 |
| 11 | Alexandre | 275 |
| 11 | Alexandre | 297 |
| 11 | Alexandre | 349 |
| 12 | Roberto | 34 |
| 12 | Roberto | 155 |
| 12 | Roberto | 166 |
| 12 | Roberto | 221 |
| 12 | Roberto | 350 |
| 12 | Roberto | 373 |
| 12 | Roberto | 395 |
| 13 | Fernanda | 35 |
| 13 | Fernanda | 58 |
| 13 | Fernanda | 80 |
| 13 | Fernanda | 132 |
| 13 | Fernanda | 253 |
| 13 | Fernanda | 264 |
| 13 | Fernanda | 319 |
| 14 | Mark | 4 |
| 14 | Mark | 133 |
| 14 | Mark | 156 |
| 14 | Mark | 178 |
| 14 | Mark | 230 |
| 14 | Mark | 351 |
| 14 | Mark | 362 |
| 15 | Jennifer | 36 |
| 15 | Jennifer | 47 |
| 15 | Jennifer | 102 |
| 15 | Jennifer | 231 |
| 15 | Jennifer | 254 |
| 15 | Jennifer | 276 |
| 15 | Jennifer | 328 |
| 16 | Frank | 13 |
| 16 | Frank | 134 |
| 16 | Frank | 145 |
| 16 | Frank | 200 |
| 16 | Frank | 329 |
| 16 | Frank | 352 |
| 16 | Frank | 374 |
| 17 | Jack | 14 |
| 17 | Jack | 37 |
| 17 | Jack | 59 |
| 17 | Jack | 111 |
| 17 | Jack | 232 |
| 17 | Jack | 243 |
| 17 | Jack | 298 |
| 18 | Michelle | 112 |
| 18 | Michelle | 135 |
| 18 | Michelle | 157 |
| 18 | Michelle | 209 |
| 18 | Michelle | 330 |
| 18 | Michelle | 341 |
| 18 | Michelle | 396 |
| 19 | Tim | 15 |
| 19 | Tim | 26 |
| 19 | Tim | 81 |
| 19 | Tim | 210 |
| 19 | Tim | 233 |
| 19 | Tim | 255 |
| 19 | Tim | 307 |
| 20 | Dan | 113 |
| 20 | Dan | 124 |
| 20 | Dan | 179 |
| 20 | Dan | 308 |
| 20 | Dan | 331 |
| 20 | Dan | 353 |
| 20 | Dan | 405 |
| 21 | Kathy | 16 |
| 21 | Kathy | 38 |
| 21 | Kathy | 90 |
| 21 | Kathy | 211 |
| 21 | Kathy | 222 |
| 21 | Kathy | 277 |
| 21 | Kathy | 406 |
| 22 | Heather | 91 |
| 22 | Heather | 114 |
| 22 | Heather | 136 |
| 22 | Heather | 188 |
| 22 | Heather | 309 |
| 22 | Heather | 320 |
| 22 | Heather | 375 |
| 23 | John | 5 |
| 23 | John | 60 |
| 23 | John | 189 |
| 23 | John | 212 |
| 23 | John | 234 |
| 23 | John | 286 |
| 23 | John | 407 |
| 24 | Frank | 92 |
| 24 | Frank | 103 |
| 24 | Frank | 158 |
| 24 | Frank | 287 |
| 24 | Frank | 310 |
| 24 | Frank | 332 |
| 24 | Frank | 384 |
| 25 | Victor | 17 |
| 25 | Victor | 69 |
| 25 | Victor | 190 |
| 25 | Victor | 201 |
| 25 | Victor | 256 |
| 25 | Victor | 385 |
| 25 | Victor | 408 |
| 26 | Richard | 70 |
| 26 | Richard | 93 |
| 26 | Richard | 115 |
| 26 | Richard | 167 |
| 26 | Richard | 288 |
| 26 | Richard | 299 |
| 26 | Richard | 354 |
| 27 | Patrick | 39 |
| 27 | Patrick | 168 |
| 27 | Patrick | 191 |
| 27 | Patrick | 213 |
| 27 | Patrick | 265 |
| 27 | Patrick | 386 |
| 27 | Patrick | 397 |
| 28 | Julia | 71 |
| 28 | Julia | 82 |
| 28 | Julia | 137 |
| 28 | Julia | 266 |
| 28 | Julia | 289 |
| 28 | Julia | 311 |
| 28 | Julia | 363 |
| 29 | Robert | 48 |
| 29 | Robert | 169 |
| 29 | Robert | 180 |
| 29 | Robert | 235 |
| 29 | Robert | 364 |
| 29 | Robert | 387 |
| 29 | Robert | 409 |
| 30 | Edward | 49 |
| 30 | Edward | 72 |
| 30 | Edward | 94 |
| 30 | Edward | 146 |
| 30 | Edward | 267 |
| 30 | Edward | 278 |
| 30 | Edward | 333 |
| 31 | Martha | 18 |
| 31 | Martha | 147 |
| 31 | Martha | 170 |
| 31 | Martha | 192 |
| 31 | Martha | 244 |
| 31 | Martha | 365 |
| 31 | Martha | 376 |
| 32 | Aaron | 50 |
| 32 | Aaron | 61 |
| 32 | Aaron | 116 |
| 32 | Aaron | 245 |
| 32 | Aaron | 268 |
| 32 | Aaron | 290 |
| 32 | Aaron | 342 |
| 33 | Ellie | 27 |
| 33 | Ellie | 148 |
| 33 | Ellie | 159 |
| 33 | Ellie | 214 |
| 33 | Ellie | 343 |
| 33 | Ellie | 366 |
| 33 | Ellie | 388 |
| 34 | João | 28 |
| 34 | João | 51 |
| 34 | João | 73 |
| 34 | João | 125 |
| 34 | João | 246 |
| 34 | João | 257 |
| 34 | João | 312 |
| 35 | Madalena | 126 |
| 35 | Madalena | 149 |
| 35 | Madalena | 171 |
| 35 | Madalena | 223 |
| 35 | Madalena | 344 |
| 35 | Madalena | 355 |
| 35 | Madalena | 410 |
| 36 | Hannah | 29 |
| 36 | Hannah | 40 |
| 36 | Hannah | 95 |
| 36 | Hannah | 224 |
| 36 | Hannah | 247 |
| 36 | Hannah | 269 |
| 36 | Hannah | 321 |
| 37 | Fynn | 6 |
| 37 | Fynn | 127 |
| 37 | Fynn | 138 |
| 37 | Fynn | 193 |
| 37 | Fynn | 322 |
| 37 | Fynn | 345 |
| 37 | Fynn | 367 |
| 38 | Niklas | 7 |
| 38 | Niklas | 30 |
| 38 | Niklas | 52 |
| 38 | Niklas | 104 |
| 38 | Niklas | 225 |
| 38 | Niklas | 236 |
| 38 | Niklas | 291 |
| 39 | Camille | 105 |
| 39 | Camille | 128 |
| 39 | Camille | 150 |
| 39 | Camille | 202 |
| 39 | Camille | 323 |
| 39 | Camille | 334 |
| 39 | Camille | 389 |
| 40 | Dominique | 8 |
| 40 | Dominique | 19 |
| 40 | Dominique | 74 |
| 40 | Dominique | 203 |
| 40 | Dominique | 226 |
| 40 | Dominique | 248 |
| 40 | Dominique | 300 |
| 41 | Marc | 106 |
| 41 | Marc | 117 |
| 41 | Marc | 172 |
| 41 | Marc | 301 |
| 41 | Marc | 324 |
| 41 | Marc | 346 |
| 41 | Marc | 398 |
| 42 | Wyatt | 9 |
| 42 | Wyatt | 31 |
| 42 | Wyatt | 83 |
| 42 | Wyatt | 204 |
| 42 | Wyatt | 215 |
| 42 | Wyatt | 270 |
| 42 | Wyatt | 399 |
| 43 | Isabelle | 84 |
| 43 | Isabelle | 107 |
| 43 | Isabelle | 129 |
| 43 | Isabelle | 181 |
| 43 | Isabelle | 302 |
| 43 | Isabelle | 313 |
| 43 | Isabelle | 368 |
| 44 | Terhi | 53 |
| 44 | Terhi | 182 |
| 44 | Terhi | 205 |
| 44 | Terhi | 227 |
| 44 | Terhi | 279 |
| 44 | Terhi | 400 |
| 44 | Terhi | 411 |
| 45 | Ladislav | 85 |
| 45 | Ladislav | 96 |
| 45 | Ladislav | 151 |
| 45 | Ladislav | 280 |
| 45 | Ladislav | 303 |
| 45 | Ladislav | 325 |
| 45 | Ladislav | 377 |
| 46 | Hugh | 10 |
| 46 | Hugh | 62 |
| 46 | Hugh | 183 |
| 46 | Hugh | 194 |
| 46 | Hugh | 249 |
| 46 | Hugh | 378 |
| 46 | Hugh | 401 |
| 47 | Lucas | 63 |
| 47 | Lucas | 86 |
| 47 | Lucas | 108 |
| 47 | Lucas | 160 |
| 47 | Lucas | 281 |
| 47 | Lucas | 292 |
| 47 | Lucas | 347 |
| 48 | Johannes | 32 |
| 48 | Johannes | 161 |
| 48 | Johannes | 184 |
| 48 | Johannes | 206 |
| 48 | Johannes | 258 |
| 48 | Johannes | 379 |
| 48 | Johannes | 390 |
| 49 | Stanisław | 64 |
| 49 | Stanisław | 75 |
| 49 | Stanisław | 130 |
| 49 | Stanisław | 259 |
| 49 | Stanisław | 282 |
| 49 | Stanisław | 304 |
| 49 | Stanisław | 356 |
| 50 | Enrique | 41 |
| 50 | Enrique | 162 |
| 50 | Enrique | 173 |
| 50 | Enrique | 228 |
| 50 | Enrique | 357 |
| 50 | Enrique | 380 |
| 50 | Enrique | 402 |
| 51 | Joakim | 42 |
| 51 | Joakim | 65 |
| 51 | Joakim | 87 |
| 51 | Joakim | 139 |
| 51 | Joakim | 260 |
| 51 | Joakim | 271 |
| 51 | Joakim | 326 |
| 52 | Emma | 11 |
| 52 | Emma | 140 |
| 52 | Emma | 163 |
| 52 | Emma | 185 |
| 52 | Emma | 237 |
| 52 | Emma | 358 |
| 52 | Emma | 369 |
| 53 | Phil | 43 |
| 53 | Phil | 54 |
| 53 | Phil | 109 |
| 53 | Phil | 238 |
| 53 | Phil | 261 |
| 53 | Phil | 283 |
| 53 | Phil | 335 |
| 54 | Steve | 20 |
| 54 | Steve | 141 |
| 54 | Steve | 152 |
| 54 | Steve | 207 |
| 54 | Steve | 336 |
| 54 | Steve | 359 |
| 54 | Steve | 381 |
| 55 | Mark | 21 |
| 55 | Mark | 44 |
| 55 | Mark | 66 |
| 55 | Mark | 118 |
| 55 | Mark | 239 |
| 55 | Mark | 250 |
| 55 | Mark | 305 |
| 56 | Diego | 119 |
| 56 | Diego | 142 |
| 56 | Diego | 164 |
| 56 | Diego | 216 |
| 56 | Diego | 337 |
| 56 | Diego | 348 |
| 56 | Diego | 403 |
| 57 | Luis | 22 |
| 57 | Luis | 33 |
| 57 | Luis | 88 |
| 57 | Luis | 217 |
| 57 | Luis | 240 |
| 57 | Luis | 262 |
| 57 | Luis | 314 |
| 58 | Manoj | 120 |
| 58 | Manoj | 131 |
| 58 | Manoj | 186 |
| 58 | Manoj | 315 |
| 58 | Manoj | 338 |
| 58 | Manoj | 360 |
| 58 | Manoj | 412 |
| 59 | Puja | 23 |
| 59 | Puja | 45 |
| 59 | Puja | 97 |
| 59 | Puja | 218 |
| 59 | Puja | 229 |
| 59 | Puja | 284 |
412 invoice matches across 59 customers. Most customers have seven invoices; one has six. Repeated customer identifiers are the correct one-to-many result, not duplicate invoices.
JOIN with DISTINCT
Return each matching customer once.
SELECT DISTINCT c.CustomerId, c.FirstName
FROM Customer AS c
JOIN Invoice AS i ON i.CustomerId = c.CustomerId
ORDER BY c.CustomerId;Show all 59 recordsShow first 10 records
| CustomerId | FirstName |
|---|---|
| 1 | Luís |
| 2 | Leonie |
| 3 | François |
| 4 | Bjørn |
| 5 | František |
| 6 | Helena |
| 7 | Astrid |
| 8 | Daan |
| 9 | Kara |
| 10 | Eduardo |
| 11 | Alexandre |
| 12 | Roberto |
| 13 | Fernanda |
| 14 | Mark |
| 15 | Jennifer |
| 16 | Frank |
| 17 | Jack |
| 18 | Michelle |
| 19 | Tim |
| 20 | Dan |
| 21 | Kathy |
| 22 | Heather |
| 23 | John |
| 24 | Frank |
| 25 | Victor |
| 26 | Richard |
| 27 | Patrick |
| 28 | Julia |
| 29 | Robert |
| 30 | Edward |
| 31 | Martha |
| 32 | Aaron |
| 33 | Ellie |
| 34 | João |
| 35 | Madalena |
| 36 | Hannah |
| 37 | Fynn |
| 38 | Niklas |
| 39 | Camille |
| 40 | Dominique |
| 41 | Marc |
| 42 | Wyatt |
| 43 | Isabelle |
| 44 | Terhi |
| 45 | Ladislav |
| 46 | Hugh |
| 47 | Lucas |
| 48 | Johannes |
| 49 | Stanisław |
| 50 | Enrique |
| 51 | Joakim |
| 52 | Emma |
| 53 | Phil |
| 54 | Steve |
| 55 | Mark |
| 56 | Diego |
| 57 | Luis |
| 58 | Manoj |
| 59 | Puja |
DISTINCT compares the entire selected row. Adding InvoiceId restores 412 distinct rows. Keep CustomerId: two people can share a name.
EXISTS: test for a match
Which customers have at least one invoice?
SELECT c.CustomerId, c.FirstName
FROM Customer AS c
WHERE EXISTS (
SELECT 1 FROM Invoice AS i
WHERE i.CustomerId = c.CustomerId
)
ORDER BY c.CustomerId;Show all 59 recordsShow first 10 records
| CustomerId | FirstName |
|---|---|
| 1 | Luís |
| 2 | Leonie |
| 3 | François |
| 4 | Bjørn |
| 5 | František |
| 6 | Helena |
| 7 | Astrid |
| 8 | Daan |
| 9 | Kara |
| 10 | Eduardo |
| 11 | Alexandre |
| 12 | Roberto |
| 13 | Fernanda |
| 14 | Mark |
| 15 | Jennifer |
| 16 | Frank |
| 17 | Jack |
| 18 | Michelle |
| 19 | Tim |
| 20 | Dan |
| 21 | Kathy |
| 22 | Heather |
| 23 | John |
| 24 | Frank |
| 25 | Victor |
| 26 | Richard |
| 27 | Patrick |
| 28 | Julia |
| 29 | Robert |
| 30 | Edward |
| 31 | Martha |
| 32 | Aaron |
| 33 | Ellie |
| 34 | João |
| 35 | Madalena |
| 36 | Hannah |
| 37 | Fynn |
| 38 | Niklas |
| 39 | Camille |
| 40 | Dominique |
| 41 | Marc |
| 42 | Wyatt |
| 43 | Isabelle |
| 44 | Terhi |
| 45 | Ladislav |
| 46 | Hugh |
| 47 | Lucas |
| 48 | Johannes |
| 49 | Stanisław |
| 50 | Enrique |
| 51 | Joakim |
| 52 | Emma |
| 53 | Phil |
| 54 | Steve |
| 55 | Mark |
| 56 | Diego |
| 57 | Luis |
| 58 | Manoj |
| 59 | Puja |
The correlation links invoices to the current outer customer. The value 1 is irrelevant: EXISTS tests the presence of a row.
IN: membership
Which customer IDs occur in Invoice?
SELECT CustomerId, FirstName
FROM Customer
WHERE CustomerId IN (SELECT CustomerId FROM Invoice)
ORDER BY CustomerId;Show all 59 recordsShow first 10 records
| CustomerId | FirstName |
|---|---|
| 1 | Luís |
| 2 | Leonie |
| 3 | François |
| 4 | Bjørn |
| 5 | František |
| 6 | Helena |
| 7 | Astrid |
| 8 | Daan |
| 9 | Kara |
| 10 | Eduardo |
| 11 | Alexandre |
| 12 | Roberto |
| 13 | Fernanda |
| 14 | Mark |
| 15 | Jennifer |
| 16 | Frank |
| 17 | Jack |
| 18 | Michelle |
| 19 | Tim |
| 20 | Dan |
| 21 | Kathy |
| 22 | Heather |
| 23 | John |
| 24 | Frank |
| 25 | Victor |
| 26 | Richard |
| 27 | Patrick |
| 28 | Julia |
| 29 | Robert |
| 30 | Edward |
| 31 | Martha |
| 32 | Aaron |
| 33 | Ellie |
| 34 | João |
| 35 | Madalena |
| 36 | Hannah |
| 37 | Fynn |
| 38 | Niklas |
| 39 | Camille |
| 40 | Dominique |
| 41 | Marc |
| 42 | Wyatt |
| 43 | Isabelle |
| 44 | Terhi |
| 45 | Ladislav |
| 46 | Hugh |
| 47 | Lucas |
| 48 | Johannes |
| 49 | Stanisław |
| 50 | Enrique |
| 51 | Joakim |
| 52 | Emma |
| 53 | Phil |
| 54 | Steve |
| 55 | Mark |
| 56 | Diego |
| 57 | Luis |
| 58 | Manoj |
| 59 | Puja |
Repeated IDs inside IN do not multiply outer rows. This positive membership test selects the same customers here; both keys are non-null.
DISTINCT is appropriate for a distinct set of projected values. It is a poor emergency fix for an unexplained join: it can hide missing conditions or merge different customers sharing a name. Decide whether you need invoice detail, customer membership or a customer summary before removing repeats.
NULL, missing conditions and hidden multiplication
For “customers with no invoices”, NOT EXISTS states absence directly. Another approach is a left join followed by checking a non-nullable right-side key for NULL. Avoid checking an optional attribute: a real matching row might have a null attribute too.
NOT EXISTS
Who has no invoices?
SELECT c.CustomerId, c.FirstName
FROM Customer AS c
WHERE NOT EXISTS (
SELECT 1 FROM Invoice AS i WHERE i.CustomerId = c.CustomerId
)
ORDER BY c.CustomerId;| CustomerId | FirstName |
|---|
No rows returned.
No rows: all 59 customers in this database have at least one invoice. NOT EXISTS still expresses absence correctly; an empty result is the answer for this population.
Here every customer has an invoice, so the absence query returns no records. The LEFT JOIN example above uses the visible condition i.Total > 20 to demonstrate customers without a qualifying invoice. A missing correlation can also be hidden by a dataset where every customer already has a match.
NOT IN is not generally interchangeable with NOT EXISTS when the subquery can produce NULL. SQL has three-valued logic: true, false or unknown. To say “2 is not in this set”, every equality must be false. A null comparison leaves uncertainty, so the predicate does not pass WHERE.
A deliberately nullable synthetic set
NOT IN with NULL
Is 2 absent from a nullable set?
WITH candidates(value) AS (VALUES (1), (NULL))
SELECT 2 AS Candidate
WHERE 2 NOT IN (SELECT value FROM candidates);| Candidate |
|---|
No rows returned.
No rows. The comparison involving NULL is unknown; WHERE retains only true predicates.
NOT EXISTS with NULL
Does any candidate equal 2?
WITH candidates(value) AS (VALUES (1), (NULL))
SELECT 2 AS Candidate
WHERE NOT EXISTS (SELECT 1 FROM candidates WHERE value = 2);| Candidate |
|---|
| 2 |
One row, 2. NULL does not equal 2. For nullable outer keys too, decide explicitly whether unknown values should count as matches.
If your rule excludes unknown values, explicitly filter nulls from the membership set and consider nullable outer values separately. If your rule is “no equal record exists”, equality-based NOT EXISTS expresses it directly. Neither form decides your business policy for missing identifiers.
Missing correlation: a hidden mistake
What happens if we forget the customer link?
SELECT c.CustomerId, c.FirstName
FROM Customer AS c
WHERE EXISTS (SELECT 1 FROM Invoice AS i)
ORDER BY c.CustomerId;Show all 59 recordsShow first 10 records
| CustomerId | FirstName |
|---|---|
| 1 | Luís |
| 2 | Leonie |
| 3 | François |
| 4 | Bjørn |
| 5 | František |
| 6 | Helena |
| 7 | Astrid |
| 8 | Daan |
| 9 | Kara |
| 10 | Eduardo |
| 11 | Alexandre |
| 12 | Roberto |
| 13 | Fernanda |
| 14 | Mark |
| 15 | Jennifer |
| 16 | Frank |
| 17 | Jack |
| 18 | Michelle |
| 19 | Tim |
| 20 | Dan |
| 21 | Kathy |
| 22 | Heather |
| 23 | John |
| 24 | Frank |
| 25 | Victor |
| 26 | Richard |
| 27 | Patrick |
| 28 | Julia |
| 29 | Robert |
| 30 | Edward |
| 31 | Martha |
| 32 | Aaron |
| 33 | Ellie |
| 34 | João |
| 35 | Madalena |
| 36 | Hannah |
| 37 | Fynn |
| 38 | Niklas |
| 39 | Camille |
| 40 | Dominique |
| 41 | Marc |
| 42 | Wyatt |
| 43 | Isabelle |
| 44 | Terhi |
| 45 | Ladislav |
| 46 | Hugh |
| 47 | Lucas |
| 48 | Johannes |
| 49 | Stanisław |
| 50 | Enrique |
| 51 | Joakim |
| 52 | Emma |
| 53 | Phil |
| 54 | Steve |
| 55 | Mark |
| 56 | Diego |
| 57 | Luis |
| 58 | Manoj |
| 59 | Puja |
All 59 customers appear because some invoice exists anywhere. Every customer happens to have an invoice in this database, so the data hides this missing-correlation bug. The predicate would also admit a newly added customer with no invoices.
Missing join condition
How many rows does every possible combination produce?
| Combinations |
|---|
| 24308 |
59 × 412 = 24,308. CROSS JOIN makes the Cartesian product explicit; a forgotten predicate in an implicit comma join does the same.
Two independent one-to-many joins can be worse: joining a customer’s invoices and support tickets produces every invoice–ticket pair for that customer. Aggregates may overcount both. Aggregate each relationship to the desired grain first, or use independent existence checks. More nesting is not automatically safer: it should clarify a real dependency.
Which is faster? Inspect the actual plan
There is no universal winner. Optimisers can transform some membership and existence tests into semi-join strategies: preserve a left row if a match exists without outputting every matching pair. Correlation can become an efficient indexed lookup; it can also cause repeated scans. Available rewrites depend on the engine, query shape, nullability and statistics.
An index beginning with Invoice.CustomerId may help equality probes or grouped summaries. Selectivity matters: if nearly all customers qualify, scanning may be cheaper. JOIN plus DISTINCT may require sorting or a temporary structure, but another engine may avoid it. Derived aggregates may materialize or flatten. Read the plan rather than inferring execution from indentation.
- Verify identical semantics, including duplicates and NULL.
- Inspect SQLite EXPLAIN QUERY PLAN or your engine’s equivalent for scans, index searches and temporary structures.
- Measure representative row counts, skew, indexes and current statistics. Record engine/version and relevant warm/cold cache conditions.
- Recheck correctness after tuning. A faster query dropping unmatched customers answers a different question.
This sample database teaches semantics; it is not a production benchmark. No timing claims are made. Adding an index here would not establish production performance.
A practical decision guide
| Need | Starting point | Check |
|---|---|---|
| Columns on every match | INNER JOIN | One-to-many multiplication |
| Unmatched left records too | LEFT JOIN | ON versus WHERE |
| At least one related record | EXISTS | Correct correlation |
| Single-column membership | IN | NULL policy for negation |
| No equal related record | NOT EXISTS | Nullable-key business rules |
| One summary per outer row | Scalar aggregate | Cardinality and missing values |
| Several summaries at known grain | Derived table + JOIN | Unique grouping key |
For the opening question, EXISTS communicates intent directly. If you next need invoice dates and amounts, a join is natural. To preserve invoice details while adding customer totals, a window function may fit better than a grouped join or repeated scalar expression.
Practise: state the grain first
Use the complete database unchanged. Predict which customers have invoices over 20 and which should receive a zero count before revealing a solution.
Beginner · qualifying customers
Find each customer with an invoice over 20, once.
Hint
You need membership, not invoice columns. Put the key comparison and amount condition inside EXISTS.
Solution and checked result
Beginner solution
Find each customer with an invoice over 20, once.
SELECT c.CustomerId, c.FirstName
FROM Customer AS c
WHERE EXISTS (
SELECT 1 FROM Invoice AS i
WHERE i.CustomerId = c.CustomerId AND i.Total > 20
)
ORDER BY c.CustomerId;| CustomerId | FirstName |
|---|---|
| 6 | Helena |
| 26 | Richard |
| 45 | Ladislav |
| 46 | Hugh |
Intermediate · count qualifying invoices including zero
Count invoices over 20 for every customer, including zero.
Hint
Put the amount condition in ON to preserve customers without a qualifying invoice. Count the right-side primary key, not the null-extended row.
Solution and checked result
Intermediate solution
Count invoices over 20 for every customer, including zero.
SELECT c.CustomerId, COUNT(i.InvoiceId) AS InvoiceCount
FROM Customer AS c
LEFT JOIN Invoice AS i ON i.CustomerId = c.CustomerId AND i.Total > 20
GROUP BY c.CustomerId
ORDER BY c.CustomerId;Show all 59 recordsShow first 10 records
| CustomerId | InvoiceCount |
|---|---|
| 1 | 0 |
| 2 | 0 |
| 3 | 0 |
| 4 | 0 |
| 5 | 0 |
| 6 | 1 |
| 7 | 0 |
| 8 | 0 |
| 9 | 0 |
| 10 | 0 |
| 11 | 0 |
| 12 | 0 |
| 13 | 0 |
| 14 | 0 |
| 15 | 0 |
| 16 | 0 |
| 17 | 0 |
| 18 | 0 |
| 19 | 0 |
| 20 | 0 |
| 21 | 0 |
| 22 | 0 |
| 23 | 0 |
| 24 | 0 |
| 25 | 0 |
| 26 | 1 |
| 27 | 0 |
| 28 | 0 |
| 29 | 0 |
| 30 | 0 |
| 31 | 0 |
| 32 | 0 |
| 33 | 0 |
| 34 | 0 |
| 35 | 0 |
| 36 | 0 |
| 37 | 0 |
| 38 | 0 |
| 39 | 0 |
| 40 | 0 |
| 41 | 0 |
| 42 | 0 |
| 43 | 0 |
| 44 | 0 |
| 45 | 1 |
| 46 | 1 |
| 47 | 0 |
| 48 | 0 |
| 49 | 0 |
| 50 | 0 |
| 51 | 0 |
| 52 | 0 |
| 53 | 0 |
| 54 | 0 |
| 55 | 0 |
| 56 | 0 |
| 57 | 0 |
| 58 | 0 |
| 59 | 0 |
Only four customers have a qualifying invoice. COUNT(i.InvoiceId) correctly returns zero for each null-extended row; COUNT(*) would incorrectly return one for the other 55 customers.
Advanced · define the population
Find invoiced customers spending more than the mean of invoiced customers.
Hint
Group Invoice to customer spending. Average those grouped amounts; do not average invoices.
Solution and checked result
Advanced solution
Find invoiced customers spending more than the mean of invoiced customers.
WITH spending AS (
SELECT CustomerId, SUM(Total) AS Amount
FROM Invoice GROUP BY CustomerId
)
SELECT CustomerId, ROUND(Amount, 2) AS Spending
FROM spending
WHERE Amount > (SELECT AVG(Amount) FROM spending)
ORDER BY CustomerId;Show all 22 recordsShow first 10 records
| CustomerId | Spending |
|---|---|
| 1 | 39.62 |
| 3 | 39.62 |
| 4 | 39.62 |
| 5 | 40.62 |
| 6 | 49.62 |
| 7 | 42.62 |
| 17 | 39.62 |
| 20 | 39.62 |
| 22 | 39.62 |
| 24 | 43.62 |
| 25 | 42.62 |
| 26 | 47.62 |
| 28 | 43.62 |
| 34 | 39.62 |
| 37 | 43.62 |
| 42 | 39.62 |
| 43 | 40.62 |
| 44 | 41.62 |
| 45 | 45.62 |
| 46 | 45.62 |
| 48 | 40.62 |
| 57 | 46.62 |
The denominator is all 59 invoiced customers. No customer is omitted from that population here. In a database with customers without invoices, including their zero spending would change the business question and average.
References and next steps
- SQLite SELECT and joins
- SQLite IN, NOT IN, EXISTS and subqueries
- SQLite optimiser overview
- PostgreSQL subquery-expression semantics
Download every example and solution · Next: Window Functions Explained · SQL & Data Skills.
Corrections: report an article issue with the query, SQLite version and expected result. AI-assisted writing with automated SQL checks and independent technical review.
