EDAF75 - lösningsförslag till tentamen i mars 2023

Tentamensuppgifterna finns här.

Problem 1

(a)

Som vanligt kan man lösa uppgiften på lite olika sätt, men vi ville ha något i stil med:

er-model.svg

(b och c)

DROP TABLE IF EXISTS mätningar;
DROP TABLE IF EXISTS analyser;
DROP TABLE IF EXISTS observationer;
DROP TABLE IF EXISTS rutor;
DROP TABLE IF EXISTS lokaler;
DROP TABLE IF EXISTS personer;
DROP TABLE IF EXISTS arter;


CREATE TABLE lokaler (
  lokal_id     TEXT DEFAULT (lower(hex(randomblob(16)))),
  lokal_namn   TEXT,
  lokal_typ    TEXT,
  gps_long     TEXT,
  gps_lat      TEXT,
  PRIMARY KEY  (lokal_id)
);

CREATE TABLE rutor (
  rut_id       TEXT DEFAULT (lower(hex(randomblob(16)))),
  lokal_id     TEXT,
  d_long       REAL,
  d_lat        REAL,
  ansvarig     TEXT,    -- personnummer
  PRIMARY KEY  (rut_id),
  FOREIGN KEY  (lokal_id) REFERENCES lokaler(lokal_id),
  FOREIGN KEY  (ansvarig) REFERENCES personer(personnummer)
);

CREATE TABLE personer (
  personnummer  TEXT,
  namn          TEXT,
  adress        TEXT,
  PRIMARY KEY   (personnummer)
);

CREATE TABLE arter (
  art_id         TEXT DEFAULT (lower(hex(randomblob(16)))),
  latinskt_namn  TEXT,
  PRIMARY KEY    (art_id)
);

CREATE TABLE observationer (
  rut_id         TEXT,
  art_id         TEXT,
  täckningsgrad  REAL,
  tidpunkt       DATETIME DEFAULT (datetime('now')),
  PRIMARY KEY    (rut_id, art_id),
  FOREIGN KEY    (rut_id) REFERENCES rutor(rut_id),
  FOREIGN KEY    (art_id) REFERENCES arter(art_id)
);

CREATE TABLE analyser (
  analys_id    TEXT DEFAULT (lower(hex(randomblob(16)))),
  typ          TEXT,
  enhet        TEXT,
  PRIMARY KEY  (analys_id)
);

CREATE TABLE mätningar (
  rut_id       TEXT,
  analys_id    TEXT,
  tidpunkt     DATETIME DEFAULT (datetime('now')),
  värde        REAL,
  PRIMARY KEY  (rut_id, analys_id, tidpunkt),
  FOREIGN KEY  (rut_id) REFERENCES rutor(rut_id),
  FOREIGN KEY  (analys_id) REFERENCES analyser(analys_id)
);


SELECT DISTINCT latinskt_namn
FROM      observationer
          JOIN rutor USING (rut_id)
          JOIN lokaler USING (lokal_id)
          JOIN arter USING (art_id)
WHERE     lokal_typ = 'Sydsvensk lövskod';

Problem 2

(a)

Ett sätt att rita detta ER-diagram är:

sales-model.svg

Vi kan även ta bort våra 'invented keys' (eftersom de egentligen är en implementationsdetalj), och rita något i stil med:

sales-model-2.svg

Som vanligt kan man rita associationsklasserna som 'vanliga' klasser mellan de två klasser som de nu binder samman, med *-1 på vardera sidan.

Det går även bra att låta Sale associera till både Item och Store, istället för att använda Offer (som ovan) – resultatet skulle i slutändan nästan bli detsamma (det som i detta fall skulle vara två separata främmande nycklar skulle motsvara den sammansatta nyckeln till Offer i figuren ovan).

(b)

Skriv ut namn och adress för alla kunder som inte köpt någon vara under 2022. TIPS: man kan få årtalet för ett datum med hjälp av standardfunktionen year.

SELECT  name, address
FROM    customers
WHERE   customer_id NOT IN (
        SELECT  customer_id
        FROM    sales
        WHERE   year(sales_date) = 2022);

Egentligen finns det ingen year-funktion i SQLite (jag bara hittade på den för att göra uppgiften lättare), men vi kan använda den lite krångligare strftime:

SELECT  name, address
FROM    customers
WHERE   customer_id NOT IN (
        SELECT  customer_id
        FROM    sales
        WHERE   strftime('%Y', sales_date) = '2022');

