653 lines
16 KiB
Go
653 lines
16 KiB
Go
|
|
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 执行成功")
|
|||
|
|
})
|
|||
|
|
}
|