Showing posts with label Performance Tuning. Show all posts
Showing posts with label Performance Tuning. Show all posts

Using MongoDB Indexes for Query Optimization & Performance


Using Indexes in MongoDB: Magic Speed Boosters!

A Super-Fast Treasure Hunt Adventure For Beginners to Experts


Table of Contents

Imagine you have a huge library with 1 million books. To find a book about “dragons”, would you check every single book one by one? No way! You’d use the library index card system to jump straight to the right shelf.

In MongoDB, indexes are exactly that magic card system! They make your find(), sort(), and update() queries super fast from seconds to milliseconds.

Indexes are a key part of MongoDB query optimization, helping developers improve database indexing strategies and achieve powerful performance tuning even on large datasets.

This tutorial is a fun speed race, super easy for students, but packed with pro racing tricks for experts.

We’ll use:
Our Hero Academy database
mongosh and MongoDB Compass
Beginners to Experts

Let’s put on our racing shoes!



What is an Index? (Simple Explanation)

Part 1: What Are Indexes & Why Do You Need Them?

Without index → Collection Scan = Reading every page of every book
With index → Index Scan = Jump straight to the right page

Note: You will see the terms COLLSCAN and IXSCAN used throughout this tutorial. To avoid repeating the same explanation multiple times:

  • COLLSCAN = MongoDB scans every document in the collection (slow).
  • IXSCAN = MongoDB uses an index to jump directly to matching documents (fast).

This section explains the difference once so later parts of the tutorial can focus only on performance results.

Beginner Example:

You have 10,000 heroes. You want all heroes named “Priya”.
Without index: MongoDB checks all 10,000 heroes → slow
With index on name: MongoDB looks in the “name phone book” → instant!

How MongoDB Uses B-Trees

Expert Truth: Indexes use B-tree (or other structures) to store sorted keys. Queries become O(log n) instead of O(n).



Part 2: Creating Your First Index (Step-by-Step)

Step 1: Add Lots of Heroes (So We Can See the Speed Difference)

Beginner Warning: Inserting 100,000 documents may run slowly on a free MongoDB Atlas cluster. If you’re on a shared or low-tier cluster, reduce the number to 10,000 to avoid timeouts or delays.


use heroAcademy

// Let's add 100,000 random heroes (run this once!)
for(let i = 1; i <= 100000; i++) {
  db.heroes.insertOne({
    name: "Hero" + i,
    power: ["Fire", "Ice", "Speed", "Fly"][Math.floor(Math.random()*4)],
    level: Math.floor(Math.random() * 100) + 1,
    team: ["Alpha", "Beta", "Gamma"][Math.floor(Math.random()*3)],
    city: "City" + Math.floor(Math.random() * 50)
  })
}

Step 2: Create Index on level


db.heroes.createIndex({ level: 1 })

Output:


{ "createdCollectionAutomatically": false, "numIndexesBefore": 1, "numIndexesAfter": 2, "ok": 1 }

Magic! MongoDB now has a sorted list of all levels.

Direction:
1 = ascending (low to high)
-1 = descending (high to low)

Step 3: See the Speed Difference!

First, run without index (turn off any index or use different field):


db.heroes.find({ city: "City25" }).explain("executionStats")

You’ll see "stage": "COLLSCAN" → totalDocsExamined: ~100,000 → slow!

Now with index on level:


db.heroes.find({ level: 85 }).explain("executionStats")

You’ll see "stage": "IXSCAN" → totalDocsExamined: ~1000 → super fast!



Index Types Explained (With Examples)

Part 3: Types of Indexes- Choose Your Power-Up

Index TypeWhen to UseCommand Example
Single FieldSearch by one field (name, email)db.heroes.createIndex({ name: 1 })
CompoundSearch by multiple fields (team + level)db.heroes.createIndex({ team: 1, level: 1 })
UniqueNo duplicates (email, username)db.users.createIndex({ email: 1 }, { unique: true })
TextFull-text search ("fire power")db.heroes.createIndex({ power: "text" })
TTLAuto-delete old data (sessions, logs)db.sessions.createIndex({ createdAt: 1 }, { expireAfterSeconds: 3600 })
GeospatialLocation queriesdb.places.createIndex({ location: "2dsphere" })
HashedFor shardingdb.collection.createIndex({ field: "hashed" })

