Introduction to MariaDB & Database Setup

Table of Contents

1. Why a Relational Database? SQL Refresher

The Problem with In-Memory Storage

In Lectures 2 and 3, we stored data in Java HashMap and ArrayList. This worked for learning, but it has a fatal flaw: all data is lost when the application restarts. Try it — stop your Spring Boot application, start it again, and all the posts and users you created are gone.

A database solves this by storing data persistently on disk. Even if your application crashes, the server reboots, or you deploy a new version, the data survives.

Why Relational Databases?

There are two major categories of databases:

Relational databases (SQL) store data in tables with rows and columns, like a spreadsheet. Tables can be linked together through relationships. Examples: MariaDB, PostgreSQL, MySQL, Oracle, SQL Server.

Non-relational databases (NoSQL) store data in various formats — documents (MongoDB), key-value pairs (Redis), graphs (Neo4j), or columns (Cassandra).

For our blog application, a relational database is the natural choice because:

  • Blog data is highly structured — posts have titles, content, authors, dates. Users have names, emails, passwords. These fit neatly into table columns.
  • Relationships are everywhere — a user has many posts, a post has many comments, a post belongs to categories. Relational databases are built for this.
  • Data integrity matters — you do not want a comment pointing to a post that does not exist. Relational databases enforce these rules with foreign keys.
  • SQL is a universal skill — learning it once lets you work with any relational database.

SQL — The Language of Relational Databases

SQL (Structured Query Language) is the standard language for interacting with relational databases. If you have taken a database course in school, you have seen SQL before. If not, do not worry — we will cover everything you need.

SQL has four main categories of commands:

CategoryPurposeCommands
DDL (Data Definition Language)Define database structureCREATEALTERDROPTRUNCATE
DML (Data Manipulation Language)Manipulate dataINSERTSELECTUPDATEDELETE
DCL (Data Control Language)Control accessGRANTREVOKE
TCL (Transaction Control Language)Manage transactionsCOMMITROLLBACKSAVEPOINT

We will use DDL and DML extensively in this series. Do not memorize the categories — just know that SQL covers both “defining the structure” and “working with the data.”


2. MariaDB Overview — History, Features, MySQL Compatibility

The Story of MariaDB

MariaDB has an interesting origin story. In 1995, Michael “Monty” Widenius co-created MySQL, which became the world’s most popular open-source database. In 2008, Sun Microsystems acquired MySQL. In 2010, Oracle acquired Sun Microsystems — and with it, MySQL.

Many in the open-source community were concerned about Oracle’s stewardship of MySQL. So Monty forked MySQL and created MariaDB in 2009, naming it after his younger daughter Maria (MySQL was named after his older daughter My).

MariaDB is a community-developed fork of MySQL. It was designed to be a drop-in replacement — meaning you can switch from MySQL to MariaDB with minimal or no code changes.

Why MariaDB for This Series?

We chose MariaDB for several practical reasons:

  • MySQL compatibility: Almost all MySQL tutorials, tools, and drivers work with MariaDB. If you learn MariaDB, you effectively know MySQL too.
  • Truly open source: MariaDB is released under the GPL license with no commercial restrictions from Oracle.
  • Performance: MariaDB includes optimizations and storage engines (like Aria and ColumnStore) that are not in MySQL.
  • Active development: MariaDB often gets new features before MySQL does — window functions, common table expressions, and JSON functions were available in MariaDB first.
  • Industry adoption: MariaDB is used by Wikipedia, Google, Samsung, and many others. It is the default database in most Linux distributions.

MariaDB vs MySQL — Key Differences

For day-to-day development, MariaDB and MySQL are almost identical. Here are the notable differences:

FeatureMariaDBMySQL
LicenseGPL (fully open source)GPL + proprietary (Oracle controls development)
Default storage engineInnoDB (same as MySQL)InnoDB
JSON supportYes (stored as text, optimized)Yes (native binary format)
Thread poolBuilt-in (all editions)Enterprise only
SequencesYes (CREATE SEQUENCE)No (use AUTO_INCREMENT only)
System versioningYes (temporal tables)No
JDBC Driverorg.mariadb.jdbccom.mysql.cj.jdbc

The JDBC driver difference matters for Spring Boot configuration. We will use the MariaDB driver (org.mariadb.jdbc:mariadb-java-client) throughout this series.

Compatibility with MySQL Tutorials

Because MariaDB is a MySQL fork, almost everything you find in MySQL tutorials works in MariaDB. The SQL syntax, data types, indexing, and most functions are identical. When you see a MySQL example online, you can use it in MariaDB without changes in most cases.


3. Installing MariaDB (Local & Docker)

You have two options for running MariaDB: install it directly on your machine, or use Docker. We will cover both.

Option A: Docker (Recommended)

Docker is the easiest and cleanest way to run MariaDB. It does not touch your system’s configuration, and you can start/stop/reset the database in seconds.

Step 1: Install Docker

If you do not have Docker installed, download it from docker.com. Docker Desktop works on Windows, Mac, and Linux.

Verify the installation:

docker --version
# Docker version 24.x.x, build xxxxx

Step 2: Run MariaDB in a Container

# Pull the official MariaDB image and start a container
docker run --name blogdb \
  -e MARIADB_ROOT_PASSWORD=rootpass \
  -e MARIADB_DATABASE=blogdb \
  -e MARIADB_USER=bloguser \
  -e MARIADB_PASSWORD=blogpass \
  -p 3306:3306 \
  -d mariadb:11

Let us break down every flag:

  • --name blogdb — Names the container “blogdb” so you can refer to it easily.
  • -e MARIADB_ROOT_PASSWORD=rootpass — Sets the root (admin) password. In production, use a strong password.
  • -e MARIADB_DATABASE=blogdb — Automatically creates a database called “blogdb” on first startup.
  • -e MARIADB_USER=bloguser — Creates a non-root user called “bloguser.”
  • -e MARIADB_PASSWORD=blogpass — Sets the password for bloguser.
  • -p 3306:3306 — Maps port 3306 on your machine to port 3306 in the container. This lets you connect to MariaDB at localhost:3306.
  • -d — Runs the container in the background (detached mode).
  • mariadb:11 — Uses the MariaDB version 11 image.

Step 3: Verify It Is Running

# Check that the container is running
docker ps

