MAD Study Hub

Home / MAD1 / Week 4

Week 4 · Models: databases, SQL, ER diagrams and SQLAlchemy

Official syllabus line: Models - Introduction to databases
About 120 minQuiz 1 material (see exam radar)BSCS2003 (+ project BSCS2003P) · Thejesh G N, Nitin Chandrachoodan
Before you start: Python basics and classes (MAD1 week 1 and 2 ideas). Nothing about databases is assumed.

The one-minute story

Think of it like this

A database is a well-run school office. Student files are not thrown in one big heap on the floor. There is one register for students, one for courses and one for who joined which course. Each student has a roll number, so two students with the same name never get mixed up. A clerk (the DBMS) hands out files, stops two people from writing in the same register at once, and refuses a form with the roll number missing.

SQL is the language you use to talk to the clerk ("give me all students in MAD1, sorted by name"). SQLAlchemy is a translator: you speak Python objects (Student(name="Asha")) and it speaks SQL to the clerk for you.

This week is the Model in MVC: the data your app remembers.

By the end of this week you can:

  • draw a small ER diagram and turn it into tables with primary and foreign keys;
  • write SQL with WHERE, GROUP BY, HAVING, ORDER BY, LIMIT and joins, and predict the output of a query on a small table;
  • explain NULL, constraints, normalisation, transactions and ACID;
  • write SQLAlchemy and Flask-SQLAlchemy models and predict what query, filter_by, commit and rollback do;
  • fix classic ORM mistakes (forgetting commit, using and in a filter, positional constructor arguments).

Words you need

Words you need
  • DBMS - Database Management System: the software that stores, protects and queries data (SQLite, MySQL, PostgreSQL).
  • RDBMS - a DBMS that stores data in related tables (the relational model).
  • Table, row, column - a table is one kind of thing; a row (record, tuple) is one item; a column (field, attribute) is one property.
  • Primary key - the column (or columns) that identifies a row uniquely and is never NULL.
  • Foreign key - a column that holds the primary key of a row in another table; it creates the link.
  • Candidate key - any column set that could serve as the primary key; the one you choose is the primary key. A composite key is a key made of two or more columns.
  • Entity, attribute, relationship - the three parts of an ER diagram: a thing, its properties, and how things connect.
  • Cardinality - how many on each side of a relationship: one-to-one, one-to-many, many-to-many.
  • Constraint - a rule the database enforces: NOT NULL, UNIQUE, DEFAULT, CHECK, primary or foreign key.
  • Transaction - a group of steps that either all happen or none happen.
  • ORM - Object Relational Mapper: maps classes to tables, objects to rows, attributes to columns.
  • Session - the ORM's "shopping basket" of pending changes until you commit.

Idea 1: Why a database, and the relational model

Why not just keep data in a text file or a Python list? Because files give you no help with searching fast, sharing between many users, avoiding duplicates, protecting against half-finished writes, and access control. A DBMS gives all of these. (A question in the papers asks which database fits a university with enrolments, prerequisites, grades, fee payments and refunds: the answer is an RDBMS, because the data is structured, linked, and needs transactions.)

In the relational model all data lives in tables.

id (PK)nameemail
1Ashaa@x.in
2Ravir@x.in

Each row is one student, each column has one type, and the order of rows has no meaning.

The idea

Keys.

  • Primary key (PK): unique and never NULL. Example: student.id.
  • Candidate keys: all the columns that could be the PK (id, and also email if every email is unique).
  • Composite key: a PK made of two columns, such as (student_id, course_code) in an enrolment table.
  • Foreign key (FK): enrolment.student_id refers to student.id. The FK may repeat many times (one student, many enrolments) but must match an existing PK value (or be NULL).

A few more words the exam uses: redundancy (the same fact stored in many places) wastes space and causes mistakes, while replication (deliberate copies on different servers) protects against loss and helps reads. One real question gave "redundancy improves performance and reduces server load" and "replication ensures data is not lost if a copy is destroyed" as a pair, and the right call was that both statements were false as written, so read such statements slowly. An index is an extra lookup structure (like the index at the back of a book) whose job is to speed up searching; it does not encrypt or replicate anything.

Watch out - real exam trap
  • A foreign key is not unique by itself. The primary key is.
  • NULL is not a value. A primary key can never be NULL.
  • "Replication" and "redundancy" are not the same word. Replication is deliberate.

