A blind MySQL string replacement on a WordPress database will wreck your site within seconds. You need WP-CLI to inspect, test, and run the domain change properly without corrupting serialized PHP data.
I learned this the hard way years ago while migrating an e-commerce client from staging to production. I ran a standard SQL REPLACE() inside phpMyAdmin to update the domain. Post links updated cleanly, but our theme customizer settings wiped out, widgets reverted to empty defaults, and custom meta vanished. That happens because WordPress packs tons of plugin and theme settings into serialized PHP strings.

Why Standard SQL Queries Corrupt Serialized Data
Serializing is how PHP packs complex data structures—like nested arrays and objects—into a flat string stored in a single database cell. Crucially, it records the exact character count of every string inside that data.
Here is what a serialized string looks like in your wp_options table:
a:1:{s:18:"header_logo_target";s:21:"http://dev.domain.lan";}See that s:21: tag right before the URL? It tells PHP’s parser that the next value is a string exactly 21 characters long. If you run a raw SQL query or do a Find/Replace inside a .sql dump to swap http://dev.domain.lan (21 characters) with https://example.com (19 characters), the string changes to this:
a:1:{s:18:"header_logo_target";s:21:"https://example.com";}The parser expects 21 characters, but only reads 19. When PHP calls unserialize() on that field, parsing fails, returns false, and discards the entire block. To WordPress, that setting simply no longer exists.
The WP-CLI search-replace command avoids this headache completely. It queries the database, extracts matching strings, unpacks serialized objects in memory, swaps the URL, recalculates byte lengths, and repacks everything before saving.
Verifying WP-CLI Access and Database Credentials
Before touching your tables, make sure WP-CLI can access your shell and read your wp-config.php file. SSH into the server, head to the web root, and test the connection.
Here is what I ran on an Ubuntu 22.04 stack with WP-CLI 2.9.0 and PHP 8.2:
cd /var/www/html/example.com
wp core is-installed --path=/var/www/html/example.comIf that looks good, confirm WP-CLI can actually pull data by grabbing the current site URL:
wp option get siteurl
wp option get homeIf you hit a database connection error here, your SSH user probably lacks permission to read wp-config.php, or MySQL is listening on a socket your CLI environment is not pointing to. Check file permissions first:
chmod 640 wp-config.php
ls -la wp-config.phpOn shared hosting or staging setups, confirm your SSH session runs the same PHP version as your web server. Take a look at our guide on how to import large WordPress databases via SSH on Hostinger if you hit memory or execution limits during this step.
Step 1: Create an Uncompressed SQL Snapshot
Do not run a search-and-replace without a snapshot. WP-CLI wraps updates inside an InnoDB transaction, but you still want a raw SQL dump handy so you can roll back within ten seconds if things go sideways.
Run wp db export to dump the database locally:
wp db export pre-migration-backup.sqlOn my 380MB database (around 410,000 rows across 38 tables), this took about 4 seconds. Move that backup outside the public web root so nobody can download it through a browser:
mv pre-migration-backup.sql ../backups/pre-migration-backup-$(date +%F).sqlStep 2: Run a Dry Run to Audit Matches
WP-CLI’s most useful feature here is the --dry-run flag. It processes the queries, unpacks serialized strings, computes replacements, and prints a summary table—all without saving a single change.
Pass your source and target URLs to test the waters:
wp search-replace 'https://staging.example.com' 'https://example.com' --dry-runYou will get a clear breakdown showing each table scanned, replacement counts, and whether serialized data was touched:
+------------------+----------------+--------------+----------------+
| Table | Column | Replacements | Type |
+------------------+----------------+--------------+----------------+
| wp_options | option_value | 42 | PHP serialized |
| wp_posts | post_content | 318 | SQL |
| wp_posts | guid | 318 | SQL |
| wp_postmeta | meta_value | 89 | PHP serialized |
| wp_usermeta | meta_value | 2 | PHP serialized |
+------------------+----------------+--------------+----------------+
Success: 769 replacements to be made.Check those numbers. If you see zero replacements across the board, double-check your trailing slashes. Including a trailing slash on one URL but omitting it on the other will throw off the match.
Step 3: Handling the GUID Column and Trailing Slashes
Keep your URL formats consistent. If your old URL ends with a slash, the replacement must end with one too. Otherwise, you get double-slashed URLs (like https://example.com//about) or missing separators.
Also: leave the guid column in wp_posts alone on any site that has been live. RSS readers rely on the GUID (Globally Unique Identifier) to identify new posts. If you change them, feed readers will treat your entire archive as unread and blast subscribers with duplicate notifications.
Tell WP-CLI to skip that column with --skip-columns=guid:
wp search-replace 'https://staging.example.com' 'https://example.com' --skip-columns=guid --dry-runIf you are simply migrating an existing domain from HTTP to HTTPS, run a targeted protocol replacement instead:
wp search-replace 'http://example.com' 'https://example.com' --skip-columns=guid --dry-runThis catches hardcoded HTTP image links in post content that trigger mixed content warnings in browsers.
Step 4: Execute the Live Database Replacement
Once your dry run returns the expected counts and excludes the GUID column, drop --dry-run to write changes to MySQL.
Run the live replacement:
wp search-replace 'https://staging.example.com' 'https://example.com' --skip-columns=guid --all-tablesThe --all-tables flag ensures tables created by plugins—like WooCommerce, Elementor, or form builders—are updated alongside standard core tables.
Your terminal will print the results as it commits the changes:
+------------------+----------------+--------------+----------------+
| Table | Column | Replacements | Type |
+------------------+----------------+--------------+----------------+
| wp_options | option_value | 42 | PHP serialized |
| wp_posts | post_content | 318 | SQL |
| wp_postmeta | meta_value | 89 | PHP serialized |
| wp_usermeta | meta_value | 2 | PHP serialized |
+------------------+----------------+--------------+----------------+
Success: Made 451 replacements.This took just under 1.5 seconds on our test server. If you are handling a live site migration and cannot afford downtime, check our walkthrough on how to migrate WordPress without downtime to time your database swaps with DNS propagation.
Step 5: Flush Object Cache and Rewrite Rules
Direct database updates do not invalidate your object cache. If you run Redis, Memcached, or another caching layer, PHP will keep serving old URLs straight out of memory until you clear it.
Flush both your object cache and rewrite rules from the command line:
wp cache flush
wp rewrite flush --hardUsing --hard forces WordPress to rewrite .htaccess or update your Nginx routing rules, stopping redirect loops before they start. If you use Redis on your server, see our guide to enable Redis Object Cache in hPanel to verify that the cache connection drops and reconnects cleanly.
If you use page builders like Elementor, regenerate their static CSS. Elementor writes absolute image URLs into CSS files under wp-content/uploads/elementor/css/:
wp elementor flush-cssGotchas and Edge Cases
1. The Subdomain .htaccess Conflict
If you migrated from a subdomain into your root directory (or vice versa), Apache may inherit rules from the parent directory and redirect traffic back to staging. Read our guide on fixing root .htaccess inheritance issues if your site throws unexpected 404s after a search-replace.
2. Multisite Networks Require –url Flag
On a WordPress Multisite network, running a basic search-replace can alter global tables and break routing for child sites. Always isolate the target sub-site using the --url parameter:
wp search-replace 'sub.staging.com' 'sub.production.com' --url=sub.staging.com --network3. Escaped JSON Strings in Elementor and Divi
Some builders store URLs as escaped JSON strings, like this:
{ "bg_image": "https://staging.example.com/bg.jpg"
}Recent WP-CLI builds handle most standard JSON strings. But if your dry-run skips matches in custom builder tables, run a second pass specifically targeting escaped slashes:
wp search-replace 'https://staging.example.com' 'https://example.com' --all-tablesFrequently Asked Questions
Can I use Better Search Replace instead of WP-CLI?
Yes. The Better Search Replace plugin uses the same deserialization approach. But on databases over 200MB, web requests often time out or blow past FastCGI buffers. WP-CLI runs in the CLI without HTTP timeout restrictions.
Does search-replace modify hardcoded values in wp-config.php?
No. WP-CLI only modifies the database. If you hardcoded WP_HOME or WP_SITEURL inside wp-config.php, those constants override database values. You will need to open wp-config.php in nano or vim and update them manually.
What should I do if the search-replace command times out?
If you have massive wp_postmeta or log tables, pass the --table-page-size flag so WP-CLI processes records in smaller chunks instead of loading entire tables into memory at once:
wp search-replace 'https://old.com' 'https://new.com' --table-page-size=5000 --all-tablesWill this fix my SSL mixed content warnings?
Yes. Swapping http://example.com for https://example.com across all tables updates hardcoded image paths, media embeds, and stylesheet links, fixing the mixed-content warnings in the browser.
Next Steps
Once you finish the search-replace and clear the cache, open an incognito window, bring up your browser’s Developer Tools, and check the Network tab. Make sure all CSS, JS, and image assets load with 200 OK status codes from the new domain. If you run into weird 500 errors on inner pages, check our guide on fixing root .htaccess rules and directives to ensure your server handles URL rewriting properly.

