279 lines
6.4 KiB
Markdown
279 lines
6.4 KiB
Markdown
# Supabase 查询数据数量的方法
|
||
|
||
## 概述
|
||
|
||
在 Supabase 中,有多种方法可以查询符合条件的数据数量。本文档将介绍各种方法的用法和适用场景。
|
||
|
||
## 方法一:使用 count + head 选项(推荐)
|
||
|
||
这是 Supabase 官方推荐的方法,使用 HEAD 请求只获取数量信息,不返回实际数据。
|
||
|
||
### JavaScript SDK 用法
|
||
```javascript
|
||
const { count, error } = await supabase
|
||
.from('table')
|
||
.select('*', { count: 'exact', head: true })
|
||
.eq('status', 'active')
|
||
```
|
||
|
||
### 我们的 UTS 实现
|
||
```typescript
|
||
const result = await supa
|
||
.from('ak_assignments')
|
||
.select('*', { count: 'exact', head: true })
|
||
.eq('teacher_id', userId)
|
||
.execute()
|
||
|
||
const count = result.count // 从 count 属性获取数量
|
||
```
|
||
|
||
### 特点
|
||
- ✅ **性能最佳**:使用 HEAD 请求,不传输数据
|
||
- ✅ **网络开销小**:只返回 count,不返回行数据
|
||
- ✅ **官方推荐**:符合 Supabase 最佳实践
|
||
- ✅ **类型安全**:返回 `{ count: number }`
|
||
|
||
## 方法二:使用 count 选项(获取数据+数量)
|
||
|
||
当你既需要数据又需要数量时使用此方法。
|
||
|
||
```typescript
|
||
const result = await supa
|
||
.from('ak_assignments')
|
||
.select('*', { count: 'exact' })
|
||
.eq('teacher_id', userId)
|
||
.limit(10)
|
||
.execute()
|
||
|
||
const data = result.data // 实际数据
|
||
const total = result.total // 总数量
|
||
```
|
||
|
||
### 特点
|
||
- ✅ **一次请求**:同时获取数据和总数
|
||
- ✅ **适合分页**:支持 limit/offset 分页
|
||
- ❌ **网络开销大**:返回完整数据
|
||
|
||
## 方法三:聚合函数(PostgREST)
|
||
|
||
使用 PostgreSQL 聚合函数直接在数据库层面计算。
|
||
|
||
```typescript
|
||
// 注意:这个语法在当前版本可能不完全支持
|
||
const result = await supa
|
||
.from('ak_assignments')
|
||
.select('id.count()')
|
||
.eq('teacher_id', userId)
|
||
.execute()
|
||
|
||
const count = result.data[0]?.count
|
||
```
|
||
|
||
### 特点
|
||
- ✅ **灵活性高**:支持复杂聚合
|
||
- ❌ **语法复杂**:需要了解 PostgREST 语法
|
||
- ❌ **兼容性**:部分客户端不完全支持
|
||
|
||
## Count 选项说明
|
||
|
||
### exact(精确计数)
|
||
```typescript
|
||
{ count: 'exact' }
|
||
```
|
||
- 执行 `COUNT(*)` 查询
|
||
- 结果完全准确
|
||
- 性能开销较大(大表)
|
||
|
||
### estimated(估算计数)
|
||
```typescript
|
||
{ count: 'estimated' }
|
||
```
|
||
- 使用 PostgreSQL 统计信息估算
|
||
- 结果是近似值
|
||
- 性能开销很小
|
||
|
||
### planned(预估计数)
|
||
```typescript
|
||
{ count: 'planned' }
|
||
```
|
||
- 使用查询计划器估算
|
||
- 结果是预估值
|
||
- 性能开销最小
|
||
|
||
## Head 模式详解
|
||
|
||
### HTTP 方法差异
|
||
|
||
**普通请求(GET):**
|
||
```
|
||
GET /rest/v1/table?select=*&count=exact
|
||
```
|
||
- 返回数据 + count
|
||
- 传输完整行数据
|
||
|
||
**Head 请求(HEAD):**
|
||
```
|
||
HEAD /rest/v1/table?select=*&count=exact
|
||
```
|
||
- 只返回 headers(包含 count)
|
||
- 不传输行数据
|
||
|
||
### 响应格式对比
|
||
|
||
**普通模式:**
|
||
```typescript
|
||
{
|
||
data: [...], // 实际数据
|
||
count: 42, // 总数
|
||
total: 42,
|
||
status: 200
|
||
}
|
||
```
|
||
|
||
**Head 模式:**
|
||
```typescript
|
||
{
|
||
data: null, // head 模式不返回数据
|
||
count: 42, // 从 headers 解析的总数
|
||
total: 42,
|
||
status: 200
|
||
}
|
||
```
|
||
|
||
## 实际应用示例
|
||
|
||
### 教师仪表板统计
|
||
|
||
```typescript
|
||
const loadTeacherStats = async (teacherId: string) => {
|
||
try {
|
||
// 并行执行多个 count 查询
|
||
const [totalAssignments, completedAssignments, pendingReview] = await Promise.all([
|
||
// 总作业数
|
||
supa.from('ak_assignments')
|
||
.select('*', { count: 'exact', head: true })
|
||
.eq('teacher_id', teacherId)
|
||
.execute(),
|
||
|
||
// 已完成作业数
|
||
supa.from('ak_assignments')
|
||
.select('*', { count: 'exact', head: true })
|
||
.eq('teacher_id', teacherId)
|
||
.eq('status', 'completed')
|
||
.execute(),
|
||
|
||
// 待批改作业数
|
||
supa.from('ak_assignments')
|
||
.select('*', { count: 'exact', head: true })
|
||
.eq('teacher_id', teacherId)
|
||
.eq('status', 'submitted')
|
||
.execute()
|
||
])
|
||
|
||
return {
|
||
total: totalAssignments.count ?? 0,
|
||
completed: completedAssignments.count ?? 0,
|
||
pending: pendingReview.count ?? 0
|
||
}
|
||
} catch (error) {
|
||
console.error('加载统计失败:', error)
|
||
return { total: 0, completed: 0, pending: 0 }
|
||
}
|
||
}
|
||
```
|
||
|
||
### 分页列表(数据+总数)
|
||
|
||
```typescript
|
||
const loadAssignmentList = async (page: number, pageSize: number) => {
|
||
const result = await supa
|
||
.from('ak_assignments')
|
||
.select('*', { count: 'exact' })
|
||
.eq('teacher_id', userId)
|
||
.order('created_at', { ascending: false })
|
||
.range((page - 1) * pageSize, page * pageSize - 1)
|
||
.execute()
|
||
|
||
return {
|
||
data: result.data,
|
||
total: result.total,
|
||
hasMore: result.hasmore
|
||
}
|
||
}
|
||
```
|
||
|
||
## 性能建议
|
||
|
||
### 1. 大表查询使用 estimated
|
||
```typescript
|
||
// 大表(100万+行)使用估算
|
||
const result = await supa
|
||
.from('large_table')
|
||
.select('*', { count: 'estimated', head: true })
|
||
.execute()
|
||
```
|
||
|
||
### 2. 频繁查询使用缓存
|
||
```typescript
|
||
const countCache = new Map()
|
||
|
||
const getCachedCount = async (key: string, queryFn: () => Promise<any>) => {
|
||
if (countCache.has(key)) {
|
||
return countCache.get(key)
|
||
}
|
||
|
||
const result = await queryFn()
|
||
countCache.set(key, result.count)
|
||
|
||
// 5分钟后清除缓存
|
||
setTimeout(() => countCache.delete(key), 5 * 60 * 1000)
|
||
|
||
return result.count
|
||
}
|
||
```
|
||
|
||
### 3. 添加适当索引
|
||
```sql
|
||
-- 为经常查询的条件添加索引
|
||
CREATE INDEX idx_assignments_teacher_status ON ak_assignments(teacher_id, status);
|
||
CREATE INDEX idx_users_role ON ak_users(role);
|
||
```
|
||
|
||
## 错误处理
|
||
|
||
```typescript
|
||
const safeGetCount = async (queryBuilder: any): Promise<number> => {
|
||
try {
|
||
const result = await queryBuilder.execute()
|
||
|
||
if (result.status >= 200 && result.status < 300) {
|
||
return result.count ?? 0
|
||
}
|
||
|
||
console.warn('Count 查询返回错误状态:', result.status)
|
||
return 0
|
||
} catch (error) {
|
||
console.error('Count 查询异常:', error)
|
||
return 0
|
||
}
|
||
}
|
||
|
||
// 使用示例
|
||
const totalCount = await safeGetCount(
|
||
supa.from('ak_assignments')
|
||
.select('*', { count: 'exact', head: true })
|
||
.eq('teacher_id', userId)
|
||
)
|
||
```
|
||
|
||
## 总结
|
||
|
||
对于 **只需要数量** 的场景:
|
||
- ✅ **首选**:`select('*', { count: 'exact', head: true })`
|
||
- ✅ **大表**:`select('*', { count: 'estimated', head: true })`
|
||
|
||
对于 **需要数据+数量** 的场景:
|
||
- ✅ **首选**:`select('*', { count: 'exact' })`
|
||
|
||
这样的实现既保证了性能,又符合 Supabase 的最佳实践。
|