Skip to content

MySQL Tutorial MySQL with PHP

Reading Rows and MySQL Types in PHP

Lesson 262 of 308 3 min read By Deepak Paswan Updated Oct 5, 2026

Reading rows is covered in PDO Select. This lesson answers the MySQL-specific question: what PHP type does each column arrive as? Getting this wrong is a classic source of bugs with money and dates.

The fetch modes you need

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

$stmt = $pdo->query('SELECT id, name, price FROM products WHERE category = "writing" ORDER BY id');
print_r($stmt->fetch());                          // one row, as an array

$names = $pdo->query('SELECT name FROM products ORDER BY id LIMIT 3')->fetchAll(PDO::FETCH_COLUMN);
echo implode(', ', $names), "\n";                 // one column as a list

$prices = $pdo->query('SELECT name, price FROM products ORDER BY id LIMIT 3')->fetchAll(PDO::FETCH_KEY_PAIR);
print_r($prices);                                 // [name => price]

$count = $pdo->query('SELECT COUNT(*) FROM products')->fetchColumn();
var_dump($count);                                 // one value
Output
OUTPUT
Array
(
    [id] => 2
    [name] => Pen
    [price] => 12.50
)
Notebook, Pen, Backpack
Array
(
    [Notebook] => 80.00
    [Pen] => 12.50
    [Backpack] => 799.00
)
int(8)
MethodReturns
fetch()The next row (or false)
fetchAll()All rows as a list
fetchAll(PDO::FETCH_COLUMN)A flat list of the first column
fetchAll(PDO::FETCH_KEY_PAIR)[first column => second column]
fetchAll(PDO::FETCH_GROUP)Groups rows by the first column
fetchColumn()The first column of the next row
fetchObject(Class::class)A row as an object

Which PHP types you get

With native prepares (and the mysqlnd driver of PHP 8.1 and later), integers and floats arrive as PHP numbers, but DECIMAL, DATE, DATETIME, TIME and JSON arrive as strings:

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

$row = $pdo->query('SELECT 1 AS int_col, 1.5 AS decimal_col, CAST(1.5 AS DOUBLE) AS double_col, "text" AS text_col, NULL AS null_col, TRUE AS bool_col, NOW() IS NOT NULL AS flag')->fetch();
foreach ($row as $name => $value) {
    echo str_pad($name, 12), get_debug_type($value), "\n";
}
Output
OUTPUT
int_col     int
decimal_col string
double_col  float
text_col    string
null_col    null
bool_col    int
flag        int

The same, from real table columns:

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

$stmt = $pdo->prepare('SELECT id, price, stock, created_at FROM products JOIN (SELECT created_at FROM customers LIMIT 1) c WHERE products.id = ?');
$stmt->execute([1]);
foreach ($stmt->fetch() as $name => $value) {
    echo str_pad($name, 12), get_debug_type($value), ' ', var_export($value, true), "\n";
}
Output
OUTPUT
id          int 1
price       string '80.00'
stock       int 25
created_at  string '2026-01-01 10:00:00'
MySQL typePHP type
INT, BIGINT, TINYINT, BOOLEANint
FLOAT, DOUBLEfloat
DECIMALstring
DATE, DATETIME, TIMESTAMP, TIMEstring
VARCHAR, TEXT, ENUM, SETstring
JSONstring (decode it yourself)
NULLnull

Money: keep DECIMAL as a string

DECIMAL arrives as a string ("12.50") on purpose, so that no precision is lost. Do the arithmetic with decimal-safe tools, or in integer minor units (paise, cents):

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

$sum = $pdo->query("SELECT SUM(price) FROM products WHERE category = 'writing'")->fetchColumn();
var_dump($sum);

$float = 0.0;
foreach ([12.50, 45.00, 5.00] as $price) { $float += $price; }
var_dump($float);
echo 0.1 + 0.2 == 0.3 ? "equal\n" : "0.1 + 0.2 is not 0.3 as a float\n";

