SQLite Tutorial for Beginners: A Reading-List Database

SQLite is an embedded relational database engine that usually stores a database in a file and runs inside the application rather than as a separate server.

Lesson 96 of 102Beginner11 min read

This SQLite tutorial creates a small reading-list database, finds books still to finish and calculates how many known pages remain. SQLite is convenient for local practice because a connection can open a file or an in-memory database without provisioning a database server. Its SQL still has important type and relationship rules to learn.

Open a new disposable database in a SQLite client or through your application’s SQLite driver. The setup below uses a STRICT table, which requires SQLite 3.37 or later. If the installed engine is older, update your practice environment or explicitly study ordinary affinity-based table behaviour instead of quietly assuming the same enforcement.

A database file and a connection

SQLite runs in the process that uses it. A database file contains the stored data and schema, while an in-memory connection can provide a disposable database for exercises. There is no server-style CREATE DATABASE or USE statement to copy from a MySQL lesson. Opening or attaching a database is handled through the SQLite interface and supported connection features.

The sqlite3 command-line shell has dot commands for tasks such as output mode and importing files. Those commands are shell instructions, not ordinary SQL to send through every driver. Keep the distinction clear when moving a script from the shell into application code. A file’s access permissions and deployment location matter because SQLite does not provide the same server account-and-role model as PostgreSQL or SQL Server.

Affinity and optional STRICT tables

Ordinary SQLite columns use type affinity rather than the rigid typing assumptions of many server schemas. A DECIMAL declaration does not provide fixed-precision decimal arithmetic by itself. SQLite values have storage classes, and conversions follow documented affinity rules. This is useful flexibility, but an import pipeline must not mistake accepted malformed values for validated domain data.

STRICT tables impose stronger type rules using a limited set of permitted type names such as INTEGER, REAL and TEXT. The reading-list table uses INTEGER counts and TEXT labels. STRICT does not replace NOT NULL, CHECK or business validation. A positive page-count rule still needs a constraint, and a title that is present but meaningless needs a separate application or domain policy.

Generated keys and missing pages

INTEGER PRIMARY KEY aliases the rowid in the relevant ordinary form and can generate an id when omitted. AUTOINCREMENT adds a specific nonreuse policy and is usually not needed for basic identity. It does not promise gap-free numbering. The example supplies explicit ids only to make its expected rows easy to compare.

pages_read can be NULL here because a book may be on the list before reading progress has been recorded. Zero pages_read would mean a known starting measurement, while NULL means no measurement was saved. Arithmetic total_pages-pages_read is unknown for that record. A report can intentionally choose a fallback, but should state whether unrecorded progress is being assumed to equal zero.

Filtering progress with a clear policy

The first query keeps unfinished books whose progress is known. Its WHERE pages_read<total_pages naturally excludes an unknown progress comparison. ORDER BY remaining_pages DESC,id makes the priority stable. The alias describes a numeric calculation rather than modifying the saved total or progress.

A complete reading-list display would need a separate treatment for unrecorded progress, such as an explicit Unknown group. Do not silently leave those books out of an interface that promises to show every unfinished or unstarted title. Define whether a finished state is stored directly or derived from progress, and avoid allowing those two models to contradict one another.

Transactions and relationship enforcement

BEGIN, COMMIT and ROLLBACK group changes on a connection. SQLite has a single writer at a time in its ordinary concurrency model, even though reading concurrency and journaling modes provide useful options. WAL mode changes operational behaviour but is not a substitute for understanding application transaction length and contention. Keep write transactions as short as the business operation allows.

Foreign-key enforcement must normally be enabled per connection with PRAGMA foreign_keys=ON. A REFERENCES declaration is not enough to assume every connection enforces the relationship. Change the setting outside an active transaction and verify it. The reading-list fixture has one table, but an author-and-books extension should include actual foreign-key checks rather than merely similarly named columns.

Portability and local maintenance

SQLite supports modern CTEs and window functions, but feature availability depends on the library version shipped with the application. RIGHT and FULL joins arrived in version 3.39. Quantified ANY and ALL subquery comparison syntax is not supported. Date functions operate on supported textual or numeric representations rather than a native date storage class.

Backups should use a supported consistent method, such as SQLite’s backup API, rather than blindly copying an actively changing database file and ignoring its journal state. EXPLAIN QUERY PLAN can inspect access strategy, but its display format is not a stable application parsing contract. For a larger reading library, index only the filters and relationships that measurements justify.

Run the complete practice setup

The four books include one finished title, two unfinished titles with known progress and one title whose progress has not been recorded. The integer counts make the remaining-page calculation exact for this domain.

SQL
CREATE TABLE reading_books (
 id INTEGER PRIMARY KEY,title TEXT NOT NULL,
 total_pages INTEGER NOT NULL CHECK(total_pages>0),
 pages_read INTEGER CHECK(pages_read>=0 AND pages_read<=total_pages)
) STRICT;
INSERT INTO reading_books (id,title,total_pages,pages_read) VALUES
 (1,'River',200,50),(2,'Cloud',120,120),
 (3,'Stone',300,NULL),(4,'Field',80,20);

Practical exercise and worked solution

Return titles with unrecorded progress and their total page count. Use the dedicated missing-value predicate.

SQL
SELECT title,total_pages FROM reading_books
WHERE pages_read IS NULL ORDER BY id;
Output
title | total_pages
Stone | 300

Stone is the only record whose pages_read value is NULL. Its total page count is known, so the report has enough context to request the missing measurement. The query does not assume the book is unstarted and does not change its saved progress. If the product deliberately treats new books as zero progress, store or document that policy consistently rather than deriving it accidentally from a generic fallback.

Official documentation

Read the SQLite quickstart, STRICT-table specification, datatype guide and foreign-key guide for the corresponding engine rules.

SQLite Tutorial for Beginners: A Reading-List Database Examples

Each example shows the code, the output it produces and what each part does.

Example 1

Find unfinished books with known progress

River has one hundred fifty pages remaining, and Field has sixty. Cloud is complete and fails the strict less-than condition. Stone has unknown progress, so it cannot establish the comparison as true. That exclusion is deliberate for this known-progress report, not evidence that Stone is finished.

SQL
SELECT id,title,total_pages-pages_read AS remaining_pages
FROM reading_books WHERE pages_read<total_pages
ORDER BY remaining_pages DESC,id;
Output
id | title | remaining_pages
1 | River | 150
4 | Field | 60

Common SQLite Tutorial for Beginners: A Reading-List Database Mistakes

Assuming NULL progress is zero automatically

total_pages-pages_read returns NULL for Stone, and pages_read<total_pages is unknown. It does not silently treat missing progress as zero. COALESCE(pages_read,0) can implement that policy when intended, but must not hide the distinction between a recorded zero and an unrecorded measurement. A separate unknown-progress list can make the data-quality state clear.

SQLite Tutorial for Beginners: A Reading-List Database FAQ

Does SQLite need a separate database server?

No. It is embedded in the application process: a library that reads and writes a single database file directly. There's nothing to install, start or configure, which makes SQLite ideal for learning, desktop and mobile apps, tests and small websites.

Does STRICT mean every business rule is validated?

No. A STRICT table rejects values of the wrong type, such as text in an INTEGER column, so it strengthens typing. Constraints such as NOT NULL, CHECK and foreign keys, and application rules, still matter for everything else.

Do SQLite date columns use a native date storage class?

No. SQLite stores dates as text, real Julian day numbers or integer Unix times. Use a documented supported representation, usually ISO text such as 2026-01-31, and the same temporal functions, date(), datetime() and strftime(), consistently.

Can SQLite handle several users at once?

Many readers can read at the same time, but only one connection can write at a time. Write-ahead logging mode, PRAGMA journal_mode = WAL, lets readers continue while a write happens. That's plenty for most small and medium websites; busy multi-writer systems are better served by PostgreSQL or MySQL.

How do I open a SQLite database?

Run sqlite3 shop.db in a terminal, which opens the file or creates it if it doesn't exist, then type SQL statements and dot-commands such as .tables and .schema. DB Browser for SQLite provides a graphical alternative, and every major programming language has a SQLite library.

SQLite Tutorial for Beginners: A Reading-List Database Exercises

Answer each one and check it instantly. If you get stuck, open the hint before the answer.

Multiple choicemedium

How many remaining pages are known for Field?

SQLite Tutorial for Beginners: A Reading-List Database Quiz

2 questions to check what you learned.

  1. What normally creates or opens a SQLite database?

    • Opening a file or in-memory database through a connection
    • CREATE DATABASE followed by USE
    • A MySQL ENGINE clause
  2. What is normally needed to enforce SQLite foreign keys?

    • Only give every reference a name ending in _id
    • Enable foreign_keys on the relevant connection
    • Use ORDER BY on every INSERT