MySQL Query Optimization & EXPLAIN: A Complete Guide for DBAs and Developers

https://minervadb.com/wp-content/uploads/2026/06/shutterstock_2360159259-1024×520.jpg

MySQL query optimization is one of the most critical skills a database administrator or developer can possess. Whether you are managing a high-traffic e-commerce platform, a data warehouse with billions of rows, or a transactional OLTP system, poorly optimized queries are the leading cause of performance degradation, increased I/O, excessive CPU usage, and frustrated end users. At the heart of MySQL’s query optimization toolkit lies the EXPLAIN statement — a powerful diagnostic command that reveals how the MySQL query optimizer intends to execute a given SQL statement.
In this comprehensive guide, we will explore MySQL query optimization from the ground up: understanding the query execution lifecycle, dissecting every column of the EXPLAIN and EXPLAIN ANALYZE output, identifying common anti-patterns, and applying proven optimization strategies that MySQL DBAs and developers rely on in production environments every day. By the end of this article, you will be equipped with the knowledge to analyze execution plans, eliminate slow queries, and design indexes that drive maximum throughput.

Understanding the MySQL Query Optimizer

Before diving into EXPLAIN, it is essential to understand what the MySQL query optimizer does. The optimizer is a cost-based component within the MySQL server that evaluates multiple possible execution plans for a given query and selects the one with the lowest estimated cost. This cost is calculated based on statistics about tables and indexes stored in the Information Schema and the InnoDB storage engine‘s internal data dictionary.
The optimizer considers factors such as row estimates, index selectivity, join order, and available access methods before producing an execution plan. However, the optimizer is not perfect — it relies on statistics that may be stale or inaccurate, which is why understanding EXPLAIN and knowing how to guide the optimizer with hints is an indispensable skill for any serious MySQL DBA or developer.

The EXPLAIN Statement: Syntax and Variants

MySQL provides several variants of the EXPLAIN statement, each offering different levels of detail about query execution. Understanding when to use each variant is key to efficient query diagnostics.

-- Basic EXPLAIN
EXPLAIN SELECT * FROM orders WHERE customer_id = 1001;

-- EXPLAIN with FORMAT=JSON for richer, structured output
EXPLAIN FORMAT=JSON SELECT * FROM orders WHERE customer_id = 1001;

-- EXPLAIN ANALYZE (MySQL 8.0.18+) - executes query and returns real metrics
EXPLAIN ANALYZE SELECT * FROM orders WHERE customer_id = 1001;

-- EXPLAIN for DML statements
EXPLAIN UPDATE orders SET status = 'shipped' WHERE order_date < '2024-01-01';
EXPLAIN DELETE FROM audit_log WHERE created_at < NOW() - INTERVAL 90 DAY;
EXPLAIN INSERT INTO archive_orders SELECT * FROM orders WHERE status = 'closed';

Sample Schema for Practical Examples

Throughout this guide, we use a realistic e-commerce schema to demonstrate every optimization technique hands-on.

CREATE TABLE customers (
    customer_id    INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    email          VARCHAR(255) NOT NULL,
    country_code   CHAR(2) NOT NULL,
    created_at     DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    status         TINYINT(1) NOT NULL DEFAULT 1,
    UNIQUE KEY uk_email (email),
    KEY idx_country_status (country_code, status),
    KEY idx_created_at (created_at)
) ENGINE=InnoDB;

CREATE TABLE orders (
    order_id       BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    customer_id    INT UNSIGNED NOT NULL,
    order_date     DATE NOT NULL,
    total_amount   DECIMAL(12,2) NOT NULL,
    status         ENUM('pending','processing','shipped','delivered','cancelled') NOT NULL,
    KEY idx_customer_id (customer_id),
    KEY idx_order_date_status (order_date, status),
    CONSTRAINT fk_orders_customer FOREIGN KEY (customer_id) REFERENCES customers(customer_id)
) ENGINE=InnoDB;

CREATE TABLE order_items (
    item_id        BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    order_id       BIGINT UNSIGNED NOT NULL,
    product_id     INT UNSIGNED NOT NULL,
    quantity       SMALLINT UNSIGNED NOT NULL,
    unit_price     DECIMAL(10,2) NOT NULL,
    KEY idx_order_id (order_id),
    KEY idx_product_id (product_id),
    CONSTRAINT fk_items_order FOREIGN KEY (order_id) REFERENCES orders(order_id)
) ENGINE=InnoDB;

CREATE TABLE products (
    product_id     INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    sku            VARCHAR(50) NOT NULL,
    category_id    INT UNSIGNED NOT NULL,
    price          DECIMAL(10,2) NOT NULL,
    stock_qty      INT NOT NULL DEFAULT 0,
    UNIQUE KEY uk_sku (sku),
    KEY idx_category_id (category_id)
) ENGINE=InnoDB;

Dissecting the EXPLAIN Output: Column by Column

The id Column

The id column represents the sequential identifier of each SELECT within the query. Simple queries have a single id of 1. Subqueries and unions produce multiple rows with different id values. Rows with the same id execute as a join; rows with higher id values represent inner subqueries executed before the outer query.

The select_type Column

The select_type column describes the type of SELECT involved. Key values include: SIMPLE (no subqueries or unions), PRIMARY (the outermost SELECT), SUBQUERY (a subquery in SELECT or WHERE), DERIVED (a subquery in the FROM clause), UNION (subsequent SELECT in a UNION), and DEPENDENT SUBQUERY (a correlated subquery — a critical performance red flag indicating the subquery re-evaluates for each outer row).

-- SIMPLE: No subqueries or unions
EXPLAIN SELECT customer_id, email FROM customers WHERE country_code = 'US';

-- PRIMARY + SUBQUERY: Subquery in WHERE clause
EXPLAIN
SELECT order_id, total_amount FROM orders
WHERE customer_id IN (
    SELECT customer_id FROM customers WHERE country_code = 'DE'
);

-- PRIMARY + DERIVED: Subquery in FROM clause (derived table)
EXPLAIN
SELECT d.country_code, COUNT(*) AS order_count
FROM (
    SELECT c.country_code, o.order_id
    FROM customers c
    JOIN orders o ON o.customer_id = c.customer_id
    WHERE o.status = 'delivered'
) d
GROUP BY d.country_code;

-- UNION: Multiple SELECT statements combined
EXPLAIN
SELECT customer_id, 'active' AS label FROM customers WHERE status = 1
UNION ALL
SELECT customer_id, 'inactive' AS label FROM customers WHERE status = 0;

The type Column: The Most Critical Field in EXPLAIN

The type column — also called the join type or access type — is the most important field in the entire EXPLAIN output. It tells you how MySQL accesses rows in a table. From best to worst performance:

  • system — The table has only one row. A special case of const.
  • const — Exactly one matching row via PRIMARY KEY or UNIQUE index. Ideal for primary key lookups.
  • eq_ref — For each row from the preceding table, exactly one row is read via PRIMARY KEY or UNIQUE NOT NULL index. The best possible join access type.
  • ref — Multiple rows may match. Occurs with non-unique indexes or leftmost prefix matches.
  • range — Only rows within a given range are retrieved using an index (BETWEEN, IN, >, <, LIKE with prefix).
  • index — A full index scan. Faster than ALL but potentially a bottleneck on large indexes.
  • ALL — A full table scan. The worst case for large tables — must be eliminated in performance-critical paths.
