Cyber Town; training data next 100 miles

ICTPRG425 Use structured query language

ICTPRG425In progressUpdated 20 September 2026

The unit as writtenunit scope

This is the official scope of the TAFE unit, kept here (folded) so the unit's intended coverage is visible at a glance and my own notes can be placed against it. The notes below are mine; they follow this scope where it still holds and go past it where current practice has moved on.

Unit: ICTPRG425 Use structured query language. A national unit from the ICT training package, used here as an elective alongside the Certificate IV in Cyber Security. The unit describes the skills and knowledge to write and run Structured Query Language statements against a relational database, from simple retrieval through to creating database objects. No prerequisites, and no licensing or regulatory requirements.

What the unit expects you to be able to do, as nine elements: write a simple statement to retrieve and sort data; write a statement that selectively retrieves data; write statements that use functions; write statements that use aggregation and filtering; write statements that retrieve data from multiple tables; write and run sub-queries; create and manipulate tables; create and use views; and create and use stored procedures.

Required knowledge. Client-server and data-integrity concepts; data-modelling structures and relational database design; database objects, data types, structures and metadata; programming concepts and query-design principles; and the SQL client environment and server architecture.

Assessment conditions. The skills must be shown in a safe environment that reflects the workplace, with access to industry-standard database software and the tools needed to write, run and test SQL.

The delivered version of this unit is taught against an Oracle database using the desktop Oracle SQL Developer application, and it teaches Oracle's SQL dialect and PL/SQL, its procedural extension. My notes keep that as the reference point, because it is what the delivered material and assessments use, and note current practice and the standard forms alongside it where the two differ, particularly on joins, tooling and the security material the unit does not cover at all.

Source: the unit description, elements and knowledge-evidence themes follow the national unit descriptor published at training.gov.au/Training/Details/ICTPRG425, cross-checked against the RMIT course mapping for the unit and the CDU session plans held in the unit folder (dated to the delivered ICTPRG425 offering). Nominal hours are not stated in the sources checked, so none is given here.

What SQL is, and why a security person learns it

Almost every organisation you will ever defend keeps its important information in a relational database, and the language you use to ask that database anything is SQL. Structured Query Language has been the way to talk to relational databases since the 1980s, it is standardised, and it has outlasted a long parade of technologies that were going to replace it. Learning it is one of the safest bets in all of computing, because the thing you learn will still be true in twenty years.

For someone heading into security rather than into database administration, SQL matters for three separate reasons, and it is worth being clear about all three because they pull the subject in slightly different directions.

The first is that the evidence lives in databases. Logs, alerts, user records, transaction histories, the output of a SIEM; when you investigate an incident you are very often querying a store of structured data, and the person who can write a precise query gets to the answer while everyone else is still scrolling. The second is that SQL is itself one of the oldest and most damaging classes of attack. SQL injection has been on the industry's list of top web vulnerabilities for two decades, and you cannot understand it, find it or fix it without understanding how queries are built in the first place. The third is that databases are where the crown jewels sit, so how access to them is granted, limited and audited is a security question in its own right, and the parts of this unit about views and permissions are quietly about exactly that.

So this page teaches the unit's SQL properly, because you need the mechanics, and then spends real time on the two things the unit leaves out: how a query becomes a vulnerability, and how writing SQL has changed now that a model will write it for you. Both of those are the reason a cyber security site teaches a database unit at all.

The relational model: tables, keys and relationships

Before any SQL, the shape of the thing you are querying. A relational database stores data in tables. A table is a grid: each row is one record, one customer or one login event, and each column is one attribute of that record, a name or a timestamp. This sounds obvious until you meet the alternative it replaced.

The alternative is the flat file, a single big table or spreadsheet holding everything. Keep a list of orders in one flat file, repeating the customer's full name and address on every order line, and two problems appear immediately. The same fact is stored many times, wasting space and, far worse, drifting out of agreement when one copy is updated and the others are not. And there is no protection against nonsense; nothing stops an order referring to a customer who does not exist. The relational model solves both by splitting data into separate tables, one per kind of thing, and linking them.