# You should see something like:
# CONTAINER ID   IMAGE        STATUS         PORTS                    NAMES
# a1b2c3d4e5f6   mariadb:11   Up 2 minutes   0.0.0.0:3306->3306/tcp   blogdb

Step 4: Connect to MariaDB

# Open a MariaDB shell inside the container
docker exec -it blogdb mariadb -u bloguser -pblogpass blogdb

You should see the MariaDB prompt:

Welcome to the MariaDB monitor.
MariaDB [blogdb]>

Type exit to leave the shell.

Useful Docker Commands:

# Stop the container
docker stop blogdb

# Start it again (data is preserved)
docker start blogdb

# View container logs (useful for debugging)
docker logs blogdb

# Remove the container completely (WARNING: data is lost!)
docker rm -f blogdb

# Restart from scratch
docker rm -f blogdb
docker run --name blogdb ... (same command as above)

Step 5 (Optional): Use Docker Compose

For convenience, create a docker-compose.yml file in your project root:

# docker-compose.yml
version: '3.8'

services:
  mariadb:
    image: mariadb:11
    container_name: blogdb
    environment:
      MARIADB_ROOT_PASSWORD: rootpass
      MARIADB_DATABASE: blogdb
      MARIADB_USER: bloguser
      MARIADB_PASSWORD: blogpass
    ports:
      - "3306:3306"
    volumes:
      # This line persists data even if the container is removed
      - mariadb_data:/var/lib/mysql

volumes:
  mariadb_data:

Then start with:

docker compose up -d

The volumes section ensures your data survives even if the container is deleted. This is important for development — you do not want to lose your test data every time you recreate the container.

Option B: Native Installation

If you prefer to install MariaDB directly on your machine:

Windows:

  1. Download the MSI installer from mariadb.org/download
  2. Run the installer — set the root password when prompted
  3. The installer adds MariaDB to your PATH automatically

macOS (Homebrew):

brew install mariadb
brew services start mariadb

# Secure the installation (set root password, remove test database)
mariadb-secure-installation

Linux (Ubuntu/Debian):

sudo apt update
sudo apt install mariadb-server

# Start the service
sudo systemctl start mariadb
sudo systemctl enable mariadb

# Secure the installation
sudo mariadb-secure-installation

After native installation, connect with:

mariadb -u root -p
# Enter your root password when prompted

Which Option to Choose?

Docker is recommended for these reasons:

  • Clean isolation — MariaDB runs in a container, not on your system
  • Easy reset — delete the container and start fresh in seconds
  • Consistent environment — same setup regardless of your operating system
  • Closer to production — most production deployments use containers

For the rest of this series, we assume Docker is used. If you installed natively, the SQL commands are identical — only the connection method differs.


4. Basic MariaDB Administration — Users, Permissions, Databases

Connecting as Root

The root user is the superadmin of MariaDB. Use it only for administrative tasks, never for your application.

# Docker:
docker exec -it blogdb mariadb -u root -prootpass

# Native:
mariadb -u root -p

Managing Databases

A MariaDB server can host multiple databases. Each database is an isolated namespace containing its own tables, views, and procedures.

-- Show all databases on this server
SHOW DATABASES;

-- Output:
-- +--------------------+
-- | Database           |
-- +--------------------+
-- | blogdb             |
-- | information_schema |
-- | mysql              |
-- | performance_schema |
-- | sys                |
-- +--------------------+

-- The ones besides 'blogdb' are system databases — do not touch them.

-- Create a new database (if you did not use the Docker environment variable)
CREATE DATABASE blogdb
  CHARACTER SET utf8mb4
  COLLATE utf8mb4_unicode_ci;

Let us explain the character set options:

  • utf8mb4 — Supports all Unicode characters, including emojis. Always use this over utf8 (which only supports 3-byte characters and cannot store emojis).
  • utf8mb4_unicode_ci — Case-insensitive collation. “Alice” and “alice” are treated as equal in comparisons and sorting. The ci stands for “case insensitive.”
-- Switch to the blogdb database (all subsequent commands will run in this database)
USE blogdb;

-- Drop (delete) a database — WARNING: this permanently deletes everything inside!
-- DROP DATABASE blogdb;

Managing Users

In production, your application should never connect as root. Create a dedicated user with limited permissions.

-- Create a user (if you did not use Docker environment variables)
CREATE USER 'bloguser'@'%' IDENTIFIED BY 'blogpass';

The 'bloguser'@'%' syntax means:

  • bloguser — the username
  • % — the host. % means “any host” (the user can connect from anywhere). For stricter security, use 'localhost' (only local connections) or a specific IP.
-- Grant permissions on the blogdb database
GRANT ALL PRIVILEGES ON blogdb.* TO 'bloguser'@'%';

-- Apply the changes
FLUSH PRIVILEGES;

The GRANT command gives bloguser full access to all tables (*) in the blogdb database. In production, you would be more restrictive:

-- Production example: only allow SELECT, INSERT, UPDATE, DELETE (no DROP, no ALTER)
GRANT SELECT, INSERT, UPDATE, DELETE ON blogdb.* TO 'bloguser'@'%';
FLUSH PRIVILEGES;

Useful user administration commands:

-- Show all users
SELECT User, Host FROM mysql.user;

-- Show permissions for a specific user
SHOW GRANTS FOR 'bloguser'@'%';

-- Change a user's password
ALTER USER 'bloguser'@'%' IDENTIFIED BY 'newpassword';

-- Delete a user
DROP USER 'bloguser'@'%';

The Principle of Least Privilege

A critical security principle: give each user only the permissions they need, and nothing more.

  • The root user should only be used for administration (creating databases, creating users).
  • Your application user (bloguser) should only have the permissions needed to run the app (SELECT, INSERT, UPDATE, DELETE on its own database).
  • read-only reporting user should only have SELECT permission.

This limits the damage if an attacker gains access to any single account.


5. SQL Essentials Review (CREATE, INSERT, SELECT, UPDATE, DELETE, JOIN)

Let us refresh your SQL skills with the essential commands. Connect to MariaDB and switch to the blogdb database:

docker exec -it blogdb mariadb -u bloguser -pblogpass blogdb

CREATE TABLE

-- Create a simple table to practice with
CREATE TABLE authors (
    id          BIGINT       AUTO_INCREMENT PRIMARY KEY,
    name        VARCHAR(100) NOT NULL,
    email       VARCHAR(255) NOT NULL UNIQUE,
    bio         TEXT,
    created_at  TIMESTAMP    DEFAULT CURRENT_TIMESTAMP
);

