Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Database Management Systems (DBMS)  Module 1 – Foundations: what a DBMS does and how it works

What a DBMS does and why files are not enough

What a database management system (DBMS) adds to plain files: integrity, concurrency, recovery and security, with a lost update and a crash reproduced.

  • Beginner
  • 30 minutes
  • Examples run with sql.js 1.14.2 and Python 3.14.8
  • By MySmartCoPilot
Free: 30 minutes

What you will learn

  • Explain the problems a DBMS solves that ad-hoc files cannot, from integrity and concurrency to recovery and security
  • Describe the three-schema architecture and the difference between logical and physical data independence
  • Classify statements as DDL, DML, DCL or TCL and name the people who use a database system
  • Compare relational, document, key-value, wide-column, graph and vector data models at a high level
  • Query a database's catalog to read its schema as data
What you’ll learn

What you will learn

  • Explain the problems a DBMS solves that ad-hoc files cannot, from integrity and concurrency to recovery and security
  • Describe the three-schema architecture and the difference between logical and physical data independence
  • Classify statements as DDL, DML, DCL or TCL and name the people who use a database system
  • Compare relational, document, key-value, wide-column, graph and vector data models at a high level
  • Query a database's catalog to read its schema as data
On this page

A database management system (DBMS) is the software that keeps a collection of data and handles every request to read or change it. Programs do not open the data files themselves: they send requests, usually in SQL, and the DBMS decides how to carry each one out. Because every read and write passes through it, the DBMS can do four jobs that plain files leave to every program that touches them. It checks the rules the data must obey (integrity). It stops two writers from undoing each other’s work (concurrency control). It brings the data back to a consistent state after a crash (recovery). And it decides who may see or change what (security). It also keeps a description of the data, the schema, so that a program can ask a question without knowing how the data is laid out on disk.

This lesson reproduces two of the failures that plain files suffer, shows what a DBMS does instead, and then covers the vocabulary every database course and interview uses: schemas and the catalog, the three-schema architecture and data independence, the classes of SQL statements, the people who work with a database, and the main data models.

Five ways plain files let you down

Picture a college that keeps its records in files: a spreadsheet of students in the office, a CSV file of hostel rooms on the warden’s computer, and a JSON file of marks that a small web app reads and rewrites. That works while one person uses each file. Once the data is shared, the same five problems appear again and again:

  • Redundant, inconsistent copies. A student’s phone number sits in the office spreadsheet and in the hostel file. A change made in one copy never reaches the other, and nobody can tell which copy is right.
  • Rules buried in code. The web app refuses a mark above 100, but the script that imports marks from the exam portal checks nothing, so a mark of 140 gets in through the script.
  • Lost updates. Two people change the same file at about the same time. Each starts from the old version, and whoever saves last silently undoes the other’s change.
  • Half-written files. A program dies, or the power fails, while it is rewriting a file, and leaves something that is neither the old version nor the new one.
  • All-or-nothing security. The operating system grants access to a whole file. It cannot let the warden read room numbers but not marks when both live in the same file.

A DBMS answers each problem with a service of its own:

Problem with files What the DBMS provides How, in one line
Redundant, inconsistent copies One shared database Each fact is stored once, and every program reads that one copy
Rules buried in code Integrity constraints The schema states the rules and every write is checked against them
Lost updates Concurrency control Transactions keep simultaneous writers from overwriting each other
Half-written files Recovery A journal or log lets the DBMS undo or finish interrupted changes
All-or-nothing security Access control Privileges on tables, columns and views, granted to users and roles

The next four sections show four of these at work, with programs whose output you can check line by line.

Two writers, one lost update

The program below has two writers, A and B, and each adds 1 to a shared counter. Real writers run at the same time, so their steps can interleave in many orders. The program fixes one unlucky order, so that every run gives the same result: both writers read the counter before either writes it back.

Two writers, one counter Python · lost_update.py
"""Two writers each add 1 to a shared counter, with their steps interleaved in a fixed order."""
import json
import sqlite3

# 1. The counter in a JSON file: each writer reads the file, adds 1 and writes it back.
with open("counter.json", "w") as f:
    json.dump({"n": 0}, f)


def read_file():
    with open("counter.json") as f:
        return json.load(f)["n"]


def write_file(n):
    with open("counter.json", "w") as f:
        json.dump({"n": n}, f)


a = read_file()      # writer A reads 0
b = read_file()      # writer B reads 0, before A has written anything
write_file(a + 1)    # A writes 1
write_file(b + 1)    # B writes 1 as well: A's increment is gone
print("JSON file, read then write:     n =", read_file())

# 2. The same two steps against SQLite: a SELECT, then an UPDATE with the value Python computed.
setup = sqlite3.connect("counter.db", autocommit=True)
setup.execute("CREATE TABLE counter (n INTEGER NOT NULL)")
setup.execute("INSERT INTO counter VALUES (0)")
A = sqlite3.connect("counter.db", autocommit=True, timeout=0)
B = sqlite3.connect("counter.db", autocommit=True, timeout=0)
READ = "SELECT n FROM counter"
a = A.execute(READ).fetchone()[0]
b = B.execute(READ).fetchone()[0]
A.execute("UPDATE counter SET n = ?", (a + 1,))
B.execute("UPDATE counter SET n = ?", (b + 1,))
print("SQLite, read then write:        n =", setup.execute(READ).fetchone()[0])

