diff --git a/api/src/main/java/com/databasir/api/DocumentDescriptionController.java b/api/src/main/java/com/databasir/api/DocumentDescriptionController.java
new file mode 100644
index 0000000..6bfc88f
--- /dev/null
+++ b/api/src/main/java/com/databasir/api/DocumentDescriptionController.java
@@ -0,0 +1,41 @@
+package com.databasir.api;
+
+import com.databasir.api.config.security.DatabasirUserDetails;
+import com.databasir.common.JsonData;
+import com.databasir.core.domain.description.data.DocumentDescriptionSaveRequest;
+import com.databasir.core.domain.description.service.DocumentDescriptionService;
+import com.databasir.core.domain.log.annotation.Operation;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.validation.Valid;
+
+@RestController
+@Validated
+@RequiredArgsConstructor
+public class DocumentDescriptionController {
+
+    private final DocumentDescriptionService documentDescriptionService;
+
+    @PostMapping(Routes.DocumentDescription.SAVE)
+    @PreAuthorize("hasAnyAuthority('SYS_OWNER', 'GROUP_OWNER?groupId='+#groupId, 'GROUP_MEMBER?groupId='+#groupId)")
+    @Operation(module = Operation.Modules.PROJECT,
+            name = "更新描述",
+            involvedProjectId = "#projectId")
+    public JsonData<Void> save(@PathVariable Integer groupId,
+                               @PathVariable Integer projectId,
+                               @RequestBody @Valid DocumentDescriptionSaveRequest request) {
+        DatabasirUserDetails principal = (DatabasirUserDetails) SecurityContextHolder.getContext()
+                .getAuthentication()
+                .getPrincipal();
+        Integer userId = principal.getUserPojo().getId();
+        documentDescriptionService.save(groupId, projectId, userId, request);
+        return JsonData.ok();
+    }
+}
diff --git a/api/src/main/java/com/databasir/api/Routes.java b/api/src/main/java/com/databasir/api/Routes.java
index 2f63a79..c33819c 100644
--- a/api/src/main/java/com/databasir/api/Routes.java
+++ b/api/src/main/java/com/databasir/api/Routes.java
@@ -96,6 +96,13 @@ public interface Routes {
         String DELETE = DISCUSSION_BASE + "/{discussionId}";
     }
 
