Tables, schemas, SQL, and connections—what you need to store and query structured data in a backend.
Tables, schemas, SQL, and connections—what you need to store and query structured data in a backend.
Lesson outline
A relational database stores data in tables (rows and columns). Each row is a record; each column has a type (integer, text, timestamp). Tables can reference each other via foreign keys, so you model relationships (e.g. one user, many orders) without duplicating data.
Examples: PostgreSQL, MySQL, SQLite, SQL Server. They speak SQL (Structured Query Language). Your backend connects to the DB, sends SQL, and gets results back.
A schema (in the "namespace" sense) groups tables; in PostgreSQL you have public by default. You create tables with CREATE TABLE: define column names, types, and constraints. PRIMARY KEY uniquely identifies a row; FOREIGN KEY points to another table’s primary key.
Example: users (id, email, created_at) and orders (id, user_id, total, created_at). orders.user_id references users.id. That enforces referential integrity: you cannot have an order without a valid user.
Your app opens a connection to the database (host, port, database name, user, password). Connections are expensive, so you use a connection pool: a set of open connections that the app borrows and returns. Libraries like pg (Node), psycopg2 (Python), database/sql (Go), JDBC (Java) handle this.
Never put credentials in code. Use environment variables or a secrets manager; in production use TLS to the DB and restrict network access (e.g. private subnet, no public IP).
SELECT reads data: SELECT id, email FROM users WHERE id = 1. You can JOIN tables (e.g. orders with users), ORDER BY, LIMIT, and GROUP BY for aggregates. INSERT adds rows; UPDATE changes them; DELETE removes them. These are the core operations you will use every day.
Parameterized queries (placeholders like $1, ?, :id) are mandatory. You pass values from the app; the driver escapes them. Never concatenate user input into SQL—that causes SQL injection.
As the app evolves, you add columns, tables, or indexes. Doing this manually is error-prone. Migrations are versioned scripts (e.g. 001_create_users.sql, 002_add_orders.sql) that you run in order. Tools like Flyway, Alembic, golang-migrate, or TypeORM track which migrations ran and apply only new ones.
Always test migrations on a copy of production data. Some changes (e.g. adding a non-null column to a big table) need a multi-step migration to avoid locking or downtime.
Ready to see how this works in the cloud?
Switch to Career Paths for structured paths (e.g. Developer, DevOps) and provider-specific lessons.
View role-based pathsSign in to track your progress and mark lessons complete.
Questions? Discuss in the community or start a thread below.
Join DiscordSign in to start or join a thread.