Dois robôs humanoides lutando boxe num ringue, com o árbitro ao lado

O que é busca vetorial, e qual banco de dados faz melhor esse papel? Hoje os quatro bancos que mais aparecem em empresa no Brasil dizem que fazem busca vetorial: Oracle 26ai, SQL Server 2025, PostgreSQL com pgvector e MySQL 9.7. Coloquei o MariaDB 11.8 junto, porque ele nasceu do MySQL, roda em muita empresa no lugar dele e tem índice vetorial nativo. Eu fui testar o que cada um faz de verdade. Peguei a tabela de produtos (SB1) de um Protheus de desenvolvimento, fiz as mesmas 100 perguntas em cada banco, no mesmo servidor, e medi. Antes de mostrar os resultados, explico do zero o que é essa busca e quais ferramentas usei, porque sem isso os números não dizem nada. O SQL Server 2025 ficou de fora do lab por uma questão de licença: a parte dele saiu da documentação da Microsoft, e eu aviso onde.

O que é busca vetorial, explicado do zero

A busca que todo sistema tem é por palavra escrita. No banco ela é o LIKE: você procura ‘%PARAFUSO SEXTAVADO INOX%’ e ele devolve as linhas que tem exatamente esse pedaço de texto. Se o produto foi cadastrado como PARAF SEXT INOX, ele não aparece, mesmo sendo o mesmo parafuso. Cadastro de ERP é cheio disso.

Busca vetorial é a busca por sentido. Em vez de comparar letras, ela compara o significado. Pra isso cada texto vira um vetor, que é só uma lista de números. Essa lista é gerada por um modelo, um programa treinado pra ler texto e devolver números de um jeito que textos com sentido parecido viram listas parecidas. “Parafuso sextavado inox” e “PARAF SEXT INOX M8X30” viram listas quase iguais, mesmo escritos diferente. O nome em inglês desse vetor é embedding; é a mesma coisa.

Com os vetores guardados na tabela, a busca funciona assim: a pergunta da pessoa também vira um vetor, e o banco procura os produtos cujo vetor está mais perto do vetor da pergunta. “Perto” é uma conta de distância entre as duas listas, e quanto menor a distância, mais parecido o sentido. Os 10 mais perto são a resposta.

Essa conta pode ser feita de dois jeitos. O jeito completo compara a pergunta com todos os produtos, um por um. Nunca erra, mas em tabela grande fica lento, porque o tempo cresce junto com a tabela. O jeito com índice usa um atalho que o banco prepara antes, e aí a busca olha só uma parte dos produtos. É rápido, mas pode deixar passar alguém. Cada banco tem o seu tipo de atalho, e eu explico cada um numa seção só pra isso, antes de entrar nos testes.

Por isso existe uma nota pro atalho, que chama recall. Dos 10 produtos que o atalho devolveu, quantos são tão bons quanto os 10 da busca completa? Recall 100% é o atalho acertando tudo. Recall 75% quer dizer que, dos 10, uns 2 ou 3 não deviam estar ali. Vou usar essa palavra o tempo todo daqui pra frente.

As ferramentas do lab e como eu usei cada uma

TABELA SB1 do Protheus 72.965 produtos, dev B1_COD B1_DESC B1_GRUPO UMA VEZ SÓ, FORA 1 vetor por produto all-MiniLM-L12-v2, 384 dim sb1_vec.csv recno;[0.01,…] PostgreSQL 18 + pgvectorHNSW, filtro, iterative scan Oracle 26ai FreeHNSW, ONNX no banco, híbrido MariaDB 11.8índice MHNSW nativo MySQL 9.7 Communityguarda o vetor, e só SQL Server 2025só documentação (licença)

O dado. Montei uma SB1 de mentira, mas com cara de verdade: 72.965 produtos em 21 grupos (parafusos, cabos, tintas, EPI e por aí vai). As descrições vêm misturadas do jeito que o cadastro real tem, o mesmo parafuso como PARAFUSO SEXTAVADO INOX M8X30, PARAF SEXT INOX M8X30 e PRF SEXT. INOX 8X30MM. Também tem 3% de registro deletado (D_E_L_E_T_ = ‘*’) e 4% bloqueado, porque é assim que a tabela vive. O LIKE mostra o problema logo de cara: procurar ‘%PARAFUSO SEXTAVADO INOX%’ acha 672 itens e deixa 1.327 abreviados de fora.

psql: pgvector 0.8.7 no PostgreSQL 18.6, carga da SB1 com 72.965 linhas, LIKE acha 672 e deixa 1.327 abreviados de fora
O LIKE acha 672 itens e deixa 1.327 abreviados de fora.

O modelo. Usei o all-MiniLM-L12-v2, que gera listas de 384 números (a gente diz 384 dimensões). Pra rodar o modelo usei o ONNX Runtime, que é um motor leve pra executar esse tipo de programa, sem precisar instalar o PyTorch, que é pesado. Gerei o vetor de cada um dos 72.965 produtos uma vez só, fora dos bancos, e gravei tudo num arquivo. Fiz assim de propósito: se cada banco gerasse o próprio vetor, eu não saberia se a diferença no resultado era do banco ou do vetor. Com o mesmo arquivo nos quatro, a comparação é justa. Vetorizar tudo levou 16 minutos nos 2 núcleos, uns 75 produtos por segundo.

