Bliv ven med SQL SELECT ved at løse rigtige mini-opgaver

Bliv ven med SQL SELECT ved at løse rigtige mini-opgaver

Det korte svar er at du lærer SQL SELECT hurtigst ved at skrive mange små queries på det samme dataset. Men det længere svar er mere interessant.

I stedet for endnu en tør teori-tekst får du her et lille, fast dataset og 12 opgaver, der følges ad som en slags træningsbane. Du kan kopiere tabellerne, køre dem i din egen database og faktisk se, hvad der sker.

Mini-dataset til alle øvelserne

Vi arbejder med tre tabeller, der ligner noget fra en rigtig webshop: customers, products og orders. Du skal ikke gætte på feltnavne, alt står her.

Start med at oprette tabellerne:

CREATE TABLE customers (
  customer_id INT PRIMARY KEY,
  name        VARCHAR(100),
  city        VARCHAR(100),
  email       VARCHAR(100)
);

CREATE TABLE products (
  product_id  INT PRIMARY KEY,
  name        VARCHAR(100),
  category    VARCHAR(50),
  price       DECIMAL(10,2)
);

CREATE TABLE orders (
  order_id    INT PRIMARY KEY,
  customer_id INT,
  product_id  INT,
  quantity    INT,
  order_date  DATE,
  FOREIGN KEY (customer_id) REFERENCES customers(customer_id),
  FOREIGN KEY (product_id)  REFERENCES products(product_id)
);

Og fyld dem med lidt testdata:

INSERT INTO customers (customer_id, name, city, email) VALUES
(1, 'Anna Andersen',   'København', 'anna@example.com'),
(2, 'Bo Bjerregaard',  'Aarhus',    'bo@example.com'),
(3, 'Carla Christensen','Odense',   'carla@example.com'),
(4, 'David Dahl',      'København', NULL),
(5, 'Emma Eskesen',    'Aalborg',   'emma@example.com');

INSERT INTO products (product_id, name, category, price) VALUES
(1, 'USB-kabel 1m',         'Elektronik', 49.00),
(2, 'Trådløs mus',          'Elektronik', 199.00),
(3, 'Kontorstol',           'Møbler',     1299.00),
(4, 'Hæve-sænkebord',       'Møbler',     2999.00),
(5, 'Keramik kaffekop',     'Køkken',     89.00),
(6, 'Espresso brygger 6-kop','Køkken',    349.00);

INSERT INTO orders (order_id, customer_id, product_id, quantity, order_date) VALUES
(1, 1, 2, 1, '2024-01-10'),
(2, 1, 5, 4, '2024-01-11'),
(3, 2, 3, 1, '2024-01-12'),
(4, 3, 1, 2, '2024-01-13'),
(5, 3, 6, 1, '2024-01-13'),
(6, 4, 4, 1, '2024-01-14'),
(7, 4, 2, 1, '2024-01-15'),
(8, 5, 5, 2, '2024-01-16');

Hvis du mangler et sted at køre SQL, kan du kigge på online værktøjer som SQLite i browseren eller installere et lille databaseværktøj lokalt. På codingclass.dk finder du også andre intro-artikler, hvis du først vil have styr på helt basic SQL.

Grundregler for SELECT, FROM og alias

Vi holder syntaksen simpel. De fleste af dine queries vil starte nogenlunde sådan her:

SELECT kolonner
FROM   tabel
[WHERE betingelse]
[ORDER BY kolonne];

Alias er bare shortcuts på kolonnenavne eller tabeller. Det gør især joins lettere at læse.

SELECT c.name AS customer_name, o.order_id
FROM customers AS c
JOIN orders AS o ON o.customer_id = c.customer_id;

Alias med AS er mest til læsbarhed. Databasen er ligeglad, men din hjerne bliver glad.

De 12 opgaver du skal løse