Each column definition has this structure: column_name DATA_TYPE CONSTRAINTS

  • BIGINT — A large integer type, perfect for IDs.
  • AUTO_INCREMENT — MariaDB automatically assigns the next number (1, 2, 3…). You do not need to specify the ID when inserting.
  • PRIMARY KEY — Uniquely identifies each row. No duplicates, no NULLs.
  • VARCHAR(100) — A variable-length string, up to 100 characters.
  • NOT NULL — This column must have a value. MariaDB rejects inserts with NULL for this column.
  • UNIQUE — No two rows can have the same value in this column.
  • TEXT — A string with no practical length limit (up to 65,535 characters).
  • TIMESTAMP DEFAULT CURRENT_TIMESTAMP — Automatically sets to the current date/time when a row is inserted.

INSERT

-- Insert a single row
INSERT INTO authors (name, email, bio)
VALUES ('Alice Johnson', 'alice@example.com', 'Software engineer and tech blogger');

-- Insert multiple rows at once
INSERT INTO authors (name, email, bio) VALUES
    ('Bob Smith', 'bob@example.com', 'Full-stack developer'),
    ('Carol Davis', 'carol@example.com', 'Database specialist'),
    ('David Wilson', 'david@example.com', NULL);

Notice that we do not specify id (AUTO_INCREMENT handles it) or created_at (the DEFAULT handles it). David has a NULL bio because we explicitly passed NULL and bio allows it (no NOT NULL constraint).

SELECT

-- Select all columns from all rows
SELECT * FROM authors;

-- Select specific columns
SELECT name, email FROM authors;

-- Filter with WHERE
SELECT * FROM authors WHERE name = 'Alice Johnson';

-- Pattern matching with LIKE
-- % means "any characters" (similar to * in file systems)
SELECT * FROM authors WHERE email LIKE '%example.com';

-- Multiple conditions
SELECT * FROM authors WHERE bio IS NOT NULL AND name LIKE 'A%';

-- Ordering results
SELECT * FROM authors ORDER BY name ASC;    -- Alphabetical (A → Z)
SELECT * FROM authors ORDER BY created_at DESC;  -- Newest first

-- Limiting results (pagination)
SELECT * FROM authors ORDER BY id LIMIT 10;         -- First 10 rows
SELECT * FROM authors ORDER BY id LIMIT 10 OFFSET 20; -- Skip 20, get next 10

-- Counting rows
SELECT COUNT(*) AS total_authors FROM authors;

-- Aggregate functions
SELECT COUNT(*) AS total, MIN(created_at) AS earliest, MAX(created_at) AS latest
FROM authors;

UPDATE

-- Update a specific row
UPDATE authors
SET bio = 'Senior software engineer and tech blogger',
    email = 'alice.johnson@example.com'
WHERE id = 1;

-- CRITICAL: Always use WHERE with UPDATE!
-- Without WHERE, ALL rows are updated:
-- UPDATE authors SET bio = 'oops';  ← This changes EVERY author's bio!

The WHERE clause is your safety net. Always double-check your WHERE clause before running UPDATE or DELETE.

DELETE

-- Delete a specific row
DELETE FROM authors WHERE id = 4;

-- CRITICAL: Always use WHERE with DELETE!
-- Without WHERE, ALL rows are deleted:
-- DELETE FROM authors;  ← This deletes EVERY author!

-- Delete with a condition
DELETE FROM authors WHERE bio IS NULL;

JOIN — Combining Tables

Joins are what make relational databases powerful. They let you combine data from multiple related tables.

First, let us create a second table:

CREATE TABLE articles (
    id          BIGINT       AUTO_INCREMENT PRIMARY KEY,
    title       VARCHAR(255) NOT NULL,
    content     TEXT         NOT NULL,
    author_id   BIGINT       NOT NULL,
    created_at  TIMESTAMP    DEFAULT CURRENT_TIMESTAMP,

    -- Foreign key: author_id must reference a valid id in the authors table
    FOREIGN KEY (author_id) REFERENCES authors(id)
);

-- Insert some articles
INSERT INTO articles (title, content, author_id) VALUES
    ('Getting Started with Spring Boot', 'Spring Boot simplifies...', 1),
    ('Advanced SQL Techniques', 'In this article we explore...', 1),
    ('Introduction to Docker', 'Docker containers provide...', 2),
    ('REST API Best Practices', 'When designing REST APIs...', 3);

Now let us join them:

-- INNER JOIN: Get articles with their author information.
-- Only returns rows where there is a match in BOTH tables.
SELECT
    a.title       AS article_title,
    a.created_at  AS published_date,
    au.name       AS author_name,
    au.email      AS author_email
FROM articles a
INNER JOIN authors au ON a.author_id = au.id;

Output:

+-------------------------------+---------------------+---------------+---------------------------+
| article_title                 | published_date      | author_name   | author_email              |
+-------------------------------+---------------------+---------------+---------------------------+
| Getting Started with Spring   | 2024-01-15 10:30:00 | Alice Johnson | alice.johnson@example.com |
| Advanced SQL Techniques       | 2024-01-15 10:31:00 | Alice Johnson | alice.johnson@example.com |
| Introduction to Docker        | 2024-01-15 10:32:00 | Bob Smith     | bob@example.com           |
| REST API Best Practices       | 2024-01-15 10:33:00 | Carol Davis   | carol@example.com         |
+-------------------------------+---------------------+---------------+---------------------------+

Let us explore other join types:

-- LEFT JOIN: Get ALL authors, even those with no articles.
-- Authors without articles will have NULL for article columns.
SELECT au.name, a.title
FROM authors au
LEFT JOIN articles a ON au.id = a.author_id;

-- Output includes authors with no articles:
-- | Alice Johnson | Getting Started with Spring Boot |
-- | Alice Johnson | Advanced SQL Techniques          |
-- | Bob Smith     | Introduction to Docker           |
-- | Carol Davis   | REST API Best Practices          |
-- | David Wilson  | NULL                             |  ← No articles

-- GROUP BY with COUNT: How many articles has each author written?
SELECT au.name, COUNT(a.id) AS article_count
FROM authors au
LEFT JOIN articles a ON au.id = a.author_id
GROUP BY au.id, au.name
ORDER BY article_count DESC;

-- Output:
-- | Alice Johnson | 2 |
-- | Bob Smith     | 1 |
-- | Carol Davis   | 1 |
-- | David Wilson  | 0 |

Summary of Join Types

Join TypeReturns
INNER JOINOnly rows with matches in both tables
LEFT JOINAll rows from the left table + matches from the right (NULL if no match)
RIGHT JOINAll rows from the right table + matches from the left (NULL if no match)
CROSS JOINEvery combination of rows from both tables (Cartesian product — rarely used)

In practice, INNER JOIN and LEFT JOIN cover 95% of use cases.


6. Data Types in MariaDB

Choosing the right data type for each column is important for storage efficiency, query performance, and data integrity. Here are the types you will use most often.

Integer Types

TypeStorageRange
TINYINT1 byte-128 to 127 (or 0 to 255 unsigned)
SMALLINT2 bytes-32,768 to 32,767
INT4 bytes-2.1 billion to 2.1 billion
BIGINT8 bytes-9.2 quintillion to 9.2 quintillion

Recommendation: Use BIGINT for primary keys and foreign keys. It is overkill for small tables, but you never have to worry about running out of IDs. The storage difference (8 bytes vs 4 bytes) is negligible.

String Types

TypeMax LengthUse Case
CHAR(n)255 charsFixed-length strings (country codes, status codes)
VARCHAR(n)65,535 charsVariable-length strings (names, emails, titles)
TEXT65,535 charsLong text (article content, descriptions)
MEDIUMTEXT16 MBVery long text
LONGTEXT4 GBExtremely long text (rarely needed)

Recommendation:

  • Use VARCHAR(n) for most strings. The n is the maximum length — it does not waste space for shorter values.
  • Use TEXT for long-form content like blog post bodies.
  • Always specify a reasonable max length for VARCHAR — it serves as documentation and validation.
-- Examples:
name        VARCHAR(100)   -- Human names are rarely longer than 100 chars
email       VARCHAR(255)   -- Email standard allows up to 254 chars
title       VARCHAR(500)   -- Blog post titles
content     TEXT           -- Blog post body
status      VARCHAR(20)    -- DRAFT, PUBLISHED, ARCHIVED
slug        VARCHAR(300)   -- URL-friendly version of the title

Date and Time Types

TypeFormatExampleUse Case
DATEYYYY-MM-DD2024-01-15Birthdays, deadlines
TIMEHH:MM:SS14:30:00Time-only values
DATETIMEYYYY-MM-DD HH:MM:SS2024-01-15 14:30:00Event timestamps (no timezone)
TIMESTAMPYYYY-MM-DD HH:MM:SS2024-01-15 14:30:00Record timestamps (auto-converts to UTC)

Recommendation: Use TIMESTAMP for created_at and updated_at columns. It automatically converts to UTC for storage and back to local time for display. Use DATETIME when you need to store a specific date and time regardless of timezone (like an event scheduled for “January 15 at 2pm in New York”).

Boolean Type

MariaDB does not have a true boolean type. BOOLEAN is an alias for TINYINT(1):

-- These are equivalent:
is_active BOOLEAN DEFAULT TRUE
is_active TINYINT(1) DEFAULT 1

-- In SQL, TRUE = 1 and FALSE = 0
SELECT * FROM users WHERE is_active = TRUE;
SELECT * FROM users WHERE is_active = 1;  -- Same thing

Decimal Types

TypeUse Case
FLOATApproximate values (calculations where small errors are acceptable)
DOUBLEApproximate values with more precision
DECIMAL(p, s)Exact values (money, financial data — always use this for money)
-- DECIMAL(10, 2) means: up to 10 digits total, with 2 decimal places
-- Maximum value: 99999999.99
price DECIMAL(10, 2)  -- $99,999,999.99

-- NEVER use FLOAT or DOUBLE for money — they cause rounding errors!
-- FLOAT: 0.1 + 0.2 might equal 0.30000000000000004
-- DECIMAL: 0.1 + 0.2 = 0.3 (exact)

JSON Type

MariaDB supports JSON storage (stored as optimized text internally):

metadata JSON

-- Insert JSON data
INSERT INTO posts (title, metadata) VALUES (
    'My Post',
    '{"tags": ["java", "spring"], "readTime": 5}'
);

-- Query JSON fields
SELECT title, JSON_EXTRACT(metadata, '$.tags') AS tags
FROM posts;

Use JSON sparingly. If you find yourself querying JSON fields frequently, those fields probably belong in their own table or column.


7. Indexes and Primary Keys

What is an Index?

An index is a data structure that speeds up data retrieval. Without an index, MariaDB must scan every row in the table to find matching records (full table scan). With an index, it can jump directly to the relevant rows.

The analogy is a book index. Without the index at the back of the book, you would have to read every page to find information about “Spring Boot.” With the index, you look up “Spring Boot,” find the page numbers, and go directly there.

Primary Key

Every table should have a primary key — a column (or combination of columns) that uniquely identifies each row.

CREATE TABLE posts (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,  -- Primary key with auto-increment
    title VARCHAR(255) NOT NULL,
    content TEXT NOT NULL
);

The primary key automatically creates a unique index. MariaDB uses the primary key to physically organize the data on disk (in InnoDB, the default storage engine), making lookups by primary key extremely fast.

Unique Index

A unique index ensures no two rows have the same value in the indexed column:

-- No two users can have the same email
CREATE TABLE users (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    email VARCHAR(255) NOT NULL,
    UNIQUE INDEX idx_users_email (email)
);

-- Alternatively, you can add the UNIQUE constraint inline:
-- email VARCHAR(255) NOT NULL UNIQUE

If you try to insert a duplicate email, MariaDB rejects the insert with an error.

Regular Index (Non-Unique)

A regular index speeds up lookups but allows duplicate values:

-- We often query posts by status, so add an index on status
CREATE INDEX idx_posts_status ON posts(status);

-- We often query posts by author_id, so add an index on author_id
CREATE INDEX idx_posts_author_id ON posts(author_id);

Composite Index

An index on multiple columns:

-- If we often filter by both status AND category, a composite index helps
CREATE INDEX idx_posts_status_category ON posts(status, category);