Man kan även bara läsa datumet som en sträng, så sales_date LIKE "2022%" fungerar fint.

(c)

Skriv ut antalet affärer som säljer varor med ett item_id som börjar på ’EF’.

Ett problem här är att vi inte vill räkna någon butik mer än en gång, så lösningen:

SELECT  count(shop_id)
FROM    offers
WHERE   item_id LIKE 'EF%';

fungerar inte riktigt (men den ger ändå en del poäng).

Ett elegant sätt att lösa problemet med att butiker kan ha flera olika varor som börjar på EF, och bara räkna varje butik en gång, är:

SELECT  count(DISTINCT shop_id)
FROM    offers
WHERE   item_id LIKE 'EF%';

Man kan även använda en subquery:

SELECT  count()
FROM    shops
WHERE   shop_id IN (
    SELECT DISTINCT shop_id
    FROM   offers
    WHERE  item_id LIKE 'EF%');

(här behöver man egentligen inte använda DISTINCT inne i subqueryn, men det känns naturligt).

Ganska många har använt GROUP BY shop_id, vilket är en fiffig idé, eftersom den gör att vi får en rad per butik. Problemet är att count() i detta fall inte räknar antalet rader i tabellen, utan istället räknar antalet rader i varje grupp – men vi kommer att rätta detta fel väldigt snällt (man får bara ett litet avdrag).

(d)

Skriv ut namn och adress för den affär (eller de affärer) som säljer varan med item_id = ’EF123-A’ till lägst pris.

SELECT   name, address
FROM     offers
         JOIN shops USING (shop_id)
WHERE    item_id = 'EF123-A' AND
         price = (SELECT min(price)
                  FROM   offers
                  WHERE  item_id = 'EF123-A');

(e)

För alla kunder som handlat för minst 1000 kronor: skriv ut kund-id, namn och totalsumman för alla inköp, sortera efter minskande totalsumma.

SELECT    customer_id, name, address, sum(price) AS total_price
FROM      sales
          JOIN offers USING (shop_id, item_id)
          JOIN customers USING (customer_id)
GROUP BY  customer_id
HAVING    total_price >= 1000
ORDER BY  total_price DESC;

Problem 3

(a)

Vi får följande funktionella beroenden:

  • stil_id -> name
  • stil_id lab_no -> group_no
  • lab_no group_no -> status

(b)

Vi har alltså:

  • R(A,B,C,D,E,F)

och:

  • fd1: A -> CF
  • fd2: C -> A
  • fd3: CE -> B
  • fd4: F -> D

Vi kan börja med att ta det transitiva höljet av vart och ett av dessa beroenden (det underlättar när vi skall beräkna nycklarna):

  • fd1: A -> CDF
  • fd2: C -> ADF
  • fd3: CE -> ABDF
  • fd4: F -> D

Det visar sig alltså direkt att CE är en nyckel (eftersom vi får alla övriga attribut).

För att hitta alla nycklar kan vi konstatera att E måste ingå i varje nyckel, eftersom den inte ingår i något högerled. Och E själv ger ingenting om vi tar dess transitiva hölje:

  • {E}+ = E

Så vi får börja med alla möjliga två-attributsnycklar som innehåller E (och vet redan att CE kommer att vara en nyckel):

  • {AE}+ = ABCDEF
  • {BE}+ = BE
  • {CE}+ = ABCDEF
  • {DE}+ = DE
  • {EF}+ = DEF

Det visar sig att både AE och CE är nycklar, men vi måste även undersöka eventuella tre-attributnycklar – dessa måste innehålla E, men får inte innehålla vare sig A eller C:

  • {BDE}+ = BDE
  • {BEF}+ = BEFD
  • {DEF}+ = DEF

Så, inga nya nycklar, men vi måste även testa den enda möjliga fyr-attributskombinationen som innehåller E men varken A eller C:

  • {BDEF}+ = BDEF

Så, det visar sig att AE och CE är våra enda nycklar.

(c)

Vi är inte i BCNF, eftersom fd1, fd2 och fd4 har vänsterled som inte är supernycklar (däremot är fd3 OK, CE är en nyckel).

(d)

Hela poängen med normaliseringen är att vi får ett antal tabeller som vi kan join-a ihop igen, till precis samma rader som vi hade från början. Och det är det som är poängen med den metod att normalisera som vi diskuterade under föreläsning 7-8 – att bara skriva upp ett antal relationer som var och en är i BCNF visar inte att vi kan återskapa ursprungstabellen utan förvrängning, så man får inga poäng om man inte följer 'receptet'.

