Curso SQL · Northwind
Tema 8 de 60
Bloque 2 · La consulta PostgreSQL 17 northwind

Buscar texto: LIKE, ILIKE y expresiones regulares

1. Qué es

= compara texto exacto. Cuando no sabes el valor entero —quieres "los que empiezan por A", "los que contienen Manager"— hace falta un operador de patrón.

PostgreSQL tiene tres niveles, de menos a más potencia:

Operador Potencia Cuándo
LIKE / ILIKE Dos comodines El 90% de los casos
SIMILAR TO Regex disfrazada de LIKE Casi nunca — ver § 5
~ / ~* Expresión regular completa Cuando LIKE no llega

Empieza siempre por LIKE. Solo cuando se quede corto, sube.

2. Sintaxis

LIKE — dos comodines y ya

Comodín Significa
% Cualquier cosa, incluida nada
_ Exactamente un carácter
Patrón Encuentra
'A%' Empieza por A
'%ez' Termina en ez
'%Chef%' Contiene Chef
'_h%' Segunda letra es h
'___' Exactamente tres caracteres

NOT LIKE niega. ILIKE es igual que LIKE pero ignora mayúsculas (la I es de insensitive) — y es exclusivo de PostgreSQL.

Regex — lo mínimo que hace falta

Operador Significa
~ Coincide con la regex
~* Coincide, ignorando mayúsculas
!~ No coincide
Símbolo Significa
^ Principio del texto
$ Final del texto
[CB] Una letra, C o B
[^CB] Una letra que no sea C ni B
[0-9] Un dígito
. Cualquier carácter
+ * ? Una o más / cero o más / cero o una

Diferencia clave con LIKE: la regex busca dentro del texto por defecto. LIKE 'Ch%' significa "empieza por Ch"; ~ 'Ch' significa "contiene Ch". Para anclar hay que poner ^.

3. Ejemplos sobre Northwind

Empieza por:

SELECT company_name FROM customers WHERE company_name LIKE 'A%';
            company_name
------------------------------------
 Alfreds Futterkiste
 Ana Trujillo Emparedados y helados
 Antonio Moreno Taquería
 Around the Horn

Contiene:

SELECT contact_title, count(*)
FROM customers
WHERE contact_title LIKE '%Manager%'
GROUP BY contact_title
ORDER BY 2 DESC;
   contact_title    | count
--------------------+-------
 Marketing Manager  |    12
 Sales Manager      |    11
 Accounting Manager |    10

El comodín de un solo carácter. Productos cuya segunda letra es h:

SELECT product_name FROM products WHERE product_name LIKE '_h%';
         product_name
------------------------------
 Chai
 Chang
 Chef Anton's Cajun Seasoning
 Chef Anton's Gumbo Mix
 Thüringer Rostbratwurst
 Chartreuse verte
 Chocolade
 Rhönbräu Klosterbier

8 productos. _ ocupa exactamente un hueco: ni cero, ni dos.

Mayúsculas: LIKE vs ILIKE

SELECT count(*) FROM customers WHERE company_name LIKE  'a%';   -- 0
SELECT count(*) FROM customers WHERE company_name ILIKE 'a%';   -- 4

Y el caso que enseña de verdad por qué importa:

SELECT company_name FROM customers WHERE company_name NOT LIKE '%a%' LIMIT 5;
       company_name
--------------------------
 Alfreds Futterkiste
 Around the Horn
 Blondesddsl père et fils
 Chop-suey Chinese
 Comércio Mineiro

Alfreds Futterkiste aparece en la lista de "no contiene la letra a". Y es correcto: contiene A, no a. Si lo que querías era "sin la letra a", el patrón correcto era NOT ILIKE '%a%'.

Los acentos son otro carácter

SELECT 'Antonio Moreno Taquería' LIKE '%Taqueria%' AS sin_tilde,
       'Antonio Moreno Taquería' LIKE '%Taquería%' AS con_tilde;
 sin_tilde | con_tilde
-----------+-----------
 f         | t

Ni LIKE ni ILIKE ignoran acentos. Northwind está lleno de Taquería, Côte de Blaye, Rhönbräu, Comércio. Un buscador que exija la tilde exacta es un buscador roto.

La solución de verdad es la extensión unaccent, disponible pero no instalada:

CREATE EXTENSION unaccent;                          -- una vez, por base
SELECT unaccent('Taquería');                        -- 'Taqueria'
SELECT * FROM customers WHERE unaccent(company_name) ILIKE unaccent('%taqueria%');

No la instales todavía en northwind. Cambiar la base fuera de seed.sql rompe la promesa de que se puede resetear. Cuando toque, va al seed.sql del repo (postgresql-northwind).

