<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: SEBASTIAN ALEJANDRO CORTEZ APAZA</title>
    <description>The latest articles on DEV Community by SEBASTIAN ALEJANDRO CORTEZ APAZA (@sebacor13).</description>
    <link>https://dev.to/sebacor13</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4160778%2Fe52ae165-455f-48bb-aa95-92b6a7a11863.jpg</url>
      <title>DEV Community: SEBASTIAN ALEJANDRO CORTEZ APAZA</title>
      <link>https://dev.to/sebacor13</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sebacor13"/>
    <language>en</language>
    <item>
      <title>Una tabla por pregunta: modelar y desplegar sobre Cassandra sin tocar un panel</title>
      <dc:creator>SEBASTIAN ALEJANDRO CORTEZ APAZA</dc:creator>
      <pubDate>Sat, 24 Oct 2026 07:41:21 +0000</pubDate>
      <link>https://dev.to/sebacor13/una-tabla-por-pregunta-modelar-y-desplegar-sobre-cassandra-sin-tocar-un-panel-1dd9</link>
      <guid>https://dev.to/sebacor13/una-tabla-por-pregunta-modelar-y-desplegar-sobre-cassandra-sin-tocar-un-panel-1dd9</guid>
      <description>&lt;p&gt;Cuando llevas años escribiendo SQL, lo primero que haces frente a una base&lt;br&gt;
de datos nueva es buscar dónde está el &lt;code&gt;JOIN&lt;/code&gt;. En Apache Cassandra no está,&lt;br&gt;
y ese hueco no es una carencia: es la decisión de diseño de la que cuelga&lt;br&gt;
todo lo demás.&lt;/p&gt;

&lt;p&gt;Para entenderlo de verdad construí algo pequeño y lo llevé hasta producción:&lt;br&gt;
un acortador de URLs con analítica de clics, sobre Cassandra gestionado en&lt;br&gt;
Astra DB, desplegado en Render mediante GitHub Actions. El código completo&lt;br&gt;
está en el repositorio, pero lo que quiero contar aquí es el cambio mental&lt;br&gt;
que exige el modelado, porque es la parte que ningún tutorial de "hola&lt;br&gt;
mundo" te obliga a atravesar.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Repositorio:&lt;/strong&gt; &lt;a href="https://github.com/SEBACOR-13/astra-shortener" rel="noopener noreferrer"&gt;https://github.com/SEBACOR-13/astra-shortener&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Aplicación en vivo:&lt;/strong&gt; &lt;a href="https://astra-shortener.onrender.com" rel="noopener noreferrer"&gt;https://astra-shortener.onrender.com&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  Por qué un acortador es el caso perfecto
&lt;/h2&gt;

&lt;p&gt;No lo elegí por original. Lo elegí porque su patrón de acceso es casi una&lt;br&gt;
caricatura de aquello en lo que Cassandra es buena:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;La lectura dominante es una sola.&lt;/strong&gt; Dado un código de siete caracteres,
devolver una URL. No hay filtros, no hay agregaciones, no hay relaciones
que recorrer. Es un acceso por clave.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;La escritura dominante es append-only y masiva.&lt;/strong&gt; Cada visita genera un
registro de clic que nunca se actualiza ni se borra.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;El crecimiento es horizontal.&lt;/strong&gt; Más enlaces significa más particiones,
y más particiones se reparten entre más nodos sin que nadie reescriba una
consulta.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Si tu carga de trabajo se parece a esto, Cassandra te da escalado lineal y&lt;br&gt;
alta disponibilidad. Si no se parece —si necesitas consultas analíticas&lt;br&gt;
flexibles sobre columnas arbitrarias— te va a hacer la vida miserable. Esa&lt;br&gt;
es la parte honesta que conviene decir antes de empezar.&lt;/p&gt;
&lt;h2&gt;
  
  
  El giro: no modelas entidades, modelas preguntas
&lt;/h2&gt;

&lt;p&gt;En el modelo relacional diseñas las tablas una vez, normalizas, y después&lt;br&gt;
escribes las consultas que necesites. El motor se encarga de resolverlas.&lt;/p&gt;

