A Database and Setting Up MySQL: Tables, Keys, Docker and a Driver
- Explain why you need a Database and what the problems of saving to a JSON file in a real system are
- Define Table, Row, Column and Primary Key and recognize them in an example
- Know the basic data types — INT, VARCHAR, DECIMAL — and NOT NULL
- Translate a Python model (a Class or a dict) into a table design
- Explain what a database server is, and why it is run in a Docker container
- Run MySQL with docker run, and start it again with docker start
- Connect to the database from VS Code with the Database Client extension and run SQL in it
- Install the Driver, connect from Python, and know why the password is not written in the code
In this lesson
This lesson is built from two chapters, in the same order as the slides:
- Chapter 1 — what a Database is: table, row, column, Primary Key and data types
- Chapter 2 — setting up MySQL: Docker, Database Client and a Driver
Chapter 1: What a Database Is — Table, Row, Column, Primary Key and Data Types
What will we learn?
- Why you need a Database.
- What a Relational Database is.
- Table, Row, Column.
- Primary Key.
- Basic Data Types.
Here we learn only the basics needed for CRUD: a connection, one table, and creating, reading, updating and deleting. We will not learn JOIN, GROUP BY, HAVING, Sub-Queries, advanced normalization, indexes or Stored Procedures. You will learn those in depth later in the track.
Why do we need this?
In the files topic we saved data in a JSON file. That worked for one program running on our computer. On a real server, it breaks:
| A problem with a JSON file | What happens |
|---|---|
| All or nothing when reading | To find one student out of a million, you load the whole file into RAM. |
| All or nothing when writing | To change one field, you write the whole file again. |
| Two users at once | Two requests read the file, each one changes something, and each one writes. The first one’s change is deleted. |
| A crash in the middle of writing | The file stays half written, and broken. |
| No rules | Nothing prevents two students with the same email, or a missing field. |
| Queries | “All the students from Haifa over age 20” = a loop in our code, over all the data. |
A Database solves all of these: reading and writing exactly what is needed, handling many users at the same time, protection against crashes, rules enforced in the database itself, and a language (SQL) for queries.
Explanation from the basics
What is a Database?
Database = a system for organized storage of data, which lets you save, find, update and delete it efficiently and reliably. The software that manages it is called a DBMS (Database Management System): MySQL, PostgreSQL, SQLite…
Relational Database — tables
The most common kind is a relational database: the data is organized in tables, like an Excel sheet with strict rules.
Table: students
┌────┬────────┬─────┬────────────────┐
│ id │ name │ age │ email │ ← columns (the structure)
├────┼────────┼─────┼────────────────┤
│ 1 │ Dana │ 24 │ dana@mail.com │ ← row = one student
│ 2 │ Avi │ 31 │ avi@mail.com │ ← row
│ 3 │ Noa │ 22 │ noa@mail.com │ ← row
└────┴────────┴─────┴────────────────┘
↑
primary key| Term | What it is | In Python (what we already know) |
|---|---|---|
| Table | A collection of records of the same kind | A Class (the template) + a list of all the objects |
| Column | One field, with a fixed name and type | An Attribute |
| Row (record) | One item | One object / one dict |
| Primary Key | A column that identifies every row uniquely | student_id |
Database = the cabinet. Table = one drawer (“students”). Columns = the printed form every file is built on (name, age, email), identical for all the files. Row = one file, of one student. Primary Key = the number on the back of the file: it never repeats, and by it you take out a file without going through all of them.
Primary Key
Primary Key = a column (usually id) whose value is:
- Unique — no two rows have the same value.
- Not empty — every row must have a value.
- Not changing — an identifier is for the whole life of the row.
Why not use the name? Because there are two “Dana"s. And the email? Because people change their email. A numeric identifier that the database produces by itself is the safe choice. (Remember the Shop that produced identifiers with max + 1? The database does it for us, and more safely.)
And that is exactly the {student_id} in the API’s path: GET /students/7 = “bring the row whose Primary Key is 7”.
Data Types — basic data types
Every column has a type. In MySQL (the database we will use) there are many types, but this is what we will need:
| SQL type | What is stored | Python | Example |
|---|---|---|---|
| INT | A whole number | int | id, age, quantity |
| DECIMAL(10, 2) | An exact decimal number — 2 digits after the point | Decimal (like float, but exact) | price |
| VARCHAR(100) | Text — up to 100 characters | str | name, email |
| (no real BOOLEAN) | Store 0/1 in an INT | bool | is_active |
| NULL | “No value” | None | An empty optional field |
Basic Constraints
Rules that the database enforces. Even if there is a bug in the code, the database will refuse:
| Constraint | Meaning |
|---|---|
PRIMARY KEY | The row’s unique identifier. |
AUTO_INCREMENT | The database produces the id by itself, in ascending order. |
NOT NULL | It must be filled in. |
UNIQUE | No two rows with the same value (for example an email). |
It is like validation in __init__ (the OOP topic), only the database itself enforces it: another layer of protection.
SQL — the language
SQL (Structured Query Language, pronounced “sequel” or “S-Q-L”) = the language in which you talk to a relational database. Query = one command in SQL. We will meet only four or five commands:
CREATE TABLE → create the structure (day 3)
INSERT → C - add a row (day 3)
SELECT → R - read rows (day 3)
UPDATE → U - change rows (day 4)
DELETE → D - remove rows (day 4)Syntax
A table design (the product of the design stage, before any code). This is what a design of the table students looks like:
Table: students
| column | type | constraints |
|--------|--------------|-----------------------------|
| id | INT | AUTO_INCREMENT PRIMARY KEY |
| name | VARCHAR(100) | NOT NULL |
| age | INT | NOT NULL |
| email | VARCHAR(100) | NOT NULL UNIQUE |And this is the SQL command that creates it (we will write and run it in the next lessons; here it is only to see where we are heading):
CREATE TABLE students (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
age INT NOT NULL,
email VARCHAR(100) NOT NULL UNIQUE
);First code example
From a Python Class to a table design. The Class Product from the OOP topic:
class Product:
def __init__(self, sku, name, price, stock=0):
...The translation:
| Attribute in Python | Column | Type | Constraints | Why |
|---|---|---|---|---|
| — | id | INT | AUTO_INCREMENT PRIMARY KEY | A stable internal identifier |
sku | sku | VARCHAR(20) | NOT NULL UNIQUE | Unique, required |
name | name | VARCHAR(100) | NOT NULL | Required |
price | price | DECIMAL(10, 2) | NOT NULL | An exact decimal number |
stock | stock | INT | NOT NULL DEFAULT 0 | A whole number, default 0 |
Breaking down the code line by line
| Decision | Reasoning |
|---|---|
Adding id even though there is a sku | sku is a business identifier that may change (rebranding). id is a technical identifier that never changes. It is common to keep both. |
sku with UNIQUE | The rule “the SKU is unique” is enforced in the database, even if other code forgets to check. |
price as DECIMAL(10, 2) | A price with agorot. DECIMAL stores the number exactly — without the rounding problems of float (0.1 + 0.2). |
| What does not go into the table | The methods (take, restock) are behavior, and they live in the code. The database stores data only. |
Another example
From dicts to a table, and “messy” types. Data that arrived from a JSON file:
students = [
{"name": "Dana", "age": 24, "email": "dana@mail.com", "phone": "050-1234567"},
{"name": "Avi", "age": "31", "email": "avi@mail.com"},
{"name": "Noa", "age": 22, "email": "dana@mail.com", "phone": None},
]Design questions:
phoneappears only in some of the rows. → a column withoutNOT NULL. When there is no value, NULL is stored (Nonein Python).- Avi’s
ageis"31"(a string). → the column isINT. In the code we will convertint(...)before saving (or Pydantic will convert, in the students API topic). - Noa and Dana have the same email. → with
UNIQUEonemail, the database will refuse to save Noa. That is exactly what we want (and we will handle this error in the next lessons). - There is no
id. →AUTO_INCREMENT: the database will give 1, 2, 3.
| column | type | constraints |
|--------|--------------|----------------------------|
| id | INT | AUTO_INCREMENT PRIMARY KEY |
| name | VARCHAR(100) | NOT NULL |
| age | INT | NOT NULL |
| email | VARCHAR(100) | NOT NULL UNIQUE |
| phone | VARCHAR(20) | (nullable) |Why is phone a VARCHAR and not an INT? Because 050-1234567 contains a hyphen, and a phone number that starts with 0 would lose the zero as an INT. Rule: a “number” you don’t do arithmetic on (a phone, a zip code, an ID number) → VARCHAR.
Common mistake
Mistake 1: a “natural” Primary Key that may change or repeat. A name, an email, a phone number. The day Dana changes her email, everyone who pointed to her by email “loses” her. A numeric id from the database.
Mistake 2: storing a list inside one column.
| id | name | grades |
| 1 | Dana | "90,85,70" |You cannot calculate an average in SQL, you cannot search for “everyone who got 90”, and it is hard to update one grade. In the relational world, grades are a separate table (grades, with a student_id). You will learn this in depth later in the track (relationships between tables and JOIN). Here we will work with one table and simple fields.
Mistake 3: giving up NOT NULL and UNIQUE, “because the code checks”. The code checks today. Tomorrow someone will write an import script that bypasses the code, or there will be a bug. The database is the last line of defense.
Mistake 4: thinking SQL is “a program”. SQL is a Declarative language: you say what you want (“all the students from Haifa”), and not how to find it (no loops). The database decides how to perform it efficiently.
Summary
- A Database solves the problems of the JSON file: focused reading and writing, many users at the same time, resistance to crashes, rules and queries.
- A relational database = tables. Table ≈ a Class + all the objects. Column ≈ an Attribute (with a type). Row ≈ one object.
- Primary Key = a unique identifier, not empty and not changing. Usually
id INT AUTO_INCREMENT PRIMARY KEY. - Types in MySQL: INT, DECIMAL, VARCHAR, and NULL (=
None). A bool is stored as 0/1. A phone and a zip code are stored as VARCHAR. - Constraints: NOT NULL, UNIQUE. The database enforces rules even when the code makes a mistake.
- SQL = a declarative language. Here: CREATE TABLE, INSERT, SELECT, UPDATE, DELETE, and that’s it.
Official documentation
Chapter 2: Setting Up MySQL — Docker, Database Client and a Driver
What will we learn?
- What a database server is, and why we will work with MySQL.
- What a container is, and how to run MySQL in Docker — with one command.
- How to see the database from VS Code with the Database Client extension.
- What a Driver is, and how Python connects to MySQL.
- Where to keep the connection details — and why not in the code.
Why do we need this?
Before writing a single INSERT, you need to answer “plumbing” questions: where does the database run? How does our code get to it? What are the connection details? In real work, the first thing you get in a new project is exactly this: “here are the connection details for the database”. You need to know what to do with them.
Explanation from the basics
A database is a server
MySQL is a database server: a separate program (a process — remember the operating system topic!) that runs all the time and listens on a port — in MySQL that is port 3306. Our code does not open a file — it connects over the network to the server, exactly like a browser that connects to a website (the networking topic: IP + Port).
| MySQL (a database server) | |
|---|---|
| Where | A separate process that listens on port 3306 |
| How you connect | Over the network: Host, Port, user, password and the database’s name |
| Who connects | Several programs and servers — at the same time |
| Where this is in the world | Websites and systems of every size. PostgreSQL and SQL Server also work this way |
Why Docker?
Installing MySQL directly on the computer is a long process: an installation wizard, passwords, a service that runs in the background, and settings that differ between Windows and Mac. And what works for one person — does not always work for another.
Docker (which we installed in the first week) solves this: it runs ready-made programs inside a container.
- Image — the template: “MySQL version 8.4, ready to work”. You download it once.
- Container — a running copy of the Image. Isolated from the rest of the computer, with everything the program needs.
A shipping container looks the same on every ship and at every port — no matter what is inside it. So does a Docker container: the same MySQL, with the same settings, runs exactly the same for all the students, on every operating system.
Database Client — the database with an interface
You can see and operate MySQL without a single line of code, with a VS Code extension named Database Client (by Weijan Chen). It shows a tree of databases and tables, opens a table like a spreadsheet, and runs SQL and shows the result as a table. We will use it throughout these topics to see what our code did in the database.
Driver — the interpreter
Our code is written in Python. MySQL speaks in a “protocol” of its own (the networking topic: protocol = communication rules). You need a middleman who translates: a Driver (also called a Database Connector) = a library that knows how to:
- Connect to a database server of a certain kind.
- Send SQL commands to it.
- Return the results as Python data (tuples, int, str).
For MySQL we will use the official Driver: mysql-connector-python. You install it with pip, and in the code you write import mysql.connector.
Your device (Python code) has an Israeli plug, and the socket in the wall (the database) is American. Driver = the adapter. Every kind of socket has its own adapter, but the device itself does not change.
The connection details — and not in the code!
To connect to MySQL you need five details:
| Detail | Ours | What it is |
|---|---|---|
host | localhost | Where the server is — our computer |
port | 3306 | MySQL’s port |
user | root | The username |
password | kodkod | The password we set in the container |
database | school | Which database inside the server |
In a real project there is a real password here. If it is written in the code and the code went up to GitHub, it has leaked (remember secrets from the Git topic!). The professional practice:
- The connection details are kept in Environment Variables, or in a
.envfile that is in.gitignore. - The code reads them:
os.environ.get("DB_PASSWORD").
Syntax
Running MySQL in a container — one command, once (in PowerShell; on Mac replace the ` with \):
docker run --name kodkod-mysql `
-e MYSQL_ROOT_PASSWORD=kodkod `
-e MYSQL_DATABASE=school `
-p 3306:3306 -d mysql:8.4| Part | Meaning |
|---|---|
docker run | Creates and starts a new container |
--name kodkod-mysql | A name for the container, so that we can refer to it |
-e MYSQL_ROOT_PASSWORD=kodkod | An environment variable: the password of the user root |
-e MYSQL_DATABASE=school | An environment variable: a database named school that is created automatically |
-p 3306:3306 | Port 3306 on our computer leads to the MySQL inside the container |
-d | Runs in the background (detached), and the Terminal stays free |
mysql:8.4 | The Image: MySQL version 8.4 |
Useful commands:
docker ps # which containers are running now
docker start kodkod-mysql # start it again (after a restart of the computer)
docker stop kodkod-mysql # stop it (the data is kept)Connecting from Python:
import mysql.connector
connection = mysql.connector.connect(
host="localhost",
port=3306,
user="root",
password="kodkod",
database="school",
)
connection.close()mysql.connector.connect(...)— opens a network connection to the server. If the server is not running — an error.port=3306is the default, and you can leave it out.
First code example
Setting up the database for these topics, step by step.
1. Run MySQL (Docker Desktop needs to be open):
docker run --name kodkod-mysql `
-e MYSQL_ROOT_PASSWORD=kodkod `
-e MYSQL_DATABASE=school `
-p 3306:3306 -d mysql:8.4
docker psOn the first run Docker downloads the Image (a few minutes). docker ps shows that the container is running:
CONTAINER ID IMAGE STATUS PORTS NAMES
ffaa661f5047 mysql:8.4 Up 10 seconds 0.0.0.0:3306->3306/tcp kodkod-mysql2. Connect from VS Code:
- The Extensions tab → search for Database Client (by Weijan Chen) → Install.
- A database icon appears in the side bar. Click it → + (Create Connection) → choose MySQL.
- Fill in: Host
127.0.0.1, Port3306, Usernameroot, Passwordkodkod, Databaseschool→ Connect. - The connection appears in the tree, and inside it the database
school— still without tables.
3. A Python project:
mkdir ~\dev\kodkod6\week-4\school-db
cd ~\dev\kodkod6\week-4\school-db
git init
python -m venv .venv
.venv\Scripts\Activate.ps1
pip install mysql-connector-python.gitignore:
__pycache__/
.venv/
.envsetup_db.py:
import os
import mysql.connector
connection = mysql.connector.connect(
host="localhost",
port=3306,
user="root",
password=os.environ.get("DB_PASSWORD", "kodkod"),
database="school",
)
print("Connected:", connection.is_connected())
print("Server version:", connection.server_info)
connection.close()
print("Connected after close:", connection.is_connected())Output:
Connected: True
Server version: 8.4.11
Connected after close: FalseBreaking down the code line by line
| Code | Explanation |
|---|---|
import mysql.connector | The Driver. Installed with pip install mysql-connector-python. |
host="localhost", port=3306 | Where the server is: our computer, on the port we opened with -p 3306:3306. |
user="root" | MySQL’s main user. |
os.environ.get("DB_PASSWORD", "kodkod") | The password — from an environment variable if one was set, and otherwise the default of the learning environment. |
database="school" | The database that was created with -e MYSQL_DATABASE=school. |
connection.is_connected() | True — the connection is open. |
connection.server_info | The version of the MySQL server. Your number may be different. |
connection.close() | Closes the connection (in the next lesson: why that matters). |
No *.db in .gitignore | The data sits in the container, not in the project folder. Only the code that creates the structure goes into Git. |
Another example
The container after restarting the computer
After a Restart, Docker Desktop comes up — but the container is not running. Run docker run again? No — the name kodkod-mysql is already taken:
docker: Error response from daemon: Conflict. The container name "/kodkod-mysql" is already in use by container ...The container exists, it is only stopped. Start it:
docker start kodkod-mysqlAll the data was kept — the tables and the rows stay in the container until you delete it (docker rm).
When the server is not running
If Docker Desktop is closed or the container is stopped, the connection from Python fails:
mysql.connector.errors.InterfaceError: 2003: Can't connect to MySQL server on 'localhost:3306' (Errno 10061: No connection could be made because the target machine actively refused it)Can't connect = nobody is listening on port 3306. Check with docker ps, and start it with docker start kodkod-mysql.
Common mistake
Mistake 1: Docker Desktop is not open.
failed to connect to the docker API ... The system cannot find the file specified.docker commands talk to Docker Desktop. Open it, wait for it to finish starting, and try again.
Mistake 2: the port is taken.
Bind for 0.0.0.0:3306 failed: port is already allocatedSomething is already running on port 3306 — usually a MySQL that was installed in the past directly on the computer. You can use another port on our computer: -p 3307:3306, and then connect with port=3307.
Mistake 3: a database password in code that goes up to GitHub.
connection = mysql.connector.connect(host="prod-db.company.com", user="admin", password="MyRealPassword123")Even if you delete it in the next Commit, the password stays in the history (remember the Git topic!). Environment variables / .env in .gitignore. (Our password kodkod is for a local learning environment only.)
Summary
- MySQL = a database server: a process that listens on port 3306, and you connect to it over the network.
- Docker runs MySQL inside a container — with one command, and the same for everyone.
docker runonce, and after thatdocker start kodkod-mysql. - Database Client (a VS Code extension) = an interface to the database: a tree of tables, running SQL and results as a table.
- Driver = a library that translates between Python and the database. For MySQL:
mysql-connector-python(import mysql.connector). - Five connection details:
host,port,user,password,database. - Passwords are not written in code: environment variables or
.envin.gitignore.
Official documentation
Practice
The exercises use only what was taught in this lesson and the lessons before it. Try on your own first, and only then open the solution.
Exercise 1: Table, row, column
id | name | age | email
---+------+-----+---------------
1 | Dana | 24 | dana@mail.com
2 | Avi | 31 | avi@mail.com- How many rows and how many columns are there in the table?
- What does each row describe, and what does each column describe?
- Which column is suitable to be the Primary Key, and why not
name?
Show solution
- 2 rows and 4 columns.
- Every Row is one student — a record. Every Column is one field, with a fixed type, that all the students have.
id— it is unique, never changes, and is created automatically.nameis not suitable: there can be two students named Dana, and a name can change.
Exercise 2: Types and constraints
Write the SQL command that creates a table students with:
id— a whole number, a Primary Key, created automatically;name— text, required;age— a whole number, required;email— text, required and unique.
Show solution
CREATE TABLE students (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
age INT NOT NULL,
email VARCHAR(100) NOT NULL UNIQUE
);INT/VARCHAR(100)— the type of each column: a whole number, and text of up to 100 characters.NOT NULL— it cannot be left empty.UNIQUE— no two students with the same email.AUTO_INCREMENT— the database chooses theidby itself.
Exercise 3: What would the database catch?
students = [
{"name": "Dana", "age": 24, "email": "d@x.com"},
{"name": "Avi", "age": 24, "email": None},
{"name": "Noa", "age": 22, "email": "d@x.com"},
]Which of this data would the students table from the previous exercise reject, and because of which constraint? And why did nobody stop them in the Python list?
Show solution
- Avi — the email is
None→ rejected because ofNOT NULL. - Noa — the same email as Dana → rejected because of
UNIQUE. - Dana — valid.
A Python list has no rules: any value goes in. In a database, the rules are defined once, in the table, and apply to everyone who writes to it — from any program.
Exercise 4: Starting MySQL
- What does each part of the following command do?
- How do you check that the container is running?
- How do you start it again after restarting the computer — without creating a new one?
docker run --name kodkod-mysql -e MYSQL_ROOT_PASSWORD=kodkod -e MYSQL_DATABASE=school -p 3306:3306 -d mysql:8.4Show solution
| Part | Meaning |
|---|---|
docker run | Creates and starts a new container |
--name kodkod-mysql | A name for the container, so that we can refer to it |
-e MYSQL_ROOT_PASSWORD=kodkod | The password of the user root |
-e MYSQL_DATABASE=school | A database named school that is created automatically |
-p 3306:3306 | Port 3306 on our computer leads to the MySQL in the container |
-d | Runs in the background, and the Terminal stays free |
mysql:8.4 | The Image: MySQL version 8.4 |
docker ps— shows the containers that are running right now.docker start kodkod-mysql— starts the existing container. Its data is kept. Runningdocker runagain would fail, because the name is already taken.
Exercise 5: A first connection
Write code that connects to the database school in the container, prints whether the connection is active, closes it, and prints again.
Expected output:
Connected: True
Connected after close: FalseShow solution
import mysql.connector
connection = mysql.connector.connect(
host="localhost",
port=3306,
user="root",
password="kodkod",
database="school",
)
print("Connected:", connection.is_connected())
connection.close()
print("Connected after close:", connection.is_connected())Connected: True
Connected after close: Falsemysql.connector.connect opens a network connection to the MySQL server in the container. If the container is not running — you get an error: Can't connect to MySQL server.