Ya sea que trabajes en desarrollo, analisis de datos, control de calidad, DevOps o producto, SQLite es una herramienta perfecta para vos. Aquí contamos por que.

Algunos hechos bien conocidos para comenzar:

  • SQLite es el DBMS más común del mundo, incluido en todos los sistemas operativos populares.
  • SQLite no tiene servidor.
  • Para quienes desarrollan, SQLite está integrado directamente en la aplicación.
  • Para todos los demás, existe una conveniente consola de base de datos (REPL), que se proporciona como un solo archivo (sqlite3.exe en Windows, sqlite3 en Linux / macOS).

Consola, importación y exportación

La consola es una característica excelente de SQLite para el análisis de datos: más poderosa que Excel y más simple que pandas. Se pueden importar datos CSV con un solo comando y la tabla se crea automáticamente:

> .import --csv city.csv city
> select count(*) from city;
1117

La consola admite funciones básicas de SQL y muestra los resultados de las consultas en una bonita tabla dibujada en ASCII. Las funciones avanzadas de SQL también son compatibles, pero más sobre eso más adelante.

select
  century || ' century' as dates,
  count(*) as city_count
from history
group by century
order by century desc;

┌────────────┬────────────┐
│   dates    │ city_count │
├────────────┼────────────┤
│ 21 century │ 1          │
│ 20 century │ 263        │
│ 19 century │ 189        │
│ 18 century │ 191        │
│ 17 century │ 137        │
│ ...        │ ...        │
└────────────┴────────────┘

Los datos se pueden exportar como SQL, CSV, JSON, incluso Markdown y HTML. Toma solo un par de comandos:

.mode json
.output city.json
select city, foundation_year, timezone from city limit 10;
.shell cat city.json
[
    { "city": "Amsterdam", "foundation_year": 1300, "timezone": "UTC+1" },
    { "city": "Berlin", "foundation_year": 1237, "timezone": "UTC+1" },
    { "city": "Helsinki", "foundation_year": 1548, "timezone": "UTC+2" },
    { "city": "Monaco", "foundation_year": 1215, "timezone": "UTC+1" },
    { "city": "Moscow", "foundation_year": 1147, "timezone": "UTC+3" },
    { "city": "Reykjavik", "foundation_year": 874, "timezone": "UTC" },
    { "city": "Sarajevo", "foundation_year": 1461, "timezone": "UTC+1" },
    { "city": "Stockholm", "foundation_year": 1252, "timezone": "UTC+1" },
    { "city": "Tallinn", "foundation_year": 1219, "timezone": "UTC+2" },
    { "city": "Zagreb", "foundation_year": 1094, "timezone": "UTC+1" }
]

Si sos más un BI que una persona de consola, las herramientas populares de exploración de datos como Metabase, Redash y Superset son compatibles con SQLite.

JSON nativo

No hay nada más conveniente que SQLite para analizar y transformar JSON. Puede seleccionar datos directamente de un archivo como si fuera una tabla normal. O importe datos a la tabla y seleccione desde allí.

select
  json_extract(value, '$.iso.code') as code,
  json_extract(value, '$.iso.number') as num,
  json_extract(value, '$.name') as name,
  json_extract(value, '$.units.major.name') as unit
from
  json_each(readfile('currency.sample.json'))
;
┌──────┬─────┬─────────────────┬──────────┐
│ code │ num │      name       │   unit   │
├──────┼─────┼─────────────────┼──────────┤
│ ARS  │ 032 │ Argentine peso  | peso     │
│ CHF  │ 756 │ Swiss Franc     │ franc    │
│ EUR  │ 978 │ Euro            │ euro     │
│ GBP  │ 826 │ British Pound   │ pound    │
│ INR  │ 356 │ Indian Rupee    │ rupee    │
│ JPY  │ 392 │ Japanese yen    │ yen      │
│ MAD  │ 504 │ Moroccan Dirham │ dirham   │
│ RUR  │ 643 │ Russian Rouble  │ rouble   │
│ SOS  │ 706 │ Somali Shilling │ shilling │
│ USD  │ 840 │ US Dollar       │ dollar   │
└──────┴─────┴─────────────────┴──────────┘

No importa qué tan profundo sea el JSON: puede extraer cualquier objeto anidado :

select
  json_extract(value, '$.id') as id,
  json_extract(value, '$.name') as name
from
  json_tree(readfile('industry.sample.json'))
where
  path like '$[%].industries'
