Tutorial

SQL COUNT Function: COUNT(*), COUNT(1) & COUNT(DISTINCT) Guide

Master the SQL COUNT function with examples of COUNT(*), COUNT(1), COUNT(DISTINCT), NULL handling, and conditional counting techniques.

Anuj SainiSep 8, 20268 min read

In data analysis and database querying, the SQL COUNT function is by far the most commonly invoked aggregate function. Whether you are validating dataset cardinality, calculating conversion funnel drops, or evaluating marketing engagement, knowing exactly how COUNT handles NULL values and distinct entities is critical for accurate reporting.

In this guide, building upon what is SQL and SQL GROUP BY vs HAVING, we dive deep into the SQL COUNT function, comparing COUNT(*), COUNT(1), COUNT(column), and COUNT(DISTINCT), with production examples for conditional aggregations and window partitions.


What is the SQL COUNT Function?

The SQL COUNT function aggregates tabular rows into a single scalar integer representing row count. When paired with GROUP BY, it computes separate subtotals for each unique group:

sql
-- Count total customer records in the database
SELECT COUNT(*) AS total_customers 
FROM customers;
 
-- Count active customers per country
SELECT 
    country, 
    COUNT(*) AS customer_count 
FROM customers 
WHERE status = 'active'
GROUP BY country 
ORDER BY customer_count DESC;

Understanding how the query engine resolves arguments within the function determines whether your metrics include or exclude missing data.


SQL COUNT(*) vs COUNT(1) vs COUNT(column_name)

A classic database interview question examines the nuances between COUNT(*), COUNT(1), and COUNT(column_name):

Feature / Criteria

Practical Demonstration of NULL Behavior

Let's test these variants against a sample table of employee referrals:

sql
-- Sample employee data:
-- employee_id | employee_name | referrer_id
-- 1           | Alice         | NULL
-- 2           | Bob           | 1
-- 3           | Charlie       | 1
-- 4           | David         | NULL
-- 5           | Eve           | 2
 
SELECT 
    COUNT(*) AS total_rows,                 -- Returns 5
    COUNT(1) AS total_rows_literal,         -- Returns 5
    COUNT(referrer_id) AS non_null_referrers, -- Returns 3 (Alice & David skipped)
    COUNT(DISTINCT referrer_id) AS unique_referrers -- Returns 2 (IDs 1 and 2)
FROM employees;

Watch Out for NULL Discrepancies

If your query intends to count total customer accounts, never use COUNT(phone_number) or COUNT(referral_code) because records without a phone number or referral code will be silently omitted. Always use COUNT(*) for total row counts.


Advanced SQL COUNT Techniques

Modern analytics requires going beyond basic row counts. Here are three powerful techniques data analysts use daily:

1. Conditional Counting with CASE WHEN

Instead of running multiple separate queries with different WHERE clauses, you can pivot multiple metrics into a single row using conditional SQL COUNT queries:

sql
SELECT 
    COUNT(*) AS total_transactions,
    COUNT(CASE WHEN payment_status = 'successful' THEN 1 END) AS successful_orders,
    COUNT(CASE WHEN payment_status = 'refunded' THEN 1 END) AS refunded_orders,
    COUNT(CASE WHEN payment_status = 'failed' THEN 1 END) AS failed_orders,
    ROUND(
        COUNT(CASE WHEN payment_status = 'successful' THEN 1 END) * 100.0 / NULLIF(COUNT(*), 0),
        2
    ) AS success_rate_pct
FROM payments;

In PostgreSQL, you can write the cleaner ANSI standard FILTER syntax:

sql
-- PostgreSQL native aggregate FILTER syntax
SELECT 
    COUNT(*) AS total_orders,
    COUNT(*) FILTER (WHERE payment_status = 'successful') AS completed_orders,
    COUNT(*) FILTER (WHERE payment_status = 'failed') AS failed_orders
FROM payments;

2. Multi-Column Distinct Counting

ANSI SQL does not permit multiple columns inside a single COUNT(DISTINCT col1, col2) statement in some engines (like SQL Server or older Oracle). In PostgreSQL and MySQL, it is supported:

sql
-- Count distinct customer and store combinations
SELECT COUNT(DISTINCT customer_id, store_id) AS unique_customer_store_visits
FROM store_visits;
 
-- Cross-platform universal workaround using string concatenation:
SELECT COUNT(DISTINCT CONCAT(customer_id, '-', store_id)) AS unique_customer_store_visits
FROM store_visits;

3. Window Function Counting: Running Totals & Partitioned Counts

Using SQL COUNT as an analytic window function allows you to append group counts to every individual row without collapsing the dataset:

sql
SELECT 
    order_id,
    customer_id,
    order_date,
    order_amount,
    -- Count total orders placed by this specific customer to date
    COUNT(*) OVER (
        PARTITION BY customer_id 
        ORDER BY order_date
        ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
    ) AS customer_order_sequence,
    -- Total count of all orders across the customer's lifetime
    COUNT(*) OVER (PARTITION BY customer_id) AS customer_lifetime_orders
FROM orders;

Explore our complete guide on SQL for Data Analyst for more real-world window function examples.


SQL COUNT with Window Functions and Partitioning

Analytical queries frequently require calculating record totals alongside unaggregated row-level details without collapsing output rows via GROUP BY. Windowed variations of the sql count function solve this seamlessly. Learn more in our SQL Tutorials hub.

Running Counts and Cumulative Volume

sql
SELECT 
    order_id,
    customer_id,
    order_date,
    -- Cumulative order count per customer
    COUNT(*) OVER (
        PARTITION BY customer_id 
        ORDER BY order_date
        ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
    ) AS cumulative_order_number
FROM orders;

This pattern assigns each transaction an incremental sequence counter per customer, immediately revealing whether a transaction represents a first-time purchase (cumulative_order_number = 1) or a repeat visit.

Calculating Contribution Percentages with Windowed SQL COUNT

To measure what percentage of a department's total headcount belongs to a specific job title:

sql
SELECT 
    department_id,
    job_title,
    COUNT(*) AS title_headcount,
    SUM(COUNT(*)) OVER (PARTITION BY department_id) AS dept_total_headcount,
    ROUND(
        COUNT(*)::NUMERIC / SUM(COUNT(*)) OVER (PARTITION BY department_id) * 100, 
        2
    ) AS pct_of_department
FROM employees
GROUP BY department_id, job_title;

Here, the inner COUNT(*) aggregates headcount by title, while the windowed SUM(COUNT(*)) OVER (PARTITION BY department_id) computes the departmental denominator without requiring a separate subquery or self-join.

Counting Distinct Elements with Approximations (HyperLogLog)

On billion-row analytics datasets, COUNT(DISTINCT user_id) can cause severe memory spill-to-disk because the database engine must build an in-memory hash set of every unique identifier. Cloud data warehouses provide probabilistic HyperLogLog functions that return counts within 1% accuracy in a fraction of the compute time:

  • Snowflake: APPROX_COUNT_DISTINCT(user_id)
  • BigQuery: APPROX_COUNT_DISTINCT(user_id)
  • PostgreSQL: hll_count_distinct() extension

Real-World Business Scenario: Conversion Funnel Step Counting

Data analysts frequently track multi-step customer acquisition funnels by pivoting filtered counts in a single pass over analytics event logs:

sql
SELECT 
    DATE_TRUNC('month', event_timestamp) AS cohort_month,
    COUNT(DISTINCT user_id) AS total_visitors,
    COUNT(DISTINCT CASE WHEN event_name = 'product_view' THEN user_id END) AS viewed_product,
    COUNT(DISTINCT CASE WHEN event_name = 'add_to_cart' THEN user_id END) AS added_to_cart,
    COUNT(DISTINCT CASE WHEN event_name = 'purchase_complete' THEN user_id END) AS completed_purchase
FROM clickstream_events
GROUP BY DATE_TRUNC('month', event_timestamp)
ORDER BY cohort_month DESC;

This single aggregation replaces four separate queries, drastically reducing warehouse billing costs on Snowflake and BigQuery.

