diff --git a/src/service/admin/mod.rs b/src/service/admin/mod.rs
index c16c5e955321969ca626aca63e4f46ec515267c8..a540eb38b1949b19685a6e2d8673dfb6cf886ad5 100644
--- a/src/service/admin/mod.rs
+++ b/src/service/admin/mod.rs
@@ -26,6 +26,7 @@
 use tokio::sync::Mutex;
 use tracing::{error, warn};
 
+use self::query::QueryCommand;
 use super::pdu::PduBuilder;
 use crate::{
 	service::admin::{
@@ -39,6 +40,7 @@
 pub(crate) mod debug;
 pub(crate) mod federation;
 pub(crate) mod media;
+pub(crate) mod query;
 pub(crate) mod room;
 pub(crate) mod room_alias;
 pub(crate) mod room_directory;
@@ -77,11 +79,12 @@ enum AdminCommand {
 	Media(MediaCommand),
 
 	#[command(subcommand)]
-	// TODO: should i split out debug commands to a separate thing? the
-	// debug commands seem like they could fit in the other categories fine
-	// this is more like a "miscellaneous" category than a debug one
 	/// - Commands for debugging things
 	Debug(DebugCommand),
+
+	#[command(subcommand)]
+	/// - Query all the database getters and iterators
+	Query(QueryCommand),
 }
 
 #[derive(Debug)]
@@ -249,10 +252,25 @@ fn parse_admin_command(&self, command_line: &str) -> Result<AdminCommand, String
 		}
 
 		// Backwards compatibility with `register_appservice`-style commands
-		let command_with_dashes;
+		let command_with_dashes_argv1;
 		if argv.len() > 1 && argv[1].contains('_') {
-			command_with_dashes = argv[1].replace('_', "-");
-			argv[1] = &command_with_dashes;
+			command_with_dashes_argv1 = argv[1].replace('_', "-");
+			argv[1] = &command_with_dashes_argv1;
+		}
+
+		// Backwards compatibility with `register_appservice`-style commands
+		let command_with_dashes_argv2;
+		if argv.len() > 2 && argv[2].contains('_') {
+			command_with_dashes_argv2 = argv[2].replace('_', "-");
+			argv[2] = &command_with_dashes_argv2;
+		}
+
+		// if the user is using the `query` command (argv[1]), replace the database
+		// function/table calls with underscores to match the codebase
+		let command_with_dashes_argv3;
+		if argv.len() > 3 && argv[1].eq("query") {
+			command_with_dashes_argv3 = argv[3].replace('_', "-");
+			argv[3] = &command_with_dashes_argv3;
 		}
 
 		AdminCommand::try_parse_from(argv).map_err(|error| error.to_string())
@@ -267,6 +285,7 @@ async fn process_admin_command(&self, command: AdminCommand, body: Vec<&str>) ->
 			AdminCommand::Federation(command) => federation::process(command, body).await?,
 			AdminCommand::Server(command) => server::process(command, body).await?,
 			AdminCommand::Debug(command) => debug::process(command, body).await?,
+			AdminCommand::Query(command) => query::process(command, body).await?,
 		};
 
 		Ok(reply_message_content)
