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 starttid LIKE '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 (isbn) WHERE classification LIKE '8%' GROUP BY publisher_id HAVING count(isbn) >= 10
Problem 3
(a)
Vi hade:
actor_id
actor_name
movie_id
movie_role
category
year
och får följande funktionella beroenden:
actor_id -> actor_name
actor_id movie_id -> movie_role
movie_id movie_role -> actor_id
category year -> movie_id movie_role
movie_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 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:
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 iD -> EH
, som vi bröt uppR2
med.- Vi kan sedan joina in
R1(D,E,H)
med hjälp av vänsterledet iD -> EH
, som vi bröt uppR
med i det första steget.
Så får vi:
SELECT * FROM R2b JOIN R2a USING (D) 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.