Core CS · DBMS
Your first constraint is the column type
One CREATE TABLE and three student rows, read column by column. You will choose between CHAR and VARCHAR, learn why money is never FLOAT, change the table with ALTER, and then run DELETE, TRUNCATE and DROP against the same three rows to see what each one actually takes away.
Empty the same table three ways →01 The idea
SQL is four sublanguages wearing one name
People say “I know SQL” and mean SELECT. SQL is a family of statement kinds, split by what the statement acts on, and the split is not academic tidiness. It predicts the two things interviewers push on: whether a statement can be undone, and whether the database writes a log record for every row it touches.
DDL defines structure: CREATE, ALTER, DROP, TRUNCATE. DML changes rows: INSERT, UPDATE, DELETE. DCL hands out permission: GRANT, REVOKE. TCL draws the boundary a group of changes lives inside: COMMIT, ROLLBACK, SAVEPOINT. That is the whole map, and TRUNCATE sitting under DDL rather than DML is the single classification that answers the most asked question in this lesson.
This lesson lives almost entirely in DDL, and specifically in the part of DDL that students skim: the type beside each column name. VARCHAR(60) is a rule that rejects the sixty-first character. NUMERIC(9,2) is a rule that refuses to lose the paise. NOT NULL is a rule that refuses a blank. None of them is advice to the storage engine. Each one is a check that runs before a row is allowed to exist, and it runs for the web app, the nightly import, and the UPDATE somebody types by hand at 2am.
02 Worked example
One student table, and what happens to one insert
Here is the table the rest of this lesson uses. Read it as nine decisions, not nine lines. Every type was chosen against one question: what values must this column accept, and what must it refuse?
CREATE TABLE student ( sid INTEGER GENERATED BY DEFAULT AS IDENTITY, -- counter lives outside the rows roll_no CHAR(6) NOT NULL, -- every roll number is 6 characters name VARCHAR(60) NOT NULL, -- up to 60; stores only what you put in city VARCHAR(40), -- nullable: "not recorded" is a real state fee_due NUMERIC(9,2) NOT NULL DEFAULT 0, -- money: exact, never FLOAT joined_on DATE NOT NULL, -- a calendar day, no time of day left_on DATE, -- still enrolled = no value yet hosteller BOOLEAN NOT NULL DEFAULT FALSE, photo BLOB, -- large object, kept away from the row CONSTRAINT student_pk PRIMARY KEY (sid), CONSTRAINT student_roll_uq UNIQUE (roll_no), CONSTRAINT student_fee_ck CHECK (fee_due >= 0), CONSTRAINT student_dates_ck CHECK (left_on IS NULL OR left_on >= joined_on));
Lines 2 to 10 declare the columns, and every rule written beside a column there is a column constraint: it sees that one column and nothing else. Lines 11 to 14 are table constraints: they are written after the columns because they are allowed to mention more than one of them. Line 14 is the reason that distinction exists. “You cannot leave before you joined” needs left_on and joined_on at the same time, so it cannot be written beside either column. Everything else on lines 11 to 13 could have been a column constraint; naming them explicitly is a choice you will be glad of in section 03.
Now follow one insert through. A clerk types a roll number and drops a zero, so '21CS1' arrives where '21CS01' was meant, five characters into a CHAR(6) column.
INSERT INTO student (roll_no, name, city, joined_on, hosteller)VALUES ('21CS1', 'Asha', 'Bengaluru', DATE '2024-07-15', TRUE);
Node 2 is the one to remember. CHAR(6) did not reject the short value and it did not warn about it. It stored '21CS1 ', six characters, one of them a space nobody typed. Node 4 is the second: a table constraint reads a row, a column constraint reads a value, and no amount of care in the column list can make a two-column rule work as a one-column rule.
Fix that row and add two more. These three rows are the whole dataset for the rest of the lesson. Note the identity counter: three inserts have been taken, so the next sid handed out is 4.
| sid | roll_no | name | city | fee_due | joined_on | left_on |
|---|---|---|---|---|---|---|
| 1 | 21CS01 | Asha | Bengaluru | 12500.00 | 2024-07-15 | NULL |
| 2 | 21CS02 | Ravi | Hyderabad | 0.00 | 2024-07-15 | NULL |
| 3 | 21CS03 | Meera | Pune | 7250.50 | 2024-08-01 | NULL |
Three facts about this table you can check by hand, and each one is a question somebody has been asked in an interview. SELECT COUNT(*) FROM student is 3. SELECT COUNT(left_on) FROM student is 0, because COUNT of a column counts values and there are none. SELECT SUM(fee_due) FROM student is 19750.50, exactly, because the column is NUMERIC and not FLOAT. Section 03 is those nine type decisions written as a table you can answer from.
03 Mechanics
Nine type families, and the trap in each
One row per family. The third column is the one to learn, because “what it holds” is in every textbook and “when to declare it” is what a schema review actually argues about. The fourth column is what gets you caught.
| Type | What it holds | Declare it when | The trap |
|---|---|---|---|
CHAR(n) |
Exactly n characters. A shorter value is padded with spaces to reach n. | Every value genuinely has n characters: a 6-character roll number, a 2-letter country code, a single-character flag. | It pads instead of rejecting. Standard SQL pads the shorter side with spaces before comparing two character strings, so = ignores trailing blanks; LIKE is defined without that padding and does not. PostgreSQL is the exception on the LIKE side, because character(n) is converted to text and loses the padding first; Oracle and SQL Server keep it. |
VARCHAR(n) |
Up to n characters. Stores what you gave it, plus a small length header. | Almost every string: names, cities, addresses, email. This is the default answer. | n is a constraint, not a reservation, so being generous costs nothing; but a value over n is rejected, not trimmed. MySQL outside strict mode truncates it with a warning instead, which is the worse of the two failures. |
SMALLINT |
Whole numbers in 2, 4 and 8 bytes. Roughly ±32 thousand, ±2.1 billion, ±9.2 quintillion. | Counters, surrogate keys, quantities, anything you will do arithmetic on and never see a fraction of. | INTEGER stops at 2,147,483,647. A key column on a table that will pass two billion rows needs BIGINT from the start, because widening it later rewrites every row and every index on it. |
NUMERIC(p,s) |
Exact decimal: p significant digits, s of them after the point. Every engine here treats the two spellings as one type; the standard only lets DECIMAL give you more precision than you asked for. | Money, tax, marks, quantities, anything a human will audit or add up and expect to match a printed total. | Exact arithmetic is slower than integer or float arithmetic. And p and s are two separate limits: more decimals than s are rounded away without a word, while too many digits in front of the point are rejected outright. |
REAL |
Binary floating point. Approximate, wide range, fast. | Measurements from an instrument, scientific values, coordinates, anything where the last bit was never meaningful. | 0.1 has no exact binary representation. Add 0.10 to a DOUBLE PRECISION 12500.00 ten times and you get 12501.000000000004. Never money, never anything compared with =. |
DATE |
A calendar day. Year, month, day, no time of day. | joined_on, date of birth, invoice date: things that happen on a day rather than at an instant. | Compared against a timestamp it is promoted to midnight, so joined_on = TIMESTAMP '2024-07-15 09:30:00' is false. Oracle's DATE is the exception that catches everybody: it carries a time down to the second. |
TIME |
A time of day; a day plus a time; and the same with a zone offset recorded. | Event times, audit columns, anything you will subtract to get a duration. Use the zoned form for anything a second server or a second country will read. | Plain TIMESTAMP is a wall-clock reading with no location attached. Two servers in two zones write the same instant as two different values, and nothing in the column says which was which. |
BOOLEAN |
TRUE, FALSE, or NULL. Three states, not two. | Flags: hosteller, is_active, fee_waived. Add NOT NULL DEFAULT unless “unknown” is a state you mean. | WHERE NOT hosteller silently skips every row where hosteller is NULL, because NOT unknown is unknown. Oracle has no BOOLEAN column type before 23c, so the convention there is CHAR(1) with CHECK (flag IN ('Y','N')); MySQL's BOOLEAN is an alias for TINYINT(1) and will store 7. |
BLOB |
Large binary and large character objects, held away from the row itself. | A scanned marksheet, a PDF, a long free-text abstract. PostgreSQL spells these BYTEA and TEXT; SQL Server uses VARBINARY(MAX) and VARCHAR(MAX). | You cannot usefully index one or compare two, and SELECT * drags every megabyte across the network. Most teams store a key or path in the table and keep the bytes in object storage. |
The other half of DDL is changing a table that already has rows in it. ALTER TABLE adds, retypes and drops both columns and constraints, and every one of those statements is checked against the three rows already there.
ALTER TABLE student ADD COLUMN email VARCHAR(120);ALTER TABLE student ALTER COLUMN email SET NOT NULL; -- fails: 3 rows hold NULLUPDATE student SET email = roll_no || '@college.edu'; -- backfill all 3ALTER TABLE student ALTER COLUMN email SET NOT NULL; -- now it passes ALTER TABLE student ALTER COLUMN city SET DATA TYPE VARCHAR(80); -- widen: nothing to checkALTER TABLE student ALTER COLUMN city SET DATA TYPE VARCHAR(6); -- fails: 'Bengaluru' is 9 ALTER TABLE student ADD CONSTRAINT student_fee_cap CHECK (fee_due <= 200000);ALTER TABLE student DROP CONSTRAINT student_fee_cap; -- by the name you gave itALTER TABLE student DROP COLUMN photo; -- the column and its bytes
Adding a column is cheap. Adding NOT NULL is not. The three existing rows have no email, so line 1 gives all three a NULL and line 2 has nothing to work with. Either give the new column a DEFAULT so the rows get a value, or do it in three statements as above: add, backfill, tighten. Line 2 and line 4 are the same statement with a different table underneath them.
Widening a type is free. Narrowing is a question about your data. Every existing value already satisfies a wider rule, so line 6 has nothing to verify. Line 7 must check all three rows and 'Bengaluru' is nine characters, so the whole statement is refused and the column keeps its old type. Nothing partial happens.
A constraint added later is validated against the rows already there. Otherwise a table would be free to hold data its own schema forbids. That is also why line 9 names the constraint: line 10 needs that name, and an engine-generated one like student_chk_1 is not something you want a migration script to depend on. Vendor spelling differs more here than anywhere else in SQL: MySQL writes MODIFY COLUMN and Oracle writes MODIFY where the standard writes ALTER COLUMN ... SET DATA TYPE.
DROP COLUMN takes the data with it, and line 3 hides a trap. Our three roll numbers are exactly six characters, so the concatenation on line 3 is clean. Had the '21CS1' row from section 02 survived, its email would be '21CS1 @college.edu' in Oracle and SQL Server, with the pad space riding along. One bad character in one column becomes one bad address in another, months later, and nothing failed.
05 Cheat sheet
DELETE, TRUNCATE and DROP on one card
This comparison is asked in almost every DBMS interview, and almost every candidate answers the first two rows and stops. The rows that separate people are the log, the counter and the rollback.
| Question | DELETE | TRUNCATE | DROP |
|---|---|---|---|
| Sublanguage | DML | DDL | DDL |
| What it removes | The rows a WHERE selects, or all of them | Every row, always | rows, columns, constraints, indexes, triggers and grants |
| Takes a WHERE clause | yes | no | no |
| Logged per row | yes, one record per row | no, page deallocations only | no |
| Cost on a million rows | slowest by a wide margin | fast, close to constant | fast, close to constant |
| Row triggers fire | yes | no | no |
| Identity counter | untouched | back to the seed on MySQL and SQL Server; PostgreSQL only with RESTART IDENTITY, Oracle not at all | gone with the table |
| ROLLBACK undoes it | yes, always | PostgreSQL and SQL Server yes; Oracle and MySQL no | same split, and Oracle keeps a recycle bin |
| The table afterwards | Exists, emptier | Exists, empty, fully defined | does not exist; the name no longer resolves |
06 Where & why
The same nine types, spelled four ways
There is no such thing as writing SQL in general. You write it against an engine, and the four below disagree on exactly the points this lesson is about: what a type is called, whether it is enforced, and whether DDL can be undone. Knowing which one you are on is a real skill, and interviewers test it by asking the same question twice with a different product name.
BEGIN; ALTER TABLE ...; ROLLBACK; puts the schema back exactly as it was, which is why migration tools can wrap a whole release in one transaction and abort it cleanly on failure. Its TEXT type has no length limit and costs the same as VARCHAR, so most PostgreSQL schemas use TEXT plus a CHECK when a limit is a genuine rule rather than a habit. SERIAL is the older spelling of an identity column and is still everywhere in existing code.
Every DDL statement commits, so a ROLLBACK after TRUNCATE or DROP does nothing at all. An over-length VARCHAR is an error in the default strict mode and a silent truncation with a warning outside it, which is the most damaging default in this lesson because your data is wrong and nothing said so. BOOLEAN is an alias for TINYINT(1), and AUTO_INCREMENT replaces the standard identity syntax.
VARCHAR2 rather than VARCHAR, NUMBER(p,s) rather than NUMERIC(p,s), and no BOOLEAN column type before 23c, so flags are CHAR(1) with a CHECK. Its DATE carries a time down to the second, which makes it closer to another engine's TIMESTAMP(0) and is the most common cross-engine bug in this lesson. A dropped table lands in the recycle bin, so FLASHBACK TABLE student TO BEFORE DROP is a real recovery route no other engine here offers.
A column declared VARCHAR(6) will hold a 200-character string without complaint, because SQLite uses type affinity rather than enforcement and any column except an INTEGER PRIMARY KEY accepts a value of any type. STRICT tables, from version 3.37, opt back into checking. It is the one engine where testing your length and type limits locally proves nothing whatsoever about production, and it is also the one most students have installed.
07 Interview questions
What they actually ask
The DELETE, TRUNCATE and DROP question comes up so often that answering it well is worth more than any other single fact in this module. Expect it early, expect a follow-up about rollback, and expect to be asked which type you would pick for a named column.
What is the difference between DDL and DML?
Would you declare a six-character roll number as CHAR(6) or VARCHAR(6)?
Why must money never be stored as FLOAT?
What is the difference between DELETE, TRUNCATE and DROP?
Can you roll back a TRUNCATE?
DELETE FROM student and TRUNCATE TABLE student both leave zero rows. You then insert one row. What sid does it get?
What is NULL, and why is city = NULL never true?
What is the difference between a column constraint and a table constraint?
Your ALTER TABLE ... ADD CONSTRAINT failed on a table you had not touched. Why?
DATE, TIME or TIMESTAMP for when a student joined the college?
What do the two numbers in NUMERIC(9,2) mean, and what is the largest value it can hold?
Where does the database keep the definition of your table?
08 Practice problems
Six to work through
Work against the three rows from section 02, and write the type or statement out in full rather than describing it. For every rejection, name the exact rule that rejected it: the type, a column constraint, or a table constraint.