Most Useful for Beginners: Single & Compound
Pro Favorite: Compound

Index Order Matters!
Rule: Equality first, then sort last

Good: { team: 1, level: -1 }
Bad: { level: 1, team: 1 } if you usually filter by team first



Part 4: Using Indexes in Compass (Click & Speed!)

Open Compass → heroAcademy → heroes
Click "Indexes" tab
Click "Create Index"
Field: level, Type: 1 (ascending)
Name: level_1 (optional)
Click Create

Compass Create Index
You’ll see the index appear instantly!



Part 5: Common Index Commands


// List all indexes
db.heroes.getIndexes()

// Drop an index
db.heroes.dropIndex("level_1")

// Drop all non-_id indexes
db.heroes.dropIndexes()

// Text search example
db.articles.createIndex({ title: "text", content: "text" })
db.articles.find({ $text: { $search: "mongodb tutorial" } })


Part 6: Mini Project – Build a Super-Fast Hero Search!


// 1. Index for team + level queries
db.heroes.createIndex({ team: 1, level: -1 })  // Perfect for sorting leaderboards

// 2. Unique index on name (no duplicate heroes!)
db.heroes.createIndex({ name: 1 }, { unique: true })

// 3. Text index for power search
db.heroes.createIndex({ power: "text" })

// Now test speed!
db.heroes.find({ team: "Alpha" }).sort({ level: -1 }).limit(10)  // Instant leaderboard!

db.heroes.find({ $text: { $search: "fire" } })  // Find all fire heroes instantly

Beginner Win: Your app now feels like lightning!



Pro Tips for Users

Part 7: Pro Tips & Warnings

For students & Beginners

Start with one index on the field you search most
Use Compass → Indexes tab to see them
Always test with .explain()

For Medium Learners

Use compound indexes wisely (ESR rule: Equality, Sort, Range)


db.heroes.aggregate([{ $indexStats: {} }])

Hint force index (rarely needed):


db.heroes.find({ level: 50 }).hint({ level: 1 })

For Experts

Partial indexes (save space):


db.heroes.createIndex(
  { level: 1 },
  { partialFilterExpression: { isActive: true } }
)

Covered queries (super fast, no document fetch):
Need index on all needed fields + _id: 0 in projection

Collation for case-insensitive:


db.users.createIndex({ username: 1 }, { collation: { locale: "en", strength: 2 } })

Avoid over-indexing, it slows writes!
Warning: Every index makes inserts/updates slower (10-30%) but reads faster. Only index what you query!

Common Mistakes to Avoid

⚠ Over-indexing slows writes
⚠ Using wrong compound index order
⚠ Creating multiple text indexes (MongoDB allows only one)
⚠ Forgetting to check explain() before adding an index



Part 8: Cheat Sheet (Print & Stick!)

CommandWhat It Does
createIndex({ field: 1 })Create ascending index
createIndex({ a: 1, b: -1 })Compound index
createIndex({ field: "text" })Text search index
createIndex({ field: 1 }, { unique: true })No duplicates
getIndexes()List all indexes
dropIndex("name_1")Delete index
.explain("executionStats")See if index is used


Part 9: Real Performance Example (You Can Try!)

Before index:


// ~500ms on 100k docs
db.heroes.find({ level: 85 }).explain("executionStats")

After index:


// ~2ms!
db.heroes.find({ level: 85 }).explain("executionStats")

Speed boost: 250x faster!



Summary: MongoDB Indexes, Performance & Optimization

In this guide, you explored how MongoDB indexes dramatically improve query performance by replacing slow collection scans with optimized index scans. You also learned how to create single-field, compound, text, unique, TTL, and advanced indexes while understanding how B-tree structures help optimize data access. Index selection and design are essential for performance optimization in MongoDB, especially when handling large datasets or real-time applications. By applying the indexing strategies in this tutorial, you can significantly boost read speed, reduce query time, and improve overall database efficiency.

This entire guide helps you build a strong foundation in MongoDB query optimization, database indexing design, and performance tuning techniques that scale with your application.



Final Words

You’re a Speed Champion!
You just learned:
What indexes are (magic phone book)
How to create single, compound, unique, text indexes
How to see speed difference with explain()
Pro tricks: partial, covered, collation

With these skills, you now understand the core of MongoDB query optimization, effective database indexing, and real-world performance tuning.