-- const: Primary key lookup
EXPLAIN SELECT * FROM customers WHERE customer_id = 42;
-- type: const, rows: 1

-- eq_ref: Unique index join (best for joins)
EXPLAIN
SELECT c.email, o.order_id, o.total_amount
FROM orders o
JOIN customers c ON c.customer_id = o.customer_id
WHERE o.order_date = '2024-06-01';
-- type for customers: eq_ref (primary key join)

-- ref: Non-unique index lookup
EXPLAIN SELECT order_id, order_date, status FROM orders WHERE customer_id = 1001;
-- type: ref

-- range: Index range scan
EXPLAIN SELECT * FROM orders
WHERE order_date BETWEEN '2024-01-01' AND '2024-03-31';
-- type: range

-- ALL: Full table scan (must be fixed for large tables!)
EXPLAIN SELECT * FROM orders WHERE total_amount > 5000;
-- type: ALL if no index on total_amount
-- Solution: CREATE INDEX idx_total_amount ON orders(total_amount);

The possible_keys, key, key_len, ref, rows, filtered, and Extra Columns

The possible_keys column lists all indexes MySQL considered; key shows the index actually chosen. When key is NULL despite available indexes in possible_keys, MySQL chose a full table scan — often because statistics suggest too many rows match. Run ANALYZE TABLE to refresh statistics.
The key_len column shows how many bytes of the chosen index are used. For composite indexes, this reveals how many columns are utilized. The rows column is MySQL’s estimated row examination count — minimize this product across joined tables for optimal performance. The filtered percentage shows what fraction of rows examined actually pass the WHERE clause.
The Extra column contains the most actionable diagnostic signals: Using index (covering index — ideal), Using temporary (temp table — investigate), Using filesort (sort without index — add covering index), Using index condition (Index Condition Pushdown active — good), and Using MRR (Multi-Range Read active — good for range scans).

-- Using index: Covering index (zero table row access)
ALTER TABLE orders ADD INDEX idx_cust_covering
    (customer_id, order_id, order_date, total_amount, status);

EXPLAIN
SELECT order_id, order_date, total_amount, status
FROM orders WHERE customer_id = 1001;
-- Extra: Using index

-- Using temporary + Using filesort: Performance red flag
EXPLAIN
SELECT country_code, COUNT(*) AS cnt
FROM customers GROUP BY country_code ORDER BY cnt DESC;
-- Fix: add index on (country_code) to avoid temp table

-- Using filesort on non-indexed ORDER BY
EXPLAIN SELECT order_id, total_amount FROM orders
ORDER BY total_amount DESC LIMIT 20;
-- Fix: CREATE INDEX idx_total_amount ON orders(total_amount);

-- Using index condition: ICP optimization
EXPLAIN SELECT * FROM orders
WHERE order_date BETWEEN '2024-01-01' AND '2024-12-31'
  AND status = 'shipped';
-- Extra: Using index condition

EXPLAIN ANALYZE: Real Execution Metrics in MySQL 8.0

EXPLAIN ANALYZE, introduced in MySQL 8.0.18, executes the query and returns both estimated and actual metrics for each node in the execution plan tree. This is critical for identifying cardinality estimation errors — cases where the optimizer’s row estimates diverge wildly from reality, leading to suboptimal plan selection.

EXPLAIN ANALYZE
SELECT
    c.country_code,
    COUNT(DISTINCT o.order_id)       AS total_orders,
    SUM(oi.unit_price * oi.quantity) AS total_revenue
FROM customers c
JOIN orders o     ON o.customer_id = c.customer_id
JOIN order_items oi ON oi.order_id = o.order_id
WHERE c.status = 1
  AND o.order_date >= '2024-01-01'
  AND o.status = 'delivered'
GROUP BY c.country_code
ORDER BY total_revenue DESC;
-> Sort: total_revenue DESC  (actual time=142.5..142.7 rows=48 loops=1)
    -> Aggregate using temporary table  (actual time=142.2..142.2 rows=48 loops=1)
        -> Nested loop inner join  (cost=18540.23 rows=9820)
                                   (actual time=0.8..138.6 rows=87342 loops=1)
            -> Nested loop inner join  (cost=5421.12 rows=3240)
                                       (actual time=0.5..22.4 rows=28918 loops=1)
                -> Filter: (c.status = 1)  (cost=1240.80 rows=8400)
                   (actual time=0.3..8.7 rows=71230 loops=1)
                    -> Index scan on c using idx_country_status
                       (cost=1240.80 rows=84000)
                       (actual time=0.2..6.9 rows=84000 loops=1)
                -> Filter: (o.order_date >= '2024-01-01') and (o.status='delivered')
                   (cost=0.25 rows=1) (actual time=0.00019..0.00019 rows=0 loops=71230)
                    -> Index lookup on o using idx_customer_id
                       (customer_id=c.customer_id)  (cost=0.25 rows=1)
                       (actual time=0.00017..0.00017 rows=1 loops=71230)
            -> Index lookup on oi using idx_order_id (order_id=o.order_id)
               (cost=1.12 rows=3) (actual time=0.003..0.004 rows=3 loops=28918)

Key analysis points: compare the estimated rows against actual rows. When these diverge by orders of magnitude, consider running ANALYZE TABLE or increasing innodb_stats_persistent_sample_pages. The actual time=start..end values are in milliseconds. The loops value shows how many times each node executed — high loop counts on expensive inner operations are the primary target for optimization.

Common Query Anti-Patterns and How to Fix Them

Anti-Pattern 1: Functions on Indexed Columns in WHERE Clauses

Wrapping an indexed column inside a function prevents MySQL from using the index, forcing a full table scan. This is one of the most common and damaging anti-patterns found in production SQL workloads — and the fix is almost always straightforward.

-- BAD: Function prevents index usage
EXPLAIN SELECT * FROM orders
WHERE YEAR(order_date) = 2024 AND MONTH(order_date) = 6;
-- type: ALL (full table scan on potentially millions of rows)

-- GOOD: Rewrite as range condition (uses index)
EXPLAIN SELECT * FROM orders
WHERE order_date >= '2024-06-01' AND order_date < '2024-07-01';
-- type: range, Extra: Using index condition

-- BAD: LIKE with leading wildcard (no index possible)
EXPLAIN SELECT * FROM products WHERE sku LIKE '%ABC%';
-- Consider FULLTEXT index for arbitrary substring searches
ALTER TABLE products ADD FULLTEXT INDEX ft_sku (sku);
SELECT * FROM products WHERE MATCH(sku) AGAINST('ABC' IN BOOLEAN MODE);

-- GOOD: LIKE with trailing wildcard (uses index prefix scan)
EXPLAIN SELECT * FROM products WHERE sku LIKE 'ABC%';
-- type: range

-- BAD: Function on indexed column breaks index usage
EXPLAIN SELECT * FROM customers WHERE LOWER(email) = 'user@example.com';

-- GOOD: Functional index (MySQL 8.0+) preserves index access
ALTER TABLE customers ADD INDEX idx_email_lower ((LOWER(email)));
EXPLAIN SELECT * FROM customers WHERE LOWER(email) = 'user@example.com';
-- type: ref, key: idx_email_lower

Anti-Pattern 2: The N+1 Query Problem

The N+1 problem occurs when an application executes one query to retrieve N records and then fires an additional query for each record — N+1 total round trips. This is catastrophic at scale and entirely preventable with proper JOIN usage or batch fetching.

-- BAD: N+1 pattern (500 pending orders = 501 queries!)
-- Query 1: SELECT order_id FROM orders WHERE status = 'pending';
-- Then for each order_id:
-- Queries 2..501: SELECT * FROM order_items WHERE order_id = ?;

-- GOOD: Single JOIN eliminates N+1 completely
EXPLAIN
SELECT
    o.order_id, o.order_date, o.total_amount,
    oi.item_id, oi.product_id, oi.quantity, oi.unit_price
FROM orders o
JOIN order_items oi ON oi.order_id = o.order_id
WHERE o.status = 'pending'
ORDER BY o.order_id, oi.item_id;
-- type for orders: ref (idx_status)
-- type for order_items: ref (idx_order_id)
-- One query, complete result set

Anti-Pattern 3: SELECT * Instead of Column Projection

Using SELECT * prevents covering index usage, transfers unnecessary data across the network, and makes execution plans less predictable as schemas evolve. Always project only the columns your application actually needs.

-- BAD: SELECT * forces table row access even when index could cover query
EXPLAIN SELECT * FROM orders WHERE customer_id = 1001;

-- GOOD: Project only needed columns enables covering index
ALTER TABLE orders ADD INDEX idx_cust_cover
    (customer_id, order_id, order_date, total_amount, status);

EXPLAIN
SELECT order_id, order_date, total_amount, status
FROM orders WHERE customer_id = 1001;
-- type: ref, Extra: Using index (all data from index - zero table access)

Advanced Indexing Strategies for MySQL Query Optimization

Composite Index Design: The Left-Prefix Rule

Composite indexes follow the left-prefix rule: MySQL can only use an index starting from the leftmost column. A composite index on (A, B, C) supports queries on A, A+B, or A+B+C — but not B or C alone. Design composite indexes with equality columns first, range condition columns second, and ORDER BY / GROUP BY columns last to eliminate filesort operations.

-- Query: WHERE status = 'shipped' AND order_date BETWEEN x AND y ORDER BY order_date
-- Optimal: equality first, range second, ORDER BY aligned with range column
ALTER TABLE orders ADD INDEX idx_status_date_opt (status, order_date);

EXPLAIN
SELECT order_id, customer_id, total_amount
FROM orders
WHERE status = 'shipped'
  AND order_date BETWEEN '2024-01-01' AND '2024-06-30'
ORDER BY order_date;
-- type: range, key: idx_status_date_opt
-- Extra: Using index condition  (NO filesort! ORDER BY uses index)

-- Verify index columns being used via key_len
-- status ENUM NOT NULL = 1 byte
-- order_date DATE NOT NULL = 3 bytes
-- key_len = 4 means BOTH columns are utilized

-- Covering composite index for aggregate queries
ALTER TABLE orders ADD INDEX idx_grp_covering
    (status, order_date, customer_id, total_amount);

EXPLAIN
SELECT status, order_date, COUNT(*) AS cnt, SUM(total_amount) AS revenue
FROM orders
WHERE status IN ('shipped', 'delivered')
  AND order_date >= '2024-01-01'
GROUP BY status, order_date;
-- Extra: Using index (full covering index - no table access whatsoever)

Invisible Indexes: Safe Index Testing Without Dropping

MySQL 8.0 introduced invisible indexes, which the optimizer ignores while InnoDB continues maintaining them. This allows DBAs to safely validate the impact of removing an index before permanently dropping it — an indispensable tool for production index lifecycle management.

-- Make an index invisible to test impact of removing it
ALTER TABLE orders ALTER INDEX idx_status INVISIBLE;

-- EXPLAIN now shows optimizer ignoring this index
EXPLAIN SELECT * FROM orders WHERE status = 'pending';
-- possible_keys: NULL (invisible index ignored)

-- Re-enable the index
ALTER TABLE orders ALTER INDEX idx_status VISIBLE;

-- Allow session to see invisible indexes for targeted testing
SET SESSION optimizer_switch = 'use_invisible_indexes=on';
EXPLAIN SELECT * FROM orders WHERE status = 'pending';
SET SESSION optimizer_switch = 'use_invisible_indexes=off';

-- Check visibility status of all indexes
SELECT index_name, is_visible
FROM information_schema.STATISTICS
WHERE table_schema = 'ecommerce' AND table_name = 'orders'
GROUP BY index_name, is_visible;

Index Hints and MySQL 8.0 Optimizer Hints

When the MySQL optimizer makes a poor index selection — often due to outdated statistics or unusual data distributions — index hints and optimizer hints allow targeted intervention. Use them sparingly and always validate with EXPLAIN, as they bypass the optimizer’s cost model.

-- FORCE INDEX: Optimizer must use this index (ignores all others)
EXPLAIN SELECT * FROM orders FORCE INDEX (idx_order_date_status)
WHERE order_date >= '2024-01-01' AND status = 'delivered';

-- USE INDEX: Suggests an index (optimizer may still ignore)
EXPLAIN SELECT * FROM orders USE INDEX (idx_customer_id)
WHERE customer_id = 1001;

-- IGNORE INDEX: Prevents use of a specific index
EXPLAIN SELECT * FROM orders IGNORE INDEX (idx_status)
WHERE status = 'pending' AND order_date >= '2024-01-01';

-- Optimizer hints (MySQL 8.0+ preferred method)
SELECT /*+ NO_HASH_JOIN(o, c) */
    o.order_id, c.email, o.total_amount
FROM orders o
JOIN customers c ON c.customer_id = o.customer_id
WHERE o.status = 'pending';

-- SET_VAR hint: Change variable scope for a single query
SELECT /*+ SET_VAR(sort_buffer_size=4194304) */
    customer_id, SUM(total_amount) AS revenue
FROM orders
GROUP BY customer_id
ORDER BY revenue DESC
LIMIT 100;

Subquery Optimization and Common Table Expressions

Subqueries can be highly efficient or devastating for performance depending on how they are written. The most dangerous anti-pattern is the correlated subquery — a subquery with a DEPENDENT SUBQUERY select_type that re-evaluates for every row of the outer query. MySQL 8.0’s Common Table Expressions (CTEs) provide both performance parity with JOINs and dramatically improved readability for complex multi-step queries.

-- BAD: Correlated subquery (re-evaluated N times for N outer rows)
EXPLAIN
SELECT o.order_id, o.total_amount,
    (SELECT SUM(oi.unit_price * oi.quantity)
     FROM order_items oi
     WHERE oi.order_id = o.order_id) AS calculated_total
FROM orders o
WHERE o.order_date >= '2024-01-01';
-- select_type: DEPENDENT SUBQUERY (executed once per outer row!)

-- GOOD: JOIN with aggregation (single pass over data)
EXPLAIN
SELECT o.order_id, o.total_amount, oi_agg.calculated_total
FROM orders o
JOIN (
    SELECT order_id, SUM(unit_price * quantity) AS calculated_total
    FROM order_items GROUP BY order_id
) oi_agg ON oi_agg.order_id = o.order_id
WHERE o.order_date >= '2024-01-01';

-- BEST: CTE for readability with equivalent performance (MySQL 8.0+)
WITH order_totals AS (
    SELECT order_id, SUM(unit_price * quantity) AS calculated_total
    FROM order_items GROUP BY order_id
)
SELECT o.order_id, o.total_amount, ot.calculated_total
FROM orders o
JOIN order_totals ot ON ot.order_id = o.order_id
WHERE o.order_date >= '2024-01-01';

-- Recursive CTE: Hierarchical queries (category trees, org charts)
WITH RECURSIVE category_tree AS (
    SELECT category_id, parent_id, name, 0 AS depth
    FROM categories WHERE parent_id IS NULL
    UNION ALL
    SELECT c.category_id, c.parent_id, c.name, ct.depth + 1
    FROM categories c
    JOIN category_tree ct ON ct.category_id = c.parent_id
)
SELECT category_id, CONCAT(REPEAT('  ', depth), name) AS indented_name
FROM category_tree ORDER BY category_id;

Optimizing Pagination: Escaping the LIMIT/OFFSET Trap

Naive pagination using high OFFSET values is a classic performance trap. As OFFSET grows, MySQL must scan and discard increasingly large numbers of rows before returning the requested page — a problem known as deep pagination. For large datasets, cursor-based pagination using the last seen primary key delivers constant-time performance regardless of page depth.

-- BAD: High offset forces full scan of 1,000,100 rows
EXPLAIN SELECT order_id, order_date, total_amount
FROM orders ORDER BY order_id
LIMIT 100 OFFSET 1000000;
-- rows: 1000100 (scans and discards 1,000,000 rows)

-- GOOD: Cursor-based (keyset) pagination - constant performance
-- First page:
SELECT order_id, order_date, total_amount
FROM orders WHERE order_id > 0
ORDER BY order_id LIMIT 100;

-- Next page (pass last_order_id from previous result set):
SELECT order_id, order_date, total_amount
FROM orders
WHERE order_id > :last_order_id
ORDER BY order_id LIMIT 100;
-- type: range, rows: 100 (reads exactly what is needed)

-- Alternative: Late row lookup for complex multi-column sort
SELECT o.*
FROM orders o
JOIN (
    SELECT order_id FROM orders
    ORDER BY total_amount DESC, order_id
    LIMIT 100 OFFSET 50000
) ids ON ids.order_id = o.order_id
ORDER BY o.total_amount DESC, o.order_id;
-- Inner query works only with index pages; outer fetches only 100 full rows

Statistics Management and the Query Optimizer

The MySQL optimizer’s decisions are only as good as the statistics it uses. Stale or inaccurate statistics lead to poor plan choices — wrong join orders, missed index usage, and cardinality estimation errors. As a MySQL DBA, proactively managing statistics is a core operational responsibility, especially after bulk data loads or large DELETE operations.

-- Refresh table statistics
ANALYZE TABLE orders, customers, order_items, products;

-- View table statistics and sizes
SELECT table_name,
    table_rows,
    ROUND(data_length / 1024 / 1024, 2)  AS data_mb,
    ROUND(index_length / 1024 / 1024, 2) AS index_mb,
    update_time
FROM information_schema.TABLES
WHERE table_schema = 'ecommerce'
ORDER BY data_length DESC;

-- Check index cardinality (higher = more selective = better)
SELECT index_name, column_name, seq_in_index, cardinality, nullable
FROM information_schema.STATISTICS
WHERE table_schema = 'ecommerce' AND table_name = 'orders'
ORDER BY index_name, seq_in_index;

-- Increase sample pages for better statistics on large tables
ALTER TABLE orders STATS_SAMPLE_PAGES = 50;
ANALYZE TABLE orders;

-- InnoDB persistent statistics settings
SHOW VARIABLES LIKE 'innodb_stats%';
-- innodb_stats_persistent = ON (recommended for production)
-- innodb_stats_persistent_sample_pages = 20 (increase for accuracy)

-- Check when InnoDB table statistics were last updated
SELECT * FROM mysql.innodb_table_stats
WHERE database_name = 'ecommerce';

Performance Schema: Identifying the Highest-Impact Slow Queries

MySQL’s Performance Schema provides comprehensive instrumentation tables for real-time query performance monitoring. For MySQL DBAs, mastering the Performance Schema is essential for identifying the highest-impact optimization targets in production — revealing far more than the slow query log alone.

-- Top 10 slowest queries by total execution time
SELECT
    DIGEST_TEXT                                 AS query_template,
    COUNT_STAR                                  AS exec_count,
    ROUND(SUM_TIMER_WAIT / 1e12, 3)            AS total_time_sec,
    ROUND(AVG_TIMER_WAIT / 1e12, 6)            AS avg_time_sec,
    ROUND(MAX_TIMER_WAIT / 1e12, 6)            AS max_time_sec,
    SUM_ROWS_EXAMINED                           AS total_rows_examined,
    ROUND(SUM_ROWS_EXAMINED / COUNT_STAR, 0)   AS avg_rows_examined,
    SUM_NO_INDEX_USED                           AS full_scans
FROM performance_schema.events_statements_summary_by_digest
WHERE SCHEMA_NAME = 'ecommerce'
ORDER BY SUM_TIMER_WAIT DESC
LIMIT 10;

-- Queries performing full table scans in production
SELECT
    DIGEST_TEXT,
    COUNT_STAR,
    SUM_NO_INDEX_USED,
    ROUND(AVG_TIMER_WAIT / 1e12, 6) AS avg_sec
FROM performance_schema.events_statements_summary_by_digest
WHERE SCHEMA_NAME = 'ecommerce' AND SUM_NO_INDEX_USED > 0
ORDER BY SUM_NO_INDEX_USED DESC LIMIT 10;

-- sys schema: Simplified top-level performance view
SELECT * FROM sys.statement_analysis
WHERE db = 'ecommerce'
ORDER BY total_latency DESC LIMIT 10;

-- sys schema: All queries doing full table scans
SELECT * FROM sys.statements_with_full_table_scans
WHERE db = 'ecommerce'
ORDER BY no_index_used_count DESC;

The Optimizer Trace: Deep-Dive Plan Analysis

When EXPLAIN and EXPLAIN ANALYZE do not provide sufficient insight, the Optimizer Trace delivers a complete JSON log of every decision the optimizer made — including all alternative plans considered and their cost estimates. This is the ultimate diagnostic instrument for resolving the most difficult query optimization problems.

-- Enable optimizer trace
SET SESSION optimizer_trace = 'enabled=on';
SET SESSION optimizer_trace_max_mem_size = 1048576;

-- Run the query to analyze
SELECT order_id, customer_id, total_amount
FROM orders
WHERE status = 'shipped'
  AND order_date BETWEEN '2024-01-01' AND '2024-06-30'
ORDER BY total_amount DESC
LIMIT 50;

-- Retrieve the trace (JSON format)
SELECT QUERY, TRACE
FROM information_schema.OPTIMIZER_TRACE\G

-- Key JSON sections to examine:
-- "considered_execution_plans": All plans evaluated
-- "best_access_path": Index chosen and why
-- "rows_estimation": Cardinality estimates per table
-- "cost_info": read_cost, eval_cost, prefix_cost per plan

-- Disable optimizer trace
SET SESSION optimizer_trace = 'enabled=off';

Key MySQL Variables for Query Performance Tuning

Beyond index design, several MySQL server variables directly influence query execution performance. Understanding and tuning these variables is a critical complement to query-level optimization in production environments.

-- Sort buffer: used when ORDER BY/GROUP BY cannot use an index
SHOW VARIABLES LIKE 'sort_buffer_size';          -- Default: 256KB
SET SESSION sort_buffer_size = 4 * 1024 * 1024; -- 4MB for heavy sorts

-- Join buffer: used for Block Nested Loop joins (non-indexed joins)
SHOW VARIABLES LIKE 'join_buffer_size';          -- Default: 256KB
SET SESSION join_buffer_size = 2 * 1024 * 1024; -- 2MB for large joins

-- Temporary table memory thresholds (exceeding causes disk spill)
SHOW VARIABLES LIKE 'tmp_table_size';            -- Default: 16MB
SHOW VARIABLES LIKE 'max_heap_table_size';       -- Default: 16MB
-- Set both equal to prevent disk-based temp tables

-- InnoDB buffer pool: the single most impactful performance variable
SHOW VARIABLES LIKE 'innodb_buffer_pool_size';   -- Target: 70-80% of total RAM

-- Enable slow query log for continuous production monitoring
SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 1;                  -- Capture queries > 1 second
SET GLOBAL log_queries_not_using_indexes = ON;   -- Capture queries without indexes
SHOW VARIABLES LIKE 'slow_query_log_file';       -- Check log file location

-- Read buffer: sequential scan performance
SHOW VARIABLES LIKE 'read_buffer_size';          -- Default: 128KB
SHOW VARIABLES LIKE 'read_rnd_buffer_size';      -- Default: 256KB

MySQL Query Optimization Checklist for DBAs and Developers

The following checklist provides a systematic approach to diagnosing and resolving slow queries in MySQL production environments. Apply these steps in order for every optimization engagement.

  1. Capture the slow query — Use the slow query log, performance_schema.events_statements_summary_by_digest, or sys.statement_analysis to identify the highest-impact queries by total execution time and examination count.
  2. Run EXPLAIN and EXPLAIN ANALYZE — Review every column starting with type (eliminate ALL and index scans), rows (minimize the cross-join product), and Extra (eliminate Using filesort and Using temporary where feasible).
  3. Verify index design — Confirm indexes exist on all columns used in WHERE, JOIN ON, GROUP BY, and ORDER BY. Design composite indexes following the left-prefix rule: equality conditions first, range conditions second.
  4. Eliminate anti-patterns — Remove functions on indexed columns in WHERE, replace SELECT * with projection, convert correlated subqueries to JOINs, and eliminate N+1 patterns entirely.
  5. Update table statistics — Run ANALYZE TABLE after bulk data changes to ensure the optimizer works with accurate cardinality estimates.
  6. Validate with EXPLAIN ANALYZE — After applying changes, re-run EXPLAIN ANALYZE to confirm actual row counts match optimizer estimates and execution time has improved measurably.
  7. Test with production-scale data — Always benchmark optimizations against data volumes comparable to production. An effective index on 10,000 rows may not scale to 100,000,000 rows.
  8. Monitor continuously — Use Performance Schema and the sys schema to continuously monitor query performance and proactively identify regressions before they impact users.

Conclusion

MySQL query optimization is both a science and an art. The science lies in understanding how the cost-based optimizer works, how indexes are structured and accessed internally by InnoDB, and how to interpret every field of the EXPLAIN and EXPLAIN ANALYZE output with precision. The art lies in applying this knowledge pragmatically — knowing when to add a composite index, when to rewrite a correlated subquery as a JOIN, when to refresh statistics, and when to override the optimizer with targeted hints.
Mastering the techniques in this guide — from dissecting EXPLAIN columns and eliminating full table scans, to designing optimal composite and covering indexes, avoiding deep pagination traps, leveraging invisible indexes for safe lifecycle management, and using the Performance Schema for continuous monitoring — equips you to build MySQL-backed systems that scale confidently to hundreds of millions of rows and thousands of concurrent connections.
The return on investment in MySQL query optimization skills is exceptional: reduced infrastructure costs, dramatically improved user experience, fewer on-call incidents, and a more resilient, predictable database tier. Every millisecond shaved from a high-frequency query executed millions of times daily translates directly into meaningful savings and competitive advantage. Start every optimization engagement with EXPLAIN, follow the evidence rigorously, and let the data guide every decision you make.

Planet for the MySQL Community

The Babylon Bee Presents The Second Civil War

http://img.youtube.com/vi/OdxH1KZbjOY/0.jpg

Babylon Bee: “We Asked AI To Simulate If The U.S. Had A Second Civil War.”

