Learn how to INSERT, UPDATE, DELETE, and REPLACE data with PDOdb.
Insert a single row and get the inserted ID:
use tommyknocker\pdodb\helpers\Db;
$userId = $db->find()->table('users')->insert([
'name' => 'Alice',
'email' => 'alice@example.com',
'age' => 30,
'created_at' => Db::now()
]);
echo "Inserted user with ID: $userId\n";Insert multiple rows at once:
$users = [
['name' => 'Alice', 'email' => 'alice@example.com', 'age' => 30],
['name' => 'Bob', 'email' => 'bob@example.com', 'age' => 25],
['name' => 'Carol', 'email' => 'carol@example.com', 'age' => 28]
];
$count = $db->find()->table('users')->insertMulti($users);
echo "Inserted $count users\n";Copy data from one table to another using insertFrom(). This method supports table names, QueryBuilder instances, closures, and subqueries:
// Copy all data from source table
$affected = $db->find()
->table('target_users')
->insertFrom('source_users');
echo "Copied {$affected} rows\n";// Copy only specific columns
$affected = $db->find()
->table('target_users')
->insertFrom('source_users', ['name', 'email', 'age']);// Copy filtered data using QueryBuilder
$affected = $db->find()
->table('target_users')
->insertFrom(function ($query) {
$query->from('source_users')
->where('status', 'active')
->select(['name', 'email', 'age', 'status']);
});// Copy data from joined tables
$affected = $db->find()
->table('target_users')
->insertFrom(function ($query) {
$query->from('source_users')
->join('user_profiles', 'source_users.id = user_profiles.user_id')
->select([
'source_users.name',
'source_users.email',
'source_users.age'
]);
});// Copy data using CTE
$affected = $db->find()
->table('target_users')
->insertFrom(function ($query) {
$query->with('filtered_source', function ($q) {
$q->from('source_users')
->where('age', 30, '>=')
->select(['name', 'email', 'age']);
})
->from('filtered_source')
->select(['name', 'email', 'age']);
});// Copy aggregated data
$affected = $db->find()
->table('target_users')
->insertFrom(function ($query) {
$query->from('source_users')
->select([
'name',
'avg_age' => Db::raw('AVG(age)'),
'count' => Db::raw('COUNT(*)')
])
->groupBy('name');
}, ['name', 'age', 'status']);// Copy with upsert behavior
$affected = $db->find()
->table('target_users')
->insertFrom('source_users', null, ['age', 'status']);Parameters:
- Source: Table name (string), QueryBuilder instance, SelectQueryBuilderInterface, or Closure that receives a QueryBuilder
- Columns: Optional array of column names to insert into. If null, uses SELECT columns
- On Duplicate: Optional array of columns to update on duplicate (upsert behavior)
Features:
- Automatic parameter merging from source queries
- Support for subqueries and CTE in source
- Automatic data type handling
- Fluent API for complex data copying scenarios
use tommyknocker\pdodb\helpers\Db;
$db->find()->table('users')->insert([
'name' => 'Alice',
'age' => 25,
'updated_at' => Db::now(), // Current timestamp
'counter' => Db::inc(), // Increment counter by 1
'meta' => Db::jsonObject(['city' => 'NYC', 'active' => true])
]);use tommyknocker\pdodb\helpers\Db;
$affected = $db->find()
->table('users')
->where('id', 1)
->update([
'name' => 'Alice Updated',
'email' => 'alice.new@example.com',
'updated_at' => Db::now()
]);
echo "Updated $affected row(s)\n";$db->find()
->table('users')
->where('id', 1)
->update([
'age' => Db::inc(5), // Increment by 5
'counter' => Db::dec() // Decrement by 1
]);
// Or using raw for complex operations
$db->find()->table('users')->update([
'score' => Db::raw('score + :bonus', ['bonus' => 100])
]);// Update all inactive users
$affected = $db->find()
->table('users')
->where('last_login', '2020-01-01', '<')
->andWhere('status', 'inactive')
->update([
'status' => 'archived',
'updated_at' => Db::now()
]);Update rows based on conditions from joined tables:
use tommyknocker\pdodb\helpers\Db;
// Update user balance based on order amount
$affected = $db->find()
->table('users')
->join('orders', 'orders.user_id = users.id')
->where('orders.status', 'completed')
->update(['balance' => Db::raw('users.balance + orders.amount')]);MySQL/MariaDB syntax:
UPDATE users JOIN orders ON orders.user_id = users.id
SET users.balance = users.balance + orders.amount
WHERE orders.status = 'completed'PostgreSQL syntax:
UPDATE users SET balance = users.balance + orders.amount
FROM orders
WHERE orders.user_id = users.id AND orders.status = 'completed'Note: SQLite doesn't support JOIN in UPDATE statements. An exception will be thrown if JOIN is used.
// Update users who have orders (MySQL/MariaDB)
$affected = $db->find()
->table('users')
->leftJoin('orders', 'orders.user_id = users.id')
->where('orders.id', null, 'IS NOT')
->update(['users.status' => 'has_orders']);
// PostgreSQL - column name doesn't need table prefix
$affected = $db->find()
->table('users')
->leftJoin('orders', 'orders.user_id = users.id')
->where('orders.id', null, 'IS NOT')
->update(['status' => 'has_orders']);Important: When using JOIN in UPDATE:
- MySQL/MariaDB: Column names in SET clause are automatically qualified with table name to avoid ambiguity
- PostgreSQL: Column names in SET clause don't need table prefix (uses FROM clause)
- SQLite: JOIN in UPDATE is not supported (throws exception)
$deleted = $db->find()
->table('users')
->where('id', 1)
->delete();
echo "Deleted $deleted row(s)\n";// Delete old inactive users
$deleted = $db->find()
->table('users')
->where('status', 'inactive')
->andWhere('created_at', '2020-01-01', '<')
->delete();
echo "Deleted $deleted inactive users\n";Delete rows based on conditions from joined tables:
// Delete users who have cancelled orders
$deleted = $db->find()
->table('users')
->join('orders', 'orders.user_id = users.id')
->where('orders.status', 'cancelled')
->delete();MySQL/MariaDB syntax:
DELETE users FROM users JOIN orders ON orders.user_id = users.id
WHERE orders.status = 'cancelled'PostgreSQL syntax:
DELETE FROM users USING orders
WHERE orders.user_id = users.id AND orders.status = 'cancelled'Note: SQLite doesn't support JOIN in DELETE statements. An exception will be thrown if JOIN is used.
// Delete users in specific IDs
$ids = [100, 101, 102, 103];
$deleted = $db->find()
->table('users')
->where(Db::in('id', $ids))
->delete();
echo "Deleted $deleted users\n";$db->find()->table('users')->truncate();
// Removes all data and resets auto-incrementNote: TRUNCATE is emulated in SQLite using DELETE + reset sequence.
Replace a row if it exists, otherwise insert:
$db->find()->table('users')->replace([
'id' => 1,
'name' => 'Alice',
'email' => 'alice@example.com'
]);$users = [
['id' => 1, 'name' => 'Alice', 'email' => 'alice@example.com'],
['id' => 2, 'name' => 'Bob', 'email' => 'bob@example.com']
];
$db->find()->table('users')->replaceMulti($users);Note: In PostgreSQL and SQLite, REPLACE is emulated as UPSERT.
use tommyknocker\pdodb\helpers\Db;
// Insert or update if duplicate
$db->find()->table('users')
->onDuplicate([
'name' => Db::raw('VALUES(name)'),
'age' => Db::raw('VALUES(age)'),
'updated_at' => Db::now()
])
->insert([
'email' => 'alice@example.com',
'name' => 'Alice',
'age' => 30
]);$db->find()->table('users')
->onDuplicate([
'login_count' => Db::inc(),
'last_login' => Db::now()
])
->insert([
'email' => 'alice@example.com',
'login_count' => 1,
'last_login' => Db::now()
]);MERGE statement performs INSERT, UPDATE, or DELETE operations based on whether rows from a source match rows in a target table. This is useful for synchronizing data between tables.
Note:
- PostgreSQL: Native MERGE support (PostgreSQL 15+)
- MySQL: Emulated via
INSERT ... SELECT ... ON DUPLICATE KEY UPDATE- SQLite: Emulated via
INSERT OR REPLACE
use tommyknocker\pdodb\helpers\Db;
// MERGE from source table to target table
$affected = $db->find()
->table('products') // Target table
->merge(
'product_updates', // Source table
'target.id = source.id', // ON condition
[ // WHEN MATCHED - update these columns
'name' => Db::raw('source.name'),
'price' => Db::raw('source.price'),
'stock' => Db::raw('source.stock')
],
[ // WHEN NOT MATCHED - insert these columns
'id' => 999,
'name' => 'Placeholder',
'price' => 0,
'stock' => 0
]
);
echo "MERGE completed: {$affected} row(s) affected\n";// MERGE using a subquery as source
$affected = $db->find()
->table('products')
->merge(
function ($q) {
$q->table('product_updates')
->select('*')
->where('updated_at', Db::now(), '>');
},
'target.id = source.id',
['price' => Db::raw('source.price'), 'stock' => Db::raw('source.stock')],
['id' => 999, 'name' => 'Placeholder', 'price' => 0, 'stock' => 0]
);// MERGE using a QueryBuilder instance as source
$sourceQuery = $db->find()
->table('product_updates')
->select('*')
->where('status', 'pending');
$affected = $db->find()
->table('products')
->merge(
$sourceQuery,
'target.id = source.id',
['name' => Db::raw('source.name'), 'price' => Db::raw('source.price')],
['id' => 999, 'name' => 'Placeholder', 'price' => 0]
);- Source: Can be a table name (string), a Closure that receives a QueryBuilder, or a SelectQueryBuilderInterface instance
- ON conditions: String or array of join conditions (e.g.,
'target.id = source.id'or['target.id = source.id', 'target.status = source.status']) - When Matched: Array of columns to update when rows match (use
Db::raw('source.column')to reference source columns) - When Not Matched: Array of columns to insert when rows don't match (use simple values or
Db::raw()) - When Not Matched By Source Delete: Boolean flag to delete target rows not matched by source (PostgreSQL 15+ only)
PostgreSQL:
- Uses native
MERGE INTO ... USING ... ON ... WHEN MATCHED ... WHEN NOT MATCHEDsyntax - Supports
WHEN NOT MATCHED BY SOURCE THEN DELETEclause
MySQL:
- Emulated using
INSERT ... SELECT ... ON DUPLICATE KEY UPDATE - Requires
WHEN NOT MATCHEDclause - Update expressions use
VALUES(column)instead ofsource.column
SQLite:
- Emulated using
INSERT OR REPLACE ... SELECT - Requires
WHEN NOT MATCHEDclause - Replaces based on PRIMARY KEY or UNIQUE constraints
For large datasets, use batch processing:
function insertBatch($db, $data, $batchSize = 1000) {
$batches = array_chunk($data, $batchSize);
foreach ($batches as $batch) {
$db->find()->table('users')->insertMulti($batch);
}
}
$users = /* array of 10,000 users */;
insertBatch($db, $users);$db->startTransaction();
try {
foreach ($userIds as $userId) {
$db->find()
->table('users')
->where('id', $userId)
->update(['status' => 'active']);
}
$db->commit();
} catch (\Exception $e) {
$db->rollBack();
throw $e;
}use tommyknocker\pdodb\helpers\Db;
// Increment by 1
$db->find()->table('users')->update(['counter' => Db::inc()]);
// Increment by 5
$db->find()->table('users')->update(['score' => Db::inc(5)]);
// Decrement by 1
$db->find()->table('users')->update(['stock' => Db::dec()]);
// Decrement by 10
$db->find()->table('users')->update(['credits' => Db::dec(10)]);$db->find()->table('users')->update([
'updated_at' => Db::now(),
'created_at' => Db::now('-1 DAY')
]);$db->find()->table('users')->insert([
'meta' => Db::jsonObject(['city' => 'NYC', 'age' => 30]),
'tags' => Db::jsonArray('php', 'mysql', 'docker')
]);// ❌ DANGEROUS: Updates all rows
$db->find()->table('users')->update(['status' => 'inactive']);
// ✅ SAFE: Updates only specific rows
$db->find()
->table('users')
->where('id', $userId)
->update(['status' => 'inactive']);$db->startTransaction();
try {
// Insert order
$orderId = $db->find()->table('orders')->insert([
'user_id' => $userId,
'total' => $total
]);
// Insert order items
foreach ($items as $item) {
$db->find()->table('order_items')->insert([
'order_id' => $orderId,
'product_id' => $item['product_id'],
'quantity' => $item['quantity']
]);
}
// Update inventory
foreach ($items as $item) {
$db->find()
->table('products')
->where('id', $item['product_id'])
->update(['stock' => Db::dec($item['quantity'])]);
}
$db->commit();
} catch (\Exception $e) {
$db->rollBack();
throw $e;
}// ❌ Slow: Multiple single inserts
foreach ($users as $user) {
$db->find()->table('users')->insert($user);
}
// ✅ Fast: Single multi-insert
$db->find()->table('users')->insertMulti($users);$db->startTransaction();
try {
// Multiple operations
$db->find()->table('users')->insert($user1);
$db->find()->table('users')->insert($user2);
$db->commit();
} catch (\Exception $e) {
$db->rollBack();
}$offset = 0;
$limit = 1000;
while (true) {
$db->startTransaction();
try {
$users = $db->find()
->from('users')
->where('old_flag', 1)
->limit($limit)
->offset($offset)
->get();
if (empty($users)) {
break;
}
foreach ($users as $user) {
$db->find()
->table('users')
->where('id', $user['id'])
->update(['new_flag' => 1]);
}
$db->commit();
$offset += $limit;
} catch (\Exception $e) {
$db->rollBack();
throw $e;
}
}$id = $db->find()->table('users')->insert(['name' => 'Alice']);
echo "Inserted ID: $id\n";$affected = $db->find()
->table('users')
->where('status', 'inactive')
->update(['status' => 'active']);
echo "Updated $affected row(s)\n";$exists = $db->find()
->from('users')
->where('email', 'alice@example.com')
->exists();
if (!$exists) {
$db->find()->table('users')->insert([
'email' => 'alice@example.com',
'name' => 'Alice'
]);
}- Simple CRUD Operations - Basic INSERT, UPDATE, DELETE
- Insert and Update - Data manipulation examples
- INSERT ... SELECT - Copying data between tables
- UPDATE/DELETE with JOIN - Update and delete with JOIN clauses
- SELECT Operations - Read data from database
- Filtering Conditions - Complex WHERE clauses
- Transactions - Transaction management
- Bulk Operations - Large dataset handling