- Khi nào nên dùng $wpdb?
- Sử dụng $wpdb->prepare() để chống SQL Injection
- Các phương thức quan trọng trong $wpdb
- Tối ưu hiệu năng với $wpdb
- Ví dụ: Lấy top 10 user có nhiều bài viết nhất
- Ví dụ: Insert dữ liệu an toàn
- Bonus: 10 mẫu truy vấn $wpdb “thực chiến”
- 1. Đếm tổng bài viết theo trạng thái nhanh gọn
- 2. Lấy danh sách ID bài có meta cụ thể (dùng IN và prepare động)
- 3. Join usermeta: tìm user bật thông báo
- 4. Lấy top category theo lượt xem (taxonomy join)
- 5. Thống kê điểm review trung bình theo category
- 6. Phân trang với OFFSET/LIMIT
- 7. Tìm kiếm có điều kiện với LIKE an toàn
- 8. Cập nhật hàng loạt bằng CASE WHEN
- 9. Xóa transients đã hết hạn (dọn rác)
- 10. “Upsert” nhẹ với ON DUPLICATE KEY (bảng custom)
- Mẹo nhanh khi viết query
- Kết luận
Khi nào nên dùng $wpdb?
Hãy ưu tiên WP_Query hoặc API có sẵn (như get_posts(), get_users()) vì chúng được cache và hỗ trợ hook. Chỉ dùng $wpdb khi:
- Bạn cần query phức tạp, có nhiều JOIN hoặc tính toán.
- Bạn cần thống kê, aggregate (COUNT, SUM, AVG…).
- API sẵn có của WordPress không đáp ứng được nhu cầu.
Sử dụng $wpdb->prepare() để chống SQL Injection
Tuyệt đối không được nối chuỗi trực tiếp từ input của user vào query. Luôn dùng $wpdb->prepare() để binding biến an toàn.
<?php
global $wpdb;
// Sai: dễ bị SQL injection
$sql = "SELECT * FROM {$wpdb->users} WHERE user_login = '" . $_GET['username'] . "'";
$wpdb->get_results($sql);
// Đúng: dùng prepare để escape dữ liệu
$sql = $wpdb->prepare(
"SELECT * FROM {$wpdb->users} WHERE user_login = %s",
$_GET['username']
);
$results = $wpdb->get_results($sql);
Các phương thức quan trọng trong $wpdb
get_results(): Trả về nhiều dòng (array of objects).get_row(): Trả về một dòng duy nhất.get_var(): Trả về một giá trị duy nhất (thường dùng cho COUNT hoặc SUM).get_col(): Trả về một cột dữ liệu.insert(),update(),delete(): Helper để thao tác CRUD.
Tối ưu hiệu năng với $wpdb
Khi dùng $wpdb, bạn chịu trách nhiệm tối ưu query. Một số tips:
- SELECT cụ thể cột: Không dùng
SELECT *, hãy chỉ lấy cột cần thiết. - LIMIT: Luôn giới hạn số dòng trả về, tránh query quá nhiều dữ liệu.
- Sử dụng index: Kiểm tra xem cột bạn lọc/ORDER BY có index chưa.
- Cache kết quả: Dùng
wp_cache_set()hoặc Transients API để lưu kết quả query tốn kém. - Tránh query trong vòng lặp: Gộp dữ liệu lại và query một lần.
Ví dụ: Lấy top 10 user có nhiều bài viết nhất
<?php
global $wpdb;
$sql = "
SELECT post_author, COUNT(ID) as total_posts
FROM {$wpdb->posts}
WHERE post_type = %s AND post_status = %s
GROUP BY post_author
ORDER BY total_posts DESC
LIMIT 10
";
$results = $wpdb->get_results(
$wpdb->prepare($sql, 'post', 'publish')
);
foreach ( $results as $row ) {
$user = get_userdata($row->post_author);
echo '<p>' . esc_html($user->display_name) . ': ' . intval($row->total_posts) . ' bài viết</p>';
}
Ví dụ: Insert dữ liệu an toàn
<?php
global $wpdb;
$wpdb->insert(
$wpdb->prefix . 'custom_table',
[
'user_id' => get_current_user_id(),
'created_at' => current_time('mysql'),
'score' => 95
],
[ '%d', '%s', '%d' ] // định nghĩa kiểu dữ liệu
);
Bonus: 10 mẫu truy vấn $wpdb “thực chiến”
Các ví dụ dưới đây đều dùng $wpdb->prepare() để an toàn, tối ưu theo từng tình huống phổ biến.
1. Đếm tổng bài viết theo trạng thái nhanh gọn
<?php
global $wpdb;
$total_published = (int) $wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT(1) FROM {$wpdb->posts} WHERE post_type = %s AND post_status = %s",
'post', 'publish'
)
);
2. Lấy danh sách ID bài có meta cụ thể (dùng IN và prepare động)
<?php
global $wpdb;
$ids = [12, 34, 56];
$placeholders = implode(',', array_fill(0, count($ids), '%d'));
$sql = $wpdb->prepare(
"SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key = %s AND post_id IN ($placeholders)",
array_merge(['chap_unlock_vcoin'], $ids)
);
$post_ids = $wpdb->get_col($sql);
3. Join usermeta: tìm user bật thông báo
<?php
global $wpdb;
$sql = $wpdb->prepare(
"SELECT u.ID, u.user_email
FROM {$wpdb->users} u
INNER JOIN {$wpdb->usermeta} um ON um.user_id = u.ID
WHERE um.meta_key = %s AND um.meta_value IN ('1','yes','enabled')
LIMIT %d",
'init_manga_notifications_enabled', 500
);
$users = $wpdb->get_results($sql);
4. Lấy top category theo lượt xem (taxonomy join)
<?php
global $wpdb;
$sql = "
SELECT t.term_id, t.name, SUM(COALESCE(pm.meta_value,0)) AS views
FROM {$wpdb->terms} t
JOIN {$wpdb->term_taxonomy} tt ON tt.term_id = t.term_id AND tt.taxonomy = 'category'
JOIN {$wpdb->term_relationships} tr ON tr.term_taxonomy_id = tt.term_taxonomy_id
JOIN {$wpdb->posts} p ON p.ID = tr.object_id AND p.post_status = 'publish'
LEFT JOIN {$wpdb->postmeta} pm ON pm.post_id = p.ID AND pm.meta_key = 'chapter_view'
GROUP BY t.term_id
ORDER BY views DESC
LIMIT 10";
$rows = $wpdb->get_results($sql);
5. Thống kê điểm review trung bình theo category
<?php
global $wpdb;
$sql = $wpdb->prepare(
"SELECT AVG(CAST(pm.meta_value AS DECIMAL(10,2))) AS avg_score
FROM {$wpdb->posts} p
JOIN {$wpdb->postmeta} pm ON pm.post_id = p.ID AND pm.meta_key = %s
WHERE p.post_type = %s AND p.post_status = %s",
'review_score', 'review', 'publish'
);
$avg_score = (float) $wpdb->get_var($sql);
6. Phân trang với OFFSET/LIMIT
<?php
global $wpdb;
$page = max(1, (int) ($_GET['page'] ?? 1));
$per_page = 50;
$offset = ($page - 1) * $per_page;
$sql = $wpdb->prepare(
"SELECT ID, post_title
FROM {$wpdb->posts}
WHERE post_type = %s AND post_status = %s
ORDER BY post_date DESC
LIMIT %d OFFSET %d",
'post', 'publish', $per_page, $offset
);
$items = $wpdb->get_results($sql);
7. Tìm kiếm có điều kiện với LIKE an toàn
<?php
global $wpdb;
$keyword = sanitize_text_field($_GET['s'] ?? '');
$like = '%' . $wpdb->esc_like($keyword) . '%';
$sql = $wpdb->prepare(
"SELECT ID, post_title
FROM {$wpdb->posts}
WHERE post_type = %s AND post_status = %s
AND (post_title LIKE %s OR post_content LIKE %s)
LIMIT 20",
'post', 'publish', $like, $like
);
$results = $wpdb->get_results($sql);
8. Cập nhật hàng loạt bằng CASE WHEN
<?php
global $wpdb;
$updates = [
101 => 5,
202 => 9,
303 => 0,
];
$ids = array_keys($updates);
$placeholders = implode(',', array_fill(0, count($ids), '%d'));
$case = '';
$params = [];
foreach ($updates as $pid => $score) {
$case .= ' WHEN %d THEN %d';
$params[] = $pid;
$params[] = $score;
}
$sql = "
UPDATE {$wpdb->postmeta}
SET meta_value = CASE post_id $case ELSE meta_value END
WHERE meta_key = %s AND post_id IN ($placeholders)
";
$params[] = 'review_score';
$params = array_merge($params, $ids);
$wpdb->query( $wpdb->prepare($sql, $params) );
9. Xóa transients đã hết hạn (dọn rác)
<?php
global $wpdb;
// Chú ý: wp_options có thể là InnoDB/auto-clean ở WP mới, nhưng thủ thuật này vẫn hữu ích trên site cũ
$now = time();
$wpdb->query(
$wpdb->prepare(
"DELETE FROM {$wpdb->options}
WHERE option_name LIKE %s
AND option_name NOT LIKE %s
AND option_value < %d",
$wpdb->esc_like('_transient_timeout_') . '%',
$wpdb->esc_like('_site_transient_timeout_') . '%',
$now
)
);
10. “Upsert” nhẹ với ON DUPLICATE KEY (bảng custom)
<?php
global $wpdb;
// Bảng custom có unique key (user_id, post_id)
$table = $wpdb->prefix . 'manga_stats';
$sql = "
INSERT INTO $table (user_id, post_id, view_count, updated_at)
VALUES (%d, %d, %d, %s)
ON DUPLICATE KEY UPDATE
view_count = view_count + VALUES(view_count),
updated_at = VALUES(updated_at)
";
$wpdb->query( $wpdb->prepare($sql, get_current_user_id(), $post_id, 1, current_time('mysql')) );
Mẹo nhanh khi viết query
- Dùng
%d,%s,%fđúng kiểu; không ép kiểu linh tinh trong SQL. - Tạo placeholder động cho mệnh đề
INbằngimplode(',', array_fill(...))rồipreparevới mảng tham số. - Ưu tiên
COUNT(1)và chọn cột cụ thể thay vìSELECT *. - Nhớ LIMIT/OFFSET, và thêm index phù hợp cho cột lọc/ORDER BY.
- Nếu query nặng và lặp lại, cache kết quả bằng Transients hoặc object cache.
Kết luận
$wpdb là công cụ mạnh mẽ nhưng đi kèm trách nhiệm: bạn phải tự tối ưu và tự bảo mật. Luôn dùng $wpdb->prepare(), tránh query trong vòng lặp, và cache kết quả nếu query nặng. Khi dùng đúng, $wpdb có thể giúp bạn xây dựng những tính năng mạnh mẽ và hiệu năng cao trong WordPress.
Bình luận