Direct SQL

You can work with Nette Database in two ways: by writing SQL queries directly (Direct Access) or by letting SQL be generated automatically (Explorer Access). Direct Access allows you to safely build queries while keeping full control over their structure.

For information on creating a connection and configuring it, see the separate page.

Basic Querying

The query() method executes database queries and returns a ResultSet object representing the result. If the query fails, the method throws an exception. You can loop through the query result using a foreach loop or use one of the helper functions.

$result = $database->query('SELECT * FROM users');

foreach ($result as $row) {
	echo $row->id;
	echo $row->name;
}

To safely insert values into SQL queries, use parameterized queries. Nette Database makes this very straightforward: just append a comma and the value to the SQL query.

$database->query('SELECT * FROM users WHERE name = ?', $name);

For multiple parameters, you can either interleave the SQL query with parameters:

$database->query('SELECT * FROM users WHERE name = ?', $name, 'AND age > ?', $age);

Or write the entire SQL query first and then append all parameters:

$database->query('SELECT * FROM users WHERE name = ? AND age > ?', $name, $age);

Protection Against SQL Injection

Why is it important to use parameterized queries? Because they protect you from SQL injection attacks, where attackers can inject malicious SQL commands to manipulate or access database data.

Never insert variables directly into an SQL query! Always use parameterized queries to protect yourself against SQL injection.

// ❌ UNSAFE CODE - vulnerable to SQL injection
$database->query("SELECT * FROM users WHERE name = '$name'");

// ✅ Safe parameterized query
$database->query('SELECT * FROM users WHERE name = ?', $name);

Be sure to familiarize yourself with potential security risks.

Query Techniques

WHERE Conditions

You can write WHERE conditions as an associative array, where the keys are column names and the values are the data to compare. Nette Database automatically selects the most appropriate SQL operator based on the value type.

$database->query('SELECT * FROM users WHERE', [
	'name' => 'John',
	'active' => true,
]);
// WHERE `name` = 'John' AND `active` = 1

You can also explicitly specify the operator in the key:

$database->query('SELECT * FROM users WHERE', [
	'age >' => 25,           // uses the > operator
	'name LIKE' => '%John%', // uses the LIKE operator
	'email NOT LIKE' => '%example.com%', // uses the NOT LIKE operator
]);
// WHERE `age` > 25 AND `name` LIKE '%John%' AND `email` NOT LIKE '%example.com%'

Special cases like null values or arrays are handled automatically:

$database->query('SELECT * FROM products WHERE', [
	'name' => 'Laptop',         // uses the = operator
	'category_id' => [1, 2, 3], // uses IN
	'description' => null,      // uses IS NULL
]);
// WHERE `name` = 'Laptop' AND `category_id` IN (1, 2, 3) AND `description` IS NULL

For negative conditions, use the NOT operator:

$database->query('SELECT * FROM products WHERE', [
	'name NOT' => 'Laptop',         // uses the <> operator
	'category_id NOT' => [1, 2, 3], // uses NOT IN
	'description NOT' => null,      // uses IS NOT NULL
	'id' => [],                     // skipped
]);
// WHERE `name` <> 'Laptop' AND `category_id` NOT IN (1, 2, 3) AND `description` IS NOT NULL

By default, conditions are combined using the AND operator. You can change this behavior using the placeholder ?or.

ORDER BY Rules

The ORDER BY clause can be defined as an array, where keys represent columns and values are booleans indicating ascending order:

$database->query('SELECT id FROM author ORDER BY', [
	'id' => true,  // ascending
	'name' => false, // descending
]);
// SELECT id FROM author ORDER BY `id`, `name` DESC

Inserting Data (INSERT)

To insert records, use the SQL INSERT statement.

$values = [
	'name' => 'John Doe',
	'email' => 'john@example.com',
];
$database->query('INSERT INTO users ?', $values);
$userId = $database->getInsertId();

The getInsertId() method returns the ID of the last inserted row. For certain databases (e.g., PostgreSQL), you must specify the sequence name using $database->getInsertId($sequenceId).

You can also pass special values, such as files, DateTime objects, or enum types, as parameters.

Inserting multiple records at once:

$database->query('INSERT INTO users ?', [
	['name' => 'User 1', 'email' => 'user1@mail.com'],
	['name' => 'User 2', 'email' => 'user2@mail.com'],
]);

Performing a batch INSERT is much faster because only a single database query is executed instead of multiple individual queries.

Security Note: Never use unvalidated data as $values. Familiarize yourself with possible risks.

Updating Data (UPDATE)

To update records, use the SQL UPDATE statement.

// Update a single record
$values = [
	'name' => 'John Smith',
];
$result = $database->query('UPDATE users SET ? WHERE id = ?', $values, 1);

You can check the number of affected rows using $result->getRowCount().

You can use the += and -= operators in UPDATE:

