Saltar al contenido
AldeaCode Logo
Regex Tester / PostgreSQL Desarrollador 100% local

Regex en PostgreSQL: ~, regexp_match y diferencias POSIX vs PCRE

PostgreSQL tiene la caja de herramientas regex más completa de cualquier base de datos seria, pero habla POSIX, no PCRE. Las clases abreviadas que usas en JavaScript y Python no existen aquí, y esa es la fuente única de todos los bugs tipo "mi regex va en el IDE pero no en la query".

Los cuatro operadores: ~ ~* !~ !~*

Para match booleano, PostgreSQL te da cuatro operadores:

texto ~ patron: match sensible a mayúsculas. texto ~* patron: match insensible a mayúsculas. texto !~ patron: match sensible negado. texto !~* patron: match insensible negado.

SELECT 'hello' ~ '^h';      -- t
SELECT 'Hello' ~ '^h';      -- f
SELECT 'Hello' ~* '^h';     -- t
SELECT 'Hello' !~ '^h';     -- t

Devuelven un boolean plano y son la herramienta correcta para WHERE y CHECK. No extraen nada. Para extraer, necesitas las versiones tipo función.

regexp_match, regexp_matches, regexp_replace, regexp_split_to_array

regexp_match(texto, patron) devuelve un array text[] con los grupos de captura, o NULL si no hay match. Devuelve como mucho una coincidencia.

regexp_matches(texto, patron, 'g') devuelve un setof text[] con todas las coincidencias. El nombre va en plural y expande filas en la cláusula FROM; eso sorprende al que solo piensa en SELECT.

regexp_replace(texto, patron, reemplazo) sustituye. regexp_replace(texto, patron, reemplazo, 'g') sustituye globalmente.

regexp_split_to_array(texto, patron) y regexp_split_to_table(...) parten un string por un patrón y devuelven array o set de filas respectivamente.

```sql -- Extraer dominio del email SELECT (regexp_match('alice@example.com', '@(.+)$'))[1]; -- → example.com

-- Quitar tiradas de espacios múltiples SELECT regexp_replace('hola mundo', '\\s+', ' ', 'g'); -- → hola mundo ```

Sabor POSIX: \d no existe

PostgreSQL implementa expresiones regulares POSIX, no PCRE. La diferencia más grande de todas: las clases abreviadas tipo \d, \w, \s no son POSIX. Funcionan en JavaScript, Python, Perl, Ruby. No funcionan en Postgres.

Los equivalentes POSIX:

[[:digit:]] para \d. [[:alpha:]] para letras. [[:alnum:]] para caracteres de palabra sin guion bajo. [[:space:]] para espacios.

O rangos planos: [0-9], [a-zA-Z0-9_], [ \t\n].

PostgreSQL acepta también algunas extensiones ARE (Advanced Regular Expression), incluidos \d y \s desde la 8.0, pero la opción segura y portable es la forma con corchetes. Si alguna vez exportas la query a otra base de datos o a un CHECK que lee otra herramienta, la forma con corchetes sobrevive intacta.

Otras pegas POSIX

Los lookarounds ((?=...), (?!...)) no están en POSIX BRE/ERE pero PostgreSQL los soporta vía la extensión ARE. Pruébalos antes de depender de ellos en código portable.

El modificador inline (?i) funciona en Postgres pero no en algunos otros motores POSIX; prefiere la forma de operador (~*) cuando la sensibilidad a mayúsculas está fija para la query.

Los anclas son ^ y $. Por defecto coinciden con el inicio y fin del string, no de línea. Pasa el flag 'n' para que sean conscientes de líneas: regexp_match(texto, '^foo', 'n').

Las backreferences (\1, \2) funcionan tanto en patrón como en reemplazo. Los grupos con nombre estilo PCRE ((?P...)) no están soportados; usa grupos posicionales.

Ejemplo completo

sql
-- Validar formato tipo código postal UK en una restricción CHECK
ALTER TABLE addresses
  ADD CONSTRAINT postcode_format
  CHECK (postcode ~ '^[A-Z]{1,2}[0-9][A-Z0-9]? [0-9][A-Z]{2}$');

-- Filtrar filas con email bien formado
SELECT *
FROM users
WHERE email ~* '^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$';

-- Quitar todo lo que no es dígito de una columna de teléfono
SELECT regexp_replace(phone, '[^[:digit:]]', '', 'g') AS phone_digits
FROM contacts;

-- Partir una columna de tags separados por coma en filas
SELECT id, regexp_split_to_table(tags, ',\s*') AS tag
FROM posts
WHERE tags IS NOT NULL;

¿Solo necesitas el resultado?

Cuando estás diseñando un CHECK o depurando por qué una regex en Postgres no se comporta como la misma en tu IDE, el tester de regex en navegador de aldeacode.com te deja iterar el patrón en modo PCRE y muestra exactamente lo que ve el motor, para que traduzcas a POSIX sin viajes a psql.

Abrir Tester de Expresiones Regulares (JavaScript) →

Preguntas frecuentes

¿Por qué mi patrón con \d no encuentra nada en Postgres?

POSIX regex no tiene \d como abreviatura. PostgreSQL lo acepta vía la extensión ARE desde 8.0, así que debería ir, pero si tu patrón viaja envuelto en un string con escape extra de backslash (un parámetro JDBC, por ejemplo) lo que llega a Postgres puede ser un solo backslash más 'd'. Cambia a [[:digit:]] o [0-9] y la ambigüedad desaparece.

¿Uso regexp_match o regexp_matches?

El singular regexp_match para como mucho un resultado. Devuelve text[] en una sola fila, con NULL si no hay match. El plural regexp_matches expande en múltiples filas si pasas la flag 'g', y tienes que LATERAL-izarlo en el FROM. Usa regexp_match en el 95% de los WHERE; regexp_matches solo cuando de verdad necesitas todas las coincidencias.

¿Cómo hago match insensible a mayúsculas eficientemente?

El operador ~* es la respuesta fácil. Para queries de alto volumen monta un índice funcional: CREATE INDEX ON users (LOWER(email)); y reescribe la query como WHERE LOWER(email) = $1. El índice funcional convierte un escaneo regex en un lookup B-tree. ~* solo fuerza secuencial salvo que tengas además un índice trigram u otro amigo de regex.