&lt;p&gt;En Cassandra el orden está invertido. Primero escribes la lista de preguntas&lt;br&gt;
que tu aplicación va a hacer, y después creas &lt;strong&gt;una tabla por pregunta&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Para el acortador, las preguntas eran cuatro:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;¿A qué URL redirijo este código corto?&lt;/li&gt;
&lt;li&gt;¿Qué enlaces creó este usuario, del más nuevo al más viejo?&lt;/li&gt;
&lt;li&gt;¿Cuáles fueron los últimos clics de este enlace?&lt;/li&gt;
&lt;li&gt;¿Cuántos clics lleva en total?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Cuatro preguntas, cuatro tablas. Veámoslas, porque cada una enseña algo&lt;br&gt;
distinto.&lt;/p&gt;
&lt;h3&gt;
  
  
  Pregunta 1: acceso por clave
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;links_by_code&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;code&lt;/span&gt;        &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;target_url&lt;/span&gt;  &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;owner&lt;/span&gt;       &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;created_at&lt;/span&gt;  &lt;span class="nb"&gt;timestamp&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;La más simple. &lt;code&gt;code&lt;/code&gt; es la &lt;em&gt;partition key&lt;/em&gt;: Cassandra calcula su hash,&lt;br&gt;
ese hash determina qué nodo del anillo guarda la fila, y la lectura va&lt;br&gt;
directo a ese nodo. Una partición, un salto de red.&lt;/p&gt;
&lt;h3&gt;
  
  
  Pregunta 2: desnormalización deliberada
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;links_by_owner&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;owner&lt;/span&gt;       &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;created_at&lt;/span&gt;  &lt;span class="nb"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;code&lt;/span&gt;        &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;target_url&lt;/span&gt;  &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="k"&gt;owner&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;CLUSTERING&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="k"&gt;ASC&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Aquí es donde un desarrollador de SQL hace una mueca: &lt;strong&gt;es la misma&lt;br&gt;
información que la tabla anterior, duplicada&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Y es exactamente lo correcto. En Cassandra escribir es barato: la escritura&lt;br&gt;
va a un commit log secuencial y a una estructura en memoria, sin leer nada&lt;br&gt;
antes. Leer mal modelado, en cambio, es carísimo, porque obliga al nodo&lt;br&gt;
coordinador a preguntarle a todos los demás. Así que la regla se invierte&lt;br&gt;
respecto a lo que enseñan en Bases de Datos I: &lt;strong&gt;duplicas al escribir para&lt;br&gt;
que cada lectura toque una sola partición&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Fíjate en la forma de la clave primaria: &lt;code&gt;((owner), created_at, code)&lt;/code&gt;. El&lt;br&gt;
primer paréntesis interno es la partition key —todos los enlaces de un&lt;br&gt;
usuario viven juntos en el mismo nodo— y lo que sigue son las &lt;em&gt;clustering&lt;br&gt;
keys&lt;/em&gt;, que determinan el orden &lt;strong&gt;físico&lt;/strong&gt; de las filas dentro de esa&lt;br&gt;
partición.&lt;/p&gt;

&lt;p&gt;Lo cual lleva al detalle que más me gustó.&lt;/p&gt;
&lt;h3&gt;
  
  
  El orden no se pide, se diseña
&lt;/h3&gt;

&lt;p&gt;Esta es la consulta que alimenta la lista de "mis enlaces":&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;owner&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;target_url&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;links_by_owner&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;owner&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No hay &lt;code&gt;ORDER BY&lt;/code&gt;. Y aun así las filas llegan de la más nueva a la más&lt;br&gt;
vieja, siempre.&lt;/p&gt;

&lt;p&gt;El orden sale del &lt;code&gt;CLUSTERING ORDER BY (created_at DESC)&lt;/code&gt; declarado al&lt;br&gt;
crear la tabla. Las filas están &lt;strong&gt;físicamente escritas en ese orden&lt;/strong&gt; dentro&lt;br&gt;
del SSTable. Ordenar en tiempo de consulta es trabajo que Cassandra&lt;br&gt;
simplemente no hace; lo trasladó al momento de la escritura, donde cuesta&lt;br&gt;
menos.&lt;/p&gt;

&lt;p&gt;La tabla de clics usa la misma idea:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;clicks_by_code&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;code&lt;/span&gt;        &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;clicked_at&lt;/span&gt;  &lt;span class="nb"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;click_id&lt;/span&gt;    &lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;referrer&lt;/span&gt;    &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;user_agent&lt;/span&gt;  &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;clicked_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;click_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;CLUSTERING&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;clicked_at&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;click_id&lt;/span&gt; &lt;span class="k"&gt;ASC&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El &lt;code&gt;click_id uuid&lt;/code&gt; al final de la clave no es decorativo: si dos clics&lt;br&gt;
caen en el mismo milisegundo, sin él la segunda fila sobrescribiría a la&lt;br&gt;
primera, porque en Cassandra un &lt;code&gt;INSERT&lt;/code&gt; sobre una clave existente es un&lt;br&gt;
&lt;em&gt;upsert&lt;/em&gt; silencioso. Es un error fácil de cometer y difícil de notar: no&lt;br&gt;
falla nada, simplemente pierdes datos.&lt;/p&gt;
&lt;h3&gt;
  
  
  Pregunta 4: por qué &lt;code&gt;COUNT(*)&lt;/code&gt; no es la respuesta
&lt;/h3&gt;

