SQLite
About SQLite
- SQLite is the most popular embedded relational database and probably the widely deployed software module.
Some features:
- The database is just a single file.
- It has an in-memory version for fast access to temporary data.
- No need for external server process.
- The client directly accesses the file containing the data.
- There is a command-line tool called
sqlite3to access the data. - There are several GUI tools to view and manipulate data.
- Most programming languages have a library to access an SQLite database.
Using SQLite in programming languages
Main features - where to use it
It is very fast for reading, but not so good for concurrent writing.
- Embedded in every web browser
- Embedded in many other applications.
- Cars etc.
Install SQLite command-line tool
Debian/Ubuntu Linux:
$ sudo apt install sqlite3
Windows
- Download
sqlite-tools-win-x64-3530000.zip(or a later version) that includes the “Command-line tools for Windows x64,”
macOS
- Use Homebrew
Version of SQLite
On Ubuntu 25.10:
$ sqlite3 --version
3.46.1 2024-08-13 09:16:08 c9c2ab54ba1f5f46360f1b4f35d849cd3f080e6fc2b6c60e91b16c63f69aalt1 (64-bit)
Downloaded from web site of SQLite
$ ~/bin/sqlite3 --version
3.53.0 2026-04-09 11:41:38 4525003a53a7fc63ca75c59b22c79608659ca12f0131f52c18637f829977f20b (64-bit)
Some of the examples only work on the most recent version.
GUI for SQLite
TODO
There are number of desktop and web-based GUI tools to be used with an SQLite database.
sudo apt install sqlitebrowser
SQLite dialect
- SQLite works according to the SQL92 standard.
- There are some SQLite specific extensions.
Data types
- Data types
- Flexible typing - SQLite does not really care about the types you defined for the columns. You can even create columns without types.
- STRICT tables - In special cases you can ask SQLite to enforce strict type-checking by setting the table to be
STRICT.
Basic types
- NULL
- INTEGER
- REAL
- TEXT
- BLOB
Conventions
SQL is case insensitive, but in order to make it easier to read some people follow the rule to always write every SQL command in capital letters and every user-defined values (e.g. names of the tables, columns etc.) in lower-case or maybe mixed case.
We’ll follow this convention in the examples.
SQLite interactive shell - .help and .quit
The command line tool has a number of internal commands starting with a dot. .help will show the list of the commands.
.quit or .exit will close the shell.
The commands can be given
- interactively
- sent through a pipe
- redirected from a file
$ sqlite3
SQLite version 3.45.1 2024-01-30 16:01:20
Enter ".help" for usage hints.
Connected to a transient in-memory database.
Use ".open FILENAME" to reopen on a persistent database.
sqlite> .help
...
sqlite> .quit
$
Help with SQLite CLI using a pipe
Pipe the command to the STDIN of sqlite3. Available on Linux and macOS and in the git-bash on Windows.
echo .help | sqlite3
Help with SQLite CLI using redirect
Redirect from an external file to the Standard Input (STDIN) of the sqlite3 command.
.help
$ sqlite3 < examples/help.sql
SQLite version
This is using the version I installed from SQLite on Ubuntu Linux.
Interactive
$ sqlite3
sqlite> .version
SQLite 3.53.0 2026-04-09 11:41:38 4525003a53a7fc63ca75c59b22c79608659ca12f0131f52c18637f829977f20b
zlib version 1.3.1
gcc-13.3.0 (64-bit)
sqlite> .quit
Piped to STDIN
$ echo .version | sqlite3
SQLite 3.53.0 2026-04-09 11:41:38 4525003a53a7fc63ca75c59b22c79608659ca12f0131f52c18637f829977f20b
zlib version 1.3.1
gcc-13.3.0 (64-bit)
Redirect from file
.version
$ sqlite3 < examples/version.sql
SQLite 3.53.0 2026-04-09 11:41:38 4525003a53a7fc63ca75c59b22c79608659ca12f0131f52c18637f829977f20b
zlib version 1.3.1
gcc-13.3.0 (64-bit)
Command line flag
$ sqlite3 --version
3.53.0 2026-04-09 11:41:38 4525003a53a7fc63ca75c59b22c79608659ca12f0131f52c18637f829977f20b (64-bit)
SQLite CLI - external commands .shell
To run any external (shell) command use .shell.
On Linux / macOS you have ls:
sqlite> .shell ls
On Windows you have dir:
sqlite> .shell dir
SQLite CLI - .schema and .tables
Using the .tables command we can see the list of tables and using the .schema command we can see the whole definition.
.schema
.tables
SQLite CLI - .mode
The .mode controls how results of SELECT statements are displayed.
Show the current mode
sqlite> .mode
.mode qbox --limits on --quote relaxed --sw auto --textjsonb on
List available modes
sqlite> .mode --list
available modes: ascii box c column count csv html insert jatom jobject json
line list markdown off psql qbox quote split table tabs tcl batch tty
Set a mode:
sqlite> .mode column
SQLite in-memory or file-based
- Using in-memory database
$ sqlite3
- Using a persistent database file. If the file does not exist, this command will create it.
$ sqlite3 my.db
DDL - Data Definition Language
- CREATE TABLE
- ALTER TABLE
- DROP TABLE
CREATE TABLE
CREATE TABLE person (
id INTEGER,
name TEXT
);
.schema
$ sqlite < examples/create-table.sql
CREATE TABLE person (
id INTEGER,
name TEXT
);
ALTER TABLE
Using ALTER TABLE we can modify the schema.
CREATE TABLE person (
id INTEGER,
name TEXT
);
.schema
ALTER TABLE person ADD COLUMN phone TEXT;
.schema
$ sqlite3 < examples/alter-table.sql
CREATE TABLE person (
id INTEGER,
name TEXT
);
CREATE TABLE person (
id INTEGER,
name TEXT
, phone TEXT);
DROP TABLE
DROP TABLE will remove the table and all its content. In this example we also used the .schema command to show what is the current schema
and the .mode batch to make the SELECT statement show a simple line.
.mode batch
CREATE TABLE person (
id INTEGER,
name TEXT
);
SELECT '-------';
.schema
DROP TABLE person;
SELECT '-------';
.schema
SELECT '-------';
$ sqlite < examples/drop-table.sql
-------
CREATE TABLE person (
id INTEGER,
name TEXT
);
-------
-------
DML - Data Manipulation Language - Basic Operations
- CREATE
- INSERT
- SELECT
- UPDATE
CREATE - INSERT - SELECT
With CREATE we define the schema.
With INSERT we add a row.
With SELECT we can fetch data from the database.
CREATE TABLE person (
id INTEGER,
name TEXT,
email TEXT
);
INSERT INTO person (id, name, email) VALUES (1, 'Joe', 'joe@example.com');
INSERT INTO person (id, name, email) VALUES (2, 'Jane', 'jane@example.com');
SELECT * FROM person;
In memory:
$ sqlite3 < create-insert-select.sql
1|Joe|joe@example.com
2|Jane|jane@example.com
In file:
$ sqlite3 demo.db < create-insert-select.sql
1|Joe|joe@example.com
2|Jane|jane@example.com
Then, if we don’t need it any more, we should remove the database file:
$ rm -f demo.db
SELECT only some of the columns
Instead of using a * to fetch all the columns, we can define the specific columns we would like to fetch.
CREATE TABLE person (
id INTEGER,
name TEXT,
email TEXT
);
INSERT INTO person (id, name, email) VALUES (1, 'Joe', 'joe@example.com');
INSERT INTO person (id, name, email) VALUES (2, 'Jane', 'jane@example.com');
INSERT INTO person (id, name, email) VALUES (3, 'Mary', 'mary@example.com');
INSERT INTO person (id, name, email) VALUES (4, 'Peter', 'peter@example.com');
SELECT id, name FROM person;
$ sqlite3 < examples/select-some-columns.sql
1|Joe
2|Jane
3|Mary
4|Peter
SELECT only some of the rows - WHERE clause
We can also use the WHERE close to select which rows we would like to see.
CREATE TABLE person (
id INTEGER,
name TEXT,
email TEXT
);
INSERT INTO person (id, name, email) VALUES (1, 'Joe', 'joe@example.com');
INSERT INTO person (id, name, email) VALUES (2, 'Jane', 'jane@example.com');
INSERT INTO person (id, name, email) VALUES (3, 'Mary', 'mary@example.com');
INSERT INTO person (id, name, email) VALUES (4, 'Peter', 'peter@example.com');
SELECT * FROM person WHERE id > 2;
$ sqlite < examples/select-some-rows.sql
3|Mary|mary@example.com
4|Peter|peter@example.com
AUTOINCREMENT
Keeping in mind to increase the id number can be annoying. SQL provides the AUTOINCREMENT option
that can help us maintain that value. However in SQLite it is not necessary to use and it isn’t even recommended.
In any case let’s see an example:
CREATE TABLE person (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT,
email TEXT
);
INSERT INTO person (name, email) VALUES ('Joe', 'joe@example.com');
INSERT INTO person (name, email) VALUES ('Jane', 'jane@example.com');
INSERT INTO person (name, email) VALUES ('Mary', 'mary@example.com');
INSERT INTO person (name, email) VALUES ('Peter', 'peter@example.com');
SELECT * from person;
.schema
$ sqlite3 < examples/autoincrement.sql
1|Joe|joe@example.com
2|Jane|jane@example.com
3|Mary|mary@example.com
4|Peter|peter@example.com
CREATE TABLE person (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT,
email TEXT
);
CREATE TABLE sqlite_sequence(name,seq);
Invisible AUTOINCREMENT (ROWID)
If we have a column that is marked as INTEGER PRIMARY KEY, it will get incremented automatically. See the AUTOINCREMENT description for details.
CREATE TABLE person (
id INTEGER PRIMARY KEY,
name TEXT,
email TEXT
);
INSERT INTO person (name, email) VALUES ('Joe', 'joe@example.com');
INSERT INTO person (name, email) VALUES ('Jane', 'jane@example.com');
INSERT INTO person (name, email) VALUES ('Mary', 'mary@example.com');
INSERT INTO person (name, email) VALUES ('Peter', 'peter@example.com');
SELECT * from person;
.schema
$ sqlite3 < examples/invisible-autoincrement.sql
1|Joe|joe@example.com
2|Jane|jane@example.com
3|Mary|mary@example.com
4|Peter|peter@example.com
CREATE TABLE person (
id INTEGER PRIMARY KEY,
name TEXT,
email TEXT
);
Missing value
When we insert data, some columns (besides the id) might be missing. SQLite will insert a NULL value.
CREATE TABLE qa (
question TEXT,
answer TEXT
);
INSERT INTO qa (question, answer) VALUES ('Language?', 'SQL');
INSERT INTO qa (question, answer) VALUES ('Database?', 'SQLite');
INSERT INTO qa (question) VALUES ('Meaning of life?');
SELECT * from qa;
$ sqlite3 < examples/missing-text-value.sql
Language?|SQL
Database?|SQLite
Meaning of life?|
This is hard to see in the above display, but in the interactive shell or if we use a real terminal it will look much better:
╭──────────────────┬────────╮
│ question │ answer │
╞══════════════════╪════════╡
│ Language? │ SQL │
│ Database? │ SQLite │
│ Meaning of life? │ NULL │
╰──────────────────┴────────╯
Missing numerical value
CREATE TABLE qa (
question TEXT,
answer INTEGER
);
INSERT INTO qa (question, answer) VALUES ('2+2', 4);
INSERT INTO qa (question, answer) VALUES ('2-2', 0);
INSERT INTO qa (question) VALUES ('Meaning of life?');
SELECT * from qa;
$ sqlite3 < examples/missing-integer-value.sql
2+2|4
2-2|0
Meaning of life?|
╭──────────────────┬────────╮
│ question │ answer │
╞══════════════════╪════════╡
│ 2+2 │ 4 │
│ 2-2 │ 0 │
│ Meaning of life? │ NULL │
╰──────────────────┴────────╯
Field with DEFAULT value
We can set up a DEFAULT value to each field and if the INSERT does not set the value then this default value will be set.
However, if the INSERT explicitly sets NULL then that will be the value.
CREATE TABLE qa (
question TEXT,
answer TEXT DEFAULT 42
);
INSERT INTO qa (question, answer) VALUES ('Language?', 'SQL');
INSERT INTO qa (question, answer) VALUES ('Database?', 'SQLite');
INSERT INTO qa (question) VALUES ('Meaning of life?');
INSERT INTO qa (question, answer) VALUES ('What is void?', NULL);
SELECT * from qa;
$ sqlite3 < examples/default-value.sql
Language?|SQL
Database?|SQLite
Meaning of life?|42
What is void?|
╭──────────────────┬────────╮
│ question │ answer │
╞══════════════════╪════════╡
│ Language? │ SQL │
│ Database? │ SQLite │
│ Meaning of life? │ '42' │
│ What is void? │ NULL │
╰──────────────────┴────────╯
NOT NULL constraint
Here we have two tables. In the restricted table we set one of the fields to be NOT NULL.
CREATE TABLE plain (
id INTEGER,
name TEXT
);
INSERT INTO plain (id, name) VALUES (1, 'Joe');
INSERT INTO plain (name) VALUES ('Jane');
SELECT * from plain;
CREATE TABLE restricted (
id INTEGER NOT NULL,
name TEXT
);
INSERT INTO restricted (id, name) VALUES (1, 'Joe');
INSERT INTO restricted (name) VALUES ('Jane');
INSERT INTO restricted (id, name) VALUES (NULL, 'Mary');
SELECT * from restricted;
$ sqlite3 < examples/not-null.sql
1|Joe
|Jane
Error near line 18: NOT NULL constraint failed: restricted.id
Error near line 19: NOT NULL constraint failed: restricted.id
1|Joe
UPDATE
With UPDATE we can change one or more fields in one or more rows.
.mode batch
CREATE TABLE people (
name TEXT,
grade INTEGER
);
INSERT INTO people (name, grade) VALUES ('Joe', 40);
INSERT INTO people (name, grade) VALUES ('Jane', 60);
SELECT * from people;
SELECT '-------';
-- SELECT 'Setting the grade of Joe to be 44';
UPDATE people SET grade = 44;
SELECT * from people;
SELECT '-------';
UPDATE people SET grade = 100 WHERE name = 'Joe';
SELECT * from people;
SELECT '-------';
$ sqlite < examples/update.sql
Joe|40
Jane|60
-------
Joe|44
Jane|44
-------
Joe|100
Jane|44
-------
SELECT without table
$ echo "SELECT 2 + 3" | sqlite3
$ echo "SELECT "Hello World'" | sqlite3
SELECT date and time
SELECT "date", date();
SELECT "time", time();
SELECT "datetime", datetime();
SELECT "julianday",julianday();
SELECT "unixepoch", unixepoch();
SELECT "strftime", strftime();
SELECT "strftime with param", strftime("%Y-%m-%d %H:%M:%S");
--SELECT timediff("%Y");
$ sqlite3 < date-and-time.sql
date|2026-04-28
time|09:40:04
datetime|2026-04-28 09:40:04
julianday|2461158.90283096
unixepoch|1777369204
strftime|
strftime with param|2026-04-28 09:40:04
Import data from CSV
Import iris.csv
$ sqlite3
sqlite> .import --csv -v iris.csv iris
CREATE TABLE "main"."iris"(
"Id" TEXT, "SepalLengthCm" TEXT, "SepalWidthCm" TEXT, "PetalLengthCm" TEXT,
"PetalWidthCm" TEXT, "Species" TEXT)
Added 150 rows with 0 errors using 151 lines of input
sqlite> .schema
CREATE TABLE IF NOT EXISTS "iris"(
"Id" TEXT, "SepalLengthCm" TEXT, "SepalWidthCm" TEXT, "PetalLengthCm" TEXT,
"PetalWidthCm" TEXT, "Species" TEXT);
sqlite> SELECT COUNT(*) FROM iris;
150
sqlite> SELECT AVG(SepalLengthCm) FROM iris;
5.84333333333333
sqlite> SELECT AVG(PetalLengthCm) FROM iris;
3.75866666666667
sqlite> SELECT AVG(PetalLengthCm), Species FROM iris GROUP BY Species;
1.464|Iris-setosa
4.26|Iris-versicolor
5.552|Iris-virginica
sqlite> DROP TABLE iris;
Load schema and Import iris.csv
CREATE TABLE iris (
Id INTEGER,
SepalLengthCm REAL,
SepalWidthCm REAL,
PetalLengthCm REAL,
PetalWidthCm REAL,
Species TEXT
);
sqlite> .schema
sqlite> .read iris.sql
sqlite> .import --csv --skip 1 -v iris.csv iris
Added 150 rows with 0 errors using 151 lines of input
sqlite> .schema
CREATE TABLE iris (
Id INTEGER,
SepalLengthCm REAL,
SepalWidthCm REAL,
PetalLengthCm REAL,
PetalWidthCm REAL,
Species TEXT
);
Import planets.csv
Planet name,Distance (AU),Mass
Mercury,0.4,0.055
Venus,0.7,0.815
Earth,1,1
Mars,1.5,0.107
Ceres,2.77,0.00015
Jupiter,5.2,318
Saturn,9.5,95
Uranus,19.6,14
Neptune,30,17
Pluto,39,0.00218
Charon,39,0.000254
sqlite> .import --csv planets.csv planets
sqlite> .schema
CREATE TABLE IF NOT EXISTS "planets"(
"Planet name" TEXT, "Distance (AU)" TEXT, "Mass" TEXT);
sqlite> select "Distance (AU)" from planets;
Flexible typing
The Advantages Of Flexible Typing
CREATE TABLE flexible_data (
id INTEGER,
name TEXT,
height REAL
);
INSERT INTO flexible_data (id, name, height) VALUES (1, "foo", 1.8);
INSERT INTO flexible_data (id, name, height) VALUES ("id", 23, "tall");
SELECT * from flexible_data;
Database normalization
- First Normal Form (1NF) - every column should store only one value.
- Second Normal Form (2NF) - the same value should not be repeated in the column.
- Third Normal Form (3NF)
- BCNF
Flat table
CREATE TABLE person (
name TEXT,
subject TEXT,
grade INTEGER
);
INSERT INTO person (name, subject, grade) VALUES
('Jane', 'Chemistry', 62),
('Jane', 'Physics', 70),
('Jane', 'Math', 23),
('Joe', 'Chemistry', 45),
('Joe', 'Physics', 85),
('Joe', 'Math', 77),
('Peter', 'Chemistry', 35),
('Peter', 'Physics', 63),
('Mary', 'Chemistry', 95),
('Dean', 'Physics', 100),
('Dana', 'Math', 97),
('George', 'Math', 97),
('George', 'Chemistry', 45);
SELECT * FROM person;
Two tables
-- enforce foreignk key integrity
PRAGMA foreign_keys = ON;
CREATE TABLE subject (
id INTEGER PRIMARY KEY,
name TEXT UNIQUE NOT NULL
);
CREATE TABLE person (
name TEXT,
subject_id INTEGER REFERENCES subject(id),
grade INTEGER
);
INSERT INTO subject (id, name) VALUES
(1, 'Chemistry'),
(2, 'Physics'),
(3, 'Math');
INSERT INTO person (name, subject_id, grade) VALUES
('Jane', 1, 62),
('Jane', 2, 70),
('Jane', 3, 23),
('Joe', 1, 45),
('Joe', 2, 85),
('Joe', 3, 77),
('Peter', 1, 35),
('Peter', 2, 63),
('Mary', 1, 95),
('Dean', 2, 100),
('Dana', 3, 97),
('Gerorge', 3, 97),
('Gerorge', 1, 45);
SELECT * FROM subject;
SELECT * FROM person;
SELECT person.name, subject.name AS subject, person.grade
FROM person, subject
WHERE person.subject_id = subject.id;
SELECT person.name, subject.name AS subject, person.grade
FROM person JOIN subject
WHERE person.subject_id = subject.id;
SELECT person.name, subject.name AS subject, person.grade
FROM person INNER JOIN subject
WHERE person.subject_id = subject.id;
FOREIGN KEY
-- turn on FOREIGN KEY checking
PRAGMA foreign_keys = ON;
CREATE TABLE people (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT
);
CREATE TABLE groups (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT,
manager INTEGER,
FOREIGN KEY(manager) REFERENCES people(id)
);
INSERT INTO people (name) VALUES ('Natalie Portman ');
INSERT INTO people (name) VALUES ('Gal Gadot');
INSERT INTO people (name) VALUES ('Cole Lawrence');
INSERT INTO people (name) VALUES ('Lior Raz');
INSERT INTO people (name) VALUES ('Erick Tryzelaar');
INSERT INTO people (name) VALUES ('Ernest Kissiedu');
SELECT * FROM people;
SELECT "-----";
INSERT INTO groups (name, manager) VALUES ('Rust London', (SELECT id FROM people WHERE name = 'Ernest Kissiedu'));
INSERT INTO groups (name, manager) VALUES ('Rust NYC', (SELECT id FROM people WHERE name = 'Cole Lawrence'));
INSERT INTO groups (name, manager) VALUES ('Rust Bay Area', (SELECT id FROM people WHERE name = 'Erick Tryzelaar'));
INSERT INTO groups (name, manager) VALUES ('Other Group', 42);
SELECT * FROM people;
SELECT "";
SELECT * FROM groups;
SELECT "-----";
DELETE FROM people WHERE name = 'Cole Lawrence';
SELECT * FROM people;
SELECT "";
SELECT * FROM groups;
SELECT "-----";
PRAGMA
Family
PRAGMA foreign_keys = ON;
CREATE TABLE people (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT,
father INTEGER,
mother INTEGER,
FOREIGN KEY (father) REFERENCES people(id),
FOREIGN KEY (mother) REFERENCES people(id)
);
INSERT INTO people (name) VALUES
('Abraham'),
('Hagar'),
('Sarah'),
('Keturah'),
('Ishmael'),
('Isaac'),
('Rebecca'),
('Jacob');
UPDATE people SET
father = (SELECT id FROM people WHERE name = 'Abraham'),
mother =(SELECT id FROM people WHERE name = 'Sarah')
WHERE name = 'Isaac';
UPDATE people SET
father = (SELECT id FROM people WHERE name = 'Abraham'),
mother =(SELECT id FROM people WHERE name = 'Hagar')
WHERE name = 'Ishmael';
UPDATE people SET
father = (SELECT id FROM people WHERE name = 'Isaac'),
mother =(SELECT id FROM people WHERE name = 'Rebecca')
WHERE name = 'Jacob';
UPDATE people SET father = 32;
SELECT child.name AS Name, father.name AS Father, mother.name AS Mother
FROM people child, people father, people mother
WHERE father.id = child.father AND mother.id = child.mother
-- ORDER BY child.id;
ORDER BY child.name;
-- SELECT child.name AS Name,
-- IFNULL(father.name, 'NULL') AS Father,
-- IFNULL(mother.name, 'NULL') AS Mother
-- FROM people AS child
-- LEFT JOIN people AS father ON father.id = child.father
-- LEFT JOIN people AS mother ON mother.id = child.mother
-- ORDER BY child.id;
--
-- CREATE VIEW family AS
-- SELECT child.name AS Name,
-- IFNULL(father.name, 'NULL') AS Father,
-- IFNULL(mother.name, 'NULL') AS Mother
-- FROM people AS child
-- LEFT JOIN people AS father ON father.id = child.father
-- LEFT JOIN people AS mother ON mother.id = child.mother
-- ORDER BY child.id;
--
-- SELECT * FROM family;
--
-- INSERT INTO people (name) VALUES ('Gabor');
-- SELECT * FROM family;
INDEX and EXPLAIN
.timer on
.import --csv -v examples/big-data.csv pairs
SELECT COUNT(*) FROM pairs;
SELECT * from pairs WHERE name = 'aaaaaaaz';
EXPLAIN SELECT * from pairs WHERE name = 'aaaaaaaz';
CREATE INDEX names ON pairs (name);
SELECT * from pairs WHERE name = 'aaaaaaaz';
EXPLAIN SELECT * from pairs WHERE name = 'aaaaaaaz';
perl generat.pl
$ sqlite3 < examples/bug-data.sql
STRICT Tables
If you really want, you can defined a table to be STRICT, but you need to do that on a per-table basis and the data types you can use are limited.
CREATE TABLE strict_data (
id INTEGER,
name TEXT,
height REAL
) STRICT;
INSERT INTO strict_data (id, name, height) VALUES (1, "foo", 1.8);
INSERT INTO strict_data (id, name, height) VALUES ("2", "bar", "2.1");
INSERT INTO strict_data (id, name, height) VALUES ("three", "Jane", 1.3);
-- Runtime error near line 10: cannot store TEXT value in INTEGER column strict_data.id (19)
SELECT * from strict_data;
Default time
CREATE TABLE login (
uid INTEGER NOT NULL,
logged_in DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO login (uid) VALUES (1);
INSERT INTO login (uid, logged_in) VALUES (2, '2026-04-29 08:06:36');
SELECT * from login;
Aggregate: COUNT, AVG, MAX, MIN
CREATE TABLE person (
name TEXT,
subject TEXT,
grade INTEGER
);
INSERT INTO person (name, subject, grade) VALUES
('Jane', 'Chemistry', 62),
('Jane', 'Physics', 70),
('Jane', 'Math', 23),
('Joe', 'Chemistry', 45),
('Joe', 'Physics', 85),
('Joe', 'Math', 77),
('Peter', 'Chemistry', 35),
('Peter', 'Physics', 63),
('Mary', 'Chemistry', 95),
('Dean', 'Physics', 100),
('Dana', 'Math', 97),
('George', 'Math', 97),
('George', 'Chemistry', 45);
SELECT * FROM person;
SELECT COUNT(*) FROM person;
SELECT "Total", COUNT(*) FROM person;
SELECT "Physycs", COUNT(*) FROM person WHERE subject == "Physics";
SELECT "Failed", COUNT(*) FROM person WHERE grade < 60;
SELECT "Maximum all", MAX(grade) FROM person;
SELECT "Maximum Chemistry", MAX(grade) FROM person WHERE subject == "Chemistry";
SELECT "Average all", AVG(grade) FROM person;
SELECT "Average Chemistry", AVG(grade) FROM person WHERE subject == "Chemistry";
SELECT "Average Physics", AVG(grade) FROM person WHERE subject == "Physics";
$ cat grades.sql aggregate.sql | sqlite3
SQLite Transaction - in a bank
- Setup Bank accounts with some initial money.
- Move some money from one account to another - two separate steps. Worked.
- Move some money from one account to another - two separate steps - stop in the middle. Failed.
- Move some money from one account to another - Transaction - stop in the middle. Worked. (money stayed where it was)
- Remove bank
CREATE TABLE bank (
name TEXT PRIMARY KEY,
balance INTEGER NOT NULL
);
INSERT INTO bank (name, balance) VALUES ("Jane", 0);
INSERT INTO bank (name, balance) VALUES ("Mary", 1000);
INSERT INTO bank (name, balance) VALUES ("Ann", 1000);
SELECT * FROM bank;
SELECT "Total", SUM(balance) FROM bank;
SELECT "-----";
UPDATE bank SET balance = (SELECT balance FROM bank WHERE name = "Mary") - 100 WHERE name = "Mary";
UPDATE bank SET balance = (SELECT balance FROM bank WHERE name = "Jane") + 100 WHERE name = "Jane";
UPDATE bank SET balance = (SELECT balance FROM bank WHERE name = "Mary") - 100 WHERE name = "Mary";
.exit
UPDATE bank SET balance = (SELECT balance FROM bank WHERE name = "Jane") + 100 WHERE name = "Jane";
BEGIN TRANSACTION;
UPDATE bank SET balance = (SELECT balance FROM bank WHERE name = "Mary") - 100 WHERE name = "Mary";
.exit
UPDATE bank SET balance = (SELECT balance FROM bank WHERE name = "Jane") + 100 WHERE name = "Jane";
COMMIT;
sqlite3 bank.db < setup_bank.sql
sqlite3 bank.db < show.sql
sqlite3 bank.db < transfer.sql
sqlite3 bank.db < show.sql
sqlite3 bank.db < without_transaction.sql
sqlite3 bank.db < show.sql
sqlite3 bank.db < with_transaction.sql
sqlite3 bank.db < show.sql
rm -f bank.db
Jane|0
Mary|1000
Ann|1000
Total|2000
-----
Jane|100
Mary|900
Ann|1000
Total|2000
-----
Jane|100
Mary|800
Ann|1000
Total|1900
-----
Jane|100
Mary|800
Ann|1000
Total|1900
-----
- TODO: loading a large CSV file into the database and running queries.
- TODO: creating a multi-tabe database, dumping it and then loading it and running queries against it.
- TODO: FOREIGN KEY - cascading deletition?
DISTINCT
.import --csv examples/iris.csv iris
SELECT COUNT(species) FROM iris;
SELECT COUNT(DISTINCT species) FROM iris;
$ sqlite3 < handle-iris.sql
LIMIT
.import --csv examples/iris.csv iris
SELECT * FROM iris LIMIT 3;
sqlite3 < limit-iris.sql
Increment value
CREATE TABLE people (
name TEXT,
grade INTEGER
);
INSERT INTO people (name, grade) VALUES ('Joe', 40);
INSERT INTO people (name, grade) VALUES ('Jane', 60);
SELECT * from people;
SELECT "-------";
SELECT "Incrementing the grade of Joe by 1";
UPDATE people SET grade = (SELECT grade FROM people WHERE name = "Joe") + 1 WHERE name = "Joe";
SELECT * from people;
SELECT "";
ALTER TABLE after adding data
CREATE TABLE person (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT
);
INSERT INTO person (name) VALUES ('Joe');
INSERT INTO person (name) VALUES ('Jane');
SELECT * FROM person;
ALTER TABLE person ADD COLUMN phone TEXT;
SELECT * FROM person;
INSERT INTO person (name, phone) VALUES ('Marcus', '1234565');
SELECT * FROM person;
ALTER TABLE not NULL
What if we would like to add a column that should be NOT NULL?
We cannot add a column that is NOT NULL as it would immediately violate the integriry of the table.
We can first add the column. Then update the existing rows, then change the column definition.
CREATE TABLE person (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT
);
INSERT INTO person (name) VALUES ('Joe');
INSERT INTO person (name) VALUES ('Jane');
SELECT * FROM person;
-- ALTER TABLE person ADD COLUMN phone TEXT NOT NULL;
-- Error near line 11: Cannot add a NOT NULL column with default value NULL
ALTER TABLE person ADD COLUMN phone TEXT;
UPDATE person SET phone = '123' WHERE name = 'Joe';
UPDATE person SET phone = '567' WHERE name = 'Jane';
SELECT * FROM person;
ALTER TABLE person ALTER phone SET NOT NULL;
-- INSERT INTO person (name) VALUES ('Mary');
-- Error near line 21: NOT NULL constraint failed: person.phone
INSERT INTO person (name, phone) VALUES ('Mary', '890');
SELECT * FROM person;
SQLite in Python
Python comes with the sqlite3 module that wraps (embeds) the C library implementing SQLite.