diff --git a/models/issues/issue_user.go b/models/issues/issue_user.go
index 24bb74648..6b59e0725 100644
--- a/models/issues/issue_user.go
+++ b/models/issues/issue_user.go
@@ -14,8 +14,8 @@ import (
 // IssueUser represents an issue-user relation.
 type IssueUser struct {
 	ID          int64 `xorm:"pk autoincr"`
-	UID         int64 `xorm:"INDEX"` // User ID.
-	IssueID     int64 `xorm:"INDEX"`
+	UID         int64 `xorm:"INDEX unique(uid_to_issue)"` // User ID.
+	IssueID     int64 `xorm:"INDEX unique(uid_to_issue)"`
 	IsRead      bool
 	IsMentioned bool
 }
diff --git a/models/migrations/fixtures/Test_AddCombinedIndexToIssueUser/issue_user.yml b/models/migrations/fixtures/Test_AddCombinedIndexToIssueUser/issue_user.yml
new file mode 100644
index 000000000..7bbb6f2f3
--- /dev/null
+++ b/models/migrations/fixtures/Test_AddCombinedIndexToIssueUser/issue_user.yml
@@ -0,0 +1,20 @@
+-
+  id: 1
+  uid: 1
+  issue_id: 1
+  is_read: true
+  is_mentioned: false
+
+-
+  id: 2
+  uid: 2
+  issue_id: 1
+  is_read: true
+  is_mentioned: false
+
+-
+  id: 3
+  uid: 2
+  issue_id: 1 # duplicated with id 2
+  is_read: false
+  is_mentioned: true
diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go
index 28e3be503..578cbca03 100644
--- a/models/migrations/migrations.go
+++ b/models/migrations/migrations.go
@@ -550,6 +550,8 @@ var migrations = []Migration{
 	NewMigration("Add auth_token table", v1_22.CreateAuthTokenTable),
 	// v282 -> v283
 	NewMigration("Add Index to pull_auto_merge.doer_id", v1_22.AddIndexToPullAutoMergeDoerID),
+	// v283 -> v284
+	NewMigration("Add combined Index to issue_user.uid and issue_id", v1_22.AddCombinedIndexToIssueUser),
 }
 
 // GetCurrentDBVersion returns the current db version
diff --git a/models/migrations/v1_22/main_test.go b/models/migrations/v1_22/main_test.go
new file mode 100644
index 000000000..efd8dbaa8
--- /dev/null
+++ b/models/migrations/v1_22/main_test.go
@@ -0,0 +1,14 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package v1_22 //nolint
+
+import (
+	"testing"
+
+	"code.gitea.io/gitea/models/migrations/base"
+)
+
+func TestMain(m *testing.M) {
+	base.MainTest(m)
+}
diff --git a/models/migrations/v1_22/v283.go b/models/migrations/v1_22/v283.go
new file mode 100644
index 000000000..b2b94845d
--- /dev/null
+++ b/models/migrations/v1_22/v283.go
@@ -0,0 +1,34 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package v1_22 //nolint
+
+import (
+	"xorm.io/xorm"
+)
+
+func AddCombinedIndexToIssueUser(x *xorm.Engine) error {
+	type OldIssueUser struct {
+		IssueID int64
+		UID     int64
+		Cnt     int64
+	}
+
+	var duplicatedIssueUsers []OldIssueUser
+	if err := x.SQL("select * from (select issue_id, uid, count(1) as cnt from issue_user group by issue_id, uid) a where a.cnt > 1").
+		Find(&duplicatedIssueUsers); err != nil {
+		return err
+	}
+	for _, issueUser := range duplicatedIssueUsers {
+		if _, err := x.Exec("delete from issue_user where id in (SELECT id FROM issue_user WHERE issue_id = ? and uid = ? limit ?)", issueUser.IssueID, issueUser.UID, issueUser.Cnt-1); err != nil {
+			return err
+		}
+	}
+
+	type IssueUser struct {
+		UID     int64 `xorm:"INDEX unique(uid_to_issue)"` // User ID.
+		IssueID int64 `xorm:"INDEX unique(uid_to_issue)"`
+	}
+
+	return x.Sync(&IssueUser{})
+}
diff --git a/models/migrations/v1_22/v283_test.go b/models/migrations/v1_22/v283_test.go
new file mode 100644
index 000000000..864f47f84
--- /dev/null
+++ b/models/migrations/v1_22/v283_test.go
@@ -0,0 +1,28 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package v1_22 //nolint
+
+import (
+	"testing"
+
+	"code.gitea.io/gitea/models/migrations/base"
+)
+
+func Test_AddCombinedIndexToIssueUser(t *testing.T) {
+	type IssueUser struct {
+		UID     int64 `xorm:"INDEX unique(uid_to_issue)"` // User ID.
+		IssueID int64 `xorm:"INDEX unique(uid_to_issue)"`
+	}
+
+	// Prepare and load the testing database
+	x, deferable := base.PrepareTestEnv(t, 0, new(IssueUser))
+	defer deferable()
+	if x == nil || t.Failed() {
+		return
+	}
+
+	if err := AddCombinedIndexToIssueUser(x); err != nil {
+		t.Fatal(err)
+	}
+}