Himanshu SharmaSoftware engineer
Let’s talk

PHP · Lesson 50 of 54

Connecting to a database with PDO

Connect PHP to MySQL or SQLite with PDO, set the error mode and fetch mode, run queries, fetch rows as arrays, and keep credentials out of your code.

13 minDatabases and securityUpdated 22 October 2026

Awaiting technical review. This lesson is readable but not yet listed in search engines.

By the end of this lessonOpen a PDO connection with safe options, run a query and fetch its rows.

Why PDO

Websites keep their data in a database: products, customers, orders, posts. PHP talks to databases through PDO (PHP Data Objects), a single interface that works with MySQL, MariaDB, SQLite, PostgreSQL, SQL Server and more. Learn it once and only the connection details change between databases.

You may see older code using the mysqli extension, or even the mysql_ functions that were removed in PHP 7. Use PDO for new code.

This lesson and the next use SQLite, a database that lives in a single file and needs no server, so every example runs anywhere. The MySQL course teaches the SQL itself; everything shown here works the same with MySQL once you change the connection line.

Connecting

A connection needs a DSN (data source name) that names the driver and the database, plus, for server databases, a user name and password. For MySQL:

php
<?php
$dsn = 'mysql:host=127.0.0.1;port=3306;dbname=shop;charset=utf8mb4';
$pdo = new PDO($dsn, 'shop_user', getenv('DB_PASSWORD'), [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);

Always include charset=utf8mb4 for MySQL, so names in any language and emoji are stored correctly, as explained in Unicode text and the mb_ functions.

For SQLite, the DSN is the path of the database file, and sqlite::memory: creates a temporary database in memory:

php
<?php
$pdo = new PDO('sqlite::memory:', options: [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);

echo "Connected with the " . $pdo->getAttribute(PDO::ATTR_DRIVER_NAME) . " driver\n";
echo "SQLite version: " . ($pdo->query('SELECT sqlite_version()')->fetchColumn() !== '' ? 'available' : 'unknown') . "\n";
result
Connected with the sqlite driver
SQLite version: available

The two options matter:

  • ERRMODE_EXCEPTION makes a failed query throw a PDOException, which you cannot miss. It is the default since PHP 8.0; set it anyway so the intent is clear.
  • FETCH_ASSOC returns each row as an associative array keyed by column name, instead of PDO's default, which repeats every value under both a name and a number.

If the connection fails (wrong password, server down), new PDO() throws a PDOException. Its message can contain the host and user name, so log it and show visitors a general error page rather than printing it.

Creating a table and some rows

exec() runs a statement that returns no rows and gives back the number of affected rows. Here is a small version of the MySQL course's sample shop:

php
<?php
$pdo = new PDO('sqlite:' . __DIR__ . '/shop.sqlite', options: [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
return $pdo;
php
<?php
$pdo = require __DIR__ . '/db.php';

$pdo->exec('DROP TABLE IF EXISTS products');
$pdo->exec('CREATE TABLE products (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL,
    category TEXT NOT NULL,
    price REAL NOT NULL,
    stock INTEGER NOT NULL DEFAULT 0
)');

$added = $pdo->exec("INSERT INTO products (name, category, price, stock) VALUES
    ('Mechanical Keyboard', 'Electronics', 89.50, 60),
    ('Noise-Cancelling Headphones', 'Electronics', 199.99, 25),
    ('Ceramic Mug Set', 'Home', 18.75, 120),
    ('Yoga Mat', 'Sports', 24.99, 80),
    ('Puzzle 1000 Pieces', 'Toys', 19.99, 0)");

echo "Added $added products\n";
result
Added 5 products

The connection lives in db.php, which returns the PDO object, so every page can get it with one line. In MySQL, the column types would be INT AUTO_INCREMENT, VARCHAR(100), DECIMAL(8,2) and so on, as in the data types lesson.

Reading rows

query() runs a SQL statement and returns a PDOStatement, from which you fetch the rows:

php
<?php
$pdo = require __DIR__ . '/db.php';

$rows = $pdo->query('SELECT name, price, stock FROM products ORDER BY price DESC')->fetchAll();
foreach ($rows as $row) {
    printf("%-28s %7.2f %4d\n", $row['name'], $row['price'], $row['stock']);
}

$count = $pdo->query('SELECT COUNT(*) FROM products WHERE stock > 0')->fetchColumn();
echo "$count products in stock\n";

$cheapest = $pdo->query('SELECT name, price FROM products ORDER BY price LIMIT 1')->fetch();
print_r($cheapest);
result
Noise-Cancelling Headphones   199.99   25
Mechanical Keyboard            89.50   60
Yoga Mat                       24.99   80
Puzzle 1000 Pieces             19.99    0
Ceramic Mug Set                18.75  120
4 products in stock
Array
(
    [name] => Ceramic Mug Set
    [price] => 18.75
)
Method Returns
fetchAll() every row, as an array of rows
fetch() the next row, or false when there are no more
fetchColumn() one value from the next row (the first column by default)
fetchAll(PDO::FETCH_COLUMN) a flat list of the first column
fetchAll(PDO::FETCH_KEY_PAIR) an array of first column => second column
php
<?php
$pdo = require __DIR__ . '/db.php';

print_r($pdo->query('SELECT name FROM products WHERE category = \'Electronics\'')->fetchAll(PDO::FETCH_COLUMN));
print_r($pdo->query('SELECT category, COUNT(*) FROM products GROUP BY category')->fetchAll(PDO::FETCH_KEY_PAIR));
result
Array
(
    [0] => Mechanical Keyboard
    [1] => Noise-Cancelling Headphones
)
Array
(
    [Electronics] => 2
    [Home] => 1
    [Sports] => 1
    [Toys] => 1
)

For large results, loop over the statement with foreach ($pdo->query($sql) as $row) instead of fetchAll(), so rows are fetched one at a time.

Never put input into query()

Everything in this lesson used fixed SQL, written entirely by you. The moment a value comes from a visitor (a search term, an ID in the URL, a form field), it must not be glued into the SQL string. That is how SQL injection happens. The next lesson shows the only safe way: prepared statements.

Keep credentials out of code

The database password is a secret. Do not write it in a file that is inside the web root, or in a repository others can see. Common approaches:

Also give the application its own database user with only the privileges it needs (usually SELECT, INSERT, UPDATE and DELETE on one database), never root. The MySQL lesson on users and privileges shows how.

Common mistakes

  • Leaving out charset=utf8mb4 in a MySQL DSN, which corrupts non-English text.
  • Printing the PDOException message to visitors, which can reveal connection details.
  • Using the root user for a web application.
  • Building SQL with user input in query() or exec().

Exercise

Using the products table above, print the total stock value (price times stock) for each category, highest first, with two decimals. Do it in SQL with GROUP BY and fetch the result with FETCH_KEY_PAIR.

Show one possible answer
php
<?php
$pdo = require __DIR__ . '/db.php';

$values = $pdo->query('SELECT category, SUM(price * stock) AS value FROM products GROUP BY category ORDER BY value DESC')
    ->fetchAll(PDO::FETCH_KEY_PAIR);

foreach ($values as $category => $value) {
    printf("%-12s %10.2f\n", $category, $value);
}
result
Electronics    10369.75
Home            2250.00
Sports          1999.20
Toys               0.00

Key points

  • PDO is PHP's database layer. The same code works with MySQL, SQLite, PostgreSQL and others; only the DSN changes.
  • Create it with new PDO($dsn, $user, $password, $options), with ERRMODE_EXCEPTION and FETCH_ASSOC.
  • query() runs SQL without user input; fetchAll() returns every row, fetch() one row, fetchColumn() a single value.
  • Keep credentials in environment variables or a config file outside the web root, never in public code.

Check yourself

1. What does a DSN such as mysql:host=127.0.0.1;dbname=shop;charset=utf8mb4 describe?
2. Why set PDO::ATTR_ERRMODE to PDO::ERRMODE_EXCEPTION?
3. Which method returns just the first column of the first row, for example a COUNT(*)?
4. Where should the database password live?