Skew is one row that too many other rows point at. It shows up in three places — the owner of the records, the parent in a relationship, and the target of a lookup, and it always causes the same two problems. Writes queue behind that one row, and any change to it forces the platform to rewrite a very large number of sharing entries. The number to remember is 10,000. Past that, you are designing a bottleneck.
The three shapes, and the one line they share
Salesforce documents each of these separately, which hides the fact that they are the same problem.
Ownership skew is a single user owning more than 10,000 records of one object. The usual culprit is not a person. It is an integration user, or a queue, or the admin who ran the data load two years ago.
Parent-child skew is more than 10,000 child records hanging off one parent. The classic version is a catch-all account (“Unassigned”, “Web Leads”, “Consumer”) with every orphan record in the org attached to it.
Lookup skew is a large number of records whose lookup field points at the same target row. This one can appear on any object, because you can put a lookup anywhere, which is exactly why it gets missed in design reviews.
In all three cases one row is a shared resource, and the platform has to protect it.
Problem one: everything serialises on that row
When you insert or update a child record, the parent is locked for the duration of the transaction. Same for the target of a lookup: it gets locked so the relationship stays valid. That is correct behaviour, and at normal volumes you never notice it.
At skewed volumes you notice immediately. Two parallel data-load batches that both contain children of the same parent are now fighting over one lock, and one of them loses with UNABLE_TO_LOCK_ROW. The error names the child object, so the first hour of debugging usually goes into the wrong place. We wrote about that failure in detail in what is actually locked when you get UNABLE_TO_LOCK_ROW.
The practical consequence for design: a skewed parent turns parallel work into sequential work. You can buy hardware and pay for a bigger org, and it will not help, because the constraint is one row.
Problem two: sharing recalculation has to rewrite everything
This is the one that surprises people, and it is the reason ownership skew has its own rule.
Salesforce stores record access in sharing tables. When a user owns a record, that user’s managers get access through the role hierarchy, and each of those grants is a row. Move a user in the hierarchy, or into or out of a role or a group used by a sharing rule, and every one of those rows has to be recalculated.
For a user who owns 40 records this is nothing. For an integration user who owns 800,000 records, the platform has to adjust a very large number of sharing entries, and the documentation is blunt about the outcome: a long-running recalculation of access rights.
Salesforce has made this less painful than it used to be. Owner-based sharing recalculation after a group or role change now runs asynchronously when the platform decides that will perform better. The role change itself applies immediately. The sharing updates land in the background, admins get an email when the job starts and finishes, and you can watch it on the Background Jobs page.
Asynchronous is better than blocking. It is not the same as free. The window between “the role changed” and “sharing finished” is a window where some users cannot see records they are supposed to see, and if you plan a reorg for Monday morning you will spend Monday morning explaining that.
The fix for ownership skew is smaller than it sounds
Take the skewed owner out of the role hierarchy.
That is the documented recommendation, and it works because the recalculation cost comes from the hierarchy: no role above the owner means no ancestor grants to rewrite. If the user must have a role — because reports or sharing rules depend on it — put them in their own role at the top of the hierarchy, on their own, and then leave them there.
Three habits keep this from coming back:
- Give integrations their own user, with no role. One integration user per system, each owning its own records, each outside the hierarchy.
- Do not park records on a person. If ownership is a placeholder, own the records with a queue, and check whether the queue’s group membership is doing the same damage the role would have.
- Ask Support about deferring sharing calculation before a bulk change. For a large one-off (a migration, a mass reassignment) deferring the recalculation and running it once at the end is far cheaper than recalculating per change.
Parent-child and lookup skew: spread the target
There is no setting for these. The fix is the data model.
For a catch-all parent, use several. Ten “Unassigned” accounts with 5,000 children each behave completely differently from one with 50,000, and the change is usually a small edit to whichever automation assigns the parent. Round-robin on a hash of the record Id is enough. It does not have to be clever, it has to be spread.
For lookup skew, the same idea, plus one more option: let the field stay empty. If a lookup is only meaningful for some records, an empty lookup takes no lock at all. Populating a default value on every record so reports look tidy is how orgs create lookup skew on purpose.
If you cannot spread the target, reduce concurrency instead. Sorting a load file by parent Id so that children of one parent land in one batch turns lock contention into ordinary sequential work inside a single batch, and it costs nothing.
How to find skew you already have
You do not need a tool. Run the counts.
SELECT OwnerId, COUNT(Id)
FROM Opportunity
GROUP BY OwnerId
ORDER BY COUNT(Id) DESC
LIMIT 10
Do the same for each parent field on your high-volume objects, and for any lookup that points at a small object from a large one. Anything over 10,000 is a finding. Anything over 100,000 is why your data loads fail at month end.
Two notes from doing this: run it for every object that carries volume, not just the obvious ones, because the offender is often a custom object nobody thinks about. And run it again after every migration, because migrations are how skew arrives.
What I would do, and when I would not bother
On a new build, I would spend twenty minutes on this during data modelling: name the integration users, decide they have no role, decide what happens to records with no natural parent, and write it down. It is one of the cheapest architecture decisions available, and it is nearly impossible to retrofit once ten million rows point at the wrong place.
On an existing org, I would not go looking for skew unless there is a symptom — lock errors, slow loads, a reorg that took a weekend. Skew below the threshold is not worth fixing, and moving ownership of a million records to fix a number in a report is its own outage.
Frequently asked
Is 10,000 a hard limit? No. Nothing fails at 10,001. It is the point where Salesforce stops calling the distribution normal, and where the cost of a hierarchy change starts to be measured in minutes rather than milliseconds.
Does skew matter if the object has no sharing rules? The locking half still applies. The recalculation half is much cheaper on an object with a public read/write org-wide default, because there is less to recalculate.
Do queues have the same problem as users? A queue owns records through a group, so a queue with a huge number of records has the same recalculation profile when its membership changes. Fewer people change queue membership on a Monday, which is the only reason it hurts less.
Will Big Objects solve this? Different problem. Big Objects change where high-volume data lives. They do not change how sharing is calculated for the standard objects you left behind.
