Why duplicate SKUs cost you orders
Inside your store the SKU looks like a label. Outside it, the SKU is the key. Warehouse software, accounting exports, marketplace feeds and stock synchronization all look up products by SKU, and a lookup that returns two rows behaves unpredictably.
- Orders pick the wrong product. A pick list, a packing slip or a fulfilment integration that resolves a SKU may land on the other product, and the customer receives the wrong item.
- Stock counts drift. Inventory tools that sync by SKU write one product's quantity onto the other, so one record oversells while the other sits on phantom stock.
- Feeds and imports collide. Google Merchant Center and marketplace feeds treat the SKU as the item identifier; duplicates get rejected or silently overwrite each other, and the next CSV import cannot tell which row it should update.
- Reporting merges two histories. Sales and returns for two different products get summed under one code, which makes the numbers you plan on quietly wrong.
How to find duplicate SKUs manually
WooCommerce stores the SKU in postmeta under _sku, for
both products and variations. With WP-CLI, this query lists every
exact-match SKU that appears more than once among published records:
wp db query "SELECT m.meta_value AS sku,
COUNT(*) AS uses,
GROUP_CONCAT(p.ID ORDER BY p.ID) AS post_ids
FROM wp_postmeta m
JOIN wp_posts p ON p.ID = m.post_id
WHERE m.meta_key = '_sku'
AND m.meta_value <> ''
AND p.post_type IN ('product', 'product_variation')
AND p.post_status = 'publish'
GROUP BY m.meta_value
HAVING uses > 1
ORDER BY uses DESC, sku;"
Adjust the table prefix if yours isn't wp_. The
post_ids column tells you which products or variations to
open in wp-admin.
This works, with the same three gaps as most one-off queries. It only
catches byte-for-byte matches, so ABC-123 and
abc-123, or a SKU pasted with a trailing space or a
typographic dash, slip past it while your warehouse system treats them
as the same code. It does not check whether a variation's parent is
itself published, so it can report collisions shoppers never see. And
it answers once: the next import or API sync can reintroduce the same
collision the following week with nobody watching.
How CatalogLift finds it
Every scan reads the SKU of each published product and each published variation of a published parent, and compares them after normalization: Unicode composition, invisible zero-width characters removed, dashes of every kind reduced to a plain hyphen, spaces around hyphens dropped, repeated whitespace collapsed, and case ignored. Records without a SKU are skipped, since an empty value is a different problem. Any normalized SKU shared by more than one record becomes a duplicate group, and every member of the group gets its own finding at high severity, carrying the raw SKU, the normalized form, how many records share it, and a sample of the colliding products and variations as evidence.
There is no AI in this check, because no model can know which of two products deserves the code. Instead you fix it in CatalogLift: with the whole group in front of you, you type the correct SKU for the record that should change, approve it, and CatalogLift writes it to WooCommerce. Nothing changes in the store before that decision, and the next scan verifies the group is gone. Because CatalogLift keeps watching, a future import that recreates the collision surfaces as a fresh finding instead of a silent regression.