Column order matters in composite indexes. The index (status, category) can speed up queries that filter by:

  • WHERE status = 'PUBLISHED' (uses the first column)
  • WHERE status = 'PUBLISHED' AND category = 'TUTORIAL' (uses both columns)

But it does NOT speed up:

  • WHERE category = 'TUTORIAL' (cannot skip the first column)

Think of it like a phone book sorted by last name, then first name. You can look up “Smith” (last name only) or “Smith, Alice” (both), but you cannot efficiently look up just “Alice” (first name only).

When to Add Indexes

Add indexes on columns that are frequently used in:

  • WHERE clauses (filtering)
  • JOIN conditions (foreign keys)
  • ORDER BY clauses (sorting)
  • GROUP BY clauses (aggregation)

Do NOT add indexes on every column. Indexes speed up reads but slow down writes (every INSERT, UPDATE, DELETE must also update the indexes). For most web applications, the right balance is:

  • Always: primary keys, foreign keys, unique constraints
  • Usually: columns used in WHERE clauses of frequent queries
  • Rarely: columns with very low cardinality (like a boolean is_active with only two values)

Viewing Indexes

-- Show all indexes on a table
SHOW INDEX FROM posts;

-- See how MariaDB executes a query (shows if indexes are used)
EXPLAIN SELECT * FROM posts WHERE status = 'PUBLISHED';

The EXPLAIN command is your best friend for query optimization. We will use it extensively in Lecture 16.


8. Introduction to Database Design — Normalization Basics

Why Design Matters

A poorly designed database leads to:

  • Data redundancy — the same information stored in multiple places
  • Update anomalies — changing data in one place but forgetting another
  • Insertion anomalies — unable to add data without unrelated data
  • Deletion anomalies — losing data unintentionally when deleting

Good database design prevents these problems through normalization.

What is Normalization?

Normalization is the process of organizing a database to reduce redundancy and improve data integrity. It is defined through a series of “normal forms.” For practical purposes, you need to know the first three.

First Normal Form (1NF) — No Repeating Groups

A table is in 1NF if:

  • Each column contains only atomic (indivisible) values
  • There are no repeating groups or arrays
-- ❌ VIOLATES 1NF — tags stored as a comma-separated string
CREATE TABLE posts_bad (
    id BIGINT PRIMARY KEY,
    title VARCHAR(255),
    tags VARCHAR(500)    -- "java,spring,tutorial" — NOT atomic!
);

-- ✅ FOLLOWS 1NF — tags stored in a separate table, one per row
CREATE TABLE posts (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    title VARCHAR(255)
);

CREATE TABLE post_tags (
    post_id BIGINT,
    tag     VARCHAR(50),
    PRIMARY KEY (post_id, tag),
    FOREIGN KEY (post_id) REFERENCES posts(id)
);

-- Each tag is its own row:
-- | post_id | tag      |
-- |---------|----------|
-- | 1       | java     |
-- | 1       | spring   |
-- | 1       | tutorial |

Second Normal Form (2NF) — No Partial Dependencies

A table is in 2NF if:

  • It is in 1NF
  • Every non-key column depends on the entire primary key (not just part of it)

This only applies to tables with composite primary keys (a primary key made of multiple columns).

-- ❌ VIOLATES 2NF — author_name depends only on author_id, not on the full key
CREATE TABLE articles_bad (
    article_id  BIGINT,
    author_id   BIGINT,
    title       VARCHAR(255),
    author_name VARCHAR(100),   -- Depends only on author_id, not on (article_id, author_id)
    PRIMARY KEY (article_id, author_id)
);

-- ✅ FOLLOWS 2NF — separate the author information into its own table
CREATE TABLE authors (
    id   BIGINT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(100)
);

CREATE TABLE articles (
    id        BIGINT AUTO_INCREMENT PRIMARY KEY,
    title     VARCHAR(255),
    author_id BIGINT,
    FOREIGN KEY (author_id) REFERENCES authors(id)
);

Third Normal Form (3NF) — No Transitive Dependencies

A table is in 3NF if:

  • It is in 2NF
  • No non-key column depends on another non-key column
-- ❌ VIOLATES 3NF — city and country have a transitive dependency
-- (country depends on city, which depends on the primary key)
CREATE TABLE users_bad (
    id      BIGINT PRIMARY KEY,
    name    VARCHAR(100),
    city    VARCHAR(100),
    country VARCHAR(100)    -- country depends on city, not directly on id
);
-- If "Hanoi" is always in "Vietnam", storing both is redundant.
-- Changing Hanoi's country in one row but not another creates inconsistency.

-- ✅ FOLLOWS 3NF — separate location information
CREATE TABLE cities (
    id      BIGINT AUTO_INCREMENT PRIMARY KEY,
    name    VARCHAR(100),
    country VARCHAR(100)
);

CREATE TABLE users (
    id      BIGINT AUTO_INCREMENT PRIMARY KEY,
    name    VARCHAR(100),
    city_id BIGINT,
    FOREIGN KEY (city_id) REFERENCES cities(id)
);

Practical Advice on Normalization

In real projects, you typically aim for 3NF as a starting point. However, sometimes you deliberately denormalize (break normalization rules) for performance. For example:

  • Storing a comment_count on the posts table instead of counting comments every time
  • Storing the author’s name alongside the author_id to avoid a JOIN for common queries

This is a trade-off: denormalization speeds up reads but requires extra effort to keep the duplicated data consistent. We will discuss this more in Lecture 16 (Performance Optimization).

For our blog application, we will follow 3NF and let JPA handle the relationships cleanly.


9. Using a GUI Client (DBeaver / HeidiSQL)

While the command-line client works fine, a GUI client makes database work much more pleasant — especially for exploring data, writing complex queries, and visualizing table structures.

DBeaver (Recommended — Cross-Platform)

DBeaver is a free, open-source database client that works on Windows, Mac, and Linux. It supports MariaDB, MySQL, PostgreSQL, SQLite, Oracle, and dozens of other databases.

Installation:

  1. Download from dbeaver.io
  2. Install and launch DBeaver

Connecting to MariaDB:

  1. Click the New Database Connection button (plug icon)
  2. Select MariaDB from the list
  3. Fill in the connection details:
    • Host: localhost
    • Port: 3306
    • Database: blogdb
    • Username: bloguser
    • Password: blogpass
  4. Click Test Connection — if it says “Connected,” click Finish

