By the end of this lesson you can describe any table by its grain (what one row is), its columns and their types, and count its rows with a query.
Why it matters: Knowing what one row of a table represents is the first thing every analyst checks. Most wrong numbers in reports come from misunderstanding a table's grain, not from bad SQL.
Dataset: Starter shop. Examples run in the editor on the right.
A relational database stores data in tables (the formal word is relations, which is where the name comes from). Every table follows the same rules:
- A table has a name that describes what it holds, usually a plural noun:
customers,products,orders. - A column is one kind of fact, with a name and a data type. Every value in the
pricecolumn is a number; every value injoined_onis a date. - A row (or record) is one thing: one customer, one product, one order. It has exactly one value (or NULL) in every column.
- The value where a row meets a column is a cell, or simply a value.
Explore the parts below. Click each highlight and read what it means.
| id | name | city |
|---|---|---|
| 1 | Noura | Riyadh |
| 2 | Faisal | Jeddah |
| 3 | Sara | NULL |
| 4 | Omar | Riyadh |
A row (record) is one customer: every column's value for that one thing.
Grain: what is one row?
The single most useful question about a table is: what does one row represent? That is its grain. In Corner Market:
customers: one row per registered customer.products: one row per item for sale.orders: one row per order, and each order is for exactly one product.
If you know the grain, you know what counting rows means. Counting the rows of orders gives the number of orders, not the number of customers who ordered (one customer can have many orders) and not the number of units sold (an order can have a quantity of 3).
To count rows, use count(*). It's your first function: a built-in tool that takes rows in and gives a value back. You'll study counting properly in the aggregation track; for now it's a handy way to measure a table.
SELECT count(*) AS customer_count
FROM customers;count(*) counts the rows that FROM produced: 12, one per customer. AS customer_count gives the result column a readable name. The answer is a table with one row and one column.
Data types keep columns honest
Each column is declared with a type, and the database refuses values that don't fit. The starter tables use four common types:
| type | holds | example |
|---|---|---|
integer | whole numbers | stock = 17 |
numeric(12,2) | exact decimals, here with 2 decimal places | price = 75.50 |
text | words and characters of any length | city = 'Jeddah' |
date | a calendar date | order_date = 2025-03-08 |
Types matter because they decide what you can do with a value: you can add prices, but not cities, and you can ask which date is earlier.
NULL: no value recorded
Sometimes a value is simply not known: a customer never told the shop their city. The database stores NULL in that cell. NULL is not zero, not an empty piece of text and not the word 'NULL'; it means missing. One Corner Market customer has a NULL city and two have a NULL email. Look for them in the next example.
SELECT *
FROM customers;12 rows, one per customer. Scan the city and email columns: one city and two emails show NULL. Those customers still exist; only those particular facts are missing.
Why split data into several tables?
Why not one giant sheet with the customer's name, city and email repeated on every order? Because repeated facts drift apart. If Noura moves to Jeddah, a single-sheet design must update every one of her orders, and missing one leaves the data contradicting itself.
A relational design stores each fact once, in the table it belongs to. The orders table doesn't repeat Noura's name or city; it stores her customer number, customer_id, which points to her one row in customers. That pointer is a relationship, and the next lesson is all about it.