Mid-level 5 min · March 05, 2026

SQL CREATE TABLE — FLOAT Breaks $49.99 Summations

Monthly reports had $0.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • 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.

CREATE TABLE users (
    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
Output
Query OK, 0 rows affected (0.04 sec)
+-------------+--------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-------------+--------------+------+-----+---------+-------+
| user_id | int | YES | | NULL | |
| username | varchar(50) | YES | | NULL | |
| email | varchar(100) | YES | | NULL | |
| age | int | YES | | NULL | |
| profile_bio | text | YES | | NULL | |
| created_at | datetime | YES | | NULL | |
| is_verified | tinyint(1) | YES | | NULL | |
+-------------+--------------+------+-----+---------+-------+
7 rows in set (0.00 sec)
Why 'Query OK, 0 rows affected'?
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.

CREATE TABLE products (
    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;
Output
Query OK, 0 rows affected (0.05 sec)
+----------------+---------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+----------------+---------------+------+-----+---------+-------+
| product_id | int | YES | | NULL | |
| product_name | varchar(150) | YES | | NULL | |
| sku_code | char(10) | YES | | NULL | |
| description | text | YES | | NULL | |
| price | decimal(10,2) | YES | | NULL | |
| stock_quantity | int | YES | | NULL | |
| weight_kg | decimal(6,3) | YES | | NULL | |
| is_available | tinyint(1) | YES | | NULL | |
| listed_on | date | YES | | NULL | |
| last_updated | timestamp | YES | | NULL | |
+----------------+---------------+------+-----+---------+-------+
10 rows in set (0.00 sec)
Watch Out: Never Store Money in a FLOAT Column
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.

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.

CREATE TABLE users (
    user_id     INT           AUTO_INCREMENT PRIMARY KEY,  -- AUTO_INCREMENT: DB generates 1, 2, 3...
                                                           -- PRIMARY KEY: unique + never NULL
    username    VARCHAR(50)   NOT NULL UNIQUE,             -- Must exist, must be unique per user
    email       VARCHAR(100)  NOT NULL UNIQUE,             -- Every user needs a unique email
    age         INT           CHECK (age >= 13),           -- Reject anyone under 13 (e.g. COPPA compliance)
    country     CHAR(2)       DEFAULT 'US',                -- If not specified, assume USA
    created_at  TIMESTAMP     DEFAULT 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.

CREATE TABLE orders (
    order_id      INT           AUTO_INCREMENT PRIMARY KEY,
    user_id       INT           NOT NULL,                   -- Cannot be NULL — every order needs an owner
    order_total   DECIMAL(10,2) NOT NULL CHECK (order_total > 0),  -- Must be positive
    order_status  VARCHAR(20)   NOT NULL DEFAULT 'pending', -- Sensible default when order is first created
    placed_at     TIMESTAMP     DEFAULT 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 too
    CONSTRAINT fk_orders_user
        FOREIGN KEY (user_id) REFERENCES users (user_id)
        ON DELETE CASCADE
);

-- Test: try inserting a valid user and order
INSERT INTO users (username, email, age) VALUES ('maya_dev', 'maya@example.com', 25);
-- user_id = 1 is auto-generated, country = 'US' by default, created_at = now

INSERT INTO orders (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 user
INSERT INTO orders (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:
a foreign key constraint fails (`mydb`.`orders`,
CONSTRAINT `fk_orders_user` FOREIGN KEY (`user_id`)
REFERENCES `users` (`user_id`) ON DELETE CASCADE)
Pro Tip: Name Your Constraints
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 once
CREATE TABLE IF NOT EXISTS blog_posts (
    post_id       INT           AUTO_INCREMENT PRIMARY KEY,
    author_id     INT           NOT NULL,
    title         VARCHAR(200)  NOT NULL,
    slug          VARCHAR(200)  NOT NULL UNIQUE,   -- URL-friendly version of title, must be unique
    body          TEXT          NOT NULL,
    is_published  BOOLEAN       NOT NULL DEFAULT FALSE,  -- Drafts are unpublished by default
    published_at  DATETIME      NULL,                    -- NULL until the post is actually published
    created_at    TIMESTAMP     DEFAULT CURRENT_TIMESTAMP,
    updated_at    TIMESTAMP     DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
                                -- ON UPDATE: automatically refreshes this column whenever the row changes

    CONSTRAINT fk_post_author
        FOREIGN KEY (author_id) REFERENCES users (user_id)
        ON DELETE RESTRICT  -- 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!
DROP TABLE IF EXISTS blog_posts;  -- Silently skips if table doesn't exist
CREATE TABLE blog_posts (
    post_id  INT AUTO_INCREMENT PRIMARY KEY,
    title    VARCHAR(200) NOT NULL
    -- ... rest of columns
);

-- PATTERN 3: Copy a table's structure for testing (no data copied)
CREATE TABLE 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)
CREATE TABLE published_posts_snapshot AS
    SELECT 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 TypeBest Used ForStorageExample ValueWatch Out For
INTIDs, counts, ages, quantities4 bytes42Can't store decimals — 3/2 becomes 1
DECIMAL(p,s)Prices, salaries, measurementsVariable, exact1299.99Always specify precision and scale explicitly
FLOAT / DOUBLEScientific data, approximations4–8 bytes3.14159265Never use for money — rounding errors guaranteed
VARCHAR(n)Names, emails, URLs, short textActual 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
TEXTBlog posts, bios, descriptionsUp to 65,535 bytes'Once upon a time...'Cannot have a DEFAULT value; can't be fully indexed
DATEBirthdays, event dates3 bytes'2024-03-15'No time component — use DATETIME if time matters
DATETIMETimestamps, scheduled events8 bytes'2024-03-15 14:30:00'Stored as-is — no timezone awareness
TIMESTAMPCreated/updated audit columns4 bytes'2024-03-15 14:30:00'Auto-converts to UTC; range is 1970–2038
BOOLEANFlags, toggles, yes/no fields1 byteTRUE / FALSEMySQL 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.
FAQ · 3 QUESTIONS

Frequently Asked Questions

01
What is the difference between VARCHAR and TEXT in SQL?
02
Can I change a table's structure after I've already created it?
03
What does NULL actually mean in SQL, and is it the same as zero or an empty string?
🔥

That's SQL Basics. Mark it forged?

5 min read · try the examples if you haven't

Previous
Introduction to SQL
2 / 16 · SQL Basics
Next
SQL INSERT UPDATE DELETE