Key Features:

  • SQL Editor — Write and execute SQL queries with syntax highlighting and auto-complete
  • Table Viewer — Browse table data in a spreadsheet-like view, edit cells directly
  • ER Diagrams — Automatically generate entity-relationship diagrams from your tables
  • Data Export — Export query results to CSV, JSON, SQL, Excel, and more
  • Multiple Connections — Connect to multiple databases simultaneously

HeidiSQL (Windows Only)

HeidiSQL is a lightweight, Windows-only client that is particularly popular with MariaDB and MySQL users.

Installation:

  1. Download from heidisql.com
  2. Install and launch HeidiSQL

Connecting:

  1. Click New in the session manager
  2. Set Network type to MariaDB or MySQL (TCP/IP)
  3. Enter: Hostname 127.0.0.1, User bloguser, Password blogpass, Port 3306
  4. Click Open

IntelliJ IDEA Database Tool (Ultimate Only)

If you have IntelliJ IDEA Ultimate, it includes a built-in database tool:

  1. Open the Database panel (View → Tool Windows → Database)
  2. Click + → Data Source → MariaDB
  3. Enter the connection details and click Test Connection
  4. Click OK

This is convenient because you can write SQL queries and Java code in the same IDE.

Command-Line Client

For quick operations, the command-line client is always available:

# Docker
docker exec -it blogdb mariadb -u bloguser -pblogpass blogdb

# Native installation
mariadb -u bloguser -p blogdb

Useful commands inside the MariaDB shell:

-- Show all tables in the current database
SHOW TABLES;

-- Show the structure of a table
DESCRIBE posts;
-- or
SHOW CREATE TABLE posts;

-- Show the current database
SELECT DATABASE();

-- Show server version
SELECT VERSION();

-- Clear the screen
\! clear


10. Hands-on: Design and Create Tables for a Blog Application

Now let us put everything together and design the database schema for our blog application. This schema will serve us for the rest of the series.

Step 1: Identify the Entities

Our blog application needs these entities:

  • Users — People who can create posts and comments
  • Posts — Blog articles written by users
  • Comments — Responses to posts
  • Categories — Groups for organizing posts (e.g., “Tutorial,” “News,” “Opinion”)
  • Tags — Labels for posts (e.g., “java,” “spring-boot,” “database”)

Step 2: Identify the Relationships

  • user can write many posts (One-to-Many)
  • user can write many comments (One-to-Many)
  • post belongs to one category (Many-to-One)
  • post can have many comments (One-to-Many)
  • post can have many tags, and a tag can belong to many posts (Many-to-Many)

Step 3: Create the Schema

Connect to your database and run the following SQL:

-- Switch to the blogdb database
USE blogdb;

-- ============================================
-- Drop existing tables (if recreating from scratch)
-- Order matters: drop tables with foreign keys first
-- ============================================
DROP TABLE IF EXISTS post_tags;
DROP TABLE IF EXISTS comments;
DROP TABLE IF EXISTS posts;
DROP TABLE IF EXISTS categories;
DROP TABLE IF EXISTS tags;
DROP TABLE IF EXISTS users;

