Newsletter
TechAnV Blog
Get updates on security engineering, Rust, eBPF, and DevSecOps. No spam, unsubscribe anytime.
Check your inbox and click the confirmation link to complete your subscription.
Counting SQLite virtual machine operations#
When SQLite executes a query, it does so by executing a sequence of virtual machine operations.
There are mechanisms for cancelling a query after a specific number of these operations, to protect against long-running queries.
To work with those mechanisms, it’s useful to get a feel for how many operations different queries execute.
Thanks to tips on the SQLite forum I now know how to count these operations using the sqlite3 command line tool, using .stats vmstats to turn on display of that number after each query:
1% sqlite3 fixtures.db2SQLite version 3.36.0 2021-06-18 18:58:493Enter ".help" for usage hints.4sqlite> .stats vmstep5sqlite> select * from facetable;61|2019-01-14 08:00:00|1|1|CA|1|Mission|["tag1", "tag2"]|[{"foo": "bar"}]|one72|2019-01-14 08:00:00|1|1|CA|1|Dogpatch|["tag1", "tag3"]|[]|two83|2019-01-14 08:00:00|1|1|CA|1|SOMA|[]|[]|94|2019-01-14 08:00:00|1|1|CA|1|Tenderloin|[]|[]|105|2019-01-15 08:00:00|1|1|CA|1|Bernal Heights|[]|[]|116|2019-01-15 08:00:00|1|1|CA|1|Hayes Valley|[]|[]|127|2019-01-15 08:00:00|1|1|CA|2|Hollywood|[]|[]|138|2019-01-15 08:00:00|1|1|CA|2|Downtown|[]|[]|149|2019-01-16 08:00:00|1|1|CA|2|Los Feliz|[]|[]|1510|2019-01-16 08:00:00|1|1|CA|2|Koreatown|[]|[]|1611|2019-01-16 08:00:00|1|1|MI|3|Downtown|[]|[]|1712|2019-01-17 08:00:00|1|1|MI|3|Greektown|[]|[]|1813|2019-01-17 08:00:00|1|1|MI|3|Corktown|[]|[]|1914|2019-01-17 08:00:00|1|1|MI|3|Mexicantown|[]|[]|2015|2019-01-17 08:00:00|2|0|MC|4|Arcadia Planitia|[]|[]|21VM-steps: 18722sqlite> select * from facetable limit 3;231|2019-01-14 08:00:00|1|1|CA|1|Mission|["tag1", "tag2"]|[{"foo": "bar"}]|one242|2019-01-14 08:00:00|1|1|CA|1|Dogpatch|["tag1", "tag3"]|[]|two253|2019-01-14 08:00:00|1|1|CA|1|SOMA|[]|[]|26VM-steps: 46Using .stats on shows a more detailed group of statistics about the query:
1sqlite> .stats on2sqlite> select * from facetable limit 3;31|2019-01-14 08:00:00|1|1|CA|1|Mission|["tag1", "tag2"]|[{"foo": "bar"}]|one42|2019-01-14 08:00:00|1|1|CA|1|Dogpatch|["tag1", "tag3"]|[]|two53|2019-01-14 08:00:00|1|1|CA|1|SOMA|[]|[]|6Memory Used: 195520 (max 195776) bytes7Number of Outstanding Allocations: 525 (max 526)8Number of Pcache Overflow Bytes: 5696 (max 5696) bytes9Largest Allocation: 122400 bytes10Largest Pcache Allocation: 4104 bytes11Lookaside Slots Used: 94 (max 122)12Successful lookaside attempts: 43113Lookaside failures due to size: 414Lookaside failures due to OOM: 015Pager Heap Usage: 23008 bytes16Page cache hits: 617Page cache misses: 418Page cache writes: 019Page cache spills: 020Schema Heap Usage: 25216 bytes21Statement Heap/Lookaside Usage: 15504 bytes22Fullscan Steps: 223Sort Operations: 024Autoindex Inserts: 025Virtual Machine Steps: 4626Reprepare operations: 027Number of times run: 128Memory used by prepared stmt: 15504There is also a sqlite_stmt virtual table (documented here) which can be enabled using a loadable module, but I have not yet managed to get this to compile and load into the Python sqlite3 environment.
Cancelling queries after a specified number of opcodes#
You can set an upper limit on the number of opcodes in the sqlite3 command line using .progress 100 --limit 1, as described here:
1sqlite> .progress 100 --limit 12sqlite> select * from facetable limit 3;31|2019-01-14 08:00:00|1|1|CA|1|Mission|["tag1", "tag2"]|[{"foo": "bar"}]|one42|2019-01-14 08:00:00|1|1|CA|1|Dogpatch|["tag1", "tag3"]|[]|two53|2019-01-14 08:00:00|1|1|CA|1|SOMA|[]|[]|6sqlite> select * from facetable;71|2019-01-14 08:00:00|1|1|CA|1|Mission|["tag1", "tag2"]|[{"foo": "bar"}]|one82|2019-01-14 08:00:00|1|1|CA|1|Dogpatch|["tag1", "tag3"]|[]|two93|2019-01-14 08:00:00|1|1|CA|1|SOMA|[]|[]|104|2019-01-14 08:00:00|1|1|CA|1|Tenderloin|[]|[]|115|2019-01-15 08:00:00|1|1|CA|1|Bernal Heights|[]|[]|126|2019-01-15 08:00:00|1|1|CA|1|Hayes Valley|[]|[]|137|2019-01-15 08:00:00|1|1|CA|2|Hollywood|[]|[]|14Progress limit reached (1)15Error: interruptedIn Python code this can be achieved like so:
1conn.set_progress_handler(lambda: 1, 100)This sets a progress handler which will be called after every 100 opcodes - but by returning 1 the handler causes the query to be cancelled:
1>>> import sqlite32>>> conn = sqlite3.connect("fixtures.db")3>>> conn.set_progress_handler(lambda: 1, 200)4>>> conn.execute("select * from facetable limit 3").rows()5# Outputs rows6>>> conn.execute("select * from facetable").fetchall()7Traceback (most recent call last):8 File "<stdin>", line 1, in <module>9sqlite3.OperationalError: interruptedThis didn’t exactly work how I expected - I had to change the number from 100 to 200 and I’m not sure why. But it illustrates the principle.