You already know what SQL is and how it's written. It's time to understand what it acts on: the structure where the data lives. In this lesson we'll take the whole hierarchy apart —server, database, schema, table, row, column—, see why the spreadsheet analogy helps at first but breaks down quickly, and walk in detail through PostgreSQL's catalogue of data types. That last point matters more than it looks: choosing a type badly is a mistake you pay for over years, and the case of money stored in floating point is the canonical example of why. We'll finish by learning to inspect tables that already exist, a skill you'll need every time you face somebody else's database.
Contents
- The hierarchy: server, database, schema, table
- Tables, rows and columns
- The spreadsheet analogy (and where it breaks)
- Numeric data types
- Why money never goes in floating point
- Text types
- Date and time types
- Booleans, UUID and JSONB
- NULL: the absence of a value
- Equivalences between PostgreSQL, MySQL and SQLite
- How to inspect existing tables
- Common Mistakes and Tips
- Exercises
- Conclusion
- The hierarchy: server, database, schema, table
In PostgreSQL, objects are organised into four levels:
graph TD
A["Server / Cluster<br/>(one process, one port: 5432)"] --> B["Database: greenstore"]
A --> B2["Database: postgres"]
A --> B3["Database: sql_tests"]
B --> C["Schema: public"]
B --> C2["Schema: information_schema"]
C --> D["Table: products"]
C --> D2["Table: customers"]
C --> D3["Table: orders"]
D --> E["Columns: id, name, price…"]
D --> E2["Rows: each individual product"]
| Level | What it is | Example in the course |
|---|---|---|
| Server (cluster) | The running PostgreSQL process, listening on a port | The pg-course container on localhost:5432 |
| Database | An isolated set of data. A connection only sees one at a time | greenstore |
| Schema | A namespace inside the database | public |
| Table | A collection of rows with the same structure | products |
| Column | A field with a name and a type | price NUMERIC(10,2) |
| Row | A specific record | The product "Raw orange blossom honey 500 g" |
Two practical consequences of this hierarchy:
- You can't query two databases at once. If
greenstoreandsql_testslive on the same server, a query can't combine them directly (you'd need extensions likedblinkorpostgres_fdw). In MySQL, on the other hand, writingSELECT ... FROM other_database.tableis common, because there "database" and "schema" are almost synonyms. - Schemas do combine freely. Inside
greenstoreyou could have asalesschema and ananalyticsschema and query them together without any trouble.
The public schema
Every PostgreSQL database is born with a schema called public. If you create a table without specifying a schema, it ends up there, and if you query it without a prefix it's looked up there. These two statements are equivalent in our setup:
The lookup order is determined by the search_path parameter:
| search_path |
|---|
| "$user", public |
It means: "look first for a schema named after the connected user; if it doesn't exist, look in public". Schemas are there to organise large databases (splitting by functional area, by client, by environment) and to avoid name collisions. All of GreenStore lives in public, so we won't have to worry about this again.
- Tables, rows and columns
A table is a collection of rows that share the same structure. Every table is defined by:
- A name (
products). - An ordered set of columns, each with a name and a data type.
- Optionally, constraints that limit which values are valid (module 5).
A simplified view of products:
| id | name | category_id | price | stock | active |
|---|---|---|---|---|---|
| 1 | Extra virgin olive oil 500 ml | 1 | 12.50 | 120 | true |
| 2 | Organic brown rice 1 kg | 1 | 3.90 | 200 | true |
| 6 | Aloe vera face cream 50 ml | 2 | 18.90 | 60 | true |
Formal terminology versus everyday terminology:
| Everyday term | Relational model term | What it is |
|---|---|---|
| Table | Relation | The set of data |
| Row / record | Tuple | One specific entity |
| Column / field | Attribute | A property of that entity |
| Column type | Domain | The set of valid values |
Three fundamental properties worth burning in from the start:
- Every row has the same columns. There's no such thing as a row "with an extra field". If a piece of data doesn't apply,
NULLis stored. - Each column has a single type. You can't store
'tomorrow'in aDATEcolumn. - Rows have no intrinsic order. A table is a set. If you don't ask for
ORDER BYexplicitly, the engine can hand them back in any order, and that order can change tomorrow. Relying on the "natural order" is a classic mistake.
- The spreadsheet analogy (and where it breaks)
At first it helps to think of a table as an Excel sheet: the first row holds the headers (columns) and the following ones the data (rows). The analogy works... up to a point.
| Aspect | Spreadsheet | Database table |
|---|---|---|
| Types | Each cell can contain anything | The whole column shares a single type, checked by the engine |
| Row order | Visible and meaningful | There's no implicit order; you ask for it with ORDER BY |
| Relationships | Simulated with VLOOKUP and fragile references |
First-class: foreign keys with guaranteed integrity |
| Size | Thousands or hundreds of thousands of rows | Millions or billions |
| Concurrency | One user at a time (or version conflicts) | Hundreds of simultaneous users with transactions |
| Integrity | Nothing stops you typing "twelve euros" in a column of amounts | Constraints that reject the invalid value |
| Formulas | Stored in the cells | The data is stored; the calculation happens at query time |
| Undo | Ctrl+Z | ROLLBACK of a transaction (module 9) |
The deepest conceptual difference is strict typing. In Excel, a column of prices can hold 12,50, 12.50, "12.50 €" and twelve fifty, and nobody warns you until the total comes out wrong. In PostgreSQL, if the column is NUMERIC(10,2), the engine rejects anything that isn't a number:
That error, which can look like a nuisance, is exactly the value a database brings: incorrect data never gets in.
- Numeric data types
| Type | Size | Range / Precision | When to use it |
|---|---|---|---|
SMALLINT |
2 bytes | -32,768 to 32,767 | Very small counters, ages |
INTEGER (INT) |
4 bytes | ±2,147,483,647 | The default for integers: ids, stock, quantities |
BIGINT |
8 bytes | ±9.2 × 10¹⁸ | Ids for enormous tables, massive counters |
NUMERIC(p,s) / DECIMAL(p,s) |
Variable | Exact, up to 131,072 digits | Money and any exact calculation |
REAL |
4 bytes | ~6 decimal digits | Approximate scientific magnitudes |
DOUBLE PRECISION |
8 bytes | ~15 decimal digits | Scientific calculations, coordinates |
SERIAL / BIGSERIAL |
4/8 bytes | Auto-incrementing integer | Primary keys (see note) |
About NUMERIC(p,s):
p(precision) is the total number of digits.s(scale) is how many of those digits go after the decimal point.NUMERIC(10,2)allows up to 99,999,999.99 → eight integer digits and two decimals.
In GreenStore we use NUMERIC(10,2) for price, cost, shipping_cost, unit_price, amount and salary, and NUMERIC(4,2) for discount (a fraction between 0 and 1, with two decimals).
Note on
SERIAL: it isn't a real type, but a shortcut that creates anINTEGERplus a sequence that auto-increments it. Since PostgreSQL 10 the form recommended by the standard isGENERATED BY DEFAULT AS IDENTITY, which is the one the course script uses. You'll see it in detail in module 5.
- Why money never goes in floating point
REAL and DOUBLE PRECISION store numbers in binary floating point (the IEEE 754 standard). The problem is that many decimals that are exact in base 10 are recurring in base 2: 0.1 in binary is infinite, just as 1/3 is in decimal. The computer stores an approximation, and those tiny errors accumulate.
Check it for yourself:
| float_sum | exact_sum |
|---|---|
| 0.30000001 | 0.3 |
And the case that would wreck an accounting close for you:
SELECT (0.1::DOUBLE PRECISION + 0.2::DOUBLE PRECISION) = 0.3 AS equal_float,
(0.1::NUMERIC + 0.2::NUMERIC) = 0.3 AS equal_exact;| equal_float | equal_exact |
|---|---|
| false | true |
Applied to GreenStore: if we stored price as REAL and added up the 47 order lines in the database, the total might come out as 143.20999999998 instead of 143.21. Multiply that by thousands of orders a month and you have an accounting mismatch that's impossible to justify.
| Type | Nature | Speed | Accuracy | Correct use |
|---|---|---|---|---|
REAL / DOUBLE PRECISION |
Approximate (binary) | Very fast | Not exact | Physics, statistics, coordinates, approximate averages |
NUMERIC(p,s) |
Exact (decimal) | Slower | Exact | Money, accounting percentages, billable quantities |
A rule with no exceptions: money →
NUMERIC. NeverFLOAT,REALorDOUBLE PRECISION. The speed penalty is irrelevant next to a lost cent.There's also a
MONEYtype in PostgreSQL, but it's not recommended: it depends on the server's regional settings and doesn't handle multiple currencies well. UseNUMERIC.
- Text types
| Type | Description | When to use it |
|---|---|---|
VARCHAR(n) |
Variable-length text with a maximum of n characters |
When the limit is a real business rule |
TEXT |
Text of unlimited length | The default option in PostgreSQL |
CHAR(n) |
Fixed length; padded with spaces up to n |
Almost never. Only for codes of strictly fixed length |
One PostgreSQL quirk that surprises people coming from other engines: TEXT and VARCHAR have exactly the same performance. Internally they're the same type; VARCHAR(n) merely adds a length check. There is no speed advantage in setting a limit.
So when should you use VARCHAR(n)? When the limit means something:
country VARCHAR(60): a country name, a reasonable limit.email VARCHAR(120): there's a known practical maximum.description TEXT: we don't know how much anybody will write.comment TEXT: a review can be long.
Avoid CHAR(n). It pads with trailing spaces and causes surprising comparisons:
| look_equal | stored_length |
|---|---|
| true | 2 |
The value is stored as 'ES ' but trailing spaces are ignored when comparing, which creates constant confusion when exporting or concatenating.
Dialect note: in MySQL there are performance and storage differences between
CHAR,VARCHARandTEXT(TEXTcolumns are stored outside the row and don't accept a default value). What makes no difference here does make a difference there.
- Date and time types
| Type | What it stores | Example | Use in GreenStore |
|---|---|---|---|
DATE |
The date only | 2026-02-14 |
order_date, signup_date, added_date, hire_date |
TIME |
The time only | 18:30:00 |
Opening hours |
TIMESTAMP |
Date and time, without a time zone | 2026-02-14 18:30:00 |
When the zone is irrelevant |
TIMESTAMPTZ |
Date and time with a time zone | 2026-02-14 18:30:00+01 |
Audit stamps, real-world events |
INTERVAL |
A duration | 3 days, 2 hours 30 minutes |
Delivery windows |
The distinction between TIMESTAMP and TIMESTAMPTZ is the one that causes the most trouble in production:
TIMESTAMPstores literally what you give it. If a French customer and a Spanish one both record "18:30", they're stored identically even though they're different moments.TIMESTAMPTZconverts to UTC on write and to the client's zone on read. It represents a real instant in time.
SELECT NOW() AS now_with_zone,
NOW()::TIMESTAMP AS now_without_zone,
CURRENT_DATE AS today,
AGE(DATE '2026-02-14', DATE '2025-11-14') AS difference;| now_with_zone | now_without_zone | today | difference |
|---|---|---|---|
| 2026-02-25 10:14:07.412+01 | 2026-02-25 10:14:07.412 | 2026-02-25 | 3 mons |
Rule of thumb: if the moment has real relevance (when something happened), use
TIMESTAMPTZ. If it's a calendar date with no time (an order date, a date of birth), useDATE. GreenStore usesDATEfor all its dates because they're calendar dates, not instants.Dialect: MySQL has
DATETIME(no zone) andTIMESTAMP(with UTC conversion, but limited to 2038). SQLite has no date type: it stores ISO text, numbers or julian days, and the date functions operate on those representations.
- Booleans, UUID and JSONB
BOOLEAN
Stores TRUE, FALSE or NULL. Takes 1 byte. In GreenStore it's used by products.active and suppliers.active.
That NULL has business meaning: "active = TRUE" is a product on sale, "FALSE" a discontinued one, and NULL would mean "we haven't decided yet". That's why booleans in SQL have three states, not two.
UUID
A 128-bit universal identifier, in the style of a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11. Takes 16 bytes.
| Advantage | Drawback |
|---|---|
| It can be generated on the client without asking the server | Takes 4 times as much space as an INTEGER |
| It doesn't reveal how many records you have | Unreadable for humans |
| Unique across different systems (useful in microservices) | Worse index locality: slower inserts |
GreenStore uses INTEGER for its id columns because it's a small, didactic database where being able to write WHERE id = 7 is a learning advantage.
JSONB
PostgreSQL lets you store JSON documents in a column. JSON stores the text as-is; JSONB stores it in an indexed binary format, and it's the one used in practice. It's for semi-structured data whose shape varies (attributes specific to each product, responses from a payment gateway, configurations).
| origin |
|---|
| "Valencia" |
We mention it here so you know it exists; its full use is covered in lesson 10-06. And a warning: JSONB is no excuse for not designing your tables properly. What is structured belongs in columns.
- NULL: the absence of a value
NULL is not zero, nor an empty string, nor FALSE. It means "there's no value here" or "it isn't known".
In GreenStore there are three nulls with a well-defined business meaning:
| Column | What its NULL means |
|---|---|
orders.employee_id |
The order came in through the web; no sales rep handled it |
customers.referred_by_id |
The customer arrived on their own, nobody referred them |
employees.manager_id |
This is the general manager: they have no superior |
The essential thing right now is to understand that NULL propagates through any operation:
| addition | concatenation | comparison |
|---|---|---|
| (null) | (null) | (null) |
Any calculation that touches a NULL returns NULL, because operating on something unknown produces something unknown. And as you saw in the previous lesson, to check whether something is null you use IS NULL, never = NULL.
Tell these three cases apart, because they aren't the same thing:
| Value | Meaning |
|---|---|
NULL |
There's no data / it's unknown |
0 |
There is data, and it's zero |
'' (empty string) |
There is data, and it's a text with no characters |
The full treatment of nulls —how they affect aggregations,
JOINs and filters— is lesson 04-03. For now the concept is enough.
- Equivalences between PostgreSQL, MySQL and SQLite
If you have to port a schema or read somebody else's code, this table will save you time:
| Concept | PostgreSQL 16 | MySQL 8 | SQLite 3 |
|---|---|---|---|
| Integer | INTEGER, BIGINT |
INT, BIGINT |
INTEGER |
| Auto-increment | GENERATED AS IDENTITY / SERIAL |
AUTO_INCREMENT |
INTEGER PRIMARY KEY AUTOINCREMENT |
| Exact decimal | NUMERIC(p,s) |
DECIMAL(p,s) |
NUMERIC (affinity, no guarantee) |
| Floating point | REAL, DOUBLE PRECISION |
FLOAT, DOUBLE |
REAL |
| Short text | VARCHAR(n) |
VARCHAR(n) |
TEXT |
| Long text | TEXT |
TEXT, LONGTEXT |
TEXT |
| Boolean | BOOLEAN (real) |
TINYINT(1) (alias) |
No type: 0 / 1 |
| Date | DATE |
DATE |
TEXT in ISO format |
| Date and time | TIMESTAMP, TIMESTAMPTZ |
DATETIME, TIMESTAMP |
TEXT / INTEGER |
| JSON | JSONB (binary, indexable) |
JSON |
TEXT + JSON1 functions |
| UUID | UUID (native) |
CHAR(36) or BINARY(16) |
TEXT |
| Type system | Strict | Strict (with strict mode on) | Dynamic: almost anything is accepted |
The most dangerous difference is in the last row. SQLite uses type affinity: if you declare a column as INTEGER and insert the text 'hello' into it, it stores it without complaint. That's convenient for prototyping and disastrous for guaranteeing integrity. It's the main reason this course uses PostgreSQL.
- How to inspect existing tables
When you join a new project, the first thing to do is understand its schema. There are two routes.
11.1. psql metacommands (fast)
List of relations Schema | Name | Type | Owner --------+---------------+-------+------------ public | categories | table | sql_course public | customers | table | sql_course public | employees | table | sql_course public | order_lines | table | sql_course public | orders | table | sql_course public | products | table | sql_course public | returns | table | sql_course public | reviews | table | sql_course public | suppliers | table | sql_course
Table "public.products"
Column | Type | Nullable | Default
--------------+-----------------------+----------+------------------------------
id | integer | not null | generated by default as identity
name | character varying(150)| not null |
category_id | integer | |
supplier_id | integer | |
price | numeric(10,2) | not null |
cost | numeric(10,2) | |
stock | integer | not null | 0
active | boolean | not null | true
added_date | date | not null | CURRENT_DATE
Indexes:
"products_pkey" PRIMARY KEY, btree (id)
Foreign-key constraints:
"products_category_id_fkey" FOREIGN KEY (category_id) REFERENCES categories(id)
"products_supplier_id_fkey" FOREIGN KEY (supplier_id) REFERENCES suppliers(id)
Referenced by:
TABLE "order_lines" CONSTRAINT ... FOREIGN KEY (product_id) REFERENCES products(id)A single command gives you columns, types, nullability, defaults, indexes, foreign keys and which other tables point at this one. Use \d+ products to see the size and comments as well.
| Metacommand | What it shows |
|---|---|
\dt |
Tables in the current schema |
\d table_name |
The full structure of a table |
\d+ table_name |
The above plus size, statistics and comments |
\dn |
Schemas |
\di |
Indexes |
\dv |
Views |
\l+ |
Databases with their size |
11.2. information_schema (portable and queryable)
The SQL standard defines a metadata schema called information_schema, available in PostgreSQL, MySQL and SQL Server. Its advantage over \d is that it's ordinary SQL: you can filter it, sort it and use it from any language.
SELECT column_name,
data_type,
character_maximum_length,
numeric_precision,
numeric_scale,
is_nullable
FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = 'products'
ORDER BY ordinal_position;| column_name | data_type | character_maximum_length | numeric_precision | numeric_scale | is_nullable |
|---|---|---|---|---|---|
| id | integer | (null) | 32 | 0 | NO |
| name | character varying | 150 | (null) | (null) | NO |
| category_id | integer | (null) | 32 | 0 | YES |
| supplier_id | integer | (null) | 32 | 0 | YES |
| price | numeric | (null) | 10 | 2 | NO |
| cost | numeric | (null) | 10 | 2 | YES |
| stock | integer | (null) | 32 | 0 | NO |
| active | boolean | (null) | (null) | (null) | NO |
| added_date | date | (null) | (null) | (null) | NO |
Useful information_schema views:
| View | What it contains |
|---|---|
information_schema.tables |
All tables and views |
information_schema.columns |
All columns with their types |
information_schema.table_constraints |
Constraints (PK, FK, UNIQUE, CHECK) |
information_schema.key_column_usage |
Which columns take part in each key |
And a query you'll use often to draw yourself a quick map of an unfamiliar database:
SELECT table_name, COUNT(*) AS num_columns
FROM information_schema.columns
WHERE table_schema = 'public'
GROUP BY table_name
ORDER BY table_name;| table_name | num_columns |
|---|---|
| categories | 3 |
| customers | 8 |
| employees | 8 |
| order_lines | 6 |
| orders | 7 |
| products | 9 |
| returns | 5 |
| reviews | 6 |
| suppliers | 5 |
information_schemais the standard, but PostgreSQL also has its own cataloguepg_catalog(pg_tables,pg_class,pg_attribute), more complete and faster, though not portable. The\dmetacommands query precisely that catalogue.
Common Mistakes and Tips
- Storing money in
FLOATorREAL. The most expensive mistake in this lesson. AlwaysNUMERIC(10,2). - Storing dates as text.
VARCHARfor a date stops you sorting properly, computing differences and validating. UseDATEorTIMESTAMPTZ. - Storing numbers that aren't quantities as numbers. A postcode (
03001), a phone number or a tax id is text: if you store them asINTEGERyou'll lose the leading zero and the+34. - Writing
VARCHAR(255)out of habit. The 255 comes from old MySQL. In PostgreSQL useTEXTunless there's a real business limit. - Using
CHAR(n). The space padding causes subtle errors. It's practically never the right choice. - Confusing
NULLwith0or''. They're three different things and they behave differently in filters and aggregations. - Relying on row order. Without
ORDER BYno order is guaranteed, however sorted they may come out today. - Tip: choose the type with five years in mind. Changing a column's type on millions of rows in production is a delicate operation (module 5).
- Tip:
\d tableis your first command on anybody else's database. Before writing a query, look at the structure. - Tip: use plural table names and singular column names.
products.namereads better thanproduct.names.
Exercises
Exercise 1
For each piece of GreenStore data, pick the most suitable PostgreSQL type and justify it in one line:
- A product's selling price.
- A supplier's country code in ISO format (
ES,PT,FR). - The comment on a review.
- A review's rating (1 to 5).
- The date a customer signed up.
- Whether a product is active or not.
- The exact instant a payment was confirmed, with customers in three countries.
- The discount applied to an order line (a fraction from 0 to 1, two decimals).
Exercise 2
Run these expressions and explain what each one demonstrates:
SELECT 1.0 / 3.0 AS a;
SELECT (1.0 / 3.0)::REAL AS b;
SELECT 100000000.0::REAL + 1 AS c;
SELECT 'abc'::CHAR(6) || '|' AS d;
SELECT NULL + 5 AS e;Exercise 3
Write a query against information_schema that shows every numeric column in the greenstore database, stating which table it belongs to and with what precision and scale. Sort them by table and by position within the table.
Solutions
Solution 1
| Data | Type | Justification |
|---|---|---|
| 1. Selling price | NUMERIC(10,2) |
It's money: it demands exact decimal arithmetic |
| 2. ISO country code | CHAR(2) or VARCHAR(2) |
Known fixed length. It's the one situation where CHAR can be defended; VARCHAR(2) avoids the space padding. (GreenStore stores the full country name, so it uses VARCHAR(60)) |
| 3. Review comment | TEXT |
Unpredictable length; no business limit |
| 4. Rating 1-5 | SMALLINT (or INTEGER) |
A very small integer. The 1-5 range is guaranteed with a CHECK constraint (module 5), not with the type |
| 5. Signup date | DATE |
A calendar date, with no relevant time |
| 6. Product active | BOOLEAN |
Two states plus the unknown one |
| 7. Instant of payment | TIMESTAMPTZ |
It's a real instant and several time zones are involved |
| 8. Discount | NUMERIC(4,2) |
An exact value between 0.00 and 1.00; it takes part in amount calculations |
Solution 2
| Expression | Result | What it demonstrates |
|---|---|---|
1.0 / 3.0 |
0.33333333333333333333 |
Decimal literals are numeric: PostgreSQL keeps many exact digits |
(1.0/3.0)::REAL |
0.33333334 |
REAL only keeps about 6-7 significant digits: information is lost |
100000000.0::REAL + 1 |
100000000 |
The +1 vanishes: REAL doesn't have enough precision to tell 100,000,000 from 100,000,001. It's the definitive argument against using floating point for money |
'abc'::CHAR(6) || '|' |
abc| |
Even though CHAR(6) pads to 6 characters, the concatenation strips the trailing spaces. Inconsistent behaviour that justifies avoiding CHAR |
NULL + 5 |
(null) |
NULL propagates: any arithmetic operation with a null gives null |
Solution 3
SELECT table_name,
column_name,
numeric_precision,
numeric_scale,
is_nullable
FROM information_schema.columns
WHERE table_schema = 'public'
AND data_type = 'numeric'
ORDER BY table_name, ordinal_position;Expected result on GreenStore:
| table_name | column_name | numeric_precision | numeric_scale | is_nullable |
|---|---|---|---|---|
| employees | salary | 10 | 2 | YES |
| order_lines | unit_price | 10 | 2 | NO |
| order_lines | discount | 4 | 2 | NO |
| orders | shipping_cost | 10 | 2 | NO |
| products | price | 10 | 2 | NO |
| products | cost | 10 | 2 | YES |
| returns | amount | 10 | 2 | YES |
Notice that data_type returns 'numeric' in lowercase, and that precision and scale come in separate columns: information_schema normalises types to the vocabulary of the SQL standard, not to the name you wrote when creating the table.
Conclusion
In this lesson you've walked through the structure SQL acts on:
- The hierarchy server → database → schema (
public) → table → columns and rows, and why you can't query two databases at once in PostgreSQL. - A table is a set of rows with the same structure, with no intrinsic order and one type per column checked by the engine: that's the big difference from a spreadsheet.
- PostgreSQL's catalogue of types: numeric (
INTEGER,BIGINT,NUMERIC(p,s),REAL), text (TEXT,VARCHAR(n),CHAR(n)), date and time (DATE,TIMESTAMP,TIMESTAMPTZ,INTERVAL),BOOLEAN,UUIDandJSONB. - The non-negotiable rule: money goes in
NUMERIC, never in floating point, because0.1 + 0.2 <> 0.3in binary. NULLas the absence of a value, distinct from0and from'', propagating through every operation.- The type equivalences between PostgreSQL, MySQL and SQLite, and the danger of SQLite's dynamic type system.
- How to inspect somebody else's database with
\dt,\d tableandinformation_schema.columns.
In the next lesson, The Relational Model: Primary and Foreign Keys, we'll see how tables stop being isolated and start relating to one another: what a primary key is and why we use surrogate id columns, how foreign keys guarantee there are no orders from non-existent customers, what happens when you delete a record that others depend on, how 1:1, 1:N and N:M cardinalities are represented —with order_lines as a real example of a bridge table— and how normalisation explains why GreenStore's schema is split into nine tables and not one.
SQL Course
Module 1: Introduction to SQL
- What is SQL?
- Setting up your SQL environment
- Basic SQL syntax
- Understanding databases and tables
- The relational model: primary and foreign keys
- The course database: GreenStore
Module 2: Basic SQL queries
- The SELECT statement
- Aliases, expressions and calculated columns
- Filtering data with WHERE
- DISTINCT and removing duplicates
- Sorting data with ORDER BY
- Limiting results with LIMIT
Module 3: Working with multiple tables
- JOIN operations
- INNER JOIN
- LEFT JOIN
- RIGHT JOIN
- FULL OUTER JOIN
- SELF JOIN and CROSS JOIN
- Set operations: UNION, INTERSECT and EXCEPT
Module 4: Advanced data filtering
- Using LIKE for pattern matching
- The IN and BETWEEN operators
- NULL values and IS NULL
- Aggregate functions: COUNT, SUM, AVG, MIN and MAX
- Aggregating data with GROUP BY
- The HAVING clause
Module 5: Data manipulation
- Creating tables and constraints with CREATE TABLE
- The INSERT statement
- The UPDATE statement
- The DELETE statement
- The UPSERT (MERGE) statement
- Changing the schema: ALTER TABLE and safe migrations
Module 6: Advanced SQL functions
- String functions
- Numeric functions
- Date and time functions
- Type conversion and handling NULL: CAST and COALESCE
- Conditional expressions
Module 7: Subqueries and nested queries
- Introduction to subqueries
- Correlated subqueries
- EXISTS and NOT EXISTS
- Using subqueries in SELECT, FROM and WHERE
- Subquery or JOIN: which one to choose
Module 8: Indexes and performance tuning
- Understanding indexes
- Creating and managing indexes
- Index types and when not to index
- Query optimization techniques
- Analyzing query performance
Module 9: Transactions and concurrency
- Introduction to transactions
- ACID properties
- Transaction control statements
- Isolation levels and concurrency anomalies
- Handling concurrency: locks and deadlocks
Module 10: Advanced topics
- Views
- Common table expressions (CTEs)
- Window functions
- Stored procedures
- Triggers
- JSON and semi-structured data
Module 11: SQL in practice
- Real-world use cases
- Best practices
- Security: SQL injection, permissions and roles
- SQL for data analysis
- SQL in web development
