Prerequisites
- Basic understanding of programming concepts ๐
- Python installation (3.8+) ๐
- VS Code or preferred IDE ๐ป
What you'll learn
- Understand the concept fundamentals ๐ฏ
- Apply the concept in real projects ๐๏ธ
- Debug common issues ๐
- Write clean, Pythonic code โจ
๐ฏ Introduction
Welcome to this exciting tutorial on Query Optimization with Indexes and Joins! ๐ Have you ever wondered why some database queries run in milliseconds while others take forever? Today, weโll unlock the secrets of lightning-fast database operations! โก
Youโll discover how indexes and joins can transform your database performance from a sluggish snail ๐ to a speedy cheetah ๐. Whether youโre building e-commerce platforms ๐, analytics dashboards ๐, or social networks ๐, understanding query optimization is essential for creating responsive applications that users love!
By the end of this tutorial, youโll be optimizing queries like a database wizard! Letโs dive in! ๐โโ๏ธ
๐ Understanding Query Optimization
๐ค What is Query Optimization?
Query optimization is like organizing a massive library ๐. Imagine searching for a specific book in a library with millions of books scattered randomly - it would take forever! But with a good cataloging system (indexes) and efficient pathways between sections (joins), you can find any book in seconds!
In database terms, query optimization involves:
- โจ Creating indexes for fast data retrieval
- ๐ Writing efficient join operations
- ๐ก๏ธ Avoiding performance bottlenecks
๐ก Why Optimize Queries?
Hereโs why developers obsess over query optimization:
- Speed Matters โก: Users expect instant responses
- Scale Efficiently ๐: Handle millions of records without breaking a sweat
- Save Resources ๐ฐ: Reduce server costs and database load
- Better User Experience ๐: Happy users = successful application
Real-world example: Imagine an online store ๐ during Black Friday. Without query optimization, searching for products could take minutes. With proper indexes and joins, customers find products instantly, leading to more sales! ๐ธ
๐ง Basic Syntax and Usage
๐ Creating Indexes in Python
Letโs start with creating indexes using SQLite:
import sqlite3
import time
# ๐ Connect to database
conn = sqlite3.connect('store.db')
cursor = conn.cursor()
# ๐จ Create a products table
cursor.execute('''
CREATE TABLE IF NOT EXISTS products (
id INTEGER PRIMARY KEY,
name TEXT,
category TEXT,
price REAL,
stock INTEGER
)
''')
# ๐ Create an index on category for faster searches
cursor.execute('''
CREATE INDEX IF NOT EXISTS idx_category
ON products(category)
''')
# โก Create a composite index for price range queries
cursor.execute('''
CREATE INDEX IF NOT EXISTS idx_category_price
ON products(category, price)
''')
print("โ
Indexes created successfully!")
๐ก Explanation: Indexes are like bookmarks ๐ in your database. They help the database engine jump directly to the data you need!
๐ฏ Basic Join Operations
Hereโs how to perform efficient joins:
# ๐๏ธ Create related tables
cursor.execute('''
CREATE TABLE IF NOT EXISTS customers (
id INTEGER PRIMARY KEY,
name TEXT,
email TEXT
)
''')
cursor.execute('''
CREATE TABLE IF NOT EXISTS orders (
id INTEGER PRIMARY KEY,
customer_id INTEGER,
product_id INTEGER,
quantity INTEGER,
order_date TEXT,
FOREIGN KEY (customer_id) REFERENCES customers(id),
FOREIGN KEY (product_id) REFERENCES products(id)
)
''')
# ๐ Efficient INNER JOIN query
query = '''
SELECT
c.name AS customer_name,
p.name AS product_name,
o.quantity,
p.price * o.quantity AS total_price
FROM orders o
INNER JOIN customers c ON o.customer_id = c.id
INNER JOIN products p ON o.product_id = p.id
WHERE p.category = ?
ORDER BY o.order_date DESC
'''
# ๐ฏ Execute with parameter (prevents SQL injection!)
cursor.execute(query, ('Electronics',))
results = cursor.fetchall()
๐ก Practical Examples
๐ Example 1: E-Commerce Product Search
Letโs build an optimized product search system:
class ProductSearchOptimizer:
def __init__(self, db_path='store.db'):
self.conn = sqlite3.connect(db_path)
self.cursor = self.conn.cursor()
self.setup_database()
def setup_database(self):
# ๐จ Create tables with optimized structure
self.cursor.execute('''
CREATE TABLE IF NOT EXISTS products (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
description TEXT,
category TEXT NOT NULL,
price REAL NOT NULL,
rating REAL,
stock INTEGER DEFAULT 0
)
''')
# ๐ Create strategic indexes
indexes = [
'CREATE INDEX IF NOT EXISTS idx_name ON products(name)',
'CREATE INDEX IF NOT EXISTS idx_category ON products(category)',
'CREATE INDEX IF NOT EXISTS idx_price ON products(price)',
'CREATE INDEX IF NOT EXISTS idx_rating ON products(rating DESC)',
'CREATE INDEX IF NOT EXISTS idx_category_price_rating ON products(category, price, rating DESC)'
]
for index in indexes:
self.cursor.execute(index)
print("โ
Database optimized for searches!")
def search_products(self, category=None, min_price=None, max_price=None, min_rating=None):
# ๐ง Build optimized query dynamically
query = "SELECT * FROM products WHERE 1=1"
params = []
if category:
query += " AND category = ?"
params.append(category)
if min_price is not None:
query += " AND price >= ?"
params.append(min_price)
if max_price is not None:
query += " AND price <= ?"
params.append(max_price)
if min_rating is not None:
query += " AND rating >= ?"
params.append(min_rating)
# ๐ Order by rating for best results first
query += " ORDER BY rating DESC, price ASC"
# โก Execute optimized query
start_time = time.time()
self.cursor.execute(query, params)
results = self.cursor.fetchall()
end_time = time.time()
print(f"๐ฏ Found {len(results)} products in {(end_time - start_time)*1000:.2f}ms!")
return results
def analyze_query_performance(self, query):
# ๐ Use EXPLAIN QUERY PLAN to analyze
self.cursor.execute(f"EXPLAIN QUERY PLAN {query}")
plan = self.cursor.fetchall()
print("๐ Query Execution Plan:")
for step in plan:
print(f" {'๐' if 'INDEX' in str(step) else '๐'} {step}")
# ๐ฎ Let's use it!
optimizer = ProductSearchOptimizer()
# ๐๏ธ Add sample products
sample_products = [
('Gaming Laptop', 'High-performance laptop', 'Electronics', 999.99, 4.5, 50),
('Wireless Mouse', 'Ergonomic design', 'Electronics', 29.99, 4.2, 200),
('Python Book', 'Learn Python programming', 'Books', 39.99, 4.8, 100),
]
optimizer.cursor.executemany(
'INSERT OR IGNORE INTO products (name, description, category, price, rating, stock) VALUES (?, ?, ?, ?, ?, ?)',
sample_products
)
# ๐ฏ Perform optimized searches
results = optimizer.search_products(category='Electronics', min_price=20, max_price=1000, min_rating=4.0)
๐ฏ Try it yourself: Add a full-text search feature using FTS5 (Full-Text Search) for product names and descriptions!
๐ Example 2: Analytics Dashboard with Complex Joins
Letโs create an optimized analytics system:
class AnalyticsDashboard:
def __init__(self):
self.conn = sqlite3.connect(':memory:') # ๐จ In-memory for speed!
self.cursor = self.conn.cursor()
self.setup_analytics_db()
def setup_analytics_db(self):
# ๐๏ธ Create optimized schema
tables = [
'''CREATE TABLE users (
id INTEGER PRIMARY KEY,
name TEXT,
country TEXT,
joined_date TEXT
)''',
'''CREATE TABLE events (
id INTEGER PRIMARY KEY,
user_id INTEGER,
event_type TEXT,
timestamp TEXT,
FOREIGN KEY (user_id) REFERENCES users(id)
)''',
'''CREATE TABLE purchases (
id INTEGER PRIMARY KEY,
user_id INTEGER,
amount REAL,
product_category TEXT,
timestamp TEXT,
FOREIGN KEY (user_id) REFERENCES users(id)
)'''
]
for table in tables:
self.cursor.execute(table)
# ๐ Create strategic indexes for analytics queries
analytics_indexes = [
'CREATE INDEX idx_users_country ON users(country)',
'CREATE INDEX idx_users_joined ON users(joined_date)',
'CREATE INDEX idx_events_user_type ON events(user_id, event_type)',
'CREATE INDEX idx_events_timestamp ON events(timestamp)',
'CREATE INDEX idx_purchases_user ON purchases(user_id)',
'CREATE INDEX idx_purchases_category ON purchases(product_category)',
'CREATE INDEX idx_purchases_timestamp ON purchases(timestamp)'
]
for index in analytics_indexes:
self.cursor.execute(index)
print("๐ Analytics database ready!")
def get_user_engagement_metrics(self, start_date, end_date):
# ๐ฏ Complex query with multiple joins
query = '''
WITH user_activity AS (
SELECT
u.id,
u.name,
u.country,
COUNT(DISTINCT e.id) as event_count,
COUNT(DISTINCT DATE(e.timestamp)) as active_days
FROM users u
LEFT JOIN events e ON u.id = e.user_id
WHERE e.timestamp BETWEEN ? AND ?
GROUP BY u.id
),
user_revenue AS (
SELECT
user_id,
SUM(amount) as total_spent,
COUNT(*) as purchase_count,
AVG(amount) as avg_purchase
FROM purchases
WHERE timestamp BETWEEN ? AND ?
GROUP BY user_id
)
SELECT
ua.name,
ua.country,
ua.event_count,
ua.active_days,
COALESCE(ur.total_spent, 0) as revenue,
COALESCE(ur.purchase_count, 0) as purchases,
COALESCE(ur.avg_purchase, 0) as avg_order_value
FROM user_activity ua
LEFT JOIN user_revenue ur ON ua.id = ur.user_id
ORDER BY revenue DESC
'''
# โก Execute with timing
start_time = time.time()
self.cursor.execute(query, (start_date, end_date, start_date, end_date))
results = self.cursor.fetchall()
execution_time = (time.time() - start_time) * 1000
print(f"โจ Generated metrics for {len(results)} users in {execution_time:.2f}ms!")
return results
def create_materialized_view(self):
# ๐ Create a materialized view for super-fast queries
self.cursor.execute('''
CREATE TABLE IF NOT EXISTS daily_user_stats AS
SELECT
u.id as user_id,
DATE(e.timestamp) as date,
COUNT(e.id) as events,
SUM(p.amount) as revenue
FROM users u
LEFT JOIN events e ON u.id = e.user_id
LEFT JOIN purchases p ON u.id = p.user_id
AND DATE(p.timestamp) = DATE(e.timestamp)
GROUP BY u.id, DATE(e.timestamp)
''')
# ๐ Index the materialized view
self.cursor.execute('CREATE INDEX idx_daily_stats ON daily_user_stats(user_id, date)')
print("๐ Materialized view created for lightning-fast queries!")
# ๐ฎ Test our analytics system
dashboard = AnalyticsDashboard()
๐ Advanced Concepts
๐งโโ๏ธ Query Plan Analysis and Optimization
When youโre ready to level up, master query plan analysis:
class QueryOptimizationWizard:
def __init__(self):
self.conn = sqlite3.connect(':memory:')
self.cursor = self.conn.cursor()
def analyze_query_performance(self, query, params=()):
# ๐ Get query plan
explain_query = f"EXPLAIN QUERY PLAN {query}"
self.cursor.execute(explain_query, params)
plan = self.cursor.fetchall()
# ๐ Analyze the plan
optimization_score = 100
suggestions = []
for step in plan:
step_detail = str(step)
if 'SCAN' in step_detail and 'INDEX' not in step_detail:
optimization_score -= 20
suggestions.append("๐ Table scan detected! Consider adding an index.")
if 'TEMP B-TREE' in step_detail:
optimization_score -= 10
suggestions.append("โ ๏ธ Temporary B-tree created. Consider optimizing ORDER BY.")
if 'USING INDEX' in step_detail:
suggestions.append("โ
Good! Using index for this operation.")
# ๐ฏ Provide optimization report
print(f"\n๐ Query Optimization Report:")
print(f"Score: {optimization_score}/100 {'๐' if optimization_score >= 80 else 'โก'}")
print("\n๐ก Suggestions:")
for suggestion in suggestions:
print(f" {suggestion}")
return optimization_score
def suggest_indexes(self, table_name):
# ๐งโโ๏ธ AI-like index suggestions based on query patterns
print(f"\n๐ฎ Analyzing table '{table_name}' for index opportunities...")
# Get table schema
self.cursor.execute(f"PRAGMA table_info({table_name})")
columns = self.cursor.fetchall()
suggestions = []
for col in columns:
col_name = col[1]
col_type = col[2]
# ๐ฏ Smart suggestions based on column patterns
if col_name.endswith('_id'):
suggestions.append(f"CREATE INDEX idx_{table_name}_{col_name} ON {table_name}({col_name})")
elif col_name in ['created_at', 'updated_at', 'timestamp', 'date']:
suggestions.append(f"CREATE INDEX idx_{table_name}_{col_name} ON {table_name}({col_name} DESC)")
elif col_name in ['status', 'type', 'category']:
suggestions.append(f"CREATE INDEX idx_{table_name}_{col_name} ON {table_name}({col_name})")
print("\nโจ Recommended indexes:")
for suggestion in suggestions:
print(f" {suggestion}")
return suggestions
# ๐ช Test the wizard
wizard = QueryOptimizationWizard()
๐๏ธ Advanced Join Strategies
Master complex join patterns:
class AdvancedJoinMaster:
def __init__(self):
self.conn = sqlite3.connect(':memory:')
self.cursor = self.conn.cursor()
def demonstrate_join_types(self):
# ๐จ Setup sample data
self.setup_sample_data()
print("๐ Advanced Join Techniques:\n")
# 1๏ธโฃ Self-join for hierarchical data
print("1๏ธโฃ Self-Join (Finding employee managers):")
self.cursor.execute('''
SELECT
e1.name as employee,
e2.name as manager
FROM employees e1
LEFT JOIN employees e2 ON e1.manager_id = e2.id
''')
# 2๏ธโฃ Multiple joins with subqueries
print("\n2๏ธโฃ Complex Join with Subquery:")
self.cursor.execute('''
SELECT
c.name,
c.total_orders,
c.total_spent,
CASE
WHEN c.total_spent > 1000 THEN '๐ VIP'
WHEN c.total_spent > 500 THEN 'โญ Gold'
ELSE '๐ค Regular'
END as customer_tier
FROM (
SELECT
cu.id,
cu.name,
COUNT(o.id) as total_orders,
SUM(o.amount) as total_spent
FROM customers cu
LEFT JOIN orders o ON cu.id = o.customer_id
GROUP BY cu.id
) c
ORDER BY c.total_spent DESC
''')
# 3๏ธโฃ Window functions with joins
print("\n3๏ธโฃ Window Functions (Running totals):")
self.cursor.execute('''
WITH sales_data AS (
SELECT
DATE(o.order_date) as date,
SUM(o.amount) as daily_total
FROM orders o
GROUP BY DATE(o.order_date)
)
SELECT
date,
daily_total,
SUM(daily_total) OVER (ORDER BY date) as running_total
FROM sales_data
''')
def setup_sample_data(self):
# ๐๏ธ Create tables for demonstrations
tables = [
'''CREATE TABLE employees (
id INTEGER PRIMARY KEY,
name TEXT,
manager_id INTEGER
)''',
'''CREATE TABLE customers (
id INTEGER PRIMARY KEY,
name TEXT,
email TEXT
)''',
'''CREATE TABLE orders (
id INTEGER PRIMARY KEY,
customer_id INTEGER,
amount REAL,
order_date TEXT
)'''
]
for table in tables:
self.cursor.execute(table)
โ ๏ธ Common Pitfalls and Solutions
๐ฑ Pitfall 1: Over-Indexing
# โ Wrong way - too many indexes!
cursor.execute('''
CREATE INDEX idx_col1 ON table(col1);
CREATE INDEX idx_col2 ON table(col2);
CREATE INDEX idx_col3 ON table(col3);
CREATE INDEX idx_col4 ON table(col4);
CREATE INDEX idx_col5 ON table(col5);
''') # ๐ฅ Slows down INSERT/UPDATE operations!
# โ
Correct way - strategic indexing!
cursor.execute('''
-- Index only frequently queried columns
CREATE INDEX idx_user_email ON users(email);
-- Composite index for common query patterns
CREATE INDEX idx_orders_user_date ON orders(user_id, order_date);
-- Covering index for specific queries
CREATE INDEX idx_products_search ON products(category, price, name);
''') # ๐ Balanced performance!
๐คฏ Pitfall 2: N+1 Query Problem
# โ Dangerous - N+1 queries!
def get_users_with_orders_bad():
users = cursor.execute("SELECT * FROM users").fetchall()
for user in users:
# ๐ฅ This runs a query for EACH user!
orders = cursor.execute(
"SELECT * FROM orders WHERE user_id = ?",
(user[0],)
).fetchall()
print(f"{user[1]} has {len(orders)} orders")
# โ
Efficient - single query with JOIN!
def get_users_with_orders_good():
cursor.execute('''
SELECT u.name, COUNT(o.id) as order_count
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
GROUP BY u.id
''')
for row in cursor.fetchall():
print(f"{row[0]} has {row[1]} orders") # โ
Much faster!
๐ ๏ธ Best Practices
- ๐ฏ Index Strategically: Index columns used in WHERE, JOIN, and ORDER BY clauses
- ๐ Monitor Query Performance: Use EXPLAIN QUERY PLAN regularly
- ๐ Use Prepared Statements: Prevent SQL injection and improve performance
- ๐พ Consider Denormalization: Sometimes duplicate data for better read performance
- ๐ Regular Maintenance: Update statistics and rebuild indexes periodically
๐งช Hands-On Exercise
๐ฏ Challenge: Build a High-Performance Blog System
Create an optimized blog database with these requirements:
๐ Requirements:
- โ Posts with title, content, author, and tags
- ๐ท๏ธ Categories and tag system with many-to-many relationships
- ๐ฌ Comments with nested replies
- ๐ View tracking and analytics
- ๐ Full-text search capability
๐ Bonus Points:
- Implement pagination with cursor-based navigation
- Add caching layer for popular posts
- Create monthly archive views
๐ก Solution
๐ Click to see solution
# ๐ฏ High-performance blog system!
class OptimizedBlogSystem:
def __init__(self):
self.conn = sqlite3.connect('blog.db')
self.cursor = self.conn.cursor()
self.setup_schema()
def setup_schema(self):
# ๐๏ธ Create optimized tables
self.cursor.executescript('''
-- Users table
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY,
username TEXT UNIQUE NOT NULL,
email TEXT UNIQUE NOT NULL,
created_at TEXT DEFAULT CURRENT_TIMESTAMP
);
-- Posts table with FTS5 for full-text search
CREATE VIRTUAL TABLE IF NOT EXISTS posts USING fts5(
title, content, author_id UNINDEXED,
created_at UNINDEXED,
tokenize='porter'
);
-- Regular posts table for relationships
CREATE TABLE IF NOT EXISTS posts_meta (
id INTEGER PRIMARY KEY,
author_id INTEGER NOT NULL,
view_count INTEGER DEFAULT 0,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (author_id) REFERENCES users(id)
);
-- Tags table
CREATE TABLE IF NOT EXISTS tags (
id INTEGER PRIMARY KEY,
name TEXT UNIQUE NOT NULL
);
-- Many-to-many relationship
CREATE TABLE IF NOT EXISTS post_tags (
post_id INTEGER,
tag_id INTEGER,
PRIMARY KEY (post_id, tag_id),
FOREIGN KEY (post_id) REFERENCES posts_meta(id),
FOREIGN KEY (tag_id) REFERENCES tags(id)
);
-- Comments with self-referencing for replies
CREATE TABLE IF NOT EXISTS comments (
id INTEGER PRIMARY KEY,
post_id INTEGER NOT NULL,
user_id INTEGER NOT NULL,
parent_id INTEGER,
content TEXT NOT NULL,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (post_id) REFERENCES posts_meta(id),
FOREIGN KEY (user_id) REFERENCES users(id),
FOREIGN KEY (parent_id) REFERENCES comments(id)
);
-- ๐ Strategic indexes
CREATE INDEX IF NOT EXISTS idx_posts_author ON posts_meta(author_id);
CREATE INDEX IF NOT EXISTS idx_posts_created ON posts_meta(created_at DESC);
CREATE INDEX IF NOT EXISTS idx_comments_post ON comments(post_id);
CREATE INDEX IF NOT EXISTS idx_comments_parent ON comments(parent_id);
CREATE INDEX IF NOT EXISTS idx_post_tags_tag ON post_tags(tag_id);
''')
print("โ
Optimized blog schema created!")
def search_posts(self, query, limit=10):
# ๐ Lightning-fast full-text search
start_time = time.time()
self.cursor.execute('''
SELECT
p.rowid,
p.title,
snippet(posts, 1, '<b>', '</b>', '...', 20) as excerpt,
u.username as author,
pm.view_count
FROM posts p
JOIN posts_meta pm ON p.rowid = pm.id
JOIN users u ON pm.author_id = u.id
WHERE posts MATCH ?
ORDER BY rank
LIMIT ?
''', (query, limit))
results = self.cursor.fetchall()
search_time = (time.time() - start_time) * 1000
print(f"๐ Found {len(results)} posts in {search_time:.2f}ms!")
return results
def get_popular_posts_with_tags(self, days=30, limit=10):
# ๐ Complex query with multiple joins
query = '''
WITH recent_posts AS (
SELECT id
FROM posts_meta
WHERE created_at > datetime('now', '-' || ? || ' days')
)
SELECT
pm.id,
p.title,
u.username as author,
pm.view_count,
GROUP_CONCAT(t.name, ', ') as tags
FROM posts_meta pm
JOIN posts p ON pm.id = p.rowid
JOIN users u ON pm.author_id = u.id
LEFT JOIN post_tags pt ON pm.id = pt.post_id
LEFT JOIN tags t ON pt.tag_id = t.id
WHERE pm.id IN recent_posts
GROUP BY pm.id
ORDER BY pm.view_count DESC
LIMIT ?
'''
self.cursor.execute(query, (days, limit))
return self.cursor.fetchall()
def get_comment_tree(self, post_id):
# ๐ณ Recursive query for comment threads
self.cursor.execute('''
WITH RECURSIVE comment_tree AS (
-- Base case: top-level comments
SELECT
id, user_id, content, parent_id,
created_at, 0 as level
FROM comments
WHERE post_id = ? AND parent_id IS NULL
UNION ALL
-- Recursive case: replies
SELECT
c.id, c.user_id, c.content, c.parent_id,
c.created_at, ct.level + 1
FROM comments c
JOIN comment_tree ct ON c.parent_id = ct.id
)
SELECT * FROM comment_tree
ORDER BY created_at
''', (post_id,))
return self.cursor.fetchall()
# ๐ฎ Test the system!
blog = OptimizedBlogSystem()
# Add sample data and test search
blog.cursor.execute("INSERT INTO users (username, email) VALUES ('PythonPro', '[email protected]')")
blog.cursor.execute("INSERT INTO posts_meta (author_id) VALUES (1)")
blog.cursor.execute("INSERT INTO posts (title, content, author_id) VALUES ('Query Optimization Magic', 'Learn amazing optimization techniques!', 1)")
results = blog.search_posts('optimization')
print("๐ Blog system is blazing fast!")
๐ Key Takeaways
Youโve mastered query optimization! Hereโs what you can now do:
- โ Create strategic indexes that supercharge your queries ๐ช
- โ Write efficient joins that handle complex relationships ๐ก๏ธ
- โ Analyze query plans to identify bottlenecks ๐ฏ
- โ Avoid common pitfalls like N+1 queries and over-indexing ๐
- โ Build high-performance database applications! ๐
Remember: Query optimization is an art and a science. Always measure, never assume! ๐ค
๐ค Next Steps
Congratulations! ๐ Youโve become a query optimization wizard!
Hereโs what to do next:
- ๐ป Practice with the blog system exercise above
- ๐๏ธ Optimize your existing database projects
- ๐ Move on to our next tutorial: Database Migrations with Alembic
- ๐ Share your optimization wins with the community!
Remember: Every millisecond saved in query time makes users happier. Keep optimizing, keep learning, and most importantly, have fun making databases fly! ๐
Happy optimizing! ๐๐โจ