Testei também um segundo modelo, o multilíngue e5-small, achando que ia ganhar por entender português. Perdeu de pouco: 92,4% contra 94,3% do MiniLM. Essa porcentagem é uma conta simples que fiz pra escolher: pego os 10 produtos mais parecidos que o modelo devolve pra cada pergunta e conto quantos caem no grupo certo (perguntei de parafuso, voltou parafuso?). Fiquei com o MiniLM, que ainda por cima é o modelo que a Oracle distribui pronto pra rodar dentro do banco (volto nisso).

Saída do script de embedding: 72.965 descrições em 973 segundos com o all-MiniLM-L12-v2, 94,3% dos 10 primeiros no grupo certo, contra 92,4% do multilingual-e5-small
Os dois modelos nas mesmas 100 perguntas. O MiniLM ficou.

As perguntas. São 100, em português de gente: “parafuso sextavado inox”, “mangueira hidráulica meia polegada”, “luva de segurança nitrílica tamanho g”. Cada uma virou um vetor com o mesmo modelo, e esse vetor foi gravado numa tabelinha em cada banco, pra consulta não depender de nada de fora.

A medição. Um script em Python faz as mesmas 100 perguntas em cada banco, três vezes, joga fora a primeira rodada (que é a de aquecimento) e anota três coisas: a mediana, que é o tempo “do meio” (metade das perguntas foi mais rápida, metade mais lenta); o p95, que é o tempo que só 5% das perguntas passaram; e o recall, comparando a resposta do banco com a busca completa feita fora, nos mesmos vetores. Como tem muita descrição repetida no cadastro (vetor igual), o recall compara pela distância e não pelo código do produto, senão o empate derruba a nota sem motivo.

Os bancos. PostgreSQL 18.6 com pgvector 0.8.7, Oracle AI Database 26ai Free (versão 23.26.3), MySQL 9.7.4 Community e MariaDB 11.8.8, cada um em container Docker, um de cada vez, no mesmo servidor de 2 vCPU e 6,7 GB de RAM. O SQL Server 2025 entra só pela documentação. Agora, um por um.

Os índices que aparecem neste lab, e a diferença entre eles

Primeiro, o que é um plano de execução, porque ele aparece em vários prints. Quando você manda um SELECT, o banco decide sozinho por onde vai buscar o dado: ler a tabela inteira, ir por um índice, em que ordem. Essa decisão é o plano. O comando EXPLAIN mostra o plano sem precisar adivinhar. Nos prints do PostgreSQL, quando aparece Seq Scan quer dizer que ele leu as 72.965 linhas uma por uma; quando aparece Index Scan using sb1_emb_hnsw, ele foi pelo índice. Na Oracle é a mesma coisa com outros nomes: TABLE ACCESS FULL é ler tudo, VECTOR INDEX HNSW SCAN é ir pelo índice. É assim que eu sei se o banco usou o atalho ou não, em vez de confiar no tempo.

Sem índice nenhum, a busca é a completa: o banco calcula a distância da pergunta pra cada um dos 72.965 vetores e fica com os 10 menores. Foi o que deu 49 ms no PostgreSQL e 200 ms na Oracle. Acerta sempre. O problema é só o tempo, que dobra se a tabela dobrar.

O HNSW é o índice que PostgreSQL e Oracle usam, e a ideia é a de um mapa de vizinhança. Na hora de montar o índice, o banco liga cada produto aos poucos produtos mais parecidos com ele, como uma rede de amigos. Na busca, ele entra num ponto qualquer da rede, olha os vizinhos, pula pro que está mais perto da pergunta, olha os vizinhos desse, e vai assim até não achar ninguém mais perto. Em vez de 72.965 comparações, faz algumas centenas. Por isso 0,6 ms em vez de 50. O preço é que ele pode parar num canto do mapa achando que chegou, quando o produto certo estava em outro canto; é daí que vem o recall abaixo de 100%. O ajuste ef_search, no pgvector, é quantos candidatos ele carrega no bolso enquanto anda: com 40 (o padrão) acertou 75%, com 400 acertou 94,5%, gastando mais tempo.

O HNSW do pgvector e o da Oracle são o mesmo tipo de mapa, mas moram em lugares diferentes. O do pgvector é gravado em disco, como qualquer índice do PostgreSQL (deu 36 MB), e o banco traz pra memória o pedaço que usa. O da Oracle só existe na memória RAM: o nome completo é ORGANIZATION INMEMORY NEIGHBOR GRAPH, e ele vive numa área reservada chamada vector pool. É por isso que na Oracle eu precisei configurar o vector_memory_size antes, que a montagem demorou quase 2 minutos (ela constrói a rede inteira na memória e ainda ajusta a precisão) e que, se o banco reiniciar, o índice é montado de novo. Em troca, a Oracle deixa você dizer o quanto pode errar, WITH TARGET ACCURACY 95, e cumpre: deu 96%.

Tem uma diferença que importa mais que a velocidade: como cada um trata o WHERE. O pgvector primeiro anda no mapa, separa os candidatos, e só depois aplica o filtro; se nenhum candidato passar no filtro, volta vazio. A Oracle olha o filtro antes de decidir, e quando acha que ele deixa poucas linhas, desiste do mapa e faz a busca completa só nessas linhas. Um erra pra menos, o outro gasta mais. Isso aparece nos testes de cada banco.

O índice híbrido, que só a Oracle tem, é outra coisa: são dois índices em um. Um de texto, do Oracle Text, que acha palavra exata (o “M8X30”); e um de vetor, que acha pelo sentido (“parafuso inox”). Na busca os dois dão nota, e a nota final junta as duas. E ele gera os vetores sozinho, com o modelo que está dentro do banco, por isso demorou 33 minutos pra montar: estava vetorizando 73 mil descrições.

O do SQL Server 2025 chama DiskANN. Também é um mapa de vizinhança, mas desenhado pra ficar em disco SSD, não só na memória, pra caber tabela maior que a RAM. É o que está em preview. Eu não medi.

O MariaDB tem o seu, que chama MHNSW. É o mesmo tipo de mapa de vizinhança do HNSW, gravado dentro do InnoDB como um índice normal da tabela, e com um detalhe bom: o MariaDB guarda os vetores do índice de um jeito compacto, então a busca é rápida e a precisão veio alta sem mexer em nada (99% no padrão). O ajuste de quantos candidatos olhar chama mhnsw_ef_search, e o padrão é 20. O que ele não tem é um jeito de corrigir o problema do WHERE, como o iterative scan do pgvector; você vai ver isso nos testes.

O MySQL Community não tem índice nenhum pra vetor, então não entra nessa conversa. Agora os testes, um banco por vez.

PostgreSQL com pgvector: funcionou de primeira, até chegar o filtro

pgvector é a extensão que dá ao PostgreSQL o tipo de coluna vector e as contas de distância. Criei a coluna vector(384), carreguei os vetores do arquivo por uma tabela temporária e fiz um UPDATE pra casar cada vetor com o seu produto: 11 segundos, 155 MB no total.

psql: ALTER TABLE adicionando a coluna vector(384), COPY dos 72.965 vetores, UPDATE em 11 segundos, tabela com 155 MB
Carga dos vetores no PostgreSQL.

Sem índice nenhum a busca já funciona. O operador <=> calcula a distância entre dois vetores, então ORDER BY emb <=> vetor LIMIT 10 é “me dá os 10 mais perto”. Devolveu os parafusos sextavados inox M8 em 50 ms. No print, embaixo do resultado, está o plano: Seq Scan on sb1, ou seja, leu as 72.965 linhas, e Buffers: shared hit=19440, que são os blocos de disco que ele passou pra fazer isso. Guarde esse número.

psql: busca exata por similaridade no PostgreSQL sem índice, 10 parafusos sextavados inox M8 em 50 ms, plano com Seq Scan e 19.440 blocos
Busca completa, sem índice: 50 ms e 19.440 blocos.

Aí fui criar o índice HNSW e tomei o primeiro erro do dia:

psql: CREATE INDEX USING hnsw falhando com ERROR could not resize shared memory segment to 533745184 bytes: No space left on device
O erro não é do pgvector, é do /dev/shm de 64 MB do Docker.

Esse erro não é do pgvector, é do Docker. O container vem com uma área de memória compartilhada (/dev/shm) de só 64 MB, e a montagem do índice pede mais que isso pra trabalhar em paralelo. Subi o container de novo com –shm-size=1g:

docker run -d --name pgvec --memory=2g --shm-size=1g -e POSTGRES_PASSWORD=lab \
  -p 55432:5432 -v /lab:/lab pgvector/pgvector:pg18 \
  -c shared_buffers=512MB -c maintenance_work_mem=512MB -c max_parallel_maintenance_workers=2

CREATE INDEX sb1_emb_hnsw ON sb1 USING hnsw (emb vector_cosine_ops);
psql: CREATE INDEX hnsw em 10 segundos, índice com 36 MB, hnsw.ef_search 40, iterative_scan off, consulta com Index Scan using sb1_emb_hnsw e 276 blocos
Índice em 10 segundos, 36 MB. A consulta caiu de 19.440 blocos para 276.

Índice pronto em 10 segundos, 36 MB. Olha o plano agora: Index Scan using sb1_emb_hnsw, e os blocos caíram de 19.440 pra 276. A mesma consulta passou de 50 ms para 0,6 ms. Só que o recall ficou em 75,6%, com o ef_search no padrão de 40. Subindo pra 100 vai a 84%, com 200 vai a 91%, com 400 vai a 94,5% e a consulta ainda fica em 2 ms. Tem outro motivo pro recall baixo no padrão: cadastro de ERP tem muita descrição repetida, e vetor repetido atrapalha a rede de vizinhos do HNSW.

Agora o que importa de verdade: filtro. Nenhum SQL de Protheus roda sem WHERE D_E_L_E_T_ = ‘ ‘ (só registro não apagado), e quase sempre tem grupo, filial, tipo junto. Pedi “correia dentada” dentro do grupo 0020, que é o grupo de correias e transmissão e tem 207 correias dentadas cadastradas:

psql: a consulta com filtro de grupo e D_E_L_E_T_ devolve 0 rows com o índice HNSW, e a mesma consulta com enable_indexscan off devolve 10 correias dentadas A-42
Zero linhas com o índice. Sem o índice, 10. Nenhum erro, nenhum aviso.

Zero linhas. Sem erro, sem aviso. A busca completa, na mesma consulta, devolve 10 correias dentadas. O que aconteceu: o HNSW separou os 40 candidatos mais parecidos com “correia dentada” na tabela inteira, e só depois o PostgreSQL aplicou o WHERE. Nenhum dos 40 era do grupo 0020 e não apagado, então sobrou nada. Pra quem está na tela do sistema, parece que não existe correia dentada no cadastro. A própria documentação do pgvector descreve isso:

README do pgvector no GitHub: com índice aproximado o filtro é aplicado depois do scan, com 10% de linhas e ef_search 40 só 4 linhas voltam em média, a solução é o iterative index scan
O próprio README do pgvector avisa.

A correção existe desde a versão 0.8.0 e vem desligada: o iterative scan. Com SET hnsw.iterative_scan = relaxed_order, o banco continua andando no mapa até juntar 10 produtos que passam no filtro, em vez de parar nos primeiros 40.

psql: SET hnsw.iterative_scan = relaxed_order e a mesma consulta devolve 10 correias dentadas, plano mostrando Rows Removed by Filter: 72, max_scan_tuples 20000, work_mem 4MB
Com iterative scan: 10 linhas, 72 removidas pelo filtro, 0,48 ms.

Nas 100 perguntas com esse filtro, a média de linhas devolvidas foi de 0,3 com o padrão (ou seja, quase sempre vazio), 9,2 com strict_order e 9,3 com relaxed_order, este com recall de 91% e 12 ms por pergunta. Sete perguntas continuaram em zero mesmo com os limites bem mais altos; são perguntas que não tem nada a ver com o grupo (“cabo de rede”, “graxa”), mas a busca completa devolve 10 mesmo assim. Nunca vi um ambiente ligar o iterative_scan antes de passar por esse susto.

Oracle 26ai AI Vector Search: o vetor nasce dentro do banco, mas a memória não vem pronta

Imagem completa do Oracle AI Database 26ai Free em container, nada pra instalar. A coluna é do tipo VECTOR(384, FLOAT32): 384 números, cada um guardado com 4 bytes. O DESC mostra DENSE, que quer dizer que todos os 384 números são gravados.

SQL*Plus: CREATE TABLE sb1 com coluna emb VECTOR(384, FLOAT32) no Oracle 26ai, DESC mostrando VECTOR(384, FLOAT32, DENSE)
A tabela no Oracle 26ai.

Carreguei os mesmos vetores do arquivo com um script Python (python-oracledb), usando TO_VECTOR pra converter o texto em vetor: 18 segundos, 144 MB. Primeira bobagem minha: COUNT(emb) numa coluna VECTOR dá ORA-22849, o COUNT não aceita esse tipo; conta com IS NOT NULL. A busca completa, com VECTOR_DISTANCE, devolveu os mesmos 10 parafusos que o PostgreSQL.

SQL*Plus: ORA-22849 ao fazer COUNT em coluna VECTOR, tabela com 144 MB, busca exata com VECTOR_DISTANCE COSINE devolvendo os 10 parafusos sextavados inox M8 em 0,32 segundos
ORA-22849 no COUNT(emb) e a busca completa com VECTOR_DISTANCE.

Medindo as 100 perguntas, a busca completa deu 200 ms por pergunta, quatro vezes o PostgreSQL na mesma máquina.

O índice HNSW na Oracle mora na memória RAM, num pedaço reservado chamado vector pool. Na Free esse pedaço vem com tamanho zero, e sem ele não dá pra criar o índice:

SQL*Plus: vector_memory_size igual a 0 e CREATE VECTOR INDEX ORGANIZATION INMEMORY NEIGHBOR GRAPH falhando com ORA-51962: The vector memory area is out of space for the current container
ORA-51962: sem vector pool não tem HNSW.

O ajuste é no banco raiz (o CDB), como usuário SYS, e só vale depois de reiniciar. A memória total do banco na Free (sga_target) é 1536M, então reservar 512M pro pool cabe. Reiniciar o container levou 25 segundos.

SQL*Plus como SYS no CDB$ROOT: sga_target 1536M, ALTER SYSTEM SET vector_memory_size = 512M SCOPE=SPFILE
O pool se configura no CDB e só vale depois do restart.

Com o pool de pé o índice saiu, mas demorou 1 minuto e 47 segundos (o pgvector fez em 10 segundos) e ocupou 131 MB do pool. Como ele vive na memória, se o banco reiniciar ele é montado de novo.

SQL*Plus: vector pool com 448 MB mais 48 MB alocados, CREATE VECTOR INDEX em 1 minuto e 47 segundos, 72.965 vetores e 131 MB no índice, consulta com FETCH APPROX FIRST 10 ROWS ONLY e plano VECTOR INDEX HNSW SCAN
1 min 47 s de build, 131 MB no pool, e o plano com VECTOR INDEX HNSW SCAN.

