{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Cuando las tablas estorban: bases de documentos\n",
    "\n",
    "Qué es una base NoSQL de documentos, cómo se ve el mismo pedido guardado de las dos maneras, y qué se gana y qué se pierde.\n",
    "\n",
    "Cuaderno de práctica del capítulo 20 de **SQL desde cero**, de Miss Yera.\n",
    "\n",
    "Corre de arriba abajo. Si lo abres en Google Colab no necesitas instalar nada.\n",
    "\n",
    "Capítulo completo: https://missyera.com/guias/sql-desde-cero/nosql-y-documentos/\n",
    "\n",
    "Los ejercicios están al final y traen una celda vacía debajo de cada uno. Las\n",
    "respuestas viven en el cuaderno de soluciones, y merece la pena pelearse un\n",
    "rato antes de abrirlo 💛"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Antes de empezar\n",
    "\n",
    "Se baja la base y se deja lista una función `q()` que corre las consultas."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import sqlite3\n",
    "import urllib.request\n",
    "\n",
    "import pandas as pd\n",
    "\n",
    "urllib.request.urlretrieve(\"https://missyera.com/static/datasets/tienda.db\", \"tienda.db\")\n",
    "con = sqlite3.connect(\"tienda.db\")\n",
    "\n",
    "def q(sql):\n",
    "    \"\"\"Corre las sentencias del bloque y devuelve la ultima como tabla.\n",
    "\n",
    "    Parte por sentencias igual que la consola de SQLite, porque un bloque puede\n",
    "    traer varias y un CREATE TRIGGER lleva punto y coma dentro de su cuerpo.\n",
    "    \"\"\"\n",
    "    resultado = None\n",
    "    trozo = \"\"\n",
    "    for linea in sql.splitlines(keepends=True):\n",
    "        trozo += linea\n",
    "        if sqlite3.complete_statement(trozo):\n",
    "            if trozo.strip():\n",
    "                cur = con.execute(trozo.strip())\n",
    "                resultado = (pd.DataFrame(cur.fetchall(),\n",
    "                                          columns=[d[0] for d in cur.description])\n",
    "                             if cur.description else None)\n",
    "            trozo = \"\"\n",
    "    if trozo.strip():\n",
    "        cur = con.execute(trozo.strip())\n",
    "        resultado = (pd.DataFrame(cur.fetchall(),\n",
    "                                  columns=[d[0] for d in cur.description])\n",
    "                     if cur.description else None)\n",
    "    con.commit()\n",
    "    return resultado\n",
    "\n",
    "q(\"SELECT name FROM sqlite_master WHERE type = 'table' ORDER BY name\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Antes de empezar\n",
    "\n",
    "Esta celda baja el ayudante que corrige tus ejercicios. Después, en cada\n",
    "ejercicio que se pueda corregir solo, vas a ver `%%revisa` arriba de la celda:\n",
    "escribe tu respuesta debajo, ejecuta, y te digo si te salió 💛"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import urllib.request\n",
    "\n",
    "# El ayudante de los cuadernos. Trae la corrección de los ejercicios y, en los\n",
    "# capítulos de consola, la celda mágica que ejecuta los comandos. Se baja en\n",
    "# vez de venir pegado aquí para que siempre sea el último.\n",
    "urllib.request.urlretrieve(\n",
    "    \"https://missyera.com/static/cuadernos/revisa.py\", \"revisa.py\")\n",
    "import revisa\n",
    "revisa.carga({\n",
    "    1: \"aWQgICAgIGN1ZXJwbwotLS0tLSAgLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0KOTk5OTkgIHsicGVkaWRvIjo5OTk5OSwiY2FuYWwiOiJXaGF0c0FwcCIsInRvdGFsX3NvbGVzIjoyNTAsIm5vdGEiOiJzaW4gY2xpZW50ZSJ9\",\n",
    "    2: \"Y2FtcG8gICAgICAgIGRvY3VtZW50b3MKLS0tLS0tLS0tLS0gIC0tLS0tLS0tLS0KcGVkaWRvICAgICAgIDg3NwpjYW5hbCAgICAgICAgODc3CmxpbmVhcyAgICAgICA4NzYKZmVjaGEgICAgICAgIDg3NgpjbGllbnRlICAgICAgODc2CnRvdGFsX3NvbGVzICAxCm5vdGEgICAgICAgICAx\",\n",
    "    3: \"aWQgIHBhcmVudCAgbm90dXNlZCAgZGV0YWlsCi0tICAtLS0tLS0gIC0tLS0tLS0gIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0KNCAgIDAgICAgICAgMCAgICAgICAgU0VBUkNIIGRvY3VtZW50b3MgVVNJTkcgSU5ERVggaWR4X2NpdWRhZF9kb2MgKDxleHByPj0/KQ==\",\n",
    "    5: \"Y2FtcG9zICAgICAgICAgICAgICAgICAgICAgICAgICAgICBkb2N1bWVudG9zCi0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLSAgLS0tLS0tLS0tLQpwZWRpZG8sZmVjaGEsY2FuYWwsY2xpZW50ZSxsaW5lYXMgIDg3NgpwZWRpZG8sY2FuYWwsdG90YWxfc29sZXMsbm90YSAgICAgIDE=\",\n",
    "    6: \"dGlwbyAgICAgbGluZWFzCi0tLS0tLS0gIC0tLS0tLQppbnRlZ2VyICAyNjA1CnRleHQgICAgIDE=\",\n",
    "}, lenguaje=\"sql\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Llevas quince capítulos con tablas, y las tablas son lo correcto casi\n",
    "siempre. Este capítulo es el \"casi\" 🌸\n",
    "\n",
    "No hace falta instalar nada raro: SQLite trae funciones JSON desde hace años,\n",
    "así que la idea entera se puede probar acá y después reconocerla en Mongo."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Un pedido, repartido en cuatro tablas"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Así lo diseñamos en el capítulo 10, y así está bien\n",
    "diseñado:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "q(\"\"\"\n",
    "SELECT p.id, c.nombre, pr.nombre AS producto, de.cantidad\n",
    "FROM pedidos p\n",
    "JOIN clientes c   ON c.id = p.id_cliente\n",
    "JOIN detalle de   ON de.id_pedido = p.id\n",
    "JOIN productos pr ON pr.id = de.id_producto\n",
    "WHERE p.id = 1;\n",
    "\"\"\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Un pedido, tres tablas unidas y cinco filas. El nombre del cliente se repite\n",
    "cinco veces en el resultado, y eso está bien: en disco está una sola vez, que\n",
    "es de lo que iba la normalización.\n",
    "\n",
    "Ahora imagínate que esto no es un reporte, es la pantalla de una app que\n",
    "tiene que abrir un pedido. Cada vez que alguien toca un pedido, tres joins."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## El mismo pedido, en una sola pieza"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "q(\"\"\"\n",
    "CREATE TABLE documentos (id INTEGER PRIMARY KEY, cuerpo TEXT);\n",
    "\n",
    "INSERT INTO documentos (id, cuerpo)\n",
    "SELECT p.id, json_object('pedido', p.id, 'fecha', p.fecha, 'canal', p.canal,\n",
    "  'cliente', json_object('nombre', c.nombre, 'ciudad', c.ciudad),\n",
    "  'lineas', (SELECT json_group_array(json_object('producto', pr.nombre,\n",
    "                                                 'cantidad', de.cantidad))\n",
    "             FROM detalle de JOIN productos pr ON pr.id = de.id_producto\n",
    "             WHERE de.id_pedido = p.id))\n",
    "FROM pedidos p JOIN clientes c ON c.id = p.id_cliente;\n",
    "\n",
    "SELECT count(*) AS documentos FROM documentos;\n",
    "\"\"\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "876 pedidos convertidos en 876 documentos. Míralo por dentro, uno cortito:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "q(\"\"\"\n",
    "SELECT cuerpo FROM documentos WHERE id = 5;\n",
    "\"\"\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ahí está el pedido entero: la cabecera, el cliente y sus líneas, todo en una\n",
    "fila. Eso es un **documento**, y un montón de documentos juntos es\n",
    "una **colección**, que es como Mongo llama a lo que aquí es una\n",
    "tabla.\n",
    "\n",
    "Fíjate en las `lineas`: es una lista dentro del documento. En una\n",
    "tabla eso no se puede, y por eso hacía falta la tabla `detalle`."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Consultarlo"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "q(\"\"\"\n",
    "SELECT json_extract(cuerpo,'$.cliente.ciudad') AS ciudad,\n",
    "       json_extract(cuerpo,'$.canal')          AS canal,\n",
    "       json_array_length(cuerpo,'$.lineas')    AS lineas\n",
    "FROM documentos WHERE id = 5;\n",
    "\"\"\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "El `$.cliente.ciudad` se lee como una ruta: entra en\n",
    "`cliente` y saca `ciudad`. En Mongo se escribe\n",
    "`\"cliente.ciudad\"`, que es lo mismo con menos símbolos.\n",
    "\n",
    "Y agrupar funciona igual que siempre:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "q(\"\"\"\n",
    "SELECT json_extract(cuerpo,'$.cliente.ciudad') AS ciudad, count(*) AS pedidos\n",
    "FROM documentos GROUP BY 1 ORDER BY 2 DESC LIMIT 5;\n",
    "\"\"\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Mismo `GROUP BY` del capítulo 6, solo que la\n",
    "columna se saca de dentro del JSON."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Lo que cuesta: desanidar"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Pregunta normal de negocio: qué producto se vende más. Con tablas es un\n",
    "`GROUP BY` sobre `detalle` y se acabó. Acá las líneas están\n",
    "metidas dentro de cada documento, así que primero hay que sacarlas:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "q(\"\"\"\n",
    "SELECT json_extract(l.value,'$.producto') AS producto,\n",
    "       sum(json_extract(l.value,'$.cantidad')) AS unidades\n",
    "FROM documentos d, json_each(d.cuerpo,'$.lineas') l\n",
    "GROUP BY 1 ORDER BY 2 DESC LIMIT 5;\n",
    "\"\"\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "`json_each` abre la lista y devuelve una fila por elemento. En\n",
    "Mongo eso se llama `$unwind` y hace exactamente lo mismo.\n",
    "\n",
    "Funciona, y fíjate en lo que pasó: para contestar una pregunta que cruza\n",
    "pedidos, tuviste que **volver a convertir los documentos en filas**.\n",
    "Ahí está el resumen del capítulo: el documento es cómodo para leer una cosa\n",
    "entera, e incómodo para preguntar por todas a la vez."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Y ahora la parte que muerde"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Meto un documento que no se parece a los otros:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "q(\"\"\"\n",
    "INSERT INTO documentos (id, cuerpo)\n",
    "VALUES (99999, json_object('pedido', 99999, 'canal', 'WhatsApp',\n",
    "                           'total_soles', 250, 'nota', 'sin cliente'));\n",
    "\n",
    "SELECT count(*) AS total,\n",
    "       count(json_extract(cuerpo,'$.cliente.ciudad')) AS con_ciudad\n",
    "FROM documentos;\n",
    "\"\"\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Entró sin una queja.** Sin cliente, con un campo\n",
    "`total_soles` que no existe en ningún otro y con una `nota`\n",
    "que nadie más tiene.\n",
    "\n",
    "877 documentos y 876 con ciudad. Ese uno es el que va a romperte el reporte\n",
    "dentro de tres meses, cuando ya nadie se acuerde de por qué está ahí.\n",
    "\n",
    "Compáralo con una tabla: si `id_cliente` es `NOT NULL`\n",
    "con su clave foránea, el mismo intento revienta en el momento, con nombre y\n",
    "apellido. Eso es lo que hacía el capítulo 11 y por eso\n",
    "insistí tanto 🧾\n",
    "\n",
    "La palabra que se usa para vender esto es *esquema flexible*. Es\n",
    "verdad, y también es verdad esto otro: **el esquema no desaparece, se\n",
    "muda**. Deja de estar en la base y pasa a estar repartido en el código de\n",
    "quien lee, en la cabeza de quien lo escribió y en ningún sitio donde se pueda\n",
    "consultar."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Cómo lo hace cada motor"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Guardar JSON dentro de una base relacional lo soportan los cuatro, con nombres\n",
    "distintos:"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Sacar un campo de dentro de un JSON\n",
    "\n",
    "| PostgreSQL | `SELECT cuerpo -> 'cliente' ->> 'ciudad' FROM documentos;` |\n",
    "|---|---|\n",
    "| MySQL | `SELECT cuerpo->>'$.cliente.ciudad' FROM documentos;` |\n",
    "| SQL Server | `SELECT JSON_VALUE(cuerpo, '$.cliente.ciudad') FROM documentos;` |\n",
    "| SQLite | `SELECT json_extract(cuerpo, '$.cliente.ciudad') FROM documentos;` |\n",
    "\n",
    "La ruta con $ la usan tres de los cuatro. PostgreSQL va por su lado con las flechas, y la doble flecha es la que devuelve texto en vez de JSON."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "PostgreSQL es el que más lejos llegó: tiene un tipo `jsonb` de\n",
    "verdad, con índices propios pensados para esto. Si tu problema es \"casi todo son\n",
    "tablas y una cosa es un documento\", esa suele ser la respuesta correcta, y te\n",
    "ahorra montar una segunda base 🧾"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Se le puede poner freno, y hay que ponerlo a mano"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Que nadie te obligue no quiere decir que no puedas obligarte tú. SQLite trae\n",
    "`json_valid`, y con un CHECK del capítulo 11 ya\n",
    "tienes una puerta:"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Esto revienta a propósito.** Se ejecuta dentro de un `try` para que puedas seguir con \"ejecutar todo\" y aun así ver la queja."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "try:\n",
    "    q(\"\"\"\n",
    "    CREATE TABLE estricta (\n",
    "      id INTEGER PRIMARY KEY,\n",
    "      cuerpo TEXT CHECK (json_valid(cuerpo))\n",
    "    );\n",
    "\n",
    "    INSERT INTO estricta (id, cuerpo) VALUES (1, '{\"pedido\": 1,');\n",
    "    \"\"\")\n",
    "except Exception as e:\n",
    "    print(f'{type(e).__name__}: {e}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Y la queja que tiene que salir es esta:\n",
    "\n",
    "```\n",
    "IntegrityError: CHECK constraint failed: json_valid(cuerpo)\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ese JSON estaba cortado a la mitad y la tabla lo rechazó. Mongo tiene lo\n",
    "equivalente, se llama validador de esquema, y allá también es opcional.\n",
    "\n",
    "Lo que quiero que te lleves: **la disciplina que la tabla te daba\n",
    "gratis, en documentos la pones tú o no la pone nadie**. Y si vas a ponerla\n",
    "tú entera, la pregunta honesta es por qué no usabas una tabla 🙂"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Cuándo cada una"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "| Te conviene una tabla si | Te conviene un documento si |\n",
    "|---|---|\n",
    "| los datos tienen forma fija | cada cosa trae campos distintos |\n",
    "| preguntas cruzando entidades | lees una cosa entera, mucho |\n",
    "| hay dinero de por medio | hay logs, eventos, catálogos raros |\n",
    "| varios sistemas escriben | escribe una sola aplicación |"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Y la respuesta que más se usa en empresas de verdad es **las dos**:\n",
    "la contabilidad en tablas, y el catálogo de productos, donde una laptop tiene\n",
    "pulgadas y un polo tiene tallas, en documentos.\n",
    "\n",
    "Ojo con una cosa que se dice mucho y es falsa: NoSQL no quiere decir \"sin\n",
    "SQL\". Quiere decir \"no solo SQL\", y de hecho casi todos estos motores acabaron\n",
    "metiendo un lenguaje de consulta que se parece bastante a lo que ya sabes 🙂"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Los otros tipos, en dos líneas"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "- **Documentos**: Mongo, y lo de este capítulo.\n",
    "\n",
    "- **Clave y valor**: Redis. Le pides algo por su nombre y te lo\n",
    "da rapidísimo. Se usa para cachés y sesiones, no para reportes.\n",
    "\n",
    "- **Columnar ancha**: Cassandra. Para escribir muchísimo y muy\n",
    "repartido.\n",
    "\n",
    "- **Grafos**: Neo4j. Cuando lo que importa son las relaciones,\n",
    "como quién conoce a quién.\n",
    "\n",
    "Los cuatro se llaman NoSQL y no se parecen en nada entre ellos. Es una\n",
    "etiqueta de lo que **no** son, que es la peor manera de nombrar\n",
    "algo."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Lo que te llevas"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "- Un documento guarda una cosa entera, con listas dentro.\n",
    "\n",
    "- Se consulta con rutas: `$.cliente.ciudad`.\n",
    "\n",
    "- Para preguntas que cruzan documentos hay que desanidar, y eso es volver a\n",
    "las filas.\n",
    "\n",
    "- Nadie te obliga a que dos documentos se parezcan, y eso lo pagas después.\n",
    "\n",
    "- El esquema no desaparece: se muda de la base al código.\n",
    "\n",
    "- NoSQL no es un tipo de base, son cuatro familias distintas.\n",
    "\n",
    "- La respuesta habitual en empresas es usar las dos."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Comprueba que se entendió"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Comprueba que lo tienes\n",
    "\n",
    "Metes en tu colección un documento que no trae el campo cliente. ¿Qué pasa?\n",
    "\n",
    "a) Nada: entra sin quejarse, y el problema aparece cuando alguien lo consulte\n",
    "\n",
    "b) Da error, porque los otros documentos sí lo traen\n",
    "\n",
    "c) Se rellena con nulo, como en una tabla\n",
    "\n",
    "d) Depende del motor: Mongo avisa y SQLite no"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Ejercicios"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 1. El documento raro sigue ahí, encuéntralo\n",
    "\n",
    "Busca los que no cumplen la forma que tú esperabas."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%revisa 1\n",
    "-- tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 2. Qué campos existen de verdad en tu colección\n",
    "\n",
    "El inventario que debería venir de fábrica."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%revisa 2\n",
    "-- tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 3. Un índice sobre algo que está dentro del JSON\n",
    "\n",
    "Se puede indexar una expresión, no solo una columna."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%revisa 3\n",
    "-- tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 4. El precio de cambiar un dato repetido\n",
    "\n",
    "Un cliente cambia de nombre. Cuenta cuánto hay que tocar de\n",
    "cada lado."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 5. Cuántas formas distintas hay en tu colección\n",
    "\n",
    "El esquema que nadie guarda, reconstruido con una consulta."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%revisa 5\n",
    "-- tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 6. Tampoco te garantiza el tipo\n",
    "\n",
    "Mete una cantidad que no es un número y mira si alguien\n",
    "protesta."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%revisa 6\n",
    "-- tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "Ese era el capítulo 20 de **SQL desde cero**. El texto completo, con las salidas de cada bloque, está en https://missyera.com/guias/sql-desde-cero/nosql-y-documentos/\n",
    "\n",
    "Que tengas lindo día! 🌸"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python",
   "version": "3.11"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
