Guides
Working with Database
Master SQL queries, relationships, and database best practices
Learn database techniques with DALT.PHP: complex queries, relationships, transactions, and performance optimization. DALT uses raw SQL with prepared statements for security and clarity.
Basic Queries
$db = App::resolve(Core\Database::class);
// Select all
$posts = $db->query('SELECT * FROM posts')->get();
// Select one
$post = $db->query('SELECT * FROM posts WHERE id = ?', [1])->find();
// Select with conditions
$published = $db->query('SELECT * FROM posts WHERE published = ?', [1])->get();Insert Data
// Insert and get ID
$db->query(
'INSERT INTO posts (title, body) VALUES (?, ?)',
['My Title', 'My Body']
);
$id = $db->connection->lastInsertId();Update Data
$db->query(
'UPDATE posts SET title = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?',
['New Title', 1]
);Delete Data
$db->query('DELETE FROM posts WHERE id = ?', [1]);Advanced Queries
WHERE Clauses
// Multiple conditions
$posts = $db->query(
'SELECT * FROM posts WHERE published = ? AND user_id = ?',
[1, $userId]
)->get();
// LIKE search
$posts = $db->query(
'SELECT * FROM posts WHERE title LIKE ?',
['%' . $search . '%']
)->get();
// IN clause
$posts = $db->query(
'SELECT * FROM posts WHERE id IN (?, ?, ?)',
[1, 2, 3]
)->get();ORDER BY and LIMIT
// Order by
$posts = $db->query(
'SELECT * FROM posts ORDER BY created_at DESC'
)->get();
// Limit
$posts = $db->query(
'SELECT * FROM posts LIMIT 10'
)->get();
// Pagination
$page = 1;
$perPage = 10;
$offset = ($page - 1) * $perPage;
$posts = $db->query(
'SELECT * FROM posts LIMIT ? OFFSET ?',
[$perPage, $offset]
)->get();Aggregate Functions
// Count
$count = $db->query('SELECT COUNT(*) as count FROM posts')->find()['count'];
// Sum
$total = $db->query('SELECT SUM(views) as total FROM posts')->find()['total'];
// Average
$avg = $db->query('SELECT AVG(rating) as avg FROM posts')->find()['avg'];
// Max/Min
$max = $db->query('SELECT MAX(created_at) as max FROM posts')->find()['max'];Relationships
One-to-Many (Posts and Comments)
-- Migration: create_comments_table.sql
CREATE TABLE IF NOT EXISTS comments (
id INTEGER PRIMARY KEY AUTOINCREMENT,
post_id INTEGER NOT NULL,
body TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE
);// Get post with comments
$post = $db->query('SELECT * FROM posts WHERE id = ?', [$id])->find();
$comments = $db->query('SELECT * FROM comments WHERE post_id = ?', [$id])->get();
// Or use JOIN
$results = $db->query(
'SELECT posts.*, comments.body as comment_body, comments.created_at as comment_date
FROM posts
LEFT JOIN comments ON posts.id = comments.post_id
WHERE posts.id = ?',
[$id]
)->get();Many-to-Many (Posts and Tags)
-- posts_tags pivot table
CREATE TABLE IF NOT EXISTS posts_tags (
post_id INTEGER NOT NULL,
tag_id INTEGER NOT NULL,
PRIMARY KEY (post_id, tag_id),
FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE,
FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE
);// Get post with tags
$tags = $db->query(
'SELECT tags.* FROM tags
INNER JOIN posts_tags ON tags.id = posts_tags.tag_id
WHERE posts_tags.post_id = ?',
[$postId]
)->get();
// Attach tag to post
$db->query(
'INSERT INTO posts_tags (post_id, tag_id) VALUES (?, ?)',
[$postId, $tagId]
);Transactions
Use transactions for operations that must succeed or fail together:
try {
$db->connection->beginTransaction();
// Create post
$db->query(
'INSERT INTO posts (title, body) VALUES (?, ?)',
['Title', 'Body']
);
$postId = $db->connection->lastInsertId();
// Attach tags
foreach ($tagIds as $tagId) {
$db->query(
'INSERT INTO posts_tags (post_id, tag_id) VALUES (?, ?)',
[$postId, $tagId]
);
}
$db->connection->commit();
} catch (\Exception $e) {
$db->connection->rollBack();
throw $e;
}Security Best Practices
Always Use Prepared Statements
// ✅ SAFE - Prepared statement
$post = $db->query('SELECT * FROM posts WHERE id = ?', [$_GET['id']])->find();
// ❌ DANGEROUS - SQL injection vulnerable
$post = $db->query("SELECT * FROM posts WHERE id = {$_GET['id']}")->find();Validate Input
// Validate before querying
if (!is_numeric($_GET['id'])) {
abort(400);
}
$post = $db->query('SELECT * FROM posts WHERE id = ?', [$_GET['id']])->find();Escape Output
// Always escape when displaying
<?= htmlspecialchars($post['title']) ?>Performance Optimization
Add Indexes
-- Index frequently queried columns
CREATE INDEX IF NOT EXISTS idx_posts_published ON posts(published);
CREATE INDEX IF NOT EXISTS idx_posts_user_id ON posts(user_id);
CREATE INDEX IF NOT EXISTS idx_comments_post_id ON comments(post_id);Use EXPLAIN
// Analyze query performance
$result = $db->query('EXPLAIN QUERY PLAN SELECT * FROM posts WHERE published = 1')->get();
print_r($result);Limit Results
// Don't fetch all rows if you only need some
$posts = $db->query('SELECT * FROM posts LIMIT 10')->get();Select Only Needed Columns
// ✅ Better - only needed columns
$posts = $db->query('SELECT id, title FROM posts')->get();
// ❌ Wasteful - all columns
$posts = $db->query('SELECT * FROM posts')->get();Common Patterns
Search with Filters
$sql = 'SELECT * FROM posts WHERE 1=1';
$params = [];
if (!empty($_GET['search'])) {
$sql .= ' AND title LIKE ?';
$params[] = '%' . $_GET['search'] . '%';
}
if (!empty($_GET['published'])) {
$sql .= ' AND published = ?';
$params[] = $_GET['published'];
}
$sql .= ' ORDER BY created_at DESC';
$posts = $db->query($sql, $params)->get();Pagination with Total Count
$page = $_GET['page'] ?? 1;
$perPage = 10;
$offset = ($page - 1) * $perPage;
// Get total count
$total = $db->query('SELECT COUNT(*) as count FROM posts')->find()['count'];
// Get paginated results
$posts = $db->query(
'SELECT * FROM posts ORDER BY created_at DESC LIMIT ? OFFSET ?',
[$perPage, $offset]
)->get();
$totalPages = ceil($total / $perPage);Soft Deletes
-- Add deleted_at column
ALTER TABLE posts ADD COLUMN deleted_at DATETIME DEFAULT NULL;// Soft delete
$db->query(
'UPDATE posts SET deleted_at = CURRENT_TIMESTAMP WHERE id = ?',
[$id]
);
// Query only non-deleted
$posts = $db->query('SELECT * FROM posts WHERE deleted_at IS NULL')->get();
// Restore
$db->query('UPDATE posts SET deleted_at = NULL WHERE id = ?', [$id]);Database Migrations
Creating Migrations
php artisan make:migration add_views_to_posts-- Add column
ALTER TABLE posts ADD COLUMN views INTEGER DEFAULT 0;
-- Add index
CREATE INDEX IF NOT EXISTS idx_posts_views ON posts(views);Running Migrations
# Run pending migrations
php artisan migrate
# Reset and re-run all
php artisan migrate:freshmigrate:fresh drops all tables! Only use in development.
What's Next
You now understand SQL queries, relationships, transactions, and security. The Database class is just a thin wrapper around PDO - you write real SQL with prepared statements for safety.
Done! You can now build database-driven applications with confidence.