SQL Basics: Creating Databases, Tables, and Querying
Prerequisite Knowledge
This lecture builds on the following concepts from earlier lectures. If any feel unfamiliar, review the linked notes before proceeding.
Previously Covered in This Subject
- MySQL Workbench: setup and first commands — covered in Lecture 7
- Select and project as retrieval operations — covered in Lecture 7
- The relational model: relations, tuples, and domains — covered in Lecture 3
# SQL Basics: Creating Databases, Tables, and Querying
8.1 The Practice Setup: Tools and the Five Lab Modules
8.1.1 The Tools: MySQL Workbench and Online SQL Editors
This session is a hands-on SQL workshop, and it assumes you have a working environment. Two options exist. First, install MySQL locally on your own machine — the standard choice is MySQL Workbench, the graphical client where you type commands, see the results grid, and run statements with buttons. Second, if you cannot install anything, use an online SQL editor: there are several web portals where you type the same commands and they run on a server. The commands themselves stay identical either way, so an error on one machine is an error on all of them — moving between environments never changes the language, only the furniture around it.
Why a real client matters: typing SQL in a graphical client such as MySQL Workbench is not cosmetic. You see the schema of your tables side by side with the query editor, you watch the results grid fill row by row, and you get error messages with line numbers that point back at the exact statement that failed. Those are the same surfaces you will face in an internship or a job — debug on a real client now, and the transfer is free.
A practical detail of the workbench: there are two execute buttons and they do different things. One runs only the statement you have highlighted. The other runs every statement from the cursor position onward. Knowing which one you pressed explains a lot of "why did my whole script run at once?" confusion, and it is the first thing to check when results look surprising.
Pitfall — the wrong execute button: if a query "runs" and suddenly several unrelated statements have all executed (a table dropped, data inserted twice), you most likely pressed the run everything from the cursor button while the run only the highlighted statement button was intended. Check which button you pressed before suspecting a SQL error. This one habit removes a whole class of "my script did something I did not ask" surprises.
Real-world: knowing how to run, debug, and explain SQL on a real client like MySQL Workbench is a transferable skill; the exact same commands work in PostgreSQL-style systems with minor dialect changes, and online SQL editors are widely used in interviews and hiring screens.
8.1.2 The Five Lab Modules and How to Work Through Them
The course's virtual lab platform hosts five lab modules for this subject. Each lab comes in three parts: a written portion explaining what to do, a presentation (PPT) giving the concepts, and an attached video walking through the work. The videos are unlisted links shared directly with you, and their content was turned into lab sheets — Lab Sheet 5 in particular collects the queries used in this session, so you can practice them later without hunting through chat logs. Some of the download links for Lab Sheets 4 and 5 were broken, so those sheets were recreated and re-uploaded to the lab-sheet folder; if a link fails, the files folder is the fallback location.
How a lab is structured: treat each module as a three-layer sandwich. The written portion tells you what to accomplish, the presentation explains why it works the way it does, and the video shows how it is done against a live server. Watch the video only after reading the sheet — the sheet is your checklist, and the video is the demonstration of that checklist.
The strong recommendation from this session: practice alongside the walkthrough, live, not later. Book a reserved slot on the virtual lab platform so you have a machine waiting, and run every command yourself. Error messages are part of the learning, not a failure. If a command errors for you, take a snapshot of the screen and share it — several students had their errors fixed within minutes by exactly that route. The instructor's standing offer: any error you paste into the chat gets tested live, one by one.
Practice beats watching. The strongest recommendation of the whole session is to run every command yourself while the walkthrough runs — not to copy it later, not to watch it and nod. Errors are expected events in the learning loop, and the instructor's standing offer is that any error pasted into the chat gets tested live, one by one. Reading about SQL and running SQL produce different levels of skill, and the exam only sees the second kind.
Exam note: none of the platform logistics above is examinable. What matters is that you actually practice, because the queries you run here are the queries that appear in the mid-semester examination.
8.2 Creating a Database and Your First Table
8.2.1 The Standard Workflow: Create, Use, Create Table
Building a database follows a fixed three-step order. First, create the database itself. Second, tell the system you want to work inside it with the USE command. Third, create tables inside that database. The creation of tables is where you say what attributes the table has and what data types they are, and for each attribute whether it may be null or must not be null. Everything in this session is a variation on that loop: create a database, use it, create a table, describe it, alter it, insert data into it, and finally pull data back out with queries.
The database that ships with a default MySQL install is named mysql; it already contains a bunch of system tables. You can use it as a sandbox, but you will want your own database for real work.
The three-step ritual. Every database you will build in this session (and on the exam) starts with the same three commands in order:
CREATE DATABASE— make the container.USE— point the session at it, so the following commands land inside it.CREATE TABLE— define the tables inside that container.
Skip step 2 and your tables land in whatever database is currently selected — usually not the one you meant. The order is not decoration; it is a dependency chain.
SHOW DATABASES;
This lists every database the server knows about. Then:
USE mysql;
selects that database, and:
SHOW TABLES;
lists the tables inside it. The three commands together give you the lay of the land before you create anything of your own.
Orientation commands. SHOW DATABASES, USE, and SHOW TABLES are your map and compass. SHOW DATABASES answers "what containers exist?", USE answers "where am I working now?", and SHOW TABLES answers "what is inside this container?" Run all three before creating anything, and the "table already exists" class of errors mostly disappears.
Q: Do we not need to create a schema first, select the schema, and only then create the table? The session went straight into table creation. A: No. When you run CREATE TABLE, the layout you define in that command is itself the schema — the overall structure of the table with its attributes and constraints. There is no separate schema-creation step before it. The table and its schema are created together.
8.2.2 CREATE TABLE Syntax: Columns, Data Types, and Punctuation
The CREATE TABLE statement starts with the keyword, then the table name, then an opening parenthesis. Inside the parenthesis you list every column. Each column needs a column name and a column data type. The punctuation rules are the ones students keep losing marks on:
- After every column definition except the last one, a comma is allowed (and needed to separate columns).
- No comma goes before the closing parenthesis.
- Every complete SQL command ends with a semicolon.
- Whitespace is immaterial — a statement spread over ten lines with generous spacing is exactly the same statement typed on a single line.
CREATE TABLE persons (
person_id INT,
first_name VARCHAR(50),
last_name VARCHAR(50),
address VARCHAR(100)
);
Here the table is persons, and its attributes are person_id, first_name, last_name, and address. Each column declares its data type — integers for person_id, character strings of a chosen length for the names and address — and nothing else is required for the simplest table. Test it in the workbench and the command returns success.
Worked example — the persons table. The command above creates a table named persons with four columns. Reading it column by column:
| Column | Data type | Meaning |
|---|---|---|
person_id |
INT |
whole numbers; used here as the row's identifier |
first_name |
VARCHAR(50) |
text of at most 50 characters |
last_name |
VARCHAR(50) |
text of at most 50 characters |
address |
VARCHAR(100) |
text of at most 100 characters |
The column list is wrapped in parentheses, columns are separated by commas (no comma after the last one), and the whole statement ends with a semicolon. Run it and the workbench reports success — the table now exists, empty, with exactly this layout. Sense-check: four columns in, four lines of output when you DESCRIBE it later.
Pitfall — punctuation loses marks. Three punctuation mistakes dominate beginners: a trailing comma before the closing parenthesis, a missing comma between two column definitions (which merges them into one malformed declaration), and a missing semicolon at the end of the statement. Each produces a syntax error. Notice that commas separate columns — the last column has nothing after it, so it gets no comma.
8.2.3 Data Types: INTEGER, VARCHAR, CHAR, and DATE
SQL distinguishes several families of data types, and choosing the right one is part of designing a table:
- INTEGER (written
INT) — whole numbers, used here for IDs, ages, salaries. - VARCHAR — a character string with a specified maximum length,
VARCHAR(50)meaning up to 50 characters. VARCHAR stores the exact characters you give it. - CHAR — a fixed-length character string; shorter values are padded to the declared length.
- DATE — a calendar date, stored in a structured form that allows date arithmetic and date functions.
A column like year could have been designed as an integer, but a full date_of_publication column of type DATE lets you call date functions on it — extracting the year, the month, and so on — which an integer column cannot do. The book table used in the queries deliberately keeps a DATE column precisely so those functions can be shown.
Choosing a data type is part of the design. The type decides three things: what values are legal (no letters in an INT), how much space a value takes (CHAR(10) always reserves 10 characters; VARCHAR(10) uses only what is needed, up to 10), and which functions work on the column (date functions need a DATE, arithmetic works on numbers, YEAR() needs a DATE). Design the column for the operations you will run on it later — that is why the book table stores a full date instead of a year integer.
Q: For an INT column, can we customize the space, like limiting it to 2 or 4 digits — INT(2), INT(4)? A: For VARCHAR that kind of size limit is definitely possible. For INT the instructor had not tested it when the question came up — the honest answer is "test it yourself." You can specify a length in the declaration and run it in the workbench to see what the system does with it. As a point of reference: in standard SQL the size in an INT(n) declaration is not a value or storage limit — it is a display width hint in MySQL, and the integer range of INT (about −2.1 billion to +2.1 billion) does not change with it. If the goal is to cap digits (say "salary has at most 5 digits, 2 after the point"), the intended SQL type is DECIMAL(i,j), where is the total number of digits (the precision) and is the number of digits after the decimal point (the scale) — DECIMAL(5,2) stores at most 5 digits, 2 of them after the point. So: for VARCHAR the size is a real limit; for INT a parenthesized size is a display hint, not a constraint; for a true digit limit use DECIMAL.
8.2.4 NULL and NOT NULL: Which Columns Must Have Values
When you create a table you can declare, per column, whether an entry is required. NOT NULL means: every row added to this table must supply a value for this column. A column left without that keyword accepts NULL, the special marker for "no value stored here."
The customers table shows the idea with three enforced columns:
CREATE TABLE customers (
id INT NOT NULL,
name VARCHAR(50) NOT NULL,
age INT NOT NULL,
address VARCHAR(100),
salary INT
);
The constraints mean that id, name, and age must be provided in every insert. The address and salary columns may be left empty, in which case the system stores NULL for them. Choosing which columns are NOT NULL is a design decision: the columns that are essential to identify or describe the row are the ones you force.
NULL is a marker, not a value. NULL means "no value stored here" — not zero, not an empty string, but absent. salary set to NULL is different from salary set to 0: zero is a salary of nothing, NULL is "we do not know / this was not supplied." The NOT NULL keyword converts a column from "absent allowed" to "always present." One way to think about it: NOT NULL is the column's own honesty rule — a NOT NULL column can never hold an unknown.
8.2.5 Primary Keys: One per Table, Never Null
A primary key is the column (or combination of columns) that uniquely identifies each row. The customers table declares id as its primary key inside the CREATE TABLE itself:
CREATE TABLE customers (
id INT NOT NULL,
name VARCHAR(50) NOT NULL,
age INT NOT NULL,
address VARCHAR(100),
salary INT,
PRIMARY KEY (id)
);
Two rules matter here. First, a table has exactly one primary key — you cannot have two. Second, a primary key column never stores NULL; the identity of a row must always be known. Declaring a primary key is not strictly mandatory — the earlier persons table had none — but it is good practice, and the framing in the session was direct: it is very, very good for you when you specify it, because the key is how you will find, update, and join rows later.
What a primary key does. A primary key is the column (or column combination) that uniquely identifies each row — the row's fingerprint. Two rules govern it, and both are exam questions in disguise:
- One per table. A table has exactly one primary key. You cannot declare two.
- Never NULL. A primary key column never stores NULL, because a row whose identity is unknown cannot be identified at all.
Notice the primary key declaration sits inside the CREATE TABLE as a separate line after the column list — it is a table-level constraint, not a column-level one. Declaring one is not mandatory (the persons table has none and exists happily), but the session's framing was direct: specifying it is very, very good for you, because the key is how you will find, update, and join rows later.
8.2.6 DESCRIBE: Inspecting a Table's Structure
After creating a table you can ask the system to show you its structure with the DESCRIBE command, abbreviated DESC:
DESCRIBE customers;
The output is a table with one row per column, showing four facts about each: the field name, the data type, whether the column accepts NULL, and whether it is part of a key. For the customers table the expected output lists id, name, age, address, salary with their types (int, varchar, ...), NULL/not-NULL flags matching the constraints, and the id row marked as key. DESC works on any table in the database, including the built-in system tables, so it is your universal tool for answering "what is in this table, exactly?"
Worked example — DESCRIBE customers. After creating the customers table above, run:
DESCRIBE customers;
The result is a five-row report — one row per column:
| Field | Type | Null | Key |
|---|---|---|---|
id |
int |
NO | PRI |
name |
varchar(50) |
NO | |
age |
int |
NO | |
address |
varchar(100) |
YES | |
salary |
int |
YES |
Reading it: id, name, and age say NO under Null (they are NOT NULL), address and salary say YES (they accept NULL), and id carries the PRI marker (it is the primary key). Sense-check: the report matches the CREATE TABLE exactly — three enforced columns, two optional, one key.
8.2.7 SQL Is Case-Insensitive
SQL does not care about case. Writing create table or CREATE TABLE, ID or id, makes no difference; keywords and column names match case-insensitively. This is a comfort for beginners and a trap for nobody — the important thing is that the spelling of a column name stays consistent, so the system can match your query's columns to the table's columns. Case-insensitivity means you can write SUM(basic) as sum(basic) and it still runs.
Why this is only half a freedom. SQL keywords are case-insensitive, and so are column and table names for matching purposes — CREATE TABLE and create table are the same command. But the spelling of a column name must be consistent between where you define it and where you use it: if the table defines last_name and the query asks for lastname, the server cannot match the names and the query fails. Case-insensitive means "either capitalization works," not "any name works."
8.2.8 SHOW DATABASES, SHOW TABLES, and CREATE TABLE IF NOT EXISTS
Before creating a table, check what already exists. SHOW DATABASES and SHOW TABLES tell you what is there. If you try to create a table whose name already exists, the server refuses — two tables cannot share a name in the same database. You can guard against the error with the IF NOT EXISTS clause:
CREATE TABLE IF NOT EXISTS persons (
person_id INT,
first_name VARCHAR(50),
last_name VARCHAR(50),
address VARCHAR(100)
);
The clause makes the command succeed whether or not the table already exists. The alternative — and the required move when you want a truly fresh table — is to drop the existing one first, which is covered in the alter section below.
Pitfall — the name is already taken. A table name is unique within its database: you cannot create a second persons table while one exists. The server refuses with an error, and "but I made a small change to the columns" does not excuse it — the old table still owns the name. Two ways out: CREATE TABLE IF NOT EXISTS makes the command harmless when the table exists (it does nothing), and dropping the old table first gives you a genuinely fresh start. The choice depends on intent: keep-if-exists for idempotent setup scripts, drop-then-create for a clean rebuild.
Everything in this section is one loop. Create the database, use it, create a table with the right types and constraints, describe it to check your work, and know that case never matters but spelling always does. This exact loop — create, use, create table, describe — is the skeleton every later section (alter, insert, select) hangs its commands on. Next, the same table gets changed after creation: that is section 8.3.
8.3 Altering an Existing Table
8.3.1 What ALTER TABLE Can Do
A table's design is not fixed at creation. When you realize a column is missing, or a constraint is wrong, you do not rebuild the table — you alter it. ALTER TABLE is a single command with no commas in the middle; the syntax is one keyword, the table name, then the action. The changes it can perform include:
- dropping a column,
- adding a column,
- changing a column's default value,
- adding or dropping a primary key,
- adding or dropping a foreign key,
- adding a uniqueness constraint.
The command family is very intuitive by design: the language was written so that what you say resembles what you mean. If you want to change the table, you literally say "alter table ...". That natural-language flavor is a recurring theme across all of SQL.
Design is not set in stone. The whole point of ALTER TABLE is that a table's design is decided at creation but is not frozen at creation. Real projects discover missing columns and wrong constraints constantly; ALTER TABLE is the surgical way to fix them without dropping and rebuilding the table (and losing the data inside it). The syntax follows a fixed skeleton — the keyword ALTER TABLE, the table name, then one action — and a comma never appears in the middle.
8.3.2 Dropping and Re-Adding a Primary Key
Because a table can hold only one primary key, changing the key means a two-step operation: drop the existing one, then add the new one.
ALTER TABLE customers DROP PRIMARY KEY;
After this command, DESCRIBE customers shows no entry in the key column at all. You can then put a key back:
ALTER TABLE customers ADD PRIMARY KEY (id);
The student error of the session came from running the drop twice in a row: once the primary key is gone, a second DROP PRIMARY KEY has nothing to drop and errors out. The same applies in general — re-running an alter that was already applied fails, which is a normal and expected event, not a sign of a broken installation.
Worked example — drop the key, re-add it as a composite. The customers table currently has a single-column primary key on id. Changing it to the composite key (id, name) is a two-step operation, because a table can hold only one primary key at a time:
Step 1 — drop the existing key:
ALTER TABLE customers DROP PRIMARY KEY;
DESCRIBE customers now shows no entry in the key column at all — the table has no primary key, temporarily.
Step 2 — add the new key:
ALTER TABLE customers ADD PRIMARY KEY (id, name);
The command succeeds, and DESCRIBE now marks both id and name as key columns: the pair is the new primary key. Final state: one primary key, spanning id and name. Sense-check: the two-step choreography matches the one-key-per-table rule — you cannot have two keys while swapping, so you drop before you add.
Pitfall — running the same alter twice. The session's live error: a student ran ALTER TABLE customers DROP PRIMARY KEY;, and ran it again — and the second run failed, because there was no primary key left to drop. The same holds for re-adding a key that already exists. This is a normal, expected event, not a broken installation: the server refuses an operation the table's current state cannot satisfy. When an alter fails, describe the table first and check what state it is actually in.
8.3.3 Composite Primary Keys
A primary key is not limited to a single column. You can declare that two or more columns, taken together, form the key:
ALTER TABLE customers ADD PRIMARY KEY (id, name);
Now the uniqueness requirement applies to the pair: no two rows may share both the same id and the same name. Individually the columns may repeat — two customers can share a name — but the combination must be unique. The workbench run of this command confirmed the table updated as expected, with both columns now showing up as key columns in DESCRIBE.
Composite keys: uniqueness of the pair. A primary key can span two or more columns. The rule changes from "each value must be unique" to "each pair (or triple) must be unique." Individually the columns may repeat freely — two customers can both be named Ramesh — but no two rows may carry the same combination. Think of it like a full name in a classroom: two students may share a first name and two may share a last name, but the pair (first, last) is expected to pick out one person. After ADD PRIMARY KEY (id, name), DESCRIBE shows both columns marked as keys.
8.3.4 Unique Constraints and Candidate Keys
A uniqueness constraint (UNIQUE) is a rule that a column's values must never repeat — every row in that column must hold a different value. It is related to the primary key but not the same thing. The invitation to guess produced the correct intuition: columns declared UNIQUE behave like candidate keys — each of them could have served as the primary key, and no duplicates are allowed in them.
Q: What is the difference between uniqueness and the primary key? Both are supposed to store only unique values, but uniqueness additionally allows you to have null values. Is that right? A: The primary key does not allow null values — that part is certain. On whether a UNIQUE column may contain NULLs, the instructor deferred: "I will have to refer to it and let us see" — the claim was acknowledged as a good one and marked for verification. The framing stands: UNIQUE columns behave like candidate keys, each of them a possible primary key. For the record: standard SQL and MySQL do allow multiple NULLs in a UNIQUE column, because NULL is never considered equal to NULL — the uniqueness rule compares values, and two NULLs do not collide. So the student's claim was right, and the primary-key rule (never NULL) is what separates the two.
8.3.5 The Table-Already-Exists Mistake
A recurring beginner error is creating a table, then creating it again with the same name — perhaps with a slightly different column list — and getting an error. The rule: you cannot create a second table with a name that already exists in the database. You can only update an existing table (with ALTER) or insert into it. To start completely fresh you must delete the whole table first, and that is what DROP TABLE does:
DROP TABLE customers;
Drop removes the table and all of its columns. After it, CREATE TABLE customers succeeds again. This exact situation played out live: a student had created customer with different column names, tried to create a customers table, hit "already exists", and the fix was to check what existed, drop it, and create fresh.
Worked example — recovering from "already exists". A student had earlier created a table named customer (different column names), then tried CREATE TABLE customers ... — and the server refused, because a table named customers already existed in the database. The recovery path, step by step:
- Look before you drop:
SHOW TABLES;confirms what actually exists. - Drop the existing table:
DROP TABLE customers;
- Create fresh:
CREATE TABLE customers (...);now succeeds.
Final state: one customers table with the new column list. The lesson: "already exists" is not a dead end — the old table can be dropped to make room, and DROP TABLE removes the table and all of its columns. Sense-check: after the drop, the name is free again, exactly as the error message implied.
Pitfall — "already exists" does not mean "impossible". A table name is unique within its database, so a second CREATE TABLE with an existing name fails. The live example of the session: a student had earlier created a table named customer with different column names, then tried to create customers — and hit "already exists" because the name was taken. The recovery path is to look first (SHOW TABLES), decide whether the existing table is wanted, and if not, DROP TABLE customers; — which removes the table and all its columns — then create fresh. DROP is destructive and permanent, so the check before it is not optional politeness; it is the difference between "reset the table" and "lost the data."
8.3.6 The Four SQL Language Families: DDL, DML, DCL, TCL
SQL divides into four language families by purpose:
- Data Definition Language (DDL) — create, alter, drop tables and other structures. This is the family everything above belongs to.
- Data Manipulation Language (DML) — insert, update, and select data.
- Data Control Language (DCL) — grant or revoke access to someone, controlling who is allowed to do what.
- Transaction Control Language (TCL) — commit, rollback, and savepoints, used when a group of operations must succeed or fail together. Commit makes changes permanent, rollback undoes them, and savepoints (checkpoints) let you roll back only part of the way.
Real-world: these four names come up constantly in job interviews and in project delegation. When a manager offloads a database task to you, the first verification is often "what is DDL? what is DCL? what is DML?" — anyone who has actually worked with SQL can answer from experience. The interview framing matters more than the closed-book exam: for the exam, do not try to cram these definitions; if you have worked through the material, expect application-based questions instead.
Four families, one way to sort any command. Every SQL command you will write lands in one of four baskets: DDL defines structures (CREATE, ALTER, DROP — this whole section), DML manipulates data (INSERT, UPDATE, SELECT — the rest of the session), DCL controls access (GRANT, REVOKE), and TCL controls transactions (COMMIT, ROLLBACK, SAVEPOINT). Sorting a command into its family is a quick interview skill and a good mental index for the language. Exam note: these four names matter for interviews, but for the closed-book exam do not cram the definitions — with the material worked through, expect application-based questions instead.
8.4 Inserting Data with INSERT INTO
8.4.1 Single-Row Insert
A created table is an empty shell — no data until you add it. The command to add one row is INSERT INTO:
INSERT INTO customers (id, name, age, address, salary)
VALUES (1, 'Ramesh', 30, 'Pune', 5000);
The syntax reads naturally: insert into the table, then the column list, then the keyword VALUES, then the actual values in parentheses. The values must appear in the same order as the listed columns — the first value lands in the first listed column, and so on. Punctuation again: no comma after the last value, semicolon at the end.
How INSERT INTO works. The statement is a column list followed by a value list, and the pairing is positional: the first value lands in the first listed column, the second in the second, and so on. INSERT INTO customers (id, name, age, address, salary) VALUES (1, 'Ramesh', 30, 'Pune', 5000); puts 1 in id, 'Ramesh' in name, 30 in age, 'Pune' in address, 5000 in salary. Swap two values and you have silently swapped two columns — the order in the value list must mirror the order in the column list, character for character.
Pitfall — value order and quote discipline. Two beginner errors dominate INSERTs. First, ordering: VALUES (30, 'Ramesh', 1, 'Pune', 5000) with the column list (id, name, age, address, salary) puts 30 into id and 1 into age — the insert may succeed while the data is wrong, which is worse than an error. Second, quoting: text values go in single quotes ('Ramesh', 'Pune'), numbers do not (30, 5000), and dates go in single quotes as '1997-01-15'. Mixing these up is the most common beginner syntax error in SQL.
8.4.2 Multi-Row Insert (the book Table)
You can add several rows in a single command by listing multiple value groups, separated by commas:
INSERT INTO book (isbn, title, author_name, date_of_publication, publisher, price)
VALUES ('B001', 'Database Systems', 'Silberschatz', '1997-01-15', 'McGraw-Hill', 30),
('B002', 'Fundamentals of DBMS', 'Elmasri', '1996-06-01', 'Pearson', 18),
('B003', 'SQL Essentials', 'Date', '1993-03-20', 'Addison-Wesley', 15);
Each bracket group is one row. After the first value group, a comma starts the next entry, and so on. Single-row inserts end after one group with a semicolon; multi-row inserts use commas between groups and the semicolon only at the very end. The book table's attributes are isbn, title, author_name, date_of_publication, publisher, and price — and note that the publication date here is a DATE, not a plain year, which the query section exploits.
Worked example — three rows in one statement. The command above inserts three rows into the book table in a single shot. Reading the groups:
| isbn | title | author_name | date_of_publication | publisher | price |
|---|---|---|---|---|---|
| B001 | Database Systems | Silberschatz | 1997-01-15 | McGraw-Hill | 30 |
| B002 | Fundamentals of DBMS | Elmasri | 1996-06-01 | Pearson | 18 |
| B003 | SQL Essentials | Date | 1993-03-20 | Addison-Wesley | 15 |
One INSERT INTO statement, three bracket groups, commas between groups, and a single semicolon at the very end. Notice date_of_publication holds full dates — 1997-01-15, not the number 1997 — which is exactly what lets the later queries extract years with YEAR(). Sense-check: the table now holds three books, each with a complete row in the correct column order.
8.4.3 Skipping Columns: Unspecified Values Become NULL
You do not have to fill every column. If the customers table has only id, name, and age as NOT NULL, then an insert that supplies just those three is legal:
INSERT INTO customers (name, address, city)
VALUES ('Rakesh', 'Delhi', 'Delhi');
The columns you omit are stored as NULL. The only hard requirement: every NOT NULL column must receive a value. So the practical reading of the earlier constraints — "these three attributes need to be specified for sure" — is exactly what makes the insert succeed or fail.
Omitted columns become NULL. The INSERT column list is a menu, not a census: you choose which columns to fill, and every column you leave out is stored as NULL — the "no value stored here" marker from section 8.2. The one hard requirement is the contract from CREATE TABLE: every NOT NULL column must receive a value. If the customers table declares id, name, and age NOT NULL, an insert that omits any of the three fails; omitting address or salary is fine and stores NULL for them. The constraints you wrote at creation are exactly what decides whether an insert succeeds or fails.
One command, one or many rows, same rules. INSERT INTO fills an empty table: one value group per row, values positioned by the column list, omitted columns stored as NULL, NOT NULL columns mandatory. The book table's three rows are now the dataset the rest of the session queries. Next up: pulling that data back out with SELECT.
8.5 The Anatomy of a SELECT Query
8.5.1 SELECT, FROM, WHERE, GROUP BY, HAVING, ORDER BY
The full shape of a retrieval query has six parts, and most of them are optional:
SELECT [DISTINCT] columns
FROM table_name
[WHERE condition]
[GROUP BY columns]
[HAVING condition]
[ORDER BY columns];
The bracket convention is itself a lesson: in SQL, anything shown inside square brackets is optional — you may include it or leave it out. Of all six clauses, exactly two are essential: SELECT, which says what attributes you want in the output, and FROM, which says which table they come from. The rest refine the result:
- WHERE — a predicate, a condition, that filters which rows are considered. SELECT limits the columns; WHERE limits the rows.
- GROUP BY — groups rows together so aggregate functions like minimum, maximum, sum, and average can be computed per group.
- HAVING — a condition applied to the groups, used only together with GROUP BY.
- ORDER BY — sorts the output in the order you choose.
SELECT limits columns, WHERE limits rows. The six-clause skeleton reads top to bottom as a pipeline: SELECT picks the columns, FROM names the table, WHERE discards rows that fail a condition, GROUP BY gathers rows into groups, HAVING filters the groups, ORDER BY sorts the survivors. Two of the six are essential — SELECT and FROM — and everything in square brackets is optional, which is exactly what the brackets mean in SQL grammar: you may include it or leave it out.
If that feels like information overload, the session's own advice applies: do not try to memorize the whole slide at once. Two clauses — SELECT and FROM — carry almost all of what you will write; the others appear one at a time with practice.
Learn the skeleton, not the slide. The six clauses look like a wall of text on first sight. The session's own advice is the antidote: do not memorize the whole slide at once. Almost everything you will write in the coming sections is SELECT plus FROM plus one more clause — WHERE here, ORDER BY there, GROUP BY later. Each new clause enters the picture one at a time, with practice.
8.5.2 SELECT * FROM book: Reading the Whole Table
The simplest useful query reads everything:
SELECT * FROM book;
The star means "all columns." The result is exactly the table you inserted — the same rows, the same columns, in insertion order. This is the first thing to run after an insert or update, because it is the ground truth: whatever the table really holds, it shows you.
The ground-truth query. SELECT * FROM book; answers "what is actually in this table, right now?" — every row, every column, in insertion order. Run it after every insert and update, because it shows whatever the table really holds, not whatever you believe you put in. When a later query's count or sum disagrees with your expectation, this is the query to run first.
8.5.3 Choosing Columns and DISTINCT
Instead of the star, name the columns you want:
SELECT title, author_name, publisher FROM book;
The output contains only those three columns, in the order you listed them. The DISTINCT keyword removes duplicate rows from the result. For example, asking for every publisher in the book table:
SELECT DISTINCT publisher FROM book;
gives each publisher once, no matter how many of its books appear in the table. The plain version without DISTINCT returns the value for every row, repetitions included.
Worked example — columns versus distinct columns. With the three books inserted in section 8.4:
SELECT title, author_name, publisher FROM book; returns three columns in the listed order — three rows, each with title, author, publisher.
SELECT publisher FROM book; returns a column with three values: McGraw-Hill, Pearson, Addison-Wesley — one per row.
SELECT DISTINCT publisher FROM book; also returns three values here, because each book has a different publisher. Add a fourth book from McGraw-Hill and the plain query returns four rows (with McGraw-Hill twice) while DISTINCT still returns three — one per distinct publisher, duplicates collapsed. Sense-check: DISTINCT removes repeated rows, it never creates new ones.
Two clauses carry the whole language. SELECT names the output columns, FROM names the source table — everything else is optional refinement. Star means "all columns," and DISTINCT collapses duplicate rows. From here on, every query is this skeleton with one extra clause bolted on: the next section bolts on WHERE.
8.6 Filtering Rows with WHERE
8.6.1 Comparison Operators
The WHERE clause uses the usual comparison operators, written in SQL's own way:
=equal to<>or!=not equal to<less than>greater than<=less than or equal to>=greater than or equal to
These are the operators you will type in almost every query. Combine two conditions with AND or OR as needed.
WHERE is a row filter. WHERE evaluates a condition against every row of the table and keeps exactly the rows for which the condition is true. The condition is built from comparison operators (six of them, all familiar from arithmetic: equal, not equal, less, greater, less-or-equal, greater-or-equal) joined by AND and OR. A query with WHERE and no other clause is the SELECT-plus-FROM skeleton from section 8.5 with one filter bolted on.
Pitfall — = for equality, not ==. SQL writes equality as a single = and inequality as <> (or !=). Two characters to watch: a single = inside WHERE is a comparison, never an assignment — and do not reach for ==, which SQL does not understand. Also remember the strictness of < and >: "greater than 25" excludes 25 itself; "25 or more" needs >=. The boundary value is where exam problems hide (section 8.7 shows one live).
8.6.2 IN and NOT IN
When a value must match one of several options, IN saves you from a long chain of ORs. These two queries are equivalent:
SELECT title, author_name, publisher, year FROM book
WHERE year = 1993 OR year = 1996 OR year = 1998;
SELECT title, author_name, publisher, year FROM book
WHERE year IN (1993, 1996, 1998);
The second is shorter and easier to read as the option list grows. The complement NOT IN selects everything that does not match any listed value.
IN is shorthand for a chain of ORs. WHERE year IN (1993, 1996, 1998) means "the year is 1993, or it is 1996, or it is 1998" — exactly the same condition as the three ORs above, so the two queries return identical results. As the list of options grows, the IN form stays one line while the OR chain grows linearly. NOT IN is the mirror image: every row whose value is in none of the listed options.
8.6.3 BETWEEN and NOT BETWEEN: Inclusive Ranges
For range conditions, BETWEEN tests inclusion in an interval, and both ends are inclusive. These two queries are equivalent:
SELECT title, author_name, publisher, price FROM book
WHERE price >= 10 AND price <= 25;
SELECT title, author_name, publisher, price FROM book
WHERE price BETWEEN 10 AND 25;
The interval picture helps: in mathematics, is the closed interval , and BETWEEN includes both boundary values. The complement:
SELECT ... WHERE price NOT BETWEEN 10 AND 25;
selects everything outside the interval — both the part below 10 and the part above 25, the union of the two rays and . If you picture the number line as running from to , BETWEEN carves out the middle segment and NOT BETWEEN keeps both ends.
BETWEEN is a closed interval, both ends included. price BETWEEN 10 AND 25 is exactly the pair of comparisons price >= 10 AND price <= 25 — the closed interval on the number line. A book priced exactly 10 and a book priced exactly 25 both qualify; the endpoints are inclusive. NOT BETWEEN 10 AND 25 keeps everything outside that segment: below 10 or above 25, the union of the two rays and . No value is ever left out — the line splits into the middle segment and the two ends, with the boundary values deciding which side they belong to.
8.6.4 Working with Dates: The YEAR() Function
A DATE column stores a full date, but queries often want just the year. The YEAR() function extracts it:
SELECT YEAR(date_of_publication) AS years FROM book;
The AS keyword renames the output column — here the extracted year appears under the heading years. Without DISTINCT this returns the year of every row, repetitions included; adding DISTINCT gives each distinct year once. You can then filter with the extracted value directly:
SELECT title FROM book WHERE YEAR(date_of_publication) = 1997;
The brute-force alternative — comparing the whole date, e.g. "from 1st January 1997 up to 31st December 1997" — also works, but YEAR() is the cleaner tool, and the function is the point of keeping the column a DATE instead of an integer.
Functions turn stored data into answers. YEAR() reads a DATE value and returns the four-digit year. Because date_of_publication is a DATE column, the function works: YEAR(date_of_publication) produces 1997, 1996, 1993, and so on. The extracted value can be displayed (SELECT YEAR(...) AS years), deduplicated (SELECT DISTINCT YEAR(...)), or used in a condition (WHERE YEAR(...) = 1997). This is why the book table stores a full date rather than a year integer — a YEAR function needs a DATE to work on. A plain integer column stores 1997 but cannot answer "what month?" or "what day?"; the DATE column answers all three.
AS renames the output column. AS years gives the computed column a readable heading; without it, the output column is headed by the entire expression YEAR(date_of_publication). The alias applies to the output only — the table's column name is untouched.
8.6.5 ORDER BY: Sorting the Results
To sort the output, add ORDER BY with the column to sort on. ORDER BY is also the answer when you have more than one candidate sort column: the first listed column is the primary sort, later ones break ties. Sorting can be ascending or descending; the query "price greater than some value, ordered by price" is the pattern for the cheapest-available-book kind of question.
ORDER BY sorts the result, last in the pipeline. ORDER BY sits at the end of the query and sorts whatever rows survived WHERE. One column is enough; with several, the first listed column is the primary sort and each later column breaks ties among rows that are still equal. The default direction is ascending; DESC flips it. The "cheapest available book" pattern is WHERE price > X ORDER BY price ASC — filter first, then sort the survivors, then the top row is the answer.
Every filtering tool is one clause. Comparison operators, IN, BETWEEN, YEAR(), and ORDER BY all bolt onto the same SELECT...FROM skeleton — and each is its own shorthand: IN for chains of OR, BETWEEN for closed intervals, YEAR() for dates, ORDER BY for sorting. These exact operators are the vocabulary of the worked queries in the next section.
8.7 Worked Queries on the book Table
8.7.1 Books from 1997 Priced Above 25
Problem: get the title, author name, and publisher of all books published in 1997 whose price is greater than 25.
SELECT title, author_name, publisher
FROM book
WHERE YEAR(date_of_publication) = 1997 AND price > 25;
The check was done by hand: two books in the table were published in 1997. Of those two, only one has a price strictly greater than 25 — the other sits exactly at 25. With price > 25 the result is one row; change the operator to >= and both 1997 books appear. This is the lesson about strict versus inclusive comparison in action, and it is why the boundary value deserves attention in exam problems.
Worked example — the boundary value decides the answer. The book table (from section 8.4) holds:
| isbn | title | author_name | date_of_publication | publisher | price |
|---|---|---|---|---|---|
| B001 | Database Systems | Silberschatz | 1997-01-15 | McGraw-Hill | 30 |
| B002 | Fundamentals of DBMS | Elmasri | 1996-06-01 | Pearson | 18 |
| B003 | SQL Essentials | Date | 1993-03-20 | Addison-Wesley | 15 |
The condition has two parts joined by AND, so a row must pass both: YEAR(date_of_publication) = 1997 keeps B001 (published 1997) and B002 (published 1996) — wait, B002 is 1996, so only B001 is 1997. The professor's check: the session's book table actually contains two 1997 books in its fuller version, one priced 30 and one priced exactly 25. With price > 25, only the book priced 30 survives: result = one row. Change the operator to >= and the book priced exactly 25 joins it: result = two rows.
The one-character change between > and >= flips the boundary book in or out. That is the strict-versus-inclusive lesson, and it is why exam problems plant boundary values deliberately — the price exactly 25 is the trap. Sense-check: with > the boundary value 25 must be excluded; with >= it must be included — the queries behave exactly as the operators promise.
8.7.2 Three Publication Years, Two Ways: OR and IN
Problem: find title, author, and publisher of books published in any of 1993, 1996, or 1998. The OR version chains three equalities; the IN version lists the three years in brackets. Both return identical results, and the session worked the IN version as the better-written one.
Worked example — OR and IN, same answer. Two formulations of one question:
-- Form A: three ORs
SELECT title, author_name, publisher FROM book
WHERE year = 1993 OR year = 1996 OR year = 1998;
-- Form B: IN list
SELECT title, author_name, publisher FROM book
WHERE year IN (1993, 1996, 1998);
Both keep exactly the rows whose year is one of the three listed — with the sample data, the books published in 1993 and 1996 (the 1998 book does not exist in this data), giving identical results from both forms. The IN form is the better-written one: one condition, one place to read the list, and the list extends by adding a comma and a value instead of rewriting the chain. Sense-check: if the two queries returned different rows, the shorthand would be lying — the session ran them side by side precisely to show they match.
8.7.3 Price Between 10 and 20: Two Equivalent Queries
Problem: all books with price between 10 and 20 inclusive. The first formulation uses two comparisons:
SELECT * FROM book
WHERE price >= 10 AND price <= 20;
The second uses BETWEEN:
SELECT * FROM book
WHERE price BETWEEN 10 AND 20;
Both return the same single book — with the sample data, only one book falls in the closed interval . Running the complement, NOT BETWEEN 10 AND 20, returns every other book in the table. The two formulations were run side by side so you can see them produce identical results, which builds trust that the shorthand means exactly the two comparisons.
Worked example — the middle segment and the two rays. Against the three-book table (prices 30, 18, 15):
WHERE price >= 10 AND price <= 20→ prices 18 and 15 qualify → two rows.WHERE price BETWEEN 10 AND 20→ the identical condition → the same two rows.
(Between the two formulations there is no difference in logic, only in length — and with a fuller table the answer is still the rows whose price lies in the closed interval .) The complement NOT BETWEEN 10 AND 20 keeps every other book — prices outside the interval, here the 30-priced book. Sense-check: every book falls on one side or the other — in the interval or out — and the two queries together cover the whole table with no overlap.
8.7.4 Extracting Years, and NOT IN on Missing Years
Problem: retrieve just the years present in the book table. The query:
SELECT DISTINCT YEAR(date_of_publication) AS years FROM book;
returns each year once. Then, filtering on years with IN for 1993, 1996, and 1998 returns nothing at all — the sample data contains no books from those years. The counterpart:
SELECT DISTINCT YEAR(date_of_publication) AS years FROM book
WHERE YEAR(date_of_publication) NOT IN (1993, 1996, 1998);
returns every year that is present. Empty results are not bugs: they are correct answers to questions the data cannot satisfy, and "we asked for years that do not exist in the table" is a legitimate finding.
Worked example — empty results are answers. The three books in the sample data were published in 1997, 1996, and 1993. Step by step:
SELECT DISTINCT YEAR(date_of_publication) AS years FROM book;→ the distinct years present: 1993, 1996, 1997 (one row per year).- Filter those years by
IN (1993, 1996, 1998)→ the table has no 1998 book, so the result is empty — zero rows. NOT IN (1993, 1996, 1998)→ everything present that is not one of those three → 1997 only.
The empty result in step 2 is not a bug: it is the correct answer to "which books were published in 1998?" when the table contains none. "We asked for years that do not exist in the table" is a legitimate finding, and an empty result set is a valid, gradeable answer when the data cannot satisfy the question. Sense-check: the two complement queries cover the space — one keeps the listed years, the other keeps everything else.
The book table is a mini catalog of every filter. The four problems of this section used every operator from 8.6 in action: strict > with a boundary trap, OR versus IN, the closed interval with BETWEEN, and YEAR() with IN/NOT IN on dates. The habit on display — checking the query's answer by hand against the raw table — is the one to keep for every query you write.
8.8 The employee Database: Schema, Data, and Counting
8.8.1 Building the employee Table and Loading It
The second table of the session is the employee table, built with the same three-step ritual: create the database, use it, create the table.
CREATE DATABASE IF NOT EXISTS employee_database;
USE employee_database;
CREATE TABLE employee (
number INT NOT NULL,
name VARCHAR(50),
department_id VARCHAR(5),
basic INT,
hra INT,
reductions INT,
tax INT
);
Then the table is populated — six employees in total, with names Ramesh, Prasanna, Sham, Rajesh, Gautam, and Ram, spread across three departments whose IDs are D1, D2, and D3. Each employee has a basic pay, an HRA (house rent allowance) component, reductions, and tax — the exact numbers were typed into the workbench during the session.
The table was deliberately created without a primary key, to show the ALTER path once more:
ALTER TABLE employee ADD PRIMARY KEY (number);
The command returned success, and the schema check confirmed the key had landed.
The employee table at a glance. The schema declares seven columns: number (an employee identifier, INT and NOT NULL), name (text), department_id (three-letter IDs like 'D1' — VARCHAR because the IDs are strings), and four numeric pay components — basic (base pay), hra (house rent allowance), reductions (deductions), and tax. The table was created without a primary key on purpose, so the ALTER path from section 8.3 could be shown once more: ALTER TABLE employee ADD PRIMARY KEY (number); added the key after creation, exactly the drop-and-re-add choreography practiced earlier.
8.8.2 Reading the Table Back
SELECT * FROM employee;
returns all six rows. It is the same table that was inserted, and it is the reference point for every query that follows: when a later query claims a count or a sum, this is the table you verify it against.
The verification habit. SELECT * FROM employee; returns all six rows and is the reference point for every query in the rest of the session. When an aggregate later claims "the answer is two" or "the total is about 29,000," the check is against this raw listing: count the D1 rows yourself, add the basics yourself. Aggregates are only as trustworthy as your understanding of the data behind them.
8.8.3 COUNT(*), COUNT(column), and COUNT(DISTINCT column)
Counting rows uses the aggregate function COUNT:
SELECT COUNT(*) FROM employee;
COUNT(*) counts all rows — six, here. COUNT(name) counts the values in one column: the six names are all present, so the answer is again six. The subtle case is counting a column that can repeat:
SELECT COUNT(department_id) FROM employee;
still returns six, because COUNT(column) counts rows, not distinct values — six rows all have a department_id, even though the same three IDs repeat. Only the explicit version counts unique values:
SELECT COUNT(DISTINCT department_id) FROM employee;
and that returns three — the number of distinct department IDs. This contrast between COUNT(column) and COUNT(DISTINCT column) is a frequent exam favorite, so the mental rule to keep: plain COUNT counts rows; COUNT DISTINCT counts distinct values.
The three COUNTs. COUNT is the row-counting aggregate, and its three forms answer three different questions:
COUNT(*)— total rows in the table: six.COUNT(name)— rows that have a value in the named column (the six names are all present): six. Note it counts rows with a value, not distinct values.COUNT(DISTINCT department_id)— distinct values in the named column: the three IDs D1, D2, D3 → three.
The trap is the middle form: COUNT(department_id) returns six, because all six rows carry a department ID even though the same three IDs repeat. Plain COUNT counts rows; COUNT DISTINCT counts distinct values. The distinction is a frequent exam favorite.
8.8.4 Worked Query: Employees in D1 with Basic Below 6000
Problem: find the number of employees in department D1 whose basic salary is less than 6000.
SELECT COUNT(name)
FROM employee
WHERE department_id = 'D1' AND basic < 6000;
COUNT(name) works as a row count here since every counted row has a name. The answer is two. Verified by hand against SELECT * FROM employee: department D1 holds exactly two employees, with basics of 4,500 and 5,000, both below 6,000. A follow-up pulled the detail row for the 4,500 employee:
SELECT * FROM employee
WHERE department_id = 'D1' AND basic = 4500;
and it returned the row for Rajesh. Reading the raw table to confirm an aggregate's answer is a habit worth building: aggregates are only as trustworthy as your understanding of the data behind them.
Worked example — count then verify. The query filters the six rows with department_id = 'D1' AND basic < 6000 and counts the survivors. The hand check against SELECT * FROM employee: department D1 holds exactly two employees, with basics 4,500 and 5,000 — both below 6,000. Answer: 2. The follow-up SELECT * FROM employee WHERE department_id = 'D1' AND basic = 4500; returned the detail row: the 4,500 employee is Rajesh. The habit on display: the aggregate said "two," and the raw-table check confirmed exactly which two. Sense-check: 4,500 < 6,000 and 5,000 < 6,000, so both D1 employees qualify and neither a third — two is consistent with the data.
8.8.5 Why Strings Need Single Quotes and Numbers Do Not
In the query above, 'D1' is wrapped in single quotes but 6000 is not. The reason is the column's data type: department_id is a character column (VARCHAR), so its values are written as strings, enclosed in single quotes. basic is numeric, so its comparison value is written as a plain number. The quiz moment of the session made the class state the rule: when we deal with varchar or char, we put the value in quotes — not inverted double quotes, but the single quote key. Mixing the two — quoting a number or unquoting a string — is one of the most common beginner syntax errors.
The data type decides the quoting. A literal value in SQL is written according to the column it is compared with. department_id is VARCHAR, so its values are strings and must be wrapped in single quotes: 'D1'. basic is INT, so its comparison value is a plain number: 6000, no quotes. The quiz moment of the session made the class state the rule out loud: when we deal with varchar or char, we put the value in quotes — not inverted double quotes, but the single quote key. Quoting a number ('6000') or unquoting a string (D1) is one of the most common beginner syntax errors — the server cannot interpret the literal as the column's type and errors out.
Pitfall — single quotes, not double. String literals use the single quote key — 'D1', 'Ramesh', 'Pune'. Double quotes are not the correct quote for string literals in the sessions' MySQL usage, and a bare unquoted D1 is read as a column name or identifier, not a value. When a query that "should" work fails with an unknown-column or syntax error, check the quotes first.
The employee table is the dataset for the rest of the session. Six employees across D1, D2, D3 with pay components; COUNT counts rows unless DISTINCT is added; literals are quoted by data type. Everything that follows — UPDATE, SUM, AVG, GROUP BY, nested queries — runs against exactly this table.
8.9 Updating Data Safely
8.9.1 UPDATE ... SET ... WHERE
Inserting and selecting are not enough; you also change existing rows. The command is UPDATE with a SET clause and a WHERE condition:
UPDATE employee
SET basic = 45000
WHERE department_id = 'D1' AND basic = 4500;
Read it as: update the employee table, set the basic column to 45,000, for exactly the rows where the department is D1 and the basic is 4,500. The WHERE clause is what makes the update surgical; without it, the SET would apply to every row in the table.
UPDATE is DML — it changes data, not structure. UPDATE is the third member of the DML family (insert, update, select) and has a three-part anatomy: the table name, the SET clause naming the column and its new value, and the WHERE clause choosing which rows change. The WHERE clause is what makes the update surgical: without it, SET applies to every row of the table. A single missing condition can rewrite a whole department's payroll, so the WHERE clause is not an optional refinement here — it is the safety mechanism.
8.9.2 The Safe-Update Error and LIMIT 1
The first attempt at this exact update failed for several students — and for the instructor, live — with an error about updating many rows at once. The server enforces a safety rule: an UPDATE without a precise enough WHERE condition, one that would touch many rows (the message referenced a threshold of 500 rows), is rejected. Two workarounds exist.
The first is LIMIT 1, which caps the update to a single row:
UPDATE employee
SET basic = 45000
WHERE department_id = 'D1' AND basic = 4500
LIMIT 1;
This ran successfully. The second workaround is turning the safe-update mode off around the statement:
SET SQL_SAFE_UPDATES = 0;
UPDATE employee SET basic = 45000 WHERE department_id = 'D1' AND basic = 4500;
SET SQL_SAFE_UPDATES = 1;
Turning the mode off removes the guard, and turning it back on restores it.
Pitfall — the safe-update guard. The first attempt at this update failed live — for students and the instructor — with an error about updating many rows at once. MySQL's safe-update mode refuses an UPDATE whose WHERE cannot be shown to affect a bounded set (the message referenced a threshold of 500 rows): the guard treats a broad update as a likely mistake, not a request. The fix that ran successfully: append LIMIT 1, which caps the operation to a single row and tells the guard "yes, I mean a small, bounded change." The heavier alternative — SET SQL_SAFE_UPDATES = 0; … SET SQL_SAFE_UPDATES = 1; — disables the guard around the statement and re-enables it after; it works, but it is a sledgehammer for a task a LIMIT solves.
Q: Is it necessary to execute the safe-update command — do we have to set SQL safe updates to 0 or 1? A: No. You do not need to change safe-update mode. The LIMIT 1 version runs fine with safe updates still on — it worked that way for the instructor. Use LIMIT when you want a restricted update; leave the mode alone.
Q: The update worked for me but a classmate got an error — why would the same query behave differently? A: Several causes were discussed, none definitively settled in the session: the other machine may not have selected the employee database first (missing USE employee_database;), the table being updated may not exist in the currently selected database, or the safe-update mode may differ between installations. The working advice: check which database is selected, confirm the table exists, and if the error persists, post it to the FAQ list so it can be resolved with the full context.
After the successful update, SELECT * FROM employee showed the change: one row now carried a basic of 45,000. Later in the session the change was reverted with the mirror update, setting 45,000 back to 4,500.
Worked example — update, verify, revert. Before the update, Rajesh's row (department D1) carries basic = 4500. The update SET basic = 45000 WHERE department_id = 'D1' AND basic = 4500 LIMIT 1; matches exactly one row — Rajesh's — and sets it to 45,000. SELECT * FROM employee confirms: one row now shows basic = 45,000. The same WHERE condition identifies the same row later, and the mirror update SET basic = 4500 WHERE department_id = 'D1' AND basic = 45000 LIMIT 1; reverts the change, restoring 4,500. Sense-check: the WHERE clause that pins the row down works in both directions — the same surgical condition found the row going up and going back down.
WHERE is the surgeon's hand; the guard is the safety check. UPDATE changes rows, SET names the change, WHERE chooses the rows — and the safe-update guard exists exactly because a careless WHERE rewrites the world. LIMIT 1 makes the guard happy for a single-row change; disabling the mode is the escape hatch. After any update, SELECT the table and confirm what changed — the mirror-update revert later in the session is exactly that habit in action.
8.10 Aggregates and Arithmetic in SELECT
8.10.1 SUM: Total Basic Pay
Problem: find the total basic pay of all employees in the organization.
SELECT SUM(basic) FROM employee;
The aggregate function SUM adds the values of the named column across the selected rows. With six employees the total came to about 29,000 — the spoken figure "29,000 something." Note also the case-insensitivity in action: SUM(basic) was typed with a lowercase column name and ran fine, because the spelling of the attribute, not its case, is what matters.
Related worked queries from the same run: the total basic pay for department D2 among employees whose basic is more than 4,000:
SELECT SUM(basic) FROM employee
WHERE department_id = 'D2' AND basic > 4000;
and the total basic bill for all employees whose salary is greater than 4,500:
SELECT SUM(basic) FROM employee
WHERE basic > 4500;
The pattern is uniform: pick the aggregate, name the column, add WHERE to narrow the rows. The spoken phrase "sum of all the sales in department D2" is a slip of the tongue — the column being summed throughout is the basic-salary column, and the queries above are the ones that match the surrounding discussion.
An aggregate turns a column into one number. SUM walks down the selected rows, adds the named column's values, and returns a single number. Its skeleton is uniform across every aggregate in this section: pick the aggregate (SUM, AVG), name the column (basic, hra), and add WHERE to narrow the rows. SELECT SUM(basic) FROM employee; with six employees totals about 29,000 — "29,000 something" as the session put it. Add WHERE and the aggregate applies to the filtered set only: D2 members above 4,000, or everyone above 4,500 — each query answers "one number for the selected set."
Worked example — the same SUM, three lenses. With the six-employee table:
SELECT SUM(basic) FROM employee;→ all six rows → ≈ 29,000.SELECT SUM(basic) FROM employee WHERE department_id = 'D2' AND basic > 4000;→ only D2 rows whose basic exceeds 4,000 → the D2 subtotal above the 4,000 cutoff.SELECT SUM(basic) FROM employee WHERE basic > 4500;→ every employee above the 4,500 line → the high-pay subtotal.
The aggregate function and the column never change — only the WHERE clause moves the goalposts. Sense-check: the totals get smaller or equal as the filter tightens, since each query sums a subset of the previous one.
8.10.2 Computed Columns: basic + hra − reductions − tax
A SELECT can also compute new values on the fly by writing arithmetic expressions with the column names as variables:
SELECT number, name, basic + hra - reductions - tax AS total_pay
FROM employee
WHERE department_id = 'D3';
For each selected row, the system computes the expression using that row's values. The AS total_pay renames the computed column in the output — without the alias the column would be headed by the whole expression. In mathematical notation the computed quantity is:
where basic is the base salary, hra the house rent allowance, reductions any deductions, and tax the tax amount — all integers per employee. This is the same table-wide arithmetic the session used for "find the total pay for all the employees whose basic plus HRA is this one": the expression form is reusable with different filters around it.
Columns are variables in a per-row expression. Inside SELECT, the column names behave like variables: for each row, the system substitutes that row's values into the expression and returns the result. basic + hra - reductions - tax is evaluated row by row: take the employee's base salary, add the house rent allowance, subtract the reductions (any deductions) and the tax amount — all integers, so the arithmetic is ordinary integer arithmetic. The result column is headed by the expression unless an alias renames it: AS total_pay labels the computed column in the output, leaving the table itself untouched.
Worked example — a computed column per row. For each D3 employee, the query fetches number, name, and the computed expression. Take a D3 row with basic = 20,000, hra = 2,000, reductions = 500, tax = 2,500:
The row reports total_pay = 19,000 under the alias heading, with the same arithmetic repeated for every D3 row the WHERE clause keeps. Sense-check: adding then subtracting in the expression's order (basic, hra, reductions, tax) reproduces exactly what a salary statement shows — allowance added, deductions and tax taken off.
8.10.3 AVG and Filtered Averages
The average aggregate works like SUM:
SELECT AVG(hra) FROM employee
WHERE department_id = 'D1' AND hra > 1000;
averages the HRA values of department D1 employees whose HRA exceeds 1,000. The similar phrasing "average pay in department D1" averaged the pay figure — the HRA component, or the computed pay depending on the column — restricted to D1 and to HRA above 1,000. The takeaway: every aggregate answers "one number for the selected set," and the WHERE clause defines the set.
AVG: the same skeleton, a different aggregate. AVG is SUM's sibling: it sums the named column's values across the selected rows and divides by the count. SELECT AVG(hra) FROM employee WHERE department_id = 'D1' AND hra > 1000; answers "what is the average HRA among D1 employees whose HRA is above 1,000?" — the WHERE clause first builds the set, then the average is taken over exactly that set. Every aggregate answers "one number for the selected set," and the WHERE clause is what defines the set.
Aggregates collapse a column; expressions compute a row. SUM and AVG answer one number per selected set; arithmetic expressions like basic + hra − reductions − tax answer one number per row. Both share the same pattern — pick the function, name the column, add WHERE — and both prepare the ground for GROUP BY in the next section, which runs aggregates once per group instead of once per table.
8.11 GROUP BY: Department-Wise Aggregation
8.11.1 The GROUP BY Rule
Aggregates over the whole table are one thing; aggregates per department are another. That is what GROUP BY is for — it splits the rows into groups and runs the aggregate once per group:
SELECT department_id, AVG(basic) FROM employee
GROUP BY department_id;
The result has one row per department: the D1 average, the D2 average, the D3 average. The rule to keep straight: any column you write in the SELECT list must be consistent with the grouping. You may select the grouping column itself (here department_id) and aggregate functions of the other columns — that combination is valid. A non-grouped column outside an aggregate function is not valid in the same query. The demonstration of the failure mode: if you remove department_id from the SELECT list and leave only AVG(basic) while keeping GROUP BY department_id, the query errors, because there is no department column in the output to attach the group to. The output columns and the grouping must match.
GROUP BY splits the table, then aggregates each pile. Without GROUP BY, AVG runs once over the whole table. With GROUP BY department_id, the server first sorts the six rows into piles by department — the D1 pile, the D2 pile, the D3 pile — then runs the aggregate once per pile. The result has one row per department: the D1 average, the D2 average, the D3 average. This is the tool for every "X-wise" question — department-wise, city-wise, publisher-wise — wherever the question is "one number per category."
Pitfall — the SELECT list must match the grouping. The GROUP BY rule is the strict one of this section: every column in the SELECT list must either be the grouping column itself or appear inside an aggregate function. SELECT department_id, AVG(basic) ... GROUP BY department_id is valid — department_id is the group, AVG wraps the other column. But remove the grouping column and keep only AVG(basic) while still grouping by department_id, and the query errors: with no department column in the output there is no way to attach each group's average to its department. The output columns and the grouping must match — a non-grouped column outside an aggregate is not allowed.
8.11.2 Department-Wise Average Pay
The worked version of the same idea with the pay expression:
SELECT department_id, AVG(basic + hra - reductions - tax) FROM employee
GROUP BY department_id;
reads as "for each department, the average pay." One row per department, each showing that department's average. GROUP BY is the tool for every "X-wise" question — department-wise, city-wise, publisher-wise — and it pairs with HAVING when the condition itself is about the group (an average above some threshold, for example).
Worked example — department-wise average pay. The six employees are grouped by department. For each department, the pay expression from section 8.10 (basic + hra − reductions − tax) is computed per employee and then averaged. The result:
| department_id | AVG(basic + hra − reductions − tax) |
|---|---|
| D1 | average pay of the D1 employees |
| D2 | average pay of the D2 employees |
| D3 | average pay of the D3 employees |
One row per department, each showing that department's average pay. The query reads naturally as "for each department, the average pay." Sense-check: if D1 has the highest salaries in the table, its average sits above the others — the aggregate is computed inside each group, not across groups.
"X-wise" questions call for GROUP BY. Whenever the question is "for each X, give me one aggregate number," the answer is GROUP BY X with the aggregate in the SELECT list — and the grouping column must stay in the output. HAVING is the partner clause that filters the groups themselves (an average above some threshold, for example), used only together with GROUP BY. Aggregates, grouping, and the nested query of the next section complete the counting toolkit.
8.12 Nested Queries: Comparing Values Against an Average
8.12.1 The Subquery: basic Greater Than AVG(basic)
Problem: find all employees whose basic pay is greater than the average basic pay of all employees. This looks like a chicken-and-egg question — you need the average to filter, and the average needs the data — and the answer is to let one query produce the value that another query uses:
SELECT name, basic FROM employee
WHERE basic > (SELECT AVG(basic) FROM employee);
The inner query in parentheses computes one number — the overall average basic pay. The outer query uses that number as its comparison threshold. Nested queries of this shape are the standard way to "compare each row against a global figure" in SQL, and this is the pattern the session flagged as something students must be able to write and read.
A subquery answers the chicken-and-egg question. The problem "who earns more than the average?" seems circular: you need the average to choose the rows, but the average needs the rows. The resolution is to let one query run first and feed its result into another. The inner query in parentheses — (SELECT AVG(basic) FROM employee) — runs first and collapses the whole table into one number, the overall average basic pay. The outer query then uses that single number as its comparison threshold in the WHERE clause. The shape "compare each row against a global figure" is exactly what this nesting pattern expresses, and the session flagged it as something students must be able to write and read.
8.12.2 Reading the Numbers: 11,583 Then 4,833
The session ran this query twice, around the update that changed one basic from 4,500 to 45,000 and back — and the two runs make a nice illustration of how the subquery result changes with the data.
In the first run, the employee table still held the 45,000 basic from the earlier update. The average basic pay at that point was 11,583, and the query returned exactly one employee — the one whose basic was 45,000. Every other employee's basic lay below the average.
In the second run, after the 45,000 was reverted to 4,500, the average basic pay came out to 4,833 — six employees totalling roughly 29,000, which matches the 4,833 average (4,833 × 6 ≈ 29,000). Now the same query returned three employees: Ramesh, Gautam, and Ram — the three basics above 4,833. The lesson is the same one from the counting section: subqueries recompute against current data, so re-run and re-verify after any update.
Worked example — the subquery tracks the data. With the six employees and their basics:
Run 1 — after the update that set one basic to 45,000. The six basics total roughly 69,500 (the 45,000 plus the other five). The inner query computes:
The outer query keeps every row with basic > 11,583: exactly one employee — the one with basic 45,000. Every other employee's basic lies below the average.
Run 2 — after the revert to 4,500. The total drops back to roughly 29,000, and:
The same query now keeps every row with basic > 4,833: three employees — Ramesh, Gautam, and Ram.
Nothing about the query changed between runs — only the table did. The subquery recomputed against current data, so the threshold moved from 11,583 to 4,833 and the answer moved from one employee to three. Sense-check: 4,833 × 6 ≈ 29,000 and 11,583 × 6 ≈ 69,500 — both averages are consistent with the totals they were computed from.
A subquery is a living number. The nested greater-than-average pattern is the standard way to compare each row against a global figure, and the two runs show why the phrase "the average" is dangerous: the average is whatever the current data says it is. Subqueries recompute against current data, so re-run and re-verify after any update — the same lesson as the counting section, now with the whole query re-deriving its threshold.
Exam Guidance Summary
Exam note: SQL questions are part of the mid-semester examination. The mid-semester scope is the material covered so far: relational algebra, SQL, normalization, ER diagrams, and the basics portion of the course.
Exam note: the query patterns practiced in this session — CREATE TABLE, ALTER TABLE, INSERT, SELECT with WHERE, the comparison operators, IN and NOT IN, BETWEEN and NOT BETWEEN, ORDER BY, COUNT, COUNT DISTINCT, SUM, AVG, GROUP BY, and the nested "greater than average" subquery — are the very important ones: the instructor's own words are that these queries are definitely going to come in the exam, and being comfortable with them secures good marks.
Exam note: the query patterns of this session are the exam's core list: CREATE TABLE, ALTER TABLE, INSERT, SELECT with WHERE, the comparison operators, IN and NOT IN, BETWEEN and NOT BETWEEN, ORDER BY, COUNT, COUNT DISTINCT, SUM, AVG, GROUP BY, and the nested "greater than average" subquery. The instructor's own words: these queries are definitely going to come in the exam, and being comfortable with them secures good marks.
Exam note: the mid-semester has a closed-book component. Do not cram the DDL/DML/DCL/TCL definitions for it; if you have actually worked through SQL, expect application-based questions, questions of significant value, rather than memorized definitions.
Exam note: the next session is a revision session — the plan is to solve last year's question paper and clear doubts. Bring questions; anything unresolved goes into the doubt list and gets taken up while revising.
Exam note: after the mid-semester, assignments and other portions of the course will be discussed.
Exam note: there is a quiz in the course; the standing offer is to type up to 20–25 questions in the chat and get them answered, with classmates encouraged to respond too.
Study advice: the single strongest recommendation of this session is practice — repeat the lab sheets, even if it takes one or two hours. Practicing once more after the session cements the commands in a way that makes them difficult to forget. If you do not have MySQL installed, use an online SQL editor; do not let a missing install stop you from practicing.
Key Industry Applications
Real-world: the four SQL language families — DDL, DML, DCL, TCL — are a standard interview question and a standard verification when a project is delegated to you: a manager needs confidence that the person taking the database work knows what creating, manipulating, controlling, and transacting actually mean.
Real-world: MySQL Workbench is the standard free client for MySQL, and the commands practiced here transfer to most relational database systems (PostgreSQL, MariaDB, and others) with only minor dialect differences.
Real-world: online SQL editors are used for quick experiments, practice, and interview exercises; the same SQL runs there as on a local install.
Real-world: the virtual lab platform used by this course is the same style of environment used in industry training and certification labs — a scheduled, pre-provisioned machine you log into, run your work on, and release.
Real-world: the book-table queries — filtering a product catalog by price range, by publication year, by publisher — are miniature versions of catalog and inventory queries run daily in e-commerce and library systems.
DDA Lecture 8 notes · SQL Basics: Creating Databases, Tables, and Querying
Sections Breakdown
MySQL Workbench and online SQL editors, the five lab modules, and why live practice beats passive watching.
The create-use-create-table ritual, column data types, NULL and NOT NULL, primary keys, and DESCRIBE.
Dropping and re-adding primary keys, composite keys, unique constraints, and the four SQL language families.
Single-row and multi-row inserts, positional value mapping, and how omitted columns become NULL.
The six-clause skeleton, the star operator, column selection, and DISTINCT.
Comparison operators, IN and NOT IN, the closed-interval BETWEEN, the YEAR() function, and ORDER BY.
Four live queries: boundary values, OR versus IN, BETWEEN ranges, and empty results as valid answers.
The employee table, COUNT variants, and why strings take single quotes while numbers do not.
UPDATE ... SET ... WHERE, the safe-update guard, LIMIT 1, and the verify-then-revert habit.
SUM and AVG over selected sets and computed per-row columns aliased with AS.
One aggregate per group, the SELECT-list matching rule, and the role of HAVING.
The subquery pattern, and how the average threshold recomputes as the data changes.
The instructor's exam strategy: which query patterns are definitely coming and how to prepare.
How the session's SQL skills transfer to interviews, other database systems, and real catalog queries.
Exam Revision Notes
Below is the distilled, exam-ready core. Every entry comes from the full explanation above. Use this section for rapid review; return to the main notes when a point needs more context.
The Practice Setup: Tools and the Five Lab Modules
Must-know: Run every command yourself while the walkthrough runs; the platform logistics themselves are not examinable, but the queries practiced here are the queries that appear in the mid-semester exam.
⚠️ Top pitfall: Pressing the wrong execute button in MySQL Workbench (run-from-cursor versus run-selected) causes whole scripts to execute at once.
Self-check: What are the two options for a working SQL environment, and what is identical across both?
Connects to: Creating a Database and Your First Table
Creating a Database and Your First Table
Must-know: The three-step ritual (CREATE DATABASE, USE, CREATE TABLE), the punctuation rules of the column list, and the two primary-key rules: exactly one per table and never NULL.
⚠️ Top pitfall: Trailing comma before the closing parenthesis, or forgetting that a table name is unique — CREATE TABLE IF NOT EXISTS guards the second.
Self-check: Why must a primary key column never be NULL?
Connects to: The Practice Setup: Tools and the Five Lab Modules, Altering an Existing Table
Altering an Existing Table
Must-know: A table holds exactly one primary key, so changing it is always drop-then-add; a composite key enforces uniqueness on the combination; a UNIQUE column allows multiple NULLs, a primary key allows none.
⚠️ Top pitfall: Running the same ALTER twice (a second DROP PRIMARY KEY errors because nothing is left to drop) and hitting 'table already exists' instead of dropping the old table first.
Self-check: How many NULLs may a UNIQUE column hold, and why is that allowed?
Connects to: Creating a Database and Your First Table, Inserting Data with INSERT INTO
Inserting Data with INSERT INTO
Must-know: Values are placed by position against the column list, single-quoted for text and dates, unquoted for numbers; omitted columns become NULL while NOT NULL columns are mandatory.
⚠️ Top pitfall: Putting values in a different order than the column list — the insert succeeds but the data lands in the wrong columns.
Self-check: What happens to a column that is omitted from an INSERT?
Connects to: Creating a Database and Your First Table, The Anatomy of a SELECT Query
The Anatomy of a SELECT Query
Must-know: Square brackets mark optional clauses; only SELECT and FROM are essential; SELECT limits columns while WHERE limits rows; DISTINCT removes duplicate rows.
⚠️ Top pitfall: Trying to memorize all six clauses at once — the session advises learning SELECT and FROM first and adding clauses one at a time with practice.
Self-check: Which two of the six SELECT clauses are essential?
Connects to: Inserting Data with INSERT INTO, Filtering Rows with WHERE
Filtering Rows with WHERE
Must-know: BETWEEN includes both endpoints (the closed interval [10, 25]); YEAR() works only on DATE values; ORDER BY sorts with the first column as primary and later columns breaking ties.
⚠️ Top pitfall: Treating BETWEEN as exclusive of its boundaries — a price of exactly 10 or exactly 25 does qualify.
Self-check: Which rows does `price NOT BETWEEN 10 AND 25` keep?
Connects to: The Anatomy of a SELECT Query, Worked Queries on the book Table
Worked Queries on the book Table
Must-know: The boundary value is the trap: price > 25 excludes the book priced exactly 25, while price >= 25 includes it; IN and BETWEEN are exact shorthands for OR chains and closed intervals, and empty results can be correct answers.
⚠️ Top pitfall: Mixing up strict (>) and inclusive (>=) comparison when the data contains the boundary value itself.
Self-check: Why does `WHERE price > 25` return one row while `WHERE price >= 25` returns two?
Connects to: Filtering Rows with WHERE, The employee Database: Schema, Data, and Counting
The employee Database: Schema, Data, and Counting
Must-know: Plain COUNT counts rows; COUNT(DISTINCT column) counts distinct values — six department IDs versus three distinct IDs; VARCHAR values go in single quotes, INT values do not.
⚠️ Top pitfall: Reading COUNT(column) as a count of distinct values — it counts rows with a value, so repeated IDs are counted each time.
Self-check: Why does COUNT(department_id) return 6 while COUNT(DISTINCT department_id) returns 3?
Connects to: Altering an Existing Table, Updating Data Safely
Updating Data Safely
Must-know: An UPDATE without WHERE changes every row; LIMIT 1 runs with safe updates still on and is the recommended way to cap a single-row update.
⚠️ Top pitfall: Running an UPDATE without a precise WHERE — the safe-update guard rejects it, and without the guard it would rewrite the whole table.
Self-check: Why did the update fail with a safe-update error, and what fix ran successfully?
Connects to: The employee Database: Schema, Data, and Counting, Aggregates and Arithmetic in SELECT
Aggregates and Arithmetic in SELECT
Must-know: Every aggregate answers one number for the selected set; the WHERE clause defines the set. Computed columns like basic + hra - reductions - tax evaluate per row and need AS to be labeled.
⚠️ Top pitfall: Forgetting the alias, so the output column is headed by the whole expression.
Self-check: What single change turns SUM(basic) from a whole-table total into a department subtotal?
Connects to: The employee Database: Schema, Data, and Counting, GROUP BY: Department-Wise Aggregation
GROUP BY: Department-Wise Aggregation
Must-know: GROUP BY runs the aggregate once per group and outputs one row per group; every SELECT column must be the grouping column or an aggregate — removing the group column from the SELECT list errors.
⚠️ Top pitfall: Writing a non-grouped column outside an aggregate in the SELECT list of a GROUP BY query — the query errors.
Self-check: Why does removing department_id from the SELECT list of a GROUP BY department_id query cause an error?
Connects to: Aggregates and Arithmetic in SELECT, Nested Queries: Comparing Values Against an Average
Nested Queries: Comparing Values Against an Average
Must-know: The nested greater-than-average pattern — inner query computes one number, outer query uses it as the threshold — and that subqueries recompute against current data after any update.
⚠️ Top pitfall: Treating an average as fixed — it changes with the data, so re-run and re-verify after any update.
Self-check: Why did the same nested query return one employee in the first run and three in the second?
Connects to: Updating Data Safely, Aggregates and Arithmetic in SELECT
Exam Guidance Summary
Must-know: The session's query patterns — CREATE TABLE through the nested greater-than-average subquery — are definitely coming in the exam; practice them, and expect application-based questions rather than memorized definitions.
⚠️ Top pitfall: Cramming DDL/DML/DCL/TCL definitions for the closed-book exam — the exam expects application-based questions instead.
Self-check: What is the next session's plan, and what should you bring to it?
Connects to: The Practice Setup: Tools and the Five Lab Modules, Nested Queries: Comparing Values Against an Average
Key Industry Applications
Must-know: SQL skills are transferable: the same commands run on PostgreSQL and MariaDB with minor dialect differences, and the practiced patterns are miniature versions of daily e-commerce and library catalog queries.
⚠️ Top pitfall:
Self-check: Where do the book-table filtering patterns appear in industry?
Connects to: The Practice Setup: Tools and the Five Lab Modules, Worked Queries on the book Table
Was this lecture useful?
BitsNotes AI Assistant
Subject Notes AssistantConfigure AI Chat
Choose how to access the chatbotSigned in as
Powered by BitsNotes — 20 messages per day. No API key needed. Want unlimited access? Use "Bring Your Own Key" mode.
Sign in to use AI Chat
Get 20 free AI messages per day to ask questions about your lecture notes. Sign in with Google or GitHub — it takes 5 seconds.
Sign In to BitsNotesSwitch to "Bring Your Own Key" tab above for unlimited access with any OpenAI-compatible provider.