Your Speed Mission:


db.heroes.createIndex({ name: 1 })  // Make name searches instant
db.heroes.find({ name: "Hero50000" }).explain("executionStats")

See the magic IXSCAN!

You’re now a Certified MongoDB Speed Racer!

Resources:

Keep making your queries fly!


Next:MongoDB Agregation


SQL Server Monitoring & Performance Tuning: Real-World Queries, Troubleshooting & Cheat Sheet

Microsoft SQL Server Tutorial Series: Beginner to Expert

Part 20: Monitoring and Performance Tuning in SQL Server


Welcome to Part 20! In this article, we'll dive into the critical areas of monitoring your SQL Server environment and applying performance tuning techniques to keep your databases running smoothly and efficiently.


📌 What You'll Learn:

  • Key performance metrics to monitor in SQL Server
  • Tools and methods for effective monitoring
  • Common performance bottlenecks and how to identify them
  • Best practices and tips for performance tuning

🔍 Why Monitor SQL Server Performance?

Monitoring helps you proactively identify issues before they impact users. By tracking critical performance indicators, you can spot bottlenecks, resource contention, and other problems early, ensuring high availability and responsiveness.


📊 Essential Metrics to Monitor

Metric Description Why It Matters
CPU Usage Percentage of CPU utilization by SQL Server processes High CPU indicates heavy query load or inefficient queries
Memory Usage Amount of RAM used by SQL Server Insufficient memory causes paging and slows queries
Disk I/O Reads and writes on physical storage High disk latency can bottleneck database operations
Wait Statistics Types of waits SQL Server experiences during query execution Identifies resource bottlenecks like locks, I/O, CPU
Blocking & Deadlocks Queries waiting on locks held by others Causes query delays and transaction failures
Execution Plans Query plans generated by the optimizer Helps find inefficient queries and suggest indexes

🛠️ Monitoring Tools and Techniques

  • SQL Server Management Studio (SSMS): Use Activity Monitor and Extended Events for real-time insights.
  • SQL Server Profiler: Trace queries, events, and performance metrics (use sparingly on production).
  • Dynamic Management Views (DMVs): Query internal server state for waits, index usage, and query stats.
  • Performance Monitor (PerfMon): Track OS-level metrics like CPU, memory, and disk I/O.
  • Third-party monitoring tools: Solutions like Redgate SQL Monitor, SolarWinds, and SentryOne offer comprehensive dashboards.

⚠️ Common Performance Bottlenecks

  • Missing or fragmented indexes causing table scans.
  • Excessive blocking or deadlocks slowing transaction throughput.
  • Long-running or inefficient queries consuming CPU and I/O.
  • Insufficient memory leading to frequent disk swapping.
  • Hardware constraints such as slow disks or inadequate CPU cores.

🔧 Performance Tuning Tips

  • Analyze and optimize query execution plans to reduce costly operations.
  • Use appropriate indexing strategies: clustered, non-clustered, filtered indexes.
  • Regularly update statistics and rebuild/reorganize indexes to maintain efficiency.
  • Identify and resolve blocking and deadlocks through lock monitoring and query optimization.
  • Monitor and configure SQL Server memory settings to optimize buffer usage.
  • Consider query parameterization and avoid excessive recompilations.
  • Partition large tables to improve manageability and query performance.
  • Ensure TempDB is properly configured with multiple data files to reduce contention.

⚙️ Example: Query to Identify Missing Indexes


SELECT 
    migs.avg_total_user_cost * migs.avg_user_impact * (migs.user_seeks + migs.user_scans) AS improvement_measure,
    mid.database_id,
    mid.object_id,
    mid.index_handle,
    mid.equality_columns,
    mid.inequality_columns,
    mid.included_columns,
    statement AS create_index_statement
FROM sys.dm_db_missing_index_group_stats AS migs
INNER JOIN sys.dm_db_missing_index_groups AS mig
    ON migs.group_handle = mig.index_group_handle
INNER JOIN sys.dm_db_missing_index_details AS mid
    ON mig.index_handle = mid.index_handle
ORDER BY improvement_measure DESC;

This query helps prioritize indexes that can have the biggest impact on performance.


📋 Performance Monitoring Cheat Sheet

