Virtual tables let you create table-like interfaces that don’t store data in the usual way, opening up a world of possibilities beyond simple file-backed storage.

Imagine you have a stream of real-time sensor data coming in, and you want to query it using SQL without writing it all to disk first. That’s where virtual tables shine.

import sqlite3

class SensorDataTable:
    def __init__(self):
        self.data = []
        self.next_id = 1

    def connect(self):
        # In a real scenario, this would connect to your sensor stream
        pass

    def disconnect(self):
        # Clean up resources
        pass

    def open(self):
        # Called when a cursor is created for this table
        return self # Return an object that implements the cursor methods

    def close(self):
        # Called when the cursor is closed
        pass

    def next(self, rowid):
        # Called to fetch the next row. rowid is the last rowid returned, or 0 for the first row.
        # For simplicity, we'll just return the next available row if rowid is 0,
        # otherwise we'll simulate finding a row (in a real case, you'd search by rowid).
        if rowid == 0 and self.data:
            return (self.data[0][0], self.data[0][1], self.data[0][2]) # Return (rowid, timestamp, value)
        elif rowid > 0 and rowid < len(self.data):
            # Simulate finding by rowid. In a real scenario, this would be efficient.
            return (self.data[rowid][0], self.data[rowid][1], self.data[rowid][2])
        return None # No more rows

    def eof(self):
        # Called to check if we've reached the end of the data
        return not self.data

    def filter(self, idx_num, idx_str, constraint_values):
        # This is where query optimization happens. For simplicity, we'll ignore constraints
        # and just return all data.
        self.data = [(self.next_id + i, 1678886400 + i*10, 20.5 + i*0.1) for i in range(5)]
        self.next_id += 5
        return self # Return self to allow chaining of cursor methods

    def get_rowid(self):
        # Return the rowid for the current row.
        # In our simple example, rowid is the first element of the tuple.
        if self.data:
            return self.data[0][0]
        return None

    def column(self, column_index):
        # Return the value for the given column index for the current row.
        # Column indices: 0 for rowid, 1 for timestamp, 2 for value
        current_row = self.data[0] if self.data else None
        if current_row:
            if column_index == 0: return current_row[0]
            if column_index == 1: return current_row[1]
            if column_index == 2: return current_row[2]
        return None

    # These methods are needed to define the schema of the virtual table
    def best_index(self, constraints, orderbys):
        # This method is crucial for query planning. It tells SQLite
        # how to best access the data based on constraints and ordering.
        # For this example, we'll always do a full table scan (index 0).
        return (None, 0, None, False, 1000) # (argv_index, index_code, index_name, constraint_usable, estimated_cost)

    def disconnect(self):
        pass # No-op for this example

    def destroy(self):
        pass # No-op for this example

    def update(self, *args):
        # Called for INSERT, UPDATE, DELETE. Not implemented here.
        raise NotImplementedError("Updates not supported for SensorDataTable")

    def begin(self): pass
    def sync(self): pass
    def commit(self): pass
    def rollback(self): pass

# Register the virtual table module
class SensorTableModule:
    def __init__(self):
        self.sensor_data = SensorDataTable()

    def Create(self, db, modulename, dbname, tablename, *args):
        # Called when CREATE VIRTUAL TABLE is executed
        return self.sensor_data

    def Connect(self, db, modulename, dbname, tablename, *args):
        # Called when opening a connection to the virtual table
        return self.sensor_data

    def Disconnect(self, p VTab):
        # Called when closing the connection
        pass

    def Destroy(self, p VTab):
        # Called when the virtual table is dropped
        pass

con = sqlite3.connect(":memory:")
con.create_module("sensor_module", SensorTableModule())
con.execute("CREATE VIRTUAL TABLE sensors USING sensor_module();")

# Simulate adding some data
sensor_data_instance = con.get_module("sensor_module").sensor_data
sensor_data_instance.data = [(1, 1678886400, 20.5), (2, 1678886410, 20.6)]
sensor_data_instance.next_id = 3

# Query the virtual table
cursor = con.execute("SELECT rowid, timestamp, value FROM sensors WHERE value > 20.5;")
for row in cursor.fetchall():
    print(row)

# Insert into the virtual table (will fail as not implemented)
try:
    con.execute("INSERT INTO sensors (timestamp, value) VALUES (1678886500, 21.0);")
except NotImplementedError as e:
    print(f"Insert failed as expected: {e}")

con.close()

The most surprising thing about virtual tables is that they allow you to bypass SQLite’s entire file I/O and storage engine, letting you serve data from anywhere – memory, network sockets, even other databases – all while speaking SQL.

Here’s a look at the SensorDataTable class in action. We set up a simple in-memory data store. The connect and disconnect methods are placeholders for real-world interactions with a sensor stream. The open and close methods manage cursor lifecycles. The filter method is where the magic happens for queries: when SELECT is called, filter is invoked to prepare the data. In this simplified version, it just populates self.data. The next method then iterates through this prepared data, simulating how SQLite fetches rows one by one. get_rowid and column provide the row identifier and column values, respectively, for the current row being processed by SQLite.

The best_index method is SQLite’s optimizer’s best friend. It’s a contract: you tell SQLite how you can satisfy query constraints and ordering. Returning (None, 0, None, False, 1000) means "I can’t use any specific index for constraints, I’ll do a full scan (index code 0), I have no index name, constraints aren’t usable for indexing, and it will cost me 1000 units." This tells SQLite to just iterate through everything. A more advanced best_index would analyze constraints and orderbys to suggest the most efficient access path.

The SensorTableModule class acts as a factory and manager for our virtual table. Create and Connect are called by SQLite to get an instance of our SensorDataTable that it can then interact with. Destroy and Disconnect handle cleanup. We register this module with con.create_module("sensor_module", SensorTableModule()) and then create our virtual table sensors using CREATE VIRTUAL TABLE sensors USING sensor_module();.

When we query SELECT rowid, timestamp, value FROM sensors WHERE value > 20.5;, SQLite calls sensor_module.Connect, then sensor_data_instance.filter(idx_num=0, idx_str=None, constraint_values=()). Since our filter populates self.data with specific values, and our best_index indicated a full scan, SQLite then repeatedly calls sensor_data_instance.next() and sensor_data_instance.column() to retrieve rows and their values until sensor_data_instance.eof() returns True. The WHERE value > 20.5 clause is actually applied after filter returns all data, because our filter and best_index didn’t implement any constraint handling. A more sophisticated implementation would filter within filter or use best_index to signal that it can handle the constraint efficiently.

The update method is where you’d implement INSERT, UPDATE, and DELETE operations. By raising NotImplementedError, we’ve explicitly told SQLite that writes are not supported for this virtual table. If you wanted to support writes, you’d implement the logic to modify your underlying data source here.

The real power comes when best_index is used intelligently. If your virtual table could efficiently search based on a timestamp range, your best_index implementation would analyze the constraints argument for a timestamp condition and return information that tells SQLite to use that specific search mechanism, drastically improving performance for targeted queries.

The transactional methods (begin, sync, commit, rollback) are essential for ensuring data integrity when your virtual table interacts with external state that supports transactions.

Want structured learning?

Take the full Sqlite course →