Idea 2: ER diagrams and turning them into tables

An ER (entity-relationship) diagram is a plan drawn before any table is made. Entities are rectangles (Student, Course), attributes are ovals or a list inside the box, and relationships are lines (often a diamond) between entities, labelled with a verb ("enrols in").

How to read it. Read the line from each side: "One channel has many videos. One video belongs to one channel." That is one-to-many. Check both directions before you answer.

CardinalityExampleHow to build tables
One-to-onePerson and passportPut an FK (marked unique) on either side, or merge the tables
One-to-manyChannel and videosPut the FK on the many side (video.channel_id)
Many-to-manyStudents and coursesA new link (association) table holding two FKs
Solved example: 1. From ER to tables

Question. A student can join many courses and a course has many students. Each enrolment also stores marks. Design the tables.

Solution. Many-to-many needs a third table. The link table's primary key is the pair of FKs (a composite key), and extra facts about the relationship (marks) go there.

CREATE TABLE student(
  id    INTEGER PRIMARY KEY,
  name  TEXT NOT NULL,
  email TEXT UNIQUE
);
CREATE TABLE course(
  code    TEXT PRIMARY KEY,
  title   TEXT NOT NULL,
  credits INTEGER DEFAULT 4
);
CREATE TABLE enrolment(
  student_id  INTEGER REFERENCES student(id),
  course_code TEXT    REFERENCES course(code),
  marks       INTEGER CHECK(marks BETWEEN 0 AND 100),
  PRIMARY KEY (student_id, course_code)
);

The composite key also stops the same student joining the same course twice.

How an examiner reads a model to find the relationship: if Enrollment has course_id = db.Column(db.Integer, db.ForeignKey("course.id")) and Course does not have a column pointing back, the FK is on the Enrollment side, so one course has many enrolments and each enrolment has one course: one-to-many. No link table, so not many-to-many.

In your project

Draw your project's ER diagram first (even on paper) and keep it in your report. The viva question is always "why is this table separate?" and "where is the foreign key and why on that side?". Name tables in a consistent style (student, not Students1).

Idea 3: SQL, the language of tables

Use SQLite (a database in a single file, built into Python). Statements end with ;. Keywords are case-insensitive, text values are in single quotes.

INSERT INTO student(name, email) VALUES ('Asha','a@x.in'), ('Ravi','r@x.in'), ('Meena','m@x.in');
INSERT INTO course(code, title) VALUES ('MAD1','App Dev 1'), ('MAD2','App Dev 2'), ('DBMS','Databases');
INSERT INTO enrolment VALUES (1,'MAD1',80), (1,'DBMS',60), (2,'MAD1',70);

SELECT name FROM student ORDER BY name DESC LIMIT 2;       -- Ravi, Meena
UPDATE enrolment SET marks = marks + 5 WHERE course_code = 'MAD1';
DELETE FROM enrolment WHERE marks < 70;

Run on those tables, SELECT name FROM student ORDER BY name DESC LIMIT 2 gives Ravi then Meena. After the update the marks are 85, 60, 75; after the delete (marks under 70) only the rows with 85 and 75 remain, so SELECT COUNT(*) FROM enrolment is 2.

The idea

The order SQL thinks in (very useful for predicting output):

  1. FROM (pick the table, do the joins)
  2. WHERE (drop rows, before grouping, cannot use aggregates)
  3. GROUP BY (make groups)
  4. HAVING (drop groups, after grouping, can use aggregates)
  5. SELECT (choose columns)
  6. ORDER BY, then LIMIT

Aggregate functions collapse many rows to one value: COUNT, SUM, AVG, MIN, MAX.

Solved example: 2. WHERE then GROUP BY then HAVING

Question. Table production(Manufacturer, Product, Quantity, Cost):

ManufacturerProductQuantityCost
Industry ABiscuits3100
Industry BShoes2500
Industry AChocolate10200
Industry CPen2050
Industry BShirts5700
Industry DBeverages30150
Industry CNotebook40250

What does this print?

SELECT Manufacturer, COUNT(*) FROM production
WHERE Quantity > 10
GROUP BY Manufacturer
HAVING COUNT(*) = 2;

Solution. Step 1: WHERE Quantity > 10 keeps Pen (20), Beverages (30), Notebook (40). Note that Chocolate (exactly 10) is not kept. Step 2: group them: Industry C has 2 rows (Pen, Notebook), Industry D has 1. Step 3: HAVING COUNT(*) = 2 keeps only C. Output: one row, Industry C | 2.

