Back to Blog

Why Your AI's Customer Count Includes People You Already Deleted

Asked for active customers and the number is high. The table never DELETEs — it sets deleted_at. The AI counted every row. Soft-deleted, archived, and cancelled records look like live ones until you filter them.

You ask for "how many customers do we have." The AI returns 14,200. The admin UI says 11,800. No join, no OR, no EXTRACT month. The extra 2,400 rows have deleted_at set. They are still in the table. The model never filtered them.

Soft delete is a column, not a DELETE

-- what the model writes
SELECT COUNT(*) FROM customers

-- what the app's default scope writes
SELECT COUNT(*) FROM customers WHERE deleted_at IS NULL

Rails (acts_as_paranoid, Discard), Laravel (SoftDeletes), Prisma (deletedAt), and a lot of hand-rolled schemas never remove the row. They stamp deleted_at / discarded_at / archived_at / is_deleted. The live UI applies that scope on every query. A chat that only saw the table name does not.

Variants that smell the same:

-- boolean flag, often 0/1 not true/false
WHERE is_deleted = false

-- status that includes a tombstone the English ignored
WHERE status NOT IN ('deleted', 'archived', 'merged')

-- subscriptions: cancelled is not deleted
WHERE cancelled_at IS NULL

Boolean mismatch ('f' vs false vs 0) stacks on top of is_deleted. Empty string vs NULL stacks if someone stored '' instead of a timestamp.

This is an over-count. The NULL filter post is an under-count. Don't mix them.

How to catch it

describe_table customers
List columns that look like deleted_at, discarded_at, archived_at,
is_deleted, cancelled_at, status.
COUNT(*) vs COUNT(*) WHERE deleted_at IS NULL.
If they differ, the dashboard is using the second one.

If describe_table shows no such column, check a status enum and a related subscriptions table before you trust COUNT(*).

The prompt that heads this off

Before counting people, orders, or orgs, describe_table and look for
deleted_at / discarded_at / archived_at / is_deleted / cancelled_at.
Default to the same scope the app UI uses (usually deleted_at IS NULL).
Show both counts if you are not sure which I mean.

The short version

"How many customers" in English means live ones. COUNT(*) means every row the table still holds. If the product soft-deletes, those are not the same number until you add deleted_at IS NULL.


Read-only MCP gateway: mcpserver.design.

Related reading: