Your foreign keys are not protecting your EAV data
The constraint says ON DELETE CASCADE. Removing nine attributes still left nearly a million orphaned rows, because setup:upgrade turns foreign key checks off.
A data patch removed nine customer attributes. The patch's own DocBlock said the setup helper "cascades the stored values", which sounded right, because every EAV value table does declare the constraint:
FOREIGN KEY (attribute_id) REFERENCES eav_attribute (attribute_id)
ON DELETE CASCADEAfter it ran in production there were 922,986 orphaned rows across three tables.
Why the constraint did nothing
The patch runs inside setup:upgrade, and setup:upgrade disables FOREIGN_KEY_CHECKS for the duration. With checks off, the cascade is not enforced. The parent rows in eav_attribute were deleted, the value rows were not, and MySQL raised nothing because it had been told not to look.
The constraints were present and unchanged the entire time. They were simply not in force at the one moment they mattered.
The shape of the damage
It was not spread evenly. The entity-id side was clean, product value tables were clean, address value tables were clean. Only the attribute side of the customer value tables was affected, and in one of them well over half the rows were orphans. That lopsidedness is a clue in itself: this is specifically what attribute removal leaves behind, not general database rot.
Count them, do not reason about them
The lesson I would want someone to take from this is narrow. Do not argue from the presence of a constraint. Go and count:
SELECT COUNT(*) FROM customer_entity_int v
WHERE NOT EXISTS (
SELECT 1 FROM eav_attribute a
WHERE a.attribute_id = v.attribute_id
);Run it per value table. It takes a minute and it either returns zero or it does not.
And the ticket is not done when the patch runs
An attribute removal needs the value cleanup written into the release instructions as well, plus an OPTIMIZE TABLE afterwards. Deleting the rows logically does not give the space back: InnoDB reported only a few megabytes of free space per table even after nearly a million rows were gone.
None of this is exotic. It is just that "there is a foreign key" feels like an answer, and here it was not one.