;
┌────────┬──────────────────────┐
│   id   │         name         │
├────────┼──────────────────────┤
│ 7.538  │ Internet provider    │
│ 7.539  │ IT consulting        │
│ 7.540  │ Software development │
│ 9.399  │ Mobile communication │
│ 9.400  │ Fixed communication  │
│ 9.401  │ Fiber-optics         │
│ 43.641 │ Audit                │
│ 43.646 │ Insurance            │
│ 43.647 │ Bank                 │
└────────┴──────────────────────┘

CTE y operaciones de conjuntos

Por supuesto, SQLite admite Expresiones de tabla comunes (cláusula WITH) y JOIN, ni siquiera daré ejemplos aquí. Si los datos son jerárquicos (la tabla se refiere a sí misma a través de una columna como parent_id), WITH RECURSIVE será útil. Cualquier jerarquía, por profunda que sea, se puede “desenrollar” con una sola consulta.

with recursive tmp(id, name, level) as (
  select id, name, 1 as level
  from area
  where parent_id is null

  union all

  select
    area.id,
    tmp.name || ', ' || area.name as name,
    tmp.level + 1 as level
  from area
    join tmp on area.parent_id = tmp.id
)

select * from tmp;
┌──────┬──────────────────────────┬───────┐
│  id  │           name           │ level │
├──────┼──────────────────────────┼───────┤
│ 93   │ US                       │ 1     │
│ 768  │ US, Washington DC        │ 2     │
│ 1833 │ US, Washington           │ 2     │
│ 2987 │ US, Washington, Bellevue │ 3     │
│ 3021 │ US, Washington, Everett  │ 3     │
│ 3039 │ US, Washington, Kent     │ 3     │
│ ...  │ ...                      │ ...   │
└──────┴──────────────────────────┴───────┘

¿Conjuntos? No hay problema: UNION, INTERSECT, EXCEPT están a su servicio.

select employer_id
from employer_area
where area_id = 1

except

select employer_id
from employer_area
where area_id = 2;

¿Calcular una columna basándose en varias otras? Ingrese las columnas generadas:

alter table vacancy
add column salary_net integer as (
  case when salary_gross = true then
    round(salary_from/1.04)
  else
    salary_from
  end
);

Las columnas generadas se pueden consultar de la misma manera que las ‘normales’:

select
  substr(name, 1, 40) as name,
  salary_net
from vacancy
where
  salary_currency = 'JPY'
  and salary_net is not null
limit 10;

Estadística matemática

¿Estadística descriptiva? Fácil: media, mediana, percentiles, desviación estándar, lo que sea. Tendrá que cargar una extensión, pero también es un solo comando (y un solo archivo).

.load sqlite3-stats

select
  count(*) as book_count,
  cast(avg(num_pages) as integer) as mean,
  cast(median(num_pages) as integer) as median,
  mode(num_pages) as mode,
  percentile_90(num_pages) as p90,
  percentile_95(num_pages) as p95,
  percentile_99(num_pages) as p99
from books;
┌────────────┬──────┬────────┬──────┬─────┬─────┬──────┐
│ book_count │ mean │ median │ mode │ p90 │ p95 │ p99  │
├────────────┼──────┼────────┼──────┼─────┼─────┼──────┤
│ 1483       │ 349  │ 295    │ 256  │ 640 │ 817 │ 1199 │
└────────────┴──────┴────────┴──────┴─────┴─────┴──────┘

Nota sobre extensiones. A SQLite le faltan muchas funciones en comparación con otros DBMS como PostgreSQL. Pero son fáciles de agregar, que es lo que hace la gente, por lo que resulta bastante complicado. Por lo tanto, decidí hacer un conjunto consistente de extensiones, dividido por área de dominio y compilado para los principales sistemas operativos. Hay pocos de ellos todavía, pero hay más en camino: sqlite-plus @ GitHub

Más diversión con las estadísticas. Puede trazar la distribución de datos directamente en la consola. Mira qué lindo que es:

with slots as (
  select
    num_pages/100 as slot,
    count(*) as book_count
  from books
  group by slot
),
max as (
  select max(book_count) as value
  from slots
)
select
  slot,
  book_count,
  printf('%.' || (book_count * 30 / max.value) || 'c', '*') as bar
from slots, max
order by slot;
┌──────┬────────────┬────────────────────────────────┐
│ slot │ book_count │              bar               │
├──────┼────────────┼────────────────────────────────┤
│ 0    │ 116        │ *********                      │
│ 1    │ 254        │ ********************           │
│ 2    │ 376        │ ****************************** │
│ 3    │ 285        │ **********************         │
│ 4    │ 184        │ **************                 │
│ 5    │ 90         │ *******                        │
│ 6    │ 54         │ ****                           │
│ 7    │ 41         │ ***                            │
│ 8    │ 31         │ **                             │
│ 9    │ 15         │ *                              │
│ 10   │ 11         │ *                              │
│ 11   │ 12         │ *                              │
│ 12   │ 2          │ *                              │
└──────┴────────────┴────────────────────────────────┘

