You can either use client-side or server-side pagination. Either way, you won't be able to use standard tools like StandardSetController or OFFSET.
Server-side, you need to use the @ReadOnly annotation to enable the 1,000,000 row limit, which might look like this:
@RemoteAction @ReadOnly public static SObject[] getRecords(Integer page) {
SObject[] results = new SObject[0];
for(SObject[] records: [SELECT ... FROM SObject WHERE ... LIMIT :page*200]) {
results = records;
}
return results;
}
From there, you just repeatedly call the value until you get all of your results:
var buffer = []
function getPage(i) {
myController.getPage(i, function(event, data) {
if(data.length) {
data.forEach(function(v) {buffer.push(v)})
getPage(i+1)
}
}
}
Client-side, it'd look more like this example from the docs:
var result = sforce.connection.query("select id, name from account");
var it = new sforce.QueryResultIterator(result);
var buffer = [];
while (it.hasNext()) {
var account = it.next();
buffer.push(account);
}
<apex:form>
tag.