Run without the WHERE, the same query would give A, B and C (each has two rows), so the filter order matters.

Solved example: 3. GROUP BY with ORDER BY and an alias

Question. table_A(ID, First_Name, Last_Name, Course) has 9 students: Java 4, Python 3, C++ 2. What does this print?

SELECT Course, count(ID) AS No_Of_Students
FROM table_A GROUP BY Course
ORDER BY No_Of_Students ASC;

Solution. One output row per course, sorted by the count, smallest first: C++ 2, Python 3, Java 4. An alias given with AS can be used in ORDER BY. The wrong options in the real paper listed nine un-grouped rows or sorted largest first.

A SQLite quirk the papers use. With a table flowers(ID, Name, color) holding Jasmine (white), Rose, Lily, Lotus (all pink) and Red rose (red), the query SELECT count(ID), Name, color FROM flowers GROUP BY color HAVING count(color) < 2 returns 1 | Jasmine | white and 1 | Red rose | red (the pink group has 3 rows, so it is dropped). Name is not an aggregate nor in GROUP BY: other databases refuse this, SQLite quietly picks a value from the group (here the only one).

Joins

A join glues rows from two tables where a condition holds (normally FK equals PK).

SELECT s.name, e.course_code, e.marks
FROM student s JOIN enrolment e ON e.student_id = s.id
ORDER BY s.name, e.course_code;

Result: Asha DBMS 60, Asha MAD1 80, Ravi MAD1 70. Meena has no enrolment, so she disappears from an inner JOIN. A LEFT JOIN keeps every student: SELECT s.name, e.course_code FROM student s LEFT JOIN enrolment e ON e.student_id = s.id gives Asha twice, Ravi, and Meena | NULL. Forgetting the ON condition produces a cross join: 3 students times 3 courses is 9 rows.

NULL behaviour

NULL means "unknown", so almost every comparison with it is not true.

SELECT NULL = NULL;            -- NULL (not 1)
SELECT NULL IS NULL;           -- 1
SELECT 1 + NULL;               -- NULL

For a column x holding 1, NULL, 3: COUNT(*) is 3, COUNT(x) is 2 (NULLs are skipped), AVG(x) is 2.0, SUM(x) is 4, WHERE x <> 1 returns only the 3 (the NULL row is not returned), and you must write WHERE x IS NULL to find it.

Constraints

CREATE TABLE t(
  id   INTEGER PRIMARY KEY,
  name TEXT NOT NULL,
  age  INTEGER CHECK(age >= 0) DEFAULT 18
);
INSERT INTO t(name) VALUES ('Amit');   -- row (1, 'Amit', 18): DEFAULT filled age
INSERT INTO t(name, age) VALUES ('x', -1);  -- fails: CHECK constraint failed: age>=0
INSERT INTO t(age) VALUES (5);              -- fails: NOT NULL constraint failed: t.name

UNIQUE rejects a duplicate value (several NULLs are still allowed in SQLite). SQLite does not enforce foreign keys unless you run PRAGMA foreign_keys = ON; for each connection. That surprises people.

Your turn: 1. Predict the SQL output

Using the student, course, enrolment tables above (enrolments: (1,MAD1,80), (1,DBMS,60), (2,MAD1,70)), what does SELECT course_code, AVG(marks), COUNT(*) FROM enrolment GROUP BY course_code HAVING COUNT(*) > 1; print?

Need a hint?

Group by course first; count the rows in each group.

Show the full solution

MAD1 has two rows (80 and 70), DBMS has one. HAVING COUNT(*) > 1 keeps only MAD1. Output: MAD1 | 75.0 | 2.

Your turn: 2. Write the query

Write SQL to list the names of students who are enrolled in at least one course, without repeating a name.

Need a hint?

You need a join, and DISTINCT removes repeats.

Show the full solution

SELECT DISTINCT s.name FROM student s JOIN enrolment e ON e.student_id = s.id; gives Asha and Ravi (Meena has none; Asha is listed once even though she has two enrolments).

Idea 4: Normalisation and transactions (ACID)

Normalisation is the discipline of splitting tables to remove redundancy so each fact is stored once.

FormRule in plain words
1NFEvery cell holds a single value (no "MAD1, MAD2" in one cell); each row unique
2NFIn a table with a composite key, every other column depends on the whole key, not part of it
3NFColumns depend on the key, not on other non-key columns (store city with the pin code table, not in every student row)