Metric What it Indicates Typical Thresholds Tools to Monitor
CPU Usage High CPU can cause slow queries or system bottlenecks Below 80% (over sustained periods) Performance Monitor, SQL Server DMVs
Memory Usage Low memory affects buffer cache, causes disk reads Aim for >75% buffer cache hit ratio SQL Server DMVs, Resource Monitor
Disk I/O High read/write latency slows query execution Latency < 15 ms preferred Performance Monitor, Extended Events
Wait Stats Shows resource waits affecting query performance Focus on high wait types like CXPACKET, PAGEIOLATCH sys.dm_os_wait_stats, Extended Events
Query Duration Long-running queries need tuning or indexing Depends on workload; monitor outliers Query Store, SQL Profiler, Extended Events
Blocking/Deadlocks Resource contention affecting concurrency Zero or minimal occurrences preferred Extended Events, SQL Server Agent alerts

🔧 Real-World Example Queries for Monitoring

Here are some practical T-SQL queries to monitor SQL Server performance and health:

Use Case Example Query Description
Check Active Sessions SELECT session_id, login_name, status, wait_time FROM sys.dm_exec_sessions WHERE status = 'running'; Lists all currently active sessions to identify workload.
Find Top CPU-Consuming Queries SELECT TOP 5 query_hash, total_worker_time/execution_count AS avg_cpu FROM sys.dm_exec_query_stats ORDER BY avg_cpu DESC; Identifies queries consuming the most CPU on average.
Check Wait Statistics SELECT wait_type, wait_time_ms, waiting_tasks_count FROM sys.dm_os_wait_stats ORDER BY wait_time_ms DESC; Shows wait types that impact SQL Server performance.
Monitor Disk I/O SELECT file_id, io_stall_read_ms, num_of_reads, io_stall_write_ms, num_of_writes FROM sys.dm_io_virtual_file_stats(NULL, NULL); Checks I/O latency and activity per database file.

⚠️ Common Troubleshooting Tips

  • High CPU Usage: Identify resource-intensive queries and optimize indexes or rewrite queries.
  • Blocking and Deadlocks: Use Extended Events or Profiler to trace blocking chains and deadlock graphs.
  • Memory Pressure: Monitor buffer cache hit ratio and page life expectancy; increase memory if needed.
  • Slow I/O: Check disk subsystem health, and consider spreading data files across multiple disks.
  • Job Failures: Always check SQL Server Agent job history and error messages for root cause analysis.
  • Network Latency: Monitor network throughput and errors, especially in distributed environments.

❓ Frequently Asked Questions (FAQ)

Q: How often should I monitor SQL Server?
Monitoring frequency depends on workload, but daily checks with alerts for anomalies are recommended.
Q: Can monitoring tools affect SQL Server performance?
Yes, especially tools like SQL Profiler. Use lightweight monitoring or Extended Events on production systems.
Q: What is the difference between blocking and deadlocks?
Blocking occurs when one query waits for another; deadlocks happen when queries wait on each other in a cycle, causing termination.
Q: How do I reduce high CPU usage?
Optimize expensive queries, add indexes, and review execution plans to identify inefficiencies.

📋 Quick Cheat Sheet: Monitoring & Performance Tuning

Action Tool/Method Purpose
Monitor CPU/Memory/Disk PerfMon, Activity Monitor Detect resource bottlenecks
Analyze Wait Stats DMVs Identify bottlenecks and delays
Check Query Plans SSMS Execution Plans Optimize query performance
Trace Queries Extended Events, Profiler Find long-running or problematic queries
Maintain Indexes Rebuild/Reorganize Improve query speed and reduce fragmentation

📌 Summary

  • Effective monitoring is essential for proactive SQL Server management.
  • Track key metrics like CPU, memory, disk I/O, waits, and blocking.
  • Use built-in tools like DMVs, Activity Monitor, and Extended Events for insights.
  • Apply performance tuning best practices: index optimization, query tuning, and resource configuration.
  • Regularly review and adjust based on workload changes.

📎 What’s Next?

Was this post helpful? Feel free to share your questions or feedback below! 💬

SQL Server Performance Tuning & Connection Pooling: Best Practices for Faster Queries


Microsoft SQL Server Tutorial Series: Beginner to Expert

Follow-Up: Performance Tuning & Connection Pooling in SQL Server

In this post, we’ll dive into how to optimize your application’s database interactions by tuning performance and leveraging connection pooling.


📌 What You’ll Learn in This Post:

  • What is connection pooling
  • How connection pooling improves performance
  • Basic SQL query performance tips
  • Tools for monitoring and tuning performance

🔍 What is Connection Pooling?

Connection pooling is a technique that reuses database connections instead of opening and closing a new connection every time your application needs to talk to the database.

Opening connections is expensive and can slow down your app, especially under load. Connection pools keep a pool of open connections ready for use, speeding up your queries.


⚙️ How Connection Pooling Works

  • When your app requests a connection, the pool returns an available connection if there is one.
  • If none are free, a new connection is created (up to a max limit).
  • When the app is done, the connection is returned to the pool—not closed.

💡 Connection Pooling Examples

Platform How to Enable/Use
Python (pyodbc) Connection pooling is enabled by default. Use persistent connection objects and don’t open/close per query.
.NET (SqlConnection) Connection pooling is on by default. Use using blocks and open/close connections per operation as pooling manages reuse.

🔧 Basic SQL Performance Tips

  • Use indexes wisely: Index columns used in JOINs and WHERE clauses.
  • Avoid SELECT *: Retrieve only necessary columns.
  • Filter early: Use WHERE clauses to reduce rows processed.
  • Analyze execution plans: Use SQL Server Management Studio (SSMS) to understand query costs.
  • Batch large inserts/updates: Avoid large single transactions that lock tables.

📊 Monitoring & Tools

  • SQL Server Profiler: Trace and analyze database activity.
  • Dynamic Management Views (DMVs): Query system stats like sys.dm_exec_query_stats.
  • Execution Plans: Visualize query performance and index usage.

📌 Summary

  • Connection pooling drastically improves app responsiveness
  • Follow SQL best practices to optimize queries
  • Use tools like SSMS and DMVs to monitor and tune your database

Implement connection pooling and optimize queries for faster, scalable apps!


📎 Next Up

SQL Indexing & Query Optimization (Part 2): Smart Queries and Speed Tricks

Indexing and Query Optimization with SQL: A Simple and Fun Tutorial for Everyone-Part 2


← Missed the first part? Start with Part 1 of this tutorial here.


Unoptimized Query:

import sqlite3
import time

conn = sqlite3.connect('toystore.db')
cursor = conn.cursor()

# Unoptimized: Select all columns, no index
start_time = time.time()
cursor.execute('''
    SELECT *
    FROM Sales
    JOIN Toys ON Sales.ToyID = Toys.ToyID
    WHERE Toys.Type = 'Action Figure' AND Sales.SaleDate = '2025-09-02'
''')
print("Unoptimized Results:", cursor.fetchall())
print("Time taken:", time.time() - start_time, "seconds")

conn.close()

Output:

Unoptimized Results: [(3, 1, 1, 30.0, '2025-09-02', 1, 'Robot', 'Action Figure', 30.0, 10)]
Time taken: 0.002 seconds

Optimized Query:

import sqlite3
import time

conn = sqlite3.connect('toystore.db')
cursor = conn.cursor()

# Create indexes
cursor.execute("CREATE INDEX IF NOT EXISTS idx_toy_type ON Toys (Type)")
cursor.execute("CREATE INDEX IF NOT EXISTS idx_sale_date ON Sales (SaleDate)")

conn.commit()

# Optimized: Select specific columns, use indexed columns
start_time = time.time()
cursor.execute('''
    SELECT Toys.Name, Sales.TotalPrice
    FROM Sales
    JOIN Toys ON Sales.ToyID = Toys.ToyID
    WHERE Toys.Type = 'Action Figure' AND Sales.SaleDate = '2025-09-02'
''')
print("Optimized Results:", cursor.fetchall())
print("Time taken:", time.time() - start_time, "seconds")

conn.close()

Output:

Optimized Results: [('Robot', 30.0)]
Time taken: 0.001 seconds

What’s Happening?

  • Indexes on Type and SaleDate make filtering faster.
  • SELECT Toys.Name, Sales.TotalPrice grabs only needed columns.
  • The query runs faster because SQLite uses the indexes.

More Optimization Tips

1. Index Frequently Searched Columns

Create indexes on columns you often use in WHERE, JOIN, or ORDER BY. For example:

CREATE INDEX idx_toy_price ON Toys (Price);