Em compensação a consulta ficou boa. Na Oracle você pede pra usar o índice com FETCH APPROX FIRST 10 ROWS ONLY (o APPROX é “pode ser aproximado”), e o plano confirma: VECTOR INDEX HNSW SCAN. Deu 1,36 ms por pergunta e recall de 96%. Na hora de criar o índice eu pedi WITH TARGET ACCURACY 95, que é “quero pelo menos 95% de acerto”, e ele entregou exatamente isso.

E o filtro? A mesma “correia dentada” no grupo 0020:

SQL*Plus: a consulta com filtro de grupo e D_E_L_E_T_ e FETCH APPROX devolve 10 correias dentadas A-42, e o plano mostra TABLE ACCESS FULL com o filtro em vez do índice vetorial
Dez linhas certas, mas por TABLE ACCESS FULL: o otimizador deixou o índice de lado.

Dez linhas certas. Só que olha o plano: TABLE ACCESS FULL, e nenhum VECTOR INDEX na lista. O otimizador, que é a parte do banco que escolhe o plano, viu o filtro, deixou o índice de lado e fez a busca completa nas 1.206 linhas do grupo. Nas 100 perguntas com filtro, 66 ms por pergunta e recall 100%. Ou seja: o PostgreSQL respondeu rápido e vazio, a Oracle respondeu certo e devagar. Nenhum dos dois faz o que a gente quer sozinho. É decisão de DBA, e tem que ser tomada antes de ir pra produção, não depois.

Agora a parte que só a Oracle tem: o modelo dentro do banco. Lembra que o modelo é o programa que transforma texto em vetor? No PostgreSQL e no MySQL ele fica do lado de fora, num Python, e a aplicação precisa chamar esse Python antes de perguntar ao banco. Na Oracle dá pra carregar o modelo no próprio banco. Baixei o all_MiniLM_L12_v2.onnx que a Oracle distribui no formato dela (133 MB), coloquei numa pasta que o banco enxerga (um DIRECTORY) e carreguei com DBMS_VECTOR.LOAD_ONNX_MODEL. A partir daí o SQL gera o vetor sozinho:

BEGIN
  DBMS_VECTOR.LOAD_ONNX_MODEL(directory => 'LAB_DIR', file_name => 'all_MiniLM_L12_v2.onnx', model_name => 'MINILM');
END;
/
SELECT VECTOR_EMBEDDING(minilm USING 'parafuso sextavado inox' AS data) FROM dual;
SQL*Plus: modelo MINILM carregado com 127 MB, VECTOR_EMBEDDING gerando vetor de 384 dimensões, distância 0 entre o vetor gerado dentro do banco e o gerado fora para as mesmas três perguntas, consulta com embedding e busca em 0,16 segundos
O vetor gerado dentro do banco é idêntico ao gerado fora: distância zero.

Comparei o vetor que o banco gerou com o que eu tinha gerado fora, com ONNX Runtime, pro mesmo texto. Distância zero nas três perguntas que testei: é o mesmo modelo, dá o mesmo vetor. Na prática isso resolve uma dor: a aplicação manda o texto da busca e o banco faz o resto, sem precisar de um serviço Python no meio. Gerar o vetor da pergunta e buscar, tudo junto, levou 0,16 segundos.

E tem o índice híbrido. Híbrido aqui quer dizer que ele junta duas buscas numa só: a busca por palavra exata (do Oracle Text, que é o mecanismo de busca de texto da Oracle) e a busca por sentido (o vetor). Um único comando, CREATE HYBRID VECTOR INDEX em cima da coluna B1_DESC apontando o modelo, faz a Oracle gerar o vetor das 73 mil descrições dentro do banco e montar o índice de texto junto. Levou 32 minutos e 59 segundos nesta máquina, e o índice ficou com 171 MB.

CREATE HYBRID VECTOR INDEX sb1_hib ON sb1 (b1_desc) PARAMETERS ('MODEL MINILM');
SQL*Plus: CREATE HYBRID VECTOR INDEX em 32 minutos e 59 segundos, índice DOMAIN com status VALID, e DBMS_HYBRID_VECTOR.SEARCH devolvendo JSON com rowid, score, vector_score e text_score
O híbrido pronto e a primeira busca devolvendo rowid com score vetorial e textual separados.

A busca é pela função DBMS_HYBRID_VECTOR.SEARCH. Você passa os parâmetros num texto em formato JSON e ela devolve, também em JSON, a nota de texto e a nota de vetor de cada produto, separadas. Cruzei os rowids (o endereço de cada linha) com a SB1 pra ler as descrições. A pergunta foi “parafuso inox M8X30”: o M8X30 é código de medida, tem que bater exato; “parafuso inox” é sentido.

SQL*Plus: resultado do índice híbrido cruzado com a SB1, os três primeiros são PARAFUSO FLANGEADO INOX M8X30 com text_score 69 e os seguintes M8X20, M8X50, M8X40 com text_score 35; a busca por "fixador sextavado em aco inoxidavel" devolve porcas sextavadas com text_score 0
Os três primeiros são exatamente M8X30. Sem palavra exata, só o vetor trabalha.

