EDAF75 – tentamen i mars 2022, lösningsförslag

Problem 1

(a)

Som ofta kan man lösa uppgiften på lite olika sätt, så det är helt OK om ni inte har precis nedanstående modell.

Det finns egentligen inte särskilt många bra naturliga nycklar i modellen, vi skulle kunna designa den så här:

Det är OK om ni skriver ut era 'invented keys', även om det egentligen är en implementationsdetalj:

Det är inte självklart hur vi skall hantera multipliciteten för resurserna, uppgiftstexten sade: "En resurs har ett namn, och en ansvarig karnevalist, och de kan användas vid flera aktiviteter (dock bara en åt gången)". I diagrammen ovan har jag markerat kopplingen mellan Resurs och Aktivitet som en *-* association, men uttrycket "dock bara en åt gången" gör att vi även kan använda en *-1-association.

Valet av vad som skall vara nycklar är ganska fritt, det viktigaste kravet är att det fungerar med lösningarna i (b) och (c).

(b)

Det mesta av översättningen från ER-modellen till tabeller klarar vi av med hjälp av några enkla regler:

  • Varje entity set (ES) i vår modell kommer att ge en tabell i vår databas:
    • attributen i vår ES kommer att bli kolumner i tabellen
    • om vi har en lämplig naturlig nyckel i vårt ES så kan vi låta den bli primärnyckel i tabellen, annars kan vi introducera en invented key
    • om vår ES har *-1-associationer till andra entity sets (alltså en 1-a på andra sidan), så lägger vi i vår tabell till främmande nycklar som unikt pekar ut ett värde i den andra tabellen (vi gör likadant för alla *-1-associationer från vårt aktuella ES)
  • Varje *-*-association ger upphov till en join-tabell, dvs en tabell som kopplar ihop rader i den ena tabellen med rader i den andra tabellen. På en del *-*-associationer har vi associationsklasser, dessa klasser kan bidra med attribut till våra join-tabeller.

    Det är inte ovanligt att studenter använder främmande nycklar på den ena, eller kanske till och med båda sidor i en *-*-association, men det fungerar inte alls, och räknas som ett grovt fel. Om vi har en *-*-association, så får vi en ny tabell.

Vi vill helst ha primärnycklar i samtliga tabeller, och vi bör inte göra dem större än nödvändigt (exempelvis kan vi låta tabellen roller ha nyckeln (stil_id, sektionsnamn), om man bara kan ha en roll i varje sektion – även tupeln (stil_id, sektionsnamn, titel) är naturligtvis unik (den är en superkey), men den är onödigt stor.

För modellen ovan kan vi få (jag har använt stil_id, och förutsatt att samtliga karnevalister har en stil_id, man kan naturligtvis tänka sig andra nycklar, även telefonnummer, även om det inte alltid känns superbeständigt):

CREATE TABLE karnevalister (
  stil_id     TEXT,
  namn        TEXT,
  telefon     TEXT,
  PRIMARY KEY (stil_id)
);

CREATE TABLE sektioner (
  sektionsnamn  TEXT,
  uppdrag       TEXT,
  PRIMARY KEY   (sektionsnamn)
);

CREATE TABLE roller (
  titel         TEXT,
  stil_id       TEXT,
  sektionsnamn  TEXT,
  PRIMARY KEY   (stil_id, sektionsnamn)
  FOREIGN KEY   (stil_id) REFERENCES karnevalister(stil_id),
  FOREIGN KEY   (sektionsnamn) REFERENCES sektioner(sektionsnamn)
);

CREATE TABLE aktiviteter (
  aktivitetsid  TEXT,
  beskrivning   TEXT,
  starttid      DATETIME,
  sluttid       DATETIME,
  PRIMARY KEY   (aktivitetsid)
);

CREATE TABLE uppgifter (
  uppgiftsid    TEXT,
  beskrivning   TEXT,
  aktivitetsid  TEXT,
  stil_id       TEXT,
  sektionsnamn  TEXT,
  PRIMARY KEY   (uppgiftsid),
  FOREIGN KEY   (aktivitetsid) REFERENCES aktiviteter(aktivitetsid),
  FOREIGN KEY   (stil_id) REFERENCES karnevalister(stil_id),
  FOREIGN KEY   (sektionsnamn) REFERENCES sektioner(sektionsnamn)
);

CREATE TABLE resurser (
  resursid     TEXT,
  beskrivning  TEXT,
  stil_id      TEXT,    -- ansvarig
  PRIMARY KEY  (resursid),
  FOREIGN KEY  (stil_id) REFERENCES karnevalister(stil_id)
);

CREATE TABLE resursbokningar (
  aktivitetsid  TEXT,
  resursid      TEXT,
  PRIMARY KEY   (resursid, aktivitetsid),
  FOREIGN KEY   (aktivitetsid) REFERENCES aktiviteter(aktivitetsid),
  FOREIGN KEY   (resursid) REFERENCES resurser(resursid)
);

(c)

SELECT  namn, telefon
FROM    uppgifter
        JOIN aktiviteter USING (aktivitetsid)
        JOIN karnevalister USING (stil_id)
WHERE   sektionsnamn = 'Barnevalen'
        AND starttid = '2022-05-21 15:00'
        AND sluttid = '2022-05-21 16:00';

Problem 2

(a)

Ett sätt att rita ER-diagrammet är:

Relationen GroupMembership är egentligen bara en 'join-tabell' för Student och Group, och den finns implicit i vår *-*-association mellan Student och Group, men vi kan rita ut den, antingen som en tom associationsklass på associationen mellan Student och Group, eller som en relation inkilad mellan Student och Group med *-1-relationer till var och en av dem.

I uppgiften ingick inte att definiera tabeller, men vi skulle ha fått något i stil med:

CREATE TABLE students (
  stil_id      TEXT,
  name         TEXT,
  PRIMARY KEY  (stil_id)
);

CREATE TABLE assignments (
  assignment_number  INT,
  description        TEXT,
  PRIMARY KEY        (assignment_number)
);

CREATE TABLE groups (
  assignment_number  INT,
  group_number       INT,
  grade              INT,
  PRIMARY KEY        (assignment_number, group_number),
  FOREIGN KEY        (assignment_number)
                     REFERENCES assignments(assignment_number)
);

CREATE TABLE group_memberships (
  stil_id            TEXT,
  assignment_number  INT,
  group_number       INT,
  PRIMARY KEY        (stil_id, assignment_number),
  FOREIGN KEY        (stil_id)
                     REFERENCES students(stil_id),
  FOREIGN KEY        (assignment_number, group_number)
                     REFERENCES groups(assignment_number, group_number)
);

(b)

Lägg in grupp 12 på uppgift 3, och lägg sedan in studenterna med stil-id ’alice’ och ’bob’ i den nya gruppen (du kan förutsätta att både uppgiften och studenterna redan finns i databasen, och att gruppen inte fanns).

INSERT
INTO    groups(assignment_number, group_number, grade)
VALUES  (3, 12, 0);

INSERT
INTO    group_memberships(stil_id, assignment_number, group_number)
VALUES  ('alice', 3, 12),
        ('bob', 3, 12);

(c)

Lista uppgiftsbeskrivning och betyg för alla de uppgifter som studenten med stil-id ’alice’ har varit inblandad i (även de ännu inte godkända), ordna efter uppgiftsnummer.

SELECT    description, grade
FROM      assignments
          JOIN groups USING (assignment_number)
          JOIN group_memberships USING (assignment_number, group_number)
WHERE     stil_id = 'alice'
ORDER BY  assignment_number;

Observera att vi testar både assignment_number och group_number när vi joinar in group_memberships, ett sätt att förstå varför vi gör det är följande:

  • Vi utgår från tabellen assignments, och har då alltså en rad per uppgift – tabellen ser ut ungefär så här:

    assignment_number  description
    ------------------------------
    1                  Uppgift 1
    2                  Uppgift 2
    ...                ...
    
  • Vi vill sedan lägga till information om grupperna för varje uppgift, och joinar därför med groups för att få samtliga projektgrupper för samtliga uppgifter – vi får då \(\sum c_k\) rader, där \(c_k\) är antalet grupper för uppgift \(k\) (om vi antar att vi har \(p\) grupper för var och en av de fyra uppgifterna blir det \(4 p\) rader). Den sammanslagna tabellen ser ut ungefär så här:

    assignment_number  description group_number grade
    -------------------------------------------------
    1                  Uppgift 1   1            3
    1                  Uppgift 1   2            3
    1                  Uppgift 1   3            4
    ...                ...
    2                  Uppgift 2   1            3
    2                  Uppgift 2   2            0
    2                  Uppgift 2   3            5
    ...                ...
    
  • Vi vill sedan lägga till information om varje student som är med i varje grupp, så för varje projektgrupp i tabellen ovan vill vi lägga in samtliga deltagande studenter genom att joina in group_memberships. För varje rad i tabellen ovan får vi alltså nu en rad per gruppmedlem – det skulle ge oss en tabell vars rader är ungefär:

    assignment_number  description group_number grade stil_id
    ---------------------------------------------------------
    1                  Uppgift 1   1            3     alice
    1                  Uppgift 1   1            3     bob
    1                  Uppgift 1   2            3     cecilia
    1                  Uppgift 1   2            3     david
    1                  Uppgift 1   3            4     emma
    1                  Uppgift 1   3            4     frank
    ...                ...
    2                  Uppgift 2   1            3     david
    2                  Uppgift 2   1            3     emma
    2                  Uppgift 2   2            3     bob
    2                  Uppgift 2   2            3     alice
    2                  Uppgift 2   3            4     frank
    2                  Uppgift 2   3            4     cecilia
    ...                ...
    

    När vi joinar in stil_id från group_memberships vill vi ju bara få de studenter som ingår i den aktuella gruppen, dvs den grupp vars assignment_number och group_number stämmer överens med raden vi lägger in stil_id på.

(d)

Lista stil-id och namn på alla de studenter som är godkända på alla fyra uppgifterna.

Vi kan göra det med en SELECT-sats och en GROUP BY med ett HAVING-villkor:

SELECT    stil_id, name
FROM      students
          JOIN group_memberships USING (stil_id)
          JOIN groups USING (assignment_number, group_number)
WHERE     grade > 0
GROUP BY  stil_id
HAVING    count() = 4;

Alternativt kan vi först bygga upp en tabell med antalet godkända uppgifter för samtliga studenter i en WITH-sats:

WITH pass_count(stil_id, nbr_of_passed) AS (
  SELECT    stil_id, count()
  FROM      students
            JOIN group_memberships USING (stil_id)
            JOIN groups USING (assignment_number, group_number)
  WHERE     grade > 0
  GROUP BY  stil_id
)
SELECT  stil_id, name
FROM    pass_count
        JOIN students USING (stil_id)
WHERE   nbr_of_passed = 4;

(e)

Lista stil-id och namn för alla studenter som har det högsta betyg som förekommer på uppgift 1. Det högsta förekommande betyget är inte nödvändigtvis 5 – om du inte kommer på hur man hanterar detta fall det kan du använda betyget 5 istället, men för full poäng vill vi att du bara jämför med det högsta förekommande betyget på uppgiften (du tappar ca 50% om du inte gör det).

Vi kan ta reda på det högsta betyget med hjälp av en subquery, och får då:

SELECT  stil_id, name
FROM    students
        JOIN group_memberships USING (stil_id)
        JOIN groups USING (assignment_number, group_number)
WHERE   assignment_number = 1
        AND grade = (SELECT max(grade)
                     FROM   groups
                     WHERE  assignment_number = 1);

(f)

Lista för varje uppgift (alltså 1..4) hur många grupper som ännu inte är godkända (dvs har betyget 0), ordna efter uppgiftsnummer. Vi antar att alla uppgifter och alla grupper är inlagda. Denna deluppgift blir lite knepigare om det finns någon uppgift som alla grupper är godkänd på – för att få poäng på uppgiften behöver du inte hantera detta fall, men för full poäng måste du göra det (du tappar ca 50% om du inte gör det).

Problemet här är att lösningen:

SELECT    assignment_number, count(group_number)
FROM      assignments
          LEFT JOIN groups USING (assignment_number)
WHERE     grade = 0
GROUP BY  assignment_number
ORDER BY  assignment_number;

inte fungerar riktigt som man skulle önska – vår LEFT JOIN ger oss visserligen en rad även för de uppgifter som inte har några grupper kopplade till sig (men alla grupper var inlagda, så det borde inte göra någon skillnad), men när vi sedan testar om grade = 0 så försvinner de uppgifter där alla är godkända, redan innan vi grupperar.

För att få det att fungera kan vi göra en LEFT JOIN (eller, om vi antar att alla grupper är inlagda, en vanlig JOIN) utan att sedan filtrera bort några rader – vi kan göra det med hjälp av en WITH-sats som definerar alla grupper som finns kvar (det finns även andra sätt att lösa det på):

WITH remaining_groups(assignment_number, group_number) AS
  SELECT  assignment_number, group_number
  FROM    groups
  WHERE   grade = 0
)
SELECT    assignment_number, count(group_number)
FROM      assignments
          LEFT JOIN remaining_groups USING (assignment_number)