2. Use EXPLAIN to Check Queries

SQLite’s EXPLAIN QUERY PLAN shows how a query runs. Try it to see if your index is used:

conn = sqlite3.connect('toystore.db')
cursor = conn.cursor()
cursor.execute("EXPLAIN QUERY PLAN SELECT * FROM Toys WHERE Name = 'Robot'")
print("Query Plan:", cursor.fetchall())
conn.close()

Sample Output:

[(3, 0, 0, 'SEARCH TABLE Toys USING INDEX idx_toy_name (Name=?)')]

Explanation: This means SQLite is using the idx_toy_name index to perform the search on the Name column. If it said SCAN TABLE, it would mean no index is being used, which is slower.

This shows if SQLite uses idx_toy_name.

3. Avoid Over-Indexing

Indexes speed up searches but slow down INSERT, UPDATE, and DELETE because the index must be updated. Only index columns you search often.

4. Combine with Joins

Use indexes on join columns (like ToyID in Sales and Toys):

CREATE INDEX idx_sale_toyid ON Sales (ToyID);

Example: Optimized Join

conn = sqlite3.connect('toystore.db')
cursor = conn.cursor()

cursor.execute("CREATE INDEX IF NOT EXISTS idx_sale_toyid ON Sales (ToyID)")
conn.commit()

cursor.execute('''
    SELECT Toys.Name, SUM(Sales.TotalPrice)
    FROM Sales
    JOIN Toys ON Sales.ToyID = Toys.ToyID
    WHERE Sales.SaleDate = '2025-09-02'
    GROUP BY Toys.Name
''')
print("Sales on 2025-09-02:", cursor.fetchall())

conn.close()

Output:

Sales on 2025-09-02: [('Doll', 25.0), ('Robot', 30.0)]

The index on ToyID makes the join faster.


Tips for Success

  1. Start Simple: Create one index and test a query.
  2. Test with EXPLAIN: Check if your index is used.
  3. Balance Indexes: Don’t index every column—only the ones you search.
  4. Write Clear Queries: Use specific columns and filters.
  5. Practice: Try indexing and optimizing queries for a game or book database.

💡 Quick Recap: Why Optimization Matters

If you’ve just joined us, or need a reminder — here’s why query optimization is a must-have skill in your SQL toolbox:

  • They are Fast: Indexes cut search time dramatically.
  • They are Easy: Just a few commands to create indexes.
  • They are Useful: Fast databases are key for apps and websites.
  • They are Fun: It’s like giving your database a turbo boost!

🚀 Ready to Speed Up Your Queries?

With just a few well-placed indexes and optimized queries, you can dramatically improve your database’s speed and performance. Now you’ve got the tools — go make your database fly!


Common Questions

1. Are indexes always good?

No, they slow down writes (INSERT, UPDATE), so use them wisely.

2. Do indexes work in other databases?

Yes, MySQL, PostgreSQL, and others use similar indexing.

3. How do I know if my query is optimized?

Use EXPLAIN QUERY PLAN to check if indexes are used.

4. Can I remove an index?

Yes, with DROP INDEX index_name.


🧠 Mini Challenge: Test Your Optimization Skills!

Try answering these on your own before checking online:

  1. Which columns in the Sales table are good candidates for indexing?
  2. What does the EXPLAIN output SCAN TABLE Toys mean?
  3. Why might too many indexes slow down INSERT operations?
  4. How would you optimize a query that joins Toys and a new Suppliers table?

Bonus: Create your own small database (e.g., books, games, recipes) and try adding indexes and optimized queries!


Wrapping Up

Indexing and query optimization are like giving your database a treasure map and shortcuts to find data fast. In this tutorial, we created indexes on our toystore.db tables, optimized queries with specific columns and filters, and saw how to check query plans. These skills make your database zoom like a superhero.

Try creating your own database for movies or games, add indexes, and optimize queries. Use DB Browser for SQLite to see your data or keep coding in Python. With indexing and query optimization, you’re now a database speed wizard, ready to make your data fly!

Happy SQL adventures, and keep speeding up your database!


Featured Post

Master MongoDB with Node.js Using Mongoose: Complete Guide

Working with MongoDB from Node.js using Mongoose Your Magical Mongoose Pet That Makes MongoDB Super Easy For Beginner to Expert Level ...

Popular Posts