Strukturen er den samme hver gang: opgaveformulering, hvad der cirka skal komme ud, løsning og forklaring. Prøv selv først, og scroll så ned til løsningen. Ja, virkelig.

Filtrering med WHERE og LIKE

Opgave 1: Alle kunder i København

Opgave: Hent navn og email på alle kunder, der bor i København.

Forventet resultat (rækker):

  • Anna Andersen, anna@example.com
  • David Dahl, NULL

Løsning:

SELECT name, email
FROM customers
WHERE city = 'København';

Hvorfor det virker: WHERE city = 'København' filtrerer rækkerne, så kun dem med præcis den værdi bliver vist. Bemærk at David også kommer med, selvom email er NULL, fordi betingelsen kun gælder på city.

Opgave 2: Find kunder med email på example.com

Opgave: Hent navn og email på kunder, hvor email-slutningen er @example.com. Ignorer rækker uden email.

Forventet resultat (rækker): Anna, Bo, Carla, Emma.

Løsning:

SELECT name, email
FROM customers
WHERE email LIKE '%@example.com';

Hvorfor det virker: LIKE med procenttegn (%) betyder “vilkårlige tegn før dette”. Vi filtrerer altså kun på slutningen. David er ude, fordi hans email er NULL, og NULL LIKE '%@example.com' bliver aldrig sandt.

Sortering og begrænsning (ORDER BY, LIMIT)

Opgave 3: Produkter sorteret efter pris

Opgave: Vis alle produkter med navn og pris, sorteret fra billigst til dyrest.

Forventet top og bund:

  • Første række: USB-kabel 1m, 49.00
  • Sidste række: Hæve-sænkebord, 2999.00

Løsning:

SELECT name, price
FROM products
ORDER BY price ASC;

Hvorfor det virker: ORDER BY price ASC sorterer stigende. Mange databaser bruger ASC som default, så du kan også skrive ORDER BY price. Men jeg anbefaler at være eksplicit, så din fremtidige hjerne ikke er i tvivl.

Opgave 4: De 3 dyreste produkter

Opgave: Vis navn og pris på de 3 dyreste produkter.

Forventet resultat (rækker): Hæve-sænkebord, Kontorstol, Espresso brygger 6-kop.

Løsning:

SELECT name, price
FROM products
ORDER BY price DESC
LIMIT 3;

Hvorfor det virker: Vi vender sorteringen med DESC (descending) og bruger LIMIT 3 til kun at tage de tre første. Det er præcis den kombination, man bruger hele tiden til “top 10”-lister.

Aggregation og GROUP BY

Opgave 5: Antal ordrer i alt

Opgave: Tæl hvor mange ordrer der er i orders-tabellen.

Forventet resultat: 8.

Løsning:

SELECT COUNT(*) AS total_orders
FROM orders;

Hvorfor det virker: COUNT(*) tæller rækker. Aliaset AS total_orders giver kolonnen et mere læsbart navn i resultatet. Det er småting, men det gør output mere brugbart, især når man bygger dashboards.

Opgave 6: Antal ordrer per kunde

Opgave: Find hvor mange ordrer hver kunde har lagt. Vis kundens id og antal ordrer.

Forventet resultat (rækkeskæring):

  • customer_id 1: 2 ordrer
  • customer_id 2: 1 ordre
  • customer_id 3: 2 ordrer
  • customer_id 4: 2 ordrer
  • customer_id 5: 1 ordre

Løsning:

SELECT customer_id, COUNT(*) AS order_count
FROM orders
GROUP BY customer_id;

Hvorfor det virker: GROUP BY customer_id samler alle rækker med samme customer_id i en gruppe, og COUNT(*) kører så én gang pr. gruppe. Husk reglen: alle kolonner i SELECT, som ikke er aggregatfunktioner, skal stå i GROUP BY.

Opgave 7: Total solgt mængde per produkt

Opgave: Find hvor mange enheder der i alt er solgt af hvert produkt. Vis produkt-id og total mængde.

