Oracle Database
Tables, Rows & Columns
A table is the fundamental unit of storage in Oracle. Every piece of data you insert, query, update, or delete lives in a table. Understanding how tables are structured, how Oracle organises rows and columns physically, and how to create well-defined tables is the foundation of everything else in this course. This lesson creates the first tables of the DataplexaStore and DataplexaHR schemas that all subsequent lessons build on.
Course Dataset
Both schemas used throughout this course — DataplexaStore and DataplexaHR — are in a single SQL file. Run it once in your Oracle XE environment (connected to XEPDB1) to create all tables and load all sample data.
Download Dataset SQL dataplexa_dataset.sql · 10 tables · ~100 rowsTables
A table is a two-dimensional structure that stores data in a fixed, organised format. Think of it like a spreadsheet grid — but with strict rules about what each column can hold, enforced by the database engine rather than left to the person entering data. Every piece of data in Oracle lives in a table — there is no other permanent storage structure for rows of data.
In Oracle, a table belongs to a schema. A schema is a named container owned by a database user — it groups all the objects that user creates. When you connect as the user dataplexa and create a table called products, the fully qualified name is dataplexa.products. Within your own session you refer to it simply as products.
Every table in Oracle has three fundamental properties:
- A name — unique within the schema, stored in uppercase internally
- A fixed set of columns — defined at creation time; every row must conform to this structure
- Zero or more rows — a table with no rows is valid; it simply has structure but no data yet
-- user_tables lists every table in the currently connected user's schema
-- num_rows is not a live count — it reflects the last time statistics were gathered;
-- use SELECT COUNT(*) FROM table_name for the real current count
SELECT table_name,
num_rows,
last_analyzed,
tablespace_name
FROM user_tables
ORDER BY table_name;─────────────── ──────── ───────────────────── ───────────────
CUSTOMERS 0 28-MAR-2024 15:00:00 USERS
EMPLOYEES 107 28-MAR-2024 15:00:00 USERS
ORDER_ITEMS 0 28-MAR-2024 15:00:00 USERS
ORDERS 0 28-MAR-2024 15:00:00 USERS
PAYMENTS 0 28-MAR-2024 15:00:00 USERS
PRODUCTS 0 28-MAR-2024 15:00:00 USERS
user_tablesshows only the tables owned by the currently connected user — useall_tablesto see tables you have been granted access to across all schemas, ordba_tablesfor every table in the entire database (requires DBA privilege)- Oracle stores all object names in uppercase internally —
CREATE TABLE productsis stored asPRODUCTS; this is whyuser_tablesalways shows uppercase names regardless of how you typed them num_rowsinuser_tablesis not a live count — it reflects the last time statistics were gathered withDBMS_STATS; useSELECT COUNT(*) FROM table_namefor the real current count
Columns
A column defines one attribute of the entity a table represents. It has three things: a name, a data type, and a nullability rule. The name identifies the attribute. The data type enforces what kind of value can be stored — text, a number, a date. The nullability rule controls whether the column must always have a value (NOT NULL) or whether it can be left empty (NULL).
Choosing columns carefully at design time matters. A column that is too wide wastes storage. A column that is too narrow causes data to be truncated or rejected. A column that allows NULL when it should not allows incomplete records into the database silently.
Every column in Oracle has these characteristics:
- Name — up to 128 characters, must start with a letter, can contain letters, numbers, and underscores; convention is lowercase with underscores —
unit_price,hire_date,customer_id - Data type — defines the kind and size of value allowed:
VARCHAR2(n)for variable-length text,NUMBER(p,s)for numeric values,DATEfor date and time. Full coverage is in Lesson 9. - NULL or NOT NULL —
NOT NULLmeans the column must always have a value;NULL(the default) means the column is optional and can be left empty - DEFAULT — an optional value Oracle uses automatically when no value is supplied during INSERT
-- DESCRIBE is a SQL Developer / SQL*Plus command, not a SQL statement
-- It prints column names, data types, and nullability in a readable format
DESCRIBE products;
-- user_tab_columns gives the same information but as queryable rows
-- Useful inside scripts where DESCRIBE cannot be embedded
-- nullable: 'N' = NOT NULL enforced, 'Y' = NULL allowed
-- data_default: shows the DEFAULT expression defined on the column e.g. SYSDATE or 0
-- table_name must be uppercase when filtering — Oracle stores all names in uppercase
SELECT column_name,
data_type,
data_length,
data_precision, -- total significant digits — applies to NUMBER columns only
data_scale, -- decimal places — e.g. 2 for NUMBER(10,2) money columns
nullable,
data_default
FROM user_tab_columns
WHERE table_name = 'PRODUCTS'
ORDER BY column_id; -- column_id preserves the order columns were defined inName Null? Type
────────────── ──────── ──────────────
PRODUCT_ID NOT NULL NUMBER(10)
PRODUCT_NAME NOT NULL VARCHAR2(150)
CATEGORY NOT NULL VARCHAR2(60)
UNIT_PRICE NOT NULL NUMBER(10,2)
STOCK_QTY NOT NULL NUMBER(8)
CREATED_DATE NOT NULL DATE
-- user_tab_columns:
COLUMN_NAME DATA_TYPE DATA_LENGTH DATA_PRECISION DATA_SCALE NULLABLE DATA_DEFAULT
───────────── ───────── ─────────── ────────────── ────────── ──────── ────────────
PRODUCT_ID NUMBER 22 10 0 N (null)
PRODUCT_NAME VARCHAR2 150 (null) (null) N (null)
CATEGORY VARCHAR2 60 (null) (null) N (null)
UNIT_PRICE NUMBER 22 10 2 N (null)
STOCK_QTY NUMBER 22 8 0 N 0
CREATED_DATE DATE 7 (null) (null) N SYSDATE
DESCRIBE(orDESC) is an Oracle SQL*Plus and SQL Developer command — it is not a SQL statement and cannot be embedded inside a query; useuser_tab_columnswhen you need column details inside a scriptnullable = 'N'inuser_tab_columnsmeans NOT NULL — the column must always have a value;nullable = 'Y'means the column accepts NULLdata_defaultshows the DEFAULT expression assigned to the column —SYSDATEforcreated_dateand0forstock_qtymatch exactly what was defined in theCREATE TABLEstatement
Rows
A row is one complete record — one instance of the entity the table describes. Every row in a table has exactly the same columns as every other row. A row in the customers table represents one customer. A row in the order_items table represents one line on one order. A row in the employees table represents one person employed by the organisation.
Oracle stores rows in data blocks — fixed-size pages of storage, typically 8 KB each, held in data files on disk and cached in the SGA buffer cache in memory. When a row is too wide to fit in a single block, Oracle chains it across multiple blocks. When multiple small rows fit in one block, Oracle packs them together. The application sees only logical rows and columns — the physical block management is handled entirely by Oracle.
Key things to understand about rows in Oracle:
- A row has no guaranteed physical order — Oracle does not store rows in any particular sequence unless you use an Index-Organised Table (IOT). A
SELECTwithout anORDER BYclause can return rows in any order and that order may change between executions - Each row has a unique physical address called a ROWID — it encodes the data file number, block number, and row position within the block. ROWID is the fastest possible way to access a single row and is used internally by Oracle indexes.
- NULL in a row means the value is unknown or not applicable — it is not zero, not an empty string, and not a space. NULL is the absence of a value. A phone column that is NULL means the phone number is not known — it does not mean the customer has no phone.
-- ROWID is Oracle's internal physical address for every row
-- It encodes the data file, block number, and row slot — used internally by indexes
-- Never store ROWID in application tables — it changes if a row is moved
SELECT ROWID,
customer_id,
full_name,
country
FROM customers;
-- IS NULL is the only correct way to test for a missing value
-- WHERE phone = NULL never matches anything — NULL is not equal to anything, including itself
SELECT customer_id,
full_name,
phone
FROM customers
WHERE phone IS NULL;
-- COUNT(*) counts all rows; COUNT(column) skips NULLs in that column
-- The difference reveals exactly how many rows are missing a phone number
SELECT COUNT(*) AS total_rows,
COUNT(phone) AS rows_with_phone,
COUNT(*) - COUNT(phone) AS rows_without_phone
FROM customers;ROWID CUSTOMER_ID FULL_NAME COUNTRY
───────────────────── ─────────── ────────────────── ──────────────
AAAWkbAAHAAAACXAAA 1001 Sarah Mitchell United Kingdom
AAAWkbAAHAAAACXAAB 1002 James Okafor Nigeria
AAAWkbAAHAAAACXAAC 1003 Priya Nair India
AAAWkbAAHAAAACXAAD 1004 Lucas Fernandez Brazil
AAAWkbAAHAAAACXAAE 1005 Emma Bergstrom Sweden
-- NULL phone rows:
CUSTOMER_ID FULL_NAME PHONE
─────────── ─────────────── ─────
1002 James Okafor (null)
1004 Lucas Fernandez (null)
-- COUNT comparison:
TOTAL_ROWS ROWS_WITH_PHONE ROWS_WITHOUT_PHONE
────────── ─────────────── ──────────────────
5 3 2
- ROWID should never be stored in application tables or used as a join condition — it changes when a row is moved (for example during a table reorganisation or partition move), making stored ROWIDs unreliable
COUNT(*)counts all rows including those with NULL values in every column;COUNT(column_name)counts only rows where that column is NOT NULL — this is whyCOUNT(*)andCOUNT(phone)produce different results when some phone values are NULL- Oracle treats NULL as not equal to anything — including another NULL.
WHERE phone = NULLnever returns rows; the correct syntax is alwaysWHERE phone IS NULL
Creating the DataplexaStore Tables
The DataplexaStore schema models an e-commerce platform. It has five tables: Customers, Products, Orders, OrderItems, and Payments. Each table is created with CREATE TABLE, specifying column names, data types, and inline constraints. Data types are covered fully in Lesson 9 — for now, NUMBER, VARCHAR2, and DATE are the three types used here.
-- CUSTOMERS — one row per registered customer
CREATE TABLE customers (
customer_id NUMBER(10) NOT NULL,
full_name VARCHAR2(100) NOT NULL,
email VARCHAR2(150) NOT NULL,
phone VARCHAR2(20), -- optional — not all customers provide a phone number
country VARCHAR2(60) NOT NULL,
city VARCHAR2(60), -- optional — city is not always provided at registration
created_date DATE DEFAULT SYSDATE NOT NULL -- auto-filled with current date on insert
);
-- PRODUCTS — one row per product available for sale
CREATE TABLE products (
product_id NUMBER(10) NOT NULL,
product_name VARCHAR2(150) NOT NULL,
category VARCHAR2(60) NOT NULL,
unit_price NUMBER(10,2) NOT NULL, -- 2 decimal places for currency values e.g. 49.99
stock_qty NUMBER(8) DEFAULT 0 NOT NULL, -- new products start at 0 stock
created_date DATE DEFAULT SYSDATE NOT NULL
);
-- ORDERS — one row per order placed by a customer
CREATE TABLE orders (
order_id NUMBER(10) NOT NULL,
customer_id NUMBER(10) NOT NULL,
order_date DATE DEFAULT SYSDATE NOT NULL,
status VARCHAR2(20) DEFAULT 'pending' NOT NULL, -- all new orders begin as pending
total_amount NUMBER(12,2) DEFAULT 0 NOT NULL -- recalculated as items are added
);
-- ORDER_ITEMS — one row per product line on an order
-- An order containing 3 different products produces 3 rows in this table
CREATE TABLE order_items (
order_item_id NUMBER(10) NOT NULL,
order_id NUMBER(10) NOT NULL,
product_id NUMBER(10) NOT NULL,
quantity NUMBER(6) NOT NULL,
unit_price NUMBER(10,2) NOT NULL -- price captured at order time — independent of product price changes
);
-- PAYMENTS — one row per payment made against an order
CREATE TABLE payments (
payment_id NUMBER(10) NOT NULL,
order_id NUMBER(10) NOT NULL,
payment_date DATE DEFAULT SYSDATE NOT NULL,
amount NUMBER(12,2) NOT NULL,
method VARCHAR2(30) NOT NULL -- e.g. credit_card, paypal, bank_transfer
);Table PRODUCTS created.
Table ORDERS created.
Table ORDER_ITEMS created.
Table PAYMENTS created.
NOT NULLis an inline constraint that prevents empty values — any attempt to insert a row without providing a value for a NOT NULL column will fail with ORA-01400DEFAULT SYSDATEmeans Oracle automatically fills in the current date and time if the caller does not supply a value — this is how audit columns likecreated_dateare typically managed without requiring the application to pass a date- Column names use lowercase with underscores — this is a widely adopted Oracle naming convention that produces readable SQL; Oracle stores them as uppercase internally but you can type them in any case in your queries
Creating the DataplexaHR Tables
The DataplexaHR schema models a human resources system. It has five tables: Departments, Jobs, Employees, Salaries, and PayrollHistory. These tables are referenced throughout the HR-focused examples in this course.
-- DEPARTMENTS — one row per department in the organisation
CREATE TABLE departments (
department_id NUMBER(6) NOT NULL,
department_name VARCHAR2(100) NOT NULL,
location VARCHAR2(100), -- optional — some departments may be fully remote
manager_id NUMBER(6) -- NULL allowed — a new department may not yet have a manager
);
-- JOBS — one row per job role, holding the salary band for that grade
CREATE TABLE jobs (
job_id VARCHAR2(20) NOT NULL, -- human-readable code e.g. 'IT_PROG', 'HR_MGR'
job_title VARCHAR2(100) NOT NULL,
min_salary NUMBER(10,2) NOT NULL,
max_salary NUMBER(10,2) NOT NULL
);
-- EMPLOYEES — one row per person employed by the organisation
CREATE TABLE employees (
employee_id NUMBER(6) NOT NULL,
first_name VARCHAR2(50) NOT NULL,
last_name VARCHAR2(50) NOT NULL,
email VARCHAR2(150) NOT NULL,
hire_date DATE NOT NULL,
job_id VARCHAR2(20) NOT NULL,
department_id NUMBER(6), -- NULL allowed — contractors or new starters may not yet be assigned
manager_id NUMBER(6) -- NULL allowed — the most senior executive has no manager
);
-- SALARIES — one row per current salary record per employee
CREATE TABLE salaries (
salary_id NUMBER(10) NOT NULL,
employee_id NUMBER(6) NOT NULL,
amount NUMBER(10,2) NOT NULL,
effective_date DATE NOT NULL,
salary_type VARCHAR2(20) DEFAULT 'monthly' NOT NULL -- monthly is the standard pay cycle
);
-- PAYROLL_HISTORY — one row per pay run per employee, grows every pay period
CREATE TABLE payroll_history (
payroll_id NUMBER(10) NOT NULL,
employee_id NUMBER(6) NOT NULL,
pay_period DATE NOT NULL, -- last day of the pay period e.g. 31-JAN-2024
gross_pay NUMBER(10,2) NOT NULL,
tax_deducted NUMBER(10,2) DEFAULT 0 NOT NULL, -- defaults to 0 if not yet calculated
net_pay NUMBER(10,2) NOT NULL
);Table JOBS created.
Table EMPLOYEES created.
Table SALARIES created.
Table PAYROLL_HISTORY created.
- The
manager_idcolumn in bothdepartmentsandemployeesis nullable — not every department has a designated manager yet, and not every employee has a manager (the CEO has no manager); allowing NULL here is intentional and correct job_idin thejobstable is aVARCHAR2rather than aNUMBER— human-readable job codes such asIT_PROGorHR_MGRare more practical as identifiers in an HR system than arbitrary integers- Primary key and foreign key constraints are not added here — they are the subject of Lesson 11 (Constraints & Keys); the tables are structurally complete but not yet fully constrained
Modifying and Dropping Tables
After a table is created you can modify its structure using ALTER TABLE. You can add new columns, modify existing column definitions, or rename columns. Dropping a table with DROP TABLE removes it permanently along with all its data.
-- ADD a new column — DEFAULT is required because the table already has rows
-- Oracle needs a value to back-fill this column for all existing customers
ALTER TABLE customers
ADD (loyalty_tier VARCHAR2(20) DEFAULT 'standard' NOT NULL);
-- ADD multiple columns in a single statement
ALTER TABLE products
ADD (
description VARCHAR2(500), -- optional — NULL allowed
is_active VARCHAR2(1) DEFAULT 'Y' NOT NULL -- Y = on sale, N = delisted
);
-- MODIFY increases the column size — safe with existing data
-- Decreasing size is only allowed if every existing value already fits within the smaller length
ALTER TABLE customers
MODIFY (phone VARCHAR2(30));
-- RENAME COLUMN — changes the name only, data and data type are unaffected
ALTER TABLE products
RENAME COLUMN is_active TO active_flag;
-- RENAME TO renames the entire table — data, constraints, and indexes all move with it
ALTER TABLE order_items
RENAME TO order_lines;
ALTER TABLE order_lines
RENAME TO order_items;
-- DROP TABLE moves the table to Oracle's Recycle Bin — recoverable with FLASHBACK
DROP TABLE payments;
-- DROP TABLE ... PURGE bypasses the Recycle Bin — permanently deleted, no recovery possible
DROP TABLE payments PURGE;Table PRODUCTS altered.
Table CUSTOMERS altered.
Table PRODUCTS altered.
Table ORDER_ITEMS renamed.
Table ORDER_LINES renamed.
Table PAYMENTS dropped.
Table PAYMENTS dropped.
- Oracle's Recycle Bin means a dropped table is not immediately gone — it can be recovered using
FLASHBACK TABLE payments TO BEFORE DROPas long as it has not been purged; this has saved many developers and DBAs from accidental data loss - You can only increase the size of a
VARCHAR2column withMODIFY— decreasing it is only possible if every existing value already fits in the smaller size, otherwise Oracle raises ORA-01441 ALTER TABLE ... ADDwithNOT NULLrequires aDEFAULTvalue when the table already contains rows — without a default, Oracle cannot populate the new column for existing rows and will raise ORA-01758
Summary
| Concept | What It Is | Key Point |
|---|---|---|
| Table | A named, structured storage object in a schema | Every piece of permanent data in Oracle lives in a table |
| Column | One attribute of the entity — has a name, data type, and nullability | Every row in the table must conform to the column definition |
| Row | One complete record — one instance of the entity | Has no guaranteed order without ORDER BY; identified by ROWID |
| ROWID | Physical address of a row — file, block, and row position | Fastest row access — never store in application tables |
| NULL | The absence of a value — unknown or not applicable | Use IS NULL / IS NOT NULL — never = NULL |
| NOT NULL | Column constraint — value must always be supplied | Raises ORA-01400 on violation |
| DEFAULT | Auto-fills a column value when none is provided | DEFAULT SYSDATE auto-fills the current date and time |
| DESCRIBE | Displays column names, types, and nullability | SQL Developer command — not a SQL statement |
| user_tab_columns | Data dictionary view with full column detail | Use inside scripts where DESCRIBE cannot be embedded |
| CREATE TABLE | Defines a new table with its columns and constraints | Column name, data type, and NOT NULL defined inline |
| ALTER TABLE | Modifies an existing table structure | ADD, MODIFY, RENAME COLUMN, RENAME TO |
| DROP TABLE | Removes a table and all its data permanently | Goes to Recycle Bin unless PURGE is specified |
Practice Questions
Practice 1. What is the difference between a column and a row in a database table?
Practice 2. What does DEFAULT SYSDATE do when defined on a column?
Practice 3. You run DROP TABLE customers. Can the table be recovered and how?
Practice 4. Why does adding a NOT NULL column to a table that already contains rows require a DEFAULT value?
Practice 5. What is the difference between user_tables, all_tables, and dba_tables?
Quiz
Quiz 1. What Oracle command displays a table's column names, data types, and whether NULL is allowed?
Quiz 2. You want to add a required mobile_number column (VARCHAR2(20), NOT NULL) to the employees table which already has 200 rows. Which statement is correct?
Quiz 3. Oracle stores table and column names internally in which case?
Quiz 4. What is the effect of DROP TABLE products PURGE compared to DROP TABLE products?
Quiz 5. Which statement correctly renames the order_items table to order_lines in Oracle?
Next up — Oracle Data Types — Learn every data type Oracle provides for storing text, numbers, dates, and large objects, and understand which type to choose for each column in your schema.