智慧水务管理系统 - 精河县供水工程综合管理平台

db_query_stress.py 25KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684
  1. #!/usr/bin/env python3
  2. """
  3. 大数据量数据库查询压力测试
  4. 测试百万级记录下各类查询的性能:
  5. - 简单查询(单表 WHERE)
  6. - 聚合查询(GROUP BY / SUM / AVG)
  7. - JOIN 查询(多表关联)
  8. - GIS 空间查询(PostGIS ST_DWithin 等)
  9. - 对比有无索引的性能差异
  10. 运行方式:
  11. python db_query_stress.py [--host HOST] [--port PORT] [--db DB] [--user USER] [--password PASS]
  12. 示例:
  13. python db_query_stress.py --host localhost --db water_management --generate-data 1000000
  14. """
  15. import argparse
  16. import json
  17. import time
  18. import random
  19. import statistics
  20. import sys
  21. from datetime import datetime, timedelta
  22. from contextlib import contextmanager
  23. try:
  24. import psycopg2
  25. from psycopg2 import pool
  26. except ImportError:
  27. print("请先安装 psycopg2: pip install psycopg2-binary")
  28. sys.exit(1)
  29. # ==================== 配置 ====================
  30. DEFAULT_HOST = "localhost"
  31. DEFAULT_PORT = 5432
  32. DEFAULT_DB = "water_management"
  33. DEFAULT_USER = "postgres"
  34. DEFAULT_PASSWORD = "postgres"
  35. # 测试查询配置
  36. QUERY_ROUNDS = 5 # 每个查询重复次数取平均值
  37. # ==================== 数据库连接 ====================
  38. class DBConnectionPool:
  39. """数据库连接池管理"""
  40. def __init__(self, host, port, db, user, password, min_conn=2, max_conn=10):
  41. self.pool = pool.ThreadedConnectionPool(
  42. min_conn, max_conn,
  43. host=host, port=port, dbname=db, user=user, password=password,
  44. connect_timeout=10,
  45. )
  46. @contextmanager
  47. def get_connection(self):
  48. conn = self.pool.getconn()
  49. try:
  50. yield conn
  51. finally:
  52. self.pool.putconn(conn)
  53. def close(self):
  54. self.pool.closeall()
  55. # ==================== 测试数据生成 ====================
  56. def generate_test_data(db_pool, num_records):
  57. """生成测试数据"""
  58. print(f"\n📦 生成 {num_records} 条测试数据...")
  59. with db_pool.get_connection() as conn:
  60. cur = conn.cursor()
  61. # 创建传感器数据表(如不存在)
  62. cur.execute("""
  63. CREATE TABLE IF NOT EXISTS perf_sensor_data (
  64. id BIGSERIAL PRIMARY KEY,
  65. sensor_id INTEGER NOT NULL,
  66. device_id VARCHAR(32) NOT NULL,
  67. timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  68. pressure REAL,
  69. flow REAL,
  70. temperature REAL,
  71. ph REAL,
  72. turbidity REAL,
  73. chlorine REAL,
  74. battery REAL,
  75. signal_strength INTEGER,
  76. lng DOUBLE PRECISION,
  77. lat DOUBLE PRECISION,
  78. area_id INTEGER,
  79. created_at TIMESTAMPTZ DEFAULT NOW()
  80. )
  81. """)
  82. # 创建告警表
  83. cur.execute("""
  84. CREATE TABLE IF NOT EXISTS perf_alarms (
  85. id BIGSERIAL PRIMARY KEY,
  86. sensor_id INTEGER NOT NULL,
  87. alarm_type VARCHAR(32) NOT NULL,
  88. severity VARCHAR(16) NOT NULL,
  89. value REAL,
  90. threshold REAL,
  91. description TEXT,
  92. acknowledged BOOLEAN DEFAULT FALSE,
  93. timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  94. created_at TIMESTAMPTZ DEFAULT NOW()
  95. )
  96. """)
  97. # 创建设备表
  98. cur.execute("""
  99. CREATE TABLE IF NOT EXISTS perf_devices (
  100. id SERIAL PRIMARY KEY,
  101. device_id VARCHAR(32) UNIQUE NOT NULL,
  102. name VARCHAR(128),
  103. device_type VARCHAR(32),
  104. area_id INTEGER,
  105. lng DOUBLE PRECISION,
  106. lat DOUBLE PRECISION,
  107. status VARCHAR(16) DEFAULT 'active',
  108. install_date DATE,
  109. created_at TIMESTAMPTZ DEFAULT NOW()
  110. )
  111. """)
  112. conn.commit()
  113. # 清空旧测试数据
  114. print(" 清空旧数据...")
  115. cur.execute("TRUNCATE TABLE perf_sensor_data, perf_alarms, perf_devices")
  116. conn.commit()
  117. # 生成设备数据
  118. print(" 生成设备数据...")
  119. device_count = min(5000, num_records // 100)
  120. devices = []
  121. for i in range(1, device_count + 1):
  122. device_id = f"DEV{i:06d}"
  123. devices.append((
  124. device_id,
  125. f"传感器-{i}",
  126. random.choice(["pressure", "flow", "quality", "level", "valve"]),
  127. random.randint(1, 50),
  128. round(random.uniform(116.0, 117.0), 6),
  129. round(random.uniform(39.5, 40.5), 6),
  130. random.choice(["active", "inactive", "maintenance"]),
  131. (datetime.now() - timedelta(days=random.randint(0, 730))).date(),
  132. ))
  133. cur.executemany("""
  134. INSERT INTO perf_devices (device_id, name, device_type, area_id, lng, lat, status, install_date)
  135. VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
  136. """, devices)
  137. conn.commit()
  138. print(f" ✅ 已生成 {device_count} 个设备")
  139. # 批量插入传感器数据
  140. print(f" 生成 {num_records} 条传感器数据...")
  141. batch_size = 10000
  142. total_inserted = 0
  143. base_time = datetime.now() - timedelta(days=365)
  144. alarm_types = ["PRESSURE_HIGH", "PRESSURE_LOW", "FLOW_ABNORMAL",
  145. "QUALITY_WARNING", "LEAK_DETECTED", "BATTERY_LOW"]
  146. severities = ["INFO", "WARNING", "CRITICAL"]
  147. while total_inserted < num_records:
  148. batch = []
  149. for _ in range(min(batch_size, num_records - total_inserted)):
  150. sensor_id = random.randint(1, device_count)
  151. ts = base_time + timedelta(
  152. seconds=random.randint(0, 365 * 24 * 3600)
  153. )
  154. batch.append((
  155. sensor_id,
  156. f"DEV{sensor_id:06d}",
  157. ts,
  158. round(random.uniform(0.1, 0.8), 3), # pressure
  159. round(random.uniform(10, 500), 2), # flow
  160. round(random.uniform(5, 35), 1), # temperature
  161. round(random.uniform(6.5, 8.5), 2), # ph
  162. round(random.uniform(0, 5), 2), # turbidity
  163. round(random.uniform(0.1, 0.8), 3), # chlorine
  164. round(random.uniform(20, 100), 1), # battery
  165. random.randint(-90, -30), # signal
  166. round(random.uniform(116.0, 117.0), 6), # lng
  167. round(random.uniform(39.5, 40.5), 6), # lat
  168. random.randint(1, 50), # area_id
  169. ))
  170. cur.executemany("""
  171. INSERT INTO perf_sensor_data
  172. (sensor_id, device_id, timestamp, pressure, flow, temperature,
  173. ph, turbidity, chlorine, battery, signal_strength, lng, lat, area_id)
  174. VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)
  175. """, batch)
  176. conn.commit()
  177. total_inserted += len(batch)
  178. if total_inserted % 100000 == 0:
  179. print(f" 进度: {total_inserted}/{num_records} ({total_inserted*100//num_records}%)")
  180. print(f" ✅ 已生成 {total_inserted} 条传感器数据")
  181. # 生成告警数据(传感器数据的 5%)
  182. alarm_count = num_records // 20
  183. print(f" 生成 {alarm_count} 条告警数据...")
  184. alarms = []
  185. for _ in range(alarm_count):
  186. sensor_id = random.randint(1, device_count)
  187. alarms.append((
  188. sensor_id,
  189. random.choice(alarm_types),
  190. random.choice(severities),
  191. round(random.uniform(0, 100), 2),
  192. round(random.uniform(50, 100), 2),
  193. f"自动告警描述 - {random.choice(alarm_types)}",
  194. random.choice([True, False]),
  195. base_time + timedelta(seconds=random.randint(0, 365 * 24 * 3600)),
  196. ))
  197. cur.executemany("""
  198. INSERT INTO perf_alarms
  199. (sensor_id, alarm_type, severity, value, threshold, description, acknowledged, timestamp)
  200. VALUES (%s,%s,%s,%s,%s,%s,%s,%s)
  201. """, alarms)
  202. conn.commit()
  203. print(f" ✅ 已生成 {alarm_count} 条告警数据")
  204. cur.close()
  205. print("✅ 测试数据生成完成\n")
  206. # ==================== 索引管理 ====================
  207. def create_indexes(db_pool):
  208. """创建性能测试用索引"""
  209. with db_pool.get_connection() as conn:
  210. cur = conn.cursor()
  211. indexes = [
  212. "CREATE INDEX IF NOT EXISTS idx_sensor_data_sensor_id ON perf_sensor_data(sensor_id)",
  213. "CREATE INDEX IF NOT EXISTS idx_sensor_data_timestamp ON perf_sensor_data(timestamp)",
  214. "CREATE INDEX IF NOT EXISTS idx_sensor_data_area_id ON perf_sensor_data(area_id)",
  215. "CREATE INDEX IF NOT EXISTS idx_sensor_data_device_id ON perf_sensor_data(device_id)",
  216. "CREATE INDEX IF NOT EXISTS idx_sensor_data_composite ON perf_sensor_data(sensor_id, timestamp)",
  217. "CREATE INDEX IF NOT EXISTS idx_alarms_sensor_id ON perf_alarms(sensor_id)",
  218. "CREATE INDEX IF NOT EXISTS idx_alarms_timestamp ON perf_alarms(timestamp)",
  219. "CREATE INDEX IF NOT EXISTS idx_alarms_type ON perf_alarms(alarm_type)",
  220. "CREATE INDEX IF NOT EXISTS idx_devices_area_id ON perf_devices(area_id)",
  221. "CREATE INDEX IF NOT EXISTS idx_devices_type ON perf_devices(device_type)",
  222. ]
  223. for idx in indexes:
  224. cur.execute(idx)
  225. conn.commit()
  226. cur.close()
  227. print("✅ 索引已创建")
  228. def drop_indexes(db_pool):
  229. """删除性能测试用索引"""
  230. with db_pool.get_connection() as conn:
  231. cur = conn.cursor()
  232. indexes = [
  233. "idx_sensor_data_sensor_id", "idx_sensor_data_timestamp",
  234. "idx_sensor_data_area_id", "idx_sensor_data_device_id",
  235. "idx_sensor_data_composite", "idx_alarms_sensor_id",
  236. "idx_alarms_timestamp", "idx_alarms_type",
  237. "idx_devices_area_id", "idx_devices_type",
  238. ]
  239. for idx in indexes:
  240. cur.execute(f"DROP INDEX IF EXISTS {idx}")
  241. conn.commit()
  242. cur.close()
  243. print("🗑️ 索引已删除")
  244. # ==================== 查询测试 ====================
  245. class QueryBenchmark:
  246. """查询性能基准测试"""
  247. def __init__(self, db_pool):
  248. self.db_pool = db_pool
  249. self.results = []
  250. def run_query(self, name, sql, params=None, rounds=QUERY_ROUNDS):
  251. """执行查询并统计耗时"""
  252. times = []
  253. row_count = 0
  254. for i in range(rounds):
  255. with self.db_pool.get_connection() as conn:
  256. cur = conn.cursor()
  257. start = time.time()
  258. try:
  259. cur.execute(sql, params)
  260. rows = cur.fetchall()
  261. row_count = len(rows)
  262. except Exception as e:
  263. print(f" ⚠️ 查询错误: {e}")
  264. conn.rollback()
  265. row_count = 0
  266. times.append(-1)
  267. cur.close()
  268. continue
  269. elapsed_ms = (time.time() - start) * 1000
  270. times.append(elapsed_ms)
  271. cur.close()
  272. valid_times = [t for t in times if t >= 0]
  273. if valid_times:
  274. result = {
  275. "name": name,
  276. "sql": sql[:100] + ("..." if len(sql) > 100 else ""),
  277. "rounds": rounds,
  278. "success_rounds": len(valid_times),
  279. "rows_returned": row_count,
  280. "min_ms": round(min(valid_times), 2),
  281. "max_ms": round(max(valid_times), 2),
  282. "avg_ms": round(statistics.mean(valid_times), 2),
  283. "median_ms": round(statistics.median(valid_times), 2),
  284. }
  285. else:
  286. result = {
  287. "name": name,
  288. "sql": sql[:100] + ("..." if len(sql) > 100 else ""),
  289. "rounds": rounds,
  290. "success_rounds": 0,
  291. "error": "all rounds failed",
  292. }
  293. self.results.append(result)
  294. status = "✅" if valid_times else "❌"
  295. avg = result.get("avg_ms", "N/A")
  296. print(f" {status} {name}: avg={avg}ms, rows={row_count}")
  297. return result
  298. def run_all_benchmarks(self):
  299. """运行所有基准测试"""
  300. print("\n🔍 运行查询性能测试...\n")
  301. # 1. 简单查询
  302. print(" --- 简单查询 ---")
  303. self.run_query(
  304. "简单查询 - 按传感器ID查询最近24小时",
  305. """SELECT * FROM perf_sensor_data
  306. WHERE sensor_id = %s AND timestamp > NOW() - INTERVAL '24 hours'
  307. ORDER BY timestamp DESC LIMIT 100""",
  308. (random.randint(1, 5000),),
  309. )
  310. self.run_query(
  311. "简单查询 - 按区域ID查询",
  312. """SELECT * FROM perf_sensor_data
  313. WHERE area_id = %s
  314. ORDER BY timestamp DESC LIMIT 100""",
  315. (random.randint(1, 50),),
  316. )
  317. self.run_query(
  318. "简单查询 - 时间范围查询",
  319. """SELECT * FROM perf_sensor_data
  320. WHERE timestamp BETWEEN %s AND %s
  321. ORDER BY timestamp DESC LIMIT 1000""",
  322. (
  323. (datetime.now() - timedelta(days=7)).isoformat(),
  324. datetime.now().isoformat(),
  325. ),
  326. )
  327. # 2. 聚合查询
  328. print("\n --- 聚合查询 ---")
  329. self.run_query(
  330. "聚合查询 - 每小时平均压力",
  331. """SELECT date_trunc('hour', timestamp) AS hour,
  332. AVG(pressure) AS avg_pressure,
  333. MAX(pressure) AS max_pressure,
  334. MIN(pressure) AS min_pressure,
  335. COUNT(*) AS readings
  336. FROM perf_sensor_data
  337. WHERE sensor_id = %s AND timestamp > NOW() - INTERVAL '7 days'
  338. GROUP BY hour ORDER BY hour""",
  339. (random.randint(1, 5000),),
  340. )
  341. self.run_query(
  342. "聚合查询 - 各区域统计",
  343. """SELECT area_id,
  344. COUNT(*) AS total_readings,
  345. AVG(pressure) AS avg_pressure,
  346. AVG(flow) AS avg_flow,
  347. AVG(temperature) AS avg_temp
  348. FROM perf_sensor_data
  349. WHERE timestamp > NOW() - INTERVAL '1 day'
  350. GROUP BY area_id ORDER BY total_readings DESC""",
  351. )
  352. self.run_query(
  353. "聚合查询 - 日统计报表",
  354. """SELECT date_trunc('day', timestamp) AS day,
  355. COUNT(*) AS readings,
  356. AVG(pressure) AS avg_pressure,
  357. AVG(flow) AS avg_flow,
  358. STDDEV(pressure) AS pressure_stddev
  359. FROM perf_sensor_data
  360. WHERE timestamp > NOW() - INTERVAL '30 days'
  361. GROUP BY day ORDER BY day""",
  362. )
  363. # 3. JOIN 查询
  364. print("\n --- JOIN 查询 ---")
  365. self.run_query(
  366. "JOIN 查询 - 传感器数据 + 设备信息",
  367. """SELECT s.timestamp, s.pressure, s.flow, s.temperature,
  368. d.name AS device_name, d.device_type, d.area_id, d.status
  369. FROM perf_sensor_data s
  370. JOIN perf_devices d ON s.device_id = d.device_id
  371. WHERE s.sensor_id = %s
  372. ORDER BY s.timestamp DESC LIMIT 100""",
  373. (random.randint(1, 5000),),
  374. )
  375. self.run_query(
  376. "JOIN 查询 - 告警 + 设备信息",
  377. """SELECT a.timestamp, a.alarm_type, a.severity, a.value, a.threshold,
  378. d.name AS device_name, d.device_type, d.lng, d.lat
  379. FROM perf_alarms a
  380. JOIN perf_devices d ON a.sensor_id = d.id
  381. WHERE a.timestamp > NOW() - INTERVAL '7 days'
  382. ORDER BY a.timestamp DESC LIMIT 200""",
  383. )
  384. self.run_query(
  385. "JOIN 查询 - 三表关联统计",
  386. """SELECT d.area_id,
  387. COUNT(DISTINCT d.id) AS device_count,
  388. COUNT(s.id) AS reading_count,
  389. COUNT(a.id) AS alarm_count,
  390. AVG(s.pressure) AS avg_pressure
  391. FROM perf_devices d
  392. LEFT JOIN perf_sensor_data s ON s.device_id = d.device_id
  393. AND s.timestamp > NOW() - INTERVAL '1 day'
  394. LEFT JOIN perf_alarms a ON a.sensor_id = d.id
  395. AND a.timestamp > NOW() - INTERVAL '1 day'
  396. GROUP BY d.area_id ORDER BY reading_count DESC""",
  397. )
  398. # 4. GIS 空间查询
  399. print("\n --- GIS 空间查询 ---")
  400. # 先检查 PostGIS 是否可用
  401. postgis_available = False
  402. try:
  403. with self.db_pool.get_connection() as conn:
  404. cur = conn.cursor()
  405. cur.execute("SELECT PostGIS_Version()")
  406. postgis_available = True
  407. cur.close()
  408. except Exception:
  409. pass
  410. if postgis_available:
  411. self.run_query(
  412. "GIS 查询 - 圆形范围内设备 (1km)",
  413. """SELECT d.id, d.device_id, d.name, d.device_type,
  414. ST_Distance(
  415. ST_SetSRID(ST_MakePoint(d.lng, d.lat), 4326)::geography,
  416. ST_SetSRID(ST_MakePoint(%s, %s), 4326)::geography
  417. ) AS distance_m
  418. FROM perf_devices d
  419. WHERE ST_DWithin(
  420. ST_SetSRID(ST_MakePoint(d.lng, d.lat), 4326)::geography,
  421. ST_SetSRID(ST_MakePoint(%s, %s), 4326)::geography,
  422. 1000
  423. )
  424. ORDER BY distance_m""",
  425. (116.4, 39.9, 116.4, 39.9),
  426. )
  427. else:
  428. # 无 PostGIS 时用距离公式近似
  429. self.run_query(
  430. "空间查询 - 距离近似计算 (无PostGIS)",
  431. """SELECT id, device_id, name, device_type,
  432. SQRT(POW(lng - %s, 2) + POW(lat - %s, 2)) * 111000 AS approx_distance_m
  433. FROM perf_devices
  434. WHERE ABS(lng - %s) < 0.01 AND ABS(lat - %s) < 0.01
  435. ORDER BY approx_distance_m
  436. LIMIT 100""",
  437. (116.4, 39.9, 116.4, 39.9),
  438. )
  439. self.run_query(
  440. "空间查询 - 区域设备统计",
  441. """SELECT area_id,
  442. COUNT(*) AS device_count,
  443. AVG(lng) AS center_lng,
  444. AVG(lat) AS center_lat,
  445. COUNT(CASE WHEN status = 'active' THEN 1 END) AS active_count
  446. FROM perf_devices
  447. GROUP BY area_id ORDER BY device_count DESC""",
  448. )
  449. # 5. 复杂查询
  450. print("\n --- 复杂查询 ---")
  451. self.run_query(
  452. "复杂查询 - 异常检测 (压力突变)",
  453. """WITH sensor_stats AS (
  454. SELECT sensor_id,
  455. AVG(pressure) AS avg_p,
  456. STDDEV(pressure) AS std_p
  457. FROM perf_sensor_data
  458. WHERE timestamp > NOW() - INTERVAL '7 days'
  459. GROUP BY sensor_id
  460. HAVING COUNT(*) > 10
  461. )
  462. SELECT s.sensor_id, s.timestamp, s.pressure,
  463. ss.avg_p, ss.std_p,
  464. ABS(s.pressure - ss.avg_p) / NULLIF(ss.std_p, 0) AS z_score
  465. FROM perf_sensor_data s
  466. JOIN sensor_stats ss ON s.sensor_id = ss.sensor_id
  467. WHERE ABS(s.pressure - ss.avg_p) > 3 * NULLIF(ss.std_p, 0)
  468. AND s.timestamp > NOW() - INTERVAL '1 day'
  469. ORDER BY z_score DESC LIMIT 50""",
  470. )
  471. self.run_query(
  472. "复杂查询 - 窗口函数 (滑动平均)",
  473. """SELECT sensor_id, timestamp, pressure,
  474. AVG(pressure) OVER (
  475. PARTITION BY sensor_id
  476. ORDER BY timestamp
  477. ROWS BETWEEN 5 PRECEDING AND 5 FOLLOWING
  478. ) AS moving_avg
  479. FROM perf_sensor_data
  480. WHERE sensor_id = %s
  481. AND timestamp > NOW() - INTERVAL '1 day'
  482. ORDER BY timestamp LIMIT 500""",
  483. (random.randint(1, 5000),),
  484. )
  485. return self.results
  486. # ==================== 索引对比测试 ====================
  487. def run_index_comparison(db_pool):
  488. """对比有无索引的查询性能"""
  489. print("\n" + "=" * 60)
  490. print("📊 索引性能对比测试")
  491. print("=" * 60)
  492. # 无索引测试
  493. print("\n--- 无索引状态 ---")
  494. drop_indexes(db_pool)
  495. bench_no_idx = QueryBenchmark(db_pool)
  496. results_no_idx = bench_no_idx.run_all_benchmarks()
  497. # 有索引测试
  498. print("\n--- 有索引状态 ---")
  499. create_indexes(db_pool)
  500. # 先 ANALYZE 更新统计
  501. with db_pool.get_connection() as conn:
  502. cur = conn.cursor()
  503. cur.execute("ANALYZE perf_sensor_data, perf_alarms, perf_devices")
  504. conn.commit()
  505. cur.close()
  506. bench_with_idx = QueryBenchmark(db_pool)
  507. results_with_idx = bench_with_idx.run_all_benchmarks()
  508. # 对比
  509. print("\n" + "=" * 60)
  510. print("📊 索引性能对比结果")
  511. print("=" * 60)
  512. print(f"{'查询名称':<40} {'无索引(ms)':>12} {'有索引(ms)':>12} {'提升':>10}")
  513. print("-" * 74)
  514. comparison = []
  515. for no_idx, with_idx in zip(results_no_idx, results_with_idx):
  516. no_avg = no_idx.get("avg_ms", 0)
  517. with_avg = with_idx.get("avg_ms", 0)
  518. if no_avg > 0:
  519. improvement = f"{(1 - with_avg / no_avg) * 100:.1f}%"
  520. else:
  521. improvement = "N/A"
  522. print(f"{no_idx['name']:<40} {no_avg:>12.2f} {with_avg:>12.2f} {improvement:>10}")
  523. comparison.append({
  524. "query": no_idx["name"],
  525. "no_index_ms": no_avg,
  526. "with_index_ms": with_avg,
  527. })
  528. return comparison
  529. # ==================== 入口 ====================
  530. def main():
  531. parser = argparse.ArgumentParser(description="数据库查询压力测试")
  532. parser.add_argument("--host", default=DEFAULT_HOST, help="数据库主机")
  533. parser.add_argument("--port", type=int, default=DEFAULT_PORT, help="数据库端口")
  534. parser.add_argument("--db", default=DEFAULT_DB, help="数据库名")
  535. parser.add_argument("--user", default=DEFAULT_USER, help="数据库用户")
  536. parser.add_argument("--password", default=DEFAULT_PASSWORD, help="数据库密码")
  537. parser.add_argument("--generate-data", type=int, default=0,
  538. help="生成指定条数的测试数据 (如 1000000)")
  539. parser.add_argument("--skip-data-gen", action="store_true", help="跳过数据生成")
  540. parser.add_argument("--index-compare", action="store_true", help="运行索引对比测试")
  541. parser.add_argument("--output", default=None, help="结果输出 JSON 文件路径")
  542. args = parser.parse_args()
  543. print(f"\n{'=' * 60}")
  544. print(f"🗄️ 数据库查询压力测试")
  545. print(f"{'=' * 60}")
  546. print(f"数据库: {args.host}:{args.port}/{args.db}")
  547. print(f"开始时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
  548. print(f"{'=' * 60}")
  549. # 连接数据库
  550. try:
  551. db_pool = DBConnectionPool(
  552. args.host, args.port, args.db, args.user, args.password
  553. )
  554. except Exception as e:
  555. print(f"❌ 数据库连接失败: {e}")
  556. sys.exit(1)
  557. all_results = {}
  558. try:
  559. # 生成测试数据
  560. if args.generate_data > 0 and not args.skip_data_gen:
  561. generate_test_data(db_pool, args.generate_data)
  562. create_indexes(db_pool)
  563. # ANALYZE
  564. with db_pool.get_connection() as conn:
  565. cur = conn.cursor()
  566. cur.execute("ANALYZE perf_sensor_data, perf_alarms, perf_devices")
  567. conn.commit()
  568. cur.close()
  569. # 运行基准查询测试
  570. bench = QueryBenchmark(db_pool)
  571. results = bench.run_all_benchmarks()
  572. all_results["query_benchmark"] = results
  573. # 索引对比测试
  574. if args.index_compare:
  575. comparison = run_index_comparison(db_pool)
  576. all_results["index_comparison"] = comparison
  577. finally:
  578. db_pool.close()
  579. # 输出汇总
  580. print(f"\n{'=' * 60}")
  581. print("📊 查询测试汇总")
  582. print(f"{'=' * 60}")
  583. for r in all_results.get("query_benchmark", []):
  584. avg = r.get("avg_ms", "ERR")
  585. print(f" {r['name']}: {avg}ms ({r.get('rows_returned', 0)} rows)")
  586. if args.output:
  587. with open(args.output, "w") as f:
  588. json.dump(all_results, f, indent=2, ensure_ascii=False, default=str)
  589. print(f"\n结果已保存到: {args.output}")
  590. print(f"\n{'=' * 60}")
  591. return all_results
  592. if __name__ == "__main__":
  593. main()