The links are made with keys. A primary key is a column, or set of columns, whose value uniquely identifies each row of a table; a customer number, an employee ID. No two rows share it, and it is never empty. A foreign key is a column in one table that holds the primary key of a row in another table, and that is the link itself. An orders table holds a customer number as a foreign key; that single number points at exactly one row in the customers table, so the customer's details are stored once, in one place, and referred to from wherever they are needed.

Definition

Referential integrity: the guarantee that a foreign key always points at a row that actually exists. The database enforces it: try to add an order for a customer who is not in the customers table, and the database refuses. This is the "data integrity" the unit's knowledge evidence names, and it is the whole point of the relational model; the structure itself makes certain kinds of wrong data impossible.

The other terms the unit uses fit around this. A relationship is the connection two tables have through a key, most often a "one to many"; one customer has many orders. Metadata is data about the data; the names of the tables and columns, their types, their keys and constraints. It is, in effect, the database describing its own shape, and you can query it like anything else.

The design skill underneath all of this is normalisation, the process of splitting data into tables so that each fact is stored once. You do not need its formal rules to pass this unit, but the instinct is worth having: if you find the same piece of information repeated down a column, it probably belongs in a table of its own with a key pointing to it. That instinct is what separates a database from a spreadsheet with ambitions.

The three sublanguages: DDL, DML and DCL

SQL is one language, but its statements fall into groups by what they do, and the unit names the groups early because they organise everything that follows. Knowing which group a statement belongs to also tells you how much damage it can do, which is a useful thing to keep in the front of your mind.

Data Definition Language (DDL) defines the structure: CREATE, ALTER and DROP build, change and remove tables and other objects. DDL changes the shape of the database, not just its contents, so it is the most consequential group; DROP TABLE customers does exactly what it says.

Data Manipulation Language (DML) works with the data inside the structure: SELECT to read, INSERT to add, UPDATE to change and DELETE to remove rows. This is where you spend most of your time, and SELECT, the read, is where this unit spends most of its.

Data Control Language (DCL) governs who may do what: GRANT gives a permission and REVOKE takes it away. DCL is small and it is where the security of a database mostly lives, because it decides which accounts can read which tables and which can change them. A fourth group, Transaction Control Language, COMMIT and ROLLBACK, decides when a set of changes becomes permanent, which is how a database keeps itself consistent when several changes must all succeed or all fail together.

Did you know?

The DDL, DML, DCL split is not academic tidiness; it is a security boundary. A well-run system gives most accounts, and especially the account a website uses to talk to its database, only the DML it needs, usually just SELECT, INSERT and UPDATE on particular tables, and none of the DDL or DCL. This is the principle of least privilege applied to a database, and it is the single control that most reduces the damage a SQL injection attack can do, a point the injection section returns to.

Choosing something to practise on

The unit is taught against Oracle, using the desktop Oracle SQL Developer application to connect to an Oracle database and write queries. That is a sound choice for learning, and it is worth keeping as your reference because it is what the assessments assume; but the tooling here is one of the few parts of the unit that has dated, so it is worth knowing the current picture so you are not confused when what you see does not match the deck.

Oracle's current database is Oracle Database 23ai, the long-term release, and its current SQL development tool is no longer the classic desktop SQL Developer. In early 2024 Oracle released the Oracle SQL Developer Extension for VS Code, and Oracle has since made clear that new features are going into that extension rather than the desktop application; the desktop tool still works and is still downloadable, but it is effectively in maintenance while the VS Code extension is the direction. Alongside it, SQLcl, Oracle's command-line SQL tool, continues and is worth knowing. So if you learn on the desktop SQL Developer because that is what is in front of you, that is fine; just expect the current workplace to be running the VS Code extension or SQLcl instead.

