SQL desde cero: SELECT, WHERE, ORDER BY y NULL
Curso de SQL 1 en SQLite 3.51: tu primera tabla en 5 minutos, WHERE, ORDER BY, LIMIT, por qué = NULL no encuentra nada y 1 millón de filas medidas.
Serie: curso de SQL 1/6 (principiante). Si estás en un bootcamp y te acaban de dar acceso a la base de datos del proyecto final, si en tu primer trabajo en una startup copias consultas del canal del equipo sin saber bien qué hacen, o si la materia de bases de datos de la universidad te dejó más dudas que certezas, esta clase es para ti. Al terminar vas a crear una tabla, sacar exactamente las filas y columnas que necesitas, ordenarlas y esquivar las dos trampas que causan la mayoría de las respuestas equivocadas: NULL y los tipos.
Casi cualquier app, servicio web o reporte que toques en el trabajo guarda sus datos en una base de datos relacional, y SQL es el idioma para hacerle preguntas. Lo peligroso no son los mensajes de error, sino las consultas que corren sin ningún error y en silencio devuelven filas equivocadas. Aquí vas a ver los dos casos. Todo lo que sigue se ejecutó con la shell de línea de comandos sqlite3 3.51.0 en una Mac con Apple Silicon (M1 Pro), y cada bloque de salida está copiado de una ejecución real.
¿Por qué SQLite? Es una base de datos SQL completa en un solo archivo, no necesita servidor y ya viene instalada en toda Mac (y dentro de cada iPhone y cada celular Android). Lo que aprendas aquí sirve igual en MySQL y PostgreSQL; donde se comportan distinto, lo digo y marco esas partes como "no ejecutado aquí".
Tu primera consulta en 5 minutos
Abre una terminal e inicia la shell de SQLite con un nombre de archivo. Si el archivo no existe, SQLite lo crea:
$ sqlite3 shop.db
SQLite version 3.51.0 2025-06-12 13:14:41
Enter ".help" for usage hints.
sqlite>
En Windows, descarga el paquete "command-line tools" desde sqlite.org; en Linux, instala el paquete sqlite3 (ninguno de los dos se ejecutó aquí). Las líneas que empiezan con punto, como .headers on, son comandos de la propia shell, no SQL. Activa los encabezados de columna y el formato de tabla:
sqlite> .headers on
sqlite> .mode table
Ahora pega esto. CREATE TABLE define una tabla (una cuadrícula de filas y columnas) y el tipo de cada columna; INSERT agrega filas:
CREATE TABLE products (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
category TEXT,
price INTEGER,
stock INTEGER
);
INSERT INTO products (name, category, price, stock) VALUES
('Notebook A5', 'stationery', 350, 120),
('Gel pen', 'stationery', 120, 300),
('USB-C cable', 'electronics', 980, 45),
('Mouse', 'electronics', 2480, 0),
('Desk lamp', 'furniture', 4200, 12),
('Sticky notes', 'stationery', 200, NULL),
('Mystery box', NULL, 1500, 5),
('Keyboard', 'electronics', 5980, 8);
INTEGER PRIMARY KEY significa "un número único para cada fila"; no lo pasamos, así que SQLite lo llenó con 1, 2, 3… NULL significa "sin valor / desconocido" (no sabemos el stock de las notas adhesivas, y la caja sorpresa no tiene categoría). Los datos de ejemplo quedan en inglés porque así aparecen en todas las salidas: stationery es papelería, electronics electrónica y furniture muebles. Tu primera consulta, SELECT * ("todas las columnas"):
SELECT * FROM products;
+----+--------------+-------------+-------+-------+
| id | name | category | price | stock |
+----+--------------+-------------+-------+-------+
| 1 | Notebook A5 | stationery | 350 | 120 |
| 2 | Gel pen | stationery | 120 | 300 |
| 3 | USB-C cable | electronics | 980 | 45 |
| 4 | Mouse | electronics | 2480 | 0 |
| 5 | Desk lamp | furniture | 4200 | 12 |
| 6 | Sticky notes | stationery | 200 | |
| 7 | Mystery box | | 1500 | 5 |
| 8 | Keyboard | electronics | 5980 | 8 |
+----+--------------+-------------+-------+-------+
Fíjate en que NULL se imprime como una celda vacía, idéntica a un texto vacío. Escribe .nullvalue NULL para hacerlo visible; desde aquí lo usamos siempre. También puedes guardar el SQL en un archivo y ejecutarlo con sqlite3 shop.db < setup.sql.
SELECT y WHERE: elegir columnas y filas
SELECT elige columnas y WHERE elige filas. Solo sobreviven las filas para las que la condición es verdadera:
SELECT name, price FROM products WHERE price < 1000;
+--------------+-------+
| name | price |
+--------------+-------+
| Notebook A5 | 350 |
| Gel pen | 120 |
| USB-C cable | 980 |
| Sticky notes | 200 |
+--------------+-------+
Los textos van entre comillas simples: WHERE category = 'electronics'. Los operadores útiles son =, <> (distinto; != también funciona), <, >=, BETWEEN 200 AND 980 (incluye ambos extremos), IN ('a', 'b') y LIKE 'mo%' (% es "cualquier cantidad de caracteres", _ es "un carácter"). Dos detalles que comprobamos: en SQLite, LIKE ignora mayúsculas y minúsculas en letras ASCII, así que LIKE 'mo%' encontró Mouse, mientras que = no las ignora, y name = 'mouse' no devolvió nada.
AND tiene más prioridad que OR
Esta consulta devuelve filas sin quejarse, y son las filas equivocadas. Queríamos "papelería o electrónica, de más de 300":
SELECT name, category, price FROM products
WHERE category = 'stationery' OR category = 'electronics' AND price > 300;
+--------------+-------------+-------+
| name | category | price |
+--------------+-------------+-------+
| Notebook A5 | stationery | 350 |
| Gel pen | stationery | 120 |
| USB-C cable | electronics | 980 |
| Mouse | electronics | 2480 |
| Sticky notes | stationery | 200 |
| Keyboard | electronics | 5980 |
+--------------+-------------+-------+
El Gel pen (120) y las Sticky notes (200) se colaron, porque SQL lo leyó como stationery OR (electronics AND price > 300). Con paréntesis, WHERE (category = 'stationery' OR category = 'electronics') AND price > 300, obtuvimos las 4 filas correctas: Notebook A5, USB-C cable, Mouse y Keyboard. Regla práctica: cada vez que mezcles AND y OR, escribe los paréntesis.
ORDER BY y LIMIT: ordenar y paginar
Sin ORDER BY, la base de datos puede devolver las filas en cualquier orden. Muchas veces parece el orden de inserción, pero no está garantizado; en la clase 3 mostramos un JOIN que regresó en otro orden. Así que ordena siempre de forma explícita, y agrega DESC para "de mayor a menor":
SELECT name, price FROM products ORDER BY price DESC LIMIT 3;
SELECT name, price FROM products ORDER BY price DESC LIMIT 3 OFFSET 3;
+-----------+-------+
| name | price |
+-----------+-------+
| Keyboard | 5980 |
| Desk lamp | 4200 |
| Mouse | 2480 |
+-----------+-------+
+-------------+-------+
| name | price |
+-------------+-------+
| Mystery box | 1500 |
| USB-C cable | 980 |
| Notebook A5 | 350 |
+-------------+-------+
LIMIT 3 OFFSET 3 es la "página 2" con 3 resultados por página, justo lo que necesitas para la paginación de una API o de un listado web. Puedes ordenar por varias columnas: ORDER BY category, price DESC. Las cláusulas se escriben en un orden fijo (SELECT … FROM … WHERE … ORDER BY … LIMIT), pero la base de datos las evalúa en otro, el que muestra el diagrama. Por eso no puedes usar en WHERE un alias definido en SELECT, pero sí en ORDER BY.
NULL: el valor que no es igual a sí mismo
NULL significa "desconocido". Cualquier comparación con algo desconocido da desconocido (NULL), y WHERE descarta todo lo que no sea verdadero. Mira:
SELECT name, stock FROM products WHERE stock = NULL; -- 0 rows!
SELECT name, stock FROM products WHERE stock IS NULL;
SELECT NULL = NULL, NULL IS NULL, NULL + 1, 'a' || NULL, coalesce(NULL, 0);
+--------------+-------+
| name | stock |
+--------------+-------+
| Sticky notes | NULL |
+--------------+-------+
+-------------+--------------+----------+-------------+-------------------+
| NULL = NULL | NULL IS NULL | NULL + 1 | 'a' || NULL | coalesce(NULL, 0) |
+-------------+--------------+----------+-------------+-------------------+
| NULL | 1 | NULL | NULL | 0 |
+-------------+--------------+----------+-------------+-------------------+
La primera consulta no imprimió absolutamente nada, ni siquiera un error. La aritmética y la concatenación de textos (||) con NULL también dan NULL; coalesce(x, 0) significa "x, o 0 si x es NULL".
Otros tres lugares donde NULL muerde, todos con la misma tabla:
WHERE stock <> 0devolvió 6 filas: el Mouse (0) quedó fuera, como queríamos, pero también las Sticky notes (NULL), porqueNULL <> 0no es verdadero.count(*)cuenta filas ycount(stock)cuenta valores que no son NULL: obtuvimos8y7.WHERE category NOT IN ('furniture', NULL)devolvió 0 filas.NOT INse expande acategory <> 'furniture' AND category <> NULL, y la segunda mitad nunca es verdadera. Sin elNULLen la lista obtuvimos 6 filas, y la Mystery box (categoría NULL) seguía faltando, por la misma razón.
Al ordenar, SQLite pone los NULL primero en orden ascendente. Desde SQLite 3.30 puedes indicar dónde van:
SELECT name, stock FROM products ORDER BY stock NULLS LAST;
Con un simple ORDER BY stock, la primera fila fue Sticky notes | NULL; con NULLS LAST pasó al final, después de Gel pen | 300. PostgreSQL hace lo contrario por defecto (NULL al final en orden ascendente; no ejecutado aquí), y esa es una buena razón para escribir siempre NULLS FIRST/LAST cuando importa.
Tipos y afinidad: por qué 'ten' > 5 es verdadero
La mayoría de las bases de datos rechazan una palabra en una columna numérica. Las tablas clásicas de SQLite no: el tipo de columna es solo una preferencia, llamada afinidad de tipo (type affinity). SQLite convierte el valor si puede y, si no, lo guarda tal cual. Insertamos datos desordenados a propósito:
CREATE TABLE items (code TEXT, qty INTEGER, price REAL);
INSERT INTO items VALUES ('9', '10', '1.50'), ('10', 'ten', 2), ('2', 3.0, '1e2'), (007, '007', NULL);
SELECT code, typeof(code), qty, typeof(qty), price, typeof(price) FROM items;
+------+--------------+-----+-------------+-------+---------------+
| code | typeof(code) | qty | typeof(qty) | price | typeof(price) |
+------+--------------+-----+-------------+-------+---------------+
| 9 | text | 10 | integer | 1.5 | real |
| 10 | text | ten | text | 2.0 | real |
| 2 | text | 3 | integer | 100.0 | real |
| 7 | text | 7 | integer | | null |
+------+--------------+-----+-------------+-------+---------------+
'10' se volvió el entero 10, 3.0 se volvió 3, '1e2' se volvió 100.0 y el número 007 se volvió el texto '7'. Pero 'ten' siguió siendo texto dentro de una columna INTEGER. Las consecuencias:
ORDER BY codedio10, 2, 7, 9: el texto se compara carácter por carácter.ORDER BY CAST(code AS INTEGER)dio2, 7, 9, 10.WHERE qty > 5devolvió10,teny7. En SQLite cualquier texto es mayor que cualquier número, así que'ten' > 5es verdadero.7 / 2es3(división entera);7 / 2.0es3.5.'3' + '4'es7,'3' || '4'es'34'y'abc' + 1es1, sin ningún error.0.1 + 0.2 = 0.3devolvió0(falso), aunque0.1 + 0.2se imprimió como0.3. Guarda el dinero como enteros (centavos, por ejemplo), nunca como REAL.
La solución es una tabla STRICT (SQLite 3.37+), que sí hace cumplir los tipos:
CREATE TABLE items_strict (code TEXT, qty INTEGER, price REAL) STRICT;
INSERT INTO items_strict VALUES ('9', '10', '1.50'); -- converted: qty integer, price real
INSERT INTO items_strict VALUES ('10', 'ten', 2);
CREATE TABLE bad (d DATETIME) STRICT;
Runtime error near line 14: cannot store TEXT value in INTEGER column items_strict.qty (19)
Parse error near line 15: unknown datatype for bad.d: "DATETIME"
(Los números de línea se refieren a nuestro archivo de script.) Las tablas STRICT solo aceptan INT, INTEGER, REAL, TEXT, BLOB y ANY; las fechas se guardan como texto ISO, por ejemplo '2026-11-12'. El (19) es el código de error de SQLite para una violación de restricción, el mismo que verás en la clase 2.
Errores comunes: qué pasa de verdad y cómo arreglarlo
Estos son los mensajes con los que se topa quien empieza. Todos salieron de nuestra tabla de la tienda; el indicador ^--- error here lo imprime la shell.
| Escribiste | Qué pasó (salida real) | Solución |
|---|---|---|
SELCT * FROM products; |
Parse error: near "SELCT": syntax error |
Error de tipeo en una palabra clave |
SELECT name, FROM products; |
Parse error: near "FROM": syntax error |
Sobra una coma antes de FROM |
SELECT * FROM product; |
Parse error: no such table: product |
Revisa el nombre con .tables |
… ORDER BY price LIMIT 2 WHERE price > 100; |
Parse error: near "WHERE": syntax error |
Orden de cláusulas: WHERE antes de ORDER BY |
WHERE category = 'stationery; |
sin error: el prompt cambia a ' ...> y se queda esperando |
La comilla quedó abierta; escribe '; para cerrarla y corrige la consulta |
SELECT name price FROM products |
sin error: una columna titulada price con los nombres |
Falta la coma: price se volvió un alias |
WHERE category = "category" |
sin error: cuenta 7 en lugar de 0 | Las comillas dobles son nombres de columna; usa '…' |
Las dos últimas son las peligrosas. En SQL, las comillas dobles son para identificadores (nombres de tablas y columnas). Por eso "category" significó la propia columna: la condición comparó cada fila consigo misma y encontró las 7 filas que no son NULL. Con comillas simples, el conteo fue 0. SQLite, cuando no existe una columna con ese nombre, termina tratando "Mouse" como texto. Nuestra CLI y el módulo sqlite3 de Python (ambos SQLite 3.51.0) lo hicieron, así que el error "funciona" hasta el día en que alguien agrega una columna con ese nombre. MySQL trata las comillas dobles como texto por defecto y PostgreSQL las rechaza (no ejecutado aquí). Usa comillas simples siempre.
La comilla sin cerrar merece un comentario: la shell no se congeló, solo cree que tu texto continúa en la línea siguiente. Si ejecutas la misma consulta desde un archivo o como argumento de la línea de comandos, obtienes unrecognized token: "'stationery;". Cuando ejecutas un archivo (sqlite3 shop.db < setup.sql), los errores dicen Parse error near line 1: … y el proceso termina con código de salida 1, así que tus scripts pueden detectar el fallo.
Una medición: cuánto cuesta 1,000,000 de filas
¿Qué tan lento es "lento"? Generamos 1,000,000 de pedidos con una consulta recursiva. Sin números aleatorios, así que los datos son idénticos en cada ejecución. Luego medimos algunas consultas desde el módulo sqlite3 de Python (enlazado con SQLite 3.51.0): una ejecución de calentamiento y después la mediana de 7, cada una incluyendo fetchall(), en la M1 Pro.
CREATE TABLE orders (id INTEGER PRIMARY KEY, customer_id INTEGER, price INTEGER, status TEXT);
WITH RECURSIVE n(i) AS (SELECT 1 UNION ALL SELECT i + 1 FROM n WHERE i < 1000000)
INSERT INTO orders (id, customer_id, price, status)
SELECT i, (i * 7919) % 50000, (i * 104729) % 10000,
CASE i % 10 WHEN 0 THEN 'cancelled' ELSE 'paid' END
FROM n;
Construirla tomó 0.47 s y produjo un archivo de 20 MB. Resultados:
| Consulta | Filas devueltas | Mediana |
|---|---|---|
WHERE id = 500000 (clave primaria) |
1 | 0.01 ms |
SELECT count(*) FROM orders |
1 | 5.9 ms |
WHERE price > 9000 con count(*) |
1 | 34.8 ms |
WHERE customer_id = 4242 |
20 | 38.5 ms |
ORDER BY price DESC LIMIT 10 |
10 | 43.0 ms |
ORDER BY price DESC (todas las filas) |
1,000,000 | 792.6 ms |
Lo que esto le dice a alguien que empieza:
- Buscar por clave primaria es unas 3,000 veces más rápido que buscar por otra columna.
EXPLAIN QUERY PLANmuestra la diferencia:SEARCH orders USING INTEGER PRIMARY KEY (rowid=?)frente aSCAN orders(leer todas las filas). La clase 4 resuelve el recorrido completo con un índice. LIMIT 10no hace gratis la búsqueda: la base de datos igual leyó el millón de filas para encontrar las 10 primeras. Solo se ahorra ordenar y transferir el resto.- Devolver un millón de filas a tu programa es la parte lenta (la mayor parte de los 793 ms se va en construir objetos de Python). Pídele a la base de datos la respuesta (
count,sum,LIMIT), no todos los datos.
Una segunda ejecución completa dio el mismo panorama (33.7 / 35.9 / 45.4 / 799.3 ms para los cuatro recorridos y ordenamientos).
Límites honestos y notas de versión
- Ejecutado solo en SQLite 3.51.0 (CLI y módulo
sqlite3de Python 3.9, ambos reportando 3.51.0). Según las notas de versión de SQLite,NULLS FIRST/LASTllegó en la 3.30.0 y las tablas STRICT en la 3.37.0; versiones antiguas de Android o de servidores pueden no tenerlas. - En SQLite,
LIKEsin distinguir mayúsculas solo aplica a letras ASCII;'É' LIKE 'é'necesita la extensión ICU. Esto importa justo en español, con tildes y eñes. En MySQL depende de la collation, y en PostgreSQLLIKEdistingue mayúsculas, conILIKEcomo forma que no las distingue (no ejecutado aquí). - La afinidad de tipos es propia de SQLite. MySQL en modo estricto y PostgreSQL rechazan
'ten'en una columna entera (no ejecutado aquí). - Los tiempos son de una sola laptop con el archivo en la caché del sistema operativo. En tu computadora los números absolutos serán otros; lo que importa son las proporciones.
Ejercicios
- Lista los productos de papelería (
stationery) con precio menor que 300, del más barato al más caro. - Encuentra los productos cuya categoría es desconocida.
- Predice antes de ejecutar:
SELECT count(*) FROM products WHERE stock > 10 OR stock <= 10;. ¿Da 8?
Respuestas
1.
SELECT name, price FROM products
WHERE category = 'stationery' AND price < 300
ORDER BY price;
Salida real: Gel pen | 120, Sticky notes | 200.
2. SELECT name, category FROM products WHERE category IS NULL; devolvió Mystery box. Con = NULL no obtienes nada.
3. Da 7, no 8. Para las Sticky notes, tanto NULL > 10 como NULL <= 10 son desconocidos, así que la fila falla las dos pruebas. "Toda fila está por encima o por debajo de 10" no es cierto cuando el valor es NULL.
Siguiente clase
La próxima clase deja de leer datos y empieza a cambiarlos: INSERT, UPDATE, DELETE y las restricciones (NOT NULL, UNIQUE, CHECK, FOREIGN KEY) que frenan los datos malos, incluido el accidente del "UPDATE sin WHERE" y cómo una transacción lo deshace.
- Siguiente: Curso de SQL 2/6: INSERT, UPDATE, DELETE y restricciones
- Más adelante en la serie: JOINs, GROUP BY y subconsultas (en inglés), índices y EXPLAIN QUERY PLAN (en inglés), transacciones y bloqueos (en inglés), funciones de ventana y CTE (en inglés)
- Dónde encaja: Fundamentos de programación para tu primer trabajo: ruta de estudio
- Relacionado: Curso de Python 1: tipos y mutabilidad, el mismo tema de "los tipos te sorprenden" en otro lenguaje
Preguntas frecuentes
¿Por qué WHERE columna = NULL no devuelve ninguna fila?
NULL significa "desconocido", y comparar cualquier cosa con un valor desconocido da NULL, no verdadero. WHERE solo conserva las filas donde la condición es verdadera, así que = NULL no encuentra nada, ni siquiera las filas que de verdad son NULL. Usa IS NULL o IS NOT NULL. En SQLite 3.51, SELECT NULL = NULL devuelve NULL y SELECT NULL IS NULL devuelve 1.
¿Por qué NOT IN no devuelve nada si la lista tiene un NULL?
x NOT IN (a, NULL) equivale a x <> a AND x <> NULL. La segunda parte siempre es NULL, así que la condición completa nunca es verdadera. En nuestra prueba, WHERE category NOT IN ('furniture', NULL) devolvió cero filas. Quita los NULL de la lista o usa NOT EXISTS.
¿Por qué SQLite ordena '10' antes que '9'?
Porque la columna guarda texto, y el texto se compara carácter por carácter: '1' va antes que '9'. Guarda los números en una columna INTEGER u ordena con ORDER BY CAST(code AS INTEGER). Una tabla STRICT (SQLite 3.37+) directamente se niega a guardar el tipo equivocado.