Library Management ER Diagram

A library schema separates the catalog from circulation: books and authors describe what the collection contains, while members and loans describe who has what and when it is due. Because a book can have several authors, book_authors is a join table, and fines hang off individual loans rather than off members.

The catalog separates titles from physical copies even though this SQL keeps a single books table with a total_copies counter. A title owns an isbn, a category_id and bibliographic data; a copy has a barcode, a shelf and a condition, none of which belong to the title. If the library lends individual objects, the next step is a book_copies table where each row is one physical item, and loans.book_id becomes loans.copy_id. book_authors already captures the relationship the catalog cannot: an anthology has many authors, an author writes many books, and the composite primary key (book_id, author_id) prevents duplicate attribution.

Reading the diagram, categories to books and authors to book_authors are ordinary one-to-many edges; book_authors is the only many-to-many and appears as a small table between the two catalogs. Circulation is a short chain: members borrow through loans, and each loan points at exactly one book with borrowed_at, due_date and a nullable returned_at. fines.loan_id is UNIQUE, so a loan carries at most one fine row, while the fine itself can be settled in place through paid_at. That uniqueness is what makes the loan the unit of accountability rather than the member.

Real circulation breaks naive schemas in two places. First, deleting a book or member with loan history must fail: both foreign keys on loans have no cascade, so the rows survive as inventory and financial records. Deleting a loan, however, cascades to its fine, since a fine without its loan is meaningless. Second, nothing prevents a member from holding two copies of the same title at once — which may be a policy choice, not a bug — but the more serious gap is that loans does not verify returned_at is after borrowed_at; a CHECK constraint belongs there.

Three additions cover most library requirements. A reservations table (book_id, member_id, requested_at, expires_at) handles holds, can be ordered by requested_at when demand exceeds supply, and converts into a loan when a copy returns. A book_copies table, keyed under books with barcode UNIQUE, turns total_copies into a derived count and lets the library track damaged or lost items individually. Finally, a categories.parent_id self-reference mirrors the tree pattern from the e-commerce schema, so a broad shelf like Science can contain Physics and Biology without flattening the hierarchy. That keeps the first year of operation from forcing a rewrite.

The diagram

1:N 1:N 1:N 1:1 N:M categories PKid integer Uname varchar(80) authors PKid integer full_name varchar(120) country varchar(60) books PKid integer FKcategory_id integer Uisbn varchar(20) title varchar(200) published_year integer total_copies integer book_authors PKbook_id integer PKauthor_id integer members PKid integer Uemail varchar(255) full_name varchar(120) joined_at date status varchar(20) loans PKid integer FKbook_id integer FKmember_id integer borrowed_at timestamp due_date date returned_at timestamp fines PKid integer FKloan_id integer amount decimal(8,2) reason varchar(120) issued_at timestamp paid_at timestamp

Tables (7)

categories

  • idintegerPK
  • namevarchar(80)U

authors

  • idintegerPK
  • full_namevarchar(120)
  • countryvarchar(60)

books

  • idintegerPK
  • category_idintegerFK
  • isbnvarchar(20)U
  • titlevarchar(200)
  • published_yearinteger
  • total_copiesinteger

book_authors

  • book_idintegerPK
  • author_idintegerPK

members

  • idintegerPK
  • emailvarchar(255)U
  • full_namevarchar(120)
  • joined_atdate
  • statusvarchar(20)

loans

  • idintegerPK
  • book_idintegerFK
  • member_idintegerFK
  • borrowed_attimestamp
  • due_datedate
  • returned_attimestamp

fines

  • idintegerPK
  • loan_idintegerFKU
  • amountdecimal(8,2)
  • reasonvarchar(120)
  • issued_attimestamp
  • paid_attimestamp

Relationships (5)

  • 1:Ncategories.id→books.category_id
  • 1:Nbooks.id→loans.book_id
  • 1:Nmembers.id→loans.member_id
  • 1:1loans.id→fines.loan_idON DELETE CASCADE
  • N:Mbooks.id→authors.id

The SQL

Copy this, paste it in the SQL tab, or just open it below.
SQL
CREATE TABLE categories (
  id SERIAL PRIMARY KEY,
  name VARCHAR(80) NOT NULL UNIQUE
);

CREATE TABLE authors (
  id SERIAL PRIMARY KEY,
  full_name VARCHAR(120) NOT NULL,
  country VARCHAR(60)
);

CREATE TABLE books (
  id SERIAL PRIMARY KEY,
  category_id INTEGER REFERENCES categories(id),
  isbn VARCHAR(20) NOT NULL UNIQUE,
  title VARCHAR(200) NOT NULL,
  published_year INTEGER,
  total_copies INTEGER NOT NULL DEFAULT 1
);

CREATE TABLE book_authors (
  book_id INTEGER NOT NULL REFERENCES books(id) ON DELETE CASCADE,
  author_id INTEGER NOT NULL REFERENCES authors(id) ON DELETE CASCADE,
  PRIMARY KEY (book_id, author_id)
);

CREATE TABLE members (
  id SERIAL PRIMARY KEY,
  email VARCHAR(255) NOT NULL UNIQUE,
  full_name VARCHAR(120) NOT NULL,
  joined_at DATE NOT NULL DEFAULT CURRENT_DATE,
  status VARCHAR(20) NOT NULL DEFAULT 'active'
);

CREATE TABLE loans (
  id SERIAL PRIMARY KEY,
  book_id INTEGER NOT NULL REFERENCES books(id),
  member_id INTEGER NOT NULL REFERENCES members(id),
  borrowed_at TIMESTAMP NOT NULL DEFAULT now(),
  due_date DATE NOT NULL,
  returned_at TIMESTAMP
);

CREATE TABLE fines (
  id SERIAL PRIMARY KEY,
  loan_id INTEGER NOT NULL UNIQUE REFERENCES loans(id) ON DELETE CASCADE,
  amount DECIMAL(8,2) NOT NULL CHECK (amount > 0),
  reason VARCHAR(120) NOT NULL,
  issued_at TIMESTAMP NOT NULL DEFAULT now(),
  paid_at TIMESTAMP
);

Keep learning