Buscar un comodín literal

Si el texto contiene _ o % de verdad, hay que escaparlo con \:

SELECT 'lote_2024' LIKE '%\_%' AS busca_guion_bajo,
       'lote 2024' LIKE '%\_%' AS con_espacio,
       'lote 2024' LIKE '%_%'  AS comodin;
 busca_guion_bajo | con_espacio | comodin
------------------+-------------+---------
 t                | f           | t

La tercera columna es la trampa: '%_%' no busca un guion bajo, busca "cualquier texto de al menos un carácter", así que es cierto para casi todo.

Cuando LIKE se queda corto: regex

Productos que empiezan por C o por B. Con LIKE harían falta dos condiciones. Con regex, una:

SELECT product_name FROM products WHERE product_name ~ '^[CB]' LIMIT 6;
         product_name
------------------------------
 Chai
 Chang
 Chef Anton's Cajun Seasoning
 Chef Anton's Gumbo Mix
 Carnarvon Tigers
 Côte de Blaye

Y sin importar mayúsculas, con ~*:

SELECT product_name FROM products WHERE product_name ~* '^ch';

4. Errores comunes

  1. Usar = con comodines. WHERE nombre = 'A%' busca el texto literal A%. No da error; devuelve cero filas.
  2. Olvidar que LIKE distingue mayúsculas. Ver el caso de Alfreds Futterkiste.
  3. Olvidar los acentos. Igual de silencioso.
  4. Creer que _ significa guion bajo. Significa un carácter cualquiera.
  5. Poner % al principio y esperar velocidad. LIKE 'A%' puede usar un índice; LIKE '%A%' no puede, obliga a leer la tabla entera. Con 91 clientes da igual; con 20 millones, no. La solución en ese caso es un índice pg_trgm — ver indices.
  6. Aplicar LIKE a una columna con nulos y esperar el complemento. NOT LIKE deja fuera los NULL, igual que <>. Ver null-y-la-logica-de-tres-valores.

5. Diferencias con SQL Server

Esta es la sección más útil del tema, porque es donde más se separan los dos motores.

Necesidad SQL Server PostgreSQL
Empieza por A LIKE 'A%' LIKE 'A%' — igual
Ignorar mayúsculas Automático (según collation) ILIKE, explícito
Un carácter de un conjunto LIKE '[CB]%' No existe en LIKE~ '^[CB]'
Un carácter fuera del conjunto LIKE '[^CB]%' ❌ → ~ '^[^CB]'
Un rango LIKE '[A-M]%' ❌ → ~ '^[A-M]'
Regex completa No hay operador nativo ~, ~*, !~