# 3. The database does the arithmetic, inside a write transaction.
setup.execute("UPDATE counter SET n = 0")
A.execute("BEGIN IMMEDIATE")              # A starts a write transaction
A.execute("UPDATE counter SET n = n + 1")
try:
    B.execute("BEGIN IMMEDIATE")          # B asks to write while A's transaction is open
except sqlite3.OperationalError as e:
    print("B asks to write while A is busy:", e)
A.execute("COMMIT")
B.execute("BEGIN IMMEDIATE")              # now it is B's turn
B.execute("UPDATE counter SET n = n + 1")
B.execute("COMMIT")
print("SQLite, UPDATE ... n = n + 1:   n =", setup.execute(READ).fetchone()[0])

Output

JSON file, read then write:     n = 1
SQLite, read then write:        n = 1
B asks to write while A is busy: database is locked
SQLite, UPDATE ... n = n + 1:   n = 2

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 lost_update.py

Read the three results in turn:

  1. The JSON file ends at 1, not 2. B computed its new value from a stale read, so A’s increment vanished. No error was raised; the data is simply wrong.
  2. SQLite ends at 1 as well. The program read the value with one statement, added 1 in Python and wrote the result with a second statement. Each statement was correct on its own; the bug lives in the gap between them. A DBMS gives you the tools against lost updates, but a program that reads, computes and writes in separate steps, outside a transaction, still loses them.
  3. UPDATE counter SET n = n + 1 ends at 2. The read and the write are now one statement that SQLite runs under its write lock, and A wraps it in a write transaction (BEGIN IMMEDIATE). While that transaction is open, B’s request to start writing is refused with “database is locked”. The program sets timeout=0 so that the refusal shows; by default Python’s sqlite3 module waits up to five seconds for the lock instead. Once A commits, B takes its turn, and both increments count.

SQLite allows any number of readers but only one writer at a time per database file, as its own documentation says. PostgreSQL and MySQL let many transactions write at once: PostgreSQL keeps them apart with row versions (multiversion concurrency control) and row-level locks, and MySQL’s InnoDB engine locks individual rows. The modules on transactions and concurrency control in this track show how.

A crash in the middle of an update

The second program raises every mark by 5 and dies halfway through, once with a JSON file and once with SQLite. Each update runs in a separate Python process that ends with os._exit(1), which stops a program the way a crash does: nothing is cleaned up, flushed or closed.

A crash in the middle of an update Python · crash_midway.py
"""A program dies halfway through an update: first with a plain file, then with SQLite."""
import hashlib
import json
import os
import sqlite3
import subprocess
import sys

STUDENTS = 1000
marks = {f"21CS{i:04}": 40 + i % 50 for i in range(STUDENTS)}


def die_after(code):
    """Runs `code` in a new Python process that is killed (os._exit) once the code has run."""
    subprocess.run([sys.executable, "-c", code + "\nimport os\nos._exit(1)"], check=False)


def fingerprint(path):
    with open(path, "rb") as f:
        return hashlib.sha256(f.read()).hexdigest()


# 1. A plain file. Every mark goes up by 5, but the program dies while it rewrites the file.
with open("marks.json", "w") as f:
    json.dump(marks, f)
die_after("""
import json
marks = json.load(open("marks.json"))
text = json.dumps({roll: mark + 5 for roll, mark in marks.items()})
f = open("marks.json", "w")        # opening a file for writing empties it at once
f.write(text[: len(text) // 2])    # half of the new text is written...
f.flush()                          # ...and handed to the operating system
""")
text = open("marks.json").read()
full = len(json.dumps(marks))
print(f"file: {len(text):,} of {full:,} characters left, ending {text[-15:]!r}")
try:
    json.loads(text)
except json.JSONDecodeError as e:
    print("file: unreadable:", e.msg)

# 2. SQLite. The same update inside a transaction; the program dies before COMMIT.
db = sqlite3.connect("marks.db", autocommit=True)
db.execute("CREATE TABLE marks (roll TEXT PRIMARY KEY, mark INTEGER NOT NULL, remarks TEXT)")
rows = [(roll, mark, "-" * 300) for roll, mark in marks.items()]
db.executemany("INSERT INTO marks VALUES (?, ?, ?)", rows)
db.close()
before = fingerprint("marks.db")
die_after("""
import sqlite3
db = sqlite3.connect("marks.db", autocommit=True)
# Room for 10 pages in memory, far fewer than the update changes, so SQLite
# writes changed pages into the database file before COMMIT.
db.execute("PRAGMA cache_size = 10")
db.execute("PRAGMA cache_spill = ON")
db.execute("BEGIN")
db.execute("UPDATE marks SET mark = mark + 5")
""")
print("database file changed on disk before COMMIT:", fingerprint("marks.db") != before)
print("rollback journal next to it:", os.path.exists("marks.db-journal"))

