-
Movie Finder: semantic search over 1M films, inside MySQL
Type “a heist that goes wrong in a snowy town” and get back real films, with posters, in a few milliseconds. The vector search behind it runs inside MySQL, with no separate vector database.
Movie Finder is the new demo app that ships with MyVector. It searches up to 1,035,695 TMDB movies by meaning, not keywords. One docker compose up gives you the whole thing: MySQL 9.7 with the MyVector component, a loader, and a small web app.
Try it live: https://demo.myvector.online/
We built it to answer the questions people ask us most. Does vector search in MySQL hold up at a million rows? Can I mix it with ordinary WHERE filters? What happens when I insert a new row? The demo shows the answers on the page, with the SQL that ran under every result.
Live: What you can do on the page
Every feature on the page maps to a MyVector capability, and every result panel shows its SQL.
Describe a movie in your own words and get the nearest films. Ask for “a boy wizard at a school of magic” and Harry Potter comes back first.
Filter by genre, year, rating and language. These are plain SQL predicates combined with the vector search, and the page tells you which filtered-search path ran and why.
Rank by similarity alone, or with a small boost for films many people have rated, so the famous match beats an obscure one at almost the same distance. Both are ordinary ORDER BY expressions.
More like this: a film’s stored vector becomes the next query, in pure SQL.
HNSW vs exact: run both side by side and compare speed and recall.
Add a movie: a plain INSERT, searchable within about a second, with no index rebuild. (Not on live demo)
Under the hood: live index details from myvector_index_status, a bar showing where each query’s time went, and a speed-vs-recall chart that sweeps ef_search from 10 to 640 against an exact scan.
How it works
The app embeds only your query text; every movie’s vector already sits in MySQL, so a search is one SQL round trip. The movie vectors come with the dataset, made by nomic-embed-text-v1.5 from each film’s title, tagline, and overview. The app uses the same model locally on the CPU, so there is no API key, and nothing leaves your machine.
New rows need no rebuild. Adding a movie is a plain INSERT; MyVector’s binlog listener picks it up and adds it to the HNSW index, usually within a second.
The SQL
The whole index is declared in a column comment. This is the movies table’s vector column:
embedding VARBINARY(3080) COMMENT
'MYVECTOR COLUMN type=HNSW,dim=768,size=...,M=16,ef=100,dist=Cosine,online=Y,idcol=id,threads=N'
The loader inserts the rows, then builds the HNSW index with one call:
CALL mysql.myvector_index_build('movies.movies.embedding', 'id');
A search turns the query vector into a nearest-first list of ids with myvector_ann_set(), then joins back to the table. JSON_TABLE keeps the order:
SELECT m.title, myvector_distance(m.embedding, @q, 'Cosine') AS distance
FROM (SELECT myvector_ann_set('movies.movies.embedding', 'id', @q,
'nn=10,ef_search=100') AS js) src,
JSON_TABLE(src.js, '$[*]' COLUMNS (rank_no FOR ORDINALITY, id INT PATH '$')) nn
JOIN movies.movies m ON m.id = nn.id
ORDER BY nn.rank_no;
“More like this” needs no embedding at all. It reads a film’s stored vector into the query variable:
SELECT embedding INTO @q FROM movies.movies WHERE id = @movie_id;
Filters pick one of two paths. The app counts each filter on its own index and takes the smallest count as an upper bound:
50,000 matches or fewer: it passes the matching keys as the fifth argument of myvector_ann_set, so HNSW searches only among them.
More than that: it calls MYVECTOR_ANN_FILTERED, which takes the nearest candidates and keeps those that pass the filter.
Run it yourself
To run your own copy, from a clone of the repository:
cd examples/movie-finder
MOVIES=100k docker compose up # then open http://localhost:8080
The first start downloads the TMDB data, about 7 GB, once. MOVIES picks how many films to load, most-voted first: 10k, 100k (the default) or full. Measured on a 16-core Arm (Neoverse-N1) Linux host with MySQL 9.7.2:
Step10k100kfull (1,035,695)Insert7 s41 s407 sBuild the HNSW index (16 threads)29 s33 s545 sHNSW search3–5 ms4–7 ms5–20 msExact search (scans every row)30 ms280 ms3 s warmRecall@10, HNSW vs exact100%100%100%
At a million movies, HNSW answers in 5–20 ms where a full scan takes 3 seconds, with the same top 10. Recall is for the unfiltered test query; a broad genre filter (Drama) at full size gave 80% at ef_search 100, and the page lets you raise ef_search to trade time for recall. For the full profile, give Docker about 12 GB of memory and set MYSQL_BUFFER_POOL=6G.
Running it on a remote server? The page and MySQL listen on localhost only. Forward the port instead of opening it: ssh -N -L 8080:127.0.0.1:8080 <server>.
Version note: the demo uses features newer than the v1.26.9 images: filtered search, MYVECTOR_ANN_FILTERED, per-query ef_search, and online updates that survive a restart. Until the next release ships, the README shows how to build a local image from main.
Data: the movie metadata and posters come from TMDB via a Hugging Face mirror. Your machine downloads it; it is not in the repository or any image. This is a non-commercial demo, not endorsed by TMDB.
Try it and tell us
Try the live demo first. To run your own, start with MOVIES=10k for a quick first look, then go to full to watch a million rows answer in milliseconds. The demo guide has screenshots and the other demos; the Movie Finder README has the full details.
If a query surprises you, or you want a feature the page doesn’t show yet, open an issue on GitHub. A star helps other MySQL users find the project.
-
Thread Pool in Percona Server and MySQL (Part 1)
Thread Pool in Percona Server and MySQL (Part 1)
A new version of Community MySQL Server July release contains two versions 26.7.0 and 9.7.2. It is an important milestone because it finally brings a Thread Pooling feature to the community. The official MySQL Server Thread Pool plugin existed for a long time, but was available exclusively for MySQL Enterprise Edition.
Before MySQL 26.7.0 / 9.7.2 the most popular free thread pooling enabled alternatives were:
Percona Server for MySQL, which shipped Thread Pool functionality as a plugin
MariaDB with the Thread Pooling built into the server
Third-party plugins such as xiezhenye/mysql-plugin-threadpool available on GitHub
Now we will be able to see what benefits the thread pooling mechanism brings to MySQL Server and compare it with the implementation of the thread pool in Percona Server for MySQL.
Part 1 will explain the basics of thread pooling and analyze the efficiency of its implementation in Percona Server for MySQL. The performance comparison between Percona Server and MySQL Server is done in Part 2.
What is Thread Pooling and why is it needed?
Thread Pooling is a technique that reuses a fixed number of pre-created threads to handle multiple client connections and execute statements inside a database.
By default, MySQL creates a dedicated thread whenever a client connects, runs the queries, and then destroys the thread when finished. When connection counts grow very large, this default method causes performance to drop significantly because creating, destroying, and constantly switching between thousands of threads wastes system resources and causes contention.
This blog post from Vadim Tkachenko explains the working of thread pool, which gives an easy to understand similarity with the car traffic:
SimCity outages, traffic control and Thread Pool for MySQL
More information about it can be found in Percona Documentation.
WARNING: the post is several pages long because it looks into different aspects of thread pooling and compares the effect of various options. If you do not want to dive into the details, feel free to read the SPOILER below and proceed to the Conclusion section.
SPOILER: Thread Pool is not a universal database booster, which always brings performance up. Like every advanced tool it should be used with full understanding of the goals and objectives. Otherwise the results might be far from perfect.
How it works
Thread Groups: The pool divides threads into distinct groups that are assigned to a specific CPU core. For this reason the number of groups often matches the number of CPU cores.
Round-Robin Assignment: As client connections come in, they are distributed evenly across these groups.
Listening and Queuing: Inside each group, a listener thread monitors incoming queries and places them into either a high-priority queue (such as queries within an active transaction) or a normal/low-priority queue.
Execution and Reuse: Worker threads pick up queries from the queues (handling high-priority items first), execute them, and then wait to handle the next request instead of shutting down.
See also:
Core concepts and usage examples
Priority connection scheduling
Thread Pool configuration:
In this research we will be sweeping across the range of various parameters of the Thread Pool and see how the performance of the server is affected.
The base line will be the server configuration when the thread pool is not enabled. This is done by setting thread_handling=one-thread-per-connection. When the pooling is enabled (thread_handling=pool-of-threads) the benchmark will set different values for the following variables that control the thread pool behavior:
Name
Sweep range
Description
thread_pool_size
10 / 20 / 40 / 80 / 120 / 160
Number of thread groups in the pool.
NOTE: thread_pool_size=5 was also tested, but the performance was unsatisfactory, for that reason it is not included in the report.
thread_pool_max_threads /
thread_pool_max_active_query_threads
12000
Maximum number of threads in the pool.
thread_pool_oversubscribe /
thread_pool_query_threads_per_group
2 / 3 / 4
Number of threads that can be active at the same time within the same group.
Other Configuration and Methodology
The configuration was as follows:
Benchmark
Sysbench OLTP Read-Write
CPU
Intel Xeon Gold 6230 (2×20 cores, HT = 80 logical CPUs)
RAM
187 GiB DDR4
Storage
NVMe SSD (2.9 TB) INTEL SSDPE2KE032T8
OS
Ubuntu 24.04, kernel 6.8.0-60-generic
DB Engines
Percona Server for MySQL 9.7.1-1 (release build)
MySQL Server 26.7.0 (release build)
Additional dimensions for the benchmark:
Database Sizes (Row Number)
24Gb (100M rows)
Number of tables in DB Schema
20 (this number is constant for all runs)
Database Schema definition can be downloaded from here:
https://percona-lab-results.github.io/2026-interactive-metrics/schema_dump.sql
Number of concurrent threads
40 / 80 / 120 / 160 / 320 / 640 / 1280 / 2560 / 5120
Buffer to Data Ratio
1:12 (I/O bound), 1:2 (Partially buffered), 1:1 (Fully buffered)
Execution of the benchmarks was done as follows:
Ramp-up
48G – 600 sec (10 min)
The Ramp-up times were established experimentally depending on the Data Size until the point when increasing them further did not bring significant changes.
Measurement window
900 sec (15 min)
Ideally it should be as long as possible, but measurements should take reasonable time. Hence, we used the experience of previous benchmarks and established that this window is adequate for the purpose.
Number of runs
1
Could be more runs, but testing took a long time.
Important Database Configuration options (the actual config files with specific settings for each run can be downloaded from the interactive graphs):
InnoDB – Buffer pool Tier
innodb_buffer_pool_size
2G/12G/32G
innodb_buffer_pool_load_at_startup
OFF
innodb_buffer_pool_dump_at_shutdown
OFF
Threading
thread_stack
512K
thread_cache_size
256
back_log
4096
InnoDB I/O
innodb_io_capacity
10000
innodb_io_capacity_max
20000
innodb_read_io_threads
16
innodb_write_io_threads
16
innodb_use_native_aio
ON
InnoDB Log / Durability
innodb_log_buffer_size
256M
innodb_flush_log_at_trx_commit
1 # full ACID
innodb_doublewrite
ON
InnoDB – Concurrency & OLTP Tuning
innodb_stats_on_metadata
OFF
innodb_open_files
65536
innodb_lock_wait_timeout
50
innodb_rollback_on_timeout
ON
Per-Session Buffers
sort_buffer_size
4M
join_buffer_size
4M
read_buffer_size
2M
read_rnd_buffer_size
4M
tmp_table_size
256M
max_heap_table_size
256M
Binary Log
disable_log_bin
ON # Disabled binlog
Other InnoDB settings
innodb_redo_log_capacity
4G
innodb_change_buffering
none
innodb_flush_method
O_DIRECT
innodb_buffer_pool_instances
Calculated as
(innodb_buffer_pool_size G / 5)
But must be in range [1..8]
Misc server settings
collation_server
utf8mb4_unicode_ci
bulk_insert_buffer_size
256M
myisam_sort_buffer_size
128M
key_buffer_size
64M # MyISAM only, keep small for OLTP
Results
The scope of results is broad and therefore the results presentation will be divided into the sections.
Percona Server 9.7.1-1 with and without thread pool:
The most impressive effect of the thread pool can be illustrated on the following graph where the level of oversubscription is 4 (os4 in the legend) and the number of thread groups is 10 (TP 10 in the legend).
Graph 1.0 – best effect of thread pooling [ INTERACTIVE GRAPH ][ TABLE ]
The green line (thread_pool_size=10/thread_pool_oversubscribe=4) is closely followed by the blue line, which corresponds to the number of groups 20 and the oversubscription 2.
As the graph shows – the effect of the thread pool on the performance in the low thread count (40/80) is relatively small. A notable effect starts to show at 120 threads when non-pooled configuration TPS plunges down. It is good to keep in mind that the thread pooling mechanism is not a universal performance booster. Though, it helps to avoid system thrashing when the number of threads is much larger than CPU cores can process.
With 2560 connections the best configuration with thread pool shows excellent efficiency of 2304 TPS against 129 TPS without the thread pool, which is 17.9 times faster. Some thread-pooled configurations are more efficient than the other ones. The combination thread_pool_size=20 with thread_pool_oversubscribe=4 fails to prevent sharp TPS drop at 120 client connections, though its shape improves as the number of connections increases.The drop from the maximum of 2485 TPS at 640 connections to 2304 TPS at 2560 connections is visible (7.5%), but not critical.
Interesting that the optimal number of groups is smaller than the number of physical CPU cores (40).
Another important observation is that in this test increase of the number of thread groups causes the TPS performance to drop: the curve with thread_pool_size=20 is below thread_pool_size=10 and thread_pool_size=40/80/120/160 are even lower.
Graph 1.1 – thread pool size variations [ INTERACTIVE GRAPH ][ TABLE ]
The next graph shows how TPS changes when we consider the optimal value for thread_pool_size=10, but change thread_pool_oversubscribe.
Graph 2.0 – oversubscription variations [ INTERACTIVE GRAPH ][ TABLE ]
With the best configuration (thread_pool_oversubscribe=4) having 2304 TPS and the least performing pooling configuration (thread_pool_oversubscribe=2) with 2043 TPS the speed difference is 11%. So, a non-optimal oversubscription value can incur a significant penalty.
The above two cases correspond to the I/O bound scenario when the data size (24G) is much larger than the server buffer size (2G).
Let us see how the buffer pool efficiency changes as innodb_buffer_pool_size increased to 12G, which allows buffering approximately half of the data set.
Graph 3.0 – partially buffered data set (12G buffer) [ INTERACTIVE GRAPH ][ TABLE ]
This time thread_pool_size=20 is the optimal value for the data size (24G) and the allocated server buffer size (12G). The non-optimal oversubscription values have a similar effect (look at INTERACTIVE GRAPH) on the performance as in Graph 2.0.
When the server configuration moves to the fully buffered data set the distribution of performance is changed:
Graph 4.0 – fully buffered data set (32G buffer) [ INTERACTIVE GRAPH ][ TABLE ]
The configuration without the thread pool wins in all number of client connections in this test. The largest deviation from the base line is observed around 80 to 640 connections, but then the performance converses to around 16K TPS for all configurations. The configuration with thread_pool_size=10 is now the slowest one and larger thread pool sizes perform better.
Varying oversubscription did not have a notable effect with fully buffered data.
NOTE: It would be interesting to see if thread pool wins with 5K or 10K connections. We might do another research devoted to extremely high thread count, but this post describes the efficiency of the thread pool across the low (40) to the high (2560) number of client connection threads.
Despite not having superior performance with regards to TPS with the fully buffered data set, the thread pool is still beneficial with regards to predictability of the server behavior. What I mean by this is most of the clients (95%) connected to the server have their transactions executed quickly, but a minority (5%) have to wait much longer. The response latency for the unlucky 5% might be out of the acceptable range. The long waiting clients do not care about other clients whose data was quickly processed. Therefore, the raw TPS does not hold much value for them.
Graph 5.0 – p95 latency [ INTERACTIVE GRAPH ][ TABLE ]
Unlike TPS when the higher value is better, the p95 Latency should be kept as low as possible.
Up to 1280 client connections the p95 latency was grouping around roughly the same numbers. However, for 2560 connections it changed dramatically. The configuration with disabled thread pooling suddenly demonstrated much longer p95 than the pooled counterparts.
The 5% of clients will have to wait longer than 787 ms if the thread pool is not used. The pooled configurations keep a much tighter range of 235-297 ms. The system performs as well as its slowest/weakest link and if a client application performs several transactions it is more likely to end up waiting significantly longer. For some outliers the latency reached 2000 ms.
Conclusion
Thread pooling does not bring any gains and even causes a small slow down on a low number of server connections.
The most visible effect of thread pooling can be seen in the scenario when I/O is one of the factors that limit performance.
Careful tuning of thread pool options is required to get the best performance. Even a seemingly small variation from optimal parameter values can cause large performance drops.
However, when properly configured, it can show spectacular results with almost 18 times TPS boost compared to the same server running without thread pool.
For system stability we should not only consider the raw TPS numbers, but also the latency related to processing of some transactions. Even if no TPS gain is obtained, the thread pool allows to minimize the waiting time for slowest transactions.
The post Thread Pool in Percona Server and MySQL (Part 1) appeared first on Percona.
-
How Nutanix Database Service Handles Oracle Database Patching: An Out-of-Place, Automated, and Reversible Workflow
Oracle patching carries operational risk. The traditional approach, manual in-place updates, leaves limited margin for error. One misstep in an Oracle software home can mean hours of unplanned downtime and a difficult root-cause conversation afterward.
-
MySQL Community Meetups Fall 2026
In our recent Where can you find MySQL from August through October 2026?, we shared a broad view of MySQL conferences, community activities, and user group meetups. Since then, community gatherings in Denver, Taipei, and Nashville have brought MySQL users, developers, and contributors together for technical discussion and local connection. We are continuing that momentum […]
-
Facebook Status Box using Node.js and MySQL
A status with angle brackets appears as text in the feed rather than turning into page markup. I appreciate that line breaks also stay visible, so a longer update reads more like what was typed. A submitted status gives you a concrete path into the feed. See how the app saves that update so the […]
|