Nette Database: a PDO-shaped layer that fetches only the columns you read
💾 A database layer with a familiar PDO-like API but much more powerful. Building queries, advanced joins, drivers for MySQL, PostgreSQL, SQLite, MS SQL Server and Oracle.
At a glance
- What is it?
- Nette Database is a PHP database layer built on PDO with two tiers, a Core wrapper for query building and an Explorer that loads related rows lazily and caches which columns were used. Its efficiency claim is about the number of queries and the columns selected, not about raw speed.
- Who is it for?
- Adopt Nette Database if you write PHP and want relationship handling without hand-written joins or an N+1 query pattern, and you are on PHP 8.3 or newer. Do not adopt it if you need HTTP/2-era database features or an ORM with change tracking, since this is a query layer and not a unit of work, and it has no migrations and no schema tooling.
- Can I use it commercially?
- Check first. The repository uses a licence we do not classify automatically, so read its LICENSE file before any commercial use.
- Is it still maintained?
- Yes. The repository last received commits 57 days ago.
- What is it written in?
- Mainly PHP, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Two layers over PDO, and the difference matters
Nette Database is described as a database layer with a familiar PDO-like API and more capability, with building queries, advanced joins, and drivers for MySQL, PostgreSQL, SQLite, MS SQL Server and Oracle.
The README splits the library into two and explains what each is for. The Nette Database Core is a wrapper around PDO and provides core functionality, and it composes SQL queries with ease, significantly simplifies retrieving data without writing SQL queries, and uses efficient queries that do not transmit unnecessary data. The Nette Database Explorer layer helps you to fetch database data more easily and in a more optimized way.
That split is the design, and it is the reason to pick this over a plain PDO wrapper. Core gives you a query builder with placeholders and parameter handling, which is the safety and ergonomics half. Explorer gives you an object graph, which is the efficiency half.
The documentation site is doc.nette.org/database, and the README is explicit that the usage section here is just a piece of documentation and points you to the website for the rest.
The project is MIT licensed in the sense that the badge reads New BSD, the primary language is PHP, the default branch is master, and the last push was on 2026-08-27. The release history is v3.2.9 from 2026-04-22, v3.2.8 from 2025-10-30 and v3.2.7 from 2025-06-03, so the 3.2 line is the current one and it is still getting patch releases.
Query building with placeholders, arrays and streams
A connection is created with the same arguments PDO uses, so the migration from raw PDO is mostly a change of class name:
$database = new Nette\Database\Explorer($dsn, $user, $password); // the same arguments as uses PDOThe query method then takes the SQL and an optional array of parameters, and the array form is what makes it safe by default:
$database->query('INSERT INTO users', [ // an array can be a parameter
'name' => 'Jim',
'created' => new DateTime, // or a DateTime object
'avatar' => fopen('image.gif', 'r'), // or a file
], ...); // it is even possible to use multiple inserts
$database->query('UPDATE users SET ? WHERE id=?', $data, $id);
$database->query('SELECT * FROM categories WHERE id=?', 123)->dump();Three things stand out in that snippet. A DateTime object can be a parameter, so the driver handles the conversion rather than the caller. A file handle can be a parameter, which is how a binary column gets filled without loading it into memory. And the trailing spread on the parameters means you can pass several rows in one call, so a bulk insert is one call rather than a loop.
The placeholder style is worth noting too. UPDATE users SET ? WHERE id=? uses a single question mark for a whole assignment, and the value is passed as a structured array. That is the query builder taking over the tedious part of building SET clauses and binding them in the right order, which is where hand-written PDO code usually goes wrong.
The Explorer fetches one table and follows relations itself
The Database Explorer section explains the primary attitude of the layer: fetch data only from one table and fetch them at once. The data are fetched into ActiveRow instances, and data from other tables connected by relationships are delivered by other queries, maintained by the Explorer layer itself.
The example in the README is a 1:N relationship between books and authors, and it sets out the two common implementations. The often used one fetches data by one SQL query with table joins. The second possibility is to fetch data separately, run one query for getting books and then get an author for each book by another query in your foreach cycle. The README says the second could be easily optimized to run only two queries, one for books and another for the needed authors, and that this is just the way the Explorer does it.
The API starts from the table, and the selection is just a table call:
$selection = $explorer->table('book'); // db table name is "book"Rows arrive as ActiveRow instances you read as properties, and get() returns one row directly:
$book = $explorer->table('book')->get(2); // returns book with id 2
echo $book->title;
echo $book->author_id;And relationships are traversed by property access rather than by a join you wrote:
$books = $explorer->table('book');
foreach ($books as $book) {
echo 'title: ' . $book->title;
echo 'written by: ' . $book->author->name;
echo 'tags: ';
foreach ($book->related('book_tag') as $bookTag) {
echo $bookTag->tag->name . ', ';
}
}So the author of a book is $book->author->name, and the tags come through the book_tag join table as related records. The joins are still there, they are just not written by you.
Why the same page costs four queries, and fewer columns
The part of the README that makes the argument concrete is the query log for the previous example, because it shows the constant number of queries rather than claiming efficiency.
SELECT * FROM `book`
SELECT * FROM `author` WHERE (`author`.`id` IN (11, 12))
SELECT * FROM `book_tag` WHERE (`book_tag`.`book_id` IN (1, 4, 2, 3))
SELECT * FROM `tag` WHERE (`tag`.`id` IN (21, 22, 23))Four queries for a page of books with authors and tags. The author query is batched with an IN list rather than one query per book, and the join table is fetched in one go as well. That is the difference between this and the N+1 pattern the README describes, and it is achieved by collecting the foreign keys first and then fetching the related rows in one batch.
The column selection is the second half. If you use caching, which is on by default, no columns will be queried unnecessarily. After the first query, the cache stores the used column names and the Explorer runs queries with only the needed columns:
SELECT `id`, `title`, `author_id` FROM `book`
SELECT `id`, `name` FROM `author` WHERE (`author`.`id` IN (11, 12))
SELECT `book_id`, `tag_id` FROM `book_tag` WHERE (`book_tag`.`book_id` IN (1, 4, 2, 3))
SELECT `id`, `name` FROM `tag` WHERE (`tag`.`id` IN (21, 22, 23))The same four queries, but the book table no longer selects every column when the page only reads the title and the author id. The cache is populated from what the code actually accessed, which means the saving is a side effect of your access pattern rather than something you configure.
Running the tests across four database servers
The Running Tests section is short, and it tells you how the project is verified across the databases it supports.
The first route needs no setup at all, because SQLite is embedded:
composer run testerTo test against MySQL, PostgreSQL and the others, you start the Docker containers and copy the configuration, then run the same command:
docker compose up -d
cp tests/databases.docker.ini tests/Database/databases.ini
composer run testerThe compose file is the interesting artefact, because it holds several server versions at once rather than one. There is a mysql service on 8.0 published on 3307, a postgres96 service on 9.6 published on 5433, a postgres13 service on 13 published on 5434, and an mssql service on the 2022-latest image published on 1434. Every one of them creates a database named nette_test, and each has a healthcheck with a ten second interval, a five second timeout and five retries, and the SQL Server one has a thirty second start period and an entrypoint that waits for the server before creating the database.
Running two PostgreSQL versions side by side is the detail that shows the intent. A query builder that generates different SQL for different server generations is where compatibility bugs live, and the cheapest way to catch them is to run the same test suite against both.
The install itself is one Composer line, and the version constraint is explicit: it requires PHP version 8.3 and supports PHP up to 8.5.
Editorial conclusion
Adopt Nette Database if you write PHP and want relationship handling without hand-written joins or an N+1 query pattern, and you are on PHP 8.3 or newer. Do not adopt it if you need HTTP/2-era database features or an ORM with change tracking, since this is a query layer and not a unit of work, and it has no migrations and no schema tooling. Before you start, run the SQLite test suite with composer run tester, which needs no setup, and only then start the Docker containers to test against MySQL, PostgreSQL and SQL Server, because the docker-compose file runs four different server versions side by side.
Frequently asked questions
How do I install Nette Database?
Through Composer with `composer require nette/database`. It requires PHP version 8.3 and supports PHP up to 8.5, and the package is published on Packagist as nette/database.
What is the difference between Nette Database Core and Explorer?
Core is a wrapper around PDO that provides query building with placeholders and parameter handling. Explorer is the layer on top that fetches related rows for you, loading them in batched queries with IN lists rather than one query per row, and caching which columns were used so later queries select only those.
How does Nette Database Explorer handle relationships?
You traverse them as properties, so $book->author->name and $book->related('book_tag') for a join table. The Explorer issues the join queries itself, batching the related ids into IN clauses, which is why the example page of books with authors and tags costs a constant four queries rather than one per book.
How do I run the Nette Database tests?
Run `composer run tester` for SQLite, which needs no setup. To test against MySQL, PostgreSQL and SQL Server, run `docker compose up -d`, copy the configuration with `cp tests/databases.docker.ini tests/Database/databases.ini`, and run the tester again.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/nette-database)Was this useful?