Debugging de duplicados silenciosos de UpsertData
Una Script Activity corrió limpia pero los counts downstream están mal. UpsertData insertó duplicados en vez de actualizar porque el primary key del DE destino falta o está mal configurado. Seis queries que confirman el patrón del insert silencioso y recorren la recuperación.
Una Script Activity corrió limpia — el log muestra cada paso completado, ningún error capturado, el status de la Activity dice Completed. Al día siguiente una SQL Query Activity downstream que hace INNER JOIN ON SubscriberKey devuelve 60.000 filas en vez de las 50.000 esperadas. O un reporte que agrega por SubscriberKey devuelve totales inflados. O el mismo SubscriberKey aparece con distintos valores de Status entre filas que deberían haber sido un solo registro. El fingerprint es Platform.Function.UpsertData contra un DE destino cuyo primary key falta o está seteado en la columna equivocada — la función insertó duplicados en vez de actualizar. Ver gotchas — #4.
Esta página es el playbook de diagnóstico para esa forma exacta — seis queries que confirman el patrón de insert silencioso, ubican sobre qué key están los duplicados, y recorren la recuperación (dedupear el data, arreglar el schema del DE, parchear el script). La Activity no falló; el data está mal. Esa distinción es lo que hace que esta sesión de debug sea distinta de los otros dos snippets de SSJS.
Cómo pasa la falla
[ El script llama a Platform.Function.UpsertData ]
↓ el backend de MC chequea la config de primary key del DE destino
[ PK configurado correctamente en la columna correcta ]
↓ fila existe por PK → UPDATE / fila falta → INSERT
[ PK falta o está en la columna equivocada ]
↓ el backend de MC trata cada llamada como INSERT
[ Los duplicados se acumulan en silencio ]El status de la Activity reporta success porque los writes en sí anduvieron — solo que no eran los writes que pretendías. El script no puede detectar el bug en runtime porque la función devuelve "success" para los outcomes de insert y de update.
Las queries abajo detectan duplicados en el DE destino, prueban el patrón de insert silencioso, y producen los recibos que necesitás para arreglar el schema con confianza.
Paso 1 — Contá duplicados por la key esperada
Arrancá en el destino. Si pretendiste que SubscriberKey sea la key única, eso es lo que agrupás. Cualquier grupo con COUNT(*) > 1 es un duplicado.
-- Reemplazá 'master_subscribers' con el nombre del DE destino
-- Reemplazá 'SubscriberKey' con la columna que pretendiste como PK
SELECT
SubscriberKey,
COUNT(*) AS Copies
FROM master_subscribers
GROUP BY SubscriberKey
HAVING COUNT(*) > 1
ORDER BY Copies DESC;Tres formas de falla acá:
- La query devuelve filas: confirma que existen duplicados en
SubscriberKey. El PK del DE destino falta o está mal configurado. Seguí al paso 2. - La query no devuelve filas:
SubscriberKeyes único. El bug no son duplicados de UpsertData en esta columna — o los duplicados están sobre una key esperada distinta, o el problema delINNER JOINdownstream está en otro DE. Reapuntá el diagnóstico. - La query timeoutea: el DE destino es muy grande y no indexado. Agregá un
TOP 1000o filtrá a una ventana de fecha reciente para sacar una muestra primero.
Paso 2 — Perfilá las filas duplicadas en sí
Para las keys con duplicados, mirá qué es distinto entre las copias. El patrón te dice sobre qué columna está realmente el PK (vs la que pretendías).
SELECT
SubscriberKey,
Status,
EmailAddress,
CreatedDate,
UpdatedDate
FROM master_subscribers
WHERE SubscriberKey IN (
SELECT SubscriberKey
FROM master_subscribers
GROUP BY SubscriberKey
HAVING COUNT(*) > 1
)
ORDER BY SubscriberKey, UpdatedDate DESC;Qué significan los patrones:
- Las copias tienen valores distintos de
EmailAddresspara el mismoSubscriberKey: probablemente el PK del DE está sobre la columnaEmailAddress(noSubscriberKey). Cada corrida del script con un subscriber re-keyeado insertó una fila nueva. - Las copias tienen
EmailAddressidéntico peroUpdatedDatedistinto: no hay un PK enforced para nada. El mismo registro lógico fue insertado en cada corrida. - Las copias tienen todo idéntico excepto
CreatedDate: el DE fue reconstruido sin truncar, o dos automations están escribiendo al mismo DE sin coordinación.
Paso 3 — Cruzá referencia con el log del script
Confirmá que los duplicados vinieron de tus llamadas a UpsertData y no de otra fuente (una SQL Query Activity en modo Append, una Import Activity, etc.).
-- Reemplazá el rango de fechas con la ventana que sospechás
SELECT
RunId,
Step,
Ts,
Message
FROM de_log_ssjs_runs
WHERE Step LIKE '%upsert%'
AND Ts BETWEEN '2026-05-01' AND '2026-05-13'
ORDER BY Ts;El patrón esperado cuando UpsertData se está portando mal: muchas filas loggeadas a lo largo de muchos RunId para el mismo DE destino. Si el row count del DE destino creció en cada corrida del script en paralelo con el número de entradas de log upsert, el script es la fuente. Si no, los duplicados vinieron de otro lado — auditá los otros pasos de la Automation o cualquier Import Activity que apunte al mismo DE.
Paso 4 — Calculá unique-key count esperado vs actual
Un solo número que captura la inflación: cuántos valores distintos de la columna-key-esperada, vs cuántas filas en total.
SELECT
COUNT(*) AS TotalRows,
COUNT(DISTINCT SubscriberKey) AS UniqueSubscriberKeys,
COUNT(*) - COUNT(DISTINCT SubscriberKey) AS Inflation
FROM master_subscribers;Si Inflation está en los miles, el patrón de insert silencioso ha estado corriendo por muchas ejecuciones del script. Cruzá referencia de la magnitud contra el conteo del log del paso 3 para estimar cuándo arrancó la mala configuración — si tenés 8.000 de inflación y el log muestra 200 upserts por día por 40 días, el bug está vivo desde la marca de los 40 días.
Paso 5 — Recuperación: stageá el dedup
Antes de arreglar el schema, stageá una copia deduplicada del DE destino. El patrón es el mismo que la regla stage-validate-promote del Style Guide de SQL: escribí a staging, validá, y después promové.
-- Stageá las filas deduplicadas. Usá el patrón MAX-por-grupo de las
-- funciones agregadas de SQL; quedate con la fila con el UpdatedDate
-- más reciente por SubscriberKey.
SELECT
s.SubscriberKey,
s.Status,
s.EmailAddress,
s.CreatedDate,
s.UpdatedDate
INTO de_stg_master_subscribers_dedup
FROM master_subscribers s
INNER JOIN (
SELECT
SubscriberKey,
MAX(UpdatedDate) AS MaxUpdated
FROM master_subscribers
GROUP BY SubscriberKey
) latest
ON s.SubscriberKey = latest.SubscriberKey
AND s.UpdatedDate = latest.MaxUpdated;Antes de promover, validá el DE de staging: el row count iguala el unique-key count del paso 4, no quedan duplicados (re-corré el paso 1 contra el DE de staging), y el UpdatedDate más reciente para cada key matchea lo que el origen upstream habría escrito.
Después en la UI de Marketing Cloud:
- Abrí las propiedades del DE destino → marcá
SubscriberKeycomo Primary Key. (Puede que necesites recrear el DE si MC no te deja agregar un PK a un DE poblado — coordiná con stakeholders para la ventana de cutover.) - Truncá el DE destino.
- Corré una SQL Activity en modo
Overwrite:SELECT * FROM de_stg_master_subscribers_dedup. - Re-corré el paso 1 contra el DE destino para confirmar cero duplicados.
La próxima ejecución del script ahora se va a portar como un upsert real porque el PK está correctamente enforced.
Paso 6 — Escribí el postmortem
Escribí el diagnóstico a de_log_ssjs_postmortems (mismo DE usado por los otros dos snippets de debugging de SSJS).
INSERT INTO de_log_ssjs_postmortems
SELECT
GETDATE() AS DiagnosedAt,
'SA_NightlyEnrichment' AS ActivityName,
'multiple-runs' AS RunId, -- el bug abarca varias corridas
NULL AS StartedAt,
NULL AS LastWriteAt,
'upsert-master-subscribers' AS LastStep,
(SELECT COUNT(*) FROM master_subscribers) - (SELECT COUNT(DISTINCT SubscriberKey) FROM master_subscribers) AS RowInflation,
'El DE destino master_subscribers no tenía PK; UpsertData se comportó como InsertData en todas las corridas. Deduplicado vía de_stg_*, DE recreado con SubscriberKey como PK, Overwrite corrido desde staging.' AS RootCause;La cifra de RowInflation es el recibo — seis meses después cuando una auditoría pregunte "qué tan grave fue el tema de duplicate-key", la respuesta está a una query.
Causas comunes rankeadas por frecuencia
| Causa | Cómo detectarla | Fix en |
|---|---|---|
| El DE destino no tiene PK | El paso 1 encuentra duplicados; el paso 2 muestra filas idénticas | Recrear el DE con PK; dedupear + Overwrite desde staging |
| El PK está sobre la columna equivocada | El paso 2 muestra duplicados con EmailAddress distinto para el mismo SubscriberKey (o similar) | Confirmar la columna PK pretendida; recrear el schema del DE |
| La lista de columnas del script no incluye el PK | La llamada UpsertData omite SubscriberKey del array de keys | Auditar el script; agregar la columna PK al argumento keys-array |
| El PK está seteado pero el script escribe a un nombre de columna distinto | El DE tiene PK en SubKey, el script escribe a SubscriberKey | Auditar el schema del DE vs las constantes de nombres de columna del script |
| Múltiples fuentes escribiendo al DE sin coordinación | El paso 3 loggea pocos upserts pero el Inflation del paso 4 es grande | Auditar el flujo de la Automation; chequear Import Activities y SQL Activities en modo Append |
| SQL Activity en modo Update también escribiendo sin PK | El DE muestra duplicados de las dos: SQL Activities y SSJS | Ver Style Guide de SQL — regla de Update-mode-sin-PK |
| El DE fue reconstruido a mitad de historia sin truncar | El paso 2 muestra pares de filas con contenido idéntico + CreatedDate distinto | Auditar la target action de la Activity de rebuild de audiencia (Overwrite vs Append) |
Relacionado
- Platform.Function — las llamadas
UpsertData/InsertData/UpdateDataque este snippet diagnostica - Gotchas de MC SSJS — #4 (el gotcha del PK-mismatch que esta página debuggea)
- Style Guide de SQL — Update-mode-sin-PK es la misma forma de falla del lado SQL
- Funciones agregadas de SQL — el patrón MAX-por-grupo de dedup usado en el paso 5
- Debugging de Script Activities trabadas — el how-to hermano para fallas que se manifiestan como crashes (este es para fallas que se manifiestan como data mal)
- Debugging de problemas de auth con WSProxy — el tercer hermano, comparte el DE de postmortem
- Style Guide — el checklist de disciplina con la regla de verificación del PK del destino