db = sqlite3.connect("marks.db")         # the next program that opens the database
CHANGED = "SELECT count(*) FROM marks WHERE mark <> 40 + CAST(substr(roll, 5) AS INTEGER) % 50"
changed = db.execute(CHANGED).fetchone()[0]
print(f"after reopening: {changed} of {STUDENTS:,} marks changed")
db.close()
print("rollback journal still there:", os.path.exists("marks.db-journal"))
print("database file identical to before the update:", fingerprint("marks.db") == before)

Output

file: 8,000 of 16,000 characters left, ending '"21CS0499": 94,'
file: unreadable: Expecting property name enclosed in double quotes
database file changed on disk before COMMIT: True
rollback journal next to it: True
after reopening: 0 of 1,000 marks changed
rollback journal still there: False
database file identical to before the update: True

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 crash_midway.py

The plain file is ruined. Opening a file for writing empties it at once, and the program died after writing half of the new text, so 8,000 of the 16,000 characters are left: the old marks are gone and only half of the new ones arrived. The JSON reader cannot even parse what is left.

The SQLite database is untouched, and not because nothing reached the disk. The program set a tiny page cache (10 pages) and allowed SQLite to spill changed pages into the database file before the transaction ended, so the file on disk really was half-updated when the program died. Before SQLite changes a page, it copies the page’s original content into a rollback journal next to the database. The next connection to read the database found that journal, wrote the original pages back and deleted it: no mark changed, and the file is byte for byte what it was before the update, as the matching SHA-256 fingerprints show.

An update that happens completely or not at all is called atomic. Careful programs can get atomic replacement of a single file by writing a new copy and renaming it over the old one: Python’s os.replace documents that a successful rename is atomic. A DBMS gives the same guarantee for a change that touches many rows of many tables, while other users keep working. SQLite does it with a rollback journal or a write-ahead log; the module on logging and recovery explains both.

Rules the database enforces

When the rules are part of the schema, the DBMS checks them on every write, whichever program sends it. This table of marks accepts a mark only for a student who exists, only between 0 and 100, and only once per student and subject:

Rules the database enforces SQL · rules.sql
-- The rules live in the schema, so the database checks them on every write,
-- whichever program sends it.
PRAGMA foreign_keys = ON;   -- SQLite checks foreign keys only when this is switched on

CREATE TABLE students (
  roll TEXT PRIMARY KEY,
  name TEXT NOT NULL
);
CREATE TABLE marks (
  roll    TEXT NOT NULL REFERENCES students (roll),
  subject TEXT NOT NULL,
  mark    INTEGER NOT NULL CHECK (mark BETWEEN 0 AND 100),
  PRIMARY KEY (roll, subject)
);

INSERT INTO students VALUES ('21CS0001', 'Asha'), ('21CS0002', 'Ravi');
INSERT INTO marks VALUES ('21CS0001', 'DBMS', 78);

INSERT INTO marks VALUES ('21CS0002', 'DBMS', 140);  -- a mark out of range
INSERT INTO marks VALUES ('21CS0099', 'DBMS', 65);   -- a student who does not exist
INSERT INTO marks VALUES ('21CS0001', 'DBMS', 81);   -- a second DBMS mark for Asha
INSERT INTO students VALUES ('21CS0003', NULL);      -- a student without a name

SELECT * FROM marks;

Output (exit status 1)

┌──────────┬─────────┬──────┐
│   roll   │ subject │ mark │
├──────────┼─────────┼──────┤
│ 21CS0001 │ DBMS    │ 78   │
└──────────┴─────────┴──────┘

Printed as an error (standard error)

Runtime error near line 19: CHECK constraint failed: mark BETWEEN 0 AND 100
Runtime error near line 20: FOREIGN KEY constraint failed
Runtime error near line 21: UNIQUE constraint failed: marks.roll, marks.subject
Runtime error near line 22: NOT NULL constraint failed: students.name

Recorded with sql.js 1.14.2 (SQLite 3.49.1) on macOS 26 arm64. To run it yourself: sqlite3 -box :memory: < rules.sql

Four bad rows were sent and four were refused, each with the name of the rule it broke: the CHECK on the range, the FOREIGN KEY to students, the UNIQUE combination of roll number and subject (the primary key), and NOT NULL on the name. Only the valid mark remains. One SQLite detail matters here: foreign keys are checked only after PRAGMA foreign_keys = ON, which every connection must run for itself, because the check is off by default for compatibility with old applications.

Who may see what

A file’s permissions apply to the whole file. A client-server DBMS grants privileges on tables, views and even single columns, to users and to groups of users. In PostgreSQL, for example, GRANT SELECT (roll, hostel, room) ON students TO warden lets the warden’s role read three columns and nothing else. SQLite has no GRANT or REVOKE at all: it is a library that reads an ordinary file, so its documentation explains that only the operating system’s file permissions apply. That is one reason SQLite describes itself as storage for one application or device rather than a store that many users share over a network.

Schema, instance and the catalog

The schema describes the data: the tables, their columns and types, the keys and constraints, the views and the indexes. It changes rarely. The instance, also called the state of the database, is the data the database holds at one moment, and every insert, update and delete changes it.

A DBMS stores its schema as data too, in tables known as the catalog or data dictionary, and you query it like any other table. SQLite keeps one row per table, index, view and trigger in sqlite_schema (older programs use its other name, sqlite_master), with the CREATE statement that would rebuild the object:

The schema, stored as data SQL · catalog.sql
CREATE TABLE students (
  roll   TEXT PRIMARY KEY,
  name   TEXT NOT NULL,
  hostel TEXT
);
CREATE INDEX students_by_hostel ON students (hostel);
CREATE VIEW hostel_list AS SELECT hostel, name FROM students;

-- The schema itself is stored as rows of a table, and you query it like any other table.
SELECT type, name, tbl_name, sql FROM sqlite_schema;

Output

┌───────┬─────────────────────────────┬─────────────┬──────────────────────────────────────────────────────────────┐
│ type  │            name             │  tbl_name   │                             sql                              │
├───────┼─────────────────────────────┼─────────────┼──────────────────────────────────────────────────────────────┤
│ table │ students                    │ students    │ CREATE TABLE students (                                      │
│       │                             │             │   roll   TEXT PRIMARY KEY,                                   │
│       │                             │             │   name   TEXT NOT NULL,                                      │
│       │                             │             │   hostel TEXT                                                │
│       │                             │             │ )                                                            │
├───────┼─────────────────────────────┼─────────────┼──────────────────────────────────────────────────────────────┤
│ index │ sqlite_autoindex_students_1 │ students    │                                                              │
├───────┼─────────────────────────────┼─────────────┼──────────────────────────────────────────────────────────────┤
│ index │ students_by_hostel          │ students    │ CREATE INDEX students_by_hostel ON students (hostel)         │
├───────┼─────────────────────────────┼─────────────┼──────────────────────────────────────────────────────────────┤
│ view  │ hostel_list                 │ hostel_list │ CREATE VIEW hostel_list AS SELECT hostel, name FROM students │
└───────┴─────────────────────────────┴─────────────┴──────────────────────────────────────────────────────────────┘

Recorded with sql.js 1.14.2 (SQLite 3.49.1) on macOS 26 arm64. To run it yourself: sqlite3 -box :memory: < catalog.sql

Three details are worth noticing. The primary key created an index of its own, sqlite_autoindex_students_1, and its sql column is empty because no statement of yours created it. The index and the view name the table they belong to in tbl_name. And the stored text is your own statement, line breaks and all, with only small normalisations such as the capitals of its first keywords.

PostgreSQL offers two catalogs. The information schema is a set of views defined by the SQL standard, so the same queries work on other systems; the view information_schema.columns, for example, lists the columns of every table you are allowed to see. The system catalogs live in the schema pg_catalog, which every PostgreSQL database contains; they are PostgreSQL’s own tables and also describe the features the standard does not know about. Its documentation warns that they are regular tables you could damage by hand, and that commands such as CREATE DATABASE are the way to change them: that command inserts a row into the catalog pg_database and creates the database on disk.

Three levels and data independence

The standard way to describe how a DBMS separates users from storage is the three-schema architecture, with one level for each kind of description:

Three levels of a database: user views on top, one logical schema in the middle, storage at the bottom.External level: viewsConceptual level: one logical schemastudents, allotments, markswith types, keys and constraintsInternal level: how it is storedfiles and pages, B-tree indexes,record layout, page sizeOffice view: roll, name, feesWarden view: roll, hostel, roomResults view: roll, subject, marklogical data independence:change the schema, keep the viewsphysical data independence:change the storage, keep the schema

The three-schema architecture and the two kinds of data independence

Text description of the diagram

The diagram is a column of three levels, from what users see at the top to how the data is stored at the bottom.

  1. External level: views, one for each group of users. The office sees roll number, name and fees; the warden sees roll number, hostel and room; the results page sees roll number, subject and mark.
  2. Conceptual level: one logical schema for the whole database, with the tables students, allotments and marks and their types, keys and constraints.
  3. Internal level: how the data is stored, as files and pages, B-tree indexes, the layout of each record and the page size.

The arrow from the external to the conceptual level is labelled "logical data independence: change the schema, keep the views". The arrow from the conceptual to the internal level is labelled "physical data independence: change the storage, keep the schema".

  • The external level holds the views that groups of users work with. The warden sees rooms, the office sees fees, and neither needs the rest.
  • The conceptual level, or logical level, holds one schema for the whole database: every table, column, type, key and constraint.
  • The internal level, or physical level, describes storage: files and pages, indexes, the layout of each record.

The DBMS maps each level onto the one below it, and those mappings are what make data independence possible, the ability to change one level without rewriting what sits above it:

  • Physical data independence means you can change the internal level, by adding an index, reorganising files or moving to a faster disk, without changing the logical schema or the programs. E. F. Codd’s 1970 paper that introduced the relational model opens with exactly this aim: users and most programs should not have to know how the data is organised inside the machine, and should keep working when that organisation changes.
  • Logical data independence means you can change the logical schema, by splitting a table or adding a column, without breaking the views and the programs that use them. PostgreSQL’s tutorial makes the same point about views: they hide the structure of your tables, which may change as an application evolves, behind interfaces that stay the same.

An index changes the plan, not the answer

EXPLAIN QUERY PLAN asks SQLite how it would run a query, without running it. Its detail column is the part to read: SCAN means every row of the table is visited, and SEARCH means only some rows are, found through an index.