There is a larger point here worth making, because it saves confusion later. SQL is a standard, and the core of it is the same everywhere; the current standard is SQL:2023 (formally ISO/IEC 9075:2023), which among other things added standard ways to query JSON and property-graph data. But every database vendor adds its own extensions on top, and those dialects differ. Oracle's procedural language is PL/SQL; Microsoft SQL Server's is T-SQL; PostgreSQL, MySQL, SQLite and the rest each have their own quirks. A plain SELECT runs almost unchanged on all of them; a date function, a way of limiting rows, or a stored procedure often does not. This matters for you in two ways: what you learn about SELECT, WHERE, joins and grouping transfers everywhere, while the Oracle-specific function names and the PL/SQL procedure syntax later on are Oracle's, and you check the manual when you move.

If you want to practise without an Oracle setup, SQLite is the gentlest option because it is a single file and needs no server, and PostgreSQL is the free, standards-respecting database most worth knowing for real work. One note on the delivered material: the first session builds a small three-table database in Microsoft Access to illustrate keys and relationships. Access is a reasonable teaching prop for seeing tables link together, but it is a desktop product with its own non-standard SQL, and it is not where relational databases live in industry; treat that exercise as a diagram you can click, not as a tool to carry forward.

SELECT: asking a database a question

SELECT is the statement you will use more than all the others put together, because reading data is most of what anyone does with a database. Its simplest form has two parts: what columns you want, and which table they come from.

SELECT first_name, surname
FROM   employees;

SELECT names the columns; FROM names the table; the semicolon ends the statement. Ask for every column with *, as in SELECT * FROM employees, which is handy while exploring but a habit to drop in real queries, because naming the columns you actually want is clearer, faster, and does not silently change when someone adds a column to the table.

Sorting is the other half of the first element, and it is ORDER BY:

SELECT first_name, surname, start_date
FROM   employees
ORDER BY start_date DESC;

ORDER BY sorts the result by one or more columns; ASC sorts ascending and is the default, DESC sorts descending. A point that trips up beginners: ORDER BY only arranges the rows you are shown; it does nothing to the order of the data in the table itself, which has no inherent order at all. A relational table is a set of rows, and if you want them in an order you must ask for it every time.

Did you know?

SQL is a declarative language, and this is the mental shift that makes it click. You describe what you want, not how to get it; you say "give me these columns from this table, sorted this way", and the database's query planner works out the actual steps, which indexes to use and in what order to do the work. This is the opposite of the step-by-step programming in a language like Python. It is also why the same result can be asked for in several different ways, and why writing SQL is often more about stating your question precisely than about instructing the machine.

Filtering rows: WHERE and its operators

A table can hold millions of rows; you almost never want all of them. WHERE is the clause that keeps only the rows matching a condition, and learning its operators is learning to ask precise questions.

SELECT first_name, surname
FROM   employees
WHERE  department = 'Security'
ORDER BY surname;

The condition after WHERE is tested against every row, and only the rows for which it is true come back. The building blocks the unit lists are worth having together. The comparison operators are =, <> or != for "not equal", and < <= > >=. The logical operators AND, OR and NOT join conditions, with the same care needed as in any language: department = 'Security' AND salary > 90000 is stricter than the same two joined with OR, and it is worth using brackets to make the grouping unambiguous when you mix them.

Several operators exist to make common questions readable. BETWEEN tests a range, salary BETWEEN 80000 AND 100000, and NOT BETWEEN inverts it. IN tests membership of a list, department IN ('Security', 'Network', 'Risk'), far cleaner than a string of ORs, and NOT IN inverts it. LIKE matches text patterns using % for any run of characters and _ for a single one, so surname LIKE 'Mc%' finds every surname starting with "Mc". DISTINCT removes duplicate rows from the result, SELECT DISTINCT department FROM employees listing each department once. And aliases, written with AS, rename a column or table in the output, SELECT salary AS annual_pay, which is cosmetic here but becomes genuinely useful once queries get long.

