SQL — Structured Query Language Grade 12
SQL (pronounced "S-Q-L" or "sequel") is the language used to communicate with a database. It lets you retrieve, add, update and delete records. In Delphi, SQL is added to a TADOQuery component.
Think of a database as a massive spreadsheet with thousands of rows. SQL is how you ask the database a question: "Give me all students in Grade 12 with a mark above 80, sorted by surname." Without SQL, you would have to scroll through every row manually.
Anatomy of a SQL Statement
Using SQL in Delphi
SQL goes into the SQL property of a TADOQuery component. Always close the query before changing the SQL, then open it again.
// Method 1: assign as one string
qryStudents.Close;
qryStudents.SQL.Text := 'SELECT * FROM tblStudents WHERE Grade = 12';
qryStudents.Open;
// Method 2: build line-by-line (easier to read for long queries)
qryStudents.Close;
qryStudents.SQL.Clear;
qryStudents.SQL.Add('SELECT Name, Surname, Mark');
qryStudents.SQL.Add('FROM tblStudents');
qryStudents.SQL.Add('WHERE Grade = 12');
qryStudents.SQL.Add('ORDER BY Surname ASC');
qryStudents.Open;SELECT — Choosing What to Display
SELECT * FROM tblCD; -- all fields, all records
SELECT CDName, Artist FROM tblCD; -- only these two fields
SELECT DISTINCT Genre FROM tblCD; -- each genre shown once only
SELECT TOP 5 * FROM tblCD; -- first 5 records onlyWHERE — Filtering Records
The WHERE clause is how you filter — like asking "show me only the records that match this condition."
WHERE Genre = "Rock" -- exact match (text in double quotes)
WHERE Price < 100 -- numbers (no quotes)
WHERE Grade <> 11 -- not equal
WHERE Mark >= 50 -- greater than or equal
WHERE Genre = "Pop" AND Price < 100 -- both must be true
WHERE Genre = "Pop" OR Genre = "Rock" -- either can be trueOperator Precedence in Compound Conditions
When a WHERE clause mixes AND and OR, SQL doesn't just evaluate left to right — it follows a fixed order of precedence, same as in maths:
| Order | Operator(s) |
|---|---|
| 1 (first) | Parentheses ( ) |
| 2 | Multiplication, division |
| 3 | Subtraction, addition |
| 4 | NOT |
| 5 | AND |
| 6 (last) | OR |
AND is evaluated before OR, so a mixed condition may not filter the way you expect unless you add brackets to force the order you want.
WHERE Genre = "Pop" OR Genre = "Rock" AND Price < 100
-- reads as: Genre = "Pop" OR (Genre = "Rock" AND Price < 100)
-- ALL Pop CDs are included, regardless of price!
-- To make BOTH genres respect the price limit, use brackets:
WHERE (Genre = "Pop" OR Genre = "Rock") AND Price < 100You don't have to memorise the precedence table if you always wrap OR conditions in brackets when combining them with AND. Brackets make your intention explicit and remove any ambiguity — for the reader and for the database.
Special WHERE Operators
| Operator | Meaning | Example |
|---|---|---|
LIKE | Pattern match. % = any chars, _ = one char | WHERE Name LIKE "S%" — starts with S |
IN | Match any value in a list | WHERE Genre IN ("Rock","Pop","Jazz") |
BETWEEN | Within a range (inclusive for numbers) | WHERE Price BETWEEN 50 AND 150 |
IS NULL | Field is empty/blank | WHERE CellNo IS NULL |
IS NOT NULL | Field has a value | WHERE CellNo IS NOT NULL |
#date# | Date values use # symbols | WHERE OrderDate = #2024-05-01# |
Access SQL can misinterpret a date written as dd/mm/yyyy, reading it as month/day/year instead. Writing the year first removes the ambiguity: prefer #2024/05/01# (yyyy/mm/dd) over #01/05/2024#.
WHERE CDName LIKE "S%" -- starts with S
WHERE CDName LIKE "%love%" -- contains the word "love"
WHERE CDName LIKE "%s" -- ends with s
WHERE Code LIKE "A__" -- starts with A followed by exactly 2 charsORDER BY — Sorting Results
SELECT * FROM tblCD ORDER BY Artist; -- ascending (default)
SELECT * FROM tblCD ORDER BY Price DESC; -- most expensive first
SELECT * FROM tblCD ORDER BY Genre, CDName ASC; -- sort by genre, then nameCalculated Fields & Aliases
You can do maths inside a SELECT and give the result a name using AS.
SELECT Name, Price, Price * 1.15 AS PriceWithVAT FROM tblProducts;
SELECT Name, Mark, ROUND(Mark / 2, 0) AS HalfMark FROM tblStudents;Aggregate Functions — Calculating Summaries
| Function | What it calculates | Example |
|---|---|---|
COUNT(*) | Number of records | SELECT COUNT(*) FROM tblStudents |
SUM(field) | Total of numeric field | SELECT SUM(Mark) FROM tblResults |
AVG(field) | Average of numeric field | SELECT AVG(Mark) FROM tblResults |
MIN(field) | Lowest value | SELECT MIN(Price) FROM tblProducts |
MAX(field) | Highest value | SELECT MAX(Price) FROM tblProducts |
-- Count students per grade
SELECT Grade, COUNT(*) AS Total
FROM tblStudents
GROUP BY Grade;
-- Only show groups with more than 10 students
SELECT Grade, COUNT(*) AS Total
FROM tblStudents
GROUP BY Grade
HAVING COUNT(*) > 10;WHERE filters individual records before grouping. HAVING filters groups after GROUP BY. If you have both, WHERE runs first.
SQL String & Math Functions
| Function | Returns | Example |
|---|---|---|
LEN(field) | Character count | SELECT LEN(Name) FROM tblStudents |
LEFT(field, n) | First n characters | LEFT(Surname, 3) → first 3 letters |
RIGHT(field, n) | Last n characters | RIGHT(IDNo, 4) |
MID(field, start, n) | Substring from position | MID(IDNo, 3, 2) → month of birth (position 1–2 is the year) |
ROUND(field, dec) | Rounded value | ROUND(Avg, 2) → 2 decimal places |
INT(field) | Integer part (no decimal) | INT(75.8) = 75 |
FORMAT(field, "0.00") | Formatted string | FORMAT(Price, "R0.00") |
Date Functions
SELECT Name, YEAR(BirthDate) AS BirthYear FROM tblStudents;
SELECT * FROM tblOrders WHERE MONTH(OrderDate) = 12; -- December orders
SELECT * FROM tblStudents WHERE YEAR(BirthDate) = 2008;INSERT, UPDATE, DELETE
-- Add a new record
INSERT INTO tblStudents (Name, Surname, Grade)
VALUES ("Alice", "Smith", 12);
-- Change existing data
UPDATE tblStudents
SET Grade = 12
WHERE StudentID = 45;
-- Remove a record
DELETE FROM tblStudents
WHERE StudentID = 45;Without a WHERE clause, UPDATE changes every record and DELETE removes every record in the table. Always double-check your condition.
Building an INSERT String with QuotedStr
Instead of a parameterised query, you will sometimes build the whole SQL statement as one string by joining Delphi variables onto it directly. Text values need quotes around them in the final SQL string — the QuotedStr function wraps a string value in quotes for you, so you don't have to type them by hand.
qryStudents.Close;
qryStudents.SQL.Text := 'INSERT INTO tblStudents (Name, Surname) VALUES (' +
QuotedStr(edtName.Text) + ', ' + QuotedStr(edtSurname.Text) + ')';
qryStudents.ExecSQL; // use ExecSQL (not Open) for INSERT/UPDATE/DELETE — they don't return recordsQuotedStr is a quick way to build a query from a string variable, but a parameterised query (below) is the safer, preferred technique — it avoids manually matching quotes and is more resistant to SQL injection.
Parameterised Queries
Instead of hardcoding a value, use a parameter that is filled in by the user at runtime.
qryStudents.Close;
qryStudents.SQL.Clear;
qryStudents.SQL.Add('SELECT * FROM tblStudents');
qryStudents.SQL.Add('WHERE Grade = :pGrade'); // :pGrade is the parameter
qryStudents.Parameters.ParamByName('pGrade').Value := sedGrade.Value;
qryStudents.Open;SQL in Delphi — Reading Field Values
// Read a field value from the current record
sName := qryStudents['Name'];
iGrade := qryStudents['Grade'];
rMark := qryStudents['Average'];
// Loop through all records
qryStudents.First;
while not qryStudents.Eof do
begin
memOut.Lines.Add(qryStudents['Name'] + ' ' + qryStudents['Surname']);
qryStudents.Next;
end;Worked Example — A Search Screen
A classic Paper 1 task: the user types (part of) a surname into edtSearch and clicks a button. The program must build a parameterised query from that input, run it, list every match in memOut, and report how many were found. This combines user input, SQL, looping a dataset and string output.
procedure TfrmMain.btnSearchClick(Sender: TObject);
var
iFound : integer;
begin
memOut.Clear;
iFound := 0;
// 1. Build a parameterised query from the user's input
qryStudents.Close;
qryStudents.SQL.Clear;
qryStudents.SQL.Add('SELECT Name, Surname, Grade, Average');
qryStudents.SQL.Add('FROM tblStudents');
qryStudents.SQL.Add('WHERE Surname LIKE :pName');
qryStudents.SQL.Add('ORDER BY Surname ASC');
// add a wildcard so 'coe' matches 'Coetzee'
qryStudents.Parameters.ParamByName('pName').Value := edtSearch.Text + '%';
qryStudents.Open;
// 2. Loop through the result set and display each record
qryStudents.First;
while not qryStudents.Eof do
begin
memOut.Lines.Add(qryStudents['Name'] + ' ' + qryStudents['Surname']
+ ' (Gr ' + IntToStr(qryStudents['Grade'])
+ ', ' + FloatToStrF(qryStudents['Average'], ffFixed, 4, 1) + '%)');
Inc(iFound);
qryStudents.Next;
end;
// 3. Report how many were found
if iFound = 0 then
ShowMessage('No learner found starting with "' + edtSearch.Text + '".')
else
lblCount.Caption := IntToStr(iFound) + ' learner(s) found';
end;Using :pName and ParamByName is safer and cleaner than gluing the user's text straight into the SQL string — it avoids quote/type errors and blocks SQL injection (see Cybercrime). The wildcard '%' is added to the value, so a partial surname still matches.