Vi utgår alltså från R(A,B,C,D,E,F) och de funktionella beroenden som vi såg ovan. När vi skall normalisera är det lämpligt att börja med att utveckla det transitiva höljet av respektive beroende, annars måste vi göra det upprepade gånger i efterföljande steg, och då är det lätt att missa något. Observera att vi i detta fall, utöver beroendena fd1 … fd4, faktiskt har hittat ett beroende till: eftersom AE är en nyckel gäller det funktionella beroendet AE -> BCDF (om vi inte skriver ut det här kommer vi ändå att kunna härleda det via A -> CDF och CE -> ABDF, men det är enklare att ha det med från början):

  • fd1: A -> CDF
  • fd2: C -> ADF
  • fd3: CE -> ABDF
  • fd4: F -> D
  • fd5: AE -> BCDF

Våra nycklar är dem vi fann ovan, och vi kan sammanställa det som:

R(A,B,C,D,E,F)
--------------
fds: A -> CDF, C -> ADF, CE -> ABDF, F -> D, AE -> BCDF
keys: AE, CE

Vi skall bryta upp på ett funktionellt beroende som bryter mot BCNF – vi gör det för att dess vänsterled kan förekomma flera gånger i tabellen, och att dess högerled kommer att upprepas varje gång (att bryta upp på ett beroende som är i BCNF, dvs har ett vänsterled som är en supernyckel, visar att man inte har förstått idén bakom normaliseringen – vi vill undvika redundans, men för ett beroende med en supernyckel i vänsterledet kan varje vänsterled bara förekomma en gång, och då får vi ingen redundans).

Vi kan exempelvis välja beroendet A -> CDF, som ju har ett vänsterled som inte är en supernyckel, och får då:

  • en tabell med alla attributen i det valda funktionella beroendet: R1(A,C,D,F), och
  • en tabell med allt utom högerledet i det valda funktionella beroendet: R2(A,B,E) – observera att vänsterledet i vårt funktionella beroende måste finnas kvar, eftersom vi behöver det för att 'joina' in attributen i dess högerled från tabellen R1.

I tabellen R1 har vi kvar beroendena A -> CDF, C -> ADF och F -> D, men de andra två beroenden vi hade försvinner, eftersom vi inte längre har hela deras vänsterled (observera att vi måste ha samtliga attribut i vänsterledet för att ett beroende skall gälla):

R1(A,C,D,F)
--------------
fds: A -> CDF, C -> ADF, F -> D
keys: A, C

I tabellen R2 har vi kvar vänsterledet i några av de beroenden vi hade ovan, men om inget av attributen i högerledet finns kvar så 'försvinner' beroendet (så A -> CDF och C -> ADF försvinner). För AE -> BCDF har vi kvar hela vänsterledet, men bara en del av högerledet – eftersom vi har hela vänsterledet kommer de delar av högerledet som återstår att gälla, alltså har vi beroendet AE -> B:

R2(A,B,E)
--------------
fds: AE -> B
keys: AE

Uppdelningarna kan vi göra med med dessa SQL-satser (detta behövde ni inte göra på tentan denna gång, men vi kommer troligen att ha det på framtida tentor):

CREATE TABLE R1 AS
  SELECT DISTINCT A,C,D,F
  FROM   R;
CREATE INDEX r1key ON R1a(A);

CREATE TABLE R2 AS
  SELECT DISTINCT A,B,E
  FROM   R;
CREATE INDEX r2key ON R2(A,E);

Vi gör SELECT DISTINCT eftersom vi i våra redundanta tabeller kan ha fått upprepningar (beroende på att vi inte var i BCNF).

Här är R2 redan i BCNF, eftersom det enda funktionella beroendet har ett vänsterled som är en supernyckel, men vi måste jobba vidare med R1, som ju har beroendet F -> D, vars vänsterled inte är en supernyckel.

Vi bryter därför ut F -> D till en ny relation R1a(F,D) och kvar från R1 har vi då R1b(A,C,F):

R1a(F,D)
--------------
fds: F -> D
keys: F

och

R1b(A,C,F)
--------------
fds: A -> CF, C -> AF
keys: A, C

Båda dessa tabeller är i BCNF, eftersom deras funktionella beroenden har supernycklar i vänsterleden.

Uppdelningen kan göras med SQL-satserna:

CREATE TABLE R1a AS
  SELECT DISTINCT F,D
  FROM   R1;
CREATE INDEX r1aKey ON R1a(F);