&lt;p&gt;Para el total de clics, el reflejo relacional sería:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;clicks_by_code&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Funciona. Y es una mala idea: recorre la partición entera cada vez. Con un&lt;br&gt;
enlace que acumule cien mil clics, estás leyendo cien mil filas para&lt;br&gt;
devolver un número.&lt;/p&gt;

&lt;p&gt;Cassandra tiene un tipo específico para esto:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;click_counts&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;code&lt;/span&gt;   &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;total&lt;/span&gt;  &lt;span class="n"&gt;counter&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;click_counts&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El tipo &lt;code&gt;counter&lt;/code&gt; resuelve incrementos concurrentes de forma distribuida,&lt;br&gt;
sin el ciclo leer-modificar-escribir que provocaría condiciones de carrera.&lt;br&gt;
Tiene una restricción que lo delata: una tabla con columnas &lt;code&gt;counter&lt;/code&gt; no&lt;br&gt;
admite ninguna otra columna fuera de la clave primaria. Por eso vive&lt;br&gt;
separada en lugar de ser un campo más de &lt;code&gt;links_by_code&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  La escritura doble, en código
&lt;/h3&gt;

&lt;p&gt;Crear un enlace significa escribir en dos tablas:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_link&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;target_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;owner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Link&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;UTC&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_insert_link_by_code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;target_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;owner&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_insert_link_by_owner&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;owner&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;target_url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Link&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;target_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;owner&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No hay transacción que las envuelva, porque Cassandra no ofrece&lt;br&gt;
transacciones multi-partición. Lo que sí ofrece es que ambos &lt;code&gt;INSERT&lt;/code&gt; son&lt;br&gt;
&lt;strong&gt;idempotentes&lt;/strong&gt;: si el segundo falla y se reintenta, el resultado es el&lt;br&gt;
mismo. Diseñar para la idempotencia reemplaza aquí al &lt;code&gt;ROLLBACK&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Un detalle de rendimiento: todas las sentencias son &lt;em&gt;prepared statements&lt;/em&gt;.&lt;br&gt;
Se compilan una vez y después viajan por la red como un identificador más&lt;br&gt;
los parámetros. Además, el driver sabe qué nodo es dueño de cada partición&lt;br&gt;
y enruta la petición directamente, sin coordinador intermedio.&lt;/p&gt;


&lt;h2&gt;
  
  
  La segunda mitad: que el deploy no lo haga un humano
&lt;/h2&gt;

&lt;p&gt;El enunciado pedía despliegue automatizado, y vale la pena tomárselo en&lt;br&gt;
serio, porque "lo subí a la nube" y "tengo un pipeline" son cosas muy&lt;br&gt;
distintas.&lt;/p&gt;

&lt;p&gt;La regla que me puse: &lt;strong&gt;el único gesto manual es &lt;code&gt;git push&lt;/code&gt;&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;push a main
   ├─ ruff check        estilo
   ├─ pytest            10 pruebas
   ├─ docker build      la imagen compila
   ├─ POST al deploy hook de Render
   └─ polling a /health hasta que responda
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Si cualquier paso falla, el deploy no se ejecuta. Un pull request pasa por&lt;br&gt;
lint y pruebas, pero nunca publica.&lt;/p&gt;
&lt;h3&gt;
  
  
  El problema del bundle binario
&lt;/h3&gt;

&lt;p&gt;Astra DB no se conecta con una cadena de conexión: exige un &lt;strong&gt;Secure Connect&lt;br&gt;
Bundle&lt;/strong&gt;, un &lt;code&gt;.zip&lt;/code&gt; con los certificados TLS y los metadatos del clúster.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;cluster&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Cluster&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;cloud&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;secure_connect_bundle&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ruta_del_bundle&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;auth_provider&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;PlainTextAuthProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;token&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;astra_token&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;En local es un archivo en disco. Pero los secrets de GitHub y de Render&lt;br&gt;
solo guardan texto, no binarios. La solución es codificarlo en base64:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;base64&lt;/span&gt; &lt;span class="nt"&gt;-w0&lt;/span&gt; secure-connect-shortener-db.zip &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; bundle.b64
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;y reconstruirlo en memoria al arrancar:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;b64decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;astra_scb_b64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;validate&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;handle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tempfile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;NamedTemporaryFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;suffix&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.zip&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;delete&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El bundle nunca entra al repositorio. El &lt;code&gt;.gitignore&lt;/code&gt; incluye&lt;br&gt;
&lt;code&gt;secure-connect-*.zip&lt;/code&gt; por si acaso.&lt;/p&gt;
&lt;h3&gt;
  
  
  Probar sin credenciales
&lt;/h3&gt;

&lt;p&gt;Un pipeline que necesita secretos de producción para correr sus pruebas es&lt;br&gt;
un pipeline frágil: no funciona en forks y empuja a poner credenciales&lt;br&gt;
reales en CI.&lt;/p&gt;

&lt;p&gt;Definí el acceso a datos como un contrato con dos implementaciones: una&lt;br&gt;
contra Cassandra y otra en memoria. Si faltan las credenciales, la&lt;br&gt;
aplicación &lt;strong&gt;arranca igual&lt;/strong&gt; con la segunda y lo declara en &lt;code&gt;/health&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ok"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"backend"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"memoria"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"keyspace"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"shortener"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;La implementación en memoria respeta el mismo orden de resultados que&lt;br&gt;
imponen las clustering keys, así que las pruebas verifican el contrato real.&lt;br&gt;
En producción &lt;code&gt;/health&lt;/code&gt; responde &lt;code&gt;"backend": "astra"&lt;/code&gt;, y el último paso del&lt;br&gt;
pipeline comprueba justamente eso: no basta con que el servicio esté vivo,&lt;br&gt;
tiene que estar hablando con la base de datos correcta.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Esperar a que el servicio quede sano&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;for intento in $(seq 1 30); do&lt;/span&gt;
      &lt;span class="s"&gt;respuesta=$(curl -fsS "$APP_URL/health" || true)&lt;/span&gt;
      &lt;span class="s"&gt;if echo "$respuesta" | grep -q '"status":"ok"'; then&lt;/span&gt;
        &lt;span class="s"&gt;echo "Servicio sano: $respuesta"&lt;/span&gt;
        &lt;span class="s"&gt;exit 0&lt;/span&gt;
      &lt;span class="s"&gt;fi&lt;/span&gt;
      &lt;span class="s"&gt;sleep 10&lt;/span&gt;
    &lt;span class="s"&gt;done&lt;/span&gt;
    &lt;span class="s"&gt;exit 1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ese bucle existe por una razón práctica: Render tarda entre uno y tres&lt;br&gt;
minutos en construir la imagen y arrancar el contenedor. Un deploy que&lt;br&gt;
"termina" en cuanto el webhook responde &lt;code&gt;200&lt;/code&gt; no ha verificado nada.&lt;/p&gt;




&lt;h2&gt;
  
  
  Lo que me llevo
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;La desnormalización deja de ser un pecado.&lt;/strong&gt; Tres semestres enseñando&lt;br&gt;
formas normales y aquí la respuesta correcta es duplicar. No es relajar el&lt;br&gt;
rigor: es que el criterio cambió. En un sistema distribuido el costo&lt;br&gt;
dominante es el salto de red, no el espacio en disco.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;La consulta se diseña antes que la tabla.&lt;/strong&gt; Es incómodo al principio,&lt;br&gt;
porque obliga a conocer los patrones de acceso antes de escribir una sola&lt;br&gt;
línea. Y es brutalmente honesto: te fuerza a admitir que no sabías cómo se&lt;br&gt;
iba a usar tu aplicación.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cassandra no tiene una red de seguridad.&lt;/strong&gt; Sin &lt;code&gt;JOIN&lt;/code&gt;, sin transacciones&lt;br&gt;
multi-partición, sin &lt;code&gt;ORDER BY&lt;/code&gt; arbitrario, sin secuencias&lt;br&gt;
autoincrementales. Cada una de esas ausencias es un mecanismo de&lt;br&gt;
coordinación que un sistema distribuido no puede pagar barato. Lo que&lt;br&gt;
recibes a cambio es escalado lineal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Un despliegue automatizado se nota cuando falla.&lt;/strong&gt; El valor del pipeline&lt;br&gt;
no está en el día que todo sale bien: está el día que una prueba se rompe y&lt;br&gt;
el deploy se detiene solo, antes de que la aplicación caída llegue a&lt;br&gt;
producción.&lt;/p&gt;




&lt;p&gt;Si quieres mirar el código, el esquema CQL está comentado línea por línea&lt;br&gt;
en &lt;code&gt;app/schema.cql&lt;/code&gt;, que es probablemente el archivo más útil del&lt;br&gt;
repositorio para alguien que esté empezando con Cassandra.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Repositorio:&lt;/strong&gt; &lt;a href="https://github.com/SEBACOR-13/astra-shortener" rel="noopener noreferrer"&gt;https://github.com/SEBACOR-13/astra-shortener&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Aplicación:&lt;/strong&gt; &lt;a href="https://astra-shortener.onrender.com" rel="noopener noreferrer"&gt;https://astra-shortener.onrender.com&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Trabajo del curso de Base de Datos II — Universidad Privada de Tacna.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>cassandra</category>
      <category>nosql</category>
      <category>devops</category>
      <category>python</category>
    </item>
    <item>
      <title>Completo, diferencial o incremental: cómo elegir tu estrategia de respaldo (y probarla bloque a bloque con SQLite)</title>
      <dc:creator>SEBASTIAN ALEJANDRO CORTEZ APAZA</dc:creator>
      <pubDate>Sun, 04 Oct 2026 03:45:02 +0000</pubDate>
      <link>https://dev.to/sebacor13/completo-diferencial-o-incremental-como-elegir-tu-estrategia-de-respaldo-y-probarla-bloque-a-1a9g</link>
      <guid>https://dev.to/sebacor13/completo-diferencial-o-incremental-como-elegir-tu-estrategia-de-respaldo-y-probarla-bloque-a-1a9g</guid>
      <description>&lt;p&gt;&lt;strong&gt;Autor:&lt;/strong&gt; Sebastian Alejandro Cortez Apaza, estudiante de Ingeniería de Sistemas de la Universidad Privada de Tacna, curso Base de Datos II.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🔗 &lt;strong&gt;Demo en vivo:&lt;/strong&gt; &lt;a href="https://sebacor-13.github.io/backup-strategies-sqlite/" rel="noopener noreferrer"&gt;https://sebacor-13.github.io/backup-strategies-sqlite/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;💻 &lt;strong&gt;Repositorio público:&lt;/strong&gt; &lt;a href="https://github.com/SEBACOR-13/backup-strategies-sqlite" rel="noopener noreferrer"&gt;https://github.com/SEBACOR-13/backup-strategies-sqlite&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  1. Los atacantes también buscan tus respaldos
&lt;/h2&gt;

&lt;p&gt;Según el estudio de Sophos de 2024, aplicado a casi 3 000 organizaciones, en el &lt;strong&gt;94 %&lt;/strong&gt; de los ataques de &lt;em&gt;ransomware&lt;/em&gt; los delincuentes intentaron comprometer también los respaldos, y lo lograron en el 57 % de esos casos. Cuando lo consiguieron, las víctimas tuvieron casi el doble de probabilidad de pagar el rescate, y el costo de recuperación fue unas ocho veces mayor (Adam, 2024).&lt;/p&gt;

&lt;p&gt;Por eso la pregunta ya no es solo “¿tengo respaldo?”, sino también:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;¿qué tipo de respaldo tengo?&lt;/li&gt;
&lt;li&gt;¿cuántas piezas necesito para restaurarlo?&lt;/li&gt;
&lt;li&gt;¿está cifrado y separado del sistema atacado?&lt;/li&gt;
&lt;li&gt;¿lo he probado alguna vez?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;En este artículo comparo las tres políticas clásicas (completa, diferencial e incremental) con &lt;strong&gt;datos medidos&lt;/strong&gt;, gracias a &lt;strong&gt;BlockBackupLab&lt;/strong&gt;, una aplicación que respalda un archivo SQLite real página por página.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Las tres políticas, en una frase cada una
&lt;/h2&gt;

&lt;p&gt;La literatura clásica de respaldos distingue entre copiar todo y copiar solo lo modificado (Chervenak et al., 1998):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Política&lt;/th&gt;
&lt;th&gt;Qué copia&lt;/th&gt;
&lt;th&gt;Para restaurar necesito&lt;/th&gt;
&lt;th&gt;Ventaja&lt;/th&gt;
&lt;th&gt;Riesgo&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Completa&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Todo, cada vez&lt;/td&gt;
&lt;td&gt;1 pieza&lt;/td&gt;
&lt;td&gt;Simplicidad y restauración rápida&lt;/td&gt;
&lt;td&gt;Mucho espacio y ventanas largas&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Diferencial&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Lo que cambió desde el último &lt;strong&gt;completo&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;2 piezas: completo + último diferencial&lt;/td&gt;
&lt;td&gt;Restauración sencilla&lt;/td&gt;
&lt;td&gt;Crece cada día hasta el siguiente completo&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Incremental&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Lo que cambió desde el respaldo &lt;strong&gt;anterior&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;Completo + &lt;strong&gt;todos&lt;/strong&gt; los incrementales&lt;/td&gt;
&lt;td&gt;Mínimo espacio y ventanas cortas&lt;/td&gt;
&lt;td&gt;La cadena es frágil: si falta un eslabón, se pierde lo posterior&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Un esquema típico es &lt;strong&gt;un completo el domingo y diferenciales o incrementales de lunes a sábado&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. ¿Qué significa “lo que cambió”? Respaldos por bloques
&lt;/h2&gt;

&lt;p&gt;Las herramientas modernas no comparan filas ni archivos enteros: comparan &lt;strong&gt;bloques&lt;/strong&gt;. SQLite es un caso ideal para entenderlo, porque toda la base vive en un único archivo dividido en &lt;strong&gt;páginas&lt;/strong&gt; de tamaño fijo. El tamaño de página se guarda en la cabecera, en el desplazamiento 16 (SQLite, s.f.-a).&lt;/p&gt;

&lt;p&gt;La idea es simple:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Divido el archivo en páginas de 4 KB.&lt;/li&gt;
&lt;li&gt;Calculo el &lt;strong&gt;SHA-256 de cada página&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Comparo esas huellas con las del respaldo de referencia y &lt;strong&gt;copio solo las páginas distintas&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Así funciona también &lt;code&gt;sqlite3_rsync&lt;/code&gt;, la herramienta oficial de SQLite para copiar una base viva: la réplica envía &lt;em&gt;hashes&lt;/em&gt; de sus páginas y el origen devuelve solo las que difieren. En bases parecidas, el tráfico puede ser del orden del 0,01 % del tamaño (SQLite, s.f.-c). PostgreSQL 17 aplica la misma idea en su respaldo incremental nativo.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Tamaño de página: bytes 16-17 de la cabecera, big-endian (1 significa 65 536)&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;tamanoPagina&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nx"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;17&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="mi"&gt;65536&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;crearRespaldo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;tipo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;catalogo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hashes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;hashesDePaginas&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="c1"&gt;// SHA-256 de cada página&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;base&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;tipo&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;diferencial&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;catalogo&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;reverse&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tipo&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;completo&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// último completo&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;tipo&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;incremental&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;catalogo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;at&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;              &lt;span class="c1"&gt;// respaldo anterior&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cambiadas&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;hashes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;flatMap&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;base&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hashes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;h&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]))&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;tipo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;padre&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;base&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;hashes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;paginas&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cambiadas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;copiaPagina&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Para restaurar, se aplica la cadena en orden: primero el completo y luego cada respaldo posterior, sobrescribiendo sus páginas. Al final se verifica el SHA-256 del archivo reconstruido contra el original.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Ojo en SQLite real:&lt;/strong&gt; nunca copies el archivo &lt;code&gt;.db&lt;/code&gt; con &lt;code&gt;cp&lt;/code&gt; mientras está en uso. Usa la &lt;strong&gt;Online Backup API&lt;/strong&gt;, &lt;code&gt;VACUUM INTO 'respaldo.db'&lt;/code&gt; o &lt;code&gt;sqlite3_rsync&lt;/code&gt;, que garantizan una copia consistente (SQLite, s.f.-b). Para respaldos continuos existe &lt;strong&gt;Litestream&lt;/strong&gt;, que replica el WAL de SQLite hacia almacenamiento S3.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  4. BlockBackupLab: medir en lugar de suponer
&lt;/h2&gt;

&lt;p&gt;La aplicación simula un inventario con &lt;strong&gt;4 almacenes, 300 productos y 6 000 movimientos&lt;/strong&gt; sobre SQLite real, ejecutado en el navegador con sql.js (WebAssembly). Cada vez que avanzas un día simulado:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Se registran unos 120 movimientos nuevos y se actualiza el stock.&lt;/li&gt;
&lt;li&gt;Se ejecuta el respaldo que indique la política.&lt;/li&gt;
&lt;li&gt;El respaldo se &lt;strong&gt;comprime con gzip&lt;/strong&gt; y se &lt;strong&gt;cifra con AES-256-GCM&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwyq34ksd1jn6pdopaxrg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwyq34ksd1jn6pdopaxrg.png" alt="Mapa de páginas copiadas en un respaldo incremental" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  4.1 Resultados de dos semanas simuladas
&lt;/h3&gt;

&lt;p&gt;Con los mismos datos, al final la base pesa 584 KB (146 páginas). Estos son los resultados medidos, sin comprimir:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Política&lt;/th&gt;
&lt;th&gt;Almacenamiento total&lt;/th&gt;
&lt;th&gt;Páginas copiadas por día (lun → sáb, semana 1)&lt;/th&gt;
&lt;th&gt;Piezas para restaurar el día 13&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Completo diario&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;7,27 MB&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;122 → 132 (todo)&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Completo dominical + diferencial&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2,45 MB&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;25 → 35 (crece)&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Completo dominical + incremental&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2,23 MB&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;25 → 26 (estable)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;7&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqdj2md72x31j4w2hk3kg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqdj2md72x31j4w2hk3kg.png" alt="Comparación de políticas en la aplicación" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Lectura de los resultados:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;El &lt;strong&gt;completo diario&lt;/strong&gt; ocupa unas &lt;strong&gt;3 veces más&lt;/strong&gt; que las otras dos políticas.&lt;/li&gt;
&lt;li&gt;El &lt;strong&gt;diferencial&lt;/strong&gt; crece cada día: el sábado vuelve a copiar todo lo del lunes. Aun así, restaurar solo necesita dos piezas.&lt;/li&gt;
&lt;li&gt;El &lt;strong&gt;incremental&lt;/strong&gt; copia siempre lo mínimo, pero restaurar un sábado exige leer y aplicar &lt;strong&gt;siete&lt;/strong&gt; archivos, sin que falte ninguno.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;En la app hay un botón 🗑 que simula “perder” un respaldo, por un disco dañado o un borrado accidental. Si se pierde un incremental intermedio, la cadena de restauración de los días siguientes &lt;strong&gt;se rompe&lt;/strong&gt; y la aplicación lo indica en rojo.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Cifrar y verificar: el respaldo como objetivo
&lt;/h2&gt;

&lt;p&gt;Si el atacante puede leer o borrar tus respaldos, no tienes respaldos. BlockBackupLab empaqueta cada uno así:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Serializa&lt;/strong&gt; las páginas con una cabecera JSON de metadatos.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Comprime&lt;/strong&gt; con gzip: un completo de 480 KB queda en unos 186 KB.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cifra&lt;/strong&gt; con &lt;strong&gt;AES-256-GCM&lt;/strong&gt;, un modo autenticado: cualquier byte alterado hace fallar el descifrado (Dworkin, 2007).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deriva la clave&lt;/strong&gt; desde una frase con &lt;strong&gt;PBKDF2-HMAC-SHA256 y 600 000 iteraciones&lt;/strong&gt;, el valor que recomienda OWASP (OWASP Foundation, s.f.).
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;clave&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subtle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deriveKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PBKDF2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;salt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;iterations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;600000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SHA-256&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="nx"&gt;material&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;AES-GCM&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;length&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;256&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;encrypt&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;decrypt&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cifrado&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subtle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encrypt&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;AES-GCM&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;iv&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;clave&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;comprimido&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;En la demostración, un &lt;em&gt;ransomware&lt;/em&gt; simulado cifra el archivo de la base y lo vuelve ilegible. La restauración trabaja &lt;strong&gt;solo con los paquetes cifrados&lt;/strong&gt; y hace lo siguiente:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Los descifra y reconstruye el archivo página por página.&lt;/li&gt;
&lt;li&gt;Comprueba que el &lt;strong&gt;SHA-256&lt;/strong&gt; sea idéntico al original.&lt;/li&gt;
&lt;li&gt;Ejecuta &lt;code&gt;PRAGMA integrity_check&lt;/code&gt;, que devuelve &lt;code&gt;ok&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Compara el número de filas de cada tabla.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F99m11sp6ro3hizv0rbhv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F99m11sp6ro3hizv0rbhv.png" alt="Restauración verificada después del ransomware" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  6. De 3-2-1 a 3-2-1-1-0
&lt;/h2&gt;

&lt;p&gt;La regla clásica pide &lt;strong&gt;3 copias, en 2 medios distintos y 1 fuera del sitio&lt;/strong&gt; (Ruggiero &amp;amp; Heckathorn, 2012). Frente al &lt;em&gt;ransomware&lt;/em&gt;, la industria la amplió a &lt;strong&gt;3-2-1-1-0&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;1&lt;/strong&gt; copia &lt;strong&gt;inmutable o desconectada&lt;/strong&gt; (&lt;em&gt;offline&lt;/em&gt; o &lt;em&gt;air-gapped&lt;/em&gt;), que el atacante no pueda cifrar ni borrar.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;0&lt;/strong&gt; errores al &lt;strong&gt;verificar&lt;/strong&gt; las restauraciones.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Es lo mismo que pide la guía #StopRansomware de CISA, la NSA y el FBI: mantener respaldos &lt;strong&gt;desconectados y cifrados&lt;/strong&gt;, y probar con regularidad su disponibilidad e integridad (CISA et al., 2023).&lt;/p&gt;

&lt;p&gt;A esto se suma una &lt;strong&gt;política de retención&lt;/strong&gt;. La aplicación aplica el esquema &lt;strong&gt;GFS&lt;/strong&gt; (abuelo-padre-hijo): conserva 7 respaldos diarios, 4 semanales y 6 mensuales, y marca el resto como “expira”.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Despliegue automatizado
&lt;/h2&gt;

&lt;p&gt;El repositorio público se despliega solo, sin pasos manuales:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;deploy.yml&lt;/code&gt;&lt;/strong&gt;: en cada &lt;em&gt;push&lt;/em&gt; a &lt;code&gt;main&lt;/code&gt; ejecuta &lt;strong&gt;11 pruebas&lt;/strong&gt; con Vitest. Cubren cadenas completas, diferenciales e incrementales, la ruptura de la cadena, el ransomware, el cifrado con frase incorrecta o paquete alterado, la comparación de políticas y la retención GFS. Después construye con Vite, publica en &lt;strong&gt;GitHub Pages&lt;/strong&gt; y hace una prueba de humo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;simulacro.yml&lt;/code&gt;&lt;/strong&gt;: cada lunes repite el simulacro de restauración, porque un respaldo que nunca se restaura no está probado.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqzyjqe789k88us426pns.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqzyjqe789k88us426pns.png" alt="GitHub Actions" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  8. ¿Cuál elijo?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Base pequeña o RTO muy exigente&lt;/strong&gt; → completo frecuente, porque se restaura en un solo paso.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Equilibrio entre espacio y simplicidad&lt;/strong&gt; → completo semanal + &lt;strong&gt;diferencial&lt;/strong&gt; diario.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Base grande con pocos cambios diarios, o ancho de banda limitado&lt;/strong&gt; → completo semanal + &lt;strong&gt;incremental&lt;/strong&gt;, con verificación automática de la cadena y copias redundantes de cada eslabón.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RPO de minutos&lt;/strong&gt; → ninguna de las tres basta por sí sola. Hay que sumar replicación o archivado continuo del log de transacciones (WAL en SQLite o PostgreSQL, &lt;em&gt;binlog&lt;/em&gt; en MySQL).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Y siempre: cifra, separa al menos una copia del sistema principal y &lt;strong&gt;ensaya la restauración&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Referencias
&lt;/h2&gt;

&lt;p&gt;Adam, S. (2024). &lt;em&gt;The impact of compromised backups on ransomware outcomes&lt;/em&gt;. Sophos. &lt;a href="https://www.sophos.com/en-us/blog/the-impact-of-compromised-backups-on-ransomware-outcomes" rel="noopener noreferrer"&gt;https://www.sophos.com/en-us/blog/the-impact-of-compromised-backups-on-ransomware-outcomes&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Chervenak, A., Vellanki, V., &amp;amp; Kurmas, Z. (1998). Protecting file systems: A survey of backup techniques. En &lt;em&gt;Proceedings of the Joint NASA and IEEE Mass Storage Conference&lt;/em&gt;. &lt;a href="https://www.researchgate.net/publication/2431989_Protecting_File_Systems_A_Survey_of_Backup_Techniques" rel="noopener noreferrer"&gt;https://www.researchgate.net/publication/2431989_Protecting_File_Systems_A_Survey_of_Backup_Techniques&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;CISA, MS-ISAC, NSA &amp;amp; FBI. (2023). &lt;em&gt;#StopRansomware guide&lt;/em&gt;. Cybersecurity and Infrastructure Security Agency. &lt;a href="https://www.cisa.gov/stopransomware/ransomware-guide" rel="noopener noreferrer"&gt;https://www.cisa.gov/stopransomware/ransomware-guide&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Dworkin, M. (2007). &lt;em&gt;Recommendation for block cipher modes of operation: Galois/Counter Mode (GCM) and GMAC&lt;/em&gt; (NIST Special Publication 800-38D). National Institute of Standards and Technology. &lt;a href="https://doi.org/10.6028/NIST.SP.800-38D" rel="noopener noreferrer"&gt;https://doi.org/10.6028/NIST.SP.800-38D&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;OWASP Foundation. (s.f.). &lt;em&gt;Password storage cheat sheet&lt;/em&gt;. OWASP Cheat Sheet Series. Recuperado el 3 de octubre de 2026, de &lt;a href="https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html" rel="noopener noreferrer"&gt;https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Ruggiero, P., &amp;amp; Heckathorn, M. A. (2012). &lt;em&gt;Data backup options&lt;/em&gt;. United States Computer Emergency Readiness Team (US-CERT). &lt;a href="https://www.cisa.gov/sites/default/files/publications/data_backup_options.pdf" rel="noopener noreferrer"&gt;https://www.cisa.gov/sites/default/files/publications/data_backup_options.pdf&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;SQLite. (s.f.-a). &lt;em&gt;Database file format&lt;/em&gt;. Recuperado el 3 de octubre de 2026, de &lt;a href="https://www.sqlite.org/fileformat.html" rel="noopener noreferrer"&gt;https://www.sqlite.org/fileformat.html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;SQLite. (s.f.-b). &lt;em&gt;SQLite backup API&lt;/em&gt;. Recuperado el 3 de octubre de 2026, de &lt;a href="https://www.sqlite.org/backup.html" rel="noopener noreferrer"&gt;https://www.sqlite.org/backup.html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;SQLite. (s.f.-c). &lt;em&gt;Database remote-copy tool for SQLite (sqlite3_rsync)&lt;/em&gt;. Recuperado el 3 de octubre de 2026, de &lt;a href="https://www.sqlite.org/rsync.html" rel="noopener noreferrer"&gt;https://www.sqlite.org/rsync.html&lt;/a&gt;&lt;/p&gt;

</description>
      <category>database</category>
      <category>sqlite</category>
      <category>backup</category>
      <category>security</category>
    </item>
  </channel>
</rss>