Forventet eksempel: product_id 5 har 6 enheder (4 i ordre 2 og 2 i ordre 8).

Løsning:

SELECT product_id, SUM(quantity) AS total_quantity
FROM orders
GROUP BY product_id;

Hvorfor det virker: SUM(quantity) lægger alle quantity-værdierne sammen inden for hver produkt-gruppe. Her begynder SQL for alvor at ligne noget, man bruger til rigtige rapporter.

JOINS (INNER og LEFT)

Opgave 8: Ordrer med kundenavn

Opgave: Vis alle ordrer med ordre-id, dato og kundens navn.

Forventet eksempelrækkke: order_id 1, 2024-01-10, Anna Andersen.

Løsning:

SELECT o.order_id, o.order_date, c.name AS customer_name
FROM orders AS o
INNER JOIN customers AS c
  ON o.customer_id = c.customer_id;

Hvorfor det virker: INNER JOIN betyder “kun rækker, hvor der er match i begge tabeller”. Vi binder tabellerne sammen på den fælles nøgle customer_id. Aliasene o og c gør det mere overskueligt.

Opgave 9: Ordrer med produktnavn og pris

Opgave: Vis ordre-id, produktnavn, antal og produktets pris.

Forventet eksempelrækkke: order_id 2, Keramik kaffekop, quantity 4, price 89.00.

Løsning:

SELECT o.order_id,
       p.name  AS product_name,
       o.quantity,
       p.price
FROM orders AS o
INNER JOIN products AS p
  ON o.product_id = p.product_id;

Hvorfor det virker: Samme mønster som før, bare med products-tabellen. Hvis du kan skrive én join, kan du skrive 90 % af dem, du møder i hverdagen. Resten er variationer.

Opgave 10: Alle kunder, også dem uden ordrer (LEFT JOIN)

Opgave: Vis alle kunder med deres ordre-id, hvis de har nogen. Kunder uden ordrer skal stadig vises.

Forventet nøglepoint: Alle 5 kunder vises. Kunder uden ordrer har NULL i order_id.

Løsning:

SELECT c.customer_id,
       c.name,
       o.order_id
FROM customers AS c
LEFT JOIN orders AS o
  ON c.customer_id = o.customer_id
ORDER BY c.customer_id, o.order_id;

Hvorfor det virker: LEFT JOIN betyder: alle rækker fra venstre tabel (customers), og match fra højre tabel hvis der findes nogen, ellers NULL. Det er standarden hvis du vil finde “hvem har ikke købt endnu”.

Business-spørgsmål på simple data

Opgave 11: Samlet omsætning per kunde

Opgave: Beregn, hvor meget hver kunde har købt for i alt. Brug pris gange antal. Vis kundens navn og total beløb.

Hint: Du skal bruge både orders og products, plus SUM() og GROUP BY.

Løsning:

SELECT c.name AS customer_name,
       SUM(o.quantity * p.price) AS total_amount
FROM orders AS o
JOIN customers AS c
  ON o.customer_id = c.customer_id
JOIN products AS p
  ON o.product_id = p.product_id
GROUP BY c.name
ORDER BY total_amount DESC;

Hvorfor det virker: Vi joiner alle tre tabeller, så hver ordre-række får både kunde og produktpris. o.quantity * p.price giver ordrelinjens beløb, og SUM() lægger det sammen per kunde. Sortering gør det let at se, hvem der er “bedste” kunde.

Opgave 12: Mest solgte kategori

Opgave: Find hvilken produktkategori der er solgt flest enheder af. Vis kategori og total mængde, sorteret så den mest solgte står øverst.

Løsning:

SELECT p.category,
       SUM(o.quantity) AS total_quantity
FROM orders AS o
JOIN products AS p
  ON o.product_id = p.product_id
GROUP BY p.category
ORDER BY total_quantity DESC;

Hvorfor det virker: Vi kobler ordrer til produkter for at kende kategorien, grupperer på kategori og summerer antal. Det er præcis sådan, rapporter om “hvilken type varer trækker mest” bliver bygget.

Typiske fejl når du starter med SQL SELECT

Hvis du sidder og får mærkelige resultater, er du ikke alene. Der er nogle klassiske faldgruber, som nærmest alle ryger i.

NULL opfører sig ikke som du tror

NULL er “ingen værdi”, ikke en tom tekst. Så = NULL virker ikke.

-- Forkert
SELECT * FROM customers WHERE email = NULL;

-- Rigtigt
SELECT * FROM customers WHERE email IS NULL;

Det samme gælder det modsatte: brug IS NOT NULL, ikke != NULL. Hvis noget føles mystisk, så tjek altid om kolonnen indeholder NULL-værdier.

Dubletter når du joiner

Hvis du rammer for brede joins, kan antallet af rækker eksplodere. Det betyder som regel at din join-betingelse ikke er specifik nok.

-- Mistænkeligt: ingen ON-betingelse
SELECT *
FROM orders AS o
JOIN products AS p;

Det her laver et såkaldt cartesisk produkt: hver ordre kombineres med alle produkter. Du vil næsten altid have en ON-betingelse på en nøgle, fx o.product_id = p.product_id. Hvis antallet af rækker pludselig er meget større end forventet, er det et rødt flag.

GROUP BY-fejl og uklare regler

Mange databaser kræver at alle ikke-aggregatkolonner i SELECT står i GROUP BY. Hvis du glemmer det, får du enten en fejl eller et underligt resultat.

-- Typisk fejl i nogle databaser
SELECT customer_id, order_date, COUNT(*)
FROM orders
GROUP BY customer_id;

Her giver order_date ingen mening, fordi du har flere datoer per kunde. Vil du fx have “første ordre-dato”, så skal du bruge en funktion som MIN(order_date) i stedet.

Filtrering før og efter aggregation (WHERE vs HAVING)

En klassiker er at forsøge at bruge WHERE på et aggregat:

-- Forkert
SELECT customer_id, COUNT(*) AS order_count
FROM orders
WHERE order_count >= 2
GROUP BY customer_id;

Aggregater findes ikke endnu i WHERE-fasen. Brug HAVING til at filtrere på resultaterne efter GROUP BY:

SELECT customer_id, COUNT(*) AS order_count
FROM orders
GROUP BY customer_id
HAVING COUNT(*) >= 2;

Tom huskeregel: WHERE filtrerer rækker før grouping, HAVING filtrerer grupper efter.

Sådan kan du øve videre

Hvis du har kørt dig igennem alle 12 opgaver, har du faktisk ram på størstedelen af det, man bruger til hverdags-SQL. Nu handler det mest om volumen: flere datasæt, flere spørgsmål, flere små queries.

Lav dine egne spørgsmål til samme dataset

Et godt næste skridt er at opfinde dine egne “business”-spørgsmål til det samme lille dataset. For eksempel:

  • Hvilken kunde i København har lagt flest ordrer?
  • Hvad er gennemsnitsprisen på produkter i kategorien “Køkken”?
  • Hvor mange ordrer er lagt efter en bestemt dato?

Hvis du vil have mere inspiration til opgaver, kan du også kigge på tutorials om SQL på Coding Class, især inden for databaser og backend.

Skift dataset, men behold mønstrene

Når du er tryg ved det lille webshop-eksempel, så byt det ud med noget andet: film, spil, bøger, kaffeopskrifter, hvad du nu synes er sjovt. Pointen er at du genbruger de samme mønstre:

  • SELECT ... FROM ... WHERE til filtrering
  • ORDER BY ... LIMIT til top-lister
  • GROUP BY + aggregater til optælling
  • JOIN til at binde tabeller sammen