NULL deserves its own paragraph because it is where careful people still slip. NULL is not zero and not an empty string; it is the absence of any value, "we do not know". Because it is unknown, the ordinary operators do not work on it: salary = NULL never matches anything, not even a row whose salary really is null, because "is this unknown value equal to unknown" is itself unknown rather than true. You must test for it with the special IS NULL and IS NOT NULL. Forgetting this is a classic source of quietly wrong results, queries that run cleanly and silently miss the rows they should have caught, which in a security context is the difference between finding the anomalous record and walking straight past it.

Functions: aggregate, numeric, string and date

A function takes one or more values and returns a value, and SQL comes with a large set for computing and reshaping data as you retrieve it. The unit groups them, and the grouping is a good way to hold them.

The aggregate functions are the ones that collapse many rows into a single answer, and they are the ones you will reach for most: COUNT counts rows, SUM totals a column, AVG averages it, and MAX and MIN find the largest and smallest. SELECT COUNT(*) FROM login_events WHERE result = 'FAIL' answers "how many failed logins" in one line, and that shape, counting the rows that match a condition, is the backbone of a great deal of security analysis.

The other groups reshape values row by row rather than collapsing them. Numeric functions like ROUND and POWER, and the arithmetic operators + - * /, do the maths. String functions are the ones a security reader uses constantly, because so much data is text: CONCAT joins strings, LENGTH measures them, UPPER and LOWER change case, and Oracle's SUBSTR and INSTR pull out and locate parts of a string; SUBSTR(ip_address, 1, 3) takes the first three characters, and INSTR finds the position of one string inside another. Date functions handle timestamps, which are everywhere in logs and which are also where dialects differ most, so this is a part of the unit where the exact function names are Oracle's and you should expect to look them up on another system.

Did you know?

COUNT(*) and COUNT(column) are not the same, and the difference is the NULL point again. COUNT(*) counts every row; COUNT(email) counts only the rows where email is not null. So COUNT(*) - COUNT(email) tells you how many rows are missing an email, which is exactly the kind of data-quality question that turns out to matter when you are relying on a field to be filled in.

Grouping and filtering groups: GROUP BY and HAVING

Aggregate functions get far more powerful the moment you combine them with GROUP BY, which is the fourth element and the point where beginners start writing genuinely useful queries. GROUP BY splits the rows into groups that share a value, and then the aggregate functions run once per group instead of once for the whole table.

SELECT   department, COUNT(*) AS staff, AVG(salary) AS avg_pay
FROM     employees
GROUP BY department
ORDER BY staff DESC;

That returns one row per department, with a count and an average for each. The rule to internalise is that every column in the SELECT must either be in the GROUP BY or be inside an aggregate function; you cannot ask for a plain column that has many different values within a group, because there would be no single value to show.

HAVING is the partner of GROUP BY, and the distinction between HAVING and WHERE is a favourite point of confusion worth settling once. WHERE filters individual rows before they are grouped; HAVING filters whole groups after the aggregation. "Departments with more than ten staff" is a question about groups, so it is HAVING COUNT(*) > 10; you cannot put that in WHERE, because at the point WHERE runs the groups do not exist yet.

The clean way to remember all of this is the order the database actually evaluates a query in, which is not the order you write it. You write SELECT ... FROM ... WHERE ... GROUP BY ... HAVING ... ORDER BY, but the database runs FROM first to get the tables, then WHERE to filter rows, then GROUP BY to form groups, then HAVING to filter groups, then SELECT to choose columns, and finally ORDER BY to sort. Once that order is in your head, a great many "why can't I use that alias there" and "why won't it let me filter on the count" questions answer themselves.

