CREATE TABLE defines the schema — column names, data types, and constraints — before any data is inserted
CHAR(n) is fixed-width and right-padded; VARCHAR(n) is variable-length — use CHAR for fixed-length codes, VARCHAR for everything else
Use DECIMAL/NUMERIC for money, never FLOAT or DOUBLE — floating-point binary approximation causes rounding errors
Constraints (NOT NULL, UNIQUE, CHECK, FOREIGN KEY) enforce data rules at the database level, not just the application level
CREATE TABLE IF NOT EXISTS prevents errors when running migration scripts multiple times
Biggest mistake: using FLOAT for monetary values — 0.1 + 0.2 = 0.30000000000000004 in binary floating point
✦ Definition~90s read
What is SQL CREATE TABLE and Data Types?
SQL CREATE TABLE is the foundational DDL statement that defines the structure of a relational database table — specifying column names, data types, and constraints before any data ever touches disk. Its primary job is to enforce a schema that guarantees data integrity at the storage layer, preventing the kind of silent corruption that happens when, say, a FLOAT column rounds $49.99 to $50.00 during aggregation.
★
Think of a database table like a spreadsheet you design before anyone fills it in.
You design the table first because every query, index, and join depends on this blueprint; a poorly chosen data type (like using FLOAT for currency instead of DECIMAL(10,2)) can introduce precision errors that cascade through financial reports. The statement lives in the SQL ecosystem alongside ALTER TABLE (for post-hoc changes) and DROP TABLE (for teardown), but CREATE TABLE is where you lock in the contract between your application and the database — and where most performance and correctness problems are born or avoided.
In practice, CREATE TABLE is not just about columns — it’s about constraints that make your data trustworthy. PRIMARY KEY enforces uniqueness and clustering, NOT NULL prevents missing values in critical fields, CHECK validates ranges (e.g., price >= 0), and FOREIGN KEY maintains referential integrity across tables. The IF NOT EXISTS clause is a defensive pattern that prevents script failures when rerunning migrations, while CREATE TABLE AS (CTAS) lets you clone a table’s structure and data in one shot — useful for staging or backups.
You should avoid CREATE TABLE when you need flexible, schema-on-read storage (use a document store like MongoDB instead) or when you’re prototyping rapidly and expect frequent column changes (consider a migration tool like Flyway or Alembic to manage versions).
Real-world usage: A production e-commerce system might execute CREATE TABLE orders (id BIGSERIAL PRIMARY KEY, total DECIMAL(10,2) NOT NULL CHECK (total >= 0), created_at TIMESTAMPTZ DEFAULT NOW()) — explicitly choosing DECIMAL over FLOAT to avoid the $0.01 rounding errors that plague summation queries. Tools like PostgreSQL, MySQL, and SQL Server all support CREATE TABLE with minor syntax differences (e.g., AUTO_INCREMENT vs SERIAL), but the core principle is universal: define your types and constraints upfront, or pay the price in corrupted data and debugging nightmares later.
Plain-English First
Think of a database table like a spreadsheet you design before anyone fills it in. The CREATE TABLE statement is you drawing the column headers and deciding what kind of data each column is allowed to hold — numbers only here, text there, dates in that column. Just like you wouldn't let someone write their age in the 'First Name' box on a paper form, SQL enforces those rules automatically so your data stays clean and consistent forever.
Every app you use stores data somewhere. Instagram stores your username, follower count, and the exact timestamp of every post. Your bank stores your account number, balance, and transaction history. That data doesn't just float around freely — it lives in structured tables inside a database, and someone had to design those tables before a single row of data was ever saved. That someone could be you, and the tool for the job is SQL's CREATE TABLE statement.
Before CREATE TABLE existed, developers had to manage data in flat files — think giant text documents where structure was implied but never enforced. Nothing stopped someone from saving the word 'banana' into a field meant to hold a bank balance. CREATE TABLE solves that by letting you declare a blueprint upfront: here are the columns, here is what type of data each column accepts, and here are the rules that every single row must follow, no exceptions.
By the end of this article you'll be able to write a complete CREATE TABLE statement from scratch, choose the right data type for any column, add constraints that protect your data integrity, and avoid the three mistakes that trip up almost every beginner. You'll also walk away knowing how to answer the CREATE TABLE questions that come up in real SQL interviews.
What a Table Actually Is — and Why You Design It First
A database table is a grid of rows and columns, almost identical to a spreadsheet tab. Each column has a name and a fixed data type. Each row is one record — one user, one order, one product. The critical difference from a spreadsheet is that you lock down the structure before any data goes in. You can't just add a random column mid-row the way you might type anywhere in Excel.
This design-first approach is what makes relational databases so reliable. When your app inserts a new user record, the database already knows exactly what shape that record must have. It rejects anything that doesn't fit — a text value in a numeric column, a date that doesn't exist, a username that's too long. That enforcement happens automatically, 24/7, without any code on your part.
Think of it like designing a parking garage before you open it. You decide the width of each space (data type), whether a space is reserved (constraints), and how many spaces exist (columns). You can't add a half-space later without a redesign. SQL works the same way — invest in the design upfront and the database protects your data forever.
The CREATE TABLE statement is literally you handing that blueprint to the database engine and saying: 'Build this structure and enforce these rules from now on.'
create_users_table.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
-- Step 1: Create a simple users table for a social media app-- This is the most basic form of CREATE TABLE — no constraints yet,-- just column names and data types. We'll add constraints in the next section.CREATETABLEusers (
user_id INT, -- A whole number to uniquely identify each user
username VARCHAR(50), -- Variable-length text, max 50 characters
email VARCHAR(100), -- Emails can be longer, so we allow up to 100 chars
age INT, -- Whole number — ages don't have decimal points
profile_bio TEXT, -- Unlimited text for longer content like bios
created_at DATETIME, -- Stores both the date AND the time of registration
is_verified BOOLEAN-- TRUE or FALSE — is this account email-verified?
);
-- To confirm the table was created, run:DESCRIBE users; -- MySQL syntax-- or-- \d users -- PostgreSQL syntax
CREATE TABLE builds the structure but inserts zero data. That's expected and correct. The table is an empty container — you'll fill it with INSERT statements later. Seeing '0 rows affected' after a CREATE TABLE is the success message, not an error.
Production Insight
Schema design decisions are the hardest to change after data exists — ALTER TABLE on a 100M row table is an hours-long operation.
Design tables to be specific: VARCHAR(100) for email (not VARCHAR(255)), CHAR(2) for country codes, TINYINT for status flags.
Document the intended domain of each column in a comment: COMMENT 'ISO 4217 currency code' on the currency column.
Key Takeaway
Table design is permanent in practice — schema changes on populated tables are expensive and risky.
Be specific with data types: over-provisioning VARCHAR(255) for a 2-char column misleads future developers.
Document column semantics with COMMENT or a schema documentation tool — the column name alone is rarely sufficient.
thecodeforge.io
SQL CREATE TABLE — FLOAT Breaks $49.99 Summations
Sql Create Table Data Types
SQL Data Types Explained — Choosing the Right One Every Time
Choosing a data type is one of the most consequential decisions you make when designing a table. Get it wrong and you either waste storage, break functionality, or both. Think of data types as choosing the right container in a kitchen: you wouldn't store soup in a paper bag or carry sand in a colander. The container has to match what's going in.
SQL data types fall into four families: Numbers, Text, Dates/Times, and Boolean. Within each family there are options that trade off range versus storage size.
Numbers: Use INT for whole numbers (user IDs, quantities, ages). Use DECIMAL(precision, scale) for money — never use FLOAT for currency because floating-point arithmetic introduces tiny rounding errors that will cost real money. DECIMAL(10, 2) means up to 10 total digits, with exactly 2 after the decimal point — perfect for prices like 1299.99.
Text: VARCHAR(n) is your default for text — it only uses as much space as the actual string, up to n characters. CHAR(n) always uses exactly n characters (padded with spaces) and is faster for fixed-length values like country codes ('US', 'UK'). TEXT holds unlimited text and is ideal for blog posts or descriptions.
Dates: DATE stores only the calendar date. TIME stores only the clock time. DATETIME stores both. TIMESTAMP also stores both but automatically converts to UTC — useful for apps with global users.
Boolean: BOOLEAN (or TINYINT(1) in MySQL) stores TRUE/FALSE — perfect for flags like is_active or is_verified.
create_products_table.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
-- A real e-commerce products table showing deliberate data type choices.-- Read the inline comments to understand WHY each type was chosen.CREATETABLEproducts (
product_id INT, -- Whole number ID — products don't have fractional IDs
product_name VARCHAR(150), -- Names vary in length; VARCHAR saves storage vs CHAR
sku_code CHAR(10), -- SKUs are always exactly 10 chars — CHAR is faster here
description TEXT, -- Product descriptions can be long — no character limit needed
price DECIMAL(10, 2), -- CRITICAL: use DECIMAL for money, never FLOAT-- DECIMAL(10,2) = up to 10 digits, always 2 decimal places-- e.g. 99999999.99 is the max value here
stock_quantity INT, -- You can't have 2.5 items in stock — whole number only
weight_kg DECIMAL(6, 3), -- Weight needs decimals: 1.250 kg is valid
is_available BOOLEAN, -- Simple flag: is the product currently for sale?
listed_on DATE, -- We only care about the calendar date, not the time
last_updated TIMESTAMP-- Auto-tracks date+time; great for audit trails
);
-- Let's also look at what FLOAT looks like to understand why we avoid it for money:-- SELECT 0.1 + 0.2; -- In many systems this returns 0.30000000000000004 — not 0.30!-- DECIMAL avoids this entirely. Always use DECIMAL for financial values.DESCRIBE products;
FLOAT uses binary floating-point math, which can't represent decimal fractions like 0.10 exactly. Run SELECT 0.1 + 0.2 in a FLOAT context and you might get 0.30000000000000004. For any financial column — prices, salaries, balances — always use DECIMAL(precision, scale). Your accountants will thank you.
Production Insight
FLOAT and DOUBLE are binary approximations — never appropriate for monetary values, quantities, or anything requiring exact decimal math.
DECIMAL(10, 2) stores exact decimal values: 10 total digits, 2 after the decimal point, maximum value 99,999,999.99.
For very large monetary values (enterprise billing), use DECIMAL(19, 4) to accommodate trillions with 4 decimal places.
Key Takeaway
DECIMAL for money — always. FLOAT is for scientific measurements where approximation is acceptable.
INT vs BIGINT: INT supports up to ~2.1 billion; BIGINT supports ~9.2 quintillion — use BIGINT for IDs on high-volume tables.
TEXT vs VARCHAR: TEXT has no length limit; VARCHAR enforces a maximum — use VARCHAR where a length limit is a data rule.
Constraints — The Rules That Make Your Data Trustworthy
A table without constraints is just a suggestion. You could insert a user with a NULL email address, a negative price, or two products with the same ID. Constraints are the rules you bolt onto columns (or the whole table) that the database enforces automatically, on every single INSERT and UPDATE, forever.
Here are the six constraints you'll use constantly:
PRIMARY KEY — Uniquely identifies each row. No two rows can share the same value, and it can never be NULL. Every table should have one. Combine with AUTO_INCREMENT (MySQL) or SERIAL (PostgreSQL) so the database generates the ID for you.
NOT NULL — This column must always have a value. NULL is not allowed. Use it on any column that genuinely must exist for every record — like email or username.
UNIQUE — Values in this column must be unique across all rows, but unlike PRIMARY KEY, a UNIQUE column can contain NULLs. Use it for things like email addresses or usernames — no two users should share one.
DEFAULT — If no value is provided during INSERT, use this fallback. Great for created_at timestamps or boolean flags that default to FALSE.
CHECK — Define a custom rule. CHECK (age >= 18) will reject any row where age is below 18. The database enforces it, not your application code.
FOREIGN KEY — Links a column in this table to the PRIMARY KEY of another table, enforcing referential integrity. You can't insert an order for a user_id that doesn't exist in the users table.
create_orders_table_with_constraints.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
-- First, we need a users table to reference from orders.-- Notice how we apply constraints at the column level — clean and readable.CREATETABLEusers (
user_id INT AUTO_INCREMENT PRIMARYKEY, -- AUTO_INCREMENT: DB generates 1, 2, 3...-- PRIMARY KEY: unique + never NULL
username VARCHAR(50) NOTNULLUNIQUE, -- Must exist, must be unique per user
email VARCHAR(100) NOTNULLUNIQUE, -- Every user needs a unique email
age INTCHECK (age >= 13), -- Reject anyone under 13 (e.g. COPPA compliance)
country CHAR(2) DEFAULT 'US', -- If not specified, assume USA
created_at TIMESTAMPDEFAULT CURRENT_TIMESTAMP -- Auto-fills with NOW() on every INSERT
);
-- Now create an orders table that references users.-- A FOREIGN KEY means: every order must belong to a real, existing user.CREATETABLEorders (
order_id INT AUTO_INCREMENT PRIMARYKEY,
user_id INTNOTNULL, -- Cannot be NULL — every order needs an owner
order_total DECIMAL(10,2) NOTNULLCHECK (order_total > 0), -- Must be positive
order_status VARCHAR(20) NOTNULLDEFAULT 'pending', -- Sensible default when order is first created
placed_at TIMESTAMPDEFAULT CURRENT_TIMESTAMP,
-- FOREIGN KEY defined at the bottom — links orders.user_id to users.user_id-- ON DELETE CASCADE means: if the user is deleted, their orders are deleted tooCONSTRAINT fk_orders_user
FOREIGNKEY (user_id) REFERENCESusers (user_id)
ONDELETECASCADE
);
-- Test: try inserting a valid user and orderINSERTINTOusers (username, email, age) VALUES ('maya_dev', 'maya@example.com', 25);
-- user_id = 1 is auto-generated, country = 'US' by default, created_at = nowINSERTINTOorders (user_id, order_total) VALUES (1, 149.99);
-- order_id = 1, status = 'pending' by default, placed_at = now-- Test: try inserting an order for a non-existent userINSERTINTOorders (user_id, order_total) VALUES (9999, 50.00);
-- This WILL FAIL — user_id 9999 doesn't exist in users table
Output
-- After CREATE TABLE users:
Query OK, 0 rows affected (0.06 sec)
-- After CREATE TABLE orders:
Query OK, 0 rows affected (0.04 sec)
-- After INSERT valid user:
Query OK, 1 row affected (0.01 sec)
-- After INSERT valid order:
Query OK, 1 row affected (0.01 sec)
-- After INSERT order with invalid user_id:
ERROR 1452 (23000): Cannot add or update a child row:
Notice we wrote CONSTRAINT fk_orders_user FOREIGN KEY ... instead of just FOREIGN KEY. Naming your constraints means that when one fails, the error message tells you exactly which rule was violated by name — invaluable when debugging. Unnamed constraints get auto-generated names like fk_8f3a2c that tell you nothing.
Production Insight
Foreign key constraints prevent orphaned records but add write overhead — every INSERT into the child table checks the parent.
CHECK constraints enforce domain rules at the database level: CHECK (price > 0) prevents negative prices regardless of which application writes to the table.
NOT NULL is the most commonly missing constraint — add it to every column that should never be empty.
Key Takeaway
Constraints are the database's immune system — they prevent bad data regardless of which application writes it.
NOT NULL, UNIQUE, CHECK, and FOREIGN KEY should be added by default and removed only with justification.
Applications get rewritten; database constraints outlive them — encode business rules in the schema.
Defensive Table Creation — IF NOT EXISTS and Real-World Patterns
In production, you rarely run CREATE TABLE just once. You run migration scripts, set up test environments, redeploy applications, and onboard new developers. If you run a CREATE TABLE statement on a database where that table already exists, you get an error that stops your entire script. That's why every professional CREATE TABLE statement includes IF NOT EXISTS.
IF NOT EXISTS tells the database: 'Create this table only if it doesn't already exist. If it does exist, skip this statement silently and move on.' This makes your setup scripts idempotent — you can run them ten times in a row and they'll only ever create the table once, without errors.
The other real-world pattern worth knowing is CREATE TABLE ... LIKE and CREATE TABLE ... AS SELECT. LIKE copies the structure of an existing table (columns, types, constraints) without any data — perfect for making a test copy. AS SELECT creates a new table and fills it with the results of a query — useful for snapshots or archiving.
Finally, always use DROP TABLE IF EXISTS before CREATE TABLE in setup scripts where you want a clean slate. The order matters: drop first, then create.
safe_table_creation_patterns.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
-- PATTERN 1: IF NOT EXISTS — the safe default for any script you'll run more than onceCREATETABLEIFNOTEXISTSblog_posts (
post_id INT AUTO_INCREMENT PRIMARYKEY,
author_id INTNOTNULL,
title VARCHAR(200) NOTNULL,
slug VARCHAR(200) NOTNULLUNIQUE, -- URL-friendly version of title, must be unique
body TEXTNOTNULL,
is_published BOOLEANNOTNULLDEFAULTFALSE, -- Drafts are unpublished by default
published_at DATETIMENULL, -- NULL until the post is actually published
created_at TIMESTAMPDEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPDEFAULT CURRENT_TIMESTAMP ONUPDATE CURRENT_TIMESTAMP,
-- ON UPDATE: automatically refreshes this column whenever the row changesCONSTRAINT fk_post_author
FOREIGNKEY (author_id) REFERENCESusers (user_id)
ONDELETERESTRICT-- RESTRICT: prevent deleting a user who has blog posts
);
-- PATTERN 2: Clean-slate setup for development or testing-- Drop if it exists, then create fresh. Use only in dev — never in production!DROPTABLEIFEXISTS blog_posts; -- Silently skips if table doesn't existCREATETABLEblog_posts (
post_id INT AUTO_INCREMENT PRIMARYKEY,
title VARCHAR(200) NOTNULL-- ... rest of columns
);
-- PATTERN 3: Copy a table's structure for testing (no data copied)CREATETABLE blog_posts_test LIKE blog_posts;
-- blog_posts_test now has identical columns and constraints but zero rows-- PATTERN 4: Create a table from a query result (useful for reporting snapshots)CREATETABLE published_posts_snapshot ASSELECT post_id, title, published_at
FROM blog_posts
WHERE is_published = TRUE;
-- Note: AS SELECT does NOT copy constraints like PRIMARY KEY — just structure + data
Output
-- IF NOT EXISTS on a brand new table:
Query OK, 0 rows affected (0.05 sec)
-- IF NOT EXISTS run a second time (table already exists):
Query OK, 0 rows affected, 1 warning (0.00 sec)
-- Warning: Table 'mydb.blog_posts' already exists — but the script keeps running!
-- DROP TABLE IF EXISTS (table exists, drops it):
Query OK, 0 rows affected (0.02 sec)
-- CREATE TABLE blog_posts (fresh):
Query OK, 0 rows affected (0.04 sec)
-- CREATE TABLE blog_posts_test LIKE blog_posts:
Query OK, 0 rows affected (0.03 sec)
-- CREATE TABLE published_posts_snapshot AS SELECT ...:
Query OK, 0 rows affected (0.06 sec)
-- (0 rows because no published posts exist yet in this example)
Interview Gold: ON DELETE CASCADE vs ON DELETE RESTRICT
ON DELETE CASCADE means 'if the parent row is deleted, automatically delete all child rows too' — useful for user data cleanup. ON DELETE RESTRICT means 'refuse to delete the parent if child rows still reference it' — safer for financial records. ON DELETE SET NULL sets the foreign key to NULL instead of deleting. Interviewers love asking which you'd use for orders vs. user preferences.
Production Insight
CREATE TABLE IF NOT EXISTS is mandatory in migration scripts — running migrations twice (common in blue-green deployments) throws errors without it.
Always include a corresponding DROP TABLE IF EXISTS in rollback scripts paired with your CREATE TABLE.
For zero-downtime schema changes on live tables, add columns with DEFAULT and NOT NULL in two steps: add nullable first, backfill, then add NOT NULL constraint.
Key Takeaway
CREATE TABLE IF NOT EXISTS makes migration scripts idempotent — safe to run multiple times without errors.
For live table changes, add nullable columns first, backfill data, then add the NOT NULL constraint.
Version-control your schema like code — every CREATE and ALTER has a rollback pair in version history.
Create Table from Existing Table — Copy Structure Without the Pain
You don't always start from scratch. Sometimes you need a staging table for ETL, a backup of a production schema, or a quick sandbox. SQL lets you clone a table's structure (and optionally its data) with a single statement. But here's the trap: CREATE TABLE new_table AS SELECT * FROM old_table copies the data but often drops your indexes, constraints, and default values. You'll get a bare-bones heap of rows with no referential integrity. That's fine for temp work. Fatal if you think it's a real copy.
Use CREATE TABLE ... LIKE when you want the exact schema — keys, defaults, and all — without the rows. Or use CREATE TABLE ... AS SELECT with a WHERE 1=0 clause to get an empty structural clone. Know which one you need before you type. Production outages start with assumptions that a cloned table behaves like the original.
CloneCustomerTable.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// io.thecodeforge — database tutorial
-- Clone structure only (indexes, defaults, constraints included)CREATETABLE staging_customer LIKE customer;
-- Clone structure with a subset of rows (no constraints copied)CREATETABLE backup_customer ASSELECT * FROM customer
WHERE signup_date >= '2024-01-01';
-- Clone structure only, empty, using AS SELECT trickCREATETABLE empty_customer ASSELECT * FROM customer
WHERE1=0;
Output
Query OK, 0 rows affected (0.02 sec)
Query OK, 5842 rows affected (0.15 sec)
Query OK, 0 rows affected (0.01 sec)
Production Trap:
CREATE TABLE ... AS SELECT discards primary keys, foreign keys, and auto-increment. If you need full constraints, use LIKE or script the DDL from SHOW CREATE TABLE.
Key Takeaway
Clone structure with LIKE for production accuracy. Use AS SELECT only for throwaway staging or bulk data snapshots.
Inserting Data into the Table — You Built It, Now Fill It Right
You wrote the CREATE TABLE. The schema is tight. Now shove data into it. But don't just throw raw INSERT statements at production. You need patterns that handle volume, prevent duplicates, and don't lock the whole table for hours.
First rule: always list the columns explicitly. INSERT INTO customer (id, name, email) VALUES (1, 'Alice', 'a@b.com') is safe. INSERT INTO customer VALUES (1, 'Alice', 'a@b.com') breaks the moment you add a column. I've seen this take down a deployment. Second rule: batch your inserts. A single INSERT with multiple rows is faster than a loop of single-row inserts. Third rule: use INSERT ... ON CONFLICT (PostgreSQL) or INSERT ... ON DUPLICATE KEY UPDATE (MySQL) for upserts. It saves you a separate SELECT check and avoids race conditions.
Never use string concatenation to build INSERT values. That's how you get SQL injection. Use parameterised queries or prepared statements in your app layer.
For loading thousands of rows, use COPY (PostgreSQL) or LOAD DATA INFILE (MySQL). They're an order of magnitude faster than INSERT loops.
Key Takeaway
Always specify columns. Batch inserts. Use upserts for idempotent loads. Never trust raw string interpolation.
Using the Command Line Client — No GUI Safety Net
If you only ever design tables through a GUI tool like DBeaver or MySQL Workbench, you're flying blind. The command line client is where you prove you actually understand what's happening. It strips away the auto-complete, the schema diagrams, and the pretty colors. You type SQL or you fail.
Here's the reality: production databases don't have a GUI. When you're on a server at 2AM debugging a locked table, you get a terminal and a blinking cursor. The command line client teaches you to think in statements, not clicks. It forces you to validate your syntax mentally before hitting enter. And it makes you read error messages — really read them — because there's no friendly popup.
Start using mysql -u root -p (or psql -U postgres) and write your CREATE TABLE statements by hand. It's faster once you're fluent, and it makes you a better engineer. No training wheels.
CreateTableCli.sqlSQL
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// io.thecodeforge — database tutorial
-- Connect to MySQL from terminal
mysql -u root -p
-- Output: Enter password:-- Output: Welcome to the MySQL monitor...USE shop_inventory;
-- Output: Database changedCREATETABLEIFNOTEXISTSwarehouse (
id INT AUTO_INCREMENT PRIMARYKEY,
location VARCHAR(100) NOTNULL,
capacity INTDEFAULT0,
manager_id INTUNIQUE
);
-- Output: Query OK, 0 rows affected (0.02 sec)SHOWTABLES;
-- Output: +-------------------------+-- Output: | Tables_in_shop_inventory |-- Output: +-------------------------+-- Output: | warehouse |-- Output: +-------------------------+DESC warehouse;
-- Output: +------------+--------------+------+-----+---------+----------------+-- Output: | Field | Type | Null | Key | Default | Extra |-- Output: +------------+--------------+------+-----+---------+----------------+-- Output: | id | int | NO | PRI | NULL | auto_increment |-- Output: | location | varchar(100) | NO | | NULL | |-- Output: | capacity | int | YES | | 0 | |-- Output: | manager_id | int | YES | UNI | NULL | |-- Output: +------------+--------------+------+-----+---------+----------------+
Pipe your CREATE TABLE statements from a .sql file directly into the client: mysql -u root -p shop_inventory < create_warehouse.sql. No manual typing, no copy-paste errors. Run it like a script.
Key Takeaway
Master the command line client. It's the only tool that works every time, everywhere — including production.
Using the Command Line Client — Row-by-Row Inserts That Don't Suck
After you CREATE TABLE from the CLI, the natural next step is populating it. And the worst thing you can do is type INSERT statements blindly. The command line client gives you immediate feedback — syntax errors, constraint violations, duplicate keys. You see them instantly, not buried in a GUI log.
Run your INSERTs line by line. It feels slow, but it's deliberate. You watch the 'Query OK, 1 row affected' and you know exactly what went in. When you hit a duplicate key error, you fix it right there. When you miss a NOT NULL column, the error is crystal clear. This is how you build muscle memory for your schema.
Batch inserts work too:INSERT INTO warehouse (location, capacity) VALUES ('NYC', 500), ('LA', 300), ('CHI', 400);. One statement, three rows, one response. But don't batch until you've validated the structure with single rows first. Trust me.
ERROR 1364 (HY000): Field 'location' doesn't have a default value
Query OK, 3 rows affected (0.01 sec)
Records: 3 Duplicates: 0 Warnings: 0
+----+----------+----------+
| id | location | capacity |
+----+----------+----------+
| 1 | NYC | 500 |
| 2 | LA | 300 |
| 3 | CHI | 400 |
| 4 | ATL | 250 |
+----+----------+----------+
Production Trap:
Never run INSERTs inside a transaction block in the CLI unless you COMMIT explicitly. A forgotten ROLLBACK kills your data. Always verify with SELECT before moving on.
Key Takeaway
Validate your INSERTs row by row from the CLI first. Batching is for speed only after structure is proven correct.
● Production incidentPOST-MORTEMseverity: high
Financial Reporting Errors from FLOAT on a Payments Table
Symptom
Monthly reconciliation reports showed discrepancies of $0.01-$0.03 on roughly 15% of transactions. Finance signed off each month but raised it as a persistent issue.
Assumption
The developer used FLOAT because 'it handles decimals.' DECIMAL seemed unnecessarily strict for a column that would always store simple monetary values.
Root cause
FLOAT stores values as binary floating-point approximations. $49.99 stored as FLOAT might round to $49.98999862670898 internally. Summations over thousands of transactions accumulated these rounding errors into visible discrepancies.
Fix
ALTER TABLE payments ALTER COLUMN price TYPE DECIMAL(10, 2). Recomputed all affected monthly totals. Added a database lint rule checking that any column with 'price', 'amount', 'cost', or 'revenue' in the name must be DECIMAL or NUMERIC.
Key lesson
Never use FLOAT or DOUBLE for monetary values — use DECIMAL(precision, scale) always
DECIMAL(10, 2) means up to 10 total digits with exactly 2 after the decimal — the exact storage type for money
Add a naming convention check in CI: price/amount/cost columns must be DECIMAL type
Production debug guideDiagnosing schema design mistakes before data is inserted3 entries
Symptom · 01
Monetary calculations show penny-level rounding errors
→
Fix
Check the column type: SHOW COLUMNS FROM table_name (MySQL) or \d table_name (PostgreSQL). If it shows float or double, alter to DECIMAL(10,2). All existing FLOAT values need recalculation.
Symptom · 02
INSERT fails with constraint violation on a supposedly optional column
→
Fix
Check for NOT NULL without a DEFAULT value. A column marked NOT NULL requires either a value in every INSERT or a DEFAULT clause: column_name VARCHAR(100) NOT NULL DEFAULT ''. Verify with DESCRIBE table_name.
Symptom · 03
Duplicate rows being inserted despite UNIQUE constraint
→
Fix
Check if the column allows NULL — UNIQUE constraints treat each NULL as distinct (multiple NULL values are allowed). To prevent duplicate NULLs, add NOT NULL to the column in addition to UNIQUE.
Data Type
Best Used For
Storage
Example Value
Watch Out For
INT
IDs, counts, ages, quantities
4 bytes
42
Can't store decimals — 3/2 becomes 1
DECIMAL(p,s)
Prices, salaries, measurements
Variable, exact
1299.99
Always specify precision and scale explicitly
FLOAT / DOUBLE
Scientific data, approximations
4–8 bytes
3.14159265
Never use for money — rounding errors guaranteed
VARCHAR(n)
Names, emails, URLs, short text
Actual length + 1-2 bytes
'john@example.com'
n is a max cap, not a fixed size — set it generously
CHAR(n)
Fixed-length codes (country, SKU)
Always n bytes
'US'
Pads shorter strings with spaces — can cause subtle bugs in comparisons
TEXT
Blog posts, bios, descriptions
Up to 65,535 bytes
'Once upon a time...'
Cannot have a DEFAULT value; can't be fully indexed
DATE
Birthdays, event dates
3 bytes
'2024-03-15'
No time component — use DATETIME if time matters
DATETIME
Timestamps, scheduled events
8 bytes
'2024-03-15 14:30:00'
Stored as-is — no timezone awareness
TIMESTAMP
Created/updated audit columns
4 bytes
'2024-03-15 14:30:00'
Auto-converts to UTC; range is 1970–2038
BOOLEAN
Flags, toggles, yes/no fields
1 byte
TRUE / FALSE
MySQL stores as TINYINT(1) — 0 is false, anything else is true
Key takeaways
1
CREATE TABLE is a blueprint, not data
it defines structure and rules before a single row exists; the database enforces those rules on every INSERT and UPDATE automatically, forever.
2
Always use DECIMAL(precision, scale) for money columns
FLOAT introduces binary rounding errors that compound into real financial discrepancies at scale.
3
Every production table needs a PRIMARY KEY with AUTO_INCREMENT
without it you have no reliable way to identify, update, or delete individual rows, and duplicates become invisible.
4
Add IF NOT EXISTS to every CREATE TABLE in scripts you'll run more than once
it makes your setup scripts idempotent and prevents them from crashing on re-runs in CI/CD pipelines or fresh dev environments.
Common mistakes to avoid
3 patterns
×
Using FLOAT or DOUBLE for monetary values
Symptom
Rounding errors accumulate in financial calculations — SUM of 1000 FLOAT prices may differ by $0.01-$0.10 from the true total. The discrepancy grows with data volume.
Fix
Use DECIMAL(10, 2) for all monetary columns. DECIMAL stores exact decimal values; FLOAT is a binary approximation that cannot represent 0.1 exactly. Existing FLOAT columns must be migrated: ALTER TABLE payments ALTER COLUMN amount TYPE DECIMAL(10, 4).
×
Forgetting PRIMARY KEY entirely
Symptom
No error at insert time, but JOINs become full table scans, UPDATE and DELETE require full scans to find rows, and duplicate rows accumulate silently
Fix
Every table needs a PRIMARY KEY. Add an auto-incrementing surrogate key if no natural key exists: id SERIAL PRIMARY KEY (PostgreSQL) or id INT AUTO_INCREMENT PRIMARY KEY (MySQL). The primary key is the only guaranteed way to uniquely identify and update a specific row.
×
Using FLOAT or DOUBLE for monetary values
Symptom
Penny-level discrepancies in financial reports that grow over time as more transactions accumulate
Fix
DECIMAL(10, 2) for standard monetary values. FLOAT is a binary approximation — 0.1 + 0.2 in binary floating point is not exactly 0.3. The difference is tiny per row but accumulates to significant errors at scale.
INTERVIEW PREP · PRACTICE MODE
Interview Questions on This Topic
Q01JUNIOR
What is the difference between CHAR and VARCHAR? When would you delibera...
Q02JUNIOR
Explain the difference between PRIMARY KEY and UNIQUE constraint. Can a ...
Q03SENIOR
What is the difference between ON DELETE CASCADE and ON DELETE RESTRICT ...
Q01 of 03JUNIOR
What is the difference between CHAR and VARCHAR? When would you deliberately choose CHAR?
ANSWER
CHAR(n) is fixed-length — it always stores exactly n characters, padding shorter values with spaces. VARCHAR(n) is variable-length — it stores only as many characters as the value contains plus 1-2 bytes for the length. CHAR is appropriate for columns where every value is the same length: ISO country codes (exactly 2 characters), status codes, CHAR(36) UUIDs. CHAR has a slight performance advantage for fixed-length data because the storage engine doesn't need to read a length prefix to know where the value ends. For everything else — names, emails, descriptions — VARCHAR is correct.
Q02 of 03JUNIOR
Explain the difference between PRIMARY KEY and UNIQUE constraint. Can a table have multiple UNIQUE constraints?
ANSWER
A PRIMARY KEY is a special UNIQUE constraint that additionally cannot be NULL. A table can have only one PRIMARY KEY but can have multiple UNIQUE constraints. UNIQUE allows NULL values (and treats each NULL as distinct — multiple NULLs are allowed unless you also add NOT NULL). PRIMARY KEY implies UNIQUE + NOT NULL + the table's main identifier. Use PRIMARY KEY for the table's identity column (id, order_id); use UNIQUE for business uniqueness rules (email must be unique, username must be unique) where NULL should also be disallowed.
Q03 of 03SENIOR
What is the difference between ON DELETE CASCADE and ON DELETE RESTRICT on a FOREIGN KEY?
ANSWER
ON DELETE CASCADE: when a parent row is deleted, all child rows referencing it are automatically deleted too. ON DELETE RESTRICT (or ON DELETE NO ACTION): the parent row cannot be deleted if any child rows reference it — the delete is blocked with an error. ON DELETE SET NULL: child rows have their foreign key column set to NULL when the parent is deleted. The right choice depends on your data model: CASCADE is appropriate for dependent entities that have no meaning without the parent (order_items deleted when an order is deleted). RESTRICT is appropriate for reference data you don't want to accidentally cascade-delete (deleting a product category should fail if products still reference it).
01
What is the difference between CHAR and VARCHAR? When would you deliberately choose CHAR?
JUNIOR
02
Explain the difference between PRIMARY KEY and UNIQUE constraint. Can a table have multiple UNIQUE constraints?
JUNIOR
03
What is the difference between ON DELETE CASCADE and ON DELETE RESTRICT on a FOREIGN KEY?
SENIOR
FAQ · 3 QUESTIONS
Frequently Asked Questions
01
What is the difference between VARCHAR and TEXT in SQL?
VARCHAR(n) stores variable-length text up to a defined maximum (n) and can be fully indexed and have default values — it's your go-to for most text columns. TEXT stores unlimited text (up to 65,535 bytes in MySQL) but cannot have a DEFAULT value and can only be partially indexed. Use VARCHAR for names, emails, and titles; use TEXT for blog posts, product descriptions, and anything open-ended in length.
Was this helpful?
02
Can I change a table's structure after I've already created it?
Yes — use the ALTER TABLE statement. ALTER TABLE users ADD COLUMN phone VARCHAR(20) adds a new column. ALTER TABLE users MODIFY COLUMN age SMALLINT changes a column's type. ALTER TABLE users DROP COLUMN age removes a column entirely. Be careful in production though: changing a column type or dropping a column on a large, live table can lock the table for minutes and cause downtime.
Was this helpful?
03
What does NULL actually mean in SQL, and is it the same as zero or an empty string?
NULL means 'no value exists' — it represents the absence of data, not zero and not an empty string. Zero is a valid integer value. An empty string '' is a valid text value. NULL is neither. This matters because NULL behaves differently in comparisons: you can't use = NULL to check for null, you must use IS NULL. A column with NOT NULL means a value is mandatory; NULL means the value is optional and might not be known.