Performance

SQLite funciona con cientos de millones de registros. Los INSERTS regulares muestran alrededor de 240K registros por segundo en mi computadora portátil. Y si conecta el archivo CSV como una tabla virtual (hay una extensión para eso), las inserciones se vuelven 2 veces más rápidas.

.load sqlite3-vsv

create virtual table temp.blocks_csv using vsv(
    filename="ipblocks.csv",
    schema="create table x(network text, geoname_id integer, registered_country_geoname_id integer, represented_country_geoname_id integer, is_anonymous_proxy integer, is_satellite_provider integer, postal_code text, latitude real, longitude real, accuracy_radius integer)",
    columns=10,
    header=on,
    nulls=on
);
.timer on
insert into blocks
select * from blocks_csv;

Run Time: real 5.176 user 4.716420 sys 0.403866
select count(*) from blocks;
3386629

Run Time: real 0.095 user 0.021972 sys 0.063716

Existe una opinión popular entre los desarrolladores de que SQLite no es adecuado para la web, porque no admite el acceso concurrente. Esto es un mito. En el modo de registro de escritura anticipada (disponible desde hace mucho tiempo), puede haber tantos lectores simultáneos como desee. Solo puede haber un escritor concurrente, pero a menudo uno es suficiente.

SQLite es perfecto para aplicaciones y sitios web pequeños. sqlite.org utiliza SQLite como base de datos, sin preocuparse por la optimización (200 solicitudes por página). Maneja 700K visitas por mes y sirve páginas más rápido que el 95% de los sitios web que he visto.

Documentos, gráficos y búsqueda

SQLite admite índices parciales e índices en expresiones, como lo hacen los DBMS ‘grandes’. Puede crear índices en columnas generadas e incluso convertir SQLite en una base de datos de documentos. Simplemente almacene JSON sin procesar y cree índices con json_extract()-ed columns:

create table currency(
  body text,
  code text as (json_extract(body, '$.code')),
  name text as (json_extract(body, '$.name'))
);

create index currency_code_idx on currency(code);

insert into currency
select value
from json_each(readfile('currency.sample.json'));
explain query plan
select name from currency where code = 'EUR';

QUERY PLAN
`--SEARCH TABLE currency USING INDEX currency_code_idx (code=?)

También puede usar SQLite como una base de datos gráfica. Un montón de complejos WITH RECURSIVE harán el truco, o tal vez prefiera agregar un poco de Python: simple-graph @ GitHub

La búsqueda de texto completo funciona desde el primer momento:

create virtual table books_fts
using fts5(title, author, publisher);

insert into books_fts
select title, author, publisher from books;

select
  author,
  substr(title, 1, 30) as title,
  substr(publisher, 1, 10) as publisher
from books_fts
where books_fts match 'ann'
limit 5;
┌─────────────────────┬────────────────────────────────┬────────────┐
│       author        │             title              │ publisher  │
├─────────────────────┼────────────────────────────────┼────────────┤
│ Ruby Ann Boxcar     │ Ruby Ann's Down Home Trailer P │ Citadel    │
│ Ruby Ann Boxcar     │ Ruby Ann's Down Home Trailer P │ Citadel    │
│ Lynne Ann DeSpelder │ The Last Dance: Encountering D │ McGraw-Hil │
│ Daniel Defoe        │ Robinson Crusoe                │ Ann Arbor  │
│ Ann Thwaite         │ Waiting for the Party: The Lif │ David R. G │
└─────────────────────┴────────────────────────────────┴────────────┘

Quizás necesite una base de datos en memoria para cálculos intermedios? Una sola línea de código Python:

db = sqlite3.connect(":memory:")

Incluso puede acceder a él desde múltiples conexiones:

db = sqlite3.connect ("file :: memory:? Cache = shared")

Y mucho más

Allí son funciones de ventana elegantes (como en PostgreSQL). UPSERT, UPDATE FROM y generate_series(). Índices R-Tree. Expresiones regulares, búsqueda difusa y geo. En términos de características, SQLite puede competir con cualquier DBMS ‘grande’.

También hay excelentes herramientas en torno a SQLite. Me gusta especialmente Datasette, una herramienta de código abierto para explorar y publicar conjuntos de datos SQLite. Y DBeaver es un excelente IDE de base de datos de código abierto con soporte para las últimas versiones de SQLite.

Autor original @ohmypy, artículo original, comentarios (que vale la pena leer) en Hacker News

revisó y publicó @jedux.