diff --git a/src/service/admin/query.rs b/src/service/admin/query.rs
new file mode 100644
index 0000000000000000000000000000000000000000..9f91dea0f3cd44d0921260b893310340320305e0
--- /dev/null
+++ b/src/service/admin/query.rs
@@ -0,0 +1,197 @@
+use clap::Subcommand;
+use ruma::{
+	events::{room::message::RoomMessageEventContent, RoomAccountDataEventType},
+	RoomId, UserId,
+};
+
+use crate::{services, Result};
+
+#[cfg_attr(test, derive(Debug))]
+#[derive(Subcommand)]
+/// Query tables from database
+pub(crate) enum QueryCommand {
+	/// - account_data.rs iterators and getters
+	#[command(subcommand)]
+	AccountData(AccountData),
+
+	/// - appservice.rs iterators and getters
+	#[command(subcommand)]
+	Appservice(Appservice),
+
+	/// - presence.rs iterators and getters
+	#[command(subcommand)]
+	Presence(Presence),
+}
+
+#[cfg_attr(test, derive(Debug))]
+#[derive(Subcommand)]
+/// All the getters and iterators from src/database/key_value/account_data.rs
+/// via services()
+pub(crate) enum AccountData {
+	/// - Returns all changes to the account data that happened after `since`.
+	ChangesSince {
+		/// Full user ID
+		user_id: Box<UserId>,
+		/// UNIX timestamp since (u64)
+		since: u64,
+		/// Optional room ID of the account data
+		room_id: Option<Box<RoomId>>,
+	},
+
+	/// - Searches the account data for a specific kind.
+	Get {
+		/// Full user ID
+		user_id: Box<UserId>,
+		/// Account data event type
+		kind: RoomAccountDataEventType,
+		/// Optional room ID of the account data
+		room_id: Option<Box<RoomId>>,
+	},
+}
+
+#[cfg_attr(test, derive(Debug))]
+#[derive(Subcommand)]
+/// All the getters and iterators from src/database/key_value/appservice.rs via
+/// services()
+pub(crate) enum Appservice {
+	/// - Gets the appservice registration info/details from the ID as a string
+	GetRegistration {
+		/// Appservice registration ID
+		appservice_id: Box<str>,
+	},
+}
+
+#[cfg_attr(test, derive(Debug))]
+#[derive(Subcommand)]
+/// All the getters and iterators from src/database/key_value/presence.rs via
+/// services()
+pub(crate) enum Presence {
+	/// - Returns the latest presence event for the given user.
+	GetPresence {
+		/// Full user ID
+		user_id: Box<UserId>,
+	},
+
+	/// - Returns the most recent presence updates that happened after the event
+	///   with id `since`.
+	PresenceSince {
+		/// UNIX timestamp since (u64)
+		since: u64,
+	},
+}
+
+/// Processes admin command
+#[allow(non_snake_case)]
+pub(crate) async fn process(command: QueryCommand, _body: Vec<&str>) -> Result<RoomMessageEventContent> {
+	match command {
+		QueryCommand::AccountData(AccountData) => account_data(AccountData).await,
+		QueryCommand::Appservice(Appservice) => appservice(Appservice).await,
+		QueryCommand::Presence(Presence) => presence(Presence).await,
+	}
+}
+
+/// All the getters and iterators in key_value/account_data.rs via services()
+async fn account_data(subcommand: AccountData) -> Result<RoomMessageEventContent> {
+	match subcommand {
+		AccountData::ChangesSince {
+			user_id,
+			since,
+			room_id,
+		} => {
+			let timer = tokio::time::Instant::now();
+			let results = services()
+				.account_data
+				.changes_since(room_id.as_deref(), &user_id, since)?;
+			let query_time = timer.elapsed();
+
+			Ok(RoomMessageEventContent::text_html(
+				format!("Query completed in {query_time:?}:\n\n```\n{:?}```", results),
+				format!(
+					"<p>Query completed in {query_time:?}:</p>\n<pre><code>{:?}\n</code></pre>",
+					results
+				),
+			))
+		},
+		AccountData::Get {
+			user_id,
+			kind,
+			room_id,
+		} => {
+			let timer = tokio::time::Instant::now();
+			let results = services()
+				.account_data
+				.get(room_id.as_deref(), &user_id, kind)?;
+			let query_time = timer.elapsed();
+
+			Ok(RoomMessageEventContent::text_html(
+				format!("Query completed in {query_time:?}:\n\n```\n{:?}```", results),
+				format!(
+					"<p>Query completed in {query_time:?}:</p>\n<pre><code>{:?}\n</code></pre>",
+					results
+				),
+			))
+		},
+	}
+}
+
+/// All the getters and iterators in key_value/appservice.rs via services()
+async fn appservice(subcommand: Appservice) -> Result<RoomMessageEventContent> {
+	match subcommand {
+		Appservice::GetRegistration {
+			appservice_id,
+		} => {
+			let timer = tokio::time::Instant::now();
+			let results = services()
+				.appservice
+				.get_registration(appservice_id.as_ref())
+				.await;
+			let query_time = timer.elapsed();
+
+			Ok(RoomMessageEventContent::text_html(
+				format!("Query completed in {query_time:?}:\n\n```\n{:?}```", results),
+				format!(
+					"<p>Query completed in {query_time:?}:</p>\n<pre><code>{:?}\n</code></pre>",
+					results
+				),
+			))
+		},
+	}
+}
+
+/// All the getters and iterators in key_value/appservice.rs via services()
+async fn presence(subcommand: Presence) -> Result<RoomMessageEventContent> {
+	match subcommand {
+		Presence::GetPresence {
+			user_id,
+		} => {
+			let timer = tokio::time::Instant::now();
+			let results = services().presence.get_presence(&user_id)?;
+			let query_time = timer.elapsed();
+
+			Ok(RoomMessageEventContent::text_html(
+				format!("Query completed in {query_time:?}:\n\n```\n{:?}```", results),
+				format!(
+					"<p>Query completed in {query_time:?}:</p>\n<pre><code>{:?}\n</code></pre>",
+					results
+				),
+			))
+		},
+		Presence::PresenceSince {
+			since,
+		} => {
+			let timer = tokio::time::Instant::now();
+			let results = services().presence.presence_since(since);
+			let query_time = timer.elapsed();
+
+			let presence_since: Vec<(_, _, _)> = results.collect();
+
+			Ok(RoomMessageEventContent::text_html(
+				format!("Query completed in {query_time:?}:\n\n```\n{:?}```", presence_since),
+				format!(
+					"<p>Query completed in {query_time:?}:</p>\n<pre><code>{:?}\n</code></pre>",
+					presence_since
+				),
+			))
+		},
+	}
+}