Laravel News Links
A first look at MySQL 26.7 Early Access
https://ronaldbradford.com/images/blog/mysql-26-7-early-access.png
MySQL has dropped its newest release
, categorized as “Early Access” and available at https://labs.mysql.com/
.
While this post is not going to go into depth, I wanted to at least validate the management changes you verify between normal MySQL upgrades.
The new release version is `26.7`, the first version with a date-based release number convention. While this is numerically increased, the first problem I found was a character ordering problem not found in prior scripts. This is because 2 is before 9, historically for any version, the versions listed alphabetically would always list older before newer. MySQL isn’t the only product that uses this naming convention, or the first to switch from one to the other, however some random MySQL script on some customer installation is going to have some small issue and there will be the unnecessary followup flamewar. See later as to why I mentioned this.
Docker Containers Setup
Preamble
TMP_DIR=${TMP_DIR:-/tmp}
TEST_CASE="first-look"
rm ${TMP_DIR}/${TEST_CASE}.*
Install MySQL 9.7
The following will install the current MySQL 9.7 docker version.
REGISTRY_NAME="container-registry.oracle.com/mysql/community-server:9.7"
CONTAINER_NAME="mysql097"
docker pull ${REGISTRY_NAME}
MYSQL_PASSWD="M#$(date | md5sum - | cut -c-20)"
OUTPUT=$(docker run -d --name ${CONTAINER_NAME} \
--platform linux/amd64 \
-e MYSQL_ROOT_PASSWORD=${MYSQL_PASSWD} \
${REGISTRY_NAME})
echo $?
echo $OUTPUT
# Not completely accurate
# docker logs ${CONTAINER_NAME} | grep "ready for connections"
sleep 5
docker exec -it ${CONTAINER_NAME} mysql -uroot -p${MYSQL_PASSWD} -sN -e "SELECT VERSION()" | grep -v "can be insecure"
docker stop ${CONTAINER_NAME} && docker rm ${CONTAINER_NAME}
Install MySQL 26.7 EA
In a separate terminal window run.
# See https://labs.mysql.com/ to obtain valid and current direct download links
wget https://downloads.mysql.com/snapshots/pb/mysql-26.7.0-labs-release/mysql-community-server-26.7.0-labs-docker-el9-x86_64.tar.gz
docker load -i mysql-community-server-26.7.0-labs-docker-el9-x86_64.tar.gz
REGISTRY_NAME="localhost/mysql/community-server:26.7.0"
CONTAINER_NAME="mysql267"
...
New Variables
The 26.7 Release Notes
are not accompanied by a current reference Manual (e.g. 9.7
) so this default checking gives a first approximate look.
What it doesn’t find is for example the setup for a replica, or different distros, or following the installation of any components in which there is noted new work.
# Run for both containers
$ docker exec -it ${CONTAINER_NAME} mysql -uroot -p${MYSQL_PASSWD} -s -e "SELECT VERSION(); SELECT VARIABLE_NAME FROM performance_schema.global_variables ORDER BY 1" > ${TMP_DIR}/${TEST_CASE}.variables.${CONTAINER_NAME}.txt
$ diff -y --suppress-common-lines ${TMP_DIR}/${TEST_CASE}.variables.*.txt
9.7.1 | 26.7.0-er
> admin_force_pqc
> admin_tls_kex
> admin_use_pqc_sign
> force_pqc
> innodb_autoinc_preallocate
> mysqlx_force_pqc
> mysqlx_tls_kex
> mysqlx_use_pqc_sign
> replication_force_pqc
> replication_tls_kex
> replication_use_pqc_sign
> tls_kex
> use_pqc_sign
One of my primary goals in further evaluation is the Post-quantum cryptography support with OpenSSL 3.5. You can find some intro information in my post Q Day Is Coming: A Plain-English Guide to Post-Quantum Cryptography
. Almost all these variables align with this functionality. The kex variables is the standard abbreviation for key exchange. The pqc variables are related to the negotiation of a PQC (or Hybrid) group. As you can see in the next section, all pqc related variables are OFF.
Based on this information, without looking into source, the force_pqc, tls_kex and use_pqc_sign would be the required three variables to enable post quantum key exchange.
Variable Values
$ docker exec -it ${CONTAINER_NAME} mysql -uroot -p${MYSQL_PASSWD} -s -e "SELECT VERSION(); SELECT VARIABLE_NAME, VARIABLE_VALUE FROM performance_schema.global_variables ORDER BY 1" > ${TMP_DIR}/${TEST_CASE}.var-value.${CONTAINER_NAME}.txt
$ diff -y --suppress-common-lines ${TMP_DIR}/${TEST_CASE}.var-value.*.txt | grep ">" | cut -d'>' -f2-
admin_force_pqc OFF
admin_tls_kex
admin_use_pqc_sign OFF
force_pqc OFF
innodb_autoinc_preallocate 50
mysqlx_force_pqc OFF
mysqlx_tls_kex
mysqlx_use_pqc_sign OFF
replication_force_pqc OFF
replication_tls_kex
replication_use_pqc_sign OFF
tls_kex
There would appear to be no changes to default values for existing variables, however this is just the sandbox docker installation.
Alphabetical and Chronological
I mentioned earlier about chronological and alphabetical issues. This is what I mean. My line of code was to diff mysql97 and mysql267 where previously these would wash out alphabetically, but this does not happen now (i.e. historically it would be mysql8, mysql84, mysql97). mysql267 preceeds mysql97, and hence why the container for simplicity of commands is called mysql097.
$ diff -y --suppress-common-lines ${TMP_DIR}/${TEST_CASE}.variables.*.txt
26.7.0-er | 9.7.1
admin_force_pqc <
admin_tls_kex <
admin_use_pqc_sign <
force_pqc <
innodb_autoinc_preallocate <
mysqlx_force_pqc <
mysqlx_tls_kex <
mysqlx_use_pqc_sign <
replication_force_pqc <
replication_tls_kex <
replication_use_pqc_sign <
tls_kex <
use_pqc_sign <
New Status Variables
$ docker exec -it ${CONTAINER_NAME} mysql -uroot -p${MYSQL_PASSWD} -s -e "SELECT VERSION(); SELECT VARIABLE_NAME FROM performance_schema.global_status ORDER BY 1" > ${TMP_DIR}/${TEST_CASE}.status.${CONTAINER_NAME}.txt
$ diff -y --suppress-common-lines ${TMP_DIR}/${TEST_CASE}.status.*.txt
9.7.1 | 26.7.0-er
> Mysqlx_force_pqc
> Mysqlx_tls_kex
> Mysqlx_use_pqc_sign
There would appear to be three new status variables related only to the mysqlx access of the post quantum support. Nothing for admin or replication, however again this is a primary and not a replica.
Status Values
Looking at values on two different systems is more complicated for many factors, however a quick scan reveals no obvious changes in volumes or measure.
$ diff -y --suppress-common-lines ${TMP_DIR}/${TEST_CASE}.status-value.*.txt
9.7.1 | 26.7.0-er
Bytes_received 2115 | Bytes_received 3343
Bytes_sent 52456 | Bytes_sent 90408
Caching_sha2_password_rsa_public_key -----BEGIN PUBLIC KEY | Caching_sha2_password_rsa_public_key -----BEGIN PUBLIC KEY
Connections 14 | Connections 17
Error_log_buffered_bytes 1224 | Error_log_buffered_bytes 1752
Error_log_buffered_events 10 | Error_log_buffered_events 12
Error_log_latest_write 1784693965980559 | Error_log_latest_write 1784693440620348
Handler_commit 592 | Handler_commit 596
Handler_external_lock 6477 | Handler_external_lock 6489
Handler_read_key 1752 | Handler_read_key 1756
Handler_read_next 4160 | Handler_read_next 4166
Handler_read_rnd_next 1768 | Handler_read_rnd_next 3087
Innodb_buffer_pool_bytes_data 19775488 | Innodb_buffer_pool_bytes_data 19906560
Innodb_buffer_pool_load_status Buffer pool(s) load completed | Innodb_buffer_pool_load_status Buffer pool(s) load completed
Innodb_buffer_pool_pages_data 1207 | Innodb_buffer_pool_pages_data 1215
Innodb_buffer_pool_pages_flushed 199 | Innodb_buffer_pool_pages_flushed 198
Innodb_buffer_pool_pages_free 6985 | Innodb_buffer_pool_pages_free 6977
Innodb_buffer_pool_read_requests 16159 | Innodb_buffer_pool_read_requests 16200
Innodb_buffer_pool_reads 1063 | Innodb_buffer_pool_reads 1070
Innodb_buffer_pool_write_requests 1971 | Innodb_buffer_pool_write_requests 1993
Innodb_data_fsyncs 75 | Innodb_data_fsyncs 69
Innodb_data_read 17485312 | Innodb_data_read 17691136
Innodb_data_reads 1089 | Innodb_data_reads 1098
Innodb_data_writes 258 | Innodb_data_writes 253
Innodb_data_written 3336704 | Innodb_data_written 3312640
Innodb_dblwr_pages_written 58 | Innodb_dblwr_pages_written 57
Innodb_dblwr_writes 4 | Innodb_dblwr_writes 3
Innodb_log_write_requests 841 | Innodb_log_write_requests 840
Innodb_log_writes 20 | Innodb_log_writes 18
Innodb_os_log_fsyncs 16 | Innodb_os_log_fsyncs 12
Innodb_os_log_written 53760 | Innodb_os_log_written 52736
Innodb_pages_created 145 | Innodb_pages_created 146
Innodb_pages_read 1062 | Innodb_pages_read 1069
Innodb_pages_written 199 | Innodb_pages_written 198
Innodb_redo_log_checkpoint_lsn 29775665 | Innodb_redo_log_checkpoint_lsn 30006513
Innodb_redo_log_current_lsn 29775665 | Innodb_redo_log_current_lsn 30006513
Innodb_redo_log_flushed_to_disk_lsn 29775665 | Innodb_redo_log_flushed_to_disk_lsn 30006513
Innodb_redo_log_uuid 1560291093 | Innodb_redo_log_uuid 2583501521
Innodb_system_rows_read 4888 | Innodb_system_rows_read 4894
Max_used_connections_time 2026-07-22 04:19:37 | Max_used_connections_time 2026-07-22 04:10:50
> Mysqlx_force_pqc OFF
Mysqlx_ssl_server_not_after Jul 19 04:19:20 2036 GMT | Mysqlx_ssl_server_not_after Jul 19 04:10:35 2036 GMT
Mysqlx_ssl_server_not_before Jul 22 04:19:20 2026 GMT | Mysqlx_ssl_server_not_before Jul 22 04:10:35 2026 GMT
> Mysqlx_tls_kex
> Mysqlx_use_pqc_sign OFF
Open_tables 74 | Open_tables 63
Opened_tables 155 | Opened_tables 144
option_tracker_usage:Traditional Optimizer 15 | option_tracker_usage:Traditional Optimizer 23
Performance_schema_session_connect_attrs_longest_seen 112 | Performance_schema_session_connect_attrs_longest_seen 116
Queries 28 | Queries 44
Questions 27 | Questions 43
Rsa_public_key -----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9 | Rsa_public_key -----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9
Select_scan 4 | Select_scan 6
Sort_rows 1604 | Sort_rows 2921
Sort_scan 3 | Sort_scan 5
Ssl_server_not_after Jul 19 04:19:20 2036 GMT | Ssl_server_not_after Jul 19 04:10:35 2036 GMT
Ssl_server_not_before Jul 22 04:19:20 2026 GMT | Ssl_server_not_before Jul 22 04:10:35 2026 GMT
Table_locks_immediate 4 | Table_locks_immediate 6
Table_open_cache_hits 3084 | Table_open_cache_hits 3101
Table_open_cache_misses 155 | Table_open_cache_misses 144
Tls_library_version OpenSSL 3.5.1 1 Jul 2025 | Tls_library_version OpenSSL 3.5.5 27 Jan 2026
Uptime 7128 | Uptime 7658
Uptime_since_flush_status 7128 | Uptime_since_flush_status 7658
This is not an exhaustive comparison, however there appears to be no new table objects. There are 3 new replication applier related columns which align with the release notes.
$ docker exec -it ${CONTAINER_NAME} mysql -uroot -p${MYSQL_PASSWD} -s -e "SELECT VERSION(); SELECT TABLE_SCHEMA, TABLE_NAME FROM information_schema.tables ORDER BY 1,2" > ${TMP_DIR}/${TEST_CASE}.tables.${CONTAINER_NAME}.txt
$ diff -y --suppress-common-lines ${TMP_DIR}/${TEST_CASE}.tables.txt
docker exec -it ${CONTAINER_NAME} mysql -uroot -p${MYSQL_PASSWD} -s -e "SELECT VERSION(); SELECT TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME, ORDINAL_POSITION FROM information_schema.columns ORDER BY 1,2,4" > ${TMP_DIR}/${TEST_CASE}.columns.${CONTAINER_NAME}.txt
$ diff -y -W 400 --suppress-common-lines ${TMP_DIR}/${TEST_CASE}.columns.*.txt
9.7.1 | 26.7.0-er
> mysql slave_relay_log_info Applier_version 16
> mysql slave_relay_log_info Applier_worker_count 17
> mysql slave_relay_log_info Applier_event_memory_limit 18
> performance_schema replication_applier_configuration APPLIER_VERSION 8
> performance_schema replication_applier_configuration APPLIER_WORKER_COUNT 9
> performance_schema replication_applier_configuration APPLIER_EVENT_MEMORY_LIMIT 10
Plugins
There are no additional plugins found by comparing via SHOW PLUGINS, however the release notes reference the previously enterprise feature of Thread Pool Plugin now in MySQL Community Server. There are no installed components by default in the docker mode for either version.
Thread Pool
The use of the Thread Pool plugin requires a MySQL configuration modification for the mysqld process, which requires a restart.
$ OUTPUT=$(docker run -d --name ${CONTAINER_NAME} \
--platform linux/amd64 \
-e MYSQL_ROOT_PASSWORD=${MYSQL_PASSWD} \
${REGISTRY_NAME} \
--plugin-load-add=thread_pool.so)
Thread Pool Logs
Following the installation instructions for the Thread Pool
for MySQL Enterprise you can see this plugin in 26.7.
$ docker logs ${CONTAINER_NAME}
...
2026-07-22T06:58:28.845787Z 0 [System] [MY-013852] [Server] Thread pool plugin started successfully with parameters: thread_pool_size = 1, thread_pool_algorithm = High Concurrency Algorithm, thread_pool_stall_limit = 6, thread_pool_prio_kickup_timer = 1000, thread_pool_max_unused_threads = 32, thread_pool_max_active_query_threads = 0, thread_pool_dedicated_listeners = 0, thread_pool_max_transactions_limit = 32, thread_pool_transaction_delay = 0, thread_pool_query_threads_per_group = 2, thread_pool_connection_report_interval = 120, thread_pool_longrun_trx_limit = 2000 ; 2000
Thread Pool Plugins
There are now 4 additional plugins in 26.7 with the thread pool plugin enabled.
mysql> SHOW PLUGINS;
+----------------------------------+----------+--------------------+----------------+-------------+
| Name | Status | Type | Library | License |
+----------------------------------+----------+--------------------+----------------+-------------+
...
| thread_pool | ACTIVE | DAEMON | thread_pool.so | PROPRIETARY |
| TP_THREAD_STATE | ACTIVE | INFORMATION SCHEMA | thread_pool.so | PROPRIETARY |
| TP_THREAD_GROUP_STATE | ACTIVE | INFORMATION SCHEMA | thread_pool.so | PROPRIETARY |
| TP_THREAD_GROUP_STATS | ACTIVE | INFORMATION SCHEMA | thread_pool.so | PROPRIETARY |
+----------------------------------+----------+--------------------+----------------+-------------+
50 rows in set (0.023 sec)
Thread Pool Tables
Note, the Enterprise 9.7 documentation lists only 3 matching tables, tp_connections appears to be new.
mysql> SELECT TABLE_NAME
-> FROM INFORMATION_SCHEMA.TABLES
-> WHERE TABLE_SCHEMA = 'performance_schema'
-> AND TABLE_NAME LIKE 'tp%';
+-----------------------+
| TABLE_NAME |
+-----------------------+
| tp_connections |
| tp_thread_group_state |
| tp_thread_group_stats |
| tp_thread_state |
+-----------------------+
4 rows in set (0.075 sec)
Conclusion
This is a quick validation I can install and validate a running MySQL 26.7 sandbox. I will be following up in future posts with additional findings.
Planet for the MySQL Community
Wirechat 0.6 adds content browsing, chat tabs, and group join requests
https://corepine.dev/assets/wirechat/preview-light.webp
Release Notes
Wirechat 0.6x expands the panel API with more opt-in chat experiences: settings, message requests, group invite links, join-request moderation, content browsing, tabs, tray support, and configurable models for the new records.
From 0.5x To 0.6x
Upgrading to 0.6x should be a small migration for most applications. The main work is updating the package, publishing the new migrations, and enabling the panel features your application needs.
Minimal Breaking Changes
Clear and delete actions are now opt-in panel features:
clearChatAction()is disabled by default.deleteChatAction()is disabled by default.
Wirechat also uses ColorTone::Soft by default in 0.6x. This is a visual breaking change: outgoing messages and related primary surfaces now use a softer tinted treatment instead of the stronger primary-color surface used before.
To keep the older stronger look, set the panel color tone to ColorTone::Solid:
use Wirechat\Wirechat\Enums\ColorTone;
use Wirechat\Wirechat\Panel;
public function panel(Panel $panel): Panel
{
return $panel
// ...
->colorTone(ColorTone::Solid);
}
If your application already shows these actions and you want to keep them visible, enable them in your panel provider:
use Wirechat\Wirechat\Panel;
public function panel(Panel $panel): Panel
{
return $panel
// ...
->clearChatAction()
->deleteChatAction();
}
Create a working branch in your application before upgrading:
git checkout -b wirechat-upgrade
Update your Composer constraint:
"wirechat/wirechat": "^0.6"
Then update the package:
composer update wirechat/wirechat
Publish the new migrations and run them:
php artisan vendor:publish --tag=wirechat-migrations
php artisan migrate
Clear compiled views after upgrading:
php artisan optimize:clear
If you have published Wirechat views, compare your local copies with the new package views before replacing them:
What Is New
Settings Drawer
Wirechat 0.6x introduces an opt-in settings drawer for each panel. When enabled, users get a Settings entry in the chats header. The first built-in section is notification preferences.
use Wirechat\Wirechat\Panel;
public function panel(Panel $panel): Panel
{
return $panel
// ...
->settings();
}
The settings system introduces the wirechat_settings table and a DTO-based API for reading user preferences:
use Wirechat\Wirechat\Facades\Wirechat;
$settings = Wirechat::settings($user);
if ($settings->notification_previews_enabled) {
// Show notification preview text.
}
Settings are disabled by default. You may also enable them conditionally:
public function panel(Panel $panel): Panel
{
return $panel
// ...
->settings(fn () => auth()->user()?->can('manage-chat-settings') ?? false);
}
Read more on the Settings page.
Message Requests
Message requests let a sender start a private conversation without immediately adding the recipient as a participant. The recipient can open the pending thread, review it, and then accept or dismiss the request.
use Wirechat\Wirechat\Panel;
public function panel(Panel $panel): Panel
{
return $panel
// ...
->messageRequests();
}
When enabled, the default new-chat UI creates review-first conversations. The sender can continue writing while the recipient sees an incoming request flow.
You can also create a request programmatically:
$conversation = auth()->user()->sendMessageRequestTo($recipient);
Accepting a request adds the recipient to the conversation. Dismissing it keeps the recipient out of the participant list and removes the pending request from the active flow.
Read more on the Message Requests page.
Group Invitations
Group invitations add shareable invite links, public invite preview pages, and in-app join handling for group conversations. They are enabled per panel:
use Wirechat\Wirechat\Panel;
public function panel(Panel $panel): Panel
{
return $panel
// ...
->groupInvitations();
}
When enabled, owners and admins can manage invite links from group tools. Invite links can join a user immediately or create a join request, depending on the group access settings.
For public invite pages, you can choose the layout used outside the authenticated chat shell:
public function panel(Panel $panel): Panel
{
return $panel
// ...
->groupInvitations()
->invitePageLayout('wirechat::layouts.app');
}
If your chat runs as a widget with full chat routes disabled, point public invite hand-offs back to the page that renders the widget:
public function panel(Panel $panel): Panel
{
return $panel
// ...
->groupInvitations()
->registerRoutes(false)
->mountUrl(fn () => route('wirechat.widget'));
}
Read more on the Groups page.
Join Requests
Join requests are the moderation layer behind approval-based group invites. A valid invite does not always mean immediate membership. If the group requires approval, Wirechat creates a JoinRequest record instead.
$request = $conversation->group->requestToJoin($user, $invite);
$conversation->group->acceptPendingJoinRequest(
$user,
reviewedBy: $admin,
markInviteUsed: true,
);
$conversation->group->dismissPendingJoinRequest(
$user,
reviewedBy: $admin,
);
This introduces a safer group-join lifecycle:
- duplicate pending requests are reused instead of recreated
- accepted requests store reviewer metadata
- dismissed requests remain auditable
- invite usage is only counted when the join is accepted
Read more on the Groups page.
Content Viewer
Content Viewer gives a conversation a dedicated place to browse shared media, documents, and links without leaving the chat interface.
use Wirechat\Wirechat\Panel;
public function panel(Panel $panel): Panel
{
return $panel
// ...
->contentViewer();
}
When enabled, Wirechat adds a content entry to the conversation details panel. Users can browse media, docs, and links from the current conversation, then jump back to the original message context.
Read more on the Content Viewer page.
Conversation Tabs
Tabs let you split the chats list into focused views such as All, Unread, or Groups.
use Illuminate\Database\Eloquent\Builder;
use Wirechat\Wirechat\Enums\ConversationType;
use Wirechat\Wirechat\Panel;
use Wirechat\Wirechat\Support\Tabs\Tab;
public function panel(Panel $panel): Panel
{
return $panel
// ...
->tabs(
Tab::make('all'),
Tab::make('groups')
->label('Groups')
->query(fn (Builder $query) => $query->where('type', ConversationType::GROUP->value))
->count(),
)
->defaultTab('all');
}
Tabs refine the existing conversations query for the current user. Use them to keep large chat lists easier to scan without building a separate chats UI.
Read more on the Tabs page.
The tray widget adds a compact floating chat entry point that can live in your authenticated layout. It keeps chat available without forcing users to leave the page they are using.
<html>
<head>
@wirechatStyles
</head>
<body>
...
@wirechatAssets()
@auth
<livewire:wirechat.tray panel="chats" />
@endauth
</body>
</html>
The tray uses the selected panel, so it follows that panel’s routes, middleware, actions, tabs, attachments, and other feature settings.
You can customize the tray launcher and opened panel:
<livewire:wirechat.tray
panel="chats"
:limit="8"
class="bottom-3 right-3 w-[28rem]"
launcherClass="rounded-full px-4 py-2 shadow-lg"
heading="Inbox"
/>
Read more on the Tray page.
Notification Preferences
Web push notifications now work with user-owned notification settings when the settings drawer is enabled. Realtime broadcasts still update unread counts and chat lists, but browser notifications respect these preferences:
notifications_enableddirect_message_notifications_enabledgroup_message_notifications_enablednotification_previews_enabled
Enable web push notifications on the panel:
use Wirechat\Wirechat\Panel;
public function panel(Panel $panel): Panel
{
return $panel
// ...
->webPushNotifications();
}
Use the same settings DTO in custom notification code:
use Wirechat\Wirechat\Facades\Wirechat;
$settings = Wirechat::settings($recipient);
$canNotify = $settings->notifications_enabled
&& ($conversation->isGroup()
? $settings->group_message_notifications_enabled
: $settings->direct_message_notifications_enabled);
Read more on the Notifications page.
Configurable Models
The config model map now includes the new records used by settings, message requests, group invites, and join-request flows:
'models' => [
'invite' => \Wirechat\Wirechat\Models\Invite::class,
'join_request' => \Wirechat\Wirechat\Models\JoinRequest::class,
'message_request' => \Wirechat\Wirechat\Models\MessageRequest::class,
'setting' => \Wirechat\Wirechat\Models\Setting::class,
],
Each custom class must extend the matching Wirechat base model. Use this when you need app-specific relationships, scopes, observers, or helper methods while keeping the package schema contract intact.
Read more on the Models page.
Upgrade Checklist
- Update
wirechat/wirechatto^0.6. - Publish and run the new migrations.
- Review any published Wirechat views before replacing them.
- Add
invite,join_request,message_request, andsettingkeys to customized config files if they are missing. - Enable only the panel features your application needs.
- Test private chats, pending message requests, group invitations, join requests, and notification preferences before deploying.
Laravel News Links
ClickHouse Schema Design and Data Modeling
https://severalnines.com/wp-content/uploads/2026/07/blog-data-modeling-for-clickhouse.png
Sometimes, we see ClickHouse queries that should normally complete in milliseconds take several seconds to finish or worse, time out entirely. When that happens, there is a good chance that the schema is the real culprit. The problem is often not the query itself, nor is it a hardware bottleneck. Instead, it can stem from a schema design decision made months ago that nobody questioned at the time.
Schema design in ClickHouse is one of those tasks that appears to be a simple, one-time setup activity, but it often becomes a recurring operational concern. When the schema is designed poorly, problems tend to surface over time, including slow queries, oversized partitions, and mutation jobs that run for hours while production traffic continues to grow.
In this article, we will cover the core concepts of ClickHouse, systematically walk through design decisions, from core concepts and operational patterns to monitoring and evolution, with the goal of giving you a framework for making and maintaining schema decisions in production.
Core Concepts for ClickHouse Schema Design
Distributed Tables, Local Tables, Shards, and Replicas
Before writing a single CREATE TABLE, it helps to have a clear mental model of how ClickHouse actually stores and serves data across a cluster. ClickHouse divides data across shards with each shard holding a horizontal slice of the total dataset. Each shard can have one or more replicas for fault tolerance. The replicas within a shard hold identical data; the shards themselves hold different data.
The two table types you’ll work with constantly are:
- Local tables (
ReplicatedMergeTreeand its variants): the actual storage layer. Each node stores its own local table containing its shard’s data. Queries against a local table only see that node’s data. - Distributed tables (
Distributedengine): a logical routing layer that sits on top of the local tables. When you query a distributed table, ClickHouse fans the query out to all shards, collects the results, and merges them. Distributed tables don’t store data themselves.
N.B. schema changes need to be applied to local tables on every node, and the distributed table definition needs to match. It sounds obvious, but it is a common source of confusion when onboarding teams who are used to a single-server database.
Shard key selection matters for data distribution. A poorly chosen shard key (or rand() used as a lazy default) can lead to uneven data distribution, e.g. one shard holding 60% of the data while others hold 20% each — this creates hot spots and makes capacity planning unreliable. The shard key should distribute data evenly and, ideally, align with how you query, if most queries filter by tenant_id, sharding by tenant_id means queries for a single tenant hit one shard instead of all of them.
Partition Key and Primary Key (Sparse Index)
These two concepts trip up almost everyone coming from a relational background, because they sound like the same thing but serve entirely different purposes in ClickHouse. The partition key controls how data is physically divided into separate directories on disk. Each partition is stored and managed independently, which means:
- Queries that filter on the partition key can skip entire partitions without reading them (partition pruning)
- Old data can be dropped by dropping a partition, instant, no heavy delete operation
- Background merges only happen within a partition, not across them
For time-series data, partitioning by month (toYYYYMM(event_time)) is the most common pattern. It gives you clean data lifecycle management (drop old months instantly) and good pruning behavior for time-bounded queries.
The primary key in ClickHouse is not a uniqueness constraint, it’s a sparse index. ClickHouse stores one index entry per 8192 rows (one granule), not one per row. This makes it memory-efficient even at billions of rows, but it means the primary key is designed for range scans and filtering, not point lookups.
The ORDER BY clause defines the physical sort order of data on disk, and the primary key must be a prefix of ORDER BY. This is worth saying clearly: the sort order is what makes your queries fast or slow. If your most common query filters by (tenant_id, event_type, event_time), your ORDER BY should reflect that. Data is stored sorted by those columns, so ClickHouse can skip irrelevant granules efficiently.
Here’s a concrete example that puts these together:
CREATE TABLE events_local
(
tenant_id UInt32,
event_time DateTime,
event_type LowCardinality(String),
user_id UInt64,
session_id UUID,
properties String,
ingested_at DateTime DEFAULT now()
)
ENGINE = ReplicatedMergeTree(
'/clickhouse/tables/{shard}/events',
'{replica}'
)
PARTITION BY toYYYYMM(event_time)
ORDER BY (tenant_id, event_type, event_time)
SETTINGS index_granularity = 8192;
A few decisions that can be taken as below:
LowCardinality(String)forevent_type, if this column has fewer than 10,000 distinct values, this encoding dramatically reduces storage and speeds up filtering.PARTITION BY toYYYYMM(event_time), monthly partitions, suitable for a 12–18 month hot data retention window.ORDER BY (tenant_id, event_type, event_time), optimized for queries that filter by tenant first, then by event type, then narrow by time range.- The ZooKeeper path uses
{shard}and{replica}macros so the same DDL can be run on every node without modification.
Materialized Views and Aggregated Tables
Materialized views in ClickHouse are not the same as in PostgreSQL. They are real-time incremental aggregations; every time data is inserted into the source table, the materialized view processes those rows and writes the aggregated result to a target table. There’s no scheduled refresh and it happens synchronously with the insert.
This makes them powerful for pre-computing aggregations that would otherwise require scanning billions of rows at query time. A common pattern is to maintain hourly or daily rollup tables alongside the raw events table.
For example, create the target table for aggregated counts and later create the MATERIALIZED VIEW with aggregation.
CREATE TABLE events_hourly_agg
(
tenant_id UInt32,
event_type LowCardinality(String),
hour DateTime,
event_count AggregateFunction(count, UInt64)
)
ENGINE = AggregatingMergeTree()
PARTITION BY toYYYYMM(hour)
ORDER BY (tenant_id, event_type, hour);
CREATE MATERIALIZED VIEW events_to_hourly
TO events_hourly_agg
AS
SELECT
tenant_id,
event_type,
toStartOfHour(event_time) AS hour,
countState() AS event_count
FROM events_local
GROUP BY tenant_id, event_type, hour;
Operationally, materialized views add write amplification, every insert into the source table triggers a write to the view’s target table. For high-ingestion workloads, this is worth monitoring. They also need to be maintained when the source schema changes, which is often forgotten until something breaks.
Operational Design Patterns
Time-Series and Event Analytics Schemas
The vast majority of ClickHouse deployments are built around time-series or event data clickstreams, application logs, metrics, IoT sensor readings. This is where ClickHouse’s design shines, and there are well-established patterns to follow.
The core principle is time as the primary organizing dimension. Partition by time (monthly or weekly depending on data volume), and include event_time in the ORDER BY so range scans are efficient. Keep raw events immutable and resist the temptation to update them in place.
For retention management, the TTL clause handles automatic expiry without manual intervention:
TTL event_time + INTERVAL 90 DAY DELETE
TTL event_time + INTERVAL 30 DAY TO DISK 'cold_storage'
The first script automatically deletes rows older than 90 days while the second scripts move cold data to a cheaper storage tier. This is operationally cleaner than scheduled delete jobs, which in ClickHouse would trigger heavy mutations.
Bulk Ingestion vs. Real-Time Streaming
How data arrives significantly affects schema and operational behavior. ClickHouse handles both, but they stress the system differently.
Bulk ingestion, i.e. large batch inserts; for example, nightly ETL from a data warehouse, is relatively forgiving. ClickHouse is designed for large INSERT batches, each batch creates one or a few data parts, and the background merge process handles compaction.
The risk is inserting too many small batches in rapid succession, which creates a flood of tiny parts that overwhelm the merge queue. The rule of thumb is: batch size matters more than frequency. Aim for inserts of at least 10,000 –100,000 rows per batch.
Real-time streaming via Kafka requires more care. The ClickHouse Kafka table engine or tools like Vector/Benthos handle ingestion, but the operational concern is the same: small, frequent inserts create merge pressure. Configure consumers to buffer and batch messages before inserting, and monitor system.parts for signs of part accumulation.
SELECT
table,
count() AS part_count,
sum(rows) AS total_rows,
formatReadableSize(sum(bytes_on_disk)) AS disk_size
FROM system.parts
WHERE active = 1
GROUP BY table
ORDER BY part_count DESC;
A healthy table has tens to low hundreds of active parts. Thousands of parts is a warning sign that inserts are too small or merges are falling behind.
Multi-Tenant Schema Isolation
If your ClickHouse cluster serves multiple tenants, you need to decide early how to isolate their data. The main options are:
- Database-per-tenant: each tenant gets their own database (and potentially their own set of tables). Clean isolation, simple access control, but doesn’t scale past a few dozen tenants without becoming a management burden.
- Table-per-tenant: all tenants share a database, each with their own table. Works at moderate scale but schema changes need to be applied to every tenant table, which is operationally painful at hundreds of tenants.
- Shared table with
tenant_idcolumn: all tenant data in one table, filtered bytenant_id. This is the most operationally maintainable pattern at scale. The key requirement is thattenant_idmust be the leading column inORDER BYso that per-tenant queries efficiently skip irrelevant data without a full scan.
ORDER BY (tenant_id, event_type, event_time)
With this sort order, a query filtering on tenant_id = 42 skips all granules that don’t contain that tenant’s data, making it effectively as fast as if the table contained only that tenant’s rows.
Schema Evolution and Operational Impact
Adding Columns, Partitions, and Handling Mutations
Schema changes in ClickHouse are generally safer than in OLTP databases, but they are not without operational cost. Adding a column is fast and non-blocking. ClickHouse uses lazy evaluation, the new column returns a default value for existing rows without rewriting data on disk. It is one of the rare DDL operations you can run in production without much anxiety:
ALTER TABLE events_local ON CLUSTER my_cluster
ADD COLUMN geo_country LowCardinality(String) DEFAULT '';
Dropping a column triggers a background data rewrite (mutation) to remove that column from existing parts. This is heavier and can be slow on large tables. Mutations are expensive in ClickHouse. For example when you run the following command, ClickHouse does not update the row in place but finds all parts with the matching condition, creates new versions of those parts with the modification already applied, replaces old parts after processing and continues serving queries while mutations run in the background.
ALTER TABLE events_local
UPDATE status = 'processed'
WHERE id = 123;
The guidance here is simple: avoid mutations in hot paths. For data corrections, prefer inserting corrected rows and using a ReplacingMergeTree or CollapsingMergeTree engine to handle deduplication, rather than updating rows in place.
If you must run a mutation, monitor its progress:
SELECT
command,
parts_to_do,
is_done,
latest_fail_reason
FROM system.mutations
WHERE table = 'events_local' AND is_done = 0;
Monitoring Schema-Related Issues
Identifying Slow Queries and Partition Problems
The most useful table in ClickHouse for day-to-day schema health monitoring is system.query_log. Queries that are reading an unexpectedly high number of rows relative to what they return are usually a sign of poor partition pruning or an ORDER BY that doesn’t align with the filter. Skipping indexes (secondary indexes in ClickHouse) are often added with good intentions but not actually used. Check whether they’re being utilized by execute the following:
SELECT
table,
name,
type,
expr
FROM system.data_skipping_indices
WHERE database = 'mydb';
Then cross-reference with system.query_log to see if queries against that table are actually benefiting, if read_rows remains high after adding an index, it may not be matching the query pattern.
Capacity Planning for Growth
Schema decisions have long-term storage implications that are not always obvious at design time. A few metrics are worth tracking regularly, such those included in this storage growth per table over time monitoring query below:
SELECT
table,
formatReadableSize(sum(bytes_on_disk)) AS total_size,
sum(rows) AS total_rows,
count() AS part_count,
max(modification_time) AS last_modified
FROM system.parts
WHERE active = 1 AND database = 'mydb'
GROUP BY table
ORDER BY sum(bytes_on_disk) DESC;
Track the table with total size, rows, partition count on a weekly basis and plot the trend. A table that grows 20% month-over-month with a 90 day TTL will eventually reach a stable size but a table with no TTL and unbounded growth will eventually cause disk pressure that affects the entire cluster.
Partition-level granularity is also useful for anticipating when TTL drops will occur and what storage they will free:
SELECT
partition,
formatReadableSize(sum(bytes_on_disk)) AS size,
sum(rows) AS rows,
count() AS parts
FROM system.parts
WHERE active = 1 AND table = 'events_local'
GROUP BY partition
ORDER BY partition DESC;
The above query shows the size per partition for the ClickHouse table events_local.
Integrating with Your Multi-Database Environment
Data Flow from OLTP to ClickHouse
Most ClickHouse deployments exist downstream of an OLTP database. Orders come in through PostgreSQL, user events flow through MySQL, and ClickHouse ingests and aggregates that data for analytics. This pipeline introduces a class of schema problems that don’t exist in single-database setups.
The OLTP schema and the ClickHouse schema should not be the same schema. OLTP tables are normalized, they are designed to minimize write amplification and enforce referential integrity. ClickHouse schemas are deliberately denormalized, trading write efficiency for read efficiency. A join that’s trivial in PostgreSQL can be expensive in ClickHouse at scale, so the right pattern is to resolve joins at ingestion time, pushing denormalized, enriched records into ClickHouse rather than replicating normalized tables and joining at query time.
This means the ingestion pipeline, whether it’s Kafka, Debezium CDC, Airbyte, or a custom ETL, is also a transformation layer. Fields get renamed, types get cast, related records get joined and flattened, and low-cardinality string fields get encoded appropriately. Operationally, this pipeline is part of the schema: changes to it have the same impact as changes to the table definition.
Managing Model Changes, Versioning, and Rollback
When the upstream OLTP schema changes eg: a new column added to orders, a field renamed in users. The downstream ClickHouse schema and the ingestion pipeline both need to change in a coordinated way. Without a versioning discipline, these changes become brittle and difficult to roll back — a few practices that hold up well in production:
- Treat DDL as code: The schema changes should live in version-controlled migration files (tools like Flyway or a custom migration runner), not applied ad-hoc from a SQL client. Every
ALTER TABLEthat went to production should be traceable to a commit. - Add before you remove: When renaming a column or changing a type, add the new column first and allow both the old and new column to coexist during a transition window. Update the ingestion pipeline to write to both, then cut over queries to the new column, then drop the old one. This avoids a hard cutover that can’t be rolled back.
- Schema rollback is hard therefore plan for it: Dropping a column or partition key change is not easily reversible. Before applying significant schema changes, take a backup of the affected table (or at minimum its most recent partition) so that recovery is possible without a full cluster restore.
- Document the lineage: For each ClickHouse table, maintain a short document describing where the data comes from, what transformations are applied, and what downstream queries or dashboards depend on it. When a schema change is proposed, this lineage makes the blast radius obvious before anything is applied.
Conclusion
ClickHouse schema design is not something that you set once, but is something you evolve over time. The implication is that choices you make when creating a table today do not just affect today’s queries but quietly shape how your system performs months down the line, from how efficiently queries run to how painful or painless future schema changes turn out to be.
Some points worth keeping in mind when designing the schema and data modeling: design your ORDER BY for readers, not writers; structure your sort key around how people query the data, not around the order it arrives in; partition by time and but avoid slicing things so finely that merge overhead becomes its own problem; Be deliberate with data types. LowCardinality and AggregateFunction are powerful tools, but only when applied with clear intent. Reaching for them out of habit rather than purpose tends to backfire. Your ingestion pipeline is part of your schema — how data flows in isn’t separate from how it’s stored, think of them as one connected system.
And remember, keep an eye on the correct metrics from the start. Schema issues seldom make themselves known in an obvious way. Identifying them through regular monitoring is much less expensive than dealing with the aftermath. The central idea here is that decisions regarding schemas have a cumulative effect. Positive choices subtly simplify all other aspects, while negative ones discreetly complicate them.
Planet for the MySQL Community
How to Throw a Tomahawk (And Nail the Target)
https://content.artofmanliness.com/uploads/2026/07/Throw-a-Tomahawk-4.jpg

While we often imagine tomahawks being thrown in battle by the early residents of our country, American Indians and mountain men rarely threw their tomahawks, or ‘hawks, in combat. Even if a warrior successfully killed his target with his throw, it meant surrendering a weapon mid-fight. Instead, the tomahawk was primarily used in hand-to-hand combat.
When folks in the 19th century did throw their tomahawks, they largely did it for fun. Once a year, mountain men would gather at a rendezvous to trade the pelts they’d collected and resupply. These gatherings became massive camps where the men held contests of all kinds, including tomahawk throwing. Some native tribes (who originated the first tomahawks) held similar contests of skill for their men to take part in and would also come to the frontiersmen’s camps to engage in trading and throw some tomahawks with the buckskin-clad mountain men.
Throwing a tomahawk continues to be a fun activity in the 21st century. Few skills are quite as gratifying as being able to bury a tomahawk into a stump with a satisfying thunk. Whether you’re at a backyard range, a campsite, or an axe-throwing establishment, tomahawk throwing is easy to grasp (literally and figuratively) and quite fun to practice. As with most physical skills that require some finesse, it’s more about smooth mechanics than raw power.
A good throw — as outlined above — depends on a relaxed grip, a fluid motion, and letting the tomahawk complete its natural rotation. Master those fundamentals, and you’ll be sticking the blade with regularity, just like an old-time mountain man.
This article was originally published on The Art of Manliness.
The Art of Manliness
Pool Noodle Numchucks #3DPrinting #3DThursday
http://img.youtube.com/vi/hf8gYLe6R9Y/0.jpg
Every week we’ll 3D print designs from the community and showcase slicer settings, use cases and of course, Time-lapses! This Week:
Pool Noodle Numchucks
By CompliantDesigns
makerworld.com/en/models/1497093-pool-noodle-numchucks
Bambu X1C
PolyMaker PLA
3hr 58mins
X:202 Y:202 Z:130mm
.2mm layer / .4mm Nozzle
10% Infill / 1mm Retraction
200C / 60C
85g
230mm/s

Every Thursday is #3dthursday here at Adafruit! The DIY 3D printing community has passion and dedication for making solid objects from digital models. Recently, we have noticed electronics projects integrated with 3D printed enclosures, brackets, and sculptures, so each Thursday we celebrate and highlight these bold pioneers!
Have you considered building a 3D project around an Arduino or other microcontroller? How about printing a bracket to mount your Raspberry Pi to the back of your HD monitor? And don’t forget the countless LED projects that are possible when you are modeling your projects in 3D!
LIVE CHAT IS HERE! http://adafru.it/discord
Adafruit on Instagram: https://www.instagram.com/adafruit
Shop for parts to build your own DIY projects http://adafru.it/3dprinting
3D Printing Projects Playlist:
3D Hangout Show Playlist:
Layer by Layer CAD Tutorials Playlist:
Timelapse Tuesday Playlist:
Connect with Noe and Pedro on Social Media:
Noe’s Twitter / Instagram: http://instagram.com/ecken
Pedro’s Twitter / Instagram: http://instagram.com/videopixil
3D printing – Adafruit Industries – Makers, hackers, artists, designers and engineers!
Marco Rubio just gave one of the best anti-communist speeches of all time 🔥
https://media.notthebee.com/articles/6a59208baf0516a59208baf052.jpg
You need to watch this.
Not the Bee
LaraBench – The Free Tinkerwell Alternative
Laravel News Links
Tired of the brain rot that’s melting your soul? Here’s a solid hour of uplifting teaching about the historical reliability of the Bible.
https://media.notthebee.com/articles/6a57b6c802b966a57b6c802b97.jpg
I don’t know if you know this, but Not the Bee’s job is to cover wacky new stories that read like Babylon Bee headlines.
Not the Bee
PostgreSQL Meta Commands that save time every day
https://www.percona.com/wp-content/uploads/2026/07/Screenshot-2026-07-15-at-11.39.10-AM-300×134.png
When most people start working with PostgreSQL, they quickly learn SQL:
SELECT * FROM employees;
But very soon, another world opens up inside psql — a set of commands that don’t look like SQL, don’t end with semicolons.
These are PostgreSQL Meta Commands, and they quietly power the daily workflow of almost every experienced DBA.
Meta commands are not about querying data — they are about navigating, inspecting, and controlling the PostgreSQL session/database efficiently.
What exactly are Meta Commands?
Meta commands are special instructions interpreted by psql, not PostgreSQL itself.
That means:
- They are not SQL
- They execute instantly on the client side
- They are specific to the psql terminal tool
- They do not end with semicolon like SQL statements
- The main focus area for meta commands is database interaction and not the interaction with the data in the database.
Cheat Sheet (Quick Reference)
The most commonly used meta commands are as follows. There are many more apart from these, however, below are the most frequently used ones:
Connect and Manage Sessions
These commands help discover databases, establish connections, and verify the current session.
| \c | Connect to another database |
| \l | List all the databases available in the cluster |
| \l+ | List all the databases available in the cluster with more details, like DB Size, etc |
| \conninfo | Displays information about the current database connection |
Please find the example of the commands used to connect and manage sessions in the screenshot below:

Inspect Database Objects
The \d family of commands is one of the most powerful features of psql . These commands can be used to discover database objects, inspect their definitions, and view additional metadata.
| \d | Describe database objects or list objects visible in the current search path. |
| \d object_name | Describe a specific table, view, sequence, or other database object. |
| \d+ object_name | Display extended information about an object. |
| \dt | List tables. Supports schema names and wildcard patterns. |
| \di | List indexes. Supports wildcard patterns. |
| \dn | List schemas in the current database. |
| \du | List database roles. |
| \db | List tablespaces |
| \dx | List installed extensions |
| \df | List functions and procedures |
| \sf function name | Displays the source code of the specific function/procedure |
Using object names and wildcards
Most object-inspection commands accept object names, schema-qualified names, and wildcard patterns.
For example:
\dt
Lists all tables in the current search path.
\dt public.*
Lists all tables in the public schema.
The same pattern matching is supported by several other meta-commands, including \di, \df, and the \d family.
Please find the example of the \d family commands in the screenshot below:

Format Query Results
Several meta-commands are available to improve the readability of query output, particularly when working with wide result sets.
| \x [on|off|auto] | Toggle expanded (vertical) display |
| \o filename | Redirect query output to a file or pipe. |
| \o | Restore query output to the terminal. |
Monitor Query Executions
These commands assist in measuring query performance and repeatedly executing queries for monitoring purposes.
| \timing [on|off] | Toggle Query execution timing |
| \watch seconds | Re-execute the current query at the specified interval |