$database->query('UPDATE users SET ? WHERE id = ?', [
	'login_count+=' => 1, // increment login_count
], 1);

To insert or update a record if it already exists, use the ON DUPLICATE KEY UPDATE technique:

$values = [
	'name' => $name,
	'year' => $year,
];
$database->query('INSERT INTO users ? ON DUPLICATE KEY UPDATE ?',
	$values + ['id' => $id],
	$values,
);
// INSERT INTO users (`id`, `name`, `year`) VALUES (123, 'Jim', 1978)
//   ON DUPLICATE KEY UPDATE `name` = 'Jim', `year` = 1978

Note that Nette Database recognizes the context of the SQL command in which a parameter with an array is used and generates the SQL code accordingly. For example, it constructed (id, name, year) VALUES (123, 'Jim', 1978) from the first array, while it converted the second into name = 'Jim', year = 1978. This is covered in more detail in the section Hints for constructing SQL.

Deleting Data (DELETE)

To delete records, use the SQL DELETE statement. Example with the number of deleted rows:

$count = $database->query('DELETE FROM users WHERE id = ?', 1)
	->getRowCount();

SQL Construction Hints

SQL placeholders allow you to control how parameter values are incorporated into SQL expressions:

Hint Description Automatically Used For
?name Used for table or column names
?values Generates (key, ...) VALUES (value, ...) INSERT ... ?, REPLACE ... ?
?set Generates assignments key = value, ... SET ?, KEY UPDATE ?
?and Joins conditions in an array with AND WHERE ?, HAVING ?
?or Joins conditions in an array with OR
?order Generates the ORDER BY clause ORDER BY ?, GROUP BY ?

For dynamically inserting table or column names, use the ?name placeholder. Nette Database ensures proper escaping according to the database's conventions (e.g., enclosing in backticks for MySQL).

$table = 'users';
$column = 'name';
$database->query('SELECT ?name FROM ?name WHERE id = 1', $column, $table);
// SELECT `name` FROM `users` WHERE id = 1 (in MySQL)

Warning: Only use the ?name placeholder for validated table and column names. Otherwise, you risk security vulnerabilities.

Other hints are usually not necessary to specify, as Nette uses smart auto-detection when constructing SQL queries (see the third column of the table). However, you can use them in situations where you want to combine conditions using OR instead of AND:

$database->query('SELECT * FROM users WHERE ?or', [
	'name' => 'John',
	'email' => 'john@example.com',
]);
// SELECT * FROM users WHERE `name` = 'John' OR `email` = 'john@example.com'

Special Values

In addition to standard scalar types (e.g., string, int, bool), you can also pass special values as parameters:

  • Files: Use fopen('file.png', 'r') to insert the binary content of a file.
  • Date and Time: DateTime objects are automatically converted to the database's date format.
  • Enum Values: Instances of enum are converted to their corresponding values.
  • SQL Literals: Created using Connection::literal('NOW()'), these are inserted directly into the query.
$database->query('INSERT INTO articles ?', [
	'title' => 'My Article',
	'published_at' => new DateTime,
	'content' => fopen('image.png', 'r'),
	'state' => Status::Draft,
]);

For databases that lack native support for the datetime type (e.g., SQLite and Oracle), DateTime values are converted according to the formatDateTime configuration option (default: U for Unix timestamp).

SQL Literals

In some cases, you may need to insert raw SQL code as a value without treating it as a string or escaping it. For this, use objects of the Nette\Database\SqlLiteral class, which can be created using the Connection::literal() method.

$result = $database->query('SELECT * FROM users WHERE', [
	'name' => $name,
	'year >' => $database::literal('YEAR()'),
]);
// SELECT * FROM users WHERE (`name` = 'Jim') AND (`year` > YEAR())

Alternatively:

$result = $database->query('SELECT * FROM users WHERE', [
	'name' => $name,
	$database::literal('year > YEAR()'),
]);
// SELECT * FROM users WHERE (`name` = 'Jim') AND (year > YEAR())

SQL literals can also contain parameters:

$result = $database->query('SELECT * FROM users WHERE', [
	'name' => $name,
	$database::literal('year > ? AND year < ?', $min, $max),
]);
// SELECT * FROM users WHERE `name` = 'Jim' AND (year > 1978 AND year < 2017)

This allows for flexible combinations:

$result = $database->query('SELECT * FROM users WHERE', [
	'name' => $name,
	$database::literal('?or', [
		'active' => true,
		'role' => $role,
	]),
]);
// SELECT * FROM users WHERE `name` = 'Jim' AND (`active` = 1 OR `role` = 'admin')

Fetching Data

Shortcuts for SELECT Queries

To simplify data retrieval, the Connection class provides several shortcuts that combine a query() call with a subsequent fetch*() call. These methods accept the same parameters as query(), i.e., an SQL query and optional parameters. A detailed description of the fetch*() methods can be found below.