flowchart LR
    A[FROM<br/>get the tables] --> B[WHERE<br/>filter rows]
    B --> C[GROUP BY<br/>form groups]
    C --> D[HAVING<br/>filter groups]
    D --> E[SELECT<br/>choose columns]
    E --> F[ORDER BY<br/>sort the result]
Joining tables

Splitting data across tables is what makes a relational database sound, but it means the answer to most real questions lives in more than one table at once, and pulling it back together is the job of the join. This is the fifth element, it is the part of SQL people find hardest, and it is also the part where the delivered material teaches an older form than current practice, so it is worth being careful.

The delivered unit introduces joins first in what it calls "implicit join notation": you list the tables in the FROM clause separated by commas, and state the link as a condition in the WHERE clause.

-- implicit (older) style
SELECT   e.first_name, d.department_name
FROM     employees e, departments d
WHERE    e.department_id = d.department_id;

That works and you must be able to read it, because a lot of existing code, and this unit's own early material, is written that way. But current standard practice, and what you should write in new queries, is the explicit join using the JOIN ... ON syntax, which has been the ANSI standard since 1992:

-- explicit (current standard) style
SELECT   e.first_name, d.department_name
FROM     employees e
JOIN     departments d ON e.department_id = d.department_id;

The two produce the same result, so why does the difference matter enough to spend a paragraph on? Because the explicit form separates the join condition from the row filter. In the explicit style the ON clause says how the tables connect and the WHERE clause says which rows you want, two different jobs kept apart; in the implicit style both are jumbled together in WHERE. The practical consequence is a real safety gain: forget the join condition in the implicit style and you do not get an error, you get a Cartesian product, every row of one table paired with every row of the other, which on two modest tables is a result of millions of nonsense rows. Forget the ON in the explicit style and the database complains. The explicit form makes the dangerous mistake loud instead of silent, which is exactly the property you want, and it is why teaching moved to it.

The kinds of join are the next thing to hold, and the unit covers them. An inner join returns only the rows that match on both sides; an employee with no department, or a department with no employees, simply does not appear. An outer join keeps the unmatched rows too: a LEFT OUTER JOIN keeps every row from the left table even where it has no match on the right, filling the missing side with NULL; a RIGHT OUTER JOIN does the same for the right table; and a FULL OUTER JOIN keeps unmatched rows from both. The difference is not academic. "Which employees are not assigned to any department" is an outer-join question, because an inner join would hide exactly the rows you are looking for; and in security work the interesting record is very often the one that does not match, the login with no matching user, the device with no matching owner.

Two more multi-table tools the unit names. A subquery is a query inside another query, most often in the WHERE clause, that computes a value or a set of values the outer query then uses: WHERE department_id IN (SELECT department_id FROM departments WHERE location = 'Darwin') finds employees in Darwin departments without naming those departments yourself. And a union stacks the results of two queries on top of each other into one list, which is useful for combining like-shaped results from different sources; it is also, not coincidentally, the mechanism behind one of the classic SQL injection techniques, which the injection section returns to.

Building and changing tables

So far the queries have read data that was already there. The seventh element crosses from reading to building; from DML into DDL, from asking questions to changing the database itself. This is more consequential work, and the care it needs is proportionate.

A table is created by naming its columns and giving each a data type, which fixes what kind of value the column can hold:

CREATE TABLE employees (
    employee_id  NUMBER       PRIMARY KEY,
    first_name   VARCHAR2(50) NOT NULL,
    surname      VARCHAR2(50) NOT NULL,
    salary       NUMBER(8,2),
    start_date   DATE
);

The type is the first line of data integrity: a DATE column will not accept "banana", a NUMBER will not accept text. The constraints are the second line, and they are the database enforcing the rules of the business. PRIMARY KEY marks the unique identifier; NOT NULL forbids an empty value; UNIQUE forbids duplicates; FOREIGN KEY ties a column to another table's key and enforces the referential integrity from earlier; and CHECK enforces a rule of your own, CHECK (salary > 0). Constraints are worth loving rather than resenting, because a rule enforced by the database holds no matter which program, script or person is writing the data, where a rule enforced only in application code holds only until someone writes to the database another way.

