Help with improving database query

Hello, I am seeking advice on how to improve a database query I am currently working with. My objective is to get the data of a prompt using its ID, and this data includes the names of the tags associated with it.The currently way I am doing this works, but feels hacking so wondering if theres a more efficient way to achieve this, possibly through restructuring the database or altering the query itself. Below are the definitions for my "prompts" and "tags" tables from supabase DB:
CREATE TABLE public.prompts (
id UUID NOT NULL DEFAULT gen_random_uuid(),
profile_id UUID NOT NULL,
title TEXT NOT NULL,
description TEXT NOT NULL,
is_favorited BOOLEAN NOT NULL DEFAULT FALSE,
tag_ids UUID[] NULL,
visibility public.prompt_visibility NOT NULL DEFAULT 'private'::prompt_visibility,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT prompts_pkey PRIMARY KEY (id),
CONSTRAINT prompts_profile_id_fkey FOREIGN KEY (profile_id) REFERENCES profiles(id) ON DELETE CASCADE
) TABLESPACE pg_default;
CREATE TABLE public.prompts (
id UUID NOT NULL DEFAULT gen_random_uuid(),
profile_id UUID NOT NULL,
title TEXT NOT NULL,
description TEXT NOT NULL,
is_favorited BOOLEAN NOT NULL DEFAULT FALSE,
tag_ids UUID[] NULL,
visibility public.prompt_visibility NOT NULL DEFAULT 'private'::prompt_visibility,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT prompts_pkey PRIMARY KEY (id),
CONSTRAINT prompts_profile_id_fkey FOREIGN KEY (profile_id) REFERENCES profiles(id) ON DELETE CASCADE
) TABLESPACE pg_default;
create table
public.tags (
id uuid not null default gen_random_uuid (),
profile_id uuid not null,
name text not null,
created_at timestamp with time zone not null default now(),
updated_at timestamp with time zone not null default now(),
constraint tags_pkey primary key (id),
constraint unique_user_tag_name unique (profile_id, name),
constraint tags_profile_id_fkey foreign key (profile_id) references profiles (id) on delete cascade
) tablespace pg_default;
create table
public.tags (
id uuid not null default gen_random_uuid (),
profile_id uuid not null,
name text not null,
created_at timestamp with time zone not null default now(),
updated_at timestamp with time zone not null default now(),
constraint tags_pkey primary key (id),
constraint unique_user_tag_name unique (profile_id, name),
constraint tags_profile_id_fkey foreign key (profile_id) references profiles (id) on delete cascade
) tablespace pg_default;
26 Replies
king.edwards
king.edwardsOP•15mo ago
My query
/**
* Fetches a prompt based on its ID.
* @param {string} promptId - The ID of the prompt.
* @returns {Promise<object|null>} The prompt data or null if not found.
* @throws {Error} Will throw an error if the query fails.
*/
export async function fetchPromptById(promptId: string) {
try {
// Fetch the prompt data
const promptData = await drizzleClient
.select({
prompt: {
title: promptsTable.title,
description: promptsTable.description,
tagIds: promptsTable.tagIds
},
creator: {
username: profilesTable.username,
fullName: profilesTable.fullName,
avatarUrl: profilesTable.avatarUrl
}
})
.from(promptsTable)
.where(eq(promptsTable.id, promptId))
.innerJoin(profilesTable, eq(profilesTable.id, promptsTable.profileId));

// If no prompt data is found, return null
if (!promptData) {
return null;
}

// Fetch the tag names associated with the tag IDs
const tagNames = await drizzleClient
.select({ name: tagsTable.name })
.from(tagsTable)
.where(inArray(tagsTable.id, promptData[0].prompt.tagIds));

// Add the tag names to the prompt data
promptData[0].prompt.tagNames = tagNames.map(tag => tag.name);

// Return the first (and only) item in the prompt data array
return promptData[0];
} catch (error) {
console.error('Error fetching prompt:', error, { promptId });
throw new Error('Error fetching prompt');
}
}
/**
* Fetches a prompt based on its ID.
* @param {string} promptId - The ID of the prompt.
* @returns {Promise<object|null>} The prompt data or null if not found.
* @throws {Error} Will throw an error if the query fails.
*/
export async function fetchPromptById(promptId: string) {
try {
// Fetch the prompt data
const promptData = await drizzleClient
.select({
prompt: {
title: promptsTable.title,
description: promptsTable.description,
tagIds: promptsTable.tagIds
},
creator: {
username: profilesTable.username,
fullName: profilesTable.fullName,
avatarUrl: profilesTable.avatarUrl
}
})
.from(promptsTable)
.where(eq(promptsTable.id, promptId))
.innerJoin(profilesTable, eq(profilesTable.id, promptsTable.profileId));

// If no prompt data is found, return null
if (!promptData) {
return null;
}

// Fetch the tag names associated with the tag IDs
const tagNames = await drizzleClient
.select({ name: tagsTable.name })
.from(tagsTable)
.where(inArray(tagsTable.id, promptData[0].prompt.tagIds));

// Add the tag names to the prompt data
promptData[0].prompt.tagNames = tagNames.map(tag => tag.name);

// Return the first (and only) item in the prompt data array
return promptData[0];
} catch (error) {
console.error('Error fetching prompt:', error, { promptId });
throw new Error('Error fetching prompt');
}
}
Angelelz
Angelelz•15mo ago
This can be done in one query Just use to innerJoins in the first query
const promptData = await drizzleClient
.select({
prompt: {
title: promptsTable.title,
description: promptsTable.description,
tagIds: promptsTable.tagIds,
},
creator: {
username: profilesTable.username,
fullName: profilesTable.fullName,
avatarUrl: profilesTable.avatarUrl,
},
tagNames: tagsTable.name
})
.from(promptsTable)
.where(eq(promptsTable.id, promptId))
.innerJoin(profilesTable, eq(profilesTable.id, promptsTable.profileId))
.innerJoin(tagsTable, inArray(tagsTable.id, promptsTable.tagIds));
const promptData = await drizzleClient
.select({
prompt: {
title: promptsTable.title,
description: promptsTable.description,
tagIds: promptsTable.tagIds,
},
creator: {
username: profilesTable.username,
fullName: profilesTable.fullName,
avatarUrl: profilesTable.avatarUrl,
},
tagNames: tagsTable.name
})
.from(promptsTable)
.where(eq(promptsTable.id, promptId))
.innerJoin(profilesTable, eq(profilesTable.id, promptsTable.profileId))
.innerJoin(tagsTable, inArray(tagsTable.id, promptsTable.tagIds));
That should work, but an even better approach would be to use drizzle's relational queries
king.edwards
king.edwardsOP•15mo ago
I tried this but getting this error:
error: {
name: 'PostgresError',
message: 'syntax error at or near ""prompts""',
stack: 'PostgresError: syntax error at or near ""prompts""\n'
},
error: {
name: 'PostgresError',
message: 'syntax error at or near ""prompts""',
stack: 'PostgresError: syntax error at or near ""prompts""\n'
},
` Using relational queries looks awesome and does look to be better approach but I am getting en error with it as well 😭 but this is due me being new to DB and not knowing how to do things properly this was the error i got
message: 'operator does not exist: uuid = uuid[]',
stack: 'PostgresError: operator does not exist: uuid = uuid[]\n'
message: 'operator does not exist: uuid = uuid[]',
stack: 'PostgresError: operator does not exist: uuid = uuid[]\n'
My schema's
export const promptsTable = pgTable('prompts', {
id: uuid('id').defaultRandom().primaryKey().notNull(),
profileId: uuid('profile_id')
.notNull()
.references(() => profilesTable.id, { onDelete: 'cascade' }),
title: text('title').notNull(),
description: text('description').notNull(),
isFavorited: boolean('is_favorited').default(false).notNull(),
tagIds: uuid('tag_ids').array(),
visibility: promptVisibility('visibility').default('private').notNull(),
createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }).defaultNow().notNull(),
updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }).defaultNow().notNull()
});


export const tagsTable = pgTable(
'tags',
{
id: uuid('id').defaultRandom().primaryKey().notNull(),
profileId: uuid('profile_id')
.notNull()
.references(() => profilesTable.id, { onDelete: 'cascade' }),
name: text('name').notNull(),
createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' })
.defaultNow()
.notNull(),
updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' })
.defaultNow()
.notNull()
},
(table) => {
return {
uniqueUserTagName: unique('unique_user_tag_name').on(table.profileId, table.name)
};
}
);

export const promptsRelations = relations(promptsTable, ({ many }) => ({
tags: many(tagsTable, { fields: [promptsTable.tagIds], references: [tagsTable.id] })
}));

export const tagsRelations = relations(tagsTable, ({ one }) => ({
prompts: one(promptsTable, { fields: [tagsTable.id], references: [promptsTable.tagIds] })
}));
export const promptsTable = pgTable('prompts', {
id: uuid('id').defaultRandom().primaryKey().notNull(),
profileId: uuid('profile_id')
.notNull()
.references(() => profilesTable.id, { onDelete: 'cascade' }),
title: text('title').notNull(),
description: text('description').notNull(),
isFavorited: boolean('is_favorited').default(false).notNull(),
tagIds: uuid('tag_ids').array(),
visibility: promptVisibility('visibility').default('private').notNull(),
createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }).defaultNow().notNull(),
updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }).defaultNow().notNull()
});


export const tagsTable = pgTable(
'tags',
{
id: uuid('id').defaultRandom().primaryKey().notNull(),
profileId: uuid('profile_id')
.notNull()
.references(() => profilesTable.id, { onDelete: 'cascade' }),
name: text('name').notNull(),
createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' })
.defaultNow()
.notNull(),
updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' })
.defaultNow()
.notNull()
},
(table) => {
return {
uniqueUserTagName: unique('unique_user_tag_name').on(table.profileId, table.name)
};
}
);

export const promptsRelations = relations(promptsTable, ({ many }) => ({
tags: many(tagsTable, { fields: [promptsTable.tagIds], references: [tagsTable.id] })
}));

export const tagsRelations = relations(tagsTable, ({ one }) => ({
prompts: one(promptsTable, { fields: [tagsTable.id], references: [promptsTable.tagIds] })
}));
and my query
const promptData = await drizzleClient.query.promptsTable.findFirst({
where: eq(promptsTable.id, promptId),
with: {
tags: true
}
});
const promptData = await drizzleClient.query.promptsTable.findFirst({
where: eq(promptsTable.id, promptId),
with: {
tags: true
}
});
Angelelz
Angelelz•15mo ago
Ok, I see now. Can you log the resulting query as drizzle builds it and copy it here? I think the problem is that your relation Prompts -> Tags is not a traditional SQL relation. Instead, you saved the tagIds as an array in your prompts table Basically throwing the relations benefit out the window Seems like Prompts -> Tags relation is one-to-many The canonical way of defining that relation is by adding a prompt_id foreign key to your tags table, and eliminate that tagIds array from the prompts table
king.edwards
king.edwardsOP•15mo ago
yes here it is have to break it into two parts due to text limit on discord part 1:
Query: select "prompts"."title", "prompts"."description", "prompts"."tag_ids", "profiles"."username", "profiles"."full_name", "profiles"."avatar_url", "tags"."name" from "prompts" inner join "profiles" on "profiles"."id" = "prompts"."profile_id" inner join "tags" on "tags"."id" in "prompts"."tag_ids" where "prompts"."id" = $1 -- params: ["bf3c8f29-7880-4a5d-a57e-ae88ce0f959f"]
Query: select "prompts"."title", "prompts"."description", "prompts"."tag_ids", "profiles"."username", "profiles"."full_name", "profiles"."avatar_url", "tags"."name" from "prompts" inner join "profiles" on "profiles"."id" = "prompts"."profile_id" inner join "tags" on "tags"."id" in "prompts"."tag_ids" where "prompts"."id" = $1 -- params: ["bf3c8f29-7880-4a5d-a57e-ae88ce0f959f"]
Query: select "email", "username", "full_name", "avatar_url" from "profiles" where "profiles"."id" = $1 -- params: ["67732809-e7b0-451c-8f21-7c343b7bf9da"]
Query: select "id", "profile_id", "title", "description", "is_favorited", "tag_ids", "visibility", "created_at", "updated_at" from "prompts" where "prompts"."profile_id" = $1 order by "prompts"."created_at" desc -- params: ["67732809-e7b0-451c-8f21-7c343b7bf9da"]
Query: select "id", "profile_id", "name", "created_at", "updated_at" from "tags" where "tags"."profile_id" = $1 order by "tags"."created_at" desc -- params: ["67732809-e7b0-451c-8f21-7c343b7bf9da"]
Query: select "email", "username", "full_name", "avatar_url" from "profiles" where "profiles"."id" = $1 -- params: ["67732809-e7b0-451c-8f21-7c343b7bf9da"]
Query: select "prompts"."title", "prompts"."description", "prompts"."tag_ids", "profiles"."username", "profiles"."full_name", "profiles"."avatar_url", "tags"."name" from "prompts" inner join "profiles" on "profiles"."id" = "prompts"."profile_id" inner join "tags" on "tags"."id" in "prompts"."tag_ids" where "prompts"."id" = $1 -- params: ["bf3c8f29-7880-4a5d-a57e-ae88ce0f959f"]
Query: select "prompts"."title", "prompts"."description", "prompts"."tag_ids", "profiles"."username", "profiles"."full_name", "profiles"."avatar_url", "tags"."name" from "prompts" inner join "profiles" on "profiles"."id" = "prompts"."profile_id" inner join "tags" on "tags"."id" in "prompts"."tag_ids" where "prompts"."id" = $1 -- params: ["bf3c8f29-7880-4a5d-a57e-ae88ce0f959f"]
Query: select "email", "username", "full_name", "avatar_url" from "profiles" where "profiles"."id" = $1 -- params: ["67732809-e7b0-451c-8f21-7c343b7bf9da"]
Query: select "id", "profile_id", "title", "description", "is_favorited", "tag_ids", "visibility", "created_at", "updated_at" from "prompts" where "prompts"."profile_id" = $1 order by "prompts"."created_at" desc -- params: ["67732809-e7b0-451c-8f21-7c343b7bf9da"]
Query: select "id", "profile_id", "name", "created_at", "updated_at" from "tags" where "tags"."profile_id" = $1 order by "tags"."created_at" desc -- params: ["67732809-e7b0-451c-8f21-7c343b7bf9da"]
Query: select "email", "username", "full_name", "avatar_url" from "profiles" where "profiles"."id" = $1 -- params: ["67732809-e7b0-451c-8f21-7c343b7bf9da"]
part 2:
Query: select "id", "profile_id", "title", "description", "is_favorited", "tag_ids", "visibility", "created_at", "updated_at" from "prompts" where "prompts"."profile_id" = $1 order by "prompts"."created_at" desc -- params: ["67732809-e7b0-451c-8f21-7c343b7bf9da"]
Query: select "id", "profile_id", "name", "created_at", "updated_at" from "tags" where "tags"."profile_id" = $1 order by "tags"."created_at" desc -- params: ["67732809-e7b0-451c-8f21-7c343b7bf9da"]
Query: select "id", "profile_id", "title", "description", "is_favorited", "tag_ids", "visibility", "created_at", "updated_at" from "prompts" where "prompts"."profile_id" = $1 order by "prompts"."created_at" desc -- params: ["67732809-e7b0-451c-8f21-7c343b7bf9da"]
Query: select "id", "profile_id", "name", "created_at", "updated_at" from "tags" where "tags"."profile_id" = $1 order by "tags"."created_at" desc -- params: ["67732809-e7b0-451c-8f21-7c343b7bf9da"]
ah ok so i am guessing i would have to do a many-to-many relation if I want a prompt to be able to have multiple tags, and tag to be able to be assigned to multiple prompts?
Angelelz
Angelelz•15mo ago
Well, that is the canonical relational way of doing it, I don't know if you have an architectural/business reason for the way you did it
king.edwards
king.edwardsOP•15mo ago
I did it that way partly due to my lack of experiences with DB's and so that user can be able to assign as many tags to a prompt as they want from the list of tags they created how would i go about doing this without keeping track of the tags currently assigned to a prompt without an array?
Angelelz
Angelelz•15mo ago
You need to create a intermediate table for your many to many relation to work at db level
Angelelz
Angelelz•15mo ago
king.edwards
king.edwardsOP•15mo ago
ah ok i'll try that out
Angelelz
Angelelz•15mo ago
There's also plenty or resources as it's a fairly common concept in relational databases
Dane
Dane•15mo ago
@king.edwards Each tag would be added as it's own row in the tags table instead of being in an array in a single row. prompt -> one-to-many -> tags If you wanted a standardized list of tags and then linking those to a prompt you would want: prompt <- one-to-one tagsAssignedToPrompt one-to-one -> tags tagsAssignedToPrompt would be an intermediate table containing the fields promptId and tagId. You would have one row for each promptId+tagId connection I have a similar setup linking products available to a program through an intermediate programProductLink table:
Dane
Dane•15mo ago
No description
king.edwards
king.edwardsOP•15mo ago
ok so say if <prompt1> has <tag1> <prompt2> has <tag1, tag2> <prompt3> has <tag1, tag7> that would mean 3 rows is created in the intermediate promptTagLink table?
Dane
Dane•15mo ago
That would be 5 total rows:
No description
king.edwards
king.edwardsOP•15mo ago
ah i see, thank you for taking the time to help and also make that visual ! that would mean i would take more more space in the DB, would that be a problem if each year has about 100 rows in their promptTagLink table? also if a tag get's deleted, i imagine that i would be able to delete all the rows that had it in the promptTagLink table with a trigger function?
Dane
Dane•15mo ago
Are you using MySQL?
king.edwards
king.edwardsOP•15mo ago
posgres
Dane
Dane•15mo ago
I am more familiar with MySQL, an "on delete cascade" in MySQL would accomplish that I imagine the same is possible with postgres the promptTagLink.tagId would be a foreign key of tag.id , so deleting the tag would trigger a cascade event deleting any promptTagLink rows with the same tag id You shouldn't need a trigger to accomplish that looks like it's called DROP CASCADE in postgres
king.edwards
king.edwardsOP•15mo ago
ok great, cause i was using a trigger for my current method of storing the tagIds in array, but casdaing makes that part simpler
Dane
Dane•15mo ago
the docs on drizzle for cascade events aren't really there... in mysql the drizzle schema is this, i would imagine the same for postgres:
productId: int("productId").references(() => products.id, {
onDelete: "cascade",
onUpdate: "cascade",
})
productId: int("productId").references(() => products.id, {
onDelete: "cascade",
onUpdate: "cascade",
})
king.edwards
king.edwardsOP•15mo ago
sweet i'll give that a shoot @angelelz @dane.stevens And thank you both for taking the time to help, much appreciated!! I am going to apply these lessons and work on getting the new structure with the intermediate table in place forgot to give an update, was able to successfully refactor to using a many-to-many model for any one who had a similar question to this post here is the new tagPromptLinkTable and it's relations
export const tagPromptLinkTable = pgTable(
'tag_prompt_link_table',
{
promptId: uuid('prompt_id')
.notNull()
.references(() => promptsTable.id, { onDelete: 'cascade' }),
tagId: uuid('tag_id')
.notNull()
.references(() => tagsTable.id, { onDelete: 'cascade' }),
createdBy: uuid('created_by')
.notNull()
.references(() => profilesTable.id, { onDelete: 'cascade' }),
createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' })
.defaultNow()
.notNull()
},
(table) => {
return {
tagPromptLinkTablePkey: primaryKey(table.promptId, table.tagId, table.createdBy)
};
}
);

export const profilesRelations = relations(profilesTable, ({ many }) => ({
createdTagPromptLinks: many(tagPromptLinkTable)
}));

export const promptsRelations = relations(promptsTable, ({ one, many }) => ({
profile: one(profilesTable, {
fields: [promptsTable.profileId],
references: [profilesTable.id]
}),

tagPromptLink: many(tagPromptLinkTable)
}));

export const tagsRelations = relations(tagsTable, ({ one, many }) => ({
profile: one(profilesTable, {
fields: [tagsTable.profileId],
references: [profilesTable.id]
}),

tagPromptLink: many(tagPromptLinkTable)
}));

export const tagPromptLinkTableRelations = relations(tagPromptLinkTable, ({ one }) => ({
profile: one(profilesTable, {
fields: [tagPromptLinkTable.createdBy],
references: [profilesTable.id]
}),

prompt: one(promptsTable, {
fields: [tagPromptLinkTable.promptId],
references: [promptsTable.id]
}),

tag: one(tagsTable, {
fields: [tagPromptLinkTable.tagId],
references: [tagsTable.id]
})
}));
export const tagPromptLinkTable = pgTable(
'tag_prompt_link_table',
{
promptId: uuid('prompt_id')
.notNull()
.references(() => promptsTable.id, { onDelete: 'cascade' }),
tagId: uuid('tag_id')
.notNull()
.references(() => tagsTable.id, { onDelete: 'cascade' }),
createdBy: uuid('created_by')
.notNull()
.references(() => profilesTable.id, { onDelete: 'cascade' }),
createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' })
.defaultNow()
.notNull()
},
(table) => {
return {
tagPromptLinkTablePkey: primaryKey(table.promptId, table.tagId, table.createdBy)
};
}
);

export const profilesRelations = relations(profilesTable, ({ many }) => ({
createdTagPromptLinks: many(tagPromptLinkTable)
}));

export const promptsRelations = relations(promptsTable, ({ one, many }) => ({
profile: one(profilesTable, {
fields: [promptsTable.profileId],
references: [profilesTable.id]
}),

tagPromptLink: many(tagPromptLinkTable)
}));

export const tagsRelations = relations(tagsTable, ({ one, many }) => ({
profile: one(profilesTable, {
fields: [tagsTable.profileId],
references: [profilesTable.id]
}),

tagPromptLink: many(tagPromptLinkTable)
}));

export const tagPromptLinkTableRelations = relations(tagPromptLinkTable, ({ one }) => ({
profile: one(profilesTable, {
fields: [tagPromptLinkTable.createdBy],
references: [profilesTable.id]
}),

prompt: one(promptsTable, {
fields: [tagPromptLinkTable.promptId],
references: [promptsTable.id]
}),

tag: one(tagsTable, {
fields: [tagPromptLinkTable.tagId],
references: [tagsTable.id]
})
}));
and the update query:
const promptData = await drizzleClient.query.promptsTable.findFirst({
where: eq(promptsTable.id, promptId),

columns: {
title: true,
description: true
},
with: {
profile: {
columns: {
username: true,
fullName: true,
avatarUrl: true
}
},
tagPromptLink: {
with: {
tag: true
}
}
}
});
const promptData = await drizzleClient.query.promptsTable.findFirst({
where: eq(promptsTable.id, promptId),

columns: {
title: true,
description: true
},
with: {
profile: {
columns: {
username: true,
fullName: true,
avatarUrl: true
}
},
tagPromptLink: {
with: {
tag: true
}
}
}
});
Angelelz
Angelelz•15mo ago
Excellent, good job!
king.edwards
king.edwardsOP•15mo ago
Thank you!
cumironin
cumironin•14mo ago
can we use inArray or in on manual query for deleted row ? case
pickDeleteUsers: async ({ url }) => {
const id = url.searchParams.get('id');
const urlId = String(id);
await db.delete(userTable).where(inArray(userTable.id, [urlId]));
}
pickDeleteUsers: async ({ url }) => {
const id = url.searchParams.get('id');
const urlId = String(id);
await db.delete(userTable).where(inArray(userTable.id, [urlId]));
}
urlId is an array uuid of string. This will reproduce error: invalid input syntax for type uuid, btw i use sveltekit form & checkbox to do so.
Want results from more Discord servers?
Add your server