Os três primeiros são PARAFUSO FLANGEADO INOX M8X30, com text_score 69 porque o M8X30 bateu; os seguintes são M8X20, M8X50 e M8X40, parecidos no sentido mas com o text_score caindo pra 35. Na segunda pergunta, “fixador sextavado em aco inoxidavel”, nenhuma palavra existe no cadastro escrita assim, o text_score vai a zero e só o vetor trabalha: trouxe porcas sextavadas, que é parente, mas não é parafuso. Pra quem tem ERP esse é o cenário mais real que existe: código de produto é busca exata, descrição é busca por sentido, e o índice híbrido faz os dois numa chamada só.

MySQL 9.7 Community: guarda o vetor e não sabe comparar

MySQL 9.7.4 Community, que é a versão gratuita e a LTS (suporte longo) de abril de 2026. Criei a coluna VECTOR(384), carreguei a SB1 com LOAD DATA e converti os vetores do arquivo com STRING_TO_VECTOR num UPDATE: 66 segundos, bem mais que os outros dois, e 174 MB. VECTOR_DIM responde 384, então o vetor está lá inteiro. Até aqui parece que vai.

mysql: MySQL 9.7.4, CREATE TABLE com emb VECTOR(384), LOAD DATA LOCAL INFILE, UPDATE com STRING_TO_VECTOR em 66 segundos, 72.965 vetores, 174 MB, VECTOR_DIM 384
Carga no MySQL 9.7: o tipo VECTOR existe e aceita os 384 valores.

O problema aparece na hora de comparar. Pra achar os parecidos, o banco precisa de uma função que calcule a distância entre dois vetores. No MySQL ela chama DISTANCE():

mysql: SELECT com DISTANCE e com VECTOR_DISTANCE falhando com ERROR 1305 (42000): FUNCTION lab.DISTANCE does not exist
FUNCTION lab.DISTANCE does not exist. VECTOR_DISTANCE também não.

Não existe. Nem ela nem o outro nome, VECTOR_DISTANCE. O manual explica: essa função só vem no MySQL HeatWave (a versão em nuvem da Oracle) e no MySQL AI. Na Community e na Commercial, não.

Manual do MySQL 9.7, Vector Functions: nota dizendo que DISTANCE() está disponível só para usuários do MySQL HeatWave on OCI e MySQL AI, não incluída nas distribuições Commercial ou Community
A nota do manual: DISTANCE() só no HeatWave e no MySQL AI.

E índice vetorial o MySQL Community não tem. Tentei dos dois jeitos que a sintaxe do MySQL aceitaria se existisse, e o banco não reconhece nenhum:

mysql: CREATE VECTOR INDEX e ALTER TABLE ADD VECTOR INDEX recusados pelo MySQL 9.7 Community, SHOW INDEX mostrando só a PRIMARY
Não existe índice vetorial no MySQL Community: só a chave primária na tabela.

O que dá pra fazer no Community é tirar os vetores do banco e comparar na aplicação. Eu fiz: trazer os 72.965 vetores levou 15,8 segundos e ocupou 112 MB de memória no programa; depois disso cada pergunta sai em 9,5 ms, com recall 100%, porque é a busca completa. Pra 70 mil produtos passa. Pra 7 milhões, não. Quem quer índice vetorial sem sair dessa família vai pro MariaDB, que é o próximo da lista.

MariaDB 11.8: o primo do MySQL que tem índice

O MariaDB saiu do MySQL em 2009 e hoje é outro banco, mas quem tem MySQL costuma conseguir trocar por ele sem mexer na aplicação. Desde a versão 11.7, e na 11.8, que é a LTS, ele tem o tipo VECTOR, as funções de distância e o índice vetorial, tudo na versão gratuita. Testei a 11.8.8. A coluna é VECTOR(384) NOT NULL (o NOT NULL é obrigatório pra poder indexar), e a carga foi com LOAD DATA mais VEC_FromText, que converte o texto em vetor: 10 segundos.

mariadb: MariaDB 11.8.8, CREATE TABLE com emb VECTOR(384) NOT NULL, LOAD DATA e INSERT com VEC_FromText em 10,4 segundos, 72.965 produtos, VEC_ToText mostrando o início do vetor
Carga no MariaDB 11.8: tipo VECTOR, VEC_FromText e VEC_ToText na versão gratuita.

A busca completa funciona de cara com VEC_DISTANCE_COSINE, e o EXPLAIN mostra type ALL com Using filesort, que é “li a tabela inteira e ordenei”. Deu 83 ms no cliente e 87 ms por pergunta na medição, perto do PostgreSQL.

mariadb: busca completa com VEC_DISTANCE_COSINE devolvendo os 10 parafusos sextavados inox M8 em 83,5 ms, EXPLAIN com type ALL e Using filesort
Busca completa no MariaDB: 83 ms, leitura da tabela inteira.

Antes do índice, uma pegadinha que custou caro no meu script de medição. Se a conversão do texto em vetor, o VEC_FromText, ficar dentro do ORDER BY, o MariaDB refaz essa conversão pra cada uma das 72.965 linhas, e a mesma consulta vai de 89 ms pra 2,9 segundos. A saída é converter uma vez, numa variável, e usar a variável na consulta. É assim que a aplicação deve mandar o vetor da pergunta.

mariadb: a mesma busca completa com VEC_FromText dentro do ORDER BY leva 2.942 ms e com o vetor numa variável de sessão leva 89 ms
VEC_FromText dentro do ORDER BY: 2,9 segundos. Numa variável: 89 ms.

O índice entra com ALTER TABLE … ADD VECTOR INDEX, e dá pra escolher o M (quantos vizinhos cada ponto do mapa guarda) e a distância. Usei M=16 e cosseno, igual aos outros. Demorou 2 minutos e 6 segundos pra montar, mais que o pgvector (10 segundos) e perto da Oracle (1 minuto e 47). Depois disso o EXPLAIN mostra o índice sb1_emb sendo usado e a consulta caiu de 83 ms pra 4,5 ms no cliente e 0,76 ms por pergunta na medição, com recall de 99% no padrão (mhnsw_ef_search = 20). Foi a melhor precisão com configuração de fábrica entre os bancos do lab.

mariadb: variáveis mhnsw, ALTER TABLE ADD VECTOR INDEX M=16 DISTANCE=cosine em 125,7 segundos, SHOW INDEX com Index_type VECTOR, consulta em 4,51 ms e EXPLAIN usando o índice sb1_emb
Índice MHNSW em 2 min 06 s; a consulta cai para 4,5 ms e o EXPLAIN mostra o índice.

E o filtro, a mesma “correia dentada” no grupo 0020? Na tela, voltou certo: 10 correias dentadas, com o EXPLAIN mostrando o índice mais Using where. Mas essa pergunta teve sorte. Nas 100 perguntas com o filtro, o MariaDB devolveu em média 3,3 linhas das 10, com mínimo zero e recall de 25,8%, e o tempo variou de 26 ms na mediana até 1,8 segundos nas piores. Ele faz como o pgvector sem iterative scan: anda no mapa, separa candidatos e aplica o WHERE depois; quando os candidatos não passam, ele tenta mais um pouco e desiste. Subir o mhnsw_ef_search pra 200 não ajudou (1,9 linhas em média). Pra consulta com filtro de ERP, hoje, é o ponto fraco dele.

mariadb: a consulta com filtro de grupo e D_E_L_E_T_ devolve 10 correias dentadas A-42 e o EXPLAIN mostra o índice sb1_emb com Using where
Com filtro: esta pergunta voltou certa, mas nas 100 a média foi 3,3 linhas.
Documentação do MariaDB, Vector Overview: tipo VECTOR, funções VEC_DISTANCE e o índice VECTOR INDEX com os parâmetros M e DISTANCE
A documentação do MariaDB sobre o tipo VECTOR e o índice.

SQL Server 2025 e o tipo VECTOR: aqui não fiz lab

No SQL Server 2025 eu não rodei o lab por uma questão de licença, então nada do que segue foi medido por mim. É o que a documentação da Microsoft dizia em 9 de novembro de 2026, e vale conferir na sua versão antes de contar com qualquer coisa.

O tipo VECTOR está pronto pra produção (GA, que é “disponível pra todo mundo”): guarda até 1.998 números por vetor no formato normal (float32) e até 3.996 no formato compacto (float16), sendo que o float16 ainda é preview no SQL Server 2025 instalado na sua máquina. Preview quer dizer “em teste, pode mudar, a Microsoft não recomenda pra produção”. A busca completa, com VECTOR_DISTANCE e ORDER BY, funciona sem ligar nada, igual à que eu fiz nos outros bancos sem índice. Já o índice (CREATE VECTOR INDEX, do tipo DiskANN) e a função de busca com índice (VECTOR_SEARCH) são preview no SQL Server 2025: pra usar, precisa ligar com ALTER DATABASE SCOPED CONFIGURATION SET PREVIEW_FEATURES = ON. No Azure SQL Database, que é a versão em nuvem, os dois já são GA.

Microsoft Learn, CREATE VECTOR INDEX: Feature availability dizendo que vector index e VECTOR_SEARCH são GA no Azure SQL Database e preview no SQL Server 2025 (17.x), com a configuração PREVIEW_FEATURES
A página oficial: GA no Azure SQL, preview no SQL Server 2025.

Tem mais um detalhe que a documentação deixa no meio do texto. Existe uma versão nova do índice, que aceita INSERT, UPDATE e DELETE na tabela e aplica o filtro do WHERE durante a busca; essa versão está na nuvem. A versão anterior deixa a tabela somente leitura depois que o índice é criado, e a documentação avisa que o filtro pode devolver menos linhas que o esperado, o mesmo comportamento que eu vi no pgvector. Antes de contar com índice vetorial em SQL Server instalado, olhe qual versão de índice você tem em sys.vector_indexes.

Microsoft Learn, tipo de dado vector: dimensões máximas de 1.998 para float32 e 3.996 para float16, e float16 em preview no SQL Server 2025
Limites de dimensão do tipo VECTOR no SQL Server.

Comparando os cinco: qual banco de dados vetorial faz melhor

Todos os números que apareceram acima saíram do script de medição, do jeito que ele imprimiu. É número de lab, serve pra comparar os bancos entre si na mesma máquina, não pra prometer pra ninguém.

