Tema representativo de entrevista

Entrevista de Backend: ¿Cómo diseñarías una paginación por cursor consistente bajo escrituras concurrentes?

BackendIntermedio
Equipo editorial de Offer.ccPublicado Actualizado

Pregunta

Una lista de pedidos con scroll infinito muestra duplicados u omite elementos mientras los usuarios paginan. Todavía se están escribiendo datos. ¿Cómo diseñarías la API y la consulta a la base de datos?

Planteamiento y contexto

Esta pregunta evalúa si un ingeniero de backend trata la paginación como un protocolo de lectura estable. Los pedidos, comentarios y registros pueden insertarse, eliminarse o actualizarse entre solicitudes. El OFFSET simple depende de posiciones cambiantes y escanea muchas filas en páginas profundas. Cubre el ordenamiento, la codificación del cursor, la vinculación de filtros, los límites de consistencia, los índices y la semántica hacia adelante/hacia atrás.

Qué evalúa el entrevistador

Las respuestas sólidas aclaran si el producto necesita saltos, conteos o resultados en tiempo real, y luego eligen keyset/cursor u offset. Un cursor vincula el ordenamiento y los filtros; su clave de ordenamiento es única, estable e indexada. El servidor lo firma y le asigna expiración. La respuesta explica por qué las filas nuevas no duplican la página siguiente, cómo afectan las eliminaciones a los resultados y cómo se devuelven next_cursor y el estado de fin de lista.

Preguntas para clarificar

  • ¿Cuál es el orden de clasificación? ¿Puede cambiar el campo de ordenamiento y cuál es el desempate único?
  • ¿Necesitamos navegación a la página anterior, saltos de página arbitrarios, conteos exactos o solo scroll infinito hacia adelante?
  • ¿Las lecturas deben usar un snapshot congelado o permitir una lista activa con consistencia eventual?
  • ¿Deben vincularse los filtros, el alcance del tenant, los permisos y el orden de clasificación dentro del cursor? ¿Por cuánto tiempo es válido?
  • ¿Cómo se manejan las eliminaciones, los soft deletes, los cambios de permisos y las lecturas entre shards?

Marco de respuesta de 30 segundos

“Utilizaría una clave compuesta estable como (created_at, id) para consultas keyset descendentes. El cursor es un token opaco firmado que contiene los últimos valores de ordenamiento, el hash de filtros, la dirección y la versión. El servidor lo valida y ejecuta WHERE (created_at,id) < (:time,:id) contra el índice compuesto correspondiente. Las filas nuevas aparecen al actualizar en lugar de insertarse en una ventana ya leída; las eliminaciones pueden acortar una página pero no generan duplicados. Si se requiere consistencia absoluta, agregaría un límite de snapshot y explicaría su costo.”

Respuesta detallada paso a paso

Paso 1: Definir la semántica de la lista

Decide si se trata de un historial de auditoría, un feed en vivo o una tabla de administración. Un historial generalmente necesita un límite estable; un feed en vivo puede mostrar filas nuevas solo después de actualizar. No prometas actualizaciones en tiempo real, saltos arbitrarios, conteos exactos y bajo costo simultáneamente.

Paso 2: Elegir una clave de ordenamiento estable

Las marcas de tiempo pueden empatar o editarse, por lo que se debe agregar un id único como desempate. Evita los campos de visualización. Si las actualizaciones pueden mover registros, utiliza una secuencia de creación inmutable o documenta explícitamente que las filas pueden moverse entre páginas.

Paso 3: Diseñar un cursor opaco

Incluye valores de ordenamiento, dirección, hash de filtros, versión de la API y expiración. Fírmalo o almacénalo en el lado del servidor; Base64 es codificación, no seguridad. Rechaza un cursor cuando los filtros cambien en lugar de devolver silenciosamente una página no relacionada.

Paso 4: Escribir la consulta keyset

Para un (created_at,id) descendente, la página siguiente usa created_at < t OR (created_at = t AND id < id0) con el mismo índice compuesto. Parametriza la consulta, limita limit y nunca concatenes valores del cursor en el SQL.

Paso 5: Manejar escrituras y eliminaciones

Las filas insertadas después de la página uno no deberían aparecer en la página dos; aparecen después de actualizar. Una fila eliminada puede hacer que una página sea más corta, lo cual es una semántica declarada aceptable. Si las omisiones son inaceptables, utiliza un límite de snapshot o una lectura versionada.

Paso 6: Vincular permisos y filtros

El hash de filtros, el tenant y el alcance de autorización del cursor deben coincidir con la solicitud. Recalcula los resultados cuando los permisos se vuelvan más estrictos; un cursor antiguo no debe eludir el control de acceso. Los coordinadores entre shards pueden fusionar cursores locales, pero la respuesta debe indicar el costo de amplificación.

Paso 7: Definir la respuesta y los errores

Devuelve items, next_cursor, has_more y opcionalmente un id de snapshot. Los cursores expirados o inválidos y los filtros modificados utilizan errores de negocio estables; el cliente borra el cursor y reinicia desde la página uno. No expongas SQL ni fallas internas de la base de datos.

Paso 8: Validar con pruebas de concurrencia

Prueba inserciones, eliminaciones, marcas de tiempo iguales, cambios de filtros, cursores manipulados y páginas profundas entre solicitudes. Verifica la ausencia de duplicados para una sesión, búsquedas por índice (index seeks) y una latencia que no crezca linealmente con el número de página. Monitorea la tasa de duplicados, la tasa de omisiones, la latencia p95 y los errores de cursor.

Pseudocódigo de la consulta

sql
SELECT id, created_at, total
FROM orders
WHERE tenant_id = :tenant
  AND (created_at, id) < (:cursor_time, :cursor_id)
ORDER BY created_at DESC, id DESC
LIMIT :page_size;

Compensaciones y límites

RequisitoElecciónCosto
Scroll infinito grandeCursor keysetSin saltos de página arbitrarios
Tabla de administración pequeñaOffsetPáginas profundas lentas e inestables bajo escrituras
Consistencia absolutaLímite de snapshotAlmacenamiento y limpieza de snapshots
Conteo exactoConteo separado o asíncronoTrabajo adicional y posible falta de actualización

La paginación por cursor resuelve la estabilidad de la posición y la eficiencia de la consulta; no resuelve automáticamente la deduplicación de negocio entre páginas, los cambios de permisos ni las actualizaciones que mueven filas. Los resultados de búsqueda también necesitan una versión de consulta; los agregados pueden requerir una lectura en un punto en el tiempo.

Plan de implementación y evidencia

Elige una lista de alta lectura y mide los duplicados actuales, las omisiones, la latencia en páginas profundas y el costo de conteo. Agrega un índice cubriente, lanza cursores versionados y añade pruebas y métricas de escrituras concurrentes. Django REST framework documenta la paginación por cursor como un cursor opaco; los materiales de Hello Interview y TechInterview enfatizan la estabilidad de cursor/keyset en conjuntos de datos cambiantes.

Criterios de salida de la prueba piloto

Las pruebas de inserción/eliminación concurrente cumplen con las semánticas declaradas de duplicados y omisiones; el p95 de páginas profundas es estable; se rechazan la manipulación y los cambios de filtros; los clientes se recuperan de forma segura a la página uno; y la revisión de autorización confirma que los tokens no filtran datos.

Cómo demostrar que la mejora es real

Con el mismo tamaño de datos y tasa de escritura, compara la latencia p95/p99 de offset frente a cursor, las filas escaneadas, la tasa de duplicados, la tasa de omisiones y la CPU de la base de datos. Separa los efectos de caché para que una sola consulta en frío no determine el resultado.

Errores comunes y preguntas de seguimiento

Tratar Base64 como un cursor seguro

Base64 es codificación. Los clientes pueden alterar un id o tenant. Firma o almacena el token y vincula filtros, versión y expiración.

Ordenar solo por marca de tiempo

Muchas filas pueden compartir el mismo milisegundo, haciendo que el límite sea ambiguo. Agrega un desempate único y un índice compuesto coincidente.

¿Puede un cursor saltar a cualquier página?

Los cursores estándar no están diseñados para saltos arbitrarios. Ofrece offset acotado, anclajes precalculados o paginación de motor de búsqueda con consistencia y costo explícitos.

¿Pueden las actualizaciones duplicar una fila?

Si el campo de ordenamiento cambia, una fila puede moverse entre páginas. Utiliza una secuencia de creación inmutable o semánticas de snapshot/versión y documenta el comportamiento.

¿Cómo implementas un botón de página anterior?

Mantén una pila de cursores anteriores o ejecuta la consulta inversa e invierte el resultado en el servidor. El cliente no debe deducir la estructura interna del cursor.

¿Se debe devolver el conteo total exacto?

No. El scroll infinito generalmente necesita has_more; los conteos exactos pueden ser asíncronos o separados para que cada página no escanee la tabla.

Fuentes públicas

Preguntas relacionadas