GROUP BY  assignment_number
ORDER BY  assignment_number;

Problem 3

(a)

Vi får följande funktionella beroenden:

  • isbn -> title year author classification
  • author year -> isbn
  • isbn format -> price

Ur dessa kan vi transitivt härleda andra attribut, men det är dessa beroenden som ger poäng (på denna tenta fick man inget avdrag alls om man angav 'härledda' attribut i högerleden av dessa beroenden – det skulle framgå av uppgiftstexten om 'onödiga' extra attribut skulle ge avdrag, normalt gör de det inte).

(b)

Vi har alltså:

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

och:

  • fd1: AB -> E
  • fd2: B -> C
  • fd3: C -> BD
  • fd4: D -> F

Vi kan börja med att ta det transitiva höljet av vart och ett av dessa beroenden:

  • fd1: AB -> CDEF
  • fd2: B -> CDF
  • fd3: C -> BDF
  • fd4: D -> F

Vi kan lösa uppgiften utan att bilda dessa transiviva höljen, men att ha beräknat dem minskar risken att missa något transitivt beroende när vi normaliserar nedan (under den sista frågestunden såg vi ett exempel på precis vad som kan hända).

Eftersom A inte förekommer i något högerled måste det ingå i en nyckel, men om vi tar dess transitiva hölje, {A}+, så får vi bara A. Vi måste därför testa alla två-attributkombinationer som innehåller A:

  • {AB}+ = ABCDEF
  • {AC}+ = ABCDEF
  • {AD}+ = ADF
  • {AE}+ = AE
  • {AF}+ = AF

