Database schema
The plugin owns one custom table plus a small set of options, user metas, transients, and cookies. This page documents every storage location.
Custom table — {prefix}bp_profile_views
Section titled “Custom table — {prefix}bp_profile_views”Created on activation via dbDelta(). The DDL that ships in class-bp-profile-views-activator.php:
CREATE TABLE {prefix}bp_profile_views ( id mediumint(9) NOT NULL AUTO_INCREMENT, user_id int(10) NOT NULL, viewer_id int(10) NOT NULL, created datetime DEFAULT current_timestamp() NOT NULL, PRIMARY KEY (id), KEY user_created (user_id, created), KEY created (created));| Column | Type | Notes |
|---|---|---|
id |
mediumint(9) AUTO_INCREMENT PRIMARY KEY |
Row identifier. |
user_id |
int(10) |
Profile owner (the person being viewed). |
viewer_id |
int(10) |
Visitor. 0 for logged-out visitors. |
created |
datetime DEFAULT current_timestamp() |
Server time at insert; never updated. |
Indexes (since 1.5.3)
Section titled “Indexes (since 1.5.3)”Before 1.5.3 the table shipped with only PRIMARY KEY (id), so every aggregate the plugin runs was a full table scan. Two indexes were added:
| Index | Covers |
|---|---|
user_created (user_id, created) |
Every query that filters by profile owner, which is nearly all of them — the Views-tab chart, the directory badge, the admin leaderboard, and the Recent Visitor widget. The created half also serves ORDER BY created DESC, so the widget gets a backward index scan instead of a filesort. |
created (created) |
The time-window-only queries that never name a user — the Popular User widget and the Views Today stat. |
viewer_id is deliberately not indexed. The only predicate on it is viewer_id != 0, which is far too low-cardinality to help.
Existing installs are migrated automatically. bp_profile_views_ensure_indexes() runs on plugins_loaded, reads INFORMATION_SCHEMA first so it only adds what is missing, and records completion in the bp_profile_views_indexes_v1 option. It is idempotent — safe to run repeatedly — and only records completion once every index exists, so a failed ALTER is retried rather than skipped. Both ALTERs are INPLACE-eligible on InnoDB, so they do not lock reads or writes on a large table.
To confirm the migration ran on your site:
SHOW INDEX FROM wp_bp_profile_views;-- expect: PRIMARY, user_created, createdThere is no foreign-key constraint to wp_users, by design. Deleting a WordPress user does not cascade. Orphan-row cleanup is your responsibility (see Useful queries below).
Options (wp_options)
Section titled “Options (wp_options)”| Key | Type | Purpose |
|---|---|---|
bp_profile_views_general_options |
serialized array | Main settings blob (see below). |
bp_profile_views_db_version |
string | Stored schema version; used for future upgrade checks. |
bp_profile_views_visitor_setting_seeded |
flag | Set to '1' after the one-time visitor_setting meta seed runs. |
edd_wbcom_BPPV_license_key |
string | EDD license key. |
edd_wbcom_BPPV_license_status |
string | EDD license status (valid, invalid, etc.). |
edd_wbcom_BPPV_license_expires |
string | License expiry date string from EDD. |
The options bp_profile_views_admin_welcome_options, bp_profile_views_members_options, and bp_profile_views_support_options are legacy stubs from 1.4.x that never held real data. Their registration was dropped in 1.5.0, and the stale wp_options rows are deleted automatically on upgrade to 1.5.3 (and on uninstall).
bp_profile_views_general_options keys
Section titled “bp_profile_views_general_options keys”| Key | Default | Controls |
|---|---|---|
save_count_by |
'session' |
Dedupe strategy: 'session' or 'referer'. |
chart_style |
'line' |
Chart.js render mode on the Views tab ('line', 'bar', 'polarArea'). |
view_member_count |
10 |
Default row limit for widget/list displays. |
show_in_directory |
'no' |
Show a view-count badge on members directory cards when 'yes'. |
show_recent_members |
'no' |
Render the recent-visitors avatar strip in the profile header when 'yes'. |
allow_user_settings |
'no' |
Let members toggle their own tracking opt-out when 'yes'. |
exclude_logout_user_count |
'yes' |
Filter out viewer_id = 0 rows from aggregate queries when 'yes'. |
show_inside_header_meta |
(absent) | If 'yes', renders the strip on bp_profile_header_meta; otherwise theme-specific hook. |
allow_user_notification |
(absent) | Fire a BP notification on each qualifying view when 'yes'. |
max_display_in_header |
5 |
Maximum avatar count in the recent-visitors strip. |
default_avatar_size |
32 |
Avatar pixel size in the strip. |
User meta (wp_usermeta)
Section titled “User meta (wp_usermeta)”| Key | Purpose |
|---|---|
visitor_setting |
Per-user opt-out: 'yes' = allow tracking, 'no' = skip. Seeded once on activation. |
bp_profile_views_user_login_session |
Array of profile_{user_id}_{session_token} strings used to dedupe views within a single login session. |
Transients and cookies
Section titled “Transients and cookies”| Key | Type | TTL | Purpose |
|---|---|---|---|
bp_profile_views_referer_transient |
transient | 6 hours | Stores HTTP_REFERER for save_count_by = 'referer' dedupe. |
bp_profile_visit |
transient | 6 hours | Logged-out visitor dedupe. |
edd_wbcom_BPPV_license_key_data |
transient | 24 hours | Cached license metadata from the EDD API. |
BP_PROFILE_VIEW_REQUEST_URI |
cookie | session | Last request URI; used only in 'referer' mode. |
BP_IS_USER |
cookie | session | Whether the prior request was a user-profile page; used only in 'referer' mode. |
The cookies and the bp_profile_views_referer_transient transient are written only when save_count_by = 'referer'. In the default 'session' mode, none of the above cookie or referer-transient writes happen.
Useful queries
Section titled “Useful queries”Top profiles by lifetime views
Section titled “Top profiles by lifetime views”global $wpdb;$rows = $wpdb->get_results( $wpdb->prepare( "SELECT user_id, COUNT(*) AS total FROM {$wpdb->prefix}bp_profile_views WHERE viewer_id != 0 GROUP BY user_id ORDER BY total DESC LIMIT %d", 100 ));Unique viewers for a profile
Section titled “Unique viewers for a profile”$viewer_ids = $wpdb->get_col( $wpdb->prepare( "SELECT DISTINCT viewer_id FROM {$wpdb->prefix}bp_profile_views WHERE user_id = %d AND viewer_id != 0 ORDER BY id DESC LIMIT %d", $user_id, $limit ));Cleanup orphans (users that no longer exist)
Section titled “Cleanup orphans (users that no longer exist)”DELETE pv FROM {prefix}bp_profile_views pvLEFT JOIN {prefix}users u ON u.ID = pv.user_idWHERE u.ID IS NULL;Trim old rows
Section titled “Trim old rows”wp db query "DELETE FROM \`$(wp db prefix)bp_profile_views\` WHERE created < DATE_SUB( NOW(), INTERVAL 1 YEAR );"Performance tuning
Section titled “Performance tuning”Since 1.5.3 the two indexes the plugin’s own queries need ship by default — see Indexes above. You should not need to add anything for normal operation.
Measured on a seeded install, the admin leaderboard query went from a full table scan to an index range scan:
before: type=ALL key=NULL rows=71after: type=range key=user_created rows=8Views Today became a covering index scan (no table access at all), and the Recent Visitor widget now uses a backward index scan instead of sorting in memory.
If you are still seeing slow queries, check these before adding indexes:
- Confirm the migration ran.
SHOW INDEX FROM {prefix}bp_profile_views;should listuser_createdandcreated. If it does not, see Indexes. - Check your object cache. The leaderboard and the Overview stat cards are cached for one minute. Without a persistent object cache (Redis / Memcached) that cache is per-request only, so every page load recomputes them.
- Consider trimming history. A view row is only useful for as long as you report on it — see Trim old rows above.
Only add further indexes if you have a specific slow query and an EXPLAIN that shows the existing indexes are not being used. Each index adds write overhead on every profile view, and this table is write-heavy by nature.
One case that is genuinely not covered: if you query by viewer rather than by profile owner (for example, “which profiles has member X visited?”), no shipped index serves that. Add one only if you have built such a report:
CREATE INDEX idx_bpv_viewer ON {prefix}bp_profile_views (viewer_id, created);Where to next
Section titled “Where to next”- REST and AJAX — endpoints that read this table.
- Hooks and filters — actions fired on insert.
- Uninstall — what gets dropped vs. preserved.

