# Transacciones y UPSERT, la red de seguridad

URL canónica: https://missyera.com/guias/sql-desde-cero/upsert-y-transacciones/
Última actualización: 2026-11-14
Autora: Gera Flores (Miss Yera)

> **Respuesta corta**
>
> Una transacción es un todo o nada. BEGIN abre, COMMIT confirma y ROLLBACK deshace, y esa costumbre es la que a mí me ha convertido más de un UPDATE sin WHERE en un susto en vez de en un desastre. El upsert, que es insertar y si ya está actualizar, se escribe ON CONFLICT DO UPDATE en PostgreSQL y SQLite, ON DUPLICATE KEY UPDATE en MySQL y MERGE en SQL Server. Y ojo con INSERT OR REPLACE, que no actualiza: borra la fila y crea otra.

Capítulo 12 de 23 del libro [SQL desde cero](https://missyera.com/guias/sql-desde-cero.md).

En el capítulo 10 apagué la tabla entera con un `UPDATE` sin `WHERE`. Se arregló porque eran cinco filas de mentira.

Este capítulo es la red que hace que eso se pueda deshacer de verdad, y de paso resuelve la otra pregunta que aparece el primer día que cargas datos: *¿y si la fila ya está?*

Y una más, que es la que de verdad separa a quien carga datos de quien los cuida: **¿cómo sabes que se cargó todo lo que tenías que cargar?** Si la respuesta es "porque no dio error", este capítulo es para ti 🧾

## Una transacción es un "todo o nada"

Vamos con el ejemplo de siempre porque es el que se entiende sin explicar nada: mover plata de una cuenta a otra.

```
CREATE TABLE saldos (
    id_cliente INTEGER PRIMARY KEY,
    saldo      REAL NOT NULL DEFAULT 0
);
INSERT INTO saldos (id_cliente, saldo) VALUES (1, 500.00), (2, 300.00);
SELECT * FROM saldos ORDER BY id_cliente;
```

```text
id_cliente  saldo
----------  -----
1           500.0
2           300.0
```

Pasar 200 del cliente 1 al 2 son dos `UPDATE`. Y el problema está en el medio: si entre el primero y el segundo se cae la luz, se cortó la red o alguien mató el proceso, el cliente 1 se quedó sin sus 200 y el 2 nunca los recibió. La plata se evaporó 😳

```
BEGIN;
UPDATE saldos SET saldo = saldo - 200 WHERE id_cliente = 1;
UPDATE saldos SET saldo = saldo + 200 WHERE id_cliente = 2;
COMMIT;
SELECT * FROM saldos ORDER BY id_cliente;
```

```text
id_cliente  saldo
----------  -----
1           300.0
2           500.0
```

`BEGIN` abre la transacción, `COMMIT` la cierra y la hace real. Entre esas dos líneas, **o pasa todo o no pasa nada**. Si el proceso se muere en el medio, la base deshace lo que llevaba y las cuentas quedan como estaban.

Y aquí está lo que de verdad te va a salvar el pellejo.

```
BEGIN;
UPDATE saldos SET saldo = saldo - 300 WHERE id_cliente = 1;
UPDATE saldos SET saldo = 0;
ROLLBACK;
SELECT * FROM saldos ORDER BY id_cliente;
```

```text
id_cliente  saldo
----------  -----
1           300.0
2           500.0
```

Ese segundo `UPDATE` es el desastre del capítulo 10: sin `WHERE`, saldo cero para todo el mundo. Y el `ROLLBACK` lo deshizo. Los saldos siguen en 300 y 500 como si nada hubiera pasado 🙌

**Esa es la costumbre que quiero que te lleves de este capítulo: `BEGIN`, haces tu cambio, miras con un `SELECT`, y recién ahí `COMMIT` o `ROLLBACK`.** Es un segundo más y convierte cualquier metida de pata en un susto.

Abrir una transacción

| PostgreSQL | `BEGIN; -- también START TRANSACTION;` |
|---|---|
| MySQL | `START TRANSACTION; -- también BEGIN;` |
| SQL Server | `BEGIN TRANSACTION; -- o BEGIN TRAN;` |
| SQLite | `BEGIN; -- también BEGIN TRANSACTION;` |

COMMIT y ROLLBACK sí se llaman igual en los cuatro. Lo que cambia es cómo se abre, y SQL Server es el único que necesita la palabra TRANSACTION.

![El ciclo de una transacción: BEGIN, el UPDATE o el DELETE, un SELECT para mirar cómo quedó, y según si es lo que querías, COMMIT o ROLLBACK, que lo deja como si nunca hubiera pasado.](https://missyera.com/static/diagramas/webp/sql-08-transaccion.webp)

*El paso que casi nadie hace es el tercero. Mirar cómo quedó antes de confirmar es lo que convierte un UPDATE sin WHERE en un susto en vez de en una tarde muy larga.*

## Deshacer solo un pedacito: SAVEPOINT

```
BEGIN;
UPDATE saldos SET saldo = saldo + 10 WHERE id_cliente = 1;
SAVEPOINT antes_del_lio;
UPDATE saldos SET saldo = 999999 WHERE id_cliente = 2;
ROLLBACK TO antes_del_lio;
COMMIT;
SELECT * FROM saldos ORDER BY id_cliente;
```

```text
id_cliente  saldo
----------  -----
1           310.0
2           500.0
```

El primer `UPDATE` se quedó (el cliente 1 pasó de 300 a 310) y el segundo se deshizo. Un `SAVEPOINT` es una marca a la que puedes volver sin tirar toda la transacción, y sirve muchísimo en cargas largas: si el paso 7 de 10 falla, vuelves al 6 y sigues, en vez de empezar de cero.

Está en los cuatro motores con el mismo nombre.

## Lo que no se puede deshacer

Aquí hay una diferencia entre motores que muerde fuerte y que casi nadie avisa.

¿Un CREATE TABLE dentro de una transacción se puede deshacer?

| PostgreSQL | `sí, el DDL es transaccional como todo lo demás` |
|---|---|
| MySQL | `NO: cualquier CREATE, ALTER o DROP confirma la transacción abierta sin avisar` |
| SQL Server | `sí` |
| SQLite | `sí` |

Lo de MySQL se llama commit implícito y es de las cosas que más caro salen: crees que estás dentro de un BEGIN, metes un ALTER TABLE en el medio, y todo lo anterior quedó confirmado. Tu ROLLBACK ya no deshace nada y no te lo dijo nadie.

Y una que vale para los cuatro: **por defecto, cada sentencia suelta es su propia transacción**. Eso se llama autocommit, y significa que un `DELETE` sin `BEGIN` está confirmado en el momento en que termina. No hay nada que deshacer.

## Insertar algo que a lo mejor ya está

Cambio de tema, mismo capítulo, porque las dos cosas van juntas en la vida real: cargar datos.

Tienes una tabla de metas por canal y cada mes te llega el archivo nuevo. La mitad de los canales ya están y hay que actualizarlos; la otra mitad son nuevos y hay que insertarlos. Eso es un **upsert**: update si está, insert si no.

```
CREATE TABLE metas (
    canal       TEXT PRIMARY KEY,
    meta        REAL NOT NULL,
    actualizada TEXT NOT NULL DEFAULT '2026-01-01'
);
INSERT INTO metas (canal, meta) VALUES ('Web', 150000), ('WhatsApp', 140000);
SELECT * FROM metas ORDER BY canal;
```

```text
canal     meta      actualizada
--------  --------  -----------
Web       150000.0  2026-01-01
WhatsApp  140000.0  2026-01-01
```

Si intentas insertar Web otra vez, ya sabes lo que pasa.

```
INSERT INTO metas (canal, meta) VALUES ('Web', 999999);
```

```
IntegrityError: UNIQUE constraint failed: metas.canal
```

La forma que sale sola es `DELETE` y después `INSERT`, y es la forma mala: entre las dos sentencias la fila no existe, así que si algo lee justo ahí, ve un hueco. Y si el `INSERT` falla, borraste el dato bueno.

```
INSERT INTO metas (canal, meta) VALUES ('Web', 160000)
ON CONFLICT (canal) DO UPDATE SET meta = excluded.meta, actualizada = '2026-06-24';
SELECT * FROM metas ORDER BY canal;
```

```text
canal     meta      actualizada
--------  --------  -----------
Web       160000.0  2026-06-24
WhatsApp  140000.0  2026-01-01
```

Se lee tal cual: "insértalo, y si choca con la clave `canal`, entonces actualiza". Ese `excluded` es la palabra clave: es **la fila que querías insertar**, la que se quedó fuera. Así puedes decir "ponle el valor nuevo" sin repetirlo.

```
INSERT INTO metas (canal, meta) VALUES ('Tienda', 120000)
ON CONFLICT (canal) DO UPDATE SET meta = excluded.meta, actualizada = '2026-06-24';
SELECT * FROM metas ORDER BY canal;
```

```text
canal     meta      actualizada
--------  --------  -----------
Tienda    120000.0  2026-01-01
Web       160000.0  2026-06-24
WhatsApp  140000.0  2026-01-01
```

La misma sentencia, sin tocar una letra, insertó Tienda porque no existía. Y fíjate en su `actualizada`: se quedó en 2026-01-01, el `DEFAULT`, porque el `DO UPDATE` no se ejecutó. Esa columna te dice de un vistazo cuáles se actualizaron y cuáles nacieron 🔍

Insertar y, si ya está, actualizar

| PostgreSQL | `INSERT ... ON CONFLICT (canal) DO UPDATE SET meta = EXCLUDED.meta` |
|---|---|
| MySQL | `INSERT ... ON DUPLICATE KEY UPDATE meta = VALUES(meta)` |
| SQL Server | `MERGE INTO ... WHEN MATCHED THEN UPDATE ... WHEN NOT MATCHED THEN INSERT ...` |
| SQLite | `INSERT ... ON CONFLICT (canal) DO UPDATE SET meta = excluded.meta` |

PostgreSQL y SQLite se escriben idéntico, MySQL cambia las palabras y no te deja elegir qué clave, y SQL Server te obliga a un MERGE de seis líneas que además arrastra fama de tener rarezas. Si tu consulta tiene que correr en los cuatro, el upsert es de las primeras cosas que se rompe.

Y un aviso sobre el `VALUES(meta)` de MySQL: está marcado como obsoleto desde la 8.0.20, y lo nuevo es ponerle alias a la fila y escribir `AS nueva ... ON DUPLICATE KEY UPDATE meta = nueva.meta`. Si copias código viejo de internet te va a salir un aviso 🫠

## Las otras dos formas de SQLite, y una que muerde

```
INSERT OR IGNORE INTO metas (canal, meta) VALUES ('Web', 1);
SELECT * FROM metas ORDER BY canal;
```

```text
canal     meta      actualizada
--------  --------  -----------
Tienda    120000.0  2026-01-01
Web       160000.0  2026-06-24
WhatsApp  140000.0  2026-01-01
```

`INSERT OR IGNORE` es "si choca, no hagas nada". Web se quedó en 160000 y la sentencia no dio error. Sirve para cargar sin duplicar cuando lo que ya está es lo bueno.

```
INSERT OR REPLACE INTO metas (canal, meta) VALUES ('Web', 170000);
SELECT * FROM metas ORDER BY canal;
```

```text
canal     meta      actualizada
--------  --------  -----------
Tienda    120000.0  2026-01-01
Web       170000.0  2026-01-01
WhatsApp  140000.0  2026-01-01
```

Ahora mira bien la columna `actualizada` de Web. Estaba en 2026-06-24 y volvió a 2026-01-01 😳

Porque `INSERT OR REPLACE` **no actualiza: borra la fila entera y mete una nueva**. Todo lo que no nombraste vuelve a su `DEFAULT`, y si la columna no tuviera `DEFAULT` quedaría en `NULL`. Y lo peor: si otra tabla apuntaba a esa fila con una clave foránea en cascada, el borrado se lleva por delante lo de allá.

Es de esas cosas que funcionan durante meses y un día te dejan sin datos. Si lo que quieres es actualizar, usa `ON CONFLICT DO UPDATE` y nombra las columnas 💛

## El upsert que calcula

```
INSERT INTO metas (canal, meta) VALUES ('WhatsApp', 5000)
ON CONFLICT (canal) DO UPDATE SET meta = metas.meta + excluded.meta;
SELECT * FROM metas ORDER BY canal;
```

```text
canal     meta      actualizada
--------  --------  -----------
Tienda    120000.0  2026-01-01
Web       170000.0  2026-01-01
WhatsApp  145000.0  2026-01-01
```

WhatsApp pasó de 140.000 a 145.000: no lo reemplazó, le sumó. En el `DO UPDATE` tienes las dos filas a mano, `metas.meta` es la vieja y `excluded.meta` es la nueva, y puedes hacer con ellas lo que quieras.

Así se llevan los acumulados sin leer primero: es una sola ida a la base en vez de un `SELECT`, una cuenta y un `UPDATE`. Menos código y, sobre todo, sin el hueco de tiempo en el que otro puede meterse.

Y el upsert también funciona con un `SELECT` en vez de `VALUES`, que es como se cargan tablas enteras.

```
INSERT INTO metas (canal, meta)
SELECT canal, ROUND(SUM(monto) * 1.1, 2) FROM pedidos GROUP BY canal
ON CONFLICT (canal) DO UPDATE SET meta = excluded.meta, actualizada = '2026-06-24';
SELECT * FROM metas ORDER BY canal;
```

```text
canal        meta       actualizada
-----------  ---------  -----------
Marketplace  149329.45  2026-01-01
Tienda       130333.58  2026-06-24
Web          154490.79  2026-06-24
WhatsApp     151765.42  2026-06-24
```

Las metas del año que viene, calculadas como la venta real más un 10%, en una sola sentencia. Los tres canales que ya estaban se actualizaron y Marketplace entró nuevo, y se distingue por la fecha.

Esta es de las consultas que más se parecen a un trabajo de verdad de todo el libro 🌟

Insertar y, si ya está, no hacer nada

| PostgreSQL | `INSERT ... ON CONFLICT DO NOTHING` |
|---|---|
| MySQL | `INSERT IGNORE INTO ...` |
| SQL Server | `no lo tiene: se resuelve con MERGE o con IF NOT EXISTS` |
| SQLite | `INSERT OR IGNORE ... -- y también ON CONFLICT DO NOTHING` |

Cuidado con el INSERT IGNORE de MySQL, que es más ancho de lo que parece: se traga también otros errores, como un texto demasiado largo o una fecha inválida, y los convierte en avisos. Lo que querías ignorar era el duplicado, no todo.

Y el `MERGE` de SQL Server, para que veas que no está en los otros.

```
MERGE INTO metas AS t USING (SELECT 'Web' AS canal) AS s ON t.canal = s.canal
WHEN MATCHED THEN UPDATE SET meta = 1;
```

```
OperationalError: near "MERGE": syntax error
```

`MERGE` es del estándar y lo tienen SQL Server y PostgreSQL desde la 15. MySQL y SQLite no. Es más potente que el upsert (puede insertar, actualizar y borrar en la misma sentencia) y también bastante más difícil de leer.

## Cuando dos personas escriben a la vez

Todo lo de arriba asume que estás tú sola. En una base de verdad hay veinte procesos escribiendo, y ahí es donde las transacciones dejan de ser una comodidad y pasan a ser lo que sostiene todo.

El nombre elegante es **aislamiento**: cuánto se enteran unas transacciones de lo que están haciendo las otras mientras van a medias.

El nivel de aislamiento por defecto

| PostgreSQL | `READ COMMITTED` |
|---|---|
| MySQL | `REPEATABLE READ` |
| SQL Server | `READ COMMITTED` |
| SQLite | `SERIALIZABLE, porque solo deja escribir a uno a la vez` |

READ COMMITTED significa que solo ves lo que otros ya confirmaron. REPEATABLE READ, el de MySQL, además te garantiza que si lees dos veces lo mismo dentro de la transacción te sale igual. Que el default no sea el mismo explica por qué el mismo código se comporta distinto al cambiar de motor, y es de las cosas más difíciles de depurar que hay.

¿Qué se bloquea mientras escribes?

| PostgreSQL | `la fila` |
|---|---|
| MySQL | `la fila, con InnoDB` |
| SQL Server | `la fila, la página o la tabla, según lo que decida el motor` |
| SQLite | `la BASE ENTERA: el que llega segundo recibe "database is locked"` |

Esto es lo que hace que SQLite sea perfecta para aprender, para una app de escritorio o para un archivo de análisis, y mala idea para una web con gente escribiendo a la vez. No es un defecto, es la decisión de diseño que la hace caber en un archivo.

## Ejercicios

Siete, y estos escriben. Sobre tu copia 💛

**1. El UPDATE que se deshace**

Sube todos los saldos un 10% y déjalos como estaban.

```
BEGIN;
UPDATE saldos SET saldo = saldo * 1.10;
ROLLBACK;
SELECT id_cliente, ROUND(saldo, 2) AS saldo FROM saldos ORDER BY id_cliente;
```

```text
id_cliente  saldo
----------  -----
1           310.0
2           500.0
```

310 y 500, los mismos de antes. El `UPDATE` tocó las dos filas y el `ROLLBACK` las devolvió.

En tu cliente de SQL mete un `SELECT` entre el `UPDATE` y el `ROLLBACK` para verlos subidos antes de decidir. Aquí no lo pongo porque cada bloque del libro publica solo lo que devuelve su última sentencia, y quiero que la última sea la prueba de que no quedó nada.

**2. Un upsert de metas por segmento**

Crea una tabla de metas por segmento y llénala con la venta real de cada uno más un 15%, de forma que se pueda volver a correr sin duplicar.

```
CREATE TABLE metas_segmento (
    segmento    TEXT PRIMARY KEY,
    meta        REAL NOT NULL,
    veces       INTEGER NOT NULL DEFAULT 1
);
INSERT INTO metas_segmento (segmento, meta)
SELECT c.segmento, ROUND(SUM(p.monto) * 1.15, 2)
FROM clientes c JOIN pedidos p ON p.id_cliente = c.id
GROUP BY c.segmento
ON CONFLICT (segmento) DO UPDATE SET meta = excluded.meta, veces = metas_segmento.veces + 1;
SELECT * FROM metas_segmento ORDER BY meta DESC;
```

```text
segmento    meta       veces
----------  ---------  -----
Horeca      174687.46  1
Bodega      166565.91  1
Mayorista   133456.91  1
Minimarket  120348.63  1
```

Esa columna `veces` es un truco que uso mucho: te dice cuántas veces se recargó cada fila. La primera corrida deja todo en 1.

**3. La misma carga, otra vez**

Corre exactamente la misma sentencia del ejercicio 2 y mira qué cambia.

```
INSERT INTO metas_segmento (segmento, meta)
SELECT c.segmento, ROUND(SUM(p.monto) * 1.15, 2)
FROM clientes c JOIN pedidos p ON p.id_cliente = c.id
GROUP BY c.segmento
ON CONFLICT (segmento) DO UPDATE SET meta = excluded.meta, veces = metas_segmento.veces + 1;
SELECT * FROM metas_segmento ORDER BY meta DESC;
```

```text
segmento    meta       veces
----------  ---------  -----
Horeca      174687.46  2
Bodega      166565.91  2
Mayorista   133456.91  2
Minimarket  120348.63  2
```

Cuatro filas, las mismas, y `veces` en 2. Eso se llama que la carga es **idempotente**: la corres una vez o diez y el resultado es el mismo. Es la propiedad más valiosa que puede tener un proceso de datos, porque el día que algo falle a la mitad, vuelves a lanzarlo y ya 🌟

**4. El REPLACE que borra sin querer**

Comprueba en tu tabla de metas por segmento que `INSERT OR REPLACE` se lleva puesta la columna que no nombraste.

```
INSERT OR REPLACE INTO metas_segmento (segmento, meta) VALUES ('Horeca', 1000);
SELECT * FROM metas_segmento ORDER BY segmento;
```

```text
segmento    meta       veces
----------  ---------  -----
Bodega      166565.91  2
Horeca      1000.0     1
Mayorista   133456.91  2
Minimarket  120348.63  2
```

Horeca perdió su `veces` y volvió a 1, porque la fila se borró y nació otra. Los demás siguen en 2.

Con `ON CONFLICT DO UPDATE SET meta = excluded.meta` eso no habría pasado: lo que no nombras, no se toca.

**5. La carga que falla a la mitad**

Inserta un segmento nuevo dentro de una transacción, deshazla, y comprueba cuántas filas quedaron.

```
BEGIN;
INSERT INTO metas_segmento (segmento, meta) VALUES ('Nuevo', 500);
ROLLBACK;
SELECT COUNT(*) AS filas FROM metas_segmento;
```

```text
filas
-----
4
```

Cuatro: el 'Nuevo' no quedó. En una carga de verdad esto es lo que hace la diferencia entre "no se cargó nada, vuelve a lanzarlo" y "se cargó a medias y ahora hay que averiguar por dónde iba" 😵‍💫

**6. Un contador que no se lee antes**

Lleva la cuenta de cuántas veces se ha visto cada canal, sumando de a uno, sin hacer un `SELECT` previo.

```
CREATE TABLE visitas_canal (canal TEXT PRIMARY KEY, veces INTEGER NOT NULL);
INSERT INTO visitas_canal (canal, veces) VALUES ('Web', 1)
ON CONFLICT (canal) DO UPDATE SET veces = visitas_canal.veces + 1;
INSERT INTO visitas_canal (canal, veces) VALUES ('Web', 1)
ON CONFLICT (canal) DO UPDATE SET veces = visitas_canal.veces + 1;
INSERT INTO visitas_canal (canal, veces) VALUES ('WhatsApp', 1)
ON CONFLICT (canal) DO UPDATE SET veces = visitas_canal.veces + 1;
SELECT * FROM visitas_canal ORDER BY canal;
```

```text
canal     veces
--------  -----
Web       2
WhatsApp  1
```

Web en 2 y WhatsApp en 1, con la misma sentencia repetida. Sin leer antes, sin saber si la fila existe, y sin un hueco de tiempo donde otro proceso pueda meterse entre el `SELECT` y el `UPDATE`.

**7. Escríbelo para los cuatro**

Sin ejecutar: subir la meta de Web a 200.000, insertándola si no existiera, en los cuatro motores.

```
-- PostgreSQL y SQLite (idénticos)
INSERT INTO metas (canal, meta) VALUES ('Web', 200000)
ON CONFLICT (canal) DO UPDATE SET meta = EXCLUDED.meta;

-- MySQL
INSERT INTO metas (canal, meta) VALUES ('Web', 200000) AS nueva
ON DUPLICATE KEY UPDATE meta = nueva.meta;

-- SQL Server
MERGE INTO metas AS destino
USING (VALUES ('Web', 200000)) AS origen (canal, meta)
    ON destino.canal = origen.canal
WHEN MATCHED THEN UPDATE SET destino.meta = origen.meta
WHEN NOT MATCHED THEN INSERT (canal, meta) VALUES (origen.canal, origen.meta);
```

Dos líneas en tres motores y cinco en SQL Server. Y ojo al punto y coma final del `MERGE`: es obligatorio, cosa que no pasa con ninguna otra sentencia de T-SQL, y es de los errores que más rabia dan porque el mensaje no te dice eso.

Si estás escribiendo algo que tiene que correr en los cuatro, este es el sitio donde toca partir el código en dos caminos. No hay forma de escribirlo una sola vez 🫠

## La fila que se perdió en silencio

Ya sabes lo que hace cada forma de insertar. Ahora mira lo que pasa cuando eliges la que no revienta por si acaso 🤫

La trampa

Cargas el archivo del mes con INSERT OR IGNORE, para que no reviente si alguna fila ya estaba. Corre limpio y sigues.

```
INSERT OR IGNORE INTO clientes VALUES
    (1, 'Bodega Aurora', 'Lima', 'Bodega', '2025-01-01'),
    (2, 'Minimarket Sur', 'Cusco', 'Bodega', '2025-01-01');
-- 1 fila insertada de 2
```

**Qué está mal**

Metiste dos filas y entró una 🤫 El `OR IGNORE` hace exactamente lo que dice: si la fila choca con algo, se la salta. Sin error, sin aviso, sin decirte cuál.

Y el problema no es esa fila. Es que la usaste porque no querías que reventara, y ahora tampoco te enteras cuando **sí había algo que saber**: que el archivo del mes traía un cliente que ya existía con otros datos, y los nuevos se perdieron.

Si lo que quieres es actualizar lo que ya está, eso tiene su propia instrucción y dice lo que hace: `ON CONFLICT (id) DO UPDATE SET ...`. Si lo que quieres de verdad es ignorar, compara el número de filas del archivo con `changes()` y avisa cuando no cuadren.

La costumbre entera cabe en una frase: cuenta antes, cuenta después, y que la diferencia no te la cuente nadie.

Comprueba que lo tienes

Dentro de una transacción borras mil filas por error y escribes ROLLBACK. ¿Qué vuelve y qué no?

- Vuelven las filas, pero un cambio de estructura hecho en medio no
- Vuelve todo, para eso está el ROLLBACK
- No vuelve nada, el DELETE es definitivo
- Vuelven solo las filas que no había leído nadie

## Lo que te llevas

- 🔒 `BEGIN`, cambio, `SELECT` para mirar, y recién ahí `COMMIT` o `ROLLBACK`. Es la costumbre que convierte un desastre en un susto.
- 📍 `SAVEPOINT` y `ROLLBACK TO` deshacen un pedacito sin tirar toda la transacción. Están en los cuatro.
- ⚠️ En MySQL, un `CREATE`, `ALTER` o `DROP` confirma la transacción abierta sin avisar. En los otros tres, el DDL se puede deshacer.
- 🔁 El upsert es `ON CONFLICT DO UPDATE` en PostgreSQL y SQLite, `ON DUPLICATE KEY UPDATE` en MySQL y `MERGE` en SQL Server.
- 🎯 `excluded` es la fila que querías insertar, y con ella puedes sumar en vez de reemplazar.
- 💣 `INSERT OR REPLACE` borra la fila y crea otra: lo que no nombras vuelve al `DEFAULT`. No es un update.
- ♻️ Una carga con upsert es idempotente: la corres diez veces y da lo mismo.
- 🚦 SQLite bloquea la base entera al escribir. Por eso es genial para aprender y mala para una web con mucha gente escribiendo.

Y si de todo el capítulo te llevas una sola frase, que sea esta:

Cuenta antes, cuenta después, y que la diferencia no te la cuente nadie.

Y cargar un archivo en una tabla es media pelea. La otra media es abrir el archivo sin romperlo, con su codificación y sus tipos, y eso está en el [libro de Python](https://missyera.com/guias/python-desde-cero/) 📂

En el capítulo 12 vamos a por qué una consulta tarda: índices, `EXPLAIN` y cómo leer lo que la base te contesta cuando le preguntas qué piensa hacer.

Que tengas lindo día! 🌸

## Seguir leyendo

- Anterior: [Crear tablas y escribir en ellas](https://missyera.com/guias/sql-desde-cero/crear-y-modificar.md)
- Siguiente: [Cuando el dato lo escribe un desconocido](https://missyera.com/guias/sql-desde-cero/inyeccion-sql.md)

## Contacto

- Diagnóstico de 30 minutos con Miss Yera: https://calendly.com/soymissyera/reunion-con-yera
- Correo: hola@missyera.com
- Esta página en HTML: https://missyera.com/guias/sql-desde-cero/upsert-y-transacciones/
- Índice de todas las páginas en texto: https://missyera.com/llms-index.txt