Här är både AB och AC nycklar, men vi måste även undersöka eventuella tre-attributnycklar – dessa måste innehålla A, men får inte innehålla varken B eller C:

  • {ADE}+ = ADEF
  • {ADF}+ = ADF
  • {AEF}+ = AEF

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

  • {ADEF}+ = ADEF

Så, det visar sig att AB och AC är våra enda nycklar.

(c)

Vi är inte i BCNF, eftersom vi har funktionella beroenden vars vänsterled inte är supernycklar – detta gäller för fd2, fd3 och fd4 (däremot är fd1 OK, AB ä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', och det innebär ganska säkert att man blir underkänd på tentan. Så kontrollera noga att du förstår lösningen nedan, och fråga under frågestunder om det är något du är osäker på.

Vi utgår denna gång från:

R(A,B,C,D,E,F)
--------------
fds: AB -> CDEF, B -> CDF, C -> BDF, D -> F
keys: AB, AC

Vi såg ovan att vi inte är i BCNF, och vi kan bryta upp med någon av B -> CDF, C -> BDF eller D -> F (som alla har för 'små' vänsterled) – vi kan välja B -> CDF, och får då dels en relation med attributen i beroendet:

R1(B,C,D,F)
--------------
fds: B -> CDF, C -> BDF, D -> F
keys: B, C

och en relation där vi tar bort de attribut som finns i högerledet i det funktionella beroende vi bröt upp nyss (vi började detta steg med R(A,B,C,D,E,F)):

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

Vi har alltså tagit bort C, D och F här, men behållit B (eftersom det stod i vänsterledet i B -> CDF, och vi därför behöver det för att slå upp C, D och F ur R1).

Här är R2 i BCNF, eftersom dess enda beroende har ett vänsterled som är en supernyckel, men för R1 bryter beroendet D -> F mot BCNF, vi måste därför bryta upp R1 på samma sätt som vi gjorde ovan – först en relation med bara attributen i det funktionella beroendet:

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

och sedan en relation där vi tagit bort attributen i högerledet i beroendet (dvs F, vi kan ju slå upp den ur R1a med hjälp av D) – eftersom vi började detta steg med R1(B,C,D,F) får vi kvar:

R1b(B,C,D)
--------------
fds: B -> CD, C -> BD
keys: B, C

och båda dessa är i BCNF, eftersom samtliga deras funktionella beroenden har vänsterled som är supernycklar.

Vi får alltså:

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

Om vi i första steget ovan istället bryter upp på C -> BDF eller D -> F så får vi andra relationer, som även de fungerar utmärkt.

(e)

För att bygga upp R(A,B,C,D,E,F) från

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

kan vi joina ihop med hjälp av vänsterleden i de funktionella beroenden som vi använde när vi normaliserade:

SELECT A,B,C,D,E,F
FROM   R1b
       JOIN R1a USING (D)
       JOIN R2 USING (B)

Med andra nedbrytningar (beroende på vilka funktionella beroenden vi bröt upp på), så får vi naturligtvis andra JOIN-satser, men oavsett vilka dessa tabeller och JOIN-satser är, så kommer vi att kunna ta oss tillbaka till ursprungsrelationen utan att förstöra någon information.

Här är ett exempel där vi ursprungligen har en tabell R(A,B,C,D,E,F) med de beroenden vi såg ovan, och följande värden:

A  B  C  D  E  F
-  -  -  -  -  --
1  1  2  3  3  9
1  2  4  5  4  15
2  1  2  3  5  9
2  2  4  5  6  15
3  1  2  3  7  9
3  2  4  5  8  15

Observera att det inte är helt godtyckliga värden – de måste uppfylla de beroenden vi hade.

Med den uppdelning vi gjorde i uppgift 3d ovan kan vi skapa våra nya tabeller som:

CREATE TABLE R1 AS
  SELECT DISTINCT B, C, D, F
  FROM   R;

CREATE TABLE R2 AS
  SELECT DISTINCT A, B, E
  FROM   R;

CREATE TABLE R1a AS
  SELECT DISTINCT D, F
  FROM   R1;

CREATE TABLE R1b AS
  SELECT DISTINCT B, C, D
  FROM   R1;

vilket ger:

---------
R1a:
---------
D  F
-  --
3  9
5  15
---------
R1b:
---------
B  C  D
-  -  -
1  2  3
2  4  5
---------
R2:
---------
A  B  E
-  -  -
1  1  3
1  2  4
2  1  5
2  2  6
3  1  7
3  2  8

Och om vi nu kör

SELECT A,B,C,D,E,F
FROM   R1b
       JOIN R1a USING (D)
       JOIN R2 USING (B)

så får vi:

A  B  C  D  E  F
-  -  -  -  -  --
1  1  2  3  3  9
1  2  4  5  4  15
2  1  2  3  5  9
2  2  4  5  6  15
3  1  2  3  7  9
3  2  4  5  8  15

vilket alltså är precis samma värden som vi startade med.

Problem 4

För att begränsa antalet studenter i en grupp kan vi lägga in en trigger före insättning i group_memberships:

DROP TRIGGER IF EXISTS limit_groups;
CREATE TRIGGER limit_groups
BEFORE INSERT ON group_memberships
WHEN
  (SELECT count(stil_id)
   FROM   group_memberships
   WHERE  assignment_number = NEW.assignment_number AND
          group_number = NEW.group_number) > 3
BEGIN
  SELECT RAISE (ROLLBACK, "Too many group members");
END;

Problem 5

I Python kan vi skriva någonting i stil med:

import sqlite3


db = sqlite3.connect("course-database.sqlite")


def find_available_groups(assignment_number):
    c = db.cursor()
    c.execute(
        """
        SELECT    group_number
        FROM      group_memberships
        WHERE     assignment_number = ?
        GROUP BY  group_number
        HAVING    count() < 4
        """,
        [assignment_number]
    )
    return [group_number for group_number, in c]


def main():
    assignment_number = int(input("Assignment number: "))
    print(f"Looking for a group for assignment {assignment_number}")
    for group_number in find_available_groups(assignment_number):
        print(f"+ {group_number}")


main()

Eftersom vi redan har valt ut en specifik uppgift behöver vi inte gruppera på både uppgiftsnummer och gruppnummer, som vi normalt skulle göra i group_memberships (men det är helt OK om ni grupperar på båda!).

Problem 6

(a)

Två goda anledningar är:

  • Vi får färre joins
  • Vi kan behålla alla funktionella beroenden (med hjälp av 'constraints' på våra tabeller) – när vi bryter ner hela vägen till BCNF kan några funktionella beroenden 'försvinna' på vägen (se exemplet med bioföreställningarna på föreläsning 8).

(b)

De fyra bokstäverna betyder:

  • Atomicity: vi vill att ändringar antingen genomförs helt, eller inte alls – vi kan göra en rollback i en transaktion om vi vill återställa ändringar som vi inte vill göra.
  • Consistenty: gör att databasen alltid tas från ett korrekt tillstånd till ett annat korrekt tillstånd – vi får denna effekt av våra transaktioner, men egentligen tack vare atomicitet, isolation och durability, så C-et i ACID är egentligen bara en bieffekt av A, I och D.
  • Isolation: gör att vi kan köra våra uppdateringar isolerat, som om ingen annan hade tillgång till databasen – detta hanteras helt av transaktioner (och graden av isolering kan ställas in – vid SERIALIZABLE får vi fullständig isolation).
  • Durability: garanterar att en ändring kommer att finnas kvar i systemet när vår transaktion väl blivit 'committed'.