The statements that change a table round out the element. INSERT adds rows; UPDATE changes values in existing rows; DELETE removes rows; ALTER TABLE changes the structure, adding or modifying a column; DROP TABLE deletes the table and everything in it; and DESC (short for describe) shows a table's structure, which is you querying its metadata.

Did you know?

UPDATE and DELETE without a WHERE clause apply to every row in the table. DELETE FROM employees empties the table; UPDATE employees SET salary = 0 sets everyone's salary to zero. There is no confirmation and, unless you are inside an uncommitted transaction, no undo. This is the origin of one of the most repeated pieces of professional advice about databases: write the WHERE clause first, or write your UPDATE as a SELECT first to see exactly which rows it will touch, and only then change the verb. The habit of never running a change you have not first read back as a query is worth building the very first time you write one.

Views

A view is a stored query that behaves like a table. You define it once with a SELECT, give it a name, and from then on anyone can query the view as though it were a real table, while the database runs the underlying query each time. Nothing is copied; a view is a saved question, a virtual table, not a second store of the data.

CREATE VIEW security_staff AS
SELECT employee_id, first_name, surname, start_date
FROM   employees
WHERE  department = 'Security';

Views earn their place for three reasons, and the third is why they sit on a security site. They simplify: a complicated join across four tables can be wrapped in a view and then queried as one simple name, so the hard query is written once and reused by people who never see its complexity. They present a stable shape: if the tables underneath are reorganised, the view can be rewritten to keep giving the same columns, so queries built on it keep working. And they restrict, which is the security use: a view can expose just some columns and just some rows of a sensitive table, and an account can be granted access to the view while being denied access to the table behind it. So a support account might see a customers_contact view with names and phone numbers, and never be able to reach the columns holding payment details at all. That is least privilege expressed in SQL, and it is a genuinely useful pattern to recognise when you are assessing how an organisation protects the data in its databases.

Programmable SQL: procedures and triggers

The last element crosses from single statements into small programs that live inside the database. This is where SQL stops being purely declarative and gains the loops, conditions and variables of a normal programming language, and it is squarely in the vendor-specific part of the subject: the unit teaches Oracle's PL/SQL, and the equivalents on other systems (T-SQL on SQL Server, PL/pgSQL on PostgreSQL) do the same jobs with different syntax.

A stored procedure is a named block of SQL and procedural code, stored in the database and run on demand. It has a header, which names it and lists its parameters, and a body, which holds the logic. Parameters come in three kinds: IN passes a value into the procedure, OUT passes a value back out, and IN OUT does both.

CREATE PROCEDURE greet (name IN VARCHAR2) AS
BEGIN
    dbms_output.put_line('Hello ' || name);
END;

You run it by calling it, and you remove it with DROP PROCEDURE. Procedures matter for the same reasons functions matter in any language; they put a piece of logic in one named place, so it is written once, tested once and reused, and a change is made in one place. They have a security dimension too: because a procedure can be granted to an account that is not allowed to touch the underlying tables directly, procedures are one way to let an application do exactly the operations it needs and nothing more.

A trigger is a block of code that the database runs automatically when a particular event happens to a table, an INSERT, UPDATE or DELETE. You do not call a trigger; it fires on its own when its event occurs. It can be set to run BEFORE or AFTER the event, and to fire once per affected row (a row-level trigger) or once per statement (a statement-level trigger), with the REFERENCING clause giving the code access to the old and new versions of the row.

Did you know?

Triggers are how a database keeps an audit trail of itself, which is why they belong on a security reading list. A trigger that fires after every UPDATE or DELETE on a sensitive table, writing who changed what and when into a separate log table, produces a tamper-evident record of activity that the application cannot forget to write, because it happens in the database regardless of which application did the change. The same mechanism that enforces a business rule automatically also enforces accountability automatically, and that is a large part of what makes an organisation's data trustworthy after an incident.

SQL injection: the security section this unit does not have

This section is not in the unit, and for a cyber security reader it is the reason the unit is on the site at all. You cannot understand, find or fix the most famous database attack without understanding how queries are built, and having built queries through this whole page, you are now in exactly the position to see how one becomes a weapon.

SQL injection happens when a program builds a SQL query by gluing untrusted input straight into the query text as a string. Picture a login form. The application takes the username and password the user typed and builds a query to check them, by joining the typed values into the query with concatenation, the string-building you met in the functions section:

-- how NOT to build a query
"SELECT * FROM users WHERE username = '" + user_input + "' AND password = '" + pw_input + "'"

Now think about what happens if the user does not type a username, but types this into the box: ' OR '1'='1. The application dutifully glues it in, and the query the database receives becomes:

SELECT * FROM users WHERE username = '' OR '1'='1' AND password = '...'

'1'='1' is always true, so the WHERE clause is now satisfied for every row, and the attacker is logged in, often as the first user in the table, which is frequently an administrator. The input was not treated as a value; it was treated as part of the query, and so the user got to rewrite the query. From that same flaw an attacker can read data they should never see, using a UNION to append a query of their own onto the results, or in the worst cases change or destroy data, because the injected text can carry any SQL the database account is permitted to run. That last clause is why the least-privilege point from the sublanguages section matters so much; injection into an account that can only SELECT is bad, injection into an account that can DROP TABLE is a catastrophe.

The fix is not to hunt for bad characters, which attackers evade endlessly; it is structural, and it is the single most important thing on this page. Use parameterised queries, also called prepared statements. Instead of building the query as a string, you write the query once with placeholders where the values go, and hand the values to the database separately:

-- the safe way: the query and the values travel apart
SELECT * FROM users WHERE username = ? AND password = ?

The database receives the query structure first and the values second, and it never treats the values as part of the query. Typing ' OR '1'='1 into a parameterised query just looks for a user whose name is literally the text ' OR '1'='1, finds none, and denies the login. The data and the code travel in separate lanes and never mix. This is the same principle as the "keep data and code apart" note in the string-handling material; injection is what happens when they are allowed to mix, and parameterisation is the discipline of keeping them apart.

flowchart TD
    subgraph unsafe [Building the query as a string]
        A1[User input] --> A2[Glued into the query text]
        A2 --> A3[Database reads input<br/>as part of the query]
        A3 --> A4[Attacker rewrites the query]
    end
    subgraph safe [Parameterised query]
        B1[Query with placeholders] --> B3[Database plans the query]
        B2[User input as values] --> B4[Values slotted in,<br/>never as query text]
        B3 --> B4
        B4 --> B5[Input can only ever be data]
    end

Three supporting defences sit around parameterisation, none of them a substitute for it. Least privilege, so the account the application uses can do only what it must, containing the blast radius if injection does occur. Input validation as defence in depth, checking that input is the shape you expected, which reduces the attack surface but must never be your only defence because validation can be bypassed and parameterisation cannot. And using well-built libraries, an ORM (object-relational mapper) or a query builder, which parameterise by default so that the safe path is the default path; though even these can be misused if you drop back to raw string-built queries inside them.

For placing this in the wider security picture: injection sits at A05 in the OWASP Top 10 of 2025, the industry's reference list of web application risks. It is worth noting that it has slipped down the list; it was A03 in 2021 and held the top spot for years before that. That fall is a genuine sign of progress, because frameworks that parameterise by default have made the naive mistake harder to make by accident. It has not gone away; injection flaws remain among the most common and most damaging when they occur, and the newer entries above it on the 2025 list, led by broken access control and by software supply-chain failures, are simply now more prevalent still. Twenty years on, the humble parameterised query remains one of the highest-value habits a person who touches databases can have.

SQL in the age of AI: text-to-SQL and generated queries

The other thing the unit could not have anticipated is that a large language model will now write your SQL for you, and this changes the day-to-day of working with databases enough to be worth a clear-eyed section, particularly for a reader who will be assessing the risk of these tools rather than only using them.

The helpful part is real. Describe what you want in plain English and a model will produce a query, a capability called text-to-SQL; it is built into coding assistants, into database tools, and increasingly into the analytics dashboards where non-technical staff ask questions of company data without writing a line themselves. For learning, a model is a patient tutor that will explain what a join does or why a query is slow. For working, it turns the blank-page problem of a complicated query into an editing problem, which is easier.

The risks are the same shape as with any AI-generated code, sharpened by the fact that SQL runs against real data and some of it changes or deletes that data. Four are worth naming.

Generated queries are confidently wrong. A model will produce a query that runs and returns rows, but joins on the wrong column, misunderstands a NULL, or quietly filters out records it should have kept, and the result looks perfectly plausible. Against a security question, a subtly wrong query is worse than no query, because it produces an answer you trust and act on. The discipline is unchanged from the rest of this page: you own the query, so you read it, you understand every clause, and against anything that matters you check it the way the joins section suggested, by reading back which rows it will actually touch.

Generated changes are dangerous in a way generated reads are not. A model asked to "clean up the old records" can produce a DELETE or an UPDATE that does far more than intended, and run against a production database that is irreversible. Never run model-generated DDL or DML against real data without reading it first, and keep the account you explore with read-only so that a bad suggestion cannot do damage even if you run it without thinking.

Hallucinated schema is a specific trap. A model that does not actually know your database will invent plausible table and column names, so a query that assumes a users.is_admin column that does not exist wastes your time at best and, if a similarly named column does exist, misleads you at worst. Always check a generated query against the real structure with DESC before trusting it.

And there is a new risk that only exists because these tools read data as well as write it. Where a model reads from a database and acts on what it finds, the contents of the database become untrusted input to the model, and a malicious value sitting in a row, a crafted piece of text in a comment field, can carry instructions that the model follows. This is prompt injection reaching into the database layer, the same family of problem as SQL injection one level up: untrusted data being treated as instructions. The mitigations rhyme with everything above; keep the model's database access least-privileged and read-only where possible, treat data flowing out of the database to a model as untrusted, and keep a human deciding on any action that changes data or matters. The pattern of this whole page holds here too: the tool changes how fast the SQL arrives, not whose job it is to make sure the SQL is right and safe before it runs.

Sources used

The unit's scope, elements and knowledge-evidence themes follow the national unit descriptor for ICTPRG425 published at training.gov.au, cross-checked against the RMIT course mapping for the unit and the CDU session plans held in the unit folder; the training.gov.au unit page is JavaScript-rendered and was read via its mirror, and nominal hours were not stated in the sources checked. The tooling picture, Oracle Database 23ai, the Oracle SQL Developer Extension for VS Code as the current direction with the desktop application in maintenance, and SQLcl, is from Oracle's SQL Developer pages at oracle.com/database/sqldeveloper/vscode and Oracle product documentation, read September 2026. The current SQL standard is SQL:2023 (ISO/IEC 9075:2023); the general availability announcement is on the Oracle SQL blog and the standard is catalogued by ISO. The SQL injection material follows the OWASP guidance, with the risk ranking from the OWASP Top 10:2025 (injection at A05) compared with the 2021 edition (injection at A03); OWASP's SQL Injection Prevention Cheat Sheet is the fuller reference for the defences described. The AI and prompt-injection material reflects OWASP's work on large-language-model risks and current practice as at September 2026; where this page describes practice as still moving, particularly the AI sections, it is written as the live picture it is and is worth re-checking against current sources before it is relied on.