// exact sum in whole paise, using string maths from the database (or the bcmath extension if installed)
$paise = (int) $pdo->query("SELECT CAST(SUM(price) * 100 AS UNSIGNED) FROM products WHERE category = 'writing'")->fetchColumn();
echo intdiv($paise, 100), '.', str_pad((string) ($paise % 100), 2, '0', STR_PAD_LEFT), "\n";
Output
OUTPUT
string(5) "62.50"
float(62.5)
0.1 + 0.2 is not 0.3 as a float
62.50

Do not store money in FLOAT or DOUBLE columns (Data Types). If you must add decimals in PHP, use bcadd() (the bcmath extension) or integer minor units, and format only when printing.

Dates and times

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

$value = $pdo->query('SELECT created_at FROM customers WHERE id = 1')->fetchColumn();
var_dump($value);

$dt = new DateTimeImmutable($value, new DateTimeZone('UTC'));       // you decide the zone
echo $dt->setTimezone(new DateTimeZone('Asia/Kolkata'))->format('d M Y, H:i'), "\n";
Output
OUTPUT
string(19) "2026-01-01 10:00:00"
01 Jan 2026, 15:30

A DATETIME has no time zone. A TIMESTAMP is stored in UTC and converted using the connection’s time_zone. The safe pattern: store UTC, set the connection to UTC, and convert to the visitor’s zone when displaying (Dates and Times). A zero date like 0000-00-00 is not valid in strict mode and DateTime would reject it.

NULL

NULL becomes PHP null. Test with === null, not empty(), because 0 and '' are also “empty” and mean something different:

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

foreach ($pdo->query('SELECT name, city FROM customers ORDER BY id') as $row) {
    echo $row['name'], ': ', $row['city'] ?? 'no city', "\n";
}
Output
OUTPUT
Maya: Pune
Rohan: Delhi
Isha: no city

JSON columns

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

$stmt = $pdo->query('SELECT name, attrs FROM products WHERE attrs IS NOT NULL ORDER BY id LIMIT 2');
foreach ($stmt as $row) {
    $attrs = json_decode($row['attrs'], true, 512, JSON_THROW_ON_ERROR);
    echo $row['name'], ': ', json_encode($attrs), "\n";
}

// let MySQL extract a single value
echo $pdo->query("SELECT attrs->>'$.color' FROM products WHERE name = 'Pen'")->fetchColumn(), "\n";

// write a JSON value from PHP
$pdo->prepare('UPDATE products SET attrs = ? WHERE name = ?')->execute([json_encode(['color' => 'red', 'tags' => ['school', 'office']]), 'Pen']);
echo $pdo->query("SELECT JSON_EXTRACT(attrs, '$.tags[1]') FROM products WHERE name = 'Pen'")->fetchColumn(), "\n";
Output
OUTPUT
Notebook: {"pages":200,"ruled":true}
Pen: {"color":"blue"}
blue
"office"

Pass JSON as a string (json_encode) and let MySQL validate it (JSON in MySQL). Note that MySQL reorders the keys of a stored JSON value and removes extra spaces, so do not compare the raw text.

Large result sets: unbuffered queries

By default, the driver loads the whole result into PHP memory. For a result with millions of rows, use an unbuffered query and read rows one at a time:

index.php
PHP
<?php
$pdo = require __DIR__ . '/db.php';
$pdo->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, false);

$stmt = $pdo->query('SELECT id, name FROM products ORDER BY id');
$n = 0;
while ($row = $stmt->fetch()) {
    $n++;                       // process one row at a time, memory stays small
}
$stmt->closeCursor();           // you must finish or close it before the next query
echo "read $n rows\n";
Output
OUTPUT
read 8 rows

With an unbuffered query, the connection is busy until you read all rows (or call closeCursor()): running another query on it first gives error 2014. Also consider keyset pagination or batches instead (Pagination in PHP).

Select only what you need

SELECT * sends every column, including large TEXT and JSON ones, and breaks when the table changes. Name the columns (SELECT).

dangerous
Never use floats for money
A price that arrives as the string “12.50” is correct. Convert it with (float) and add many of them, and cents start to go missing.