A new index changes the plan, not the answer SQL · physical_independence.sql
CREATE TABLE students (roll TEXT PRIMARY KEY, name TEXT NOT NULL, hostel TEXT);
INSERT INTO students VALUES
  ('21CS0001', 'Asha', 'Kaveri'),
  ('21CS0002', 'Ravi', 'Godavari'),
  ('21CS0003', 'Meera', 'Kaveri');

-- The query, and how the engine plans to run it, before the index exists.
EXPLAIN QUERY PLAN SELECT name FROM students WHERE hostel = 'Kaveri' ORDER BY name;
SELECT name FROM students WHERE hostel = 'Kaveri' ORDER BY name;

-- A change at the storage level: a new index.
CREATE INDEX students_by_hostel ON students (hostel);

-- The same query text: a different plan, the same result.
EXPLAIN QUERY PLAN SELECT name FROM students WHERE hostel = 'Kaveri' ORDER BY name;
SELECT name FROM students WHERE hostel = 'Kaveri' ORDER BY name;

Output

┌────┬────────┬─────────┬──────────────────────────────┐
│ id │ parent │ notused │            detail            │
├────┼────────┼─────────┼──────────────────────────────┤
│ 3  │ 0      │ 216     │ SCAN students                │
│ 11 │ 0      │ 0       │ USE TEMP B-TREE FOR ORDER BY │
└────┴────────┴─────────┴──────────────────────────────┘
┌───────┐
│ name  │
├───────┤
│ Asha  │
│ Meera │
└───────┘
┌────┬────────┬─────────┬───────────────────────────────────────────────────────────┐
│ id │ parent │ notused │                          detail                           │
├────┼────────┼─────────┼───────────────────────────────────────────────────────────┤
│ 4  │ 0      │ 62      │ SEARCH students USING INDEX students_by_hostel (hostel=?) │
│ 13 │ 0      │ 0       │ USE TEMP B-TREE FOR ORDER BY                              │
└────┴────────┴─────────┴───────────────────────────────────────────────────────────┘
┌───────┐
│ name  │
├───────┤
│ Asha  │
│ Meera │
└───────┘

Recorded with sql.js 1.14.2 (SQLite 3.49.1) on macOS 26 arm64. To run it yourself: sqlite3 -box :memory: < physical_independence.sql

Before the index, SQLite scans the whole table; after it, it searches the index for hostel = 'Kaveri'. Both times it sorts the result for ORDER BY in a temporary B-tree, and both times the answer is Asha and Meera. Not a word of the query changed. That is physical data independence, and it is the everyday kind: database administrators add and drop indexes on live systems, and the programs never notice, apart from the speed.

A view keeps the old shape, for reading

This time the logical schema changes. Hostel rooms move out of students into a table of their own, and a view with the old name puts the old shape back together:

A view keeps the old shape SQL · logical_independence.sql
-- Version 1 of the schema: hostel rooms are columns of students.
CREATE TABLE students (roll TEXT PRIMARY KEY, name TEXT NOT NULL, hostel TEXT, room INTEGER);
INSERT INTO students VALUES
  ('21CS0001', 'Asha', 'Kaveri', 101),
  ('21CS0002', 'Ravi', 'Godavari', 214),
  ('21CS0003', 'Meera', NULL, NULL);

-- A report that programs already run:
SELECT name, hostel, room FROM students ORDER BY roll;

-- Version 2: room allotments move to a table of their own...
CREATE TABLE student (roll TEXT PRIMARY KEY, name TEXT NOT NULL);
CREATE TABLE allotment (
  roll   TEXT PRIMARY KEY REFERENCES student (roll),
  hostel TEXT NOT NULL,
  room   INTEGER NOT NULL
);
INSERT INTO student SELECT roll, name FROM students;
INSERT INTO allotment SELECT roll, hostel, room FROM students WHERE hostel IS NOT NULL;
DROP TABLE students;

-- ...and a view keeps the old shape under the old name.
CREATE VIEW students AS
  SELECT s.roll, s.name, a.hostel, a.room
  FROM student AS s LEFT JOIN allotment AS a ON a.roll = s.roll;

-- The old report runs unchanged and returns the same rows:
SELECT name, hostel, room FROM students ORDER BY roll;

-- But a program that writes to the old shape now fails:
INSERT INTO students VALUES ('21CS0004', 'Kiran', 'Kaveri', 102);

Output (exit status 1)

┌───────┬──────────┬──────┐
│ name  │  hostel  │ room │
├───────┼──────────┼──────┤
│ Asha  │ Kaveri   │ 101  │
│ Ravi  │ Godavari │ 214  │
│ Meera │          │      │
└───────┴──────────┴──────┘
┌───────┬──────────┬──────┐
│ name  │  hostel  │ room │
├───────┼──────────┼──────┤
│ Asha  │ Kaveri   │ 101  │
│ Ravi  │ Godavari │ 214  │
│ Meera │          │      │
└───────┴──────────┴──────┘

Printed as an error (standard error)

Parse error near line 31: cannot modify students because it is a view

Recorded with sql.js 1.14.2 (SQLite 3.49.1) on macOS 26 arm64. To run it yourself: sqlite3 -box :memory: < logical_independence.sql