Saída do script de medição: PostgreSQL exato 49,7 ms e recall 100%, HNSW 0,59 ms e 75,6% com ef_search 40 até 94,5% com 400, filtro com iterative scan off devolvendo 0,3 linhas em média, Oracle exato 199,6 ms, HNSW 1,36 ms e 96%, filtro 66,5 ms por full scan, MySQL 72.965 vetores trazidos em 15,8 segundos e 9,5 ms por pergunta na aplicação, MariaDB exato 87 ms, MHNSW 0,76 ms e 99%, filtro 3,3 linhas e recall 25,8%
Todas as medições, do jeito que o script imprimiu.

Lendo os números. Na busca completa, sem índice, o PostgreSQL foi quatro vezes mais rápido que a Oracle (49 ms contra 200 ms), e os dois acertam tudo, porque olham a tabela inteira. Com índice, os dois respondem em 1 ou 2 ms; o pgvector monta o índice em 10 segundos e acerta 75% no padrão, a Oracle leva quase 2 minutos pra montar e acerta 96%. Com filtro no WHERE, que é a vida real de ERP, o pgvector devolve vazio até você ligar o iterative scan, e a Oracle devolve certo mas desiste do índice e lê o grupo inteiro. O MariaDB 11.8 surpreendeu na precisão: 99% com a configuração de fábrica, em 0,8 ms, com índice montado em 2 minutos; mas com filtro no WHERE devolve em média 3 linhas das 10 e não tem o ajuste que o pgvector tem. O MySQL Community guarda o vetor, mas não compara nem indexa; a conta tem que ser feita fora, e pra 70 mil produtos ainda cabe. O SQL Server 2025 tem o tipo pronto e o índice em preview na versão instalada; a busca completa funciona hoje.

Pra fechar, um ranking dos cinco. O critério é o que cada banco entrega hoje pra busca vetorial, na versão que você instala no seu servidor, com os números deste lab e sem olhar preço. Se o preço entrar na conta, o PostgreSQL passa pra primeiro, e o MariaDB fica colado nele.

Pos.BancoBusca com índiceMontar índiceFiltro no WHERESituação hoje
1ºOracle 26aiAI Vector Search1,36 ms, recall 96%busca completa: 200 ms1 min 47 s131 MB de RAM reservada (vector pool)devolve certomas larga o índice e lê o grupo inteiro, 66 mspronto pra produçãomodelo dentro do banco e índice híbrido; precisa configurar o vector_memory_size
2ºPostgreSQL 18pgvector 0.8.70,59 ms, recall 75,6%94,5% com ef_search 400; busca completa: 49 ms10 s36 MB em discodevolve vazio no padrãoliga o iterative scan e vai a 91%pronto pra produçãogratuito; vetor gerado fora, na aplicação; ajustar ef_search e iterative_scan
3ºMariaDB 11.8MHNSW, versão gratuita0,76 ms, recall 99%busca completa: 87 ms2 min 06 síndice dentro do InnoDBdevolve 3 linhas das 10recall 26%, p95 de 1,8 s; sem ajuste equivalente ao iterative scanpronto pra produçãogratuito; vetor gerado fora; cuidado com VEC_FromText dentro do ORDER BY
4ºSQL Server 2025sem lab, só documentaçãoíndice em previewVECTOR_SEARCH pede PREVIEW_FEATURESpreviewversão antiga do índice deixa a tabela só leiturapode devolver menos linhasfiltro iterativo só na versão da nuvemtipo VECTOR pronto, índice nãobusca completa com VECTOR_DISTANCE funciona; vetor gerado fora
5ºMySQL 9.7Communitynão temDISTANCE() só no HeatWave e no MySQL AInão temnão existe índice vetorial no Communitysó na aplicação73 mil vetores em 15,8 s, depois 9,5 ms por perguntasó guarda o vetora saída é o MariaDB, logo acima

Pra um cadastro de dezenas ou centenas de milhares de itens, o banco que você já tem faz busca por sentido. PostgreSQL com pgvector é o mais barato e o mais maduro, desde que o iterative scan seja ligado antes do primeiro WHERE. Oracle 26ai é o mais completo, com o modelo dentro do banco e o índice híbrido, mas cobra em memória reservada e em tempo pra montar o índice. MariaDB é a saída natural pra quem está em MySQL e quer índice vetorial sem trocar de família, desde que a consulta com filtro seja testada antes. Um banco vetorial separado, pra esse tamanho de problema, é mais um sistema pra cuidar sem necessidade.

Quando chamar um especialista

Colocar busca vetorial no banco de produção mexe em memória (o vector pool da Oracle, a memória compartilhada e o maintenance_work_mem do PostgreSQL), em tempo pra montar índice e no jeito que a consulta com filtro se comporta, que é o caso de quase toda tela de ERP. Se você quer saber como o seu ambiente aguenta isso antes de ligar, a sustentação de banco de dados da Furushima faz esse levantamento. Pode pedir um health check ou falar com a gente pelo contato.

Imagem de capa: robôs humanoides em luta de boxe, reprodução de captura de tela publicada por O Globo em 26/05/2025.

Referências

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *