SingleStore is useful when MySQL or MariaDB starts struggling with large tables, high write volume, or more expensive analytical queries.
Setup
Setup your server with Singlestore requirements in mind.
sysctl
/etc/sysctl.conf
vm.max_map_count=1000000000
vm.min_free_kbytes=1318193
vm.swappiness=10
net.core.rmem_max=8388606
net.core.wmem_max=8388606
Apply:
sudo sysctl -p
swap
sudo dd if=/dev/zero of=/swap_file bs=64GB count=32
sudo chmod 0600 /swap_file
sudo mkswap /swap_file
sudo swapon /swap_file
Tuning
Tune the database for better performance. Focusing on targeted queries and not compactations.
Columnstore segment rows
SELECT @@columnstore_segment_rows;
SET GLOBAL columnstore_segment_rows = 16384;
Queue depth
max_connection_threads should stay 25% above the desired queue depth.
SET GLOBAL max_connection_threads = 150;
SET GLOBAL workload_management_max_queue_depth = 120;
Aggregator sync auto increment
Use AGGREGATOR SYNC AUTO_INCREMENT after imports, restores, or manual inserts when the auto increment value may be behind the highest existing row ID. It resyncs the aggregator-side counter with the current table state.
Schema Notes
This section gathers the extra SingleStore schema features that help when structuring tables and columns.
Table types
These two choices are independent from the distribution type, so they can be combined as needed.
Columnstore: good for bulk updates and deletes. Default choice.Rowstore: better for specific lookups and concurrent updates.
Distribution types
Sharded: partitioned across all nodes. Default choice.Reference: every node keeps a copy of the table. Good for more advanced joins, but uses more space.
Sparse is not a table type. It only applies the sparse property to columns.
Column properties
Sparse column
Useful when a column has many NULL or zero values. This is common in tables with many optional columns.
$table->string('name')->nullable()->sparse();
$table->sparse();
Shard key
Works similarly to an index, but should be used where the selected column contains mostly unique values.
$table->string('name')->shardKey();
$table->shardKey(['f_name', 'l_name']);
Sort key
Used with columnstore tables. It groups repeating values into segments and can make queries faster.
A common case is indexing a price column in a products table. Queries that select by price or filter with WHERE price ... can run more efficiently because the data is organized in segments.
$table->string('name')->sortKey();
$table->sortKey(['f_name', 'l_name']);
Series timestamp
This is also a special index. Only one series timestamp can be defined per table, and it is used to optimize time-based queries such as FIRST(), LAST(), and TIME_BUCKET().
$table->datetime('created_at')->seriesTimestamp();
Migration
Mysqldump
Use this when you want a portable SQL dump.
Export on the old server:
mysqldump --complete-insert --extended-insert --quick --no-create-info --ignore-table=the_database_name.migrations -u user -h 127.0.0.1 -p database_main > backup.sql
Useful flags:
--extended-insert: writesINSERTstatements using multiple-row syntax with severalVALUESlists. This makes the dump smaller and speeds up inserts when the file is reloaded.--quick: useful for large tables. It retrieves rows one at a time instead of buffering the full result set in memory before writing it.--no-create-info: skipsCREATE TABLEstatements and exports only row data. Use this when the target schema already exists.--complete-insert: writes completeINSERTstatements with column names. This is useful when fixing insert issues related to non-default values.
Transfer the dump to the new server over SSH:
rsync -avz --progress backup.sql user@192.168.100.20:/home/user/backup.sql
Import on the new server:
mysql -h 127.0.0.1 -u root -p the_database_name < /home/user/backup.sql
Backup and restore
Use this when moving a SingleStore database directly between servers.
Important note: this method speed depends on the number of partitions in the database. If the database has 8 partitions, backup can use 8 cores. If it has 32 partitions, it can use all 32 cores and run much faster.
From the source server:
mkdir /home/user/dbbackup
chmod 777 /home/user/dbbackup
BACKUP DATABASE database_main TO '/home/user/dbbackup';
sudo chmod -R 777 /home/user/dbbackup
On the destination server:
sudo apt install parallel
Standard transfer:
rsync -avz --progress user@192.168.100.10:/home/user/dbbackup/ /home/destination/dbbackup/
Parallel transfer:
mkdir /home/destination/dbbackup
ssh user@192.168.100.10 "find /home/user/dbbackup -type f" | \
parallel --tag --line-buffer -j 4 rsync -avz --progress user@192.168.100.10:"{}" /home/destination/dbbackup/
Fix permissions:
chmod -R 777 /home/destination/dbbackup
Restore:
RESTORE DATABASE database_main AS database_main_backup FROM '/home/destination/dbbackup/'
Replicate
REPLICATE DATABASE database_main FROM user:'db_password'@192.168.100.10:3306;
This method is Enterprise only.
Chunked copy script
Useful for copying large datasets in parallel inside SingleStore.
import singlestoredb as s2
from concurrent.futures import ThreadPoolExecutor
import math
chunk_size = 200000
host = '127.0.0.1'
user = 'user'
password = 'pass'
old_database = 'database_old'
new_database = 'database_new'
tables = [
'test_table',
]
def connect(database):
return s2.connect(
host=host,
port=3306,
user=user,
password=password,
database=database
)
def copy_table(table_name):
source_conn = connect(old_database)
with source_conn.cursor() as cursor:
cursor.execute(f"SELECT MIN(id), MAX(id) FROM {old_database}.{table_name}")
min_id, max_id = cursor.fetchone()
total_chunks = math.ceil((max_id - min_id) / chunk_size)
def process_chunk(chunk_num):
target_conn = connect(new_database)
start_id = min_id + (chunk_num * chunk_size)
end_id = start_id + chunk_size
with target_conn.cursor() as cursor:
query = f"""
INSERT INTO {new_database}.{table_name}
SELECT * FROM {old_database}.{table_name}
WHERE id >= {start_id} AND id < {end_id}
"""
cursor.execute(query)
target_conn.commit()
target_conn.close()
print(f"✓ {table_name}: chunk {chunk_num + 1}/{total_chunks} completed (IDs {start_id}-{end_id})")
with ThreadPoolExecutor(max_workers=8) as executor:
executor.map(process_chunk, range(total_chunks))
source_conn.close()
for table_name in tables:
copy_table(table_name)
print("Migration complete!")
Sources
- Setup
- Tuning
- Schema
- Migration
- Monitoring
Performance Cases
- Raw SQL
- Laravel Eloquent
- Laravel Pagination