Doing a setkey here would be costly (even if you were to use the fast ordering in 1.8.11), because it has to move the data (by reference) as well.
However, you can get around this case by using floor function. Basically, if you want all the numbers in [1,2] (Note: inclusive of 1 and 2 here), then floor will provide a value of "1" for all these values. That is, you can do:
system.time(t1 <- dt[floor(a) == 1])
# user system elapsed
# 0.234 0.001 0.238
This is equivalent to doing dt[a >= 1 & a <=2] and is twice as fast.
system.time(t2 <- dt[a >= 1 & a <= 2])
# user system elapsed
# 0.518 0.081 0.601
identical(t1,t2) # [1] TRUE
However, since you don't want the equality, you can use a hack to subtract the tolerance = .Machine$double.eps^0.5 from column a. If the value is in the range [1, 1+tolerance), then it's still considered to be 1. And if it's just more, then it's not 1 anymore (internally). That is, it's the smallest number > 1 that the machine can identify as not 1. So, if you subtract 'a' by tolerance all numbers that are internally represented as "1" will become < 1 and floor(.) will result in 0. So, you'll get the range > 1 and < 2 instead. That is,
dt[floor(a-.Machine$double.eps^0.5)==1]
will give the equivalent result as dt[a>1 & a<2].
If you've to do this repetitively, then probably creating a new column with this floor function and setting key on that integer column could help:
dt[, fa := as.integer(floor(a-.Machine$double.eps^0.5))]
system.time(setkey(dt, fa)) # v1.8.11
# user system elapsed
# 0.852 0.158 1.043
Now, you can query whatever range you want using binary search:
> system.time(dt[J(1L)]) # equivalent to > 1 & < 2
# user system elapsed
# 0.071 0.002 0.076
> system.time(dt[J(1:4)]) # equivalent to > 1 & < 5
# user system elapsed
# 0.082 0.002 0.085
betweenwill not save any time because it contains the codex >= lower & x <= upper.dt[a > 1 & a < 2]will be just as fast