Back to blogSQL Secrets for Every Data Analyst: Hidden Tricks and Queries to Level Up Your Game
SQL18 min read
Atabay Aghalarbayli
Data Analytics Expert

SQL Secrets for Every Data Analyst: Hidden Tricks and Queries to Level Up Your Game

SQL is like a classic novel. It never gets old, but every time you revisit it, you discover something new. Most analysts stop at the basics (JOINs, GROUP BY, simple aggregates), but the real magic of SQL shines when you uncover the tricks hiding beneath the surface. Let's walk through a few secrets that rarely get the spotlight but can make you the go-to expert in the room.

1. The Mysterious Lateral Join: Dynamic Subqueries in Style

We all know INNER JOIN and LEFT JOIN. But have you met LATERAL JOIN? Think of it as the adaptable friend who always has the perfect reply. It lets subqueries reference each row of the main query dynamically.

Example 1: Find the top product per category:

SELECT c.category_id, c.category_name, p_top.product_name, p_top.sales
FROM categories c
LEFT JOIN LATERAL (
    SELECT p.product_name, p.sales
    FROM products p
    WHERE p.category_id = c.category_id
    ORDER BY p.sales DESC
    LIMIT 1
) AS p_top;

Why it matters: No temp tables, no hacks. Just a clean, elegant solution.

Example 2: Keep aggregations out of the SELECT for a cleaner query

In Snowflake and Postgres, you can move all calculations into a lateral block and just reference them:

SELECT c.customer_id, c.customer_name, o.product_name,
       stats.total_orders, stats.total_spend
FROM customers c
LEFT JOIN orders o
  ON o.customer_id = c.customer_id
LEFT JOIN LATERAL (
    SELECT COUNT(*) AS total_orders,
           SUM(o.amount) AS total_spend
) AS stats;

Why it matters: Your main SELECT stays neat, while the heavy lifting happens inside the lateral subquery.

Quick note: Not every engine supports LATERAL. In SQL Server or Oracle, use OUTER APPLY instead. They behave the same, but syntax differs.

2. Window Functions Aren't Just for Windows

ROW_NUMBER() and RANK() get all the attention, but PERCENT_RANK() deserves love too. It shows where a value stands within a group, between 0 and 1.

Example: Percentile rank of sales within a region:

SELECT region_id, sales,
       PERCENT_RANK() OVER (PARTITION BY region_id ORDER BY sales DESC) AS percentile_rank
FROM sales_data;

Why it matters: Perfect for benchmarking, spotting outliers, or impressing colleagues who rely on Excel percentiles.

3. Recursive CTEs: SQL's Hidden Superpower

If you've ever mapped out org charts or family trees, recursive CTEs are your secret weapon. They let you explore hierarchical data without painful loops.

Example: Build an org chart:

WITH RECURSIVE OrgChart AS (
    SELECT employee_id, manager_id, employee_name, 1 AS level
    FROM employees
    WHERE manager_id IS NULL
    UNION ALL
    SELECT e.employee_id, e.manager_id, e.employee_name, oc.level + 1
    FROM employees e
    JOIN OrgChart oc ON e.manager_id = oc.employee_id
)
SELECT * FROM OrgChart
ORDER BY level, manager_id;

Why it matters: Elegantly untangles hierarchies that would otherwise drive you mad.

Before the query (employees table):

employee_idmanager_idemployee_name
1NULLAlice
21Bob
31Carol
42Dave

After the query (OrgChart):

employee_idmanager_idemployee_namelevel
1NULLAlice1
21Bob2
31Carol2
42Dave3

4. Pivoting Data Without Losing Your Mind

Yes, you can reshape rows into columns right in SQL—no Excel detour required.

Example: Monthly sales as columns:

SELECT *
FROM (
    SELECT product_id, MONTH(order_date) AS order_month, sales
    FROM orders
) src
PIVOT (
    SUM(sales)
    FOR order_month IN ([1], [2], [3], [4], [5], [6], [7], [8], [9], [10], [11], [12])
) AS pvt;

Why it matters: Instant dashboard, ready tables without extra tools.

Before the query (orders table):

product_idorder_datesales
1012024-01-15500
1012024-02-20300
1022024-01-10200
1022024-03-05400

After the query (pivoted view):

product_id123
1015003000
1022000400

5. JSON Queries: When SQL Meets JavaScript

Modern databases let you query JSON directly. No ETL pipelines, no painful flattening.

Example: Extract preferences:

SELECT user_id, data->>'name' AS user_name
FROM user_profiles
WHERE data->'preferences'->>'language' = 'English';

Why it matters: Handle messy, semi-structured data right inside SQL.

Before the query (user_profiles table):

user_iddata
1{"name": "Alice", "preferences": {"language": "English"}}
2{"name": "Bob", "preferences": {"language": "French"}}
3{"name": "Carol", "preferences": {"language": "English"}}

After the query (filtered & extracted):

user_iduser_name
1Alice
3Carol

6. Regex in SQL: Text Matching Sorcery

SQL can wield regular expressions and yes, inside your queries.

Example: Filter emails by domain:

SELECT email
FROM users
WHERE email REGEXP '^[a-zA-Z0-9._%+-]+@example\.com$';

Why it matters: Clean data on the fly without shipping it to Python or R.

Before the query (users table):

user_idemail
1[email protected]
2[email protected]
3[email protected]
4[email protected]

After the query (filtered emails):

email
[email protected]
[email protected]

7. Temporal Tables & Time Travel

Different engines handle "time travel" differently.

SQL Server (system-versioned temporal):

SELECT *
FROM SalesHistory
FOR SYSTEM_TIME AS OF '2024-11-01 10:00:00';
Notes: The table must be created as SYSTEM_VERSIONED with a history table. Great for audits and point-in-time checks.

Snowflake (Time Travel):

-- Point-in-time query
SELECT *
FROM SALES
AT (TIMESTAMP => '2024-11-01 10:00:00'::timestamp);

-- Or relative offset (e.g., 1 hour ago)
SELECT *
FROM SALES
AT (OFFSET => -3600);

-- Show how rows changed between two times
SELECT *
FROM SALES
VERSIONS BETWEEN '2024-11-01 09:00:00'::timestamp AND '2024-11-01 10:00:00'::timestamp;
Notes: Time Travel retention depends on account/table settings. You can also use BEFORE (STATEMENT => '...') to rewind to a statement boundary.

8. Leveraging Index-Only Scans

Most people think of indexes as tools to make WHERE conditions faster, but in reality, a well-designed index can sometimes satisfy an entire query without touching the base table at all. This is called an index-only scan.

Why it works: If every column needed by the query is included in an index, the database engine can answer the query directly from the index structure. Since indexes are smaller and ordered, the scan is much faster than going back and forth to the full table.

Example: Create a covering index:

CREATE INDEX idx_covering ON sales (product_id, order_date, sales);

SELECT product_id, order_date, sales
FROM sales
WHERE product_id = 12345;

In this case, the query only touches the index. No need to fetch rows from the sales table.

Practical use cases:

  • Reporting queries: If your BI dashboards repeatedly access the same dimensions and metrics, covering indexes can speed them up dramatically.
  • Range filters: When filtering by date ranges (e.g., last 30 days), an index that includes both the filter column and the output columns can make queries fly.
  • JOIN helpers: Creating composite indexes on foreign keys and the columns you usually select reduces unnecessary table lookups.

Gotchas:

  • Not all engines support index-only scans (some still check the table for visibility of rows, e.g., older PostgreSQL versions).
  • Indexes take space and slow down writes (INSERT/UPDATE/DELETE), so balance speed with storage and maintenance costs.
  • For very wide tables, consider INCLUDE columns (SQL Server) or similar features to add extra columns to the index without bloating the key.

9. CASE with Aggregates: The Sneaky Filter

Add conditions directly inside aggregates for flexible calculations.

Example: Count orders by status:

SELECT
    COUNT(CASE WHEN status = 'Completed' THEN 1 END) AS completed_orders,
    COUNT(CASE WHEN status = 'Pending' THEN 1 END) AS pending_orders