Du kan også begynde at kombinere det med andre teknologier, fx at lave små scripts i Python, der sender SQL-queries til en database. Der ligger flere introartikler til Python på codingclass.dk, hvis du vil den vej.

Brug små daglige øvelser

Hvis du vil lære SQL hurtigt, er det langt bedre at skrive lidt hver dag end at læse én lang teoretisk bog. Tag for eksempel én type opgave ad gangen:

  • Dag 1: kun simple SELECT og WHERE
  • Dag 2: kun ORDER BY og LIMIT
  • Dag 3: kun GROUP BY og COUNT/SUM
  • Dag 4: kun JOIN-varianter

Det føles måske langsomt, men det sætter sig meget bedre fast end at prøve alt på én dag. Lidt ligesom at lære en ny boulder-rute: du tager et par greb ad gangen, ikke hele væggen første forsøg.

Hvis du kun gør én ting anderledes efter at have læst det her, så vælg ét lille dataset, gem det, og brug det som din faste træningsbane til SQL SELECT, indtil du kan lave dine egne spørgsmål og svar uden at kigge opskrifter af.

Gem SQL-sætningen i en fil, fx seed.sql, og kør i terminalen med sqlite3 dev.db '.read seed.sql'. Alternativt kan du bruge GUI'en DB Browser for SQLite og paste statements i SQL-vinduet hvis du foretrækker en visuel tilgang.
Det skyldes ofte at der findes flere matchende rækker i den anden tabel eller manglende/ukorrekt join-betingelse, så rækker multipliceres. Brug klare ON-betingelser, overvej DISTINCT eller aggregering, og tjek om du i stedet skal bruge LEFT/INNER afhængigt af hvilke rækker du vil have med.
På det lille testdataset giver indexes kun minimal gevinst, så de er ikke nødvendige for læring. I produktion eller på større datasæt forbedrer indexes forespørgselsperformance; opret dem fx med CREATE INDEX idx_orders_customer ON orders(customer_id) og vær opmærksom på skriveomkostninger ved nye indexes.
Tilføj opgaver med aggregation (SUM, COUNT), grouping, subqueries, window-funktioner (ROW_NUMBER(), RANK()), og tidsbaserede queries. Du kan også udvide datasættet med flere rækker via et script eller bruge et bibliotek som Faker til at simulere mere realistiske scenarier.

Sara Vestergaard er selvlært kode-nørd, der stille og roligt er gået fra at rode med en enkelt HTML-side til at bygge små værktøjer, scripts og hjemmesider til sig selv og vennerne. Hun startede med at lave en simpel band-hjemmeside som teenager og opdagede, hvor tilfredsstillende det er, når noget, du har skrevet, pludselig lever på skærmen.

For Sara handler kodning ikke om store ord eller imponerende titler, men om meget konkrete problemer: den kedelige opgave, der tager for lang tid, den ven der mangler en lille porteføljeside, eller den liste, der burde sortere sig selv. Hun elsker at pille ting fra hinanden – også kode – for at se, hvad der egentlig foregår, og hun har brugt utallige aftener på at google fejlbeskeder, teste små eksempler og langsomt bygge sin forståelse op.

På Coding Class deler hun den tilgang videre. Hun skriver til dig, der gerne vil lære at kode ved at gøre det i praksis: små projekter, korte kodebidder og forklaringer, der hænger sammen med det, du faktisk sidder med på skærmen. Hun skærer ind til benet, viser typiske fejl og deres løsninger og giver altid et forslag til, hvordan du kan bygge en tand videre, når grundideen først virker.

Når hun ikke skriver til Coding Class eller nørkler med nye små projekter, hænger Sara på klatrevæggen, vander sine altanplanter eller spiller gamle Nintendo-spil. Men hun ender næsten altid tilbage ved tasterne – for der er altid endnu en lille ting, der kunne være smartere, hurtigere eller bare lidt sjovere at bruge.

Send kommentar

You May Have Missed