Example of a violation: an orders table with customer_name and customer_phone copied on every order. Change the phone once and some rows keep the old one (an update anomaly). Fix: a customer table and an FK.

A transaction is a group of statements treated as one unit. The classic: transferring 500 rupees means "subtract from A" and "add to B". If the power fails in between, money vanishes. The database promises ACID:

The idea
  • Atomicity: all of the transaction happens or none of it.
  • Consistency: the data moves from one valid state to another (constraints still hold).
  • Isolation: parallel transactions do not see each other's half-done work.
  • Durability: once committed, the data survives a crash.

COMMIT makes the changes permanent; ROLLBACK undoes everything since the last commit.

Python's built-in sqlite3 module shows it clearly:

import sqlite3
con = sqlite3.connect("shop.db")      # creates the file if missing
cur = con.cursor()
cur.execute("CREATE TABLE IF NOT EXISTS acct(name TEXT PRIMARY KEY, bal INTEGER)")
cur.execute("INSERT OR REPLACE INTO acct VALUES (?, ?)", ("A", 1000))
con.commit()                          # saved
cur.execute("UPDATE acct SET bal = bal - 500 WHERE name = 'A'")
con.rollback()                        # undone
print(cur.execute("SELECT bal FROM acct WHERE name='A'").fetchall())   # [(1000,)]
con.close()

Always pass values with ? placeholders, never by pasting strings (that invites SQL injection).

Idea 5: SQLAlchemy and the ORM

Writing raw SQL strings inside Python gets messy. An ORM maps class to table, object to row, attribute to column. SQLAlchemy has these parts:

  • Engine: knows how to reach the database (create_engine("sqlite:///shop.db")).
  • Connection / Session: the Session holds your pending changes and talks to the engine. You add objects, then commit once.
  • Declarative base: the parent class of all your models.
from sqlalchemy import create_engine, Column, Integer, String, ForeignKey
from sqlalchemy.orm import declarative_base, sessionmaker, relationship

Base = declarative_base()

class Dept(Base):
    __tablename__ = "dept"
    id    = Column(Integer, primary_key=True)
    name  = Column(String, nullable=False)
    staff = relationship("Emp", back_populates="dept")

class Emp(Base):
    __tablename__ = "emp"
    id      = Column(Integer, primary_key=True)
    name    = Column(String)
    dept_id = Column(Integer, ForeignKey("dept.id"))
    dept    = relationship("Dept", back_populates="staff")

engine = create_engine("sqlite:///:memory:")
Base.metadata.create_all(engine)          # CREATE TABLE for every model
s = sessionmaker(bind=engine)()           # a Session

d = Dept(name="CS")
d.staff.append(Emp(name="Ria"))           # link through the relationship
s.add(d)
s.commit()

e = s.query(Emp).first()
print(e.name, e.dept.name, d.staff[0].name)   # Ria CS Ria

What happened: ForeignKey("dept.id") is the database-level link (a column). relationship(...) is the Python-level convenience: d.staff is a list of Emp, and e.dept is one Dept. back_populates says the two relationship attributes are two ends of the same link (use the same names on both sides). The older shortcut backref="dept" on one side creates the other side automatically. The commit wrote both rows and filled dept_id for you.

A model declaration must match the table: a question gave CREATE TABLE "Items" (ID INTEGER, name TEXT, cost INTEGER, PRIMARY KEY(ID AUTOINCREMENT)) and the correct class has __tablename__ = "Items", ID = Column(Integer, primary_key=True, autoincrement=True), name = Column(String), cost = Column(Integer). Same class name, table name and column names as the SQL.

Commit and rollback. s.add(Emp(name="Tom")); s.rollback() throws the pending Tom away, so s.query(Emp).count() is still 1.

Idea 6: Flask-SQLAlchemy and CRUD

Flask-SQLAlchemy wires SQLAlchemy into Flask so you write less setup. The names are different, so learn them as a set.

from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy import func

app = Flask(__name__)
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///shop.sqlite3"
db = SQLAlchemy(app)                      # or db = SQLAlchemy() then db.init_app(app)

class Author(db.Model):
    id    = db.Column(db.Integer, primary_key=True)
    name  = db.Column(db.String(80), nullable=False)
    books = db.relationship("Book", backref="author", lazy=True)

class Book(db.Model):
    id        = db.Column(db.Integer, primary_key=True)
    title     = db.Column(db.String(100), nullable=False, unique=True)
    price     = db.Column(db.Integer, default=100)
    author_id = db.Column(db.Integer, db.ForeignKey("author.id"))
    def __repr__(self): return f"<Book {self.title}>"

with app.app_context():                   # needed outside a request
    db.create_all()                       # creates missing tables only

Notes: the table name is the lower-case class name (Author becomes author), which is why ForeignKey("author.id") uses lower case. In Flask-SQLAlchemy 3 the models and db.session need an application context (inside a view you already have one; in a script use with app.app_context():). create_all() does not alter a table that already exists: after changing a model, delete the .sqlite3 file (or use migrations).

The idea

CRUD with the ORM (assume the context is active):

ActionCode
Createb = Book(title="SQL", author=a) then db.session.add(b) then db.session.commit()
Read allBook.query.all() (a list of objects)
Read oneBook.query.filter_by(title="SQL").first() (object or None), or db.session.get(Book, 1)
FilterBook.query.filter(Book.price > 150).all(), filter_by(price=100)
Sort / limitBook.query.order_by(Book.price.desc()).limit(1).all()
CountBook.query.filter_by(price=100).count()
Updatechange an attribute (b.price += 50), then db.session.commit()
Deletedb.session.delete(b) then db.session.commit()
Solved example: 4. Run a whole CRUD session

Question. What does each print? (Fresh database, the models above.)

with app.app_context():
    db.create_all()
    a = Author(name="Asha")
    b1 = Book(title="DB 101", price=300, author=a)
    b2 = Book(title="SQL", author=a)
    db.session.add_all([a, b1, b2]); db.session.commit()
    print(a.books)                        # [<Book DB 101>, <Book SQL>]
    print(b1.author.name, b2.price)       # Asha 100
    print(Book.query.filter(Book.price > 150).all())      # [<Book DB 101>]
    print(db.session.query(func.max(Book.price)).scalar())   # 300
    b = Book.query.filter_by(title="SQL").first()
    b.price += 50; db.session.commit()
    print(b.price)                        # 150
    db.session.delete(b); db.session.commit()
    print(Book.query.all())               # [<Book DB 101>]

Solution. b2 has no price, so the default=100 is used when the row is saved: the price is 100. func.max(...) with .scalar() returns the single number 300; without .scalar() you only print the SQL text SELECT max(book.price) ..., not the answer. That exact trap was in a real question: the right choice was print(db.session.query(func.max(ExamScores.score)).scalar()), and db.session.query.max(...) is invalid.

Constructing objects. SQLAlchemy's default constructor takes keyword arguments only. Book("a", 1, 1) raises TypeError: __init__() takes 1 positional argument but 4 were given. Use Book(title="a", price=1). The right order is create, add, then commit; commit before add saves nothing.

Violating a constraint. Adding a second book titled "SQL" and committing raises IntegrityError (the unique=True). The session is then in a failed state, so always call db.session.rollback() in the except block.

Solved example: 5. What does this update print?

Question. Table student with first names John (roll M01, last Doe), John (M02, Luther), Harry (M03, Doe). Code:

user1 = Student.query.filter_by(first_name="John").first()
user1.first_name = "Harry"
user1.last_name = "Luther"
db.session.commit()
for s in Student.query.all():
    print(s.first_name, s.last_name)

Solution. .first() returns only the first matching row (M01), not every John. It becomes Harry Luther. The other two rows are untouched. Output: Harry Luther, John Luther, Harry Doe. This matches the real-paper answer.

Your turn: 3. Update many rows

Table product(id, name, type, form, quantity). Write code to change every edible liquid to type perishable and add 450 to its quantity.

Need a hint?

You need two filters, a loop, and += (not =), and a commit.

Show the full solution
items = Product.query.filter_by(type="edible", form="liquid").all()
for item in items:
    item.type = "perishable"
    item.quantity += 450
db.session.commit()

Using = 450 would overwrite instead of add; filtering only on type would also change semi-solid items.

Your turn: 4. Days between two dates

A GlassRepair model has date columns broken_at and fixed_at. Write a list of (id, days) pairs.

Need a hint?

Subtracting two date objects gives a timedelta; take .days.

Show the full solution
repairs = GlassRepair.query.all()
result = [(r.id, (r.fixed_at - r.broken_at).days) for r in repairs]