FROM orders;

Why it matters: Multiple counts in one query: efficient and elegant.

Before the query (orders table):

order_idcustomer_idstatus
1101Completed
2102Pending
3101Completed
4103Pending
5102Completed

After the query (result):

completed_orderspending_orders
32

10. Temporary vs Transient Tables: Which to Use and When

What's the difference?

  • TEMP: session-only, private, auto-drops. Best for one-off, multi-step queries.
  • TRANSIENT (Snowflake): persists across sessions, cheaper storage, no Fail-safe (rebuild if lost). Best for staging you can regenerate.
  • PERMANENT: full durability + recovery/history. Best for curated data you must keep.

Rule of thumb: Scratch ➜ TEMP. Reusable + rebuildable ➜ TRANSIENT. Must-keep ➜ PERMANENT.

Snowflake demo (longer, real-life flow)

-- 1) Permanent target (gold layer)
CREATE OR REPLACE TABLE dim_product (
  product_id NUMBER,
  product_name STRING,
  category STRING,
  is_active BOOLEAN,
  updated_at TIMESTAMP
);

INSERT INTO dim_product (product_id, product_name, category, is_active, updated_at) VALUES
  (101, 'Road Bike', 'Bikes', TRUE, '2025-07-01 09:00:00'),
  (102, 'Helmet', 'Accessories', TRUE, '2025-07-01 09:00:00');

-- 2) Transient staging for repeated daily loads (cheaper, no Fail-safe)
CREATE OR REPLACE TRANSIENT TABLE stg_product (
  product_id NUMBER,
  product_name STRING,
  category STRING,
  is_active BOOLEAN,
  updated_at TIMESTAMP
);

-- Simulate today's incoming feed
INSERT INTO stg_product VALUES
  (101, 'Road Bike', 'Bikes', TRUE,  '2025-08-15 08:00:00'),
  (102, 'Helmet',    'Accessories', FALSE, '2025-08-15 08:00:00'),
  (103, 'Bottle',    'Accessories', TRUE,  '2025-08-15 08:00:00');

-- 3) Temp table for session-only scratch work (dedupe/filter before merge)
CREATE OR REPLACE TEMPORARY TABLE tmp_product AS
SELECT *
FROM (
  SELECT *,
         ROW_NUMBER() OVER (PARTITION BY product_id ORDER BY updated_at DESC) AS rn
  FROM stg_product
)
WHERE rn = 1
  AND is_active = TRUE;

-- 4) Merge cleaned rows into the permanent dimension
MERGE INTO dim_product d
USING tmp_product t
  ON d.product_id = t.product_id
WHEN MATCHED THEN UPDATE SET
  d.product_name = t.product_name,
  d.category     = t.category,
  d.is_active    = t.is_active,
  d.updated_at   = t.updated_at
WHEN NOT MATCHED THEN INSERT (product_id, product_name, category, is_active, updated_at)
VALUES (t.product_id, t.product_name, t.category, t.is_active, t.updated_at);

-- 5) Optional: inspect results
SELECT * FROM dim_product ORDER BY product_id;

What happened here?

  • We kept the gold table (dim_product) PERMANENT for durability and auditability.
  • We used a TRANSIENT stg_product to hold daily feeds cheaply; it survives sessions but can be rebuilt any day.
  • We used a TEMP tmp_product for session-only cleanup (dedupe + filter) before the MERGE.

When to use which:

  • TEMP for throwaway steps within a job.
  • TRANSIENT for recurring staging that's safe to recreate.
  • PERMANENT for trusted, long-lived datasets.

Final Thoughts

SQL isn't just about SELECT * FROM table. It has hidden layers that make queries faster, cleaner, and more powerful. Mastering these tricks will set you apart—not just as someone who writes queries, but as someone who truly understands data.

If even a couple of these tips surprised you, that's your sign to dig deeper. And if you'd like to explore more ways to level up your analytics game, you know where to connect with me.

SQLData AnalysisAdvanced TechniquesDatabaseTutorialBest Practices