Los corchetes de LIKE son una extensión de SQL Server, no del estándar SQL. En PostgreSQL, LIKE '[CB]%' no da error: busca literalmente un texto que empiece por el carácter [. Devuelve cero filas y no avisa. Es exactamente el tipo de consulta que se copia de un curso de SQL Server, parece funcionar y no funciona.

SIMILAR TO sí acepta corchetes y es la traducción literal:

SELECT product_name FROM products WHERE product_name SIMILAR TO '[CB]%';

Da el mismo resultado que ~ '^[CB]'. Aun así, usa la regex: SIMILAR TO es un híbrido raro (comodines de LIKE + sintaxis de regex) que casi nadie lee bien, y la propia documentación de PostgreSQL desaconseja su uso.

6. Ejercicios

  1. Clientes cuyo nombre de empresa termina en s.
  2. Productos que contienen la palabra Chocolade o Chocolate, sin importar mayúsculas. ¿Cuántos hay de cada uno?
  3. Empleados cuyo cargo (title) contiene Sales, y cuántos son de cada cargo.
  4. Traduce a PostgreSQL esta consulta de SQL Server, que busca clientes cuya ciudad empieza por una letra entre la A y la C: sql SELECT * FROM customers WHERE city LIKE '[A-C]%';
  5. Clientes cuyo código postal es exactamente de 5 caracteres. Hazlo con LIKE y luego con regex, y di cuál prefieres.

7. Pregunta de negocio

Marketing quiere lanzar una campaña a los clientes "de tipo restaurante o comida": cualquiera cuyo nombre de empresa sugiera que está en hostelería. Te piden la lista.

No hay una columna que diga "sector". Hay que fabricarla desde el texto — y decir en qué no te fías.

SolucionesResuélvelos antes de abrir

1.

SELECT company_name FROM customers WHERE company_name LIKE '%s';

23 clientes. Y aquí aparece un matiz que conviene coger la costumbre de comprobar: LIKE '%s' distingue mayúsculas, así que se dejaría fuera cualquier nombre acabado en S. Resulta que no hay ninguno — ILIKE '%s' devuelve los mismos 23 — pero eso hay que verificarlo, no suponerlo.

2.

SELECT product_name FROM products WHERE product_name ILIKE '%chocolad%'
                                     OR product_name ILIKE '%chocolat%';
        product_name
----------------------------
 Teatime Chocolate Biscuits
 Chocolade

Dos productos. Y falta uno. La versión con regex lo encuentra:

SELECT product_name FROM products WHERE product_name ~* 'cho[ck]ola';
        product_name
----------------------------
 Teatime Chocolate Biscuits
 Schoggi Schokolade
 Chocolade

El que se escapaba es Schoggi Schokolade: en alemán se escribe con k, y ni chocolad ni chocolat lo pillan. Esto es lo que enseña de verdad la búsqueda de texto: el dato no está escrito como tú lo dirías. Buscar dos grafías te da una respuesta creíble y equivocada; el único motivo por el que lo has detectado es que la tabla tiene 77 filas y se puede mirar entera.

3.

SELECT title, count(*)
FROM employees
WHERE title LIKE '%Sales%'
GROUP BY title;
          title           | count
--------------------------+-------
 Inside Sales Coordinator |     1
 Sales Manager            |     1
 Sales Representative     |     6
 Vice President, Sales    |     1

9 empleados, que son todos. Northwind entero es un departamento comercial. El detalle que merece atención es Inside Sales Coordinator: contiene Sales en medio, no al principio. Si hubieras escrito LIKE 'Sales%' en vez de LIKE '%Sales%' habrías perdido ese y Vice President, Sales, y te habrías quedado con 7 de 9 sin enterarte.

4. Los corchetes no existen en el LIKE de PostgreSQL:

SELECT company_name, city FROM customers WHERE city ~ '^[A-C]';

22 clientes. La versión ingenua —LIKE '[A-C]%'— devuelve 0 filas sin dar error, y ese es el punto del ejercicio.

5.

SELECT company_name, postal_code FROM customers WHERE postal_code LIKE '_____';   -- 5 guiones bajos
SELECT company_name, postal_code FROM customers WHERE postal_code ~ '^.{5}$';

Las dos devuelven 50 clientes. La regex es mejor, y no por potencia: por legibilidad. '_____' obliga a contar guiones bajos con el dedo en la pantalla; '^.{5}$' dice cinco. Un patrón que hay que contar es un patrón que alguien va a escribir mal.

(Un tercer camino, más honesto todavía cuando lo que quieres es la longitud: WHERE length(postal_code) = 5. Si la pregunta es sobre longitud, usa la función de longitud, no un patrón — ver funciones-dentro-del-select.)

Pregunta de negocio.

SELECT company_name, country
FROM customers
WHERE company_name ~* 'restaurant|taquer|pizz|caf[eé]|delicat|market|grocer|food|comida|bistro'
ORDER BY company_name;
         company_name         |  country
------------------------------+-----------
 Antonio Moreno Taquería      | Mexico
 Bólido Comidas preparadas    | Spain
 Bottom-Dollar Markets        | Canada
 Cactus Comidas para llevar   | Argentina
 Great Lakes Food Market      | USA
 GROSELLA-Restaurante         | Venezuela
 Hungry Owl All-Night Grocers | Ireland
 LINO-Delicateses             | Venezuela
 Lonesome Pine Restaurant     | USA
 Old World Delicatessen       | USA
 Pericles Comidas clásicas    | Mexico
 Rattlesnake Canyon Grocery   | USA
 Save-a-lot Markets           | USA
 Simons bistro                | Denmark
 Tortuga Restaurante          | Mexico
 White Clover Markets         | USA

16 clientes de 91.

Y ahora lo que hay que decir al entregarla, que es el 80% del valor:

  1. Esto no es el sector, es una corazonada sobre el nombre. Nombres como Around the Horn, Bon app' o La maison d'Asie no los pilla ningún patrón, y podrían ser perfectamente hostelería. No hay forma de saberlo desde el nombre.
  2. El idioma rompe cualquier lista de palabras. Northwind tiene clientes en 21 países. Buscar restaurant no encuentra Taquería, ni Trattoria, ni Comidas. Cada palabra que añades es un idioma que recuerdas y diez que no.
  3. Falsos positivos garantizados. Market pilla supermercados, que quizá Marketing no considere hostelería.
  4. Lo que hay que proponer: si el sector del cliente importa para el negocio, es una columna, no un LIKE. La consulta sirve para hacer un primer barrido y que alguien la revise a mano una vez; no para repetirla cada trimestre.

Esa última frase —"esto no debería resolverse con una búsqueda de texto"— es la respuesta profesional. La consulta la entregas igual.