The subtraction must be done in Python, on the objects. GlassRepair.query(GlassRepair.id, ...) is wrong because Model.query is not callable; a SQL-level fixed_at - broken_at > 7 does not work on date columns this way. For the filtered version, add if (r.fixed_at - r.broken_at).days > 7 inside the list comprehension.

Combining conditions

Order.query.filter(Order.status == "PAID").filter(Order.amount > 500).all()   # correct
Order.query.filter(Order.status == "PAID", Order.amount > 500).all()          # correct, same
from sqlalchemy import and_, or_
Order.query.filter(and_(Order.status == "PAID", Order.amount > 500)).all()    # correct
Order.query.filter(Order.status == "PAID" and Order.amount > 500)             # TypeError!

The last one uses Python's and on two SQL expressions, which fails with TypeError: Boolean value of this clause is not defined. Use , / chained .filter / and_ / & (with brackets). filter_by takes keyword equality only (filter_by(status="PAID")); filter takes expressions (Order.amount > 500).

Watch out - real exam trap
  • first() gives one object; all() gives a list; get or first can give None.
  • No commit() means no permanent change (and another connection will not see it).
  • print(first_name) inside the loop instead of emp1.first_name is a NameError: the real paper's answer was "None of these" for exactly this reason. Read the print line.
  • Model.query is an attribute, not a function: Book.query(...) is wrong.
  • and / or / not do not work on SQLAlchemy expressions.
  • Table name is lower case: ForeignKey("author.id"), not "Author.id".
In your project

In the viva you will be asked "show me your model file and explain each relationship". Know which side has the ForeignKey, what backref or back_populates gives you, and what happens to children when a parent is deleted. Keep db.create_all() in one obvious place and mention that you do not use migrations.

Exam radar

Exam radar

The evidence file counts graded questions whose text matches week 4 topics (keyword match, so these are approximate): about 52 of 692 in Quiz 1, about 121 of 624 in Quiz 2 and about 209 of 1600 in End Term. This is one of the heaviest topics of Quiz 2, with about 108 distinct questions in our sample.

What the sample asks, most common first:

  • Predict the output of a SQL query on a small table (GROUP BY, HAVING, COUNT, ORDER BY, aliases), about 4 or 5 of the sampled questions.
  • Predict what a Flask-SQLAlchemy snippet prints after filter_by(...).first() and commit() (changing one row, many rows), also about 4 or 5.
  • Pick the correct model class or query: Column(...), nullable=False, primary_key=True, func.max(...).scalar(), filter with two conditions, subtracting dates.
  • Name the relationship from an ER picture or from ForeignKey lines (one-to-many came up repeatedly).
  • Theory: which DBMS fits a requirement (an RDBMS for enrolments and fee payments), what an index does, what redundancy and replication mean.
  • The correct insert sequence is create_all(), build the object with keyword arguments, db.session.add(obj), then db.session.commit().
Watch out - real exam trap
  • A real wrong option repeated "an unrelated row is also changed"; the right answer touched only the first match.
  • Wrong options often swap the order commit then add, or use positional arguments in the constructor.
  • COUNT(*) after HAVING removes whole groups; WHERE removes rows before the groups form.
  • item.quantity = 450 versus += 450.

Quick quiz

Quick quiz

The table t has x values 1, NULL, 3. What is SELECT COUNT(x) FROM t?

  • 3
  • 2
  • 1
  • NULL
COUNT(column) skips NULLs; COUNT(*) would give 3.

In SQL, which clause may use an aggregate such as COUNT(*) to filter groups?

  • WHERE
  • ORDER BY only
  • HAVING
  • LIMIT
WHERE runs before grouping; HAVING runs after, on each group.

Which of these correctly prints the highest score using Flask-SQLAlchemy?

  • print(db.session.query.max(ExamScores.score))
  • print(db.session.query(func.max(ExamScores.score)).scalar())
  • print(db.session.query(func.max(ExamScores.score)))
  • print(ExamScores.query.max())
scalar() returns the single value; without it you print the query text.

Enrollment has course_id = db.Column(db.Integer, db.ForeignKey("course.id")) and Course has no such column. The relationship between Course and Enrollment is...

  • many-to-many
  • one-to-many
  • one-to-one
  • unrelated
The FK sits on the many side (Enrollment); one course has many enrolments.

