EDAF75 - lösningsförslag till tentamen i mars 2024
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:
(b och c)
DROP TABLE IF EXISTS evenemang;
CREATE TABLE evenemang (
evenemangsid TEXT DEFAULT (lower(hex(randomblob(16)))),
starttid DATETIME,
beskrivning TEXT,
PRIMARY KEY (evenemangsid)
);
DROP TABLE IF EXISTS tipssidor;
CREATE TABLE tipssidor (
tipssida TEXT,
url TEXT,
senast_granskad BOOLEAN,
PRIMARY KEY (tipssida)
);
DROP TABLE IF EXISTS evenemangstips;
CREATE TABLE evenemangstips (
evenemangsid TEXT,
tipssida TEXT,
PRIMARY KEY (evenemangsid, tipssida),
FOREIGN KEY (evenemangsid) REFERENCES evenemang(evenemangsid),
FOREIGN KEY (tipssida) REFERENCES tipssidor(tipssida)
);
DROP TABLE IF EXISTS kategorier;
CREATE TABLE kategorier (
kategorinamn TEXT,
PRIMARY KEY (kategorinamn)
);
DROP TABLE IF EXISTS kategoriseringar;
CREATE TABLE kategoriseringar (
evenemangsid TEXT,
kategorinamn TEXT,
PRIMARY KEY (evenemangsid, kategorinamn),
FOREIGN KEY (evenemangsid) REFERENCES evenemang(evenemangsid),
FOREIGN KEY (kategorinamn) REFERENCES kategorier(kategorinamn)
);
DROP TABLE IF EXISTS familjemedlemmar;
CREATE TABLE familjemedlemmar (
namn TEXT,
PRIMARY KEY (namn)
);
DROP TABLE IF EXISTS intressen;
CREATE TABLE intressen (
namn TEXT,
kategorinamn TEXT,
PRIMARY KEY (namn, kategorinamn),
FOREIGN KEY (namn) REFERENCES familjemedlemmar(namn),
FOREIGN KEY (kategorinamn) REFERENCES kategori(kategorinamn)
);
DROP TABLE IF EXISTS informationssidor;
CREATE TABLE informationssidor (
url TEXT,
evenemangsid TEXT,
PRIMARY KEY (url),
FOREIGN KEY (evenemangsid) REFERENCES evenemang(evenemangsid)
);
DROP TABLE IF EXISTS recensioner;
CREATE TABLE recensioner (
evenemangsid TEXT,
namn TEXT,
recensionstext TEXT,
PRIMARY KEY (evenemangsid, namn),
FOREIGN KEY (evenemangsid) REFERENCES evenemang(evenemangsid),
FOREIGN KEY (namn) REFERENCES familjemedlemmar(namn)
);
-- Utgående från dina tabeller i uppgift b, skriv en SQL-sats som
-- skriver ut URL-en för alla tips- sidor från vilka vi lagt in tips
-- om innebandymatcher under 2024 (du kan använda funktionen year()
-- för att hämta årtalet för en DATETIME).
SELECT
url
FROM
evenemang
JOIN kategoriseringar USING (evenemangsid)
JOIN evenemangstips USING (evenemangsid)
JOIN tipssidor USING (tipssida)
WHERE
kategorinamn = 'innebandy'
AND year(starttid) = 2024;
Problem 2
(a)
Ett sätt att rita detta ER-diagram är:
(b)
Skriv ut titel och förlag för de böcker som är utlånade för tillfället (sorterat efter titel).
SELECT title, publisher_name
FROM books
JOIN loans USING (isbn)
JOIN publishers USING (publisher_id)
WHERE actual_return_date IS NULL
ORDER BY title
(c)
Ta bort alla böcker som aldrig varit utlånade.
DELETE
FROM books
WHERE isbn NOT IN (
SELECT isbn
FROM loans)
(d)
Skriv ut namnet på de låntagare som aldrig lånat en skönlitterär bok (skönlitterära böcker har en klassificering som börjar på 8).
Enklast är nog att använda en sub-query:
SELECT name
FROM borrowers
WHERE card_nbr NOT IN (
SELECT card_nbr, true
FROM loans
JOIN books USING (isbn)
WHERE classification LIKE '8%')
Vi kan även göra det med hjälp av en outer join, men måste då trixa lite för att få ett extra attribut som vi kan testa på efteråt:
WITH
readers_of_fiction(card_nbr, found) AS (
SELECT DISTINCT card_nbr, true
FROM loans
JOIN books USING (isbn)
WHERE classification LIKE '8%'
)
SELECT name
FROM borrowers
LEFT JOIN readers_of_fiction USING (card_nbr)
WHERE found IS NULL
(e)
Skriv ut namnen på de låntagare som vid något tillfälle har lånat någon bok med fransk poesi som är publicerad av förlaget med namnet ”ACME böcker” – texten ”Fransk poesi” finns som en beskrivning (description) i tabellen classifications.
SELECT name
FROM borrowers
WHERE card_nbr IN (
SELECT card_nbr
FROM loans
JOIN books USING (isbn)
JOIN classifications USING (classification)
JOIN publishers USING (publisher_id)
WHERE description = 'Fransk poesi' AND
publisher_name = 'ACME böcker')
(f)
Skriv för de olika förlagen ut hur många av deras skönlitterära böcker biblioteket har i sin databas – vi vill dock bara ha med förlag med minst 10 sådana böcker (skönlitterära böcker har en klassificering som börjar på 8).
SELECT publisher_name, count(isbn)
FROM books
JOIN publishers USING (publisher_id)
WHERE classification LIKE '8%'
GROUP BY publisher_id
HAVING count(isbn) >= 10
Problem 3
(a)
Vi hade:
actor_idactor_namemovie_idmovie_rolecategoryyear
och får följande funktionella beroenden:
actor_id -> actor_nameactor_id movie_id -> movie_rolemovie_id movie_role -> actor_idcategory year -> movie_id movie_rolemovie_id -> year
Man behöver inte komma på alla dessa för full poäng, man får en poäng för varje, och maximalt fyra poäng på uppgiften.
(b)
Vi har alltså:
R(A,B,C,D,E,F,G,H)
och:
- FD1:
ADF -> C - FD2:
D -> EH - FD3:
F -> BG
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:
ADF -> BCEGH - FD2:
D -> EH - FD3:
F -> BG
Det visar sig alltså direkt att ADF är en nyckel (eftersom vi får alla övriga attribut), samtidigt kan vi konstatera att vi aldrig kan härleda någon av attributen A, D eller F, så de måste alla ingå i en nyckel.
Detta innebär att ADF är den enda möjliga nyckeln.
(c)
Vi är inte i BCNF, eftersom FD2 och FD3 har vänsterled som inte är supernycklar (däremot är FD1 OK, ADF är en supernyckel).
(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,G,H) och de funktionella beroenden som vi såg ovan – för att göra saker enklare kan vi direkt använda de transitiva höljen vi beräknade ovan.
- FD1:
ADF -> BCEGH - FD2:
D -> EH - FD3:
F -> BG
Vår enda nyckel är ADF, och vi kan sammanställa det som:
R(A,B,C,D,E,F,G,H) -------------- fds: ADF -> BCEGH, D -> EH, F -> BG keys: ADF
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 D -> EH, 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(D,E,H), och - en tabell med allt utom högerledet i det valda funktionella beroendet:
R2(A,B,C,D,F,G)– 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 tabellenR1.
I tabellen R1 har vi kvar beroendet D -> EH, men de andra två beroenden vi hade i R 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(D,E,H) -------------- fds: D -> EH keys: D
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å D -> EH försvinner). För ADF -> BCEGH 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 ADF -> BCG (plus F -> BG, som inte påverkas av att vi brutit ut E och H):
R2(A,B,C,D,F,G) -------------- fds: ADF -> BCG, F -> BG keys: ADF
Uppdelningarna kan vi göra med med dessa SQL-satser (detta behövde ni inte göra på tentan denna gång):
CREATE TABLE R1 AS SELECT DISTINCT D,E,H FROM R; CREATE INDEX r1key ON R1a(D); CREATE TABLE R2 AS SELECT DISTINCT A,B,C,D,F,G FROM R; CREATE INDEX r2key ON R2(A,D,F);
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).
Efter våra uppdelningar ovan är R1 redan i BCNF, eftersom det enda funktionella beroendet har ett vänsterled som är en supernyckel, men vi måste jobba vidare med R2, som ju har beroendet F -> BG, vars vänsterled inte är en supernyckel.
Vi bryter därför ut F -> BG till en ny relation R2a(F,B,G) och kvar från R2 har vi då samma symboler som tidigare, utom högerledet i F -> BG, dvs R2b(A,C,D,F):
R2a(F,B,G) -------------- fds: F -> BG keys: F
och
R2b(A,C,D,F) -------------- fds: ADF -> C keys: ADF
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 R2a AS SELECT DISTINCT F,B,G FROM R2; CREATE INDEX r2aKey ON R2a(F); CREATE TABLE R2b AS SELECT DISTINCT A,C,D,F FROM R2; CREATE INDEX r2bKey ON R2b(A,D,F);
Så, vi kan alltså bryta upp i de tre relationerna:
R1(D,E,H)R2a(F,B,G)R2b(A,C,D,F)
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).
(e)
Vi kan nu enkelt bygga tillbaka ursprungsrelationen R – om vi använder uppdelningen ovan:
R2b(A,C,D,F)kan vi först joina ihop medR2a(F,B,G)med hjälp av vänsterledet iF -> BG, som vi bröt uppR2med.- Vi kan sedan joina in
R1(D,E,H)med hjälp av vänsterledet iD -> EH, som vi bröt uppRmed i det första steget.
Så vi får:
SELECT *
FROM R2b
JOIN R2a USING (F)
JOIN R1 USING (D)
Problem 4
DROP TRIGGER IF EXISTS limit_loans; CREATE TRIGGER limit_loans BEFORE INSERT ON loans WHEN (SELECT count() FROM loans WHERE card_nbr = NEW.card_nbr ) >= 10 BEGIN SELECT RAISE (ROLLBACK, "Too many loans"); END;
Problem 5
Skriv ett Python- eller Javaprogram som med databasen i uppgift 2 löser nedanstående uppgift.
Vi antar att vi i databasen har triggern från uppgift 4, oavsett om du löst uppgift 4 eller inte, och att vi definierat tabellen loans så att borrow_date och latest_return_date får rätt startvärden automatiskt.
Läs in ett lånekortnummer och en ISBN-kod, och notera i databasen att låntagaren med det givna lånekortnumret lånar boken med det givna ISBN-numret. Om låntagaren redan har 10 utestående lån skall lånet inte genomföras, och programmet skriva ut att ett nytt lån skulle leda till för många utestående lån. Skriv därefter ut samtliga utestående lån för den aktuelle låntagaren, och visa sista returdatum för varje lån (utskriften skall göras oavsett om lånet kunde genomföras eller ej).
import sqlite3
db = sqlite3.connect("library.sqlite")
def borrow_book(card_nbr, isbn):
c = db.cursor()
try:
c.execute(
"""
INSERT
INTO loans(card_nbr, isbn)
VALUES (?, ?)
""",
[card_nbr, isbn]
)
db.commit()
except SQLException as e:
print(f"Note: {str(e)}")
print("Currently borrowed books")
c.execute(
"""
SELECT isbn, title, latest_return_date
FROM loans
JOIN books USING (isbn)
WHERE card_nbr = ?
AND actual_return_date IS NULL
""",
[card_nbr]
)
for isbn, title, latest_return_date in c:
print(f"{isbn}: {latest_return_date}: {title}")
def main():
card_nbr = input('Card number: ')
isbn = input('ISBN: ')
borrow_book(card_nbr, isbn)
if __name__ == "__main__":
main()
Problem 6
Se kurshemsidorna.