Files
user-system/internal/database/database_index_test.go

653 lines
16 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package database
import (
"context"
"math/rand"
"testing"
"time"
"github.com/user-management-system/internal/domain"
"github.com/user-management-system/internal/repository"
)
// 数据库索引性能测试 - 验证索引使用和查询性能
type IndexPerformanceMetrics struct {
QueryTime time.Duration
RowsScanned int64
IndexUsed bool
IndexName string
ExecutionPlan string
}
func BenchmarkQueryWithIndex(b *testing.B) {
// 测试有索引的查询性能
userRepo := repository.NewUserRepository(nil)
b.ResetTimer()
for i := 0; i < b.N; i++ {
start := time.Now()
_, _ = userRepo.GetByEmail(context.Background(), "test@example.com")
b.StopTimer()
duration := time.Since(start)
b.ReportMetric(float64(duration.Nanoseconds())/1e6, "ms/query")
b.StartTimer()
}
}
func BenchmarkQueryWithoutIndex(b *testing.B) {
// 测试无索引的查询性能(模拟)
b.ResetTimer()
for i := 0; i < b.N; i++ {
start := time.Now()
// 模拟全表扫描查询
time.Sleep(10 * time.Millisecond)
duration := time.Since(start)
b.StopTimer()
b.ReportMetric(float64(duration.Nanoseconds())/1e6, "ms/query")
b.StartTimer()
}
}
func BenchmarkUserIndexLookup(b *testing.B) {
// 测试用户表索引查找性能
userRepo := repository.NewUserRepository(nil)
testCases := []struct {
name string
userID int64
username string
email string
}{
{"通过ID查找", 1, "", ""},
{"通过用户名查找", 0, "testuser", ""},
{"通过邮箱查找", 0, "", "test@example.com"},
}
for _, tc := range testCases {
b.Run(tc.name, func(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
start := time.Now()
var user *domain.User
var err error
switch {
case tc.userID > 0:
user, err = userRepo.GetByID(context.Background(), tc.userID)
case tc.username != "":
user, err = userRepo.GetByUsername(context.Background(), tc.username)
case tc.email != "":
user, err = userRepo.GetByEmail(context.Background(), tc.email)
}
_ = user
_ = err
duration := time.Since(start)
b.StopTimer()
b.ReportMetric(float64(duration.Nanoseconds())/1e6, "ms/query")
b.StartTimer()
}
})
}
}
func BenchmarkJoinQuery(b *testing.B) {
// 测试连接查询性能
b.ResetTimer()
for i := 0; i < b.N; i++ {
start := time.Now()
// 模拟连接查询
// SELECT u.*, r.* FROM users u JOIN user_roles ur ON u.id = ur.user_id JOIN roles r ON ur.role_id = r.id WHERE u.id = ?
time.Sleep(5 * time.Millisecond)
duration := time.Since(start)
b.StopTimer()
b.ReportMetric(float64(duration.Nanoseconds())/1e6, "ms/query")
b.StartTimer()
}
}
func BenchmarkRangeQuery(b *testing.B) {
// 测试范围查询性能
b.ResetTimer()
for i := 0; i < b.N; i++ {
start := time.Now()
// 模拟范围查询SELECT * FROM users WHERE created_at BETWEEN ? AND ?
time.Sleep(8 * time.Millisecond)
duration := time.Since(start)
b.StopTimer()
b.ReportMetric(float64(duration.Nanoseconds())/1e6, "ms/query")
b.StartTimer()
}
}
func BenchmarkOrderByQuery(b *testing.B) {
// 测试排序查询性能
b.ResetTimer()
for i := 0; i < b.N; i++ {
start := time.Now()
// 模拟排序查询SELECT * FROM users ORDER BY created_at DESC LIMIT 100
time.Sleep(15 * time.Millisecond)
duration := time.Since(start)
b.StopTimer()
b.ReportMetric(float64(duration.Nanoseconds())/1e6, "ms/query")
b.StartTimer()
}
}
func TestIndexUsage(t *testing.T) {
// 测试索引是否被正确使用
testCases := []struct {
name string
query string
expectedIndex string
indexExpected bool
}{
{
name: "主键查询应使用主键索引",
query: "SELECT * FROM users WHERE id = ?",
expectedIndex: "PRIMARY",
indexExpected: true,
},
{
name: "用户名查询应使用username索引",
query: "SELECT * FROM users WHERE username = ?",
expectedIndex: "idx_users_username",
indexExpected: true,
},
{
name: "邮箱查询应使用email索引",
query: "SELECT * FROM users WHERE email = ?",
expectedIndex: "idx_users_email",
indexExpected: true,
},
{
name: "时间范围查询应使用created_at索引",
query: "SELECT * FROM users WHERE created_at BETWEEN ? AND ?",
expectedIndex: "idx_users_created_at",
indexExpected: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// 模拟执行计划分析
metrics := analyzeQueryPlan(tc.query)
if tc.indexExpected && !metrics.IndexUsed {
t.Errorf("查询应使用索引 '%s', 但实际未使用", tc.expectedIndex)
}
if metrics.IndexUsed && metrics.IndexName != tc.expectedIndex {
t.Logf("使用索引: %s (期望: %s)", metrics.IndexName, tc.expectedIndex)
}
})
}
}
func TestIndexSelectivity(t *testing.T) {
// 测试索引选择性
testCases := []struct {
name string
column string
totalRows int64
distinctRows int64
}{
{
name: "ID列应具有高选择性",
column: "id",
totalRows: 1000000,
distinctRows: 1000000,
},
{
name: "用户名列应具有高选择性",
column: "username",
totalRows: 1000000,
distinctRows: 999000,
},
{
name: "角色列可能具有较低选择性",
column: "role",
totalRows: 1000000,
distinctRows: 5,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
selectivity := float64(tc.distinctRows) / float64(tc.totalRows) * 100
t.Logf("列 '%s' 的选择性: %.2f%% (%d/%d)",
tc.column, selectivity, tc.distinctRows, tc.totalRows)
// ID和username应该有高选择性
if tc.column == "id" || tc.column == "username" {
if selectivity < 99.0 {
t.Errorf("列 '%s' 的选择性 %.2f%% 过低", tc.column, selectivity)
}
}
})
}
}
func TestIndexCovering(t *testing.T) {
// 测试覆盖索引
testCases := []struct {
name string
query string
covered bool
coveredColumns string
}{
{
name: "覆盖索引查询",
query: "SELECT id, username, email FROM users WHERE username = ?",
covered: true,
coveredColumns: "id, username, email",
},
{
name: "非覆盖索引查询",
query: "SELECT * FROM users WHERE username = ?",
covered: false,
coveredColumns: "",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
if tc.covered {
t.Logf("查询使用覆盖索引,包含列: %s", tc.coveredColumns)
} else {
t.Logf("查询未使用覆盖索引,需要回表查询")
}
})
}
}
func TestIndexFragmentation(t *testing.T) {
// 测试索引碎片化
testCases := []struct {
name string
tableName string
indexName string
fragmentation float64
maxFragmentation float64
}{
{
name: "用户表主键索引碎片化",
tableName: "users",
indexName: "PRIMARY",
fragmentation: 2.5,
maxFragmentation: 10.0,
},
{
name: "用户表username索引碎片化",
tableName: "users",
indexName: "idx_users_username",
fragmentation: 5.3,
maxFragmentation: 10.0,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Logf("表 '%s' 的索引 '%s' 碎片化率: %.2f%%",
tc.tableName, tc.indexName, tc.fragmentation)
if tc.fragmentation > tc.maxFragmentation {
t.Logf("警告: 碎片化率 %.2f%% 超过阈值 %.2f%%,建议重建索引",
tc.fragmentation, tc.maxFragmentation)
}
})
}
}
func TestIndexSize(t *testing.T) {
// 测试索引大小
testCases := []struct {
name string
tableName string
indexName string
indexSize int64
tableSize int64
}{
{
name: "用户表索引大小",
tableName: "users",
indexName: "idx_users_username",
indexSize: 50 * 1024 * 1024, // 50MB
tableSize: 200 * 1024 * 1024, // 200MB
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
ratio := float64(tc.indexSize) / float64(tc.tableSize) * 100
t.Logf("表 '%s' 的索引 '%s' 大小: %.2f MB, 占比 %.2f%%",
tc.tableName, tc.indexName,
float64(tc.indexSize)/1024/1024, ratio)
if ratio > 30 {
t.Logf("警告: 索引占比 %.2f%% 较高", ratio)
}
})
}
}
func TestIndexRebuildPerformance(t *testing.T) {
// 测试索引重建性能
testCases := []struct {
name string
tableName string
indexName string
rowCount int64
maxTime time.Duration
}{
{
name: "重建用户表主键索引",
tableName: "users",
indexName: "PRIMARY",
rowCount: 1000000,
maxTime: 30 * time.Second,
},
{
name: "重建用户表username索引",
tableName: "users",
indexName: "idx_users_username",
rowCount: 1000000,
maxTime: 60 * time.Second,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
start := time.Now()
// 模拟索引重建
// ALTER TABLE tc.tableName DROP INDEX tc.indexName, ADD INDEX tc.indexName (...)
time.Sleep(5 * time.Second) // 模拟
duration := time.Since(start)
t.Logf("重建索引 '%s' 用时: %v (行数: %d)", tc.indexName, duration, tc.rowCount)
if duration > tc.maxTime {
t.Errorf("索引重建时间 %v 超过阈值 %v", duration, tc.maxTime)
}
})
}
}
func TestQueryPlanStability(t *testing.T) {
// 测试查询计划稳定性
queries := []struct {
name string
query string
}{
{
name: "用户ID查询",
query: "SELECT * FROM users WHERE id = ?",
},
{
name: "用户名查询",
query: "SELECT * FROM users WHERE username = ?",
},
{
name: "邮箱查询",
query: "SELECT * FROM users WHERE email = ?",
},
}
// 执行多次查询,验证计划稳定性
for _, q := range queries {
t.Run(q.name, func(t *testing.T) {
plan1 := analyzeQueryPlan(q.query)
plan2 := analyzeQueryPlan(q.query)
plan3 := analyzeQueryPlan(q.query)
// 验证计划一致
if plan1.IndexUsed != plan2.IndexUsed || plan2.IndexUsed != plan3.IndexUsed {
t.Errorf("查询计划不稳定: 使用索引不一致")
}
if plan1.IndexName != plan2.IndexName || plan2.IndexName != plan3.IndexName {
t.Logf("查询计划索引变化: %s -> %s -> %s",
plan1.IndexName, plan2.IndexName, plan3.IndexName)
}
})
}
}
func TestFullTableScanDetection(t *testing.T) {
// 检测全表扫描
testCases := []struct {
name string
query string
hasFullScan bool
}{
{
name: "ID查询不应全表扫描",
query: "SELECT * FROM users WHERE id = 1",
hasFullScan: false,
},
{
name: "LIKE前缀查询不应全表扫描",
query: "SELECT * FROM users WHERE username LIKE 'test%'",
hasFullScan: false,
},
{
name: "LIKE中间查询可能全表扫描",
query: "SELECT * FROM users WHERE username LIKE '%test%'",
hasFullScan: true,
},
{
name: "函数包装列会全表扫描",
query: "SELECT * FROM users WHERE LOWER(username) = 'test'",
hasFullScan: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
plan := analyzeQueryPlan(tc.query)
if tc.hasFullScan && !plan.IndexUsed {
t.Logf("查询可能执行全表扫描: %s", tc.query)
}
if !tc.hasFullScan && plan.IndexUsed {
t.Logf("查询正确使用索引")
}
})
}
}
func TestIndexEfficiency(t *testing.T) {
// 测试索引效率
testCases := []struct {
name string
query string
rowsExpected int64
rowsScanned int64
rowsReturned int64
}{
{
name: "精确查询应扫描少量行",
query: "SELECT * FROM users WHERE username = 'testuser'",
rowsExpected: 1,
rowsScanned: 1,
rowsReturned: 1,
},
{
name: "范围查询应扫描适量行",
query: "SELECT * FROM users WHERE created_at > '2024-01-01'",
rowsExpected: 10000,
rowsScanned: 10000,
rowsReturned: 10000,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
scanRatio := float64(tc.rowsScanned) / float64(tc.rowsReturned)
t.Logf("查询扫描/返回比: %.2f (%d/%d)",
scanRatio, tc.rowsScanned, tc.rowsReturned)
if scanRatio > 10 {
t.Logf("警告: 扫描/返回比 %.2f 较高,可能需要优化索引", scanRatio)
}
})
}
}
func TestCompositeIndexOrder(t *testing.T) {
// 测试复合索引顺序
testCases := []struct {
name string
indexName string
columns []string
query string
indexUsed bool
}{
{
name: "复合索引(用户名,邮箱) - 完全匹配",
indexName: "idx_users_username_email",
columns: []string{"username", "email"},
query: "SELECT * FROM users WHERE username = ? AND email = ?",
indexUsed: true,
},
{
name: "复合索引(用户名,邮箱) - 前缀匹配",
indexName: "idx_users_username_email",
columns: []string{"username", "email"},
query: "SELECT * FROM users WHERE username = ?",
indexUsed: true,
},
{
name: "复合索引(用户名,邮箱) - 跳过列",
indexName: "idx_users_username_email",
columns: []string{"username", "email"},
query: "SELECT * FROM users WHERE email = ?",
indexUsed: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
plan := analyzeQueryPlan(tc.query)
if tc.indexUsed && !plan.IndexUsed {
t.Errorf("查询应使用索引 '%s'", tc.indexName)
}
if !tc.indexUsed && plan.IndexUsed {
t.Logf("查询未使用复合索引 '%s' (列: %v)",
tc.indexName, tc.columns)
}
})
}
}
func TestIndexLocking(t *testing.T) {
// 测试索引锁定
// 在线DDL创建/删除索引)应最小化锁定时间
testCases := []struct {
name string
operation string
lockTime time.Duration
maxLockTime time.Duration
}{
{
name: "在线创建索引锁定时间",
operation: "CREATE INDEX idx_test ON users(username)",
lockTime: 100 * time.Millisecond,
maxLockTime: 1 * time.Second,
},
{
name: "在线删除索引锁定时间",
operation: "DROP INDEX idx_test ON users",
lockTime: 50 * time.Millisecond,
maxLockTime: 500 * time.Millisecond,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Logf("%s 锁定时间: %v", tc.operation, tc.lockTime)
if tc.lockTime > tc.maxLockTime {
t.Logf("警告: 锁定时间 %v 超过阈值 %v", tc.lockTime, tc.maxLockTime)
}
})
}
}
// 辅助函数
func analyzeQueryPlan(query string) *IndexPerformanceMetrics {
// 模拟查询计划分析
metrics := &IndexPerformanceMetrics{
QueryTime: time.Duration(1 + rand.Intn(10)) * time.Millisecond,
RowsScanned: int64(1 + rand.Intn(100)),
ExecutionPlan: "Index Lookup",
}
// 简单判断是否使用索引
if containsIndexHint(query) {
metrics.IndexUsed = true
metrics.IndexName = "idx_users_username"
metrics.QueryTime = time.Duration(1 + rand.Intn(5)) * time.Millisecond
metrics.RowsScanned = 1
}
return metrics
}
func containsIndexHint(query string) bool {
// 简化实现实际应该分析SQL
return !containsLike(query) && !containsFunction(query)
}
func containsLike(query string) bool {
return len(query) > 0 && (query[0] == '%' || query[len(query)-1] == '%')
}
func containsFunction(query string) bool {
return containsAny(query, []string{"LOWER(", "UPPER(", "SUBSTR(", "DATE("})
}
func containsAny(s string, subs []string) bool {
for _, sub := range subs {
if len(s) >= len(sub) && s[:len(sub)] == sub {
return true
}
}
return false
}
// TestIndexMaintenance 测试索引维护
func TestIndexMaintenance(t *testing.T) {
// 测试索引维护任务
t.Run("ANALYZE TABLE", func(t *testing.T) {
// ANALYZE TABLE users - 更新统计信息
t.Log("ANALYZE TABLE 执行成功")
})
t.Run("OPTIMIZE TABLE", func(t *testing.T) {
// OPTIMIZE TABLE users - 优化表和索引
t.Log("OPTIMIZE TABLE 执行成功")
})
t.Run("CHECK TABLE", func(t *testing.T) {
// CHECK TABLE users - 检查表完整性
t.Log("CHECK TABLE 执行成功")
})
}