Select all statements that are TRUE. (select all that apply)

  • A composite primary key is made of two or more columns.
  • db.session.rollback() discards uncommitted changes.
  • A foreign key must be unique in its own table.
  • Model.query.first() returns a list.
An FK can repeat; first() returns one object or None.

What happens with Book.query.filter(Book.price > 50 and Book.title == "SQL")?

  • It returns books satisfying both conditions
  • It returns all books
  • It raises a TypeError
  • It returns an empty list
Python and cannot combine SQLAlchemy expressions; use ,, and_ or chained filters.

Viva: say it out loud

Viva: say it out loud
Why did you use a database instead of a file?

A database gives fast searching, safe writes (transactions), constraints that reject bad data and many users at once. A file gives none of these. SQLite is enough for my project because it is a single file with no server to run.

Explain the relationships in your ER diagram.

I will read each line both ways: one user has many orders, and each order belongs to one user, so it is one-to-many and the foreign key user_id sits in the order table. Students and courses are many-to-many, so I made a link table holding both foreign keys.

What is the difference between a primary key, a foreign key and a candidate key?

A primary key uniquely identifies a row and is never NULL. A foreign key stores another table's primary key to link rows. A candidate key is any column set that could have been the primary key, for example email if it is unique.

What is an ORM and why use SQLAlchemy?

An ORM maps classes to tables and objects to rows, so I write Python (Book.query.filter_by(...)) instead of raw SQL strings. It also gives some protection from SQL injection and makes the code easier to change if the database changes.

What is the difference between add, commit and rollback?

add places a new object in the session. commit writes all pending changes permanently in one transaction. rollback throws the pending changes away, which I use after an error such as a unique-constraint violation.

What does db.create_all() do and what does it not do?

It creates tables for all models that do not exist yet. It does not change existing tables, so after editing a model I delete the database file or I would need a migration tool.

What is ACID?

Atomicity (all or nothing), Consistency (rules still hold), Isolation (parallel work does not interfere) and Durability (committed data survives a crash). A money transfer is my example.

Show me how you would change a student's email live.

s = Student.query.filter_by(id=1).first(); s.email = "new@x.in"; db.session.commit(). I would check for None first and handle a duplicate email with try, rollback.

Recap

Remember this
  • A DBMS stores structured data safely; the relational model uses tables with PK, FK, candidate and composite keys.
  • ER diagram: entities, attributes, relationships. One-to-many puts the FK on the many side; many-to-many needs a link table.
  • SQL order of thought: FROM, WHERE, GROUP BY, HAVING, SELECT, ORDER BY, LIMIT. WHERE filters rows, HAVING filters groups.
  • NULL is not equal to anything (use IS NULL); COUNT(col) skips NULLs.
  • Normalisation stores each fact once; ACID describes safe transactions.
  • SQLAlchemy: engine, session, declarative models, relationship plus ForeignKey. Flask-SQLAlchemy: db.Model, db.session, Model.query, db.create_all(), SQLALCHEMY_DATABASE_URI.
  • CRUD: add/commit, query.all/first/filter_by, edit an attribute then commit, delete then commit. Use keyword arguments in constructors.
Code and facts to keep
  • CREATE TABLE t(id INTEGER PRIMARY KEY, name TEXT NOT NULL UNIQUE, n INTEGER DEFAULT 0 CHECK(n>=0));
  • SELECT cols FROM t JOIN u ON t.id=u.t_id WHERE ... GROUP BY ... HAVING ... ORDER BY ... LIMIT n;
  • Aggregates: COUNT SUM AVG MIN MAX; NULL test: IS NULL
  • SQLite file URI: sqlite:///name.sqlite3; memory: sqlite:///:memory:
  • db = SQLAlchemy(app); models extend db.Model; db.Column(db.String(80), nullable=False, unique=True)
  • db.relationship("Child", backref="parent") plus db.ForeignKey("parent.id") on the child
  • db.session.add(obj), db.session.commit(), db.session.rollback(), db.session.delete(obj)
  • Model.query.all() / .first() / .filter_by(a=1) / .filter(Model.a > 1) / .order_by(...) / .count()
  • db.session.query(func.max(M.col)).scalar()
  • with app.app_context(): db.create_all()
Connected weeks (learn them together)

Practise with real exam questions

These come straight from past papers (with the real figures and the official answer key). Open 60 real questions on this week   All Quiz 1 papers