Skip to main content

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.

Cómo hacerlo·Actualizado 2026-05-13·Escrito por Lira · Editado por German Medina

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: SubscriberKey es único. El bug no son duplicados de UpsertData en esta columna — o los duplicados están sobre una key esperada distinta, o el problema del INNER JOIN downstream está en otro DE. Reapuntá el diagnóstico.
  • La query timeoutea: el DE destino es muy grande y no indexado. Agregá un TOP 1000 o 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 EmailAddress para el mismo SubscriberKey: probablemente el PK del DE está sobre la columna EmailAddress (no SubscriberKey). Cada corrida del script con un subscriber re-keyeado insertó una fila nueva.
  • Las copias tienen EmailAddress idéntico pero UpdatedDate distinto: 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:

  1. Abrí las propiedades del DE destino → marcá SubscriberKey como 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.)
  2. Truncá el DE destino.
  3. Corré una SQL Activity en modo Overwrite: SELECT * FROM de_stg_master_subscribers_dedup.
  4. 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