CREATE TABLE R1b AS
  SELECT DISTINCT A,C,F
  FROM   R1;
CREATE INDEX r1bKey ON R1b(A);

Så, vi kan alltså bryta upp i de tre relationerna:

  • R2(A,B,E)
  • R1b(A,C,F)
  • R1a(F,D)

och sättet vi brutit upp på garanterar att vi kan slå ihop dem till ursprungstabellen ovan, utan att tappa rader, eller få nya rader (se nästa deluppgift).

Det finns alternativa lösningar, eftersom vi kan välja att börja bryta upp på något annat av våra BCNF-brytande beroenden – en ganska vanlig lösning på tentan var att börja med att bryta ut F -> D.

Vi utgår då återigen från:

R(A,B,C,D,E,F)
--------------
fds: A -> CDF, C -> ADF, CE -> ABDF, F -> D, AE -> BCDF
keys: AE, CE

och får nu:

R1(F,D)
--------------
fds: F -> D
keys: F

och

R2(A,B,C,E,F)
--------------
fds: A -> CF, C -> AF, CE -> ABF, AE -> BCF
keys: AE, CE

Här är R1 i BCNF, eftersom vänsterledet i F -> D är en supernyckel, men för R2 bryter både A -> CF och C -> AF mot BCNF – om vi väljer att bryta upp på A -> CF så får vi:

R2a(A,C,F)
--------------
fds: A -> CF, C -> AF
keys: A, C

och

R2b(A,B,E)
--------------
fds: AE -> B
keys: AE

Båda dessa är i BCNF, eftersom det inte finns något beroende vars vänsterled inte är en supernyckel.

Våra tre relationer här blir:

  • R1(F,D)
  • R2a(A,C,F)
  • R2b(A,B,E)

vilket faktiskt är samma relationer som vi fick ovan (bortsett från namnen på relationerna) – olika val av beroenden att bryta upp på i de olika stegen kan ge olika slut-relationer, men med den metod vi använt kan vi alltid vara säkra på att vi kan återskapa ursprungsrelationen utan att tappa något, eller få något extra.

(e)

Vi kan nu enkelt bygga tillbaka ursprungsrelationen R – om vi använder den första uppdelningen ovan:

  • R2(A,B,E) (detta är resultatet efter att vi tog bort A -> CDF från R) – vi kan joina ihop den med återstoden med hjälp av join-predikatet A
  • R1b(A,C,F) (detta är återstoden efter att vi först skapade R1(A,C,D,F) och sedan tog bort F -> D), vi kan joina in D med hjälp av join-predikatet F
  • R1a(F,D)

så får vi:

SELECT    *
FROM      R2
          JOIN R1b USING (A)
          JOIN R1a USING (F)

Problem 4

Vi behöver en trigger som håller koll på hur mycket en kund har handlat för under den aktuella dagen, och om summan efter det nya inköpet skulle överstiga 32 000, så avbryter vi. Enklast i detta fall är att köra triggern efter insättningen i sales (eftersom vi då får med totalbeloppet inklusive det nya inköpet):

DROP TRIGGER IF EXISTS limit_purchases;
CREATE TRIGGER limit_purchases
AFTER INSERT ON sales
WHEN
  (SELECT sum(price)
   FROM   sales
          JOIN offers USING (shop_id, item_id)
   WHERE  customer_id = NEW.customer_id
          AND sales_date = NEW.sales_date
  ) > 32000
BEGIN
  SELECT RAISE (ROLLBACK, "Trying to buy for more than 32 000 during one day");
END;

Vi kan naturligtvis även köra triggern före insättningen, och då testa att den tidigare summan plus det nya beloppet inte är större än 32000.

Problem 5

Läs in ett artikelnummer (item_id), och skriv ut de (högst) tre dagar då varan såldes flest gånger – skriv även ut antalet gånger den såldes de aktuella dagarna.

import sqlite3


db = sqlite3.connect("shopping.sqlite")


def show_most_sales(item_id):
    c = db.cursor()
    c.execute(
        """
        SELECT    sales_date, count(sales_date) AS times_sold
        FROM      sales
        WHERE     item_id = ?
        GROUP BY  sales_date
        ORDER BY  times_sold DESC
        LIMIT     3
        """,
        [item_id]
    )
    for sales_date, times_sold in c:
        print(f"{sales_date:>20}: {times_sold:5d}")


def main():
    item_id = input('Item id: ')
    show_most_sales(item_id)


if __name__ == "__main__":
    main()

Problem 6

Se kurshemsidorna.