
Every WordPress site owner eventually needs to swap one string for another across the whole database: moving from a staging domain to production, switching from HTTP to HTTPS, or renaming a multisite subdomain. The instinct is to open phpMyAdmin and run a SQL UPDATE ... REPLACE() query. That instinct causes more broken sites than almost any other single mistake in WordPress maintenance, and the reason has nothing to do with SQL syntax. It has to do with how WordPress stores data.
Why plain SQL replaces break things
WordPress stores a lot of structured information (widget settings, theme mods, page builder layouts, plugin options) as PHP serialized strings inside single database columns. A serialized string is not just text, it is text with an embedded length count. A simple array might be stored like this:
a:2:{s:4:"name";s:11:"Robbie Chen";}Notice the s:11 before "Robbie Chen". That number tells PHP exactly how many bytes to expect for that string. If you run a SQL REPLACE() that changes Robbie Chen to something a different length, say a domain name of a different character count, the string in the array changes but the length prefix does not. PHP’s unserialize() function reads the old length, grabs the wrong number of characters, and fails. Depending on the WordPress version and the data involved, that failure shows up as a blank widget, a missing theme setting, a fatal error on a specific page, or in worse cases a completely white screen on the whole site.
This is not a rare edge case. Serialized data shows up constantly in the wp_options table, in post meta for page builders like Elementor or Divi, in widget configurations, and in many plugin settings. Any find-and-replace across a full WordPress database is going to touch serialized data whether you intend it to or not.
What wp-cli does differently
wp-cli’s search-replace command was built specifically to solve this problem. Instead of treating the database as flat text, it understands PHP serialization. When it finds a serialized value, it unserializes it, walks through the actual data structure, replaces the target string inside the real array or object, then re-serializes the whole thing with a correct, freshly calculated length prefix. The result is a database that is structurally valid, not just visually similar.
This is the single biggest reason wp-cli is the standard tool for WordPress database migrations, and why most competent hosts and developers refuse to run raw SQL replaces on a live site.
Running a safe search-replace
The basic syntax replaces an old string with a new one across all tables that wp-cli recognizes as belonging to WordPress:
wp search-replace 'https://staging.example.com' 'https://example.com'Before running that for real, always do a dry run. This reports what would change without touching a single row:
wp search-replace 'https://staging.example.com' 'https://example.com' --dry-runRead the summary output. It tells you how many tables were scanned, how many cells were changed, and flags anything unusual. If the numbers look wildly off from what you expect (say, a single-site install reporting changes across dozens of tables it should not touch), stop and investigate before proceeding.
Once you are confident, drop --dry-run and let it run. On larger databases this can take a while; that is expected, since wp-cli is unserializing and re-serializing data rather than doing a blind text swap.
Flags that matter
A handful of flags make search-replace more precise and safer for production use:
--skip-columns=guid: theguidcolumn inwp_postsis meant to be a permanent, unique identifier for each post and generally should not change during a domain migration. Skipping it avoids altering historical GUIDs that some feeds or external services may reference.--precise: forces wp-cli to use a PHP-based string search instead of a faster SQL-based one. This is slower but more reliable when dealing with multibyte characters or unusual serialization edge cases. Use it if you see any warning about serialized data problems, or when you are not fully confident in the standard pass.--all-tables: extends the operation beyond WordPress’s own tables to every table in the database, useful if a plugin created custom tables outside the standard prefix convention.--export=backup-before-replace.sql: instead of writing changes directly to the database, this exports the result to a SQL file so you can review or import it manually. Handy for a final safety check.--network: for multisite installs, this runs the replacement across every site in the network rather than just the current one. Multisite serialized data spans multiple blog ID prefixes, so this flag matters more than it might seem.
A well-formed production command for a domain migration often looks like this:
wp search-replace 'https://staging.example.com' 'https://example.com' --skip-columns=guid --precise --dry-runConfirm the dry run looks right, then remove --dry-run and run it for real.
Back up first, always
Even a tool built to respect serialization is still making bulk edits to your live database. Before any search-replace operation, take a full database export:
wp db export pre-replace-backup.sqlKeep that file somewhere safe until you have verified the site works correctly after the change. If your host takes automatic backups (this is one of the routine things a managed host like ServerBorn handles in the background), that is a good safety net, but a manual export you control gives you an exact restore point tied to this specific operation.
Common gotchas
A few things trip people up even when using wp-cli correctly. Hardcoded URLs inside serialized page builder content sometimes live in unusual meta keys that are easy to overlook when you are scanning results manually, so trust the tool’s full-table scan rather than trying to guess which tables matter. HTTP to HTTPS migrations are a special case of search-replace, not a separate process; a plain string swap of http:// for https:// works the same way. And on multisite, remember that each site in the network has its own table prefix and its own set of serialized option data, which is exactly why the --network flag exists rather than requiring you to loop through sites manually.
The takeaway
Serialized data is not a WordPress quirk to work around, it is a core part of how the platform stores structured settings efficiently. Raw SQL replaces ignore that structure and quietly corrupt it. wp-cli’s search-replace command was built to respect it, correctly rewriting length prefixes as it goes. Run a dry run first, back up your database, use --skip-columns=guid and --precise where appropriate, and a domain migration that used to be a source of dread becomes a five-minute task with a predictable, verifiable outcome.