How the count() function works in SQLite?
Learn how SQLite count(*) and count(column) differ, how DISTINCT handles duplicates, and how to inspect query plans for count queries.
On this page
Counting records is one of the most fundamental operations in database work, and SQLite’s count() function serves as your trusty tool for this essential task. Whether you’re building reports, analyzing data trends, or simply checking how many users signed up this week, count() provides the answer with minimal fuss. But don’t let its apparent simplicity fool you—this versatile function has several behaviors worth understanding to use it effectively in your SQLite queries.
The Many Faces of count()
The count() function wears different hats depending on how you call it. At its core, it tallies records, but what exactly it counts can vary:
SELECT count(*) FROM users; -- Counts all rows, no matter what
SELECT count(email) FROM users; -- Counts only non-NULL email values
SELECT count(DISTINCT country) FROM users; -- Counts unique countries
These variations make count() adaptable to different counting scenarios in your data analysis.
Counting Everything vs. Counting Specifics
The asterisk (*) version counts every single row in the result set, regardless of content. This is your go-to when you simply need to know “how many records exist?”:
-- How many orders do we have total?
SELECT count(*) AS total_orders FROM orders;
-- How many products are priced over $100?
SELECT count(*) FROM products WHERE price > 100;
When you specify a column name instead of *, the rules change—now count() only increments for non-NULL values in that column:
-- How many users provided a phone number?
SELECT count(phone_number) FROM customers;
Counting Unique Values
Adding the DISTINCT keyword transforms count() into a tool for measuring diversity in your data:
-- How many different countries are represented?
SELECT count(DISTINCT country_code) FROM users;
-- How many unique products were ordered last month?
SELECT count(DISTINCT product_id) FROM order_items
WHERE order_date >= date('now', '-1 month');
This version is particularly useful for understanding the variety in your datasets.
count(DISTINCT X) excludes NULL values as well as duplicates. In this sample, four rows produce two distinct non-NULL country codes:
SELECT count(*) AS rows,
count(DISTINCT country_code) AS unique_non_null_countries
FROM (
SELECT 'US' AS country_code
UNION ALL SELECT 'US'
UNION ALL SELECT 'CA'
UNION ALL SELECT NULL
);
SQLite documents count(X) as counting non-NULL values and DISTINCT as removing duplicate inputs before aggregation; see the aggregate function reference.
Combining count() with GROUP BY
One of count()’s most powerful partnerships is with GROUP BY, enabling breakdowns by category:
-- Number of users per country
SELECT country, count(*) AS user_count
FROM users
GROUP BY country;
-- Orders per customer (only for active customers)
SELECT customer_id, count(*) AS order_count
FROM orders
WHERE status = 'completed'
GROUP BY customer_id
HAVING count(*) > 1; -- Only show repeat customers
These grouped counts form the backbone of most analytical queries.
Query plans and performance
There is no universal speed ordering for these forms. count(*) counts every row in its input; count(column) counts only non-NULL values; count(DISTINCT column) also removes duplicates. A WHERE clause can reduce rows visited when SQLite can use a suitable index and its cost estimate favors that plan, but an unindexed filter may still require a full scan. A covering index may also let SQLite answer a count without reading table rows.
Use EXPLAIN QUERY PLAN with the schema and data you actually have. SQLite reports SCAN when it visits all entries in a table or index, and SEARCH when it visits a subset. A distinct count may also require a temporary B-tree to remove duplicates.
-- Check whether the status predicate uses an index or scans all rows.
CREATE INDEX IF NOT EXISTS idx_orders_status ON orders(status);
EXPLAIN QUERY PLAN
SELECT count(*) FROM orders WHERE status = 'shipped';
-- Check how SQLite reads values and handles duplicate removal.
EXPLAIN QUERY PLAN
SELECT count(tracking_number) FROM orders;
EXPLAIN QUERY PLAN
SELECT count(DISTINCT customer_id) FROM orders;
The result depends on available indexes, table contents, and the chosen plan. See SQLite’s EXPLAIN QUERY PLAN guide and query planner documentation.
Counting with Joins and Subqueries
count() behaves interestingly in more complex queries. Watch for these patterns:
-- May overcount due to join multiplication
SELECT count(*) FROM orders JOIN order_items USING (order_id);
-- Precise counting with subqueries
SELECT count(*) FROM (
SELECT DISTINCT order_id FROM order_items
);
-- Counting related items per parent
SELECT p.product_name,
(SELECT count(*) FROM reviews
WHERE reviews.product_id = p.product_id) AS review_count
FROM products p;
Edge Cases and Surprising Behaviors
Even simple functions have quirks. Note these count() behaviors:
-- Counting an empty table returns 0, not NULL
SELECT count(*) FROM empty_table; -- 0
-- Counting with all NULLs returns 0
SELECT count(NULL) FROM (SELECT 1 UNION SELECT 2); -- 0
-- count() with no rows returns 0, but other aggregates return NULL
SELECT count(*), avg(price) FROM products WHERE 1=0;
-- Returns: 0 | NULL
Conclusion
The humble count() function is arguably the most frequently used aggregate in SQLite, yet it offers surprising depth in its various forms. From simple row counting to sophisticated analysis of unique values and grouped distributions, mastering count() is essential for effective database work. Remember that while count(*) gives you raw totals, the column-specific versions and DISTINCT modifier provide more targeted insights into your data’s structure and quality. Next time you need to measure something in your database, consider which flavor of count() best answers your question—it’s likely more nuanced than just counting all rows. Whether you’re building dashboards, optimizing queries, or exploring datasets, a solid grasp of count() will serve you well in nearly every SQLite project.