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
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.
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.
● 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.