Here are the results:

  • “In the city, we’re used to being able to burn down a target and no one does anything. I guess it’s different in the suburbs, though.”
  • “One of the big issues is how we hate guns. But the right loves them. I guess none of us considered how big a disadvantage that would put us in a civil war.”
  • “Many of the losing combatants fled to the far north. Starvation was rampant among them from lack of access to DoorDash.”
  • Lawrence Person’s BattleSwarm Blog

    Supreme Court Says Courts Can’t Smuggle Gun Control Into Bruen Step One

    https://www.ammoland.com/wp-content/uploads/2026/06/AR-15-Rifle-Scott-Witner-500×281.jpg

    AR-15 Rifle. IMG Scott Witner
    The Supreme Court’s Wolford v. Lopez ruling did more than strike down Hawaii’s “vampire law.” It warned lower courts not to smuggle gun-control arguments into Bruen Step One before the government has to prove a historical tradition. IMG Scott Witner

    The Supreme Court of the United States issued a landmark ruling in Wolford v. Lopez that struck down Hawaii’s so-called “vampire law,” but the decision’s impact extends far beyond that single victory. The opinion provides crucial clarification that lower courts cannot employ certain procedural strategies to evade rulings against restrictive gun control measures.

    On the surface, Wolford addresses whether private property owners must post “gun welcome” signs before individuals may lawfully carry firearms on their premises. However, like many other landmark Second Amendment decisions, Wolford transcends this narrow issue. It delivers important guidance on how courts must properly apply the constitutional framework established in Bruen and Heller.

    Bruen Step One Just Got Harder to Beat

    The most significant clarification is that courts may no longer shoehorn gun-control reasoning into Step One of the Bruen analysis.

    Step One focuses strictly on the plain text of the Second Amendment. If the regulated individual is part of “the people” and the arm in question is “bearable,” the conduct is presumptively protected. Liberal courts have repeatedly attempted to dismiss cases at this early stage by asserting that certain popular arms, such as AR-15-style rifles, fall outside constitutional protection.

    The Wolford decision firmly rejects this tactic, making clear that such maneuvering cannot be used to bypass Step Two’s historical-tradition analysis.

    In Footnote 1, Associate Justice Amy Coney Barrett wrote: “No one disputes that the Second Amendment’s text has a fixed meaning that must be satisfied before a law is deemed presumptively unconstitutional.” She noted the disagreement centers on whether courts can improperly import additional limits from regulatory tradition into the plain-text inquiry. “The answer is and always has been no.”

    Associate Justice Samuel Alito reinforced the two-step structure from Bruen, emphasizing that courts must first determine whether the challenged law implicates the core protections of the Amendment regarding “the people,” “keep[ing],” and “bear[ing]” Arms.

    Another common tactic courts have used to uphold “assault weapons” bans is to claim that firearms like AR-15s are not “in common use” specifically for self-defense. In Heller, the Court held that arms in common use cannot be banned, yet anti-gun courts have tried to append “for self-defense” to narrow that protection. This strained interpretation has been deployed to justify not only bans on commonly owned rifles but also restrictions on magazines and other accessories.

    What “Offensive or Defensive Purposes” Means for Gun Owners

    Wolford firmly rejects this reasoning. It clarifies that the Second Amendment protects “weapons customarily used for offensive or defensive purposes,” not merely those carried for self-defense.

    Heller never intended to confine the right to a narrow self-defense test; rather, it safeguards arms for all lawful purposes. Wolford confirms that bearable arms in common use are protected regardless of whether they are handguns or other types of firearms. A defensive firearm is one someone might carry outside the home for self-defense, such as a pistol that millions of Americans carry every day. Firearms kept mainly inside the home can be used for defensive purposes, but they can also be used legally for offensive purposes, such as clearing a house in case of an intruder. Also, guns like AR-15s can be used for hunting. This is not a defensive purpose. It is an offensive purpose.

    Americans also have the right to bear arms to resist tyranny. In fact, that is the main purpose of the Second Amendment.

    Wolford makes it clear these arms are protected under the Second Amendment and deviates from the anti-gun idea that the primary function of a gun must be for self-defense to be protected by the Second Amendment. Since many anti-gun defendants have argued that the Second Amendment only applies to firearms that are primarily used for self-defense, their arguments now fall short.

    Thanks to this ruling, anti-gun courts will face significantly greater difficulty in upholding unconstitutional restrictions. By closing off these procedural loopholes, Wolford strengthens the Second Amendment’s safeguards and ensures more faithful application of constitutional text and history.

    This opinion will have lasting ramifications far beyond Hawaii’s “vampire rule,” providing a powerful tool for defenders of the right to keep and bear arms across the country for years to come.


    About John Crump

    Mr. Crump is an NRA instructor and a constitutional activist. John has written about firearms, interviewed people from all walks of life, and on the Constitution. John lives in Northern Virginia with his wife and sons, follow him on X at @right2bear, or at www.crumpy.com.

    John Crump


    AmmoLand Shooting Sports News

    Behind the Sounds (and Scenes) of The Price Is Right

    https://theawesomer.com/photos/2026/06/the_price_is_right_t.jpg

    Behind the Sounds (and Scenes) of The Price Is Right

    Dallas Taylor’s Inside Sound goes behind the scenes of TV shows and musicals to learn how they capture the show’s audio. In this episode, broadcast mixer Henry Muehlhausen takes us on a tour of The Price Is Right. Along the way, we see where they hide microphones and a look at the game show’s iconic set pieces, including The Big Wheel and Plinko.

    The Awesomer

    Bob Belderbos: There Is No Magic: An AI Agent in 60 Lines of Python

    https://belderbos.dev/images/og/build-minimal-ai-agent-python.png

    Everybody talks about agents, and a lot of people assume they’re some new kind of model. They aren’t. An agent is a small amount of plumbing around an LLM you already understand. Let’s build one from scratch in Python and see exactly what that plumbing is.

    The formula

    An agent is: Model + Instructions + Memory + Tools + Execution Loop.

    Five parts. None of them is magic. The model is a brain in a jar: useful, fast, but stateless. It generates text; the code around it decides what to do with that text. That second half is the entire job and it’s code we can reason about.

    I made the same argument about the control layer being the real product. Here it is as a program.

    Start with the model. A real one calls an LLM API; we use a fake one that satisfies the same interface:

    from dataclasses import dataclass
    from typing import Protocol
    
    @dataclass(frozen=True)
    class Say:
     text: str
    
    @dataclass(frozen=True)
    class Call:
     tool: str
     arg: str
    
    Reply = Say | Call
    
    class Model(Protocol):
     def respond(self, system: str, history: list[str]) -> Reply: ...

    The Model protocol has a single method, respond, which takes the system prompt and the conversation history and returns a Reply. It’s a Protocol, so any object with a matching respond method counts as a Model, no inheritance required.

    For this minimal agent, the Reply type captures the two actions we support: say something to the user, or call a tool with an argument. The model is free to return either one, and the agent will execute it. (Real models can also emit plans, ask clarifying questions, or request several tool calls at once; we keep it to two to stay legible.)

    The agent’s entire decision space is those two variants. The match in the loop below reads as a clean two-way branch, one case per reply, instead of a tangle of flags.

    from dataclasses import dataclass, field
    from typing import Callable
    
    Tool = Callable[[str], str]
    
    @dataclass
    class Agent:
     model: Model # 1. Model
     system: str # 2. Instructions
     history: list[str] = field(default_factory=list) # 3. Memory
     tools: dict[str, Tool] = field(default_factory=dict) # 4. Tools

    In this example, a tool is a function taking a string and returning a string. The agent holds the other four parts as plain fields:

    • The model is any object satisfying the Model protocol: a fake model goes in for testing and a real one for production.
    • The system prompt is a string that tells the model what to do.
    • The history is the agent’s working memory: the conversation and tool outputs that get replayed back into the model. Real agents often add retrieval, summarization, or external state on top, because context windows are finite.
    • The tools field is a mapping of tool names to functions that implement them.

    The loop is the agent

    The part that turns a well-instructed chatbot into something agent-like is the fifth piece: an execution loop that lets the model observe outcomes and decide what to do next. Observe, think, act, check, repeat. Greatly simplified, of course, but this is the piece that does the work.

    Because the model is stateless, the agent must keep track of what happened and feed the history back into the model until the model decides the job is done.

     def run(self, user_input: str) -> str:
     self.history.append(f"user: {user_input}")
     while True: # real agents cap the iterations; see termination guards below
     match self.model.respond(self.system, self.history):
     case Say(text):
     self.history.append(f"agent: {text}")
     return text
     case Call(tool, arg):
     fn = self.tools.get(tool)
     result = fn(arg) if fn else f"no such tool: {tool}"
     self.history.append(f"tool[{tool}]: {result}")
     # loop again: the model sees the result and decides what's next

    Read it as the cycle:

    • Observe: append the input.
    • Think: ask the model.
    • Act: if it asked for a tool, run the tool.
    • Check and repeat: feed the result back into the history and loop, so the model sees what happened and decides whether it needs another tool or can finally answer.

    There is no separate "check" block in the code. The check happens implicitly when the loop restarts and calls respond again with the new history. That step is the one that matters, because a model has no native sense of when a job is finished, and nothing stops it from asking for one more tool forever. The loop keeps going until the model returns Say instead of Call.

    To run the whole thing without an API key, swap in a fake model and a real tool:

    from pathlib import Path
    
    def read_file(path: str) -> str:
     try:
     return f"{len(Path(path).read_text())} bytes"
     except OSError as e:
     return f"error: {e}"
    
    class FakeModel:
     def respond(self, system: str, history: list[str]) -> Reply:
     last = history[-1] if history else ""
     if last.startswith("tool["):
     return Say(f"Done: {last}")
     if last.startswith("user: read "):
     return Call("read_file", last.removeprefix("user: read ").strip())
     return Say("I can read files. Try: read <path>")

    Wire it into a small main that builds the agent, reads a line, calls agent.run, and prints the reply:

    def main() -> None:
     agent = Agent(
     model=FakeModel(),
     system="You can read files.",
     tools={"read_file": read_file},
     )
     while True:
     try:
     line = input("> ")
     except EOFError:
     break
     print(agent.run(line.strip()))
    
    if __name__ == "__main__":
     main()

    Now you can talk to it with no API key. Run it with python agent.py and type at the prompt:

    > read pyproject.toml
    Done: tool[read_file]: 76 bytes

    That one exchange is a complete agent loop: the model asked for a tool, the loop ran it, fed the byte count back, and the model wrapped up on the second pass. The main thing standing between it and a real one is replacing FakeModel.respond with an HTTP call that returns the same Reply.

    The whole thing as one runnable file is here as a GitHub gist. Save it, run python agent.py, and type at the prompt.

    What this earns you

    Sure, this is a simplified example, and the hard parts are exactly what FakeModel stubs out: prompt design, retries, tool schemas, context compaction, error recovery, and termination guards that stop the loop when a model keeps hallucinating tools. But the core of an agent is 60 lines and easy to reason about. The engineering lives in the control layer around the model.

    Build the loop by hand once and frameworks stop feeling magical. LangChain’s agent executor, AutoGen’s shared memory, a coding agent’s plan mode are all variations on these same five parts: engineering tradeoffs, not magic.

    Keep reading

    Planet Python

    Datadock

    https://datadock.devium.be/assets/og-image.png

    Organized connections

    Projects → environment folders → connections. Color-coded, encrypted at rest, shareable, with a read-only safe mode.

    Seven engines

    PostgreSQL · MySQL/MariaDB · SQLite · SQL Server · MongoDB · Redis · InfluxDB — all from one app.

    Redis & live queues

    Browse keys by prefix, inspect any value type, run raw commands — plus a realtime queue dashboard with framework auto-detect (Laravel/Horizon · BullMQ · Sidekiq · RQ · Celery).

    SSH tunneling

    Reach databases behind a bastion via private key, password or agent.

    Spreadsheet-style editing

    Paginate, sort, filter, inline- and bulk-edit — every change committed in a transaction.

    Visual exploration

    Click-through foreign keys, a record Explorer, an interactive ER diagram and a dependency map.

    Structure editor

    Create/drop tables and edit columns, types, foreign keys & indexes — no hand-written DDL.

    Query, your way

    Multi-tab editor, schema-aware autocomplete, history, snippets, variables, formatter & EXPLAIN.

    Built-in AI

    NL→SQL, explain, fix-with-AI and chat-with-your-data — Claude, Gemini, Mistral, Grok or Ollama.

    Performance & insights

    Slow-query dashboard, index hints, pool diagnostics, table sizes & column search.

    Import & export

    CSV · Excel · JSON · SQL · zipped whole-DB dumps · result → new table.

    Data masking

    Anonymize columns with realistic fake data on export — safely copy production into a local database.

    Server tools

    Databases, users & roles and a process list (with kill).

    Comfortable to live in

    Dark / light themes, a ⌘K command palette and collapsible panels.

    Laravel News Links

    Print in Place Planetary Gear Spinner #3DThursday #3DPrinting

    https://cdn-blog.adafruit.com/uploads/2026/06/2023-11-06_8df0f406c74fc.png.webp

    Shared by Hamstah on MakerWorld:

    A print-in-place planetary gear spinner that rotates freely straight off the plate. Two profiles included — standard and heavy outer ring (43g) for added inertia

    Download the files and learn more


    649-1
    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!

    3D printing – Adafruit Industries – Makers, hackers, artists, designers and engineers!

    How to run Minecraft: Bedrock Edition on a Mac

    https://photos5.appleinsider.com/gallery/67946-143244-macosminecraft1-xl.jpg

    Playing Minecraft is better with Bedrock, but it’s not directly available for macOS. Here’s how to get around the limitation and improve your building experience.

    Minecraft is one of the longest-running online games that is still actively being played by a lot of people. Originally playable since 2009 and officially released in 2011, it has stood the test of time.

    However, while it has been improved over the years, Mac gamers have missed out on one important update: Bedrock Edition. It’s a version that is available on many other platforms, but never made its way to macOS.

    Though you can continue using the original Java version on Mac, it is possible to get Bedrock working. It takes a little work, a GitHub project, and buying the game from the Google Play Store.

    What is Minecraft Bedrock?

    The original version of Minecraft was made in Java, which enabled it to be easily ported to multiple platforms without much trouble. It’s also a version that is very easily modded by the community, without necessarily requiring permission from Microsoft beforehand.

    However, the Java edition has a key issue, in that it’s not natively built for any specific platforms. It wasn’t really intended to build a game as complex as Minecraft at all.

    As an interpreted language, Java has to be compiled into an intermediate "bytecode" format before being run by a Java Virtual Machine. This just-in-time interpretation means that processing performance is impacted directly compared to a compiled native version.

    There is also the issue of the earliest iterations of Minecraft being developed by Markus "Notch" Persson, and later by his studio, Mojang. Eventually, Microsoft got involved with its purchase of the studio in 2014.

    Man in glasses onstage beside large screen showing Minecraft city scene with tall blocky buildings, trees, villagers, and cloudy sky, suggesting a presentation about the game or technology

    Tim Cook introduced Minecraft on Apple TV in 2016. It survived until 2018.

    Since it was made by Notch alone at first, it meant that there were elements of code that he would be able to manage, but a team of developers would struggle with. After years of development, there was enough technical debt to prompt a rethink by those managing the game.

    Cue the development of a C++ version, which started off with a demo of Pocket Edition in 2011. Over time, the codebase was expanded and improved upon, until it was rebranded as Bedrock Edition in 2022.

    With that change, it became a more widely available version, including a release for Windows. The change also made it possible to create versions of Minecraft for other platforms, and for the games to more easily communicate with each other between different platforms.

    The change also meant Microsoft could incorporate an in-game store, monetizing their expensive acquisition, as well as other elements.

    Working around the limits

    While there’s Minecraft: Bedrock Edition for Windows, Xbox, PlayStation, Nintendo Switch, Android, and iOS, there is not a specific macOS version. You also can’t use the workaround of buying the iOS version and trying the iPadOS game in macOS, as that has been disabled.

    There’s nothing wrong with sticking to the Java edition of Minecraft on your Mac, but there are ways to use the Bedrock edition. Just not by officially buying a macOS app.

    A legitimate way of doing it is through using Windows on your Mac. Software like Parallels will let you run the Windows version of Minecraft Bedrock, but you again get that dreaded performance penalty.

    Three Minecraft launcher windows on a brown abstract background, showing installation instructions, a Microsoft account sign in screen, and an error message with green and red buttons

    Phases of installing the Minecraft Launcher

    There’s also the possibility of sideloading an iOS or iPadOS version, but we’d rather not anger Apple with that method.

    Another way is to use the Linux Minecraft Launcher. There’s a build available for macOS, which works using the Android version of the game.

    If you happen to have a Google account with Minecraft Bedrock already on it, you can use that. If not, you will have to pay for it from the Google Play Store.

    This can be a bit tricky if you don’t have an Android device on the account. By running the launcher and trying to download the game without the purchased version on your Google account, it will come up as a device under the Google Play Store.

    How to run Minecraft Bedrock Edition on a Mac using Linux Minecraft Launcher

    • Download the macOS launcher from GitHub.
    • Open the DMG. Drag the Minecraft Bedrock Launcher to the Applications folder shortcut. After the transfer, you can close the installer and unmount the DMG.
    • Open Minecraft Bedrock Launcher. If you’re blocked from opening, head to System Settings then Privacy & Security, then next to the blocked app warning, click Open Anyway.
    • On the Linux Minecraft Launcher changelog, click Continue.
    • Log into the Google account associated with the Android game’s purchase. You will be asked to create a password to save the credentials, then click Save & Complete Login.
    • Click Download And Play.

    Once completed, the game will run in a window, which you can make larger from the edges. There are also video settings available, both in a menu at the top and in the game’s settings.

    Minecraft game window open on a computer desktop, showing the title screen with a grassy field, flowers, villagers, and a copper golem-like figure in the center with Start Game button

    You should see this if installing the Minecraft launcher goes correctly.

    Feel free to push things like the draw distance and frame rate up, as well as the resolution. It’s arguably one of the best features of Bedrock edition over Java, and you can use it to the fullest on your Mac desktop.

    Now, go mine some redstone.

    AppleInsider News

    See what your database is doing right now with Connections

    https://planetscale.com/assets/see-what-your-database-is-doing-right-now-social-B8T8hdB0.png

    See what your database is doing right now with Connections

    Brett Warminski |

    Much of database debugging eventually turns into carefully inspecting what each connection is doing. In Postgres, this means watching pg_stat_activity in a loop. In Vitess, it means watching SHOW FULL PROCESSLIST the same way.

    Tools like Query Insights are useful for exploring the recent history of queries. They can tell you what was slow, what’s consuming resources, and where to spend tuning effort.

    But during an active incident, the questions are more immediate. What’s happening this second? Did the last thing I changed fix it?

    Here’s a manual version of this workflow in Postgres:

    SELECT pid, state, wait_event_type, wait_event,
     now() - xact_start AS tx_age,
     pg_blocking_pids(pid) AS blocked_by,
     left(query, 60) AS query
    FROM pg_stat_activity
    WHERE state <> 'idle'
    ORDER BY tx_age DESC;
    

    Run it over and over again in a terminal and it’s a pretty effective view of the database.

    It’s also a rough interface.

    You’re scanning rows as they move around, trying to reconstruct what’s blocking progress, and hunting for the one detail that actually matters for the fix.

    The worst version of this problem is when you can’t connect at all because the database has exhausted all of its connections. You can’t fix what you can’t connect to.

    That workflow shaped the design of Connections, a new feature of the pscale CLI available today for PlanetScale Postgres and Vitess (MySQL) databases.

    Here’s that same debugging flow using the new pscale branch connections top functionality with a Postgres database, instead of pasting that pg_stat_activity query in a loop and comparing output:

    pscale branch connections top <database> <branch>
    

    Connections opens an interactive live view that refreshes about once a second and sorts the sessions most likely to matter toward the top. There are keyboard shortcuts to navigate the list of connections and inspect each one in more detail.

    Columns in the list include the Process ID (PID), status, number of blocked queries, why they’re waiting, and more.

    The pscale branch connections top view, with a stuck checkout transaction at the top

    Say your writes are backing up and the app is timing out. In this example, an idle transaction from checkout-api is holding up three other writes. Open the row, and the blocker tree shows the queue behind it:

    The blocker tree: one idle checkout-api transaction holding up the refund, payment, and cancel updates queued behind it

    From there you can decide whether the right fix is to cancel a query or terminate the connection. You no longer need to remember the syntax of pg_stat_activity, retrace the blocker chain by hand or copy and paste PIDs around.

    Another problem with running that query in a loop is that the interesting moment flies by. Connections keeps a recent rolling history, so you can pause, step forward and backward with [ and ], and see how the state has changed.

    You can also capture a session to a file. You can record everything you see in Connections by pressing C. This includes the recent history already buffered in memory and keeps appending from there. Perfect for handing off logs to agents to assist with debugging.

    That also makes it easier to write a postmortem, share what happened with a teammate, or replay the same view later instead of describing it from memory.

    The stress of debugging an active incident is worse when you can’t even connect to the database yourself.

    Connections uses a reserved administrative connection, so the inspection path still works when regular application connections are exhausted.

    Managed databases should remove the need to SSH into a box, not remove your ability to debug an incident.

    You can still get in, see what is running, and act from there.

    The PlanetScale CLI’s new Connections feature also works with Vitess databases (MySQL). In this case, the live view is the PlanetScale version of watching SHOW FULL PROCESSLIST, with the ability to cancel the current query or terminate the connection from this unified interface.

    The main difference is scope. Vitess connections are shown for one keyspace (and one shard) at a time. If a branch has multiple keyspaces, or a sharded keyspace, pass --keyspace and --shard to choose the tablet:

    pscale branch connections top <database> <branch> --keyspace <keyspace> --shard <shard>
    

    The same live monitoring, pause, history, capture, and replay workflow applies. The actions are MySQL-specific: canceling a query runs KILL QUERY, and terminating a connection runs KILL. See the Inspect live Vitess connections guide for the full command behavior.

    Connections is available for PlanetScale Postgres and Vitess. Update to the latest version of pscale and run:

    pscale branch connections top <database> <branch>
    

    See the CLI reference, the Inspect live Postgres connections guide, and the Inspect live Vitess connections guide for more details.

    Try it next time you need to troubleshoot active database connections.

    Planet for the MySQL Community