Cloud Databases

Cloud SQL vs RDS vs Azure Database: Managed MySQL and PostgreSQL Compared for 2026

A detailed comparison of Google Cloud SQL, Amazon RDS, and Azure Database for MySQL and PostgreSQL — HA architecture, failover times, connection pooling, pricing for equivalent instances, and platform-specific features.

JusDB Team
March 18, 2025
Updated June 20, 2026
13 min read

Your e-commerce platform has grown to 8 million product listings. A customer types "wireless noise cancelling headphones" into the search box, and your LIKE '%wireless noise cancelling headphones%' query kicks off a full table scan — locking up your primary MySQL instance for 4.2 seconds and timing out for a third of users during peak traffic. You add ft_min_word_len tweaks and an index on the description column, but a B-tree index cannot rescue a pattern that starts with a wildcard. The fix is not more hardware or a cache layer — it is a fundamentally different indexing mechanism: MySQL's built-in FULLTEXT index combined with MATCH() AGAINST() queries, which can cut that 4.2-second scan to sub-100ms response times without leaving MySQL at all.

TL;DR
  • FULLTEXT indexes tokenize text columns into an inverted index; they support multi-column indexes across CHAR, VARCHAR, and TEXT columns.
  • MATCH(col) AGAINST('term' IN NATURAL LANGUAGE MODE) returns relevance-ranked results; IN BOOLEAN MODE supports +required, -excluded, * prefix, "" phrase, and ~ weight operators.
  • The 50% rule: a word appearing in more than 50% of rows scores 0 in natural language mode — results look blank, which is a common production gotcha.
  • InnoDB's minimum token size is controlled by innodb_ft_min_token_size (default 3); MyISAM uses ft_min_word_len (default 4) — both require an OPTIMIZE TABLE rebuild after changing.
  • FULLTEXT does not support partial-word (infix) search — head* matches "headphones" but *phones does not match anything.
  • For datasets beyond ~10 million rows with complex linguistic requirements, consider Elasticsearch or OpenSearch; for moderate scale with relevance ranking, MySQL FTS is a production-ready, zero-dependency solution.

Background

MySQL has shipped FULLTEXT search support since version 3.23 for MyISAM and extended it to InnoDB in MySQL 5.6. Unlike a standard B-tree index — which stores sorted values and excels at equality lookups and range scans — a FULLTEXT index builds an inverted index: a mapping from each significant word to the list of rows and positions where that word appears. This makes it structurally similar to what a search engine like Elasticsearch builds under the hood, just without the distributed architecture.

When MySQL parses a FULLTEXT index, it runs the column text through a tokenizer (the built-in parser splits on whitespace and punctuation), removes stopwords, discards tokens shorter than innodb_ft_min_token_size, and writes the remaining tokens into three auxiliary InnoDB tables: the index table itself, a delete table, and a deleted cache. The result is that a MATCH() AGAINST() query can jump directly to matching rows rather than scanning every value.

There are three search modes, each with distinct behaviour: natural language mode (default, relevance ranked), boolean mode (operator-driven), and query expansion mode (two-pass, broadens results). Choosing the right mode for your use case is half the battle.

Creating FULLTEXT Indexes

You can add a FULLTEXT index at table creation time or alter an existing table. Both approaches produce identical results; the ALTER TABLE path is more common for production tables where data already exists.

At CREATE TABLE time

sql
CREATE TABLE products (
  id          INT UNSIGNED NOT NULL AUTO_INCREMENT,
  title       VARCHAR(255)  NOT NULL,
  description TEXT          NOT NULL,
  brand       VARCHAR(100)  NOT NULL,
  PRIMARY KEY (id),
  FULLTEXT INDEX ft_search (title, description, brand)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

A FULLTEXT index can span multiple columns. When you later query with MATCH(title, description, brand), you must list exactly the same columns in the same order as the index definition — MySQL will not use the index otherwise.

Adding to an existing table

sql
-- Add a fulltext index on a single column
ALTER TABLE products ADD FULLTEXT INDEX ft_title (title);

-- Add a multi-column fulltext index
ALTER TABLE products ADD FULLTEXT INDEX ft_full (title, description);

-- Equivalent using CREATE INDEX syntax
CREATE FULLTEXT INDEX ft_brand ON products (brand);
Warning: ALTER TABLE rebuilds the index online

On large InnoDB tables, adding a FULLTEXT index with ALTER TABLE takes an exclusive metadata lock during the final phase and can cause brief write stalls even with ALGORITHM=INPLACE. Plan this operation during a maintenance window or use pt-online-schema-change for critical tables.

Controlling minimum token size

sql
-- Check current settings (InnoDB FTS)
SHOW VARIABLES LIKE 'innodb_ft_min_token_size';
-- Default: 3

-- Check MyISAM setting
SHOW VARIABLES LIKE 'ft_min_word_len';
-- Default: 4

-- Change in my.cnf (requires restart + OPTIMIZE TABLE)
-- [mysqld]
-- innodb_ft_min_token_size = 2

-- After restarting, rebuild the index
OPTIMIZE TABLE products;
Tip: Two-character searches

If your users search for short terms like "AI", "DB", or product codes like "M3", lower innodb_ft_min_token_size to 2 in your my.cnf and rebuild all FULLTEXT indexes. Without this change, two-character tokens are silently dropped from the index and searches for them return nothing.

MATCH AGAINST Modes

IN NATURAL LANGUAGE MODE

This is the default mode. MySQL calculates a relevance score for each row based on how frequently the search terms appear and how rare they are across the dataset (a TF-IDF-style calculation). Rows with a score of 0 are excluded from results.

sql
-- Basic relevance search — returns rows ordered by relevance
SELECT
  id,
  title,
  MATCH(title, description) AGAINST('wireless headphones') AS relevance_score
FROM products
WHERE MATCH(title, description) AGAINST('wireless headphones')
ORDER BY relevance_score DESC
LIMIT 20;

Calling MATCH() AGAINST() twice — once in WHERE and once in SELECT — does not double the work. MySQL detects the duplicate and computes the score only once.

The 50% Rule — Most Common Production Gotcha

In natural language mode, any word that appears in more than 50% of the rows in the table is treated as a stopword and assigned a relevance score of 0. On small tables (fewer than a few thousand rows) or tables with highly repetitive text, this means common words in your dataset silently return zero results. Switch to IN BOOLEAN MODE to bypass this behaviour entirely.

IN BOOLEAN MODE

Boolean mode gives you direct operator control over which terms must, must not, or optionally appear. It does not apply the 50% rule and does not sort by relevance by default (though you can still retrieve the score).

sql
-- + means the term MUST be present
-- - means the term MUST NOT be present
SELECT id, title
FROM products
WHERE MATCH(title, description) AGAINST('+wireless -wired' IN BOOLEAN MODE);

-- * is a prefix wildcard (right-side only)
-- Matches: headphone, headphones, headband
SELECT id, title
FROM products
WHERE MATCH(title, description) AGAINST('head*' IN BOOLEAN MODE);

-- Phrase search with double quotes
SELECT id, title
FROM products
WHERE MATCH(title, description) AGAINST('"noise cancelling headphones"' IN BOOLEAN MODE);

-- ~ negates the relevance weight (demotes rows, does not exclude them)
SELECT id, title,
  MATCH(title, description) AGAINST('+headphones ~cheap' IN BOOLEAN MODE) AS score
FROM products
WHERE MATCH(title, description) AGAINST('+headphones ~cheap' IN BOOLEAN MODE)
ORDER BY score DESC;

-- Combining multiple operators
SELECT id, title
FROM products
WHERE MATCH(title, description)
  AGAINST('+wireless +"noise cancelling" -earbuds brand*' IN BOOLEAN MODE);
Tip: No infix wildcard

The * wildcard only works as a suffix (prefix match). head* matches "headphones", but *phones or head*phone* are not valid syntax. For infix matching you need Elasticsearch or PostgreSQL's trigram index (pg_trgm).

WITH QUERY EXPANSION

Query expansion runs a two-pass search: first it finds the top-ranked rows using natural language mode, then it extracts the most significant words from those rows and runs a second search using the expanded term list. This broadens recall at the cost of precision.

sql
-- First pass finds rows matching 'database'
-- Second pass re-searches using words from those rows (e.g. 'MySQL', 'PostgreSQL', 'query')
SELECT id, title
FROM products
WHERE MATCH(title, description) AGAINST('database' WITH QUERY EXPANSION)
LIMIT 10;

Query expansion is best for short, ambiguous queries where users may not know the exact terminology. Avoid it for very small tables (the expansion set is noisy) or highly specific searches where precision matters more than recall.

InnoDB vs MyISAM Full-Text

MySQL 5.6 introduced FULLTEXT support in InnoDB, making MyISAM largely unnecessary for full-text workloads. The differences matter for production systems:

Feature InnoDB MyISAM
Transaction support Yes (ACID) No
Min token size variable innodb_ft_min_token_size (default 3) ft_min_word_len (default 4)
Custom stopword list innodb_ft_server_stopword_table ft_stopword_file
Index rebuild on write Incremental with auxiliary tables; synced on OPTIMIZE Immediate in-place rebuild
Row locking Row-level Table-level
Crash recovery Automatic via redo log Manual REPAIR TABLE
Recommended for new projects Yes No
Tip: Custom stopword lists in InnoDB

By default InnoDB uses a built-in stopword list (the, is, a, etc.). You can replace it with a custom table to stop irrelevant domain-specific terms from inflating or suppressing results. Set innodb_ft_server_stopword_table = 'mydb/my_stopwords' in my.cnf, populate the table with a value VARCHAR(18) column, then rebuild your FULLTEXT indexes.

Performance Tuning

How InnoDB calculates relevance

InnoDB's relevance formula is a variant of BM25 (not strict TF-IDF, but similar in spirit). For each search term, the score for a row increases with term frequency in that row and decreases as the term becomes common across all rows. The final score you see in MATCH() AGAINST() is a floating-point value — higher is better, 0 means excluded.

sql
-- Inspect relevance scores to understand ranking
SELECT
  id,
  title,
  ROUND(MATCH(title, description) AGAINST('wireless audio' IN NATURAL LANGUAGE MODE), 4) AS score
FROM products
WHERE MATCH(title, description) AGAINST('wireless audio' IN NATURAL LANGUAGE MODE)
ORDER BY score DESC
LIMIT 10;

-- Check the FTS auxiliary tables (useful for debugging)
SET GLOBAL innodb_ft_aux_table = 'mydb/products';
SELECT * FROM INFORMATION_SCHEMA.INNODB_FT_INDEX_CACHE LIMIT 20;
SELECT * FROM INFORMATION_SCHEMA.INNODB_FT_INDEX_TABLE LIMIT 20;

Using EXPLAIN with FULLTEXT queries

sql
EXPLAIN SELECT id, title
FROM products
WHERE MATCH(title, description) AGAINST('wireless headphones' IN BOOLEAN MODE)\G

-- Look for: type = fulltext, key = ft_full
-- This confirms the FULLTEXT index is being used
-- rows should be much lower than total table rows

Combining FULLTEXT with other indexes

sql
-- Filter by category first (B-tree index), then rank by FTS relevance
SELECT p.id, p.title,
  MATCH(p.title, p.description) AGAINST('wireless headphones') AS score
FROM products p
WHERE p.category_id = 42
  AND MATCH(p.title, p.description) AGAINST('wireless headphones')
ORDER BY score DESC
LIMIT 20;

-- For the above query, add a composite index to help the category filter:
-- ALTER TABLE products ADD INDEX idx_category (category_id);
-- MySQL will often use the B-tree index to narrow rows, then apply FTS scoring
Warning: FULLTEXT and OR conditions

Mixing MATCH() AGAINST() with OR conditions in the WHERE clause often forces a full table scan because MySQL cannot combine a FULLTEXT scan with a range scan via OR. Use UNION or restructure the query to use AND conditions where possible.

Managing the FTS cache and sync

sql
-- innodb_ft_cache_size controls the per-index FTS cache (default 8MB)
-- innodb_ft_total_cache_size controls the global limit (default 640MB)
-- After bulk inserts, force a sync to flush the cache to disk:
OPTIMIZE TABLE products;

-- Check pending deletes (rows deleted but not yet purged from FTS index)
SELECT COUNT(*) FROM INFORMATION_SCHEMA.INNODB_FT_DELETED;

-- Force FTS index rebuild entirely (also re-tokenizes all rows)
ALTER TABLE products DROP INDEX ft_full;
ALTER TABLE products ADD FULLTEXT INDEX ft_full (title, description);

MySQL FTS vs Elasticsearch

The right tool depends on your scale, query complexity, and operational appetite. Below is a practical comparison:

Dimension MySQL FULLTEXT LIKE / REGEXP Elasticsearch / OpenSearch PostgreSQL FTS (tsvector + GIN)
Indexing model Inverted index (token-based) No index (full scan) Inverted index (Lucene) Inverted index (tsvector + GIN)
Relevance ranking BM25-style score None BM25 (configurable) ts_rank / ts_rank_cd
Partial / infix search Prefix only (word*) Any pattern (slow) Wildcard, regex, ngram Via pg_trgm trigrams
Language analyzers Built-in parser only; no stemming N/A 50+ language analyzers; stemming, synonyms Language-specific dictionaries, Ispell
Faceting / aggregations Use GROUP BY (limited) Use GROUP BY Native, fast Limited
Operational complexity Zero (already in MySQL) Zero High (separate cluster, JVM, snapshots) Low (same PostgreSQL instance)
Horizontal scale No (single node or read replicas) No Yes (sharding, replication built-in) No (Citus for sharding)
Suitable row count Up to ~10–50M rows Up to ~100K rows Billions of documents Up to ~100M rows
ACID consistency with app data Yes (same transaction) Yes No (eventual consistency) Yes (same transaction)
Tip: When to stay with MySQL FTS

If your text search is over columns already in MySQL, your result set is in the low millions of rows, you need ACID consistency (e.g. search must reflect an uncommitted transaction), and you do not need stemming or synonym expansion, MySQL FULLTEXT is the right call. You avoid an entire infrastructure component, dual-write complexity, and eventual consistency bugs.

Warning: When to move to Elasticsearch

Once you need language-aware stemming (searching "ran" should match "running"), synonym dictionaries, faceted navigation over millions of documents, or sub-second aggregations across large datasets, MySQL FULLTEXT will begin to show its limits. At that point, look at OpenSearch vs Elasticsearch 2026 to choose your external search engine.

Key Takeaways

Key Takeaways
  • Create FULLTEXT indexes with CREATE FULLTEXT INDEX or ALTER TABLE ADD FULLTEXT; the column list in MATCH() must exactly match the index definition.
  • Use IN BOOLEAN MODE by default for production queries — it avoids the 50% rule, supports operators (+, -, *, "", ~), and gives you predictable results on small tables.
  • The 50% rule silently returns zero results in natural language mode when a word appears in more than half the rows — always test on representative data volumes, not just development seeds.
  • Lower innodb_ft_min_token_size to 2 in my.cnf if you need to search short tokens (abbreviations, product codes), then run OPTIMIZE TABLE to rebuild the index.
  • FULLTEXT does not support infix wildcards (*phones) — only suffix prefix matches (head*); for substring matching, combine MySQL FTS with a trigram approach or move to Elasticsearch.
  • For datasets up to tens of millions of rows with moderate search complexity, MySQL FULLTEXT delivers production-grade relevance ranking with zero additional infrastructure.
  • Always run EXPLAIN on FULLTEXT queries to confirm the index is being used (type: fulltext), and combine with B-tree index filters using AND (not OR) for best performance.

Working with JusDB on MySQL

Implementing MySQL FULLTEXT search correctly — tuning token sizes, managing stopword lists, combining FTS with B-tree indexes, deciding when to move to an external search engine — requires deep familiarity with InnoDB internals and production query patterns. JusDB's MySQL managed services cover the full lifecycle: schema design, index strategy, query tuning, replication, and scaling decisions.

If your application is hitting slow LIKE queries or you are evaluating whether MySQL FULLTEXT can replace a planned Elasticsearch cluster, our team can audit your current schema and query workload and give you a concrete recommendation.

Share this article

Keep reading

Migrate On-Premises SQL Server to Amazon RDS: Native Backup/Restore vs AWS DMS

A step-by-step guide to migrating an on-premises Microsoft SQL Server database to Amazon RDS for SQL Server — covering native backup/restore via S3 with the rds_restore_database stored procedure, AWS DMS full-load + CDC for near-zero downtime, option group and IAM setup, cutover, and post-migration hardening.

AWS15 minJun 2, 2026
Read

Azure SQL Managed Instance vs SQL Server on Azure VM vs Azure SQL Database

The decision matrix for the three Azure SQL flavors: eight constraints (cross-DB queries, SQL Agent, CLR, linked servers, networking, licensing…) that pick Managed Instance vs Azure SQL Database vs SQL Server on Azure VM, with a cost comparison and migration paths.

Azure14 minMay 27, 2026
Read

EC2 for Databases (2026): Graviton4, M7i, X2gd & Instance Selection Cheat Sheet

Choose the optimal EC2 instance for your database workload. Compare R, X, I, and Graviton families with real-world benchmarks and cost-performance analysis.

AWS37 minMay 13, 2026
Read