fetch($sql, ...$params): ?Row Executes the query and fetches the first row as a Row object.
fetchAll($sql, ...$params): array Executes the query and fetches all rows as an array of Row objects.
fetchPairs($sql, ...$params): array Executes the query and fetches an associative array where the first column is the key and the second is the value.
fetchField($sql, ...$params): mixed Executes the query and fetches the value of the first cell in the first row.
fetchList($sql, ...$params): ?array Executes the query and fetches the first row as an indexed array.

Example:

// fetchField() - returns the value of the first cell
$count = $database->query('SELECT COUNT(*) FROM articles')
	->fetchField();

foreach – Iterating Over Rows

After executing a query, a ResultSet object is returned, which allows you to iterate over the results in various ways. The simplest and most memory-efficient way to fetch rows is by iterating in a foreach loop. This method processes rows one at a time and avoids storing all data in memory at once.

$result = $database->query('SELECT * FROM users');

foreach ($result as $row) {
	echo $row->id;
	echo $row->name;
	// ...
}

The ResultSet can only be iterated once. If you need to iterate over it multiple times, you must first load the data into an array, for example, using the fetchAll() method.

fetch(): ?Row

Executes the query and fetches a single row as a Row object. If no more rows are available, it returns null. This method advances the internal pointer to the next row.

$result = $database->query('SELECT * FROM users');
$row = $result->fetch(); // fetches the first row
if ($row) {
	echo $row->name;
}

fetchAll(): array

Fetches all remaining rows from the ResultSet as an array of Row objects.

$result = $database->query('SELECT * FROM users');
$rows = $result->fetchAll(); // fetches all rows
foreach ($rows as $row) {
	echo $row->name;
}

fetchPairs (string|int|null $key = null, string|int|null $value = null)array

Fetches results as an associative array. The first argument specifies the column to use as the key, and the second specifies the column to use as the value:

$result = $database->query('SELECT id, name FROM users');
$names = $result->fetchPairs('id', 'name');
// [1 => 'John Doe', 2 => 'Jane Doe', ...]

If only the first parameter is provided, the value will be the entire row (as a Row object):

$rows = $result->fetchPairs('id');
// [1 => Row(id: 1, name: 'John'), 2 => Row(id: 2, name: 'Jane'), ...]

If null is passed as the key, the array will be indexed numerically starting from zero:

$names = $result->fetchPairs(null, 'name');
// [0 => 'John Doe', 1 => 'Jane Doe', ...]

fetchPairs (Closure $callback)array

Alternatively, you can provide a callback that determines the key-value pairs or values for each row.

$result = $database->query('SELECT * FROM users');
$items = $result->fetchPairs(fn($row) => "$row->id - $row->name");
// ['1 - John', '2 - Jane', ...]

// The callback can also return an array with a key & value pair:
$names = $result->fetchPairs(fn($row) => [$row->name, $row->age]);
// ['John' => 46, 'Jane' => 21, ...]

fetchField(): mixed

Fetches the value of the first cell in the current row. If no more rows are available, it returns null. This method advances the internal pointer to the next row.

$result = $database->query('SELECT name FROM users');
$name = $result->fetchField(); // fetches the name from the first row

fetchList(): ?array

Fetches the row as an indexed array. If no more rows are available, it returns null. This method advances the internal pointer to the next row.

$result = $database->query('SELECT name, email FROM users');
$row = $result->fetchList(); // ['John', 'john@example.com']

getRowCount(): ?int

Returns the number of rows affected by the last UPDATE or DELETE query. For SELECT queries, it returns the number of rows fetched, but this may not always be known—in such cases, it returns null.

getColumnCount(): ?int

Returns the number of columns in the ResultSet.

Query Information

To retrieve details about the most recently executed query, use:

echo $database->getLastQueryString(); // outputs the SQL query

$result = $database->query('SELECT * FROM articles');
echo $result->getQueryString();    // outputs the SQL query
echo $result->getTime();           // outputs the execution time in seconds

To display the result as an HTML table, use:

$result = $database->query('SELECT * FROM articles');
$result->dump();

You can also retrieve information about column types from the ResultSet:

$result = $database->query('SELECT * FROM articles');
$types = $result->getColumnTypes();

foreach ($types as $column => $type) {
	echo "$column is of type $type->type"; // e.g., 'id is of type int'
}

Query Logging

You can implement custom query logging. The onQuery event is an array of callbacks that are invoked after each query execution:

$database->onQuery[] = function ($database, $result) use ($logger) {
	$logger->info('Query: ' . $result->getQueryString());
	$logger->info('Time: ' . $result->getTime());

	if ($result->getRowCount() > 1000) {
		$logger->warning('Large result set: ' . $result->getRowCount() . ' rows');
	}
};
version: 4.0 3.x 2.x