sql-toolkitQuery, design, migrate, and optimize SQL databases. Use when working with SQLite, PostgreSQL, or MySQL — schema design, writing queries, creating migrations, indexing, backup/restore, and debugging slow queries. No ORMs required.
Install via ClawdBot CLI:
clawdbot install gitgoodordietrying/sql-toolkitWork with relational databases directly from the command line. Covers SQLite, PostgreSQL, and MySQL with patterns for schema design, querying, migrations, indexing, and operations.
SQLite is included with Python and available on every system. Use it for local data, prototyping, and single-file databases.
# Create/open a database
sqlite3 mydb.sqlite
# Import CSV directly
sqlite3 mydb.sqlite ".mode csv" ".import data.csv mytable" "SELECT COUNT(*) FROM mytable;"
# One-liner queries
sqlite3 mydb.sqlite "SELECT * FROM users WHERE created_at > '2026-01-01' LIMIT 10;"
# Export to CSV
sqlite3 -header -csv mydb.sqlite "SELECT * FROM orders;" > orders.csv
# Interactive mode with headers and columns
sqlite3 -header -column mydb.sqlite
-- Create table
CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('now'))
);
-- Create with foreign key
CREATE TABLE orders (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
total REAL NOT NULL CHECK(total >= 0),
status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending','paid','shipped','cancelled')),
created_at TEXT DEFAULT (datetime('now'))
);
-- Add column
ALTER TABLE users ADD COLUMN phone TEXT;
-- Create index
CREATE INDEX idx_orders_user_id ON orders(user_id);
CREATE UNIQUE INDEX idx_users_email ON users(email);
-- View schema
.schema users
.tables
# Connect
psql -h localhost -U myuser -d mydb
# Connection string
psql "postgresql://user:pass@localhost:5432/mydb?sslmode=require"
# Run single query
psql -h localhost -U myuser -d mydb -c "SELECT NOW();"
# Run SQL file
psql -h localhost -U myuser -d mydb -f migration.sql
# List databases
psql -l
-- Use UUIDs for distributed-friendly primary keys
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
email TEXT NOT NULL,
name TEXT NOT NULL,
password_hash TEXT NOT NULL,
role TEXT NOT NULL DEFAULT 'user' CHECK(role IN ('user','admin','moderator')),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT users_email_unique UNIQUE(email)
);
-- Auto-update updated_at
CREATE OR REPLACE FUNCTION update_modified_column()
RETURNS TRIGGER AS $
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$ LANGUAGE plpgsql;
CREATE TRIGGER update_users_modtime
BEFORE UPDATE ON users
FOR EACH ROW EXECUTE FUNCTION update_modified_column();
-- Enum type (PostgreSQL-specific)
CREATE TYPE order_status AS ENUM ('pending', 'paid', 'shipped', 'delivered', 'cancelled');
CREATE TABLE orders (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
status order_status NOT NULL DEFAULT 'pending',
total NUMERIC(10,2) NOT NULL CHECK(total >= 0),
metadata JSONB DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Partial index (only index active orders — smaller, faster)
CREATE INDEX idx_orders_active ON orders(user_id, created_at)
WHERE status NOT IN ('delivered', 'cancelled');
-- GIN index for JSONB queries
CREATE INDEX idx_orders_metadata ON orders USING GIN(metadata);
-- Store JSON
INSERT INTO orders (user_id, total, metadata)
VALUES ('...', 99.99, '{"source": "web", "coupon": "SAVE10", "items": [{"sku": "A1", "qty": 2}]}');
-- Query JSON fields
SELECT * FROM orders WHERE metadata->>'source' = 'web';
SELECT * FROM orders WHERE metadata->'items' @> '[{"sku": "A1"}]';
SELECT metadata->>'coupon' AS coupon, COUNT(*) FROM orders GROUP BY 1;
-- Update JSON field
UPDATE orders SET metadata = jsonb_set(metadata, '{source}', '"mobile"') WHERE id = '...';
mysql -h localhost -u root -p mydb
mysql -h localhost -u root -p -e "SELECT NOW();" mydb
-- Auto-increment (not SERIAL)
CREATE TABLE users (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE,
name VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- JSON type (MySQL 5.7+)
CREATE TABLE orders (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT UNSIGNED NOT NULL,
metadata JSON,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
-- Query JSON
SELECT * FROM orders WHERE JSON_EXTRACT(metadata, '$.source') = 'web';
-- Or shorthand:
SELECT * FROM orders WHERE metadata->>'$.source' = 'web';
-- Inner join (only matching rows)
SELECT u.name, o.total, o.status
FROM users u
INNER JOIN orders o ON o.user_id = u.id
WHERE o.created_at > '2026-01-01';
-- Left join (all users, even without orders)
SELECT u.name, COUNT(o.id) AS order_count, COALESCE(SUM(o.total), 0) AS total_spent
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
GROUP BY u.id, u.name;
-- Self-join (find users with same email domain)
SELECT a.name, b.name, SPLIT_PART(a.email, '@', 2) AS domain
FROM users a
JOIN users b ON SPLIT_PART(a.email, '@', 2) = SPLIT_PART(b.email, '@', 2)
WHERE a.id < b.id;
-- Group by with having
SELECT status, COUNT(*) AS cnt, SUM(total) AS revenue
FROM orders
GROUP BY status
HAVING COUNT(*) > 10
ORDER BY revenue DESC;
-- Running total (window function)
SELECT date, revenue,
SUM(revenue) OVER (ORDER BY date) AS cumulative_revenue
FROM daily_sales;
-- Rank within groups
SELECT user_id, total,
RANK() OVER (PARTITION BY user_id ORDER BY total DESC) AS rank
FROM orders;
-- Moving average (last 7 entries)
SELECT date, revenue,
AVG(revenue) OVER (ORDER BY date ROWS BETWEEN 6 PRECEDING AND CURRENT ROW) AS ma_7
FROM daily_sales;
-- Readable multi-step queries
WITH monthly_revenue AS (
SELECT DATE_TRUNC('month', created_at) AS month,
SUM(total) AS revenue
FROM orders
WHERE status = 'paid'
GROUP BY 1
),
growth AS (
SELECT month, revenue,
LAG(revenue) OVER (ORDER BY month) AS prev_revenue,
ROUND((revenue - LAG(revenue) OVER (ORDER BY month)) /
NULLIF(LAG(revenue) OVER (ORDER BY month), 0) * 100, 1) AS growth_pct
FROM monthly_revenue
)
SELECT * FROM growth ORDER BY month;
-- Recursive CTE (org chart / tree traversal)
WITH RECURSIVE org_tree AS (
SELECT id, name, manager_id, 0 AS depth
FROM employees
WHERE manager_id IS NULL
UNION ALL
SELECT e.id, e.name, e.manager_id, t.depth + 1
FROM employees e
JOIN org_tree t ON e.manager_id = t.id
)
SELECT REPEAT(' ', depth) || name AS org_chart FROM org_tree ORDER BY depth, name;
#!/bin/bash
# migrate.sh - Run numbered SQL migration files
DB_URL="${1:?Usage: migrate.sh <db-url>}"
MIGRATIONS_DIR="./migrations"
# Create tracking table
psql "$DB_URL" -c "CREATE TABLE IF NOT EXISTS schema_migrations (
version TEXT PRIMARY KEY,
applied_at TIMESTAMPTZ DEFAULT NOW()
);"
# Run pending migrations in order
for file in $(ls "$MIGRATIONS_DIR"/*.sql | sort); do
version=$(basename "$file" .sql)
already=$(psql "$DB_URL" -tAc "SELECT 1 FROM schema_migrations WHERE version='$version';")
if [ "$already" = "1" ]; then
echo "SKIP: $version (already applied)"
continue
fi
echo "APPLY: $version"
psql "$DB_URL" -f "$file" && \
psql "$DB_URL" -c "INSERT INTO schema_migrations (version) VALUES ('$version');" || {
echo "FAILED: $version"
exit 1
}
done
echo "All migrations applied."
migrations/
001_create_users.sql
002_create_orders.sql
003_add_users_phone.sql
004_add_orders_metadata_index.sql
Each file:
-- 003_add_users_phone.sql
-- Up
ALTER TABLE users ADD COLUMN phone TEXT;
-- To reverse: ALTER TABLE users DROP COLUMN phone;
-- Show query plan
EXPLAIN SELECT * FROM orders WHERE user_id = '...' AND status = 'paid';
-- Show actual execution times
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT * FROM orders WHERE user_id = '...' AND status = 'paid';
What to look for:
Seq Scan on large tables → needs an indexNested Loop with large row counts → consider Hash Join (may need more work_mem)Rows Removed by Filter being high → index doesn't cover the filterANALYZE tablename; to update statistics-- Single column (most common)
CREATE INDEX idx_orders_user_id ON orders(user_id);
-- Composite (for queries filtering on both columns)
CREATE INDEX idx_orders_user_status ON orders(user_id, status);
-- Column ORDER matters: put equality filters first, range filters last
-- Covering index (includes data columns to avoid table lookup)
CREATE INDEX idx_orders_covering ON orders(user_id, status) INCLUDE (total, created_at);
-- Partial index (smaller, faster — only index what you query)
CREATE INDEX idx_orders_pending ON orders(user_id) WHERE status = 'pending';
-- Check unused indexes
SELECT schemaname, tablename, indexname, idx_scan
FROM pg_stat_user_indexes
WHERE idx_scan = 0 AND indexname NOT LIKE '%pkey%'
ORDER BY pg_relation_size(indexrelid) DESC;
EXPLAIN QUERY PLAN SELECT * FROM orders WHERE user_id = 5;
-- Look for: SCAN (bad) vs SEARCH USING INDEX (good)
# Full dump (custom format, compressed)
pg_dump -Fc -h localhost -U myuser mydb > backup.dump
# Restore
pg_restore -h localhost -U myuser -d mydb --clean --if-exists backup.dump
# SQL dump (portable, readable)
pg_dump -h localhost -U myuser mydb > backup.sql
# Dump specific tables
pg_dump -h localhost -U myuser -t users -t orders mydb > partial.sql
# Copy table to CSV
psql -c "\copy (SELECT * FROM users) TO 'users.csv' CSV HEADER"
# Backup (just copy the file, but use .backup for consistency)
sqlite3 mydb.sqlite ".backup backup.sqlite"
# Dump to SQL
sqlite3 mydb.sqlite .dump > backup.sql
# Restore from SQL
sqlite3 newdb.sqlite < backup.sql
# Dump
mysqldump -h localhost -u root -p mydb > backup.sql
# Restore
mysql -h localhost -u root -p mydb < backup.sql
TIMESTAMPTZ (not TIMESTAMP) in PostgreSQL for timezone-aware datesPRAGMA journal_mode=WAL; in SQLite for concurrent read performanceEXPLAIN before deploying any query that runs on large tables\d+ tablename shows columns, indexes, and size. \di+ lists all indexes with sizessqlite3 :memory: ".mode csv" ".import file.csv t" "SELECT ..."Generated Feb 24, 2026
An online retailer uses SQL Toolkit to analyze sales data, track customer behavior, and optimize inventory management. They run complex queries with joins and aggregations to generate sales reports, identify top-selling products, and forecast demand based on historical trends.
A clinic employs SQL Toolkit to manage patient databases, ensuring secure and efficient access to medical records. They design schemas with foreign keys for relational data, create backups for compliance, and use indexing to speed up queries for patient history and appointment scheduling.
A financial technology startup uses SQL Toolkit to handle high-volume transaction data, detecting fraud and ensuring regulatory compliance. They implement PostgreSQL with JSONB for flexible metadata storage, write migration scripts for schema updates, and optimize queries with EXPLAIN to maintain performance under load.
A software-as-a-service provider leverages SQL Toolkit to track user engagement and feature usage across their platform. They use SQLite for quick prototyping of analytics dashboards, migrate to PostgreSQL for production scalability, and create indexes to analyze user retention and conversion rates efficiently.
A logistics company uses SQL Toolkit to manage shipment databases, optimizing routes and inventory levels. They design MySQL schemas with timestamp columns for tracking, write queries to aggregate delivery times, and use backup/restore features to safeguard operational data against failures.
A business offers SQL-powered analytics as a service, where clients pay monthly fees for customized reports and data insights. They use SQL Toolkit to query client databases, generate visualizations, and provide recommendations based on query results, generating recurring revenue from ongoing subscriptions.
A consultancy firm uses SQL Toolkit to audit and optimize clients' database performance, charging project-based or hourly rates. They analyze slow queries, design efficient schemas, and implement indexing strategies, helping businesses reduce costs and improve application speed for increased revenue per engagement.
A service provider specializes in migrating data between different SQL databases or integrating systems, using SQL Toolkit to create and execute migration scripts. They charge fixed fees per migration project, ensuring data integrity and minimal downtime, with revenue from one-time or repeat client projects.
💬 Integration Tip
Integrate SQL Toolkit into existing workflows by using command-line scripts for automation, such as scheduling backups with cron jobs or embedding queries in CI/CD pipelines for data validation during deployments.
Use the @steipete/oracle CLI to bundle a prompt plus the right files and get a second-model review (API or browser) for debugging, refactors, design checks, or cross-validation.
Manage Things 3 via the `things` CLI on macOS (add/update projects+todos via URL scheme; read/search/list from the local Things database). Use when a user asks Clawdbot to add a task to Things, list inbox/today/upcoming, search tasks, or inspect projects/areas/tags.
Local search/indexing CLI (BM25 + vectors + rerank) with MCP mode.
Use when designing database schemas, writing migrations, optimizing SQL queries, fixing N+1 problems, creating indexes, setting up PostgreSQL, configuring EF Core, implementing caching, partitioning tables, or any database performance question.
Connect to Supabase for database operations, vector search, and storage. Use for storing data, running SQL queries, similarity search with pgvector, and managing tables. Triggers on requests involving databases, vector stores, embeddings, or Supabase specifically.
Design data models for construction projects. Create entity-relationship diagrams, define schemas, and generate database structures.