Migrating a WordPress site to a new domain, switching http to https, or rebranding all need URL updates across many database tables. This DevAnswers guide walks the safe canonical paths: wp-cli search-replace for shell users, Better Search Replace plugin for non-CLI workflows. The headline rule: never use a plain SQL REPLACE() on a WordPress database; it silently corrupts serialized PHP data.
Prerequisites
- A WordPress 6.x install on a Linux server (Ubuntu 22.04 / 24.04 / 26.04 or Debian 12 / 13).
- Shell access to the WordPress install directory (or sudo on the host) for the wp-cli path; admin access to wp-admin for the plugin path.
- WP-CLI installed for Method 1. If you don’t have it yet, see the official WP-CLI install handbook.
- A current backup of the WordPress database. This is non-negotiable; see the MySQL backup guide for the cron-automated workflow. If you haven’t secured your data yet, our guide on how to back up a WordPress site covers both files and databases.
- Database user with at least
SELECTandUPDATEprivileges on the WordPress tables for Method 1 and Method 2; Method 3 additionally requiresDROPon existing tables (root or equivalent admin user works for all three).
Why You Cannot Use a Plain SQL REPLACE()
The dangerous-and-common pattern (and the one the legacy 2019 version of this guide led with) is a raw MySQL update via phpMyAdmin or the mysql shell:
-- DO NOT RUN; this silently corrupts WordPress
UPDATE wp_options
SET option_value = REPLACE(option_value, 'http://oldsite.com', 'https://newsite.com');
The problem is invisible until later: WordPress stores plenty of values in wp_options, wp_postmeta, and wp_usermeta as serialized PHP arrays. Widget configs, theme mods, Advanced Custom Fields data, most plugin settings: all of them. Serialized PHP strings look like this:
a:1:{s:7:"siteurl";s:18:"http://oldsite.com";}
The s:18: tells PHP that the string that follows is exactly 18 bytes long. When you run a plain SQL REPLACE() that swaps http://oldsite.com (18 bytes) for https://newsite.com (19 bytes), the string changes but the s:18: prefix does not. PHP’s unserialize() hits the value, reads 18 bytes, and gets garbage. Widgets disappear from the sidebar, theme settings revert to defaults, plugins stop working.
UPDATE wp_options SET option_value = REPLACE(...) (or the same on wp_postmeta or wp_usermeta) on a WordPress database. The s:N:"..." length prefix becomes stale and corrupts widget configs, theme mods, ACF data, and most plugin settings. Use one of the two serialization-aware methods below (Method 1 or Method 2) instead. Method 3 is a last-resort fallback that is NOT serialization-aware and works only for same-length replacements. Method 1: wp-cli search-replace (Recommended)
The WP-CLI search-replace command handles PHP serialized data by unserializing the value, performing the find-replace, and reserializing with correct length prefixes. It is the canonical safe path on any server with shell access.
Change directory into the WordPress install (the folder that contains wp-config.php), then always run the command with --dry-run first to preview the count of replacements without writing anything:
cd /var/www/html
wp search-replace 'http://oldsite.com' 'https://newsite.com' --dry-run --all-tables
The output reports the per-table count of replacements that would happen. Inspect the numbers; if anything looks wrong (zero replacements where you expected matches, or matches in tables you didn’t intend to touch), refine the search string before running for real.
+------------------------+-----------------------+--------------+------+
| Table | Column | Replacements | Type |
+------------------------+-----------------------+--------------+------+
| wp_options | option_value | 7 | PHP |
| wp_postmeta | meta_value | 23 | PHP |
| wp_posts | post_content | 41 | SQL |
+------------------------+-----------------------+--------------+------+
Success: 71 replacements to be made.
When the dry-run looks right, re-run without --dry-run to commit:
wp search-replace 'http://oldsite.com' 'https://newsite.com' --all-tables
The --all-tables flag scans every table in the WordPress database, not just those registered to $wpdb. That’s usually what you want for a domain change because some plugins create their own tables outside the standard WordPress schema. Add --precise if you need to treat all column values as raw strings rather than letting the command auto-detect serialized values (rare; only useful when you know a column does not contain serialized data, since this skips the safe unserialize/re-serialize path).
Method 2: Better Search Replace Plugin (No CLI)
If you don’t have shell access (typical on shared hosting) or simply prefer a UI, the Better Search Replace plugin from WP Engine does the same serialization-aware work as wp-cli, all from the WordPress admin. It is free, GPL, and has been the canonical no-CLI alternative since 2015.
- Install the Better Search Replace plugin from the WordPress admin (Plugins → Add New → search for it).
- Activate the plugin. A new menu item appears under Tools → Better Search Replace.
- Enter the old string in the “Search for” field and the new string in “Replace with”.
- Hold Ctrl (or Cmd on macOS) and click each table you want to process (or pick the whole list).
- Leave “Run as dry run?” CHECKED, then click “Run Search/Replace”. The plugin reports the count of matches per table.
- Uncheck “Run as dry run?” and click again to commit the change.
The plugin handles all serialized data, supports case-insensitive search, and is particularly useful for migrating WordPress websites from a development or staging environment to production by updating the site URL and other references.
Method 3: Export, Find/Replace in Text Editor, Re-Import (Last Resort)
For environments without shell access AND without the ability to install plugins (some restrictive shared hosts), the legacy escape hatch is to export the database as SQL, do a text-editor find/replace, and re-import. This works only because the SQL dump file represents serialized values as literal text, but it is brittle: a careless replace can still corrupt serialized data if the old string and new string differ in byte length. Use it only when Method 1 and Method 2 are unavailable.
- Export the WordPress database as a .sql file (phpMyAdmin Export, or
mysqldumpif available). - Open the dump file in a text editor that handles large files (Sublime Text, VS Code, Notepad++). Do not use a word processor.
- Find and replace your old string with the new one. If the byte lengths differ, this will corrupt serialized values; consider this method only when the replacement is a same-length swap (e.g. one path segment to another of equal length).
- Drop the existing database tables (or import into a fresh DB) and re-import the edited .sql.
Verify the Migration
- Visit the homepage and confirm the new URL appears in the browser address bar and in
view-source. - Open a post or page that uses sidebar widgets and confirm the widgets still render.
- If the site uses Advanced Custom Fields or a page builder (Elementor, Beaver, Bricks), open an edit screen and confirm the field values and modules are intact (this is where serialized-data corruption usually surfaces).
- Check the WordPress site health screen (Tools → Site Health) for any new warnings.
Conclusion
The headline rule is unchanged whether you migrate a domain, switch to HTTPS, or rebrand: always use a serialization-aware tool, never a raw SQL REPLACE(). wp-cli is the canonical safe path from the command line; Better Search Replace is the plugin-based equivalent for no-CLI environments. Both have been the WordPress-community-recommended tooling for over a decade. After you’ve migrated, schedule the database backup as a daily cron so you never need to do disaster recovery again. The MySQL backup guide walks the production-grade cron script.
Thank you so much for laying this out and explaining it! I’ve found other “tutorials” but they all “assume” you already know where to put what. Your directions were EXACTLY what I was looking for.