The report that read students runs unchanged and returns the same three rows, now from two tables. The last statement shows the limit. A program that inserted into students now fails, because a view in SQLite is read-only; writes through it need INSTEAD OF triggers that send each change to the right table. PostgreSQL would refuse the insert too: it updates a view automatically only when the view reads exactly one table, and this one joins two.

This is why logical data independence is the harder kind. Views hide changes from readers well, from writers only with extra work, and some changes they cannot hide at all: once a column is dropped, no view can bring its values back.

Four kinds of SQL statements

SQL statements are usually grouped into four classes:

Class What it does Typical statements
DDL, data definition language Defines and changes the schema CREATE, ALTER, DROP
DML, data manipulation language Reads and changes the rows SELECT, INSERT, UPDATE, DELETE
DCL, data control language Gives and takes away privileges GRANT, REVOKE
TCL, transaction control language Groups statements into transactions BEGIN, COMMIT, ROLLBACK, SAVEPOINT

These four names are a teaching convention, and the manuals of real systems draw the lines differently. Oracle Database’s SQL reference lists GRANT, REVOKE and TRUNCATE as DDL, counts SELECT as DML, and adds classes for session control and system control. MySQL’s manual says that TRUNCATE TABLE, although it empties a table much as DELETE does, is a DDL statement. Some textbooks give SELECT a class of its own, DQL, for data query language. For an exam, use the four classes above; for real work, check what your own DBMS does with each statement.

The difference matters most for transactions. In SQLite, DDL takes part in the transaction it runs in, so a ROLLBACK undoes a CREATE TABLE:

CREATE TABLE inside a transaction SQL · ddl_rollback.sql
-- In SQLite, CREATE TABLE is part of the transaction it runs in.
BEGIN;
CREATE TABLE notices (id INTEGER PRIMARY KEY, body TEXT NOT NULL);
INSERT INTO notices (body) VALUES ('The DBMS lab moves to room 204');
SELECT name AS tables_inside_the_transaction FROM sqlite_schema WHERE type = 'table';
ROLLBACK;

SELECT count(*) AS tables_after_rollback FROM sqlite_schema WHERE type = 'table';

Output

┌───────────────────────────────┐
│ tables_inside_the_transaction │
├───────────────────────────────┤
│ notices                       │
└───────────────────────────────┘
┌───────────────────────┐
│ tables_after_rollback │
├───────────────────────┤
│ 0                     │
└───────────────────────┘

Recorded with sql.js 1.14.2 (SQLite 3.49.1) on macOS 26 arm64. To run it yourself: sqlite3 -box :memory: < ddl_rollback.sql

PostgreSQL behaves the same way for most DDL. Its manual says, for example, that TRUNCATE is rolled back if the transaction around it does not commit, and the few commands that cannot run inside a transaction at all, such as CREATE DATABASE, say so on their own manual pages. MySQL behaves differently. Statements such as CREATE TABLE, ALTER TABLE, DROP TABLE and TRUNCATE TABLE end any open transaction with an implicit commit, and Oracle Database commits before and after every DDL statement. A migration script that mixes schema changes and data changes can therefore be all or nothing on PostgreSQL and SQLite, but not on MySQL or Oracle Database.

Who works with a database

Several groups of people use the same database in different ways:

  • End users work through applications and never write SQL: a student checking results, a warden allotting rooms.
  • Application developers write the programs that send SQL, and the tests for them.
  • Database designers decide the schema: the tables, keys and constraints. The ER model and normalisation, later in this track, are their tools.
  • Database administrators (DBAs) run the system: they create users, grant privileges, plan backups and recovery, watch performance and upgrade versions.
  • Analysts query the data directly to answer questions, often on a read-only copy.

In PostgreSQL, each of these people, or the application acting for them, connects as a role. A role can be a single user or a group of users, can own tables and other objects, and can grant privileges on them to other roles. In a small team one person often plays several of these parts.

Data models at a glance

A data model is the set of ideas a DBMS gives you for describing data: what a record looks like, how records refer to each other and which operations exist. Six models cover nearly every database you will meet:

  • Relational: tables of rows with typed columns. Keys link the tables, and SQL combines them with joins. PostgreSQL, MySQL, SQLite, Microsoft SQL Server and Oracle Database are relational.
  • Document: each record is a document of fields and values, which can nest other documents and arrays. MongoDB stores documents in BSON, a binary form of JSON.
  • Key-value: you store a value under a key and fetch it by that key. Redis is a widely used example, with values that can be strings, hashes, lists, sets and other structures.
  • Wide-column: rows are grouped into partitions, and a hash of each row’s partition key decides which machine stores it, so one table spreads across many machines. Apache Cassandra works this way.
  • Graph: nodes and the relationships between them, both with properties, and queries that follow paths. Even the SQL standard now has a part for it, Property Graph Queries (SQL/PGQ).
  • Vector: each item is stored with an embedding, a long list of numbers, and queries look for the nearest neighbours by distance. The pgvector extension adds this to PostgreSQL.

For an application that records business events such as orders, bookings, marks or payments, the relational model is the sensible default: it checks rules, joins freely and runs transactions. Choose another model when one access pattern clearly needs it. The system design track compares the models by access pattern in Choosing a data model.

How widely each one is used is a different question from how good it is. In the 2025 Stack Overflow Developer Survey, the databases that the most respondents had done extensive work with in the past year were PostgreSQL (55.6% of the 26,083 who answered), MySQL (40.5%) and SQLite (37.5%), ahead of Microsoft SQL Server (30.1%), Redis (28%) and MongoDB (24%). Many projects use more than one.

Where the relational model came from

The first database systems were hierarchical or network systems: records were linked into trees or more general networks, and a program found data by following those links in the order the structure allowed. When the structure changed, the programs had to change with it. Codd’s paper of 1970 proposed something else: data as relations, which are tables, described only by their contents, so that programs depend on what the data means rather than on how it is stored. Relational systems and SQL followed. SQL is now an international standard, ISO/IEC 9075 “Database Language SQL”, revised every few years; PostgreSQL 18 measures its conformance against the 2023 revision, SQL:2023.

GATE CS

The GATE 2027 syllabus for Computer Science and Information Technology lists Databases as Section 9: the ER model, the relational model (relational algebra, tuple calculus and SQL), integrity constraints, normal forms, file organisation, indexing with B and B+ trees, and transactions and concurrency control (GATE 2027 syllabus). This lesson’s integrity, concurrency and recovery examples are a first look at three of those topics.

SQLite Online (SQL Playground) Run this lesson's SQL on a SQLite database in your browser, or open a .db file and query its sqlite_schema table. SQL to ER Diagram Paste CREATE TABLE statements, such as the ones in this lesson, to see the schema drawn as an ER diagram.

Exercises

Exercise · Easy · SQL

List every index with the table it belongs to

A database's schema is stored in its catalog, and in SQLite you can query it like any table. Write a query on sqlite_schema that returns one row for every index of the database, in two columns:

  • index_name: the name of the index;
  • table_name: the table the index belongs to.

Include the indexes that SQLite creates by itself for PRIMARY KEY and UNIQUE constraints, and leave out tables, views and triggers. For a database made by

CREATE TABLE rooms (room_no INTEGER PRIMARY KEY, hostel TEXT NOT NULL, label TEXT UNIQUE);
CREATE INDEX rooms_by_hostel ON rooms (hostel);

the result would be:

┌──────────────────────────┬────────────┐
│        index_name        │ table_name │
├──────────────────────────┼────────────┤
│ sqlite_autoindex_rooms_1 │ rooms      │
│ rooms_by_hostel          │ rooms      │
└──────────────────────────┴────────────┘

The order of the rows does not matter. The sample tests run your query on two small databases of their own.

Starter code · query.sql

-- Return index_name and table_name for every index of the database.
SELECT name FROM sqlite_schema;
The sample tests · sample_tests.yaml
# Sample tests (public): each test builds its own schema in a fresh database, runs the learner's query and compares
# the result with the expected rows (the order of rows does not matter; column names must match).
tests:
  - name: lists the index of a primary key and an index created by hand
    setup: |
      CREATE TABLE students (roll TEXT PRIMARY KEY, name TEXT NOT NULL, hostel TEXT);
      CREATE INDEX students_by_hostel ON students (hostel);
      CREATE VIEW hostel_list AS SELECT hostel, name FROM students;
    expected:
      columns: [index_name, table_name]
      rows:
        - [sqlite_autoindex_students_1, students]
        - [students_by_hostel, students]
  - name: finds indexes on several tables and skips an INTEGER PRIMARY KEY, which needs no index
    setup: |
      CREATE TABLE courses (course_id INTEGER PRIMARY KEY, code TEXT NOT NULL UNIQUE, title TEXT NOT NULL);
      CREATE TABLE enrolments (
        student_id INTEGER NOT NULL,
        course_id  INTEGER NOT NULL REFERENCES courses (course_id),
        PRIMARY KEY (student_id, course_id)
      );
      CREATE INDEX enrolments_by_course ON enrolments (course_id);
      CREATE TRIGGER no_empty_code BEFORE INSERT ON courses WHEN NEW.code = '' BEGIN SELECT RAISE(ABORT, 'empty code'); END;
    expected:
      columns: [index_name, table_name]
      rows:
        - [sqlite_autoindex_courses_1, courses]
        - [sqlite_autoindex_enrolments_1, enrolments]
        - [enrolments_by_course, enrolments]
A hint

Every object of the schema is one row of sqlite_schema, and its type column says what it is: 'table', 'index', 'view' or 'trigger'. The name column holds the object's own name and tbl_name the table it belongs to; rename both with AS.

