Oracle Database
Creating Tables in Oracle
Lesson 8 introduced tables at a structural level. This lesson goes deeper — applying the data types from Lesson 9 to build production-quality table definitions with correct column sizing, appropriate defaults, and well-reasoned nullability decisions. By the end of this lesson every table in both schemas will be fully defined with the right types on every column, ready for constraints to be added in Lesson 11.
To follow along with the examples in this lesson, use the course dataset from Lesson 8.
CREATE TABLE Syntax in Full
The full CREATE TABLE syntax in Oracle covers the table name, column list, and optional table-level clauses. Each column is defined with a name, a data type, an optional DEFAULT value, and an optional NOT NULL constraint. The TABLESPACE clause directs Oracle to store the table in a specific tablespace rather than the default USERS tablespace.
Oracle evaluates column definitions in order — the first column defined is the leftmost column in every row. Column order matters for readability and convention: identifiers first, descriptive columns next, foreign key references after that, status and flag columns near the end, and audit columns like created_date last.
-- TABLESPACE directs storage to a named tablespace rather than the default USERS
CREATE TABLE customers (
customer_id NUMBER(10) NOT NULL,
full_name VARCHAR2(100) NOT NULL,
email VARCHAR2(150) NOT NULL,
phone VARCHAR2(20),
country VARCHAR2(60) NOT NULL,
city VARCHAR2(60),
loyalty_tier VARCHAR2(20) DEFAULT 'standard' NOT NULL,
created_date DATE DEFAULT SYSDATE NOT NULL
)
TABLESPACE dataplexa_store_ts;
-- Verify the table landed in the correct tablespace
SELECT table_name,
tablespace_name,
num_rows,
last_analyzed
FROM user_tables
WHERE table_name = 'CUSTOMERS';TABLE_NAME TABLESPACE_NAME NUM_ROWS LAST_ANALYZED
─────────── ──────────────────── ──────── ─────────────
CUSTOMERS DATAPLEXA_STORE_TS 0 (null)
- If
TABLESPACEis omitted Oracle places the table in the default tablespace for the connected user — in XE this isUSERS; in production always specify it explicitly num_rowsshows 0 immediately after creation because statistics have not been gathered yet — useSELECT COUNT(*)for an accurate live count- Column order is permanent — Oracle does not allow reordering columns after creation without dropping and recreating the table
Designing Column Definitions
Good column definitions are precise — sized to hold the real data, typed to match the real meaning, and constrained to reflect the real business rules. Every column definition answers three questions: what type of data is it, how large can it be, and is it always required?
A common mistake is oversizing columns — using VARCHAR2(4000) for a status code or NUMBER without precision for a price. Oversizing does not increase storage for VARCHAR2 but it communicates nothing about the data and allows invalid values in. Right-sizing is documentation as well as enforcement.
-- DataplexaStore — all five tables with production-quality column definitions
CREATE TABLE customers (
customer_id NUMBER(10) NOT NULL,
full_name VARCHAR2(100) NOT NULL,
email VARCHAR2(150) NOT NULL,
phone VARCHAR2(20), -- optional — international format e.g. +44 7700 900123
country VARCHAR2(60) NOT NULL,
city VARCHAR2(60),
loyalty_tier VARCHAR2(20) DEFAULT 'standard' NOT NULL,
created_date DATE DEFAULT SYSDATE NOT NULL
)
TABLESPACE dataplexa_store_ts;
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,
stock_qty NUMBER(8) DEFAULT 0 NOT NULL,
description VARCHAR2(500),
active_flag CHAR(1) DEFAULT 'Y' NOT NULL, -- CHAR(1): always exactly 'Y' or 'N'
created_date DATE DEFAULT SYSDATE NOT NULL
)
TABLESPACE dataplexa_store_ts;
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,
total_amount NUMBER(12,2) DEFAULT 0 NOT NULL -- NUMBER(12,2): totals can exceed a single item price
)
TABLESPACE dataplexa_store_ts;
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 at time of order — stored independently of products.unit_price
)
TABLESPACE dataplexa_store_ts;
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
)
TABLESPACE dataplexa_store_ts;Table PRODUCTS created.
Table ORDERS created.
Table ORDER_ITEMS created.
Table PAYMENTS created.
unit_priceinorder_itemsis separate fromproducts.unit_priceby design — product prices change over time, but a historical order line must always reflect the price at the moment of purchaseactive_flag CHAR(1)is one of the few legitimate uses ofCHAR— the value is always exactly one character, never longer or shortertotal_amountinordersusesNUMBER(12,2)rather thanNUMBER(10,2)— order totals aggregate multiple items and can legitimately exceed the range of a single item price
Creating the DataplexaHR Tables
The HR schema introduces one important design consideration: self-referencing columns. The manager_id in employees references the same table — an employee's manager is also an employee. Both manager_id columns are nullable because not every row has a parent — the most senior executive has no manager, and a newly created department may not yet have one assigned.
CREATE TABLE departments (
department_id NUMBER(6) NOT NULL,
department_name VARCHAR2(100) NOT NULL,
location VARCHAR2(100),
manager_id NUMBER(6) -- NULL allowed — a new department may not yet have a manager
)
TABLESPACE dataplexa_hr_ts;
CREATE TABLE jobs (
job_id VARCHAR2(20) NOT NULL, -- 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
)
TABLESPACE dataplexa_hr_ts;
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 — new starters may not yet be assigned
manager_id NUMBER(6) -- NULL allowed — the most senior employee has no manager
)
TABLESPACE dataplexa_hr_ts;
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
)
TABLESPACE dataplexa_hr_ts;
CREATE TABLE payroll_history (
payroll_id NUMBER(10) NOT NULL,
employee_id NUMBER(6) NOT NULL,
pay_period DATE NOT NULL,
gross_pay NUMBER(10,2) NOT NULL,
tax_deducted NUMBER(10,2) DEFAULT 0 NOT NULL, -- defaults to 0 before payroll is processed
net_pay NUMBER(10,2) NOT NULL
)
TABLESPACE dataplexa_hr_ts;Table JOBS created.
Table EMPLOYEES created.
Table SALARIES created.
Table PAYROLL_HISTORY created.
job_idisVARCHAR2(20)rather thanNUMBER— codes likeIT_PROGandHR_MGRare readable in queries and reports; a numeric surrogate key would carry no meaning on its ownhire_dateisNOT NULL— there is no valid scenario where an employee exists in a payroll system without a known start date- Creation order matters when foreign keys are added in Lesson 11 —
jobsanddepartmentsmust exist beforeemployees, andemployeesbeforesalariesandpayroll_history
Querying Table Structure
After creating tables, Oracle's data dictionary gives you several ways to inspect and verify the structure. These queries are essential during development and debugging when you need to confirm exactly what was created.
-- List all tables in the current schema with tablespace assignment
SELECT table_name,
tablespace_name,
num_rows
FROM user_tables
ORDER BY tablespace_name, table_name;
-- Full column detail for a specific table — column_id preserves creation order
SELECT column_id,
column_name,
data_type,
data_length,
data_precision,
data_scale,
nullable,
data_default
FROM user_tab_columns
WHERE table_name = 'EMPLOYEES'
ORDER BY column_id;
-- Count columns per table across both schemas
SELECT table_name,
COUNT(*) AS column_count
FROM user_tab_columns
WHERE table_name IN (
'CUSTOMERS','PRODUCTS','ORDERS','ORDER_ITEMS','PAYMENTS',
'DEPARTMENTS','JOBS','EMPLOYEES','SALARIES','PAYROLL_HISTORY'
)
GROUP BY table_name
ORDER BY table_name;TABLE_NAME TABLESPACE_NAME NUM_ROWS
─────────────── ──────────────────── ────────
DEPARTMENTS DATAPLEXA_HR_TS 0
EMPLOYEES DATAPLEXA_HR_TS 0
JOBS DATAPLEXA_HR_TS 0
ORDER_ITEMS DATAPLEXA_STORE_TS 0
ORDERS DATAPLEXA_STORE_TS 0
CUSTOMERS DATAPLEXA_STORE_TS 0
PAYMENTS DATAPLEXA_STORE_TS 0
PAYROLL_HISTORY DATAPLEXA_HR_TS 0
PRODUCTS DATAPLEXA_STORE_TS 0
SALARIES DATAPLEXA_HR_TS 0
-- user_tab_columns for EMPLOYEES:
COLUMN_ID COLUMN_NAME DATA_TYPE DATA_LENGTH PRECISION SCALE NULLABLE DATA_DEFAULT
───────── ───────────── ───────── ─────────── ───────── ───── ──────── ────────────
1 EMPLOYEE_ID NUMBER 22 6 0 N (null)
2 FIRST_NAME VARCHAR2 50 (null) (null) N (null)
3 LAST_NAME VARCHAR2 50 (null) (null) N (null)
4 EMAIL VARCHAR2 150 (null) (null) N (null)
5 HIRE_DATE DATE 7 (null) (null) N (null)
6 JOB_ID VARCHAR2 20 (null) (null) N (null)
7 DEPARTMENT_ID NUMBER 22 6 0 Y (null)
8 MANAGER_ID NUMBER 22 6 0 Y (null)
-- Column counts:
TABLE_NAME COLUMN_COUNT
─────────────── ────────────
CUSTOMERS 8
DEPARTMENTS 4
EMPLOYEES 8
JOBS 4
ORDER_ITEMS 5
ORDERS 5
PAYMENTS 5
PAYROLL_HISTORY 6
PRODUCTS 8
SALARIES 5
nullable = 'Y'forDEPARTMENT_IDandMANAGER_IDconfirms the NULL decisions made at design time — a quick way to audit that nullability was applied correctlydata_defaultshows(null)for columns with no default — this means no default expression was defined, not that the column itself allows NULL- Use
user_tab_columnsinside scripts rather thanDESCRIBE—DESCRIBEis an interactive tool that cannot be embedded in SQL or PL/SQL
Summary
| Concept | What It Does | Key Point |
|---|---|---|
| CREATE TABLE ... TABLESPACE | Creates a table in a named tablespace | Always specify TABLESPACE in production |
| Column order | Defines left-to-right order of columns in every row | Permanent — cannot be changed after creation |
| Right-sizing columns | Matching type and size to the real data | Oversizing allows invalid data — sizing is documentation |
| NOT NULL | Enforces a column must always have a value | Raises ORA-01400 on violation |
| DEFAULT SYSDATE | Auto-fills current date and time on insert | Standard pattern for created_date audit columns |
| CHAR(1) for flags | Fixed single-character column for Y/N values | One of the few correct uses of CHAR over VARCHAR2 |
| user_tables | Lists all tables in the current schema | num_rows reflects last stats gather — not a live count |
| user_tab_columns | Lists every column with full type and constraint detail | Use in scripts — DESCRIBE cannot be embedded in SQL |
Practice Questions
Practice 1. Why is column order in a CREATE TABLE statement considered a permanent design decision?
Practice 2. The order_items table stores unit_price separately from products. Why is this intentional and not a mistake?
Practice 3. Why is hire_date defined as NOT NULL in the employees table?
Practice 4. You need to add a notes column to the customers table for free-form text up to 2000 characters. Write the ALTER TABLE statement.
Practice 5. What is the difference between user_tables and user_tab_columns, and when would you use each?
Quiz
Quiz 1. Which clause in CREATE TABLE directs Oracle to store the table in a specific tablespace?
Quiz 2. A salary column stores values like 9500.00 and 10500.00. Which data type is most appropriate?
Quiz 3. Which data dictionary view gives the data type and nullability of every column in a table?
Quiz 4. Why does manager_id in the employees table allow NULL?
Quiz 5. You query user_tables after inserting rows and see num_rows = 0. What is the most likely reason?
Next up — Constraints & Keys — Add primary keys, foreign keys, unique constraints, and check constraints to enforce data integrity across both schemas at the database level.