Performance Tuning Tips for Large Scale SQL COUNT

On massive datasets containing hundreds of millions of rows, COUNT(*) without an index requires a sequential table scan:

  1. Approximate Row Counts for Dashboards: If your application displays an approximate count (such as "Over 500,000 items listed"), avoid COUNT(*). Instead, query system catalog statistics:
    sql
    -- PostgreSQL approximate instant row count
    SELECT reltuples::BIGINT AS estimated_count 
    FROM pg_class 
    WHERE relname = 'large_transaction_table';
  2. Index-Only Scans: Ensure the database engine can satisfy your COUNT query via an index rather than scanning the table heap. An index on (status, created_at) allows fast index scans when counting filtered orders.
  3. Understand Query Execution Order: Remember that COUNT aggregates execute after FROM, JOIN, and WHERE, but before HAVING and ORDER BY. Read our guide on order of execution in SQL.

Summary Checklist for SQL COUNT

  • Use COUNT(*) to count total rows in a table or group.
  • Use COUNT(column) only when you intentionally want to exclude NULL entries.
  • Use COUNT(DISTINCT column) to compute unique entity volumes.
  • Combine COUNT(CASE WHEN ... THEN 1 END) to pivot multiple metrics in one query.
  • Guard against division by zero using NULLIF(COUNT(*), 0).

SQL COUNT in Statistical Quality Audits and Anomaly Detection

Data quality engineering relies on the sql count function to validate ingestion pipelines and detect pipeline corruptions:

  • Null Rate Monitoring: Calculate the exact missing percentage across critical dimension columns:
    sql
    SELECT 
        COUNT(*) AS total_records,
        COUNT(email) AS populated_emails,
        ROUND((1 - COUNT(email)::NUMERIC / COUNT(*)) * 100, 2) AS email_null_percentage
    FROM staging_users;
  • Duplicate Key Audits: Flag non-unique business identifiers before dimension table loads:
    sql
    SELECT transaction_reference, COUNT(*) AS occurence_count
    FROM payment_webhooks
    GROUP BY transaction_reference
    HAVING COUNT(*) > 1;
  • Volume Spike Alerting: Compare today's record count against a rolling 30-day average using windowed counts to detect ETL ingestion drops or bot traffic spikes.

Browse our complete collection in the SQL Tutorials hub and practice on Topfolio Practice.

Practice SQL Aggregations & Interview Queries

Master SQL COUNT, window functions, and real-world analytical queries on Topfolio Practice.

Try Free SQL Practice

Frequently Asked Questions

What does the SQL COUNT function do?

The SQL COUNT function is an aggregate function that counts the number of rows returned by a query. Depending on whether you pass an asterisk, an integer literal, or a column name, it counts all rows or only non-NULL rows.

Is there any performance difference between COUNT(*) and COUNT(1) in SQL?

No. In all modern relational database engines (PostgreSQL, MySQL, SQL Server, Oracle, and Snowflake), COUNT(*) and COUNT(1) produce identical execution plans. The query optimizer treats COUNT(1) identically to COUNT(*).

Does SQL COUNT ignore NULL values?

Yes, when supplied with a specific column name like COUNT(column_name), it strictly ignores NULL values and counts only rows where that column is NOT NULL. However, COUNT(*) counts every qualifying row regardless of NULL column values.

How do you count unique values in SQL?

Use COUNT(DISTINCT column_name). This counts only unique non-NULL occurrences of values across the specified column or expression.

How do you perform conditional counting in SQL?

Use COUNT(CASE WHEN condition THEN 1 END) or SUM(CASE WHEN condition THEN 1 ELSE 0 END). In PostgreSQL, you can also use the native aggregate filter clause: COUNT(*) FILTER (WHERE condition).

Anuj Saini

Written by

Anuj SainiFounder & Lead Instructor

Founder at Topfolio with 6+ years in data & analytics across JPMC, Ultrahuman, and high-growth startups. Sat on hiring panels, reviewed 500+ resumes, and writes practical SQL & data guides.