The sample tests run on this device, in your browser (sql.js): nothing is sent to mysmartcopilot.com. The first run downloads SQLite (about 0.7 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Check yourself

8 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.

  1. Question 1 of 8 The hostel office and the warden both update fees.json. Each program reads the file, changes one student's balance and writes the whole file back. Now and then a payment disappears, with no error. Which DBMS service prevents this?

    Choose one answer.

    Show the answer to question 1

    Answer: Concurrency control

    This is a lost update: both programs start from the same old version, and the one that saves last overwrites the other's change. A DBMS keeps simultaneous writers apart with transactions and locks, and an UPDATE that computes the new value itself (balance = balance + 500) reads and writes in one step.

  2. Question 2 of 8 A program that adds this month's mess charges to 2,000 student accounts loses power halfway. Afterwards, some accounts have the charge and some do not. Which DBMS service would have left either all accounts or none charged?

    Choose one answer.

    Show the answer to question 2

    Answer: Recovery, which makes the update atomic

    An update that is interrupted must be undone completely or finished completely. The DBMS records what it changes in a journal or a write-ahead log, and after the crash it uses that record to put the database back to a consistent state, as the crash example in this lesson shows with SQLite's rollback journal.

  3. Question 3 of 8 The marks web app refuses any mark above 100, but a mark of 140 appears in the data after a bulk import by another script. What should hold the rule so that every program obeys it?

    Choose one answer.

    Show the answer to question 3

    Answer: A CHECK constraint in the schema, such as CHECK (mark BETWEEN 0 AND 100)

    A rule in one program protects only that program's writes. As an integrity constraint in the schema, the DBMS checks it on every INSERT and UPDATE, whichever program sends it, and refuses the bad row with an error. An index or a view does not stop the row from being stored.

  4. Question 4 of 8 Room numbers and marks are both columns of one table. The warden must read room numbers but never marks. What does a client-server DBMS such as PostgreSQL offer that file permissions cannot?

    Choose one answer.

    Show the answer to question 4

    Answer: Privileges on single columns or on a view, granted to the warden's role

    File permissions apply to the whole file. A client-server DBMS grants privileges on tables, views and single columns, for example GRANT SELECT (roll, hostel, room) ON students TO warden. A second copy would bring back the problem of redundant, inconsistent data.

  5. Question 5 of 8 A database administrator adds an index on students(hostel). The same queries now return the same rows faster, and no program was changed. Which kind of data independence is this?

    Choose one answer.

    Show the answer to question 5

    Answer: Physical data independence

    An index belongs to the internal level, the way data is stored. Changing it changed the query plan (SCAN became SEARCH in this lesson's example) but not the logical schema, the query text or the result.

  6. Question 6 of 8 The students table is split into student and allotment, and a view named students joins them back together, so the old reports keep working. Which kind of data independence is this?

    Choose one answer.

    Show the answer to question 6

    Answer: Logical data independence

    The logical schema changed, and the view keeps the external level the same for the programs that read it. It is the harder kind: in this lesson's example, a program that inserts into students fails, because the view is read-only.

  7. Question 7 of 8 In the four-class scheme of DDL, DML, DCL and TCL, which of these statements is DCL?

    Choose one answer.

    Show the answer to question 7

    Answer: GRANT SELECT ON marks TO analyst

    GRANT and REVOKE give and take away privileges, which is data control. TRUNCATE is usually counted as DDL, SAVEPOINT is transaction control and SELECT is DML (some textbooks call it DQL). Real manuals differ: Oracle Database's SQL reference lists GRANT and REVOKE among its DDL statements.

  8. Question 8 of 8 In this lesson's lost-update example, SQLite lost an increment in one run and kept both in another. What made the difference?

    Choose one answer.

    Show the answer to question 8

    Answer: The database computed n + 1 itself, in one UPDATE inside a write transaction

    The run that read the value with a SELECT and wrote back a value computed in Python lost an update in SQLite too. Only when the read and the write became one statement, run under SQLite's write lock inside BEGIN IMMEDIATE, did both increments count. timeout=0 only made the refusal visible instead of waiting for the lock.

Interview questions

Fresher · What is data independence, and which kind is harder to achieve?

Data independence is the ability to change how data is described at one level without changing what sits above it. Physical data independence lets you change storage, such as indexes, file organisation or disks, without changing the logical schema or the programs. Logical data independence lets you change the logical schema, such as splitting a table or adding a column, without breaking the external views and the programs that use them. Physical independence is easier and routine: adding an index changes the query plan but not the query text or its result. Logical independence is harder, because views can rebuild an old shape for reading, but writes through a view need extra work such as INSTEAD OF triggers, and a change that removes information cannot be hidden by any view.

Fresher · Is TRUNCATE a DDL or a DML statement?

It depends on the system, and that is the point to make. TRUNCATE empties a table, as a DELETE without a WHERE clause would, but MySQL’s manual and Oracle Database’s SQL reference both classify it as DDL. In MySQL it causes an implicit commit and cannot be rolled back, and it does not fire ON DELETE triggers. In PostgreSQL it is transaction-safe: a TRUNCATE inside a transaction that rolls back leaves the rows in place. So name the textbook class, then say what your own DBMS does with it, because that decides whether a script containing it can be undone.

Key takeaways

  • A DBMS keeps the data and handles every access to it, which lets it enforce integrity, control concurrency, recover from crashes and decide who may see what: jobs that plain files leave to every program.
  • Lost updates come from reading, computing and writing in separate steps; let the database do the arithmetic inside a transaction (UPDATE … SET n = n + 1) and the update is safe.
  • A rollback journal or write-ahead log makes a change atomic: after a crash, the database is either fully updated or exactly as it was.
  • The catalog stores the schema as data: sqlite_schema in SQLite, information_schema and pg_catalog in PostgreSQL.
  • The three-schema architecture separates views, the logical schema and storage. Physical data independence is routine; logical data independence is harder, because views hide changes from readers more easily than from writers.
  • DDL, DML, DCL and TCL are textbook classes; real systems classify some statements differently and treat DDL inside transactions differently.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.