When will I get to 1,000,000 Anki reviews?

Recently I’ve been wondering how long it would take me to get to 1,000,000 reviews. Right now I’m sitting at between 800,000 and 900,000 reviews and for no other reason than pure ridiculous curiosity I was curious whether I could get SQLite to compute it directly for me. Turns out the answer is “yes, you can.”

CAUTION
Before you run anything that accesses the Anki database outside of the Anki application itself, you absolutely should backup your database first. You have been warned.

Here’s the query in its gory detail and then I’ll walk through how it works:

WITH 
daily_review_rate AS (
  SELECT AVG(reviews_per_day) as avg_daily_reviews
  FROM (
    SELECT 
      date(ROUND(id/1000), 'unixepoch') as review_date,
      COUNT(*) as reviews_per_day
    FROM revlog
    WHERE date(ROUND(id/1000), 'unixepoch') >= date('now', '-2 years')
    GROUP BY review_date
    HAVING reviews_per_day >= 25
  )
),
current_total AS (
  SELECT COUNT(*) as total_reviews
  FROM revlog
)
SELECT 
  ROUND((1000000 - total_reviews) / avg_daily_reviews) as days_to_million,
  date('now', '+' || ROUND((1000000 - total_reviews) / avg_daily_reviews) || ' days') as target_date
FROM current_total, daily_review_rate
WHERE total_reviews < 1000000;

Today when I run the query, I get: days_to_million = 698 and target_date - 2026-12-12.

Common Table Expressions

The first step in understanding the query is to grasp Common Table Expressions (CTEs) which are like functions that compute intermediate values for use in the main query. You can recognize CTEs by the use of WITH:

WITH 
  cte1 AS (...),
  cte2 AS (...),
  cte3 AS (...)
SELECT ... FROM ...

It’s basically telling SQLite, before you run the main query, compute these intermediate steps first.

Each CTE defined in the WITH clause:

  • Can be referenced by name in the main query
  • Can reference CTEs defined before it (like cte2 could use cte1)
  • Only exists for the duration of the query
  • Can be used multiple times in the main query if needed

We could get by without CTEs here if we were willing to compromise legibility by using more nested queries.


I’ll break down the query step by step:

  1. First, we have two Common Table Expressions (CTEs) that prepare our data:

    WITH 
    daily_review_rate AS (...)
    current_total AS (...)
    
  2. Let’s look at the inner workings of daily_review_rate:

    SELECT AVG(reviews_per_day) as avg_daily_reviews
    FROM (
      SELECT 
        date(ROUND(id/1000), 'unixepoch') as review_date,
        COUNT(*) as reviews_per_day
      FROM revlog
      WHERE date(ROUND(id/1000), 'unixepoch') >= date('now', '-2 years')
      GROUP BY review_date
      HAVING reviews_per_day >= 25
    )
    
    • The innermost subquery converts millisecond timestamps to dates (date(ROUND(id/1000), 'unixepoch'))
    • Groups these by date and counts reviews per day
    • Filters for days with 25+ reviews
    • The innter query only accepts reviews in the last 2 years so that we only consider these more recent data in the calculations.
    • The outer query then averages these daily counts
  3. The current_total CTE is simpler:

    SELECT COUNT(*) as total_reviews
    FROM revlog
    
    • Just counts all reviews ever done
  4. The main query brings it all together:

    SELECT 
      ROUND((1000000 - total_reviews) / avg_daily_reviews) as days_to_million,
      date('now', '+' || ROUND((1000000 - total_reviews) / avg_daily_reviews) || ' days') as target_date
    FROM current_total, daily_review_rate
    WHERE total_reviews < 1000000;
    
    • Calculates remaining reviews needed (1000000 - total_reviews)
    • Divides by average daily rate to get days needed
    • Uses SQLite’s date function to add these days to today
    • Only returns results if we haven’t hit 1 million yet

Each step builds on the previous ones, using the CTEs like building blocks to construct the final calculation. The query is structured this way to make each calculation clear and separate, rather than trying to do everything in one giant nested query.

Enjoy! You can run the query from the sqlite3 on the command line if you want, or if you’re on macOS you could use Base or another GUI SQLite application. Happy reviewing!

For any questions, you can get in touch at my contact page.

Why I'm quitting Facebook (again)

This isn’t the first time, but I hope it will be the last.

Facebook, for me has long been a source of enjoyment and connection. But it also leaves me feeling cringey. So what changed?