-- ============================================
-- 1. USERS TABLE
-- ============================================
CREATE TABLE users (
    -- Primary key: auto-incrementing BIGINT
    id              BIGINT          AUTO_INCREMENT PRIMARY KEY,

    -- User's display name, required
    username        VARCHAR(50)     NOT NULL,

    -- Login email, required and unique (no two users can share an email)
    email           VARCHAR(255)    NOT NULL,

    -- Hashed password (we will NEVER store plain text passwords)
    -- BCrypt hashes are 60 characters long
    password_hash   VARCHAR(255)    NOT NULL,

    -- User's full name, optional
    full_name       VARCHAR(100),

    -- Short biography, optional
    bio             TEXT,

    -- User role for authorization: ROLE_USER, ROLE_AUTHOR, ROLE_ADMIN
    role            VARCHAR(20)     NOT NULL DEFAULT 'ROLE_USER',

    -- Soft delete: instead of removing rows, we mark them as inactive
    is_active       BOOLEAN         NOT NULL DEFAULT TRUE,

    -- Timestamps for auditing
    created_at      TIMESTAMP       NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at      TIMESTAMP       NOT NULL DEFAULT CURRENT_TIMESTAMP
                                    ON UPDATE CURRENT_TIMESTAMP,

    -- Unique constraints (these also create indexes automatically)
    UNIQUE INDEX idx_users_username (username),
    UNIQUE INDEX idx_users_email (email)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

Let us explain the new concepts:

  • ON UPDATE CURRENT_TIMESTAMP — Automatically updates the updated_at column whenever the row is modified. No manual code needed.
  • DEFAULT 'ROLE_USER' — If no role is specified during insert, the user gets ROLE_USER.
  • ENGINE=InnoDB — Specifies the storage engine. InnoDB supports transactions and foreign keys. It is the default, but being explicit is good practice.
  • DEFAULT CHARSET=utf8mb4 — Sets the character set for the table, ensuring emoji and international character support.
  • Soft delete (is_active) — Instead of permanently deleting users, we set is_active = FALSE. This preserves their posts and comments.
-- ============================================
-- 2. CATEGORIES TABLE
-- ============================================
CREATE TABLE categories (
    id          BIGINT          AUTO_INCREMENT PRIMARY KEY,
    name        VARCHAR(100)    NOT NULL,
    slug        VARCHAR(100)    NOT NULL,
    description TEXT,
    created_at  TIMESTAMP       NOT NULL DEFAULT CURRENT_TIMESTAMP,

    UNIQUE INDEX idx_categories_name (name),
    UNIQUE INDEX idx_categories_slug (slug)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

The slug column stores a URL-friendly version of the name. For example, “Getting Started” becomes “getting-started.” This is used in URLs: /categories/getting-started instead of /categories/1.

-- ============================================
-- 3. TAGS TABLE
-- ============================================
CREATE TABLE tags (
    id          BIGINT          AUTO_INCREMENT PRIMARY KEY,
    name        VARCHAR(50)     NOT NULL,
    slug        VARCHAR(50)     NOT NULL,
    created_at  TIMESTAMP       NOT NULL DEFAULT CURRENT_TIMESTAMP,

    UNIQUE INDEX idx_tags_name (name),
    UNIQUE INDEX idx_tags_slug (slug)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- ============================================
-- 4. POSTS TABLE
-- ============================================
CREATE TABLE posts (
    id              BIGINT          AUTO_INCREMENT PRIMARY KEY,
    title           VARCHAR(500)    NOT NULL,
    slug            VARCHAR(500)    NOT NULL,
    content         TEXT            NOT NULL,
    excerpt         VARCHAR(1000),
    status          VARCHAR(20)     NOT NULL DEFAULT 'DRAFT',

    -- Foreign keys: link posts to users and categories
    author_id       BIGINT          NOT NULL,
    category_id     BIGINT,

    -- SEO fields
    meta_title      VARCHAR(200),
    meta_description VARCHAR(500),

    -- Timestamps
    published_at    TIMESTAMP       NULL,
    created_at      TIMESTAMP       NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at      TIMESTAMP       NOT NULL DEFAULT CURRENT_TIMESTAMP
                                    ON UPDATE CURRENT_TIMESTAMP,

    -- Unique constraint on slug (for URL routing)
    UNIQUE INDEX idx_posts_slug (slug),

    -- Index on status (frequently filtered)
    INDEX idx_posts_status (status),

    -- Index on author_id (frequently joined and filtered)
    INDEX idx_posts_author_id (author_id),

    -- Index on category_id (frequently joined and filtered)
    INDEX idx_posts_category_id (category_id),

    -- Foreign key constraints
    -- RESTRICT means: do not allow deleting a user who has posts
    FOREIGN KEY (author_id) REFERENCES users(id) ON DELETE RESTRICT,

    -- SET NULL means: if a category is deleted, set category_id to NULL
    FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

Foreign key actions explained:

  • ON DELETE RESTRICT — Prevents deleting the parent row if child rows exist. You cannot delete a user who has posts.
  • ON DELETE SET NULL — If the parent row is deleted, set the foreign key to NULL. If a category is deleted, posts in that category become uncategorized.
  • ON DELETE CASCADE — If the parent row is deleted, delete all child rows too. We will use this for comments (if a post is deleted, its comments are deleted too).
-- ============================================
-- 5. COMMENTS TABLE
-- ============================================
CREATE TABLE comments (
    id          BIGINT      AUTO_INCREMENT PRIMARY KEY,
    content     TEXT        NOT NULL,

    -- Foreign keys
    post_id     BIGINT      NOT NULL,
    author_id   BIGINT      NOT NULL,

    -- Self-referencing foreign key for nested replies
    -- A comment can be a reply to another comment
    parent_id   BIGINT      NULL,

    -- Timestamps
    created_at  TIMESTAMP   NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at  TIMESTAMP   NOT NULL DEFAULT CURRENT_TIMESTAMP
                            ON UPDATE CURRENT_TIMESTAMP,

    INDEX idx_comments_post_id (post_id),
    INDEX idx_comments_author_id (author_id),
    INDEX idx_comments_parent_id (parent_id),

    -- CASCADE: if a post is deleted, all its comments are deleted too
    FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE,
    FOREIGN KEY (author_id) REFERENCES users(id) ON DELETE RESTRICT,
    FOREIGN KEY (parent_id) REFERENCES comments(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

-- ============================================
-- 6. POST_TAGS TABLE (Many-to-Many junction table)
-- ============================================
-- This table exists solely to represent the many-to-many relationship
-- between posts and tags. It has no columns of its own except the
-- two foreign keys, which together form the primary key.
CREATE TABLE post_tags (
    post_id     BIGINT NOT NULL,
    tag_id      BIGINT NOT NULL,

    -- Composite primary key: the combination of post_id + tag_id must be unique
    -- This prevents duplicate tag assignments
    PRIMARY KEY (post_id, tag_id),

    FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE,
    FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

Step 4: Insert Sample Data

-- Insert users
INSERT INTO users (username, email, password_hash, full_name, bio, role) VALUES
    ('alice', 'alice@example.com', '$2a$10$dummyhash1', 'Alice Johnson', 'Senior developer and blogger', 'ROLE_ADMIN'),
    ('bob', 'bob@example.com', '$2a$10$dummyhash2', 'Bob Smith', 'Full-stack developer', 'ROLE_AUTHOR'),
    ('carol', 'carol@example.com', '$2a$10$dummyhash3', 'Carol Davis', 'Tech enthusiast', 'ROLE_USER');

-- Insert categories
INSERT INTO categories (name, slug, description) VALUES
    ('Tutorial', 'tutorial', 'Step-by-step programming tutorials'),
    ('News', 'news', 'Latest tech news and announcements'),
    ('Opinion', 'opinion', 'Personal thoughts and perspectives');

-- Insert tags
INSERT INTO tags (name, slug) VALUES
    ('Java', 'java'),
    ('Spring Boot', 'spring-boot'),
    ('Database', 'database'),
    ('Docker', 'docker'),
    ('REST API', 'rest-api');

-- Insert posts
INSERT INTO posts (title, slug, content, excerpt, status, author_id, category_id, published_at) VALUES
    ('Getting Started with Spring Boot',
     'getting-started-with-spring-boot',
     'Spring Boot makes it easy to create stand-alone, production-grade Spring based applications...',
     'A beginner-friendly guide to Spring Boot',
     'PUBLISHED', 1, 1, NOW()),
    ('Understanding REST APIs',
     'understanding-rest-apis',
     'REST (Representational State Transfer) is an architectural style for designing networked applications...',
     'Learn the fundamentals of RESTful API design',
     'PUBLISHED', 1, 1, NOW()),
    ('Docker for Developers',
     'docker-for-developers',
     'Docker containers provide a consistent environment for development and deployment...',
     'A practical guide to Docker',
     'DRAFT', 2, 1, NULL),
    ('The Future of Java',
     'the-future-of-java',
     'Java continues to evolve with regular releases every six months...',
     'Where is Java heading?',
     'PUBLISHED', 2, 2, NOW());

-- Assign tags to posts (many-to-many)
INSERT INTO post_tags (post_id, tag_id) VALUES
    (1, 1),    -- "Getting Started" tagged with "Java"
    (1, 2),    -- "Getting Started" tagged with "Spring Boot"
    (2, 5),    -- "Understanding REST" tagged with "REST API"
    (2, 2),    -- "Understanding REST" tagged with "Spring Boot"
    (3, 4),    -- "Docker for Developers" tagged with "Docker"
    (4, 1);    -- "The Future of Java" tagged with "Java"

-- Insert comments
INSERT INTO comments (content, post_id, author_id, parent_id) VALUES
    ('Great article! Very helpful for beginners.', 1, 3, NULL),
    ('Thanks Carol! Glad you found it useful.', 1, 1, 1),
    ('Could you cover Spring Security next?', 1, 2, NULL),
    ('Excellent overview of REST principles.', 2, 3, NULL);

Step 5: Verify the Schema

Run these queries to verify everything was created correctly:

-- Show all tables
SHOW TABLES;

-- Verify table structures
DESCRIBE users;
DESCRIBE posts;
DESCRIBE comments;
DESCRIBE post_tags;

-- Test the relationships with joins

-- Get all posts with their author name and category
SELECT p.title, u.full_name AS author, c.name AS category, p.status
FROM posts p
JOIN users u ON p.author_id = u.id
LEFT JOIN categories c ON p.category_id = c.id
ORDER BY p.created_at DESC;

-- Get all tags for each post
SELECT p.title, GROUP_CONCAT(t.name SEPARATOR ', ') AS tags
FROM posts p
JOIN post_tags pt ON p.id = pt.post_id
JOIN tags t ON pt.tag_id = t.id
GROUP BY p.id, p.title;

-- Get comments with their authors and reply structure
SELECT
    c.content,
    u.username AS commenter,
    c.parent_id AS reply_to,
    c.created_at
FROM comments c
JOIN users u ON c.author_id = u.id
WHERE c.post_id = 1
ORDER BY c.created_at;

-- Count posts per category
SELECT c.name AS category, COUNT(p.id) AS post_count
FROM categories c
LEFT JOIN posts p ON c.id = p.category_id
GROUP BY c.id, c.name;

-- Count posts per author
SELECT u.full_name, COUNT(p.id) AS post_count
FROM users u
LEFT JOIN posts p ON u.id = p.author_id
GROUP BY u.id, u.full_name
ORDER BY post_count DESC;

Step 6: Save the Schema as a SQL File

Create a file called schema.sql in your project’s src/main/resources directory. Copy all the CREATE TABLE and INSERT statements above into this file. This serves as documentation and can be used to recreate the database from scratch.

In Lecture 10, we will replace this manual approach with Flyway database migrations for proper version control of your schema.

Step 7: Exercises

  1. Add a views_count column: Add an integer column views_count (default 0) to the posts table. Write an UPDATE query that increments the count when a post is viewed.-- Hint: ALTER TABLE posts ADD COLUMN views_count INT NOT NULL DEFAULT 0; UPDATE posts SET views_count = views_count + 1 WHERE id = 1;
  2. Write a “popular posts” query: Select the top 3 posts ordered by comment count. You will need a JOIN with GROUP BY and ORDER BY.
  3. Write a “recent activity” query: Select the 10 most recent comments across all posts, including the post title, comment content, and commenter username.
  4. Test foreign key constraints: Try to delete a user who has posts. What happens? Try to delete a category that has posts. What happens? This demonstrates the difference between ON DELETE RESTRICT and ON DELETE SET NULL.
  5. Add a bookmarks table: Design and create a junction table that allows users to bookmark posts (many-to-many between users and posts). Insert a few bookmarks and write a query to get all bookmarked posts for a specific user.

Summary

In this lecture, you have set up everything needed on the database side before connecting it to Spring Boot:

  • Why relational databases: Persistent storage with structured data, enforced relationships, and data integrity through constraints and foreign keys.
  • MariaDB: A community-driven fork of MySQL that is fully compatible, truly open source, and actively developed. Everything you learn with MariaDB applies to MySQL too.
  • Installation: Docker provides the cleanest setup — a single command starts a fully configured MariaDB server. Docker Compose with volumes ensures data persistence.
  • Administration: Create dedicated users with limited permissions. Never use root for your application. Follow the principle of least privilege.
  • SQL essentials: CREATE TABLE defines structure, INSERT adds data, SELECT reads data (with filtering, sorting, pagination), UPDATE modifies data, DELETE removes data, and JOIN combines tables.
  • Data types: Use BIGINT for IDs, VARCHAR for strings, TEXT for long content, TIMESTAMP for dates, DECIMAL for money, BOOLEAN for flags.
  • Indexes: Speed up reads at the cost of slower writes. Always index primary keys, foreign keys, and frequently queried columns.
  • Normalization: Organize tables to reduce redundancy (1NF: atomic values, 2NF: no partial dependencies, 3NF: no transitive dependencies).
  • Blog schema: Six tables (users, categories, tags, posts, comments, post_tags) with proper relationships, constraints, indexes, and sample data.

What is Next

In Lecture 5, we will connect Spring Boot to this MariaDB database using Spring Data JPA and Hibernate. You will learn how to map Java classes to database tables using annotations, and replace our in-memory repositories with real database-backed repositories — without writing a single line of SQL.


Quick Reference

ConceptDescription
Relational DatabaseStores data in tables with rows and columns, linked by relationships
MariaDBOpen-source MySQL fork, fully compatible, community-developed
SQLStructured Query Language — standard language for relational databases
DDLData Definition Language (CREATE, ALTER, DROP)
DMLData Manipulation Language (INSERT, SELECT, UPDATE, DELETE)
PRIMARY KEYUniquely identifies each row; auto-indexed
FOREIGN KEYLinks a column to a primary key in another table
UNIQUEConstraint ensuring no duplicate values
NOT NULLConstraint requiring a value (no NULLs)
AUTO_INCREMENTAutomatically assigns the next integer ID
INDEXData structure that speeds up data retrieval
JOINCombines rows from multiple tables based on related columns
INNER JOINReturns only rows with matches in both tables
LEFT JOINReturns all rows from the left table + matches from the right
NormalizationOrganizing tables to minimize redundancy (1NF, 2NF, 3NF)
ON DELETE CASCADEDelete child rows when parent is deleted
ON DELETE RESTRICTPrevent deletion of parent if children exist
ON DELETE SET NULLSet foreign key to NULL when parent is deleted
utf8mb4Character set supporting all Unicode including emojis
Docker ComposeTool for defining and running multi-container Docker apps
DBeaverFree cross-platform database GUI client

Leave a Reply

Your email address will not be published. Required fields are marked *