diff --git a/src/query/mod.rs b/src/query/mod.rs
index ee8d74e2fe3cde4b59b701347fc168952febf496..8f463aa10c4423a3603f54f7bb3745973432a241 100644
--- a/src/query/mod.rs
+++ b/src/query/mod.rs
@@ -244,7 +244,7 @@ impl From<(u64, Unit)> for Duration {
 
 /// Specify time offset for the queries
 #[derive(Debug, Clone, Copy)]
-pub struct Offset(i64, Unit);
+pub struct Offset(pub i64, pub Unit);
 
 impl Display for Offset {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
@@ -269,6 +269,71 @@ impl Display for Scalar {
 impl Sealed for Scalar {}
 impl ops::Operable for Scalar {}
 
+/// A subquery that lets you make a ranged query on an instant query with an optional resolution/set and offset
+///
+/// This type can also be constructed using the [`subquery`] method provided by the [`QueryExt`] trait.
+#[derive(Clone, Debug)]
+pub struct SubQry<E: Operable> {
+    expr: E,
+    dur: (u64, Unit),
+    res: Option<(u64, Unit)>,
+    off: Option<Offset>,
+}
+
+impl<E: Operable> SubQry<E> {
+    /// Create a subquery using an instant query
+    pub fn new(expr: E, duration: Duration) -> Self {
+        Self {
+            expr,
+            dur: (duration.0, duration.1),
+            res: None,
+            off: None,
+        }
+    }
+    /// Set the resolution/step for the subquery
+    pub fn resolution(mut self, res: Duration) -> Self {
+        self.res.replace((res.0, res.1));
+        self
+    }
+
+    /// Set the subquery's offset duration
+    pub fn offset(mut self, off: Offset) -> Self {
+        self.off.replace(off);
+        self
+    }
+}
+
+impl<E: Operable> Display for SubQry<E> {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(
+            f,
+            "({expr})[{v}{unit}",
+            expr = self.expr,
+            v = self.dur.0,
+            unit = self.dur.1
+        )?;
+        if let Some(res) = self.res.as_ref() {
+            write!(f, ":{v}{unit}", v = res.0, unit = res.1)?;
+        }
+        f.write_str("]")?;
+        if let Some(off) = self.off.as_ref() {
+            write!(f, " {off}")?;
+        }
+        Ok(())
+    }
+}
+
+impl<E: Operable> Sealed for SubQry<E> {}
+
+impl<E: Operable> ops::Operable for SubQry<E> {}
+
+impl<E: Operable> IntoQuery for SubQry<E> {
+    type Target = String;
+    fn into_query(self) -> Self::Target {
+        self.to_string()
+    }
+}
+
 /// Raw Expression type that allows raw strings to be used in the builder API where necessary
 ///
 /// This also allows combining complex queries, especially when a function is generic over a single type
@@ -339,7 +404,7 @@ impl Display for RawExpr {
 /// Additional/extended query builder API functionality
 ///
 /// This trait has a blanket implementation for all types that implement [`Operable`]
-pub trait QueryExt {
+pub trait QueryExt: Operable {
     /// Convert a query builder API type into a [`RawExpr`] type
     ///
     /// This is useful when a function takes a sequence (array/vector/iterator) of a generic type `T: Operable`
@@ -348,7 +413,9 @@ pub trait QueryExt {
     /// While the `String` type could have been allowed to be part of the builder API directly,
     /// it is by design that a user must explicitly turn a string into a [`RawExpr`] saying that
     /// they're opting in to use a raw string in the query builder API.
-    fn to_expr(&self) -> RawExpr;
+    fn to_expr(&self) -> RawExpr {
+        RawExpr(self.to_string())
+    }
     /// Call a function or closure on `Self` to transform it into another type
     ///
     /// This is similar to the `Iterator's` `map` method, except this is eagerly evaluated and operates on a single item.
@@ -368,22 +435,25 @@ pub trait QueryExt {
     /// > idea of what's happening when writing/reading the complex queries.
     fn then<R>(self, func: impl FnOnce(Self) -> R) -> R
     where
-        Self: Sized;
-}
-
-impl<T: Operable> QueryExt for T {
-    fn to_expr(&self) -> RawExpr {
-        RawExpr(self.to_string())
+        Self: Sized,
+    {
+        func(self)
     }
 
-    fn then<R>(self, func: impl FnOnce(Self) -> R) -> R
+    /// Turn the current query expression into a subquery using the provided range
+    ///
+    /// Optional resolution and offset can be set for the subquery using the provided methods of the
+    /// returned type.
+    fn subquery(self, dur: Duration) -> SubQry<Self>
     where
         Self: Sized,
     {
-        func(self)
+        SubQry::new(self, dur)
     }
 }
 
+impl<T: Operable> QueryExt for T {}
+
 #[cfg(test)]
 mod tests {
     use super::*;
@@ -407,4 +477,16 @@ mod tests {
             metric
         )
     }
+
+    #[test]
+    fn subquery() {
+        let qry = Metric::new("metric").subquery(Duration(30, Unit::Day));
+        assert_eq!(qry.to_string(), "(metric)[30d]");
+
+        let qry = qry.resolution(Duration(5, Unit::Min));
+        assert_eq!(qry.to_string(), "(metric)[30d:5m]");
+
+        let qry = qry.offset(Offset(1, Unit::Day));
+        assert_eq!(qry.to_string(), "(metric)[30d:5m] offset 1d");
+    }
 }
diff --git a/tests/query.rs b/tests/query.rs
index 69c2002eb929f75b92f5bab55c0bfd656cf88c70..36167bbeaa62bb118adc090491ec6156aba77029 100644
--- a/tests/query.rs
+++ b/tests/query.rs
@@ -1,6 +1,6 @@
 use mquery::query::{
     ops::{Arithmetic, Comparison, Logical},
-    Label, Metric, Scalar,
+    Duration, Label, Metric, Offset, QueryExt, Scalar, Unit,
 };
 
 mod utils;
@@ -18,6 +18,20 @@ async fn basic_query_with_labels() {
     utils::send_query(query).await.unwrap();
 }
 
+#[tokio::test]
+async fn basic_subquery() {
+    let query = Metric::new("test_metric")
+        .labels([
+            Label::Eq("eq", "eqvalue"),
+            Label::RxEq("rxeq", ".*"),
+            Label::RxNe("rxne", ".*"),
+        ])
+        .subquery(Duration(30, Unit::Day))
+        .resolution(Duration(10, Unit::Min))
+        .offset(Offset(1, Unit::Sec));
+    utils::send_query(query).await.unwrap();
+}
+
 #[tokio::test]
 async fn arithmetic_ops() {
     let query = Metric::new("test_metric")