What changed is that Facebook has gone full-on MAGA and I’m not OK with that:

  • “Meta CEO Mark Zuckerberg met with President-elect Donald Trump on Friday [January 10, 2025] at Mar-a-Lago, two sources familiar tell CNN. Meta declined to comment on the meeting between Zuckerberg and Trump.” - Source
  • Meta said today [January 7, 2025] it will end its fact-checking program in exchange for X-style community notes as part of a slate of changes targeting ‘censorship’ and embracing ‘free expression’. - Source,
    • We all know how this has gone at “X”, where self-proclaimed “free speech absolutist” has actively shaped pro-Republican messaging on the platform.
  • “Joel Kaplan, a prominent Republican, replaced Meta’s policy chief Nick Clegg last week. (He said Meta’s third-party fact-checkers have demonstrated ‘too much political bias’ in a Fox News interview this morning [January 7, 2025.)” - Source
  • “CEO Mark Zuckerberg dined at Mar-a-Lago on Thanksgiving eve. [November 27, 2024]” - Source
  • “The company [Meta/Facebook] pledged a $1 million donation to Trump’s inauguration.” - Source
  • “On Monday, it [Meta] added three people to its board, including close Trump ally Dana White.” - Source
    • I didn’t know who Dana White was but he appears to be the president and CEO of Ultimate Fighting Championship (UFC) and the owner of Power Slap, which is a “slap fighting” promotion, whatever that is. The bottom line is that he sounds like he’s rich and into violence, just the type of person that would appeal to Tr*mp.

So thanks for the memories, Facebook. But for me this is the end of the road.

I write things here from time-to-time and you can also get in touch at my contact page.

Registering a custom collation to query Anki database

While working on a project that requires querying the Anki database directly outside of the Anki desktop application, I encountered an interesting issue with sqlite3 collations. This is is just a short post about how I went about registering a collation in order to execute SQL queries against the Anki db. CAUTION Before you run anything that accesses the Anki database, you absolutely should backup your database first.

Fix your Anki streak - the script edition

Like many Anki users, I keep track of my streaks because it motivates me to do my reviews each day. But since life gets in the way sometimes, I may miss my reviews in one or more decks. It has been years since I’ve neglected to do all of my reviews; but sometimes I will forget to come back later in the day to finish up some of my decks. Since I like to have a clean review heatmap, I will “fix” my streak in a skipped deck.

An API (sort of) for adding links to ArchiveBox

I use ArchiveBox extensively to save web content that might change or disappear. While a REST API is apparently coming eventually, it doesn’t appear to have been merged into the main fork. So I cobbled together a little application to archive links via a POST request. It takes advantage of the archivebox command line interface. If you are impatient, you can skip to the full source code. Otherwise I’ll describe my setup to provide some context.

A Keyboard Maestro action to save bookmarks to Espial

So this is a little esoteric, but it meets a need I encountered; and it may meet yours if you use Espial, Keyboard Maestro and are on macOS. For several years I’ve been using Espial a bookmark manager that looks and feels like Pinboard, but is both self-hosted and drama-free1. Espial is easy to setup, stores its data in a comprehensible sqlite database and has an API, which comes in handy when it came to solving the problem I encountered.

Louisiana and the Ten Commandments

Recently, the governor of Louisiana signed a bill requiring all public school classrooms in the state to display a poster-sized copy of the Ten Commandments. In the “Beforetimes” (before the current partisan Supreme Court took shape), this would have been struck down immediately as a violation of the Establishment Clause of the First Amendment. This bill is a clear violation of that clause. I imagine that the justices will dance around the cultural and historical significance of the document without stopping to consider the state’s motives in passing this law.

Improving vegetable seed germination with chemical pretreatment

Some vegetable seeds, particularly many exotic chilli pepper varieties and some Asian eggplants are tricky to germinate. After trying the obvious things - cold-induced forced dormancy (cold stratification), abundant moisture, high humidity, and temperatures over 80F, I’ve found that some seeds simply do not germinate with much success at all. But having read a number of articles on this problem, we decided to try an intensive chemical process to see if we could achieve better results.

A quick word on ATtiny 1-series interrupts

The Atmel AVR 8-bit microcontrollers have always been a favourite for tinkering; and the massive popularity of the Arduino based on the ATmega 168 and 328 MCUs introduced a lot of hobbyists to this series. The companion ATtiny series from Atmel were the poor stepchildren of the ATmega controllers to an extent - useful for small projects but often quite limited. However, the acquisition of Atmel by Microchip Technology in 2016 ushered in a new series of MCUs bearing the same moniker of ATtiny, but much more capable and innovative.

FreeRTOS stack size on ESP32 - words or bytes?

Although FreeRTOS1 is an indispensible tool for working on anything more than the simplest application on ESP32, there are some difficulties to master, such as multitasking. Multitasking using FreeRTOS is accomplished by creating tasks with xTaskCreate() or xTaskCreatePinnedToCore(). In both of these calls, one of the parameters is uxStackDepth which is the allocated stack size for the task. The FreeRTOS documentation on the subject is clear about the units for uxStackDepth: