Cerrar el día local de ventas de cada recinto

PostgreSQLBeginner
Practicar Ahora

Introducción

La exposición itinerante vende entradas en Harbor Tickets, en Nueva York, y Berlin Pop-Up, en Berlín. Cada recinto cierra su día de ventas en su propia medianoche local. En un día de cambio de hora, ese día local puede durar 23 o 25 horas. Dos recibos de Harbor muestran 01:30, pero sus distintos desfases respecto a UTC identifican dos instantes reales diferentes y dos entradas distintas.

Guardarás consultas reutilizables de PostgreSQL para los intervalos de día local y los registros completos de entradas, y después entregarás informes de cierre CSV reales. Necesitas conocimientos básicos para moverte por la terminal, sobre conexiones de PostgreSQL y sobre SELECT/WHERE. Los tipos de fecha y hora, las zonas con nombre, las plantillas de consulta, las uniones y la sintaxis de exportación se explican antes de usarlos. PostgreSQL, los datos sintéticos originales de entradas y Terminal están preparados; no se requiere desarrollo en Python ni de interfaces web.

Crea o edita windows.sql, daily-close.sql y los informes close-YYYY-MM-DD.csv solicitados en /home/labex/project/day-close. Conserva las tablas originales, los ID, nombres y zonas de los recintos, los ID, instantes, importes y notas del personal de las entradas, y las herramientas proporcionadas. Las entradas gratuitas siguen contando como entradas, y los recibos distintos con el mismo precio o la misma hora de reloj siguen separados. Este es un ejercicio simplificado de cierre de ventas de entradas, sin impuestos, reembolsos, liquidación del proveedor de pagos ni garantías contables.

Todos los instantes fuente tienen información explícita de UTC o de desfase. Usamos fechas históricas fijas y medianoches locales conocidas; interpretar horas de reloj ambiguas sin desfase queda fuera de este ejercicio. Las etiquetas y salidas del entorno se mantienen en inglés.

Crear los intervalos correctos de día local

En este paso, convertirás la fecha de calendario solicitada para cada recinto en dos instantes límite reales, sin suponer que un día local dura 24 horas.

Empieza en Terminal, en el intérprete normal del sistema. ./ ejecuta un programa proporcionado en el directorio actual. La herramienta db abre una conexión de PostgreSQL de solo lectura; desk ejecutará tus consultas guardadas y mostrará sus resultados reales. Primero consulta los dos recibos en la hora de Nueva York y lee la configuración de los recintos:

Un here-document envía a db las líneas entre el marcador SQL entre comillas y el SQL de cierre. El marcador de cierre debe estar solo en su línea. Las instrucciones SQL terminan con punto y coma. Esta conexión se cierra al terminar el bloque; su SET TIME ZONE afecta solo a esta conexión. La siguiente invocación de ./db inicia una conexión nueva en UTC. WHERE ... IN (...) conserva los ID de entrada enumerados y ORDER BY ordena la presentación por ID.

cd /home/labex/project/day-close
./db <<'SQL'
SET TIME ZONE 'America/New_York';
SELECT ticket_id, sold_at, amount_cents
FROM tickets
WHERE ticket_id IN ('FOLD-1', 'FOLD-2')
ORDER BY ticket_id;
SELECT venue_id, venue_name, time_zone FROM venues ORDER BY venue_id;
SQL

Los recibos muestran 2024-11-03 01:30:00-04 y 2024-11-03 01:30:00-05. Los desfases indican cuatro y cinco horas por detrás de UTC, la referencia horaria común. El reloj marca lo mismo, pero el segundo instante es una hora posterior. Conserva ambos ID, aunque ambas entradas cuesten 500 centavos.

sold_at tiene el tipo de PostgreSQL timestamptz, abreviatura de timestamp with time zone. Representa un instante y lo muestra en el TimeZone de la conexión. No conserva el nombre de zona horaria original ni el desfase escrito originalmente. Por ello, cada recinto tiene un nombre de zona IANA separado, como America/New_York, cuyas reglas específicas para cada fecha determinan su desfase. Un desfase fijo no sustituye a ese nombre.

Lee los mismos instantes almacenados en una conexión UTC nueva. Aquí -c ejecuta la cadena SQL completa entre comillas y sale:

./db -c "SELECT ticket_id, sold_at FROM tickets WHERE ticket_id IN ('FOLD-1','FOLD-2') ORDER BY ticket_id;"

Los valores son 05:30 y 06:30 con el desfase +00. Cambiar una zona de visualización no modificó las entradas.

Un valor date es una etiqueta de calendario. Un valor timestamp without time zone es una hora de reloj, todavía sin identificar un instante global. ::type de PostgreSQL convierte un valor a un tipo. Convertir una fecha a timestamp da esa fecha a las 00:00. Aplicado a este timestamp local, AT TIME ZONE lo interpreta usando la zona con nombre del recinto y devuelve un instante timestamptz. Fecha más el entero 1 significa la siguiente fecha de calendario.

Para el día de ventas del 3 de noviembre de Harbor, convierte cada medianoche por separado:

./db <<'SQL'
SELECT '2024-11-03'::date::timestamp AT TIME ZONE 'America/New_York' AS opens_at,
       ('2024-11-03'::date + 1)::timestamp AT TIME ZONE 'America/New_York' AS closes_at;
SQL

La apertura UTC es el 3 de noviembre a las 04:00 y el cierre UTC es el 4 de noviembre a las 05:00: 25 horas transcurridas. El cambio de primavera del 10 de marzo tiene solo 23 horas transcurridas. Añadir 24 horas fijas a la apertura daría un instante de cierre incorrecto en ambos días. Los desfases de zona no tienen por qué ser horas enteras y algunos cambios de hora son de solo media hora; usa las reglas reales en ambas medianoches en lugar de fijar un desfase o una duración en el código.

Guarda una consulta reutilizable para todos los recintos. WITH windows AS (...) da nombre a una consulta intermedia, llamada expresión de tabla común. v es un alias corto de tabla para venues. AS da a una columna calculada su nombre de salida. Esto separa el trabajo con el calendario local del formato de presentación.

:'trading_date' es una variable de psql proporcionada por el puesto para cada fecha solicitada. Los dos puntos solicitan la sustitución y las comillas simples hacen que psql entrecomille el valor como una cadena SQL. Convierte ese valor a date en lugar de fijar una fecha en la consulta.

En el SELECT final, AT TIME ZONE 'UTC' funciona en el sentido contrario: convierte un instante timestamptz en su representación de reloj UTC. to_char da formato a esa representación. YYYY-MM-DD indica año, mes y día, HH24 un reloj de 24 horas, MI significa minutos y SS, segundos. La T y la Z entre comillas dobles en el formato son etiquetas literales. Z significa UTC, así que convierte a UTC antes de añadir esa etiqueta; imprimir solo Z no convierte nada.

Restar los instantes da un intervalo transcurrido. EXTRACT(EPOCH FROM ...) lee su total de segundos; dividir entre 3600 da las horas transcurridas, incluida una fracción de hora cuando corresponda. Leer solo el componente de horas no daría la duración completa.

cat > windows.sql escribe el archivo completo de consulta siguiente. El marcador SQL entre comillas impide que el intérprete expanda el cuerpo. Pega todo el bloque, manteniendo el marcador de cierre solo en su línea:

cat > windows.sql <<'SQL'
WITH windows AS (
  SELECT v.venue_id, v.venue_name, v.time_zone,
         :'trading_date'::date AS trading_date,
         :'trading_date'::date::timestamp AT TIME ZONE v.time_zone AS opens_at,
         (:'trading_date'::date + 1)::timestamp AT TIME ZONE v.time_zone AS closes_at
  FROM venues AS v
)
SELECT venue_id, venue_name, time_zone, trading_date,
       to_char(opens_at AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS"Z"') AS opens_utc,
       to_char(closes_at AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS"Z"') AS closes_utc,
       EXTRACT(EPOCH FROM (closes_at - opens_at)) / 3600 AS hours
FROM windows
ORDER BY venue_id;
SQL

Usa el puesto para proporcionar distintas fechas de actividad al mismo archivo. --zone cambia la zona de visualización de la conexión para esta invocación, no la fecha del recinto:

./desk windows 2024-11-03
./desk windows 2024-03-10
./desk windows 2024-11-03 --zone Asia/Tokyo

Las columnas de salida son venue_id, venue_name, time_zone, trading_date, opens_utc, closes_utc y hours, en ese orden. Los valores de Harbor son 25 y 23 horas; Berlín tiene 24 en estas fechas. --zone cambia la zona de visualización de la conexión de base de datos, no la fecha solicitada del recinto. Los límites UTC explícitos permanecen iguales.

Si escribes mal un nombre de archivo o un comando, lee el error, usa ls para inspeccionar los archivos reales y vuelve a intentar el comando corregido. Un Day close error es un error de ejecución, no un informe correcto sin ventas. Para reparar una consulta, corrige el SQL guardado y ejecútalo de nuevo; los datos originales de entradas no necesitan cambiar. Ejecuta la comprobación del paso después de leer los intervalos reales.

Mantener cada entrada dentro del día de su recinto

En este paso, usarás los límites del día local para seleccionar cada entrada original que corresponda exactamente una vez, conservando los recintos que no tuvieron ventas.

Permanece en Terminal y en /home/labex/project/day-close. Conserva windows.sql. Un intervalo diario es semiabierto: incluye su instante de apertura y excluye el de cierre. Un recibo en la medianoche siguiente pertenece al día siguiente. Las comparaciones SQL son >= para mayor o igual y < para estrictamente menor. BETWEEN incluye ambos extremos, por lo que contaría un recibo en el cierre en dos días adyacentes.

Para el intervalo de Harbor que acabas de observar, consulta las entradas reales usando literales explícitos de límites UTC. Aquí -c ejecuta el SQL entre comillas y sale; el +00 de cada literal hace explícito su significado UTC:

./db -c "SELECT ticket_id, sold_at, amount_cents FROM tickets WHERE venue_id='HARBOR' AND sold_at >= '2024-11-03 04:00:00+00'::timestamptz AND sold_at < '2024-11-04 05:00:00+00'::timestamptz ORDER BY sold_at,ticket_id;"

Seis entradas pertenecen a este día local. Los dos recibos de la hora repetida siguen presentes; la entrada gratuita tiene amount_cents igual a 0. Un importe cero no significa que no hubiera una entrada. El recibo NEXT de la medianoche de cierre queda fuera de este día. El recibo LATE inmediatamente anterior está dentro, incluidos sus microsegundos. No aproximes el final exclusivo restando un segundo.

Amplía la CTE del intervalo para crear una consulta reutilizable del registro. Una unión relaciona filas de dos fuentes. w hace referencia a una fila de intervalo y t a una entrada. LEFT JOIN conserva un intervalo aunque ninguna entrada cumpla sus condiciones ON. En ese caso, sus columnas de entrada son NULL, que significa que no hay un valor coincidente. Coloca las condiciones temporales en ON; moverlas a un WHERE final eliminaría el recinto sin ventas. La condición de ID de recinto impide que un recinto reciba los recibos de otro.

Cada entrada real produce una fila con su ID sin cambios, su instante UTC y sus centavos enteros. Un recinto sin ventas produce una fila con el ID de entrada, el instante de venta y el importe en blanco en la representación CSV. Es una fila de recinto sin ventas, no una entrada de precio cero. Conserva la identidad, el nombre, la zona y la fecha del recinto en cada fila.

Las columnas de salida, en orden, son venue_id, venue_name, time_zone, trading_date, opens_utc, closes_utc, ticket_id, sold_utc, amount_cents. Los campos UTC deben identificar UTC explícitamente, con Z o un desfase cero equivalente. El instante completo de una entrada real debe conservarse; US en el patrón de formato conserva seis dígitos de microsegundos. Los instantes de venta null siguen siendo null al pasar por to_char. El orden de las filas de resultado puede variar, pero deben conservarse todas las filas y los valores requeridos.

Escribe la consulta guardada completa. Usa el mismo parámetro de fecha y las conversiones de ambas medianoches que ya practicaste:

cat > daily-close.sql <<'SQL'
WITH windows AS (
  SELECT v.venue_id, v.venue_name, v.time_zone,
         :'trading_date'::date AS trading_date,
         :'trading_date'::date::timestamp AT TIME ZONE v.time_zone AS opens_at,
         (:'trading_date'::date + 1)::timestamp AT TIME ZONE v.time_zone AS closes_at
  FROM venues AS v
)
SELECT w.venue_id, w.venue_name, w.time_zone, w.trading_date,
       to_char(w.opens_at AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS"Z"') AS opens_utc,
       to_char(w.closes_at AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS"Z"') AS closes_utc,
       t.ticket_id,
       to_char(t.sold_at AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.US"Z"') AS sold_utc,
       t.amount_cents
FROM windows AS w
LEFT JOIN tickets AS t
  ON t.venue_id = w.venue_id
 AND t.sold_at >= w.opens_at
 AND t.sold_at < w.closes_at
ORDER BY w.venue_id, t.sold_at, t.ticket_id;
SQL

La acción close del puesto ejecuta el SQL guardado sin cambios y muestra sus filas reales junto con un resumen derivado únicamente de esas filas devueltas. No añade entradas ausentes ni elige un límite correcto ni corrige un precio erróneo. El resumen cuenta un ID de entrada real aunque su importe sea 0. Solicita el otoño, la primavera y una fecha sin ventas:

./desk close 2024-11-03
./desk close 2024-03-10
./desk close 2024-07-01

Para el 3 de noviembre, Harbor tiene seis entradas que suman 4750 centavos; Berlín tiene cuatro que suman 3750. Para el 10 de marzo, Harbor tiene cuatro que suman 3750 y Berlín sigue apareciendo con cero entradas y cero centavos. El 1 de julio tiene una fila de recinto sin ventas para cada recinto y ambos resúmenes son cero. Un error SQL es distinto de estas filas válidas sin ventas.

Las horas de reloj iguales, los precios iguales y los nombres de recinto duplicados no son reglas de identidad. Conserva los ID originales de entrada y recinto; no fusiones recibos diferentes con DISTINCT ni reemplaces el registro por un total únicamente. Ejecuta la comprobación del paso después de inspeccionar tanto las filas como los totales.

Entregar informes que conserven su significado

En este paso, exportarás las filas reales de la consulta de cierre a dos archivos CSV reales y confirmarás que la zona de visualización de una conexión no mueve una venta a un día diferente del recinto.

Permanece en Terminal y en el directorio day-close. Conserva ambos archivos SQL. --zone elige el TimeZone de visualización de la conexión PostgreSQL para esa única invocación del puesto. No cambia la zona almacenada del recinto, su fecha de actividad ni las entradas fuente. Compara el mismo día cerrado con otras dos zonas de conexión:

./desk close 2024-11-03 --zone Asia/Tokyo
./desk close 2024-11-03 --zone America/Los_Angeles

Ambas salidas siguen conteniendo las mismas entradas originales, fechas de recinto, intervalos UTC explícitos y totales. Si una consulta convierte directamente sold_at a date o da formato a una hora local y la etiqueta como Z, cambiar la zona de conexión puede revelar el error. Corrige la consulta en lugar de cambiar la configuración global de la base de datos o reescribir los tiempos fuente.

CSV almacena una cabecera y filas delimitadas; las comillas protegen los valores que contienen comas. La acción export del puesto escribe la salida CSV nativa de PostgreSQL en close-YYYY-MM-DD.csv en este espacio de trabajo. Si el archivo ya existe, reemplaza únicamente el informe de esa fecha. Ningún cálculo de referencia corrige la consulta antes de escribir. La acción read interpreta únicamente el archivo guardado y deriva su resumen mostrado de esas filas reales; no hace ninguna consulta a la base de datos y nunca completa filas omitidas desde la fuente.

Exporta ambas fechas documentadas y lee después sus archivos reales. Puedes conservar y volver a abrir estos informes cuando la base de datos fuente no esté disponible:

./desk export 2024-11-03
./desk export 2024-03-10
ls
./desk read close-2024-11-03.csv
./desk read close-2024-03-10.csv

Los dos archivos conservan el registro documentado de nueve columnas. Sus recuentos y totales coinciden con los resultados reales de consulta que inspeccionaste: 6/4750 y 4/3750 en otoño, y después 4/3750 y 0/0 en primavera para Harbor/Berlín. Ambos ID de las 01:30 repetidas permanecen en el archivo de otoño y la entrada gratuita sigue siendo una fila real. Una captura de pantalla o un total escrito a mano no conservarían estos datos.

Conserva windows.sql, daily-close.sql, ambos informes solicitados y la base de datos y herramientas originales. Una corrección de consulta no actualiza silenciosamente un archivo anterior: exporta de nuevo esa fecha explícitamente y lee después el archivo guardado para comprobarlo. Si escribes mal el nombre de un informe, usa ls y vuelve a intentarlo con el nombre real en lugar de cambiar la base de datos. Ejecuta la comprobación final después de confirmar las filas guardadas.

Resumen

Distinguiste etiquetas de calendario, horas de reloj, instantes y reglas de zonas IANA. Convertiste ambas medianoches locales de forma independiente, usaste un intervalo semiabierto para conservar cada entrada original una sola vez, mantuviste los recintos sin ventas y exportaste informes reales con instantes UTC explícitos. Tus consultas guardadas funcionan aunque cambie el TimeZone de la sesión y tu lector de archivos muestra únicamente las filas realmente guardadas. Estos son cierres históricos simplificados de entradas, no liquidaciones de pagos ni interpretación de horas ambiguas arbitrarias.