+    interface DocumentDescription {
+
+        String DISCUSSION_BASE = BASE + "/groups/{groupId}/projects/{projectId}/descriptions";
+
+        String SAVE = DISCUSSION_BASE;
+    }
+
     interface Setting {
 
         String GET_SYS_EMAIL = BASE + "/settings/sys_email";
diff --git a/core/src/main/java/com/databasir/core/domain/description/converter/DocumentDescriptionPojoConverter.java b/core/src/main/java/com/databasir/core/domain/description/converter/DocumentDescriptionPojoConverter.java
new file mode 100644
index 0000000..e6105f2
--- /dev/null
+++ b/core/src/main/java/com/databasir/core/domain/description/converter/DocumentDescriptionPojoConverter.java
@@ -0,0 +1,17 @@
+package com.databasir.core.domain.description.converter;
+
+import com.databasir.core.domain.description.data.DocumentDescriptionSaveRequest;
+import com.databasir.dao.tables.pojos.DocumentDescriptionPojo;
+import org.mapstruct.Mapper;
+import org.mapstruct.Mapping;
+
+@Mapper(componentModel = "spring")
+public interface DocumentDescriptionPojoConverter {
+
+    @Mapping(target = "id", ignore = true)
+    @Mapping(target = "createAt", ignore = true)
+    @Mapping(target = "updateAt", ignore = true)
+    DocumentDescriptionPojo of(Integer projectId,
+                               Integer updateBy,
+                               DocumentDescriptionSaveRequest request);
+}
diff --git a/core/src/main/java/com/databasir/core/domain/description/data/DocumentDescriptionSaveRequest.java b/core/src/main/java/com/databasir/core/domain/description/data/DocumentDescriptionSaveRequest.java
new file mode 100644
index 0000000..9eeb6e0
--- /dev/null
+++ b/core/src/main/java/com/databasir/core/domain/description/data/DocumentDescriptionSaveRequest.java
@@ -0,0 +1,14 @@
+package com.databasir.core.domain.description.data;
+
+import lombok.Data;
+
+@Data
+public class DocumentDescriptionSaveRequest {
+
+    private String tableName;
+
+    private String columnName;
+
+    private String content;
+
+}
diff --git a/core/src/main/java/com/databasir/core/domain/description/service/DocumentDescriptionService.java b/core/src/main/java/com/databasir/core/domain/description/service/DocumentDescriptionService.java
new file mode 100644
index 0000000..661ed8f
--- /dev/null
+++ b/core/src/main/java/com/databasir/core/domain/description/service/DocumentDescriptionService.java
@@ -0,0 +1,32 @@
+package com.databasir.core.domain.description.service;
+
+import com.databasir.core.domain.description.converter.DocumentDescriptionPojoConverter;
+import com.databasir.core.domain.description.data.DocumentDescriptionSaveRequest;
+import com.databasir.dao.impl.DocumentDescriptionDao;
+import com.databasir.dao.tables.pojos.DocumentDescriptionPojo;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+@RequiredArgsConstructor
+public class DocumentDescriptionService {
+
+    private final DocumentDescriptionDao documentDescriptionDao;
+
+    private final DocumentDescriptionPojoConverter documentDescriptionPojoConverter;
+
+    @Transactional
+    public void save(Integer groupId,
+                     Integer projectId,
+                     Integer userId,
+                     DocumentDescriptionSaveRequest request) {
+
+        DocumentDescriptionPojo pojo = documentDescriptionPojoConverter.of(projectId, userId, request);
+        if (!documentDescriptionDao.exists(projectId, request.getTableName(), request.getColumnName())) {
+            documentDescriptionDao.insertAndReturnId(pojo);
+        } else {
+            documentDescriptionDao.update(pojo);
+        }
+    }
+}
diff --git a/core/src/main/java/com/databasir/core/domain/document/converter/DocumentResponseConverter.java b/core/src/main/java/com/databasir/core/domain/document/converter/DocumentResponseConverter.java
index 9d70386..f0f3557 100644
--- a/core/src/main/java/com/databasir/core/domain/document/converter/DocumentResponseConverter.java
+++ b/core/src/main/java/com/databasir/core/domain/document/converter/DocumentResponseConverter.java
@@ -28,21 +28,25 @@ public interface DocumentResponseConverter {
     @SuppressWarnings("checkstyle:all")
     DatabaseDocumentResponse.TableDocumentResponse of(TableDocumentPojo tableDocument,
                                                       Integer discussionCount,
+                                                      String description,
                                                       List<DatabaseDocumentResponse.TableDocumentResponse.ColumnDocumentResponse> columns,
                                                       List<TableIndexDocumentPojo> indexes,
                                                       List<TableTriggerDocumentPojo> triggers);
 
     DatabaseDocumentResponse.TableDocumentResponse.ColumnDocumentResponse of(TableColumnDocumentPojo pojo,
-                                                                             Integer discussionCount);
+                                                                             Integer discussionCount,
+                                                                             String description);
 
     default List<DatabaseDocumentResponse.TableDocumentResponse.ColumnDocumentResponse> of(
             List<TableColumnDocumentPojo> columns,
             String tableName,
-            Map<String, Integer> discussionCountMapByJoinName) {
+            Map<String, Integer> discussionCountMapByJoinName,
+            Map<String, String> descriptionMapByJoinName) {
         return columns.stream()
                 .map(column -> {
                     Integer count = discussionCountMapByJoinName.get(tableName + "." + column.getName());
-                    return of(column, count);
+                    String description = descriptionMapByJoinName.get(tableName + "." + column.getName());
+                    return of(column, count, description);
                 })
                 .collect(Collectors.toList());
     }
diff --git a/core/src/main/java/com/databasir/core/domain/document/converter/DocumentSimpleResponseConverter.java b/core/src/main/java/com/databasir/core/domain/document/converter/DocumentSimpleResponseConverter.java
index cc29ed4..8abf891 100644
--- a/core/src/main/java/com/databasir/core/domain/document/converter/DocumentSimpleResponseConverter.java
+++ b/core/src/main/java/com/databasir/core/domain/document/converter/DocumentSimpleResponseConverter.java
@@ -21,14 +21,18 @@ public interface DocumentSimpleResponseConverter {
     DatabaseDocumentSimpleResponse of(DatabaseDocumentPojo databaseDocument,
                                       List<DatabaseDocumentSimpleResponse.TableData> tables);
 
-    DatabaseDocumentSimpleResponse.TableData of(TableDocumentPojo tables, Integer discussionCount);
+    DatabaseDocumentSimpleResponse.TableData of(TableDocumentPojo tables,
+                                                Integer discussionCount,
+                                                String description);
 
     default List<DatabaseDocumentSimpleResponse.TableData> of(List<TableDocumentPojo> tables,
-                                                              Map<String, Integer> discussionCountMapByTableName) {
+                                                              Map<String, Integer> discussionCountMapByTableName,
+                                                              Map<String, String> descriptionMapByTableName) {
         return tables.stream()
                 .map(table -> {
                     Integer count = discussionCountMapByTableName.get(table.getName());
-                    return of(table, count);
+                    String description = descriptionMapByTableName.get(table.getName());
+                    return of(table, count, description);
                 })
                 .collect(Collectors.toList());
     }
diff --git a/core/src/main/java/com/databasir/core/domain/document/data/DatabaseDocumentResponse.java b/core/src/main/java/com/databasir/core/domain/document/data/DatabaseDocumentResponse.java
index a0dcda6..e3cebe0 100644
--- a/core/src/main/java/com/databasir/core/domain/document/data/DatabaseDocumentResponse.java
+++ b/core/src/main/java/com/databasir/core/domain/document/data/DatabaseDocumentResponse.java
@@ -48,6 +48,8 @@ public class DatabaseDocumentResponse {
 
         private Integer discussionCount;
 
+        private String description;
+
         @Builder.Default
         private List<ColumnDocumentResponse> columns = new ArrayList<>();
 
@@ -76,6 +78,8 @@ public class DatabaseDocumentResponse {
 
             private String comment;
 
+            private String description;
+
             private Boolean isPrimaryKey;
 
             private String nullable;
diff --git a/core/src/main/java/com/databasir/core/domain/document/data/DatabaseDocumentSimpleResponse.java b/core/src/main/java/com/databasir/core/domain/document/data/DatabaseDocumentSimpleResponse.java
index 1be8388..38e7bfe 100644
--- a/core/src/main/java/com/databasir/core/domain/document/data/DatabaseDocumentSimpleResponse.java
+++ b/core/src/main/java/com/databasir/core/domain/document/data/DatabaseDocumentSimpleResponse.java
@@ -37,5 +37,7 @@ public class DatabaseDocumentSimpleResponse {
         private String comment;
 
         private Integer discussionCount;
+
+        private String description;
     }
 }
diff --git a/core/src/main/java/com/databasir/core/domain/document/service/DocumentService.java b/core/src/main/java/com/databasir/core/domain/document/service/DocumentService.java
index 544c689..a44e7df 100644
--- a/core/src/main/java/com/databasir/core/domain/document/service/DocumentService.java
+++ b/core/src/main/java/com/databasir/core/domain/document/service/DocumentService.java
@@ -59,6 +59,8 @@ public class DocumentService {
 
     private final DocumentDiscussionDao documentDiscussionDao;
 
+    private final DocumentDescriptionDao documentDescriptionDao;
+
     private final DocumentPojoConverter documentPojoConverter;
 
     private final DocumentResponseConverter documentResponseConverter;
@@ -146,7 +148,15 @@ public class DocumentService {
                     documentDiscussionDao.selectTableDiscussionCount(projectId)
                             .stream()
                             .collect(Collectors.toMap(d -> d.getTableName(), d -> d.getCount(), (a, b) -> a));
-            var tableMetas = documentSimpleResponseConverter.of(tables, discussionCountMapByTableName);
+            Map<String, String> descriptionMapByTableName =
+                    documentDescriptionDao.selectTableDescriptionByProjectId(projectId)
+                            .stream()
+                            .collect(Collectors.toMap(d -> d.getTableName(), d -> d.getContent(), (a, b) -> a));
+            var tableMetas = documentSimpleResponseConverter.of(
+                    tables,
+                    discussionCountMapByTableName,
+                    descriptionMapByTableName
+            );
             return documentSimpleResponseConverter.of(document, tableMetas);
         });
     }
@@ -206,19 +216,26 @@ public class DocumentService {
         }
         var tables =
                 tableDocumentDao.selectByDatabaseDocumentIdAndIdIn(databaseDocumentId, tableIds);
+        // column
         var columns =
                 tableColumnDocumentDao.selectByDatabaseDocumentIdAndTableIdIn(databaseDocumentId, tableIds);
-        var indexes =
-                tableIndexDocumentDao.selectByDatabaseDocumentIdAndIdIn(databaseDocumentId, tableIds);
-        var triggers =
-                tableTriggerDocumentDao.selectByDatabaseDocumentIdAndIdIn(databaseDocumentId, tableIds);
-        var discussions = documentDiscussionDao.selectAllDiscussionCount(projectId);
         Map<Integer, List<TableColumnDocumentPojo>> columnsGroupByTableMetaId = columns.stream()
                 .collect(Collectors.groupingBy(TableColumnDocumentPojo::getTableDocumentId));
+
+        // index
+        var indexes =
+                tableIndexDocumentDao.selectByDatabaseDocumentIdAndIdIn(databaseDocumentId, tableIds);
         Map<Integer, List<TableIndexDocumentPojo>> indexesGroupByTableMetaId = indexes.stream()
                 .collect(Collectors.groupingBy(TableIndexDocumentPojo::getTableDocumentId));
+
+        // trigger
+        var triggers =
+                tableTriggerDocumentDao.selectByDatabaseDocumentIdAndIdIn(databaseDocumentId, tableIds);
         Map<Integer, List<TableTriggerDocumentPojo>> triggersGroupByTableMetaId = triggers.stream()
                 .collect(Collectors.groupingBy(TableTriggerDocumentPojo::getTableDocumentId));
+
+        // discussion
+        var discussions = documentDiscussionDao.selectAllDiscussionCount(projectId);
         Map<String, Integer> discussionCountMapByJoinName = discussions.stream()
                 .collect(Collectors.toMap(
                         d -> String.join(".",
@@ -226,6 +243,17 @@ public class DocumentService {
                                 StringUtils.defaultIfBlank(d.getColumnName(), "")),
                         DocumentDiscussionCountPojo::getCount,
                         (a, b) -> a));
+
+        // description
+        var descriptions = documentDescriptionDao.selectByProjectId(projectId);
+        Map<String, String> descriptionMapByJoinName = descriptions.stream()
+                .collect(Collectors.toMap(
+                        d -> String.join(".",
+                                d.getTableName(),
+                                StringUtils.defaultIfBlank(d.getColumnName(), "")),
+                        DocumentDescriptionPojo::getContent,
+                        (a, b) -> a));
+
         return tables.stream()
                 .map(table -> {
                     Integer tableId = table.getId();
@@ -233,9 +261,14 @@ public class DocumentService {
                     var subIndexes = indexesGroupByTableMetaId.getOrDefault(tableId, Collections.emptyList());
                     var subTriggers = triggersGroupByTableMetaId.getOrDefault(tableId, Collections.emptyList());
                     var discussionCount = discussionCountMapByJoinName.get(table.getName());
+                    var description= descriptionMapByJoinName.get(table.getName());
                     var columnResponses =
-                            documentResponseConverter.of(subColumns, table.getName(), discussionCountMapByJoinName);
-                    return documentResponseConverter.of(table, discussionCount, columnResponses, subIndexes,
+                            documentResponseConverter.of(
+                                    subColumns,
+                                    table.getName(),
+                                    discussionCountMapByJoinName,
+                                    descriptionMapByJoinName);
+                    return documentResponseConverter.of(table, discussionCount,description, columnResponses, subIndexes,
                             subTriggers);
                 })
                 .collect(Collectors.toList());
diff --git a/dao/src/main/java/com/databasir/dao/impl/DocumentDescriptionDao.java b/dao/src/main/java/com/databasir/dao/impl/DocumentDescriptionDao.java
new file mode 100644
index 0000000..aa95280
--- /dev/null
+++ b/dao/src/main/java/com/databasir/dao/impl/DocumentDescriptionDao.java
@@ -0,0 +1,62 @@
+package com.databasir.dao.impl;
+
+import com.databasir.dao.tables.pojos.DocumentDescriptionPojo;
+import com.databasir.dao.tables.records.DocumentDescriptionRecord;
+import lombok.Getter;
+import org.jooq.Condition;
+import org.jooq.DSLContext;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+
+import static com.databasir.dao.Tables.DOCUMENT_DESCRIPTION;
+
+@Repository
+public class DocumentDescriptionDao extends BaseDao<DocumentDescriptionPojo> {
+
+    @Autowired
+    @Getter
+    private DSLContext dslContext;
+
+    public DocumentDescriptionDao() {
+        super(DOCUMENT_DESCRIPTION, DocumentDescriptionPojo.class);
+    }
+
+    public boolean exists(Integer projectId, String tableName, String columnName) {
+        Condition condition = DOCUMENT_DESCRIPTION.PROJECT_ID.eq(projectId)
+                .and(DOCUMENT_DESCRIPTION.TABLE_NAME.eq(tableName));
+        if (columnName == null) {
+            condition = condition.and(DOCUMENT_DESCRIPTION.COLUMN_NAME.isNull());
+        } else {
+            condition = condition.and(DOCUMENT_DESCRIPTION.COLUMN_NAME.eq(columnName));
+        }
+        return this.exists(condition);
+    }
+
+    public void update(DocumentDescriptionPojo pojo) {
+        Condition condition = DOCUMENT_DESCRIPTION.TABLE_NAME.eq(pojo.getTableName());
+        if (pojo.getColumnName() == null) {
+            condition = condition.and(DOCUMENT_DESCRIPTION.COLUMN_NAME.isNull());
+        } else {
+            condition = condition.and(DOCUMENT_DESCRIPTION.COLUMN_NAME.eq(pojo.getColumnName()));
+        }
+        DocumentDescriptionRecord record = getDslContext().newRecord(DOCUMENT_DESCRIPTION, pojo);
+        getDslContext().executeUpdate(record, condition);
+    }
+
+    public List<DocumentDescriptionPojo> selectByProjectId(Integer projectId) {
+        return selectByCondition(DOCUMENT_DESCRIPTION.PROJECT_ID.eq(projectId));
+    }
+
+    public List<DocumentDescriptionPojo> selectTableDescriptionByProjectId(Integer projectId) {
+        return selectByCondition(DOCUMENT_DESCRIPTION.PROJECT_ID.eq(projectId)
+                .and(DOCUMENT_DESCRIPTION.COLUMN_NAME.isNull()));
+    }
+
+    public List<DocumentDescriptionPojo> selectByCondition(Condition condition) {
+        return this.getDslContext()
+                .selectFrom(DOCUMENT_DESCRIPTION).where(condition)
+                .fetchInto(DocumentDescriptionPojo.class);
+    }
+}