Curso SQL · Northwind
Tema 10 de 64
Bloque 2 · La consulta PostgreSQL 17 northwind

Buscar texto: LIKE, ILIKE y expresiones regulares

Like

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 la sección 5
~ / ~* Expresión regular completa Cuando LIKE no llega

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

2. Sintaxis

2.1 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.

2.2 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

Pregunta: ¿qué clientes tienen un nombre de empresa que empieza por A?

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

4 de 91 clientes. LIKE 'A%' se lee "empieza por A y sigue lo que sea".

Pregunta: ¿qué clientes de Alemania tienen a algún tipo de gerente (Manager) como contacto?

SELECT
    company_name,
    country,
    contact_title
FROM
    customers
WHERE
    contact_title LIKE '%Manager%'
    AND country = 'Germany'
ORDER BY
    contact_title, company_name;
    company_name    | country |   contact_title
--------------------+---------+--------------------
 QUICK-Stop         | Germany | Accounting Manager
 Frankenversand     | Germany | Marketing Manager
 Toms Spezialitäten | Germany | Marketing Manager
(3 filas)

Mira la columna contact_title: ninguno dice literalmente "Manager" a secas — dice "Accounting Manager" o "Marketing Manager". %Manager% los encuentra a todos sin que tengas que enumerar cada variante: el % a ambos lados significa "y lo que sea antes, y lo que sea después".

Pregunta: ¿qué productos tienen la letra h justo en la segunda posición del nombre?

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

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

3.1 Mayúsculas: LIKE vs ILIKE

Pregunta: ¿cuántos clientes tienen un nombre que empieza por "a", escrita tal cual la escribiría alguien sin pensarlo, en minúscula?

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

Cero. Los cuatro clientes de la sección anterior existen, pero LIKE distingue mayúsculas y ninguno empieza por a minúscula. Con ILIKE, que la ignora, aparecen los mismos 4:

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

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

Pregunta: ¿qué clientes NO tienen la letra "a" en su nombre?

SELECT
    company_name
FROM
    customers
WHERE
    company_name NOT LIKE '%a%'
ORDER BY
    company_name
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%'.

3.2 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.

Resolverlo bien no es trivial: tilde aguda, diéresis y circunflejo son marcas distintas, y un patrón que cubra una se sigue rompiendo con otra. La extensión unaccent —específica de PostgreSQL, no existe en SQL Server ni MySQL— lo resuelve de raíz quitando cualquier marca diacrítica sin tener que enumerarlas.

3.3 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.

3.4 Cuando LIKE se queda corto: regex

Pregunta: ¿qué productos del catálogo empiezan por C o por B?

Con LIKE harían falta dos condiciones separadas por OR. Con regex, una sola clase de caracteres:

SELECT
    product_name
FROM
    products
WHERE
    product_name ~ '^[CB]'
ORDER BY
    product_name;
         product_name
------------------------------
 Boston Crab Meat
 Camembert Pierrot
 Carnarvon Tigers
 Chai
 Chang
 Chartreuse verte
 Chef Anton's Cajun Seasoning
 Chef Anton's Gumbo Mix
 Chocolade
 Côte de Blaye
(10 filas)

10 productos de 77 empiezan por B o por C.

Pregunta: ¿y si además no importan las mayúsculas — qué productos empiezan por "ch" en cualquier combinación?

SELECT
    product_name
FROM
    products
WHERE
    product_name ~* '^ch'
ORDER BY
    product_name;
         product_name
------------------------------
 Chai
 Chang
 Chartreuse verte
 Chef Anton's Cajun Seasoning
 Chef Anton's Gumbo Mix
 Chocolade
(6 filas)

Los mismos 6 de la lista anterior que empiezan por Ch, ni uno más — ~* es a ~ lo que ILIKE es a LIKE.

3.5 NOT LIKE y los nulos: no es un complemento real

Pregunta: ¿qué clientes alemanes tienen un fax que no sea de Berlín (código 030)?

Alemania tiene 11 clientes.

SELECT
    company_name,
    fax
FROM
    customers
WHERE
    country = 'Germany'
    AND fax NOT LIKE '030%'
ORDER BY
    company_name;
       company_name       |     fax
--------------------------+--------------
 Blauer See Delikatessen  | 0621-08924
 Die Wandernde Kuh        | 0711-035428
 Drachenblut Delikatessen | 0241-059428
 Frankenversand           | 089-0877451
 Lehmanns Marktstand      | 069-0245874
 Ottilies Käseladen       | 0221-0765721
 Toms Spezialitäten       | 0251-035695
(7 filas)

7, y parece razonable. Solo uno de los 11 tiene fax 030% (Alfreds Futterkiste), así que "los que no son de Berlín" debería ser el resto: 10. Faltan 3.

Los 3 que faltan son Königlich Essen, Morgenstern Gesundkost y QUICK-Stop — y no aparecen porque no tienen fax en absoluto. Aquí está la trampa: es fácil asumir que "no contiene 030" ya cubre a los que no tienen nada que contener — total, si no hay fax, mal puede empezar por 030. Pero para el motor no es así: NULL NOT LIKE '030%' no evalúa a TRUE, evalúa a UNKNOWN, y el WHERE lo descarta igual que si hubiera dado FALSE. La consulta no dice "estos 3 no calificaron"; simplemente los hace desaparecer.

Si el objetivo real incluye a los que no tienen fax —tiene sentido, porque no tener fax también significa "no es de Berlín"—, hay que decirlo explícitamente:

SELECT
    company_name,
    fax
FROM
    customers
WHERE
    country = 'Germany'
    AND (fax NOT LIKE '030%' OR fax IS NULL)
ORDER BY
    company_name;
       company_name       |     fax
--------------------------+--------------
 Blauer See Delikatessen  | 0621-08924
 Die Wandernde Kuh        | 0711-035428
 Drachenblut Delikatessen | 0241-059428
 Frankenversand           | 089-0877451
 Königlich Essen          |
 Lehmanns Marktstand      | 069-0245874
 Morgenstern Gesundkost   |
 Ottilies Käseladen       | 0221-0765721
 QUICK-Stop               |
 Toms Spezialitäten       | 0251-035695
(10 filas)

Regla: NOT LIKE (igual que <>) nunca es un verdadero complemento de LIKE cuando la columna admite nulos — deja fuera lo que no cumple y lo que no se pudo evaluar, sin distinguir entre ambos. Si quieres el complemento real, súmale OR columna IS NULL a propósito. Más sobre por qué pasa esto en null-y-la-logica-de-tres-valores.

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 § 3.5.

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]%'
ORDER BY
    product_name;

Da los mismos 10 productos 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. Lista su nombre completo y su 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.

? RespuestaRespóndela antes de abrir
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.

✓ SolucionesResuélvelos antes de abrir

1.

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

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%'
ORDER BY
    product_name;
        product_name
----------------------------
 Chocolade
 Teatime Chocolate Biscuits

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

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

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
    first_name,
    last_name,
    title
FROM
    employees
WHERE
    title LIKE '%Sales%'
ORDER BY
    title, last_name;
 first_name | last_name |          title
------------+-----------+--------------------------
 Laura      | Callahan  | Inside Sales Coordinator
 Steven     | Buchanan  | Sales Manager
 Nancy      | Davolio   | Sales Representative
 Anne       | Dodsworth | Sales Representative
 Robert     | King      | Sales Representative
 Janet      | Leverling | Sales Representative
 Margaret   | Peacock   | Sales Representative
 Michael    | Suyama    | Sales Representative
 Andrew     | Fuller    | Vice President, Sales
(9 filas)

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.

(Contar cuántos hay de cada cargo es un GROUP BY — se ve en el bloque 4. Aquí basta con mirar la lista y notar que Sales Representative se repite 6 veces.)

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

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

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.)