Execute and Automate tasks
These commands simplify repetitive tasks and enable integration between psql, SQL scripts, and the operating system
| \i filename | Execute the commands from the file |
| \gexec | Execute each field returned by a query as an SQL statement. |
| \! command | Execute a shell command without leaving a psql prompt |

Get Help
Built-in help commands provide quick access to both psql meta-command documentation and PostgreSQL SQL syntax without leaving the terminal.
| \? | Display all available psql meta-commands. |
| \h | List SQL commands for which syntax help is available. |
| \h command | Display syntax help for a specific SQL command. |
What is .psqlrc?
.psqlrc is a startup file in the home directory that psql reads when a session begins. It can hold meta-commands and SQL that run before the first prompt. The main benefit is consistent defaults — timing, formatting, and a custom prompt — without repeating setup each time, which speeds daily work and reduces connection mistakes across databases.
A minimal .psqlrc might look like this:
\timing on \x auto
These settings load automatically on every new psql session as highlighted below:

Conclusion
PostgreSQL is powerful because of SQL — but for DBAs, psql meta commands make daily management far easier and more efficient.
Most developers use only a handful like \dt or \d. But experienced DBAs rely on a much broader toolkit to:
- Investigate production issues faster
- Navigate systems efficiently
- Reduce reliance on repetitive SQL
- Repetitive tasks can be automated
- Debug complex problems quickly
An easy way to understand the relationship between SQL and PostgreSQL meta commands is to compare them to driving a car.
SQL is like driving the car — it is the primary means of reaching a destination. It is used to retrieve, insert, update, and delete data, enabling applications and users to interact with the information stored in the database.
Meta commands, on the other hand, are like the car’s dashboard. While the dashboard does not move the vehicle, it provides essential information such as speed, fuel level, engine health, navigation status, and warning indicators. Driving without a dashboard is certainly possible, but it would mean operating with limited visibility into the vehicle’s condition and performance.
Similarly, SQL is responsible for manipulating and retrieving data, whereas PostgreSQL meta commands provide valuable insight into the database environment itself. They help administrators inspect database objects, navigate schemas, monitor sessions, examine roles and privileges, review object definitions, and perform numerous administrative tasks efficiently.
In essence, SQL enables interaction with the data, while meta commands enable interaction with the PostgreSQL environment. Together, they form a complementary toolkit that allows database professionals to work more effectively, troubleshoot issues faster, and administer PostgreSQL with greater confidence.
The post PostgreSQL Meta Commands that save time every day appeared first on Percona.
Blog – Percona