{
 "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 soluciones 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",
    "Este es el cuaderno de **soluciones**. Trae el código de cada ejercicio, la\n",
    "explicación de la trampa y la respuesta del quiz. Si vienes del cuaderno de\n",
    "práctica sin haberlo intentado, vuelve 🙂"
   ]
  },
  {
   "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": [
    "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\n",
    "\n",
    "---\n",
    "\n",
    "**La correcta es la a.**\n",
    "\n",
    "*b)* No hay nada que compare un documento nuevo con los que ya estaban. Esa comprobación es justo lo que hace una tabla y no hace una colección.\n",
    "\n",
    "*c)* En una tabla existiría la columna con NULL dentro. En un documento el campo directamente no está, que no es lo mismo al consultar.\n",
    "\n",
    "*d)* Mongo tampoco avisa por defecto. Se le puede poner un validador de esquema, y hay que ponerlo a mano.\n",
    "\n",
    "La flexibilidad no es gratis: lo que la tabla te obligaba a decidir, la colección te lo deja para después 🧾"
   ]
  },
  {
   "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": [
    "q(\"\"\"\n",
    "SELECT id, cuerpo FROM documentos\n",
    "WHERE json_extract(cuerpo,'$.cliente.ciudad') IS NULL;\n",
    "\"\"\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "id     cuerpo\n",
    "-----  --------------------------------------------------------------------------\n",
    "99999  {\"pedido\":99999,\"canal\":\"WhatsApp\",\"total_soles\":250,\"nota\":\"sin cliente\"}\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Esta consulta es la que en una base de documentos hay que correr a mano cada\n",
    "tanto, porque nadie la corre por ti. En una tabla la haría el motor cada vez que\n",
    "alguien intenta insertar."
   ]
  },
  {
   "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": [
    "q(\"\"\"\n",
    "SELECT k.key AS campo, count(*) AS documentos\n",
    "FROM documentos d, json_each(d.cuerpo) k\n",
    "GROUP BY 1 ORDER BY 2 DESC;\n",
    "\"\"\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "campo        documentos\n",
    "-----------  ----------\n",
    "pedido       877\n",
    "canal        877\n",
    "lineas       876\n",
    "fecha        876\n",
    "cliente      876\n",
    "total_soles  1\n",
    "nota         1\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Léelo como un diagnóstico: `pedido` y `canal` están en\n",
    "los 877, y hay dos campos que aparecen una sola vez. Esa tabla es el esquema que\n",
    "la base no te guarda, reconstruido a posteriori. Guárdate esta consulta."
   ]
  },
  {
   "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": [
    "q(\"\"\"\n",
    "CREATE INDEX idx_ciudad_doc\n",
    "ON documentos (json_extract(cuerpo,'$.cliente.ciudad'));\n",
    "\n",
    "EXPLAIN QUERY PLAN\n",
    "SELECT count(*) FROM documentos\n",
    "WHERE json_extract(cuerpo,'$.cliente.ciudad') = 'Piura';\n",
    "\"\"\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "id  parent  notused  detail\n",
    "--  ------  -------  -------------------------------------------------------\n",
    "4   0       0        SEARCH documentos USING INDEX idx_ciudad_doc (<expr>=?)\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "SEARCH y no SCAN, o sea que usó el índice, igual que en el capítulo\n",
    "14. La condición es que la expresión del índice sea\n",
    "**idéntica** a la de la consulta: si en el WHERE escribes la ruta de\n",
    "otra manera, el índice no se usa y no te avisa nadie."
   ]
  },
  {
   "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": [
    "q(\"\"\"\n",
    "SELECT count(*) AS documentos_a_tocar\n",
    "FROM documentos\n",
    "WHERE json_extract(cuerpo,'$.cliente.nombre') =\n",
    "      (SELECT json_extract(cuerpo,'$.cliente.nombre')\n",
    "       FROM documentos WHERE id = 1);\n",
    "\"\"\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "documentos_a_tocar\n",
    "------------------\n",
    "9\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Y ahora del lado de las tablas:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "q(\"\"\"\n",
    "SELECT count(*) AS filas_a_tocar_en_tablas\n",
    "FROM clientes WHERE id = (SELECT id_cliente FROM pedidos WHERE id = 1);\n",
    "\"\"\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "filas_a_tocar_en_tablas\n",
    "-----------------------\n",
    "1\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Nueve contra una. Eso es justo lo que la normalización del capítulo\n",
    "10 venía a evitar, y el documento lo deshace a propósito\n",
    "para ganar velocidad de lectura.\n",
    "\n",
    "Si olvidas uno de los nueve, tienes el mismo cliente con dos nombres y ningún\n",
    "error. Por eso los documentos van bien cuando lo de dentro no cambia, como el\n",
    "pedido de una fecha concreta, y mal cuando cambia, como los datos de un\n",
    "cliente."
   ]
  },
  {
   "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": [
    "q(\"\"\"\n",
    "SELECT campos, count(*) AS documentos FROM (\n",
    "  SELECT d.id, group_concat(k.key, ',') AS campos\n",
    "  FROM documentos d, json_each(d.cuerpo) k\n",
    "  GROUP BY d.id)\n",
    "GROUP BY campos ORDER BY 2 DESC;\n",
    "\"\"\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "campos                             documentos\n",
    "---------------------------------  ----------\n",
    "pedido,fecha,canal,cliente,lineas  876\n",
    "pedido,canal,total_soles,nota      1\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Dos formas. Con dos se vive; el día que veas veinte, tu colección dejó de\n",
    "tener forma y cada consulta que escribas va a ser una apuesta.\n",
    "\n",
    "Este es el primer diagnóstico que corro cuando me pasan una base de documentos\n",
    "que no conozco, antes de escribir nada más."
   ]
  },
  {
   "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": [
    "q(\"\"\"\n",
    "INSERT INTO documentos (id, cuerpo)\n",
    "VALUES (99998, json_object('pedido', 99998, 'canal', 'Web',\n",
    "       'cliente', json_object('nombre','X','ciudad','Lima'),\n",
    "       'lineas', json_array(json_object('producto','Producto 01',\n",
    "                                        'cantidad','muchas'))));\n",
    "\n",
    "SELECT json_type(l.value,'$.cantidad') AS tipo, count(*) AS lineas\n",
    "FROM documentos d, json_each(d.cuerpo,'$.lineas') l\n",
    "GROUP BY 1;\n",
    "\"\"\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "tipo     lineas\n",
    "-------  ------\n",
    "integer  2605\n",
    "text     1\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Una línea con la cantidad escrita como texto, y entró igual. Si mañana sumas\n",
    "esa columna, SQLite trata `'muchas'` como cero y tu total sale bajo\n",
    "sin decir nada.\n",
    "\n",
    "Es el mismo problema del capítulo 4, solo que allá la\n",
    "columna tenía un tipo declarado y acá cada documento decide el suyo.\n",
    "`json_type` es la herramienta para auditarlo, y hay que acordarse de\n",
    "usarla porque no salta sola."
   ]
  },
  {
   "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
}
