init ana page with apis
This commit is contained in:
35
scripts/check-clickhouse.sh
Normal file
35
scripts/check-clickhouse.sh
Normal file
@@ -0,0 +1,35 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
echo -e "${YELLOW}开始检查ClickHouse表结构...${NC}"
|
||||
|
||||
# 加载环境变量
|
||||
set -a
|
||||
source .env
|
||||
set +a
|
||||
|
||||
# 获取ClickHouse配置
|
||||
CLICKHOUSE_HOST=${CLICKHOUSE_HOST:-"localhost"}
|
||||
CLICKHOUSE_PORT=${CLICKHOUSE_PORT:-"8123"}
|
||||
CLICKHOUSE_USER=${CLICKHOUSE_USER:-"default"}
|
||||
CLICKHOUSE_PASSWORD=${CLICKHOUSE_PASSWORD:-""}
|
||||
CLICKHOUSE_DATABASE=${CLICKHOUSE_DATABASE:-"default"}
|
||||
|
||||
echo -e "${GREEN}连接到ClickHouse: ${CLICKHOUSE_HOST}:${CLICKHOUSE_PORT}${NC}"
|
||||
|
||||
# 检查link_events表结构
|
||||
echo -e "${GREEN}检查link_events表结构:${NC}"
|
||||
curl -s "http://${CLICKHOUSE_HOST}:${CLICKHOUSE_PORT}/?user=${CLICKHOUSE_USER}&password=${CLICKHOUSE_PASSWORD}" \
|
||||
-d "DESCRIBE TABLE ${CLICKHOUSE_DATABASE}.link_events"
|
||||
|
||||
# 查询一行数据样本
|
||||
echo -e "\n${GREEN}查询link_events表样本数据:${NC}"
|
||||
curl -s "http://${CLICKHOUSE_HOST}:${CLICKHOUSE_PORT}/?user=${CLICKHOUSE_USER}&password=${CLICKHOUSE_PASSWORD}" \
|
||||
-d "SELECT * FROM ${CLICKHOUSE_DATABASE}.link_events LIMIT 1 FORMAT JSON"
|
||||
|
||||
echo -e "\n${YELLOW}检查完成${NC}"
|
||||
212
scripts/db/db-inspector/clickhouse-schema.js
Normal file
212
scripts/db/db-inspector/clickhouse-schema.js
Normal file
@@ -0,0 +1,212 @@
|
||||
// 检查ClickHouse数据库结构的脚本
|
||||
const { createClient } = require('@clickhouse/client');
|
||||
const dotenv = require('dotenv');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
// 加载环境变量
|
||||
dotenv.config({ path: path.resolve(__dirname, '../../../.env') });
|
||||
|
||||
// 定义输出目录
|
||||
const DB_REPORTS_DIR = path.resolve(__dirname, '../db-reports');
|
||||
|
||||
// 获取ClickHouse配置
|
||||
const clickhouseHost = process.env.CLICKHOUSE_HOST || 'localhost';
|
||||
const clickhousePort = process.env.CLICKHOUSE_PORT || '8123';
|
||||
const clickhouseUser = process.env.CLICKHOUSE_USER || 'default';
|
||||
const clickhousePassword = process.env.CLICKHOUSE_PASSWORD || '';
|
||||
const clickhouseDatabase = process.env.CLICKHOUSE_DATABASE || 'default';
|
||||
|
||||
console.log('ClickHouse配置:');
|
||||
console.log(` - 主机: ${clickhouseHost}`);
|
||||
console.log(` - 端口: ${clickhousePort}`);
|
||||
console.log(` - 用户: ${clickhouseUser}`);
|
||||
console.log(` - 数据库: ${clickhouseDatabase}`);
|
||||
|
||||
// 创建ClickHouse客户端 - 使用0.2.10版本的API
|
||||
const client = createClient({
|
||||
url: `http://${clickhouseHost}:${clickhousePort}`,
|
||||
username: clickhouseUser,
|
||||
password: clickhousePassword,
|
||||
database: clickhouseDatabase
|
||||
});
|
||||
|
||||
// 获取所有表
|
||||
async function getAllTables() {
|
||||
console.log('\n获取所有表...');
|
||||
|
||||
try {
|
||||
const query = `
|
||||
SELECT name
|
||||
FROM system.tables
|
||||
WHERE database = '${clickhouseDatabase}'
|
||||
`;
|
||||
|
||||
const resultSet = await client.query({
|
||||
query,
|
||||
format: 'JSONEachRow'
|
||||
});
|
||||
|
||||
const tables = await resultSet.json();
|
||||
|
||||
if (!tables || tables.length === 0) {
|
||||
console.log(`数据库 ${clickhouseDatabase} 中没有找到任何表`);
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log(`数据库 ${clickhouseDatabase} 中找到以下表:`);
|
||||
tables.forEach(table => {
|
||||
console.log(` - ${table.name}`);
|
||||
});
|
||||
|
||||
return tables.map(table => table.name);
|
||||
} catch (error) {
|
||||
console.error('获取所有表时出错:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取表结构
|
||||
async function getTableSchema(tableName) {
|
||||
console.log(`\n获取表 ${tableName} 的结构...`);
|
||||
|
||||
try {
|
||||
const query = `
|
||||
DESCRIBE TABLE ${clickhouseDatabase}.${tableName}
|
||||
`;
|
||||
|
||||
const resultSet = await client.query({
|
||||
query,
|
||||
format: 'JSONEachRow'
|
||||
});
|
||||
|
||||
const columns = await resultSet.json();
|
||||
|
||||
if (!columns || columns.length === 0) {
|
||||
console.log(`表 ${tableName} 不存在或没有列`);
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log(`表 ${tableName} 的列:`);
|
||||
columns.forEach(column => {
|
||||
console.log(` - ${column.name} (${column.type}, ${column.default_type === '' ? '无默认值' : `默认值: ${column.default_expression}`})`);
|
||||
});
|
||||
|
||||
return columns;
|
||||
} catch (error) {
|
||||
console.error(`获取表 ${tableName} 结构时出错:`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取表数据示例
|
||||
async function getTableDataSample(tableName, limit = 5) {
|
||||
console.log(`\n获取表 ${tableName} 的数据示例 (最多 ${limit} 行)...`);
|
||||
|
||||
try {
|
||||
const query = `
|
||||
SELECT *
|
||||
FROM ${clickhouseDatabase}.${tableName}
|
||||
LIMIT ${limit}
|
||||
`;
|
||||
|
||||
const resultSet = await client.query({
|
||||
query,
|
||||
format: 'JSONEachRow'
|
||||
});
|
||||
|
||||
const rows = await resultSet.json();
|
||||
|
||||
if (!rows || rows.length === 0) {
|
||||
console.log(`表 ${tableName} 中没有数据`);
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log(`表 ${tableName} 的数据示例:`);
|
||||
rows.forEach((row, index) => {
|
||||
console.log(` 行 ${index + 1}:`);
|
||||
Object.entries(row).forEach(([key, value]) => {
|
||||
console.log(` ${key}: ${value}`);
|
||||
});
|
||||
});
|
||||
|
||||
return rows;
|
||||
} catch (error) {
|
||||
console.error(`获取表 ${tableName} 数据示例时出错:`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 主函数
|
||||
async function main() {
|
||||
let outputBuffer = '';
|
||||
const originalConsoleLog = console.log;
|
||||
|
||||
// 重定向console.log到buffer和控制台
|
||||
console.log = function() {
|
||||
// 调用原始的console.log
|
||||
originalConsoleLog.apply(console, arguments);
|
||||
|
||||
// 写入到buffer
|
||||
outputBuffer += Array.from(arguments).join(' ') + '\n';
|
||||
};
|
||||
|
||||
try {
|
||||
// 获取所有表
|
||||
const tables = await getAllTables();
|
||||
|
||||
if (!tables) {
|
||||
console.error('无法获取表列表');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('\n所有ClickHouse表:');
|
||||
console.log(tables.join(', '));
|
||||
|
||||
// 获取每个表的结构,但不获取数据示例
|
||||
for (const tableName of tables) {
|
||||
await getTableSchema(tableName);
|
||||
// 移除数据示例检查
|
||||
// await getTableDataSample(tableName);
|
||||
}
|
||||
|
||||
console.log('\nClickHouse数据库结构检查完成');
|
||||
|
||||
// 保存输出到指定目录
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
|
||||
// 确保目录存在
|
||||
if (!fs.existsSync(DB_REPORTS_DIR)) {
|
||||
fs.mkdirSync(DB_REPORTS_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
const outputPath = path.join(DB_REPORTS_DIR, `clickhouse-schema-${timestamp}.log`);
|
||||
fs.writeFileSync(outputPath, outputBuffer);
|
||||
originalConsoleLog(`结果已保存到: ${outputPath}`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('检查ClickHouse数据库结构时出错:', error);
|
||||
} finally {
|
||||
// 恢复原始的console.log
|
||||
console.log = originalConsoleLog;
|
||||
|
||||
// 关闭客户端连接
|
||||
await client.close();
|
||||
}
|
||||
}
|
||||
|
||||
// 导出函数
|
||||
module.exports = {
|
||||
getAllTables,
|
||||
getTableSchema,
|
||||
getTableDataSample,
|
||||
main
|
||||
};
|
||||
|
||||
// 如果直接运行此脚本,则执行main函数
|
||||
if (require.main === module) {
|
||||
main().catch(error => {
|
||||
console.error('运行脚本时出错:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
329
scripts/db/db-inspector/postgres-schema.js
Normal file
329
scripts/db/db-inspector/postgres-schema.js
Normal file
@@ -0,0 +1,329 @@
|
||||
// 检查数据库结构的脚本
|
||||
const { Client } = require('pg');
|
||||
const dotenv = require('dotenv');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
// 加载环境变量
|
||||
dotenv.config({ path: path.resolve(__dirname, '../../../.env') });
|
||||
|
||||
// 获取数据库连接字符串
|
||||
const databaseUrl = process.env.DATABASE_URL;
|
||||
|
||||
if (!databaseUrl) {
|
||||
console.error('缺少数据库连接字符串。请确保.env文件中包含DATABASE_URL');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 定义输出目录
|
||||
const DB_REPORTS_DIR = path.resolve(__dirname, '../db-reports');
|
||||
|
||||
// 连接数据库
|
||||
async function connect() {
|
||||
console.log('使用PostgreSQL连接字符串连接数据库...');
|
||||
|
||||
// 创建PostgreSQL客户端
|
||||
const client = new Client({
|
||||
connectionString: databaseUrl,
|
||||
});
|
||||
|
||||
try {
|
||||
await client.connect();
|
||||
console.log('成功连接到数据库');
|
||||
return client;
|
||||
} catch (error) {
|
||||
console.error('连接数据库失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 断开数据库连接
|
||||
async function disconnect(client) {
|
||||
try {
|
||||
await client.end();
|
||||
console.log('已断开数据库连接');
|
||||
} catch (error) {
|
||||
console.error('断开数据库连接失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 获取所有表
|
||||
async function getAllTables(client) {
|
||||
console.log('\n获取所有表...');
|
||||
|
||||
try {
|
||||
const query = `
|
||||
SELECT table_name
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = 'limq'
|
||||
ORDER BY table_name;
|
||||
`;
|
||||
|
||||
const result = await client.query(query);
|
||||
|
||||
if (!result.rows || result.rows.length === 0) {
|
||||
console.log('没有找到任何表');
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log('找到以下表:');
|
||||
result.rows.forEach(row => {
|
||||
console.log(` - ${row.table_name}`);
|
||||
});
|
||||
|
||||
return result.rows.map(row => row.table_name);
|
||||
} catch (error) {
|
||||
console.error('获取所有表时出错:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取表结构
|
||||
async function getTableSchema(client, tableName) {
|
||||
console.log(`\n获取表 ${tableName} 的结构...`);
|
||||
|
||||
try {
|
||||
// 获取基本列信息
|
||||
const columnsQuery = `
|
||||
SELECT
|
||||
column_name,
|
||||
data_type,
|
||||
is_nullable,
|
||||
column_default,
|
||||
character_maximum_length,
|
||||
numeric_precision,
|
||||
numeric_scale
|
||||
FROM
|
||||
information_schema.columns
|
||||
WHERE
|
||||
table_schema = 'limq' AND
|
||||
table_name = $1
|
||||
ORDER BY
|
||||
ordinal_position;
|
||||
`;
|
||||
|
||||
const columnsResult = await client.query(columnsQuery, [tableName]);
|
||||
|
||||
if (!columnsResult.rows || columnsResult.rows.length === 0) {
|
||||
console.log(`表 ${tableName} 不存在或没有列`);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 获取主键信息
|
||||
const primaryKeyQuery = `
|
||||
SELECT
|
||||
kcu.column_name
|
||||
FROM
|
||||
information_schema.table_constraints tc
|
||||
JOIN information_schema.key_column_usage kcu
|
||||
ON tc.constraint_name = kcu.constraint_name
|
||||
AND tc.table_schema = kcu.table_schema
|
||||
WHERE
|
||||
tc.constraint_type = 'PRIMARY KEY' AND
|
||||
tc.table_schema = 'limq' AND
|
||||
tc.table_name = $1
|
||||
ORDER BY
|
||||
kcu.ordinal_position;
|
||||
`;
|
||||
|
||||
const primaryKeyResult = await client.query(primaryKeyQuery, [tableName]);
|
||||
|
||||
// 获取外键信息
|
||||
const foreignKeysQuery = `
|
||||
SELECT
|
||||
kcu.column_name,
|
||||
ccu.table_name AS foreign_table_name,
|
||||
ccu.column_name AS foreign_column_name
|
||||
FROM
|
||||
information_schema.table_constraints tc
|
||||
JOIN information_schema.key_column_usage kcu
|
||||
ON tc.constraint_name = kcu.constraint_name
|
||||
AND tc.table_schema = kcu.table_schema
|
||||
JOIN information_schema.constraint_column_usage ccu
|
||||
ON tc.constraint_name = ccu.constraint_name
|
||||
AND tc.table_schema = ccu.table_schema
|
||||
WHERE
|
||||
tc.constraint_type = 'FOREIGN KEY' AND
|
||||
tc.table_schema = 'limq' AND
|
||||
tc.table_name = $1;
|
||||
`;
|
||||
|
||||
const foreignKeysResult = await client.query(foreignKeysQuery, [tableName]);
|
||||
|
||||
// 获取索引信息
|
||||
const indexesQuery = `
|
||||
SELECT
|
||||
indexname,
|
||||
indexdef
|
||||
FROM
|
||||
pg_indexes
|
||||
WHERE
|
||||
schemaname = 'public' AND
|
||||
tablename = $1;
|
||||
`;
|
||||
|
||||
const indexesResult = await client.query(indexesQuery, [tableName]);
|
||||
|
||||
// 输出列信息
|
||||
console.log(`表 ${tableName} 的列:`);
|
||||
columnsResult.rows.forEach(column => {
|
||||
console.log(` - ${column.column_name} (${column.data_type}${
|
||||
column.character_maximum_length ? `(${column.character_maximum_length})` :
|
||||
(column.numeric_precision ? `(${column.numeric_precision},${column.numeric_scale})` : '')
|
||||
}, ${column.is_nullable === 'YES' ? '可为空' : '不可为空'}, 默认值: ${column.column_default || 'NULL'})`);
|
||||
});
|
||||
|
||||
// 输出主键信息
|
||||
if (primaryKeyResult.rows.length > 0) {
|
||||
console.log(` 主键: ${primaryKeyResult.rows.map(row => row.column_name).join(', ')}`);
|
||||
} else {
|
||||
console.log(' 主键: 无');
|
||||
}
|
||||
|
||||
// 输出外键信息
|
||||
if (foreignKeysResult.rows.length > 0) {
|
||||
console.log(' 外键:');
|
||||
foreignKeysResult.rows.forEach(fk => {
|
||||
console.log(` - ${fk.column_name} -> ${fk.foreign_table_name}.${fk.foreign_column_name}`);
|
||||
});
|
||||
} else {
|
||||
console.log(' 外键: 无');
|
||||
}
|
||||
|
||||
// 输出索引信息
|
||||
if (indexesResult.rows.length > 0) {
|
||||
console.log(' 索引:');
|
||||
indexesResult.rows.forEach(idx => {
|
||||
console.log(` - ${idx.indexname}: ${idx.indexdef}`);
|
||||
});
|
||||
} else {
|
||||
console.log(' 索引: 无');
|
||||
}
|
||||
|
||||
return {
|
||||
columns: columnsResult.rows,
|
||||
primaryKey: primaryKeyResult.rows,
|
||||
foreignKeys: foreignKeysResult.rows,
|
||||
indexes: indexesResult.rows
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(`获取表 ${tableName} 结构时出错:`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取表数据示例
|
||||
async function getTableDataSample(client, tableName, limit = 5) {
|
||||
console.log(`\n获取表 ${tableName} 的数据示例 (最多 ${limit} 行)...`);
|
||||
|
||||
try {
|
||||
const query = `
|
||||
SELECT *
|
||||
FROM "${tableName}"
|
||||
LIMIT $1;
|
||||
`;
|
||||
|
||||
const result = await client.query(query, [limit]);
|
||||
|
||||
if (!result.rows || result.rows.length === 0) {
|
||||
console.log(`表 ${tableName} 中没有数据`);
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log(`表 ${tableName} 的数据示例:`);
|
||||
result.rows.forEach((row, index) => {
|
||||
console.log(` 行 ${index + 1}:`);
|
||||
Object.entries(row).forEach(([key, value]) => {
|
||||
console.log(` ${key}: ${value}`);
|
||||
});
|
||||
});
|
||||
|
||||
return result.rows;
|
||||
} catch (error) {
|
||||
console.error(`获取表 ${tableName} 数据示例时出错:`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 主函数
|
||||
async function main() {
|
||||
let client = null;
|
||||
let outputBuffer = '';
|
||||
const originalConsoleLog = console.log;
|
||||
|
||||
// 重定向console.log到buffer和控制台
|
||||
console.log = function() {
|
||||
// 调用原始的console.log
|
||||
originalConsoleLog.apply(console, arguments);
|
||||
|
||||
// 写入到buffer
|
||||
outputBuffer += Array.from(arguments).join(' ') + '\n';
|
||||
};
|
||||
|
||||
try {
|
||||
// 连接数据库
|
||||
client = await connect();
|
||||
|
||||
// 获取所有表
|
||||
const tables = await getAllTables(client);
|
||||
|
||||
if (!tables) {
|
||||
console.error('无法获取表列表');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('\n所有PostgreSQL表:');
|
||||
console.log(tables.join(', '));
|
||||
|
||||
// 获取所有表的结构,而不只是特定表
|
||||
for (const tableName of tables) {
|
||||
await getTableSchema(client, tableName);
|
||||
// 移除数据示例检查
|
||||
// await getTableDataSample(client, tableName);
|
||||
}
|
||||
|
||||
console.log('\n数据库结构检查完成');
|
||||
|
||||
// 保存输出到指定目录
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
|
||||
// 确保目录存在
|
||||
if (!fs.existsSync(DB_REPORTS_DIR)) {
|
||||
fs.mkdirSync(DB_REPORTS_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
const outputPath = path.join(DB_REPORTS_DIR, `postgres-schema-${timestamp}.log`);
|
||||
fs.writeFileSync(outputPath, outputBuffer);
|
||||
originalConsoleLog(`结果已保存到: ${outputPath}`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('检查数据库结构时出错:', error);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
// 恢复原始的console.log
|
||||
console.log = originalConsoleLog;
|
||||
|
||||
// 关闭数据库连接
|
||||
if (client) {
|
||||
await disconnect(client);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 导出函数
|
||||
module.exports = {
|
||||
connect,
|
||||
disconnect,
|
||||
getAllTables,
|
||||
getTableSchema,
|
||||
getTableDataSample,
|
||||
main
|
||||
};
|
||||
|
||||
// 如果直接运行此脚本,则执行main函数
|
||||
if (require.main === module) {
|
||||
main().catch(error => {
|
||||
console.error('运行脚本时出错:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
102
scripts/db/db-inspector/run-all.js
Normal file
102
scripts/db/db-inspector/run-all.js
Normal file
@@ -0,0 +1,102 @@
|
||||
// 一键运行所有数据库检查脚本
|
||||
const { exec } = require('child_process');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
// 定义脚本路径
|
||||
const postgresScriptPath = path.join(__dirname, 'postgres-schema.js');
|
||||
const clickhouseScriptPath = path.join(__dirname, 'clickhouse-schema.js');
|
||||
|
||||
// 定义输出目录
|
||||
const DB_REPORTS_DIR = path.resolve(__dirname, '../db-reports');
|
||||
|
||||
// 确保目录存在
|
||||
if (!fs.existsSync(DB_REPORTS_DIR)) {
|
||||
fs.mkdirSync(DB_REPORTS_DIR, { recursive: true });
|
||||
console.log(`创建输出目录: ${DB_REPORTS_DIR}`);
|
||||
}
|
||||
|
||||
// 定义日期时间格式化函数,用于生成日志文件名
|
||||
function getTimestampString() {
|
||||
return new Date().toISOString().replace(/[:.]/g, '-');
|
||||
}
|
||||
|
||||
// 运行PostgreSQL脚本
|
||||
async function runPostgresScript() {
|
||||
return new Promise((resolve, reject) => {
|
||||
console.log('\n=======================================');
|
||||
console.log('正在运行PostgreSQL数据库结构检查脚本...');
|
||||
console.log('=======================================\n');
|
||||
|
||||
const process = exec(`node --no-inspect ${postgresScriptPath}`, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
console.error(`PostgreSQL脚本运行出错: ${error.message}`);
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (stderr) {
|
||||
console.error(`PostgreSQL脚本错误: ${stderr}`);
|
||||
}
|
||||
|
||||
console.log(stdout);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 运行ClickHouse脚本
|
||||
async function runClickHouseScript() {
|
||||
return new Promise((resolve, reject) => {
|
||||
console.log('\n=======================================');
|
||||
console.log('正在运行ClickHouse数据库结构检查脚本...');
|
||||
console.log('=======================================\n');
|
||||
|
||||
const process = exec(`node --no-inspect ${clickhouseScriptPath}`, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
console.error(`ClickHouse脚本运行出错: ${error.message}`);
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (stderr) {
|
||||
console.error(`ClickHouse脚本错误: ${stderr}`);
|
||||
}
|
||||
|
||||
console.log(stdout);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 主函数
|
||||
async function main() {
|
||||
try {
|
||||
console.log('开始运行所有数据库结构检查脚本...');
|
||||
console.log(`输出目录: ${DB_REPORTS_DIR}`);
|
||||
console.log(`时间戳: ${getTimestampString()}`);
|
||||
|
||||
// 运行PostgreSQL脚本
|
||||
await runPostgresScript();
|
||||
|
||||
// 运行ClickHouse脚本
|
||||
await runClickHouseScript();
|
||||
|
||||
console.log('\n=======================================');
|
||||
console.log('所有数据库结构检查脚本已完成!');
|
||||
console.log('报告已保存到以下目录:');
|
||||
console.log(DB_REPORTS_DIR);
|
||||
console.log('=======================================');
|
||||
} catch (error) {
|
||||
console.error('运行脚本时出错:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// 执行主函数
|
||||
if (require.main === module) {
|
||||
main().catch(error => {
|
||||
console.error('运行脚本时出错:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
|
||||
获取所有表...
|
||||
数据库 limq 中找到以下表:
|
||||
- .inner_id.5d9e5f95-ad7d-4750-ae56-bffea63e14fb
|
||||
- .inner_id.711eb652-7c90-4f9a-80a0-8979011080cc
|
||||
- .inner_id.abec445d-1704-4482-bc72-66c9eb67ecd1
|
||||
- .inner_id.c1eb844d-7f11-4cfc-8931-c433faaa16b0
|
||||
- .inner_id.f9640e70-5b7f-444c-80de-bc5b25848024
|
||||
- .inner_id.fe81eeba-acc5-4260-ac9a-973c2f9ce1ea
|
||||
- link_daily_stats
|
||||
- link_events
|
||||
- link_hourly_patterns
|
||||
- links
|
||||
- platform_distribution
|
||||
- project_daily_stats
|
||||
- projects
|
||||
- qr_scans
|
||||
- qrcode_daily_stats
|
||||
- qrcodes
|
||||
- sessions
|
||||
- team_daily_stats
|
||||
- team_members
|
||||
- teams
|
||||
|
||||
所有ClickHouse表:
|
||||
.inner_id.5d9e5f95-ad7d-4750-ae56-bffea63e14fb, .inner_id.711eb652-7c90-4f9a-80a0-8979011080cc, .inner_id.abec445d-1704-4482-bc72-66c9eb67ecd1, .inner_id.c1eb844d-7f11-4cfc-8931-c433faaa16b0, .inner_id.f9640e70-5b7f-444c-80de-bc5b25848024, .inner_id.fe81eeba-acc5-4260-ac9a-973c2f9ce1ea, link_daily_stats, link_events, link_hourly_patterns, links, platform_distribution, project_daily_stats, projects, qr_scans, qrcode_daily_stats, qrcodes, sessions, team_daily_stats, team_members, teams
|
||||
|
||||
获取表 .inner_id.5d9e5f95-ad7d-4750-ae56-bffea63e14fb 的结构...
|
||||
|
||||
获取表 .inner_id.711eb652-7c90-4f9a-80a0-8979011080cc 的结构...
|
||||
|
||||
获取表 .inner_id.abec445d-1704-4482-bc72-66c9eb67ecd1 的结构...
|
||||
|
||||
获取表 .inner_id.c1eb844d-7f11-4cfc-8931-c433faaa16b0 的结构...
|
||||
|
||||
获取表 .inner_id.f9640e70-5b7f-444c-80de-bc5b25848024 的结构...
|
||||
|
||||
获取表 .inner_id.fe81eeba-acc5-4260-ac9a-973c2f9ce1ea 的结构...
|
||||
|
||||
获取表 link_daily_stats 的结构...
|
||||
表 link_daily_stats 的列:
|
||||
- date (Date, 无默认值)
|
||||
- link_id (String, 无默认值)
|
||||
- total_clicks (UInt64, 无默认值)
|
||||
- unique_visitors (UInt64, 无默认值)
|
||||
- unique_sessions (UInt64, 无默认值)
|
||||
- total_time_spent (UInt64, 无默认值)
|
||||
- avg_time_spent (Float64, 无默认值)
|
||||
- bounce_count (UInt64, 无默认值)
|
||||
- conversion_count (UInt64, 无默认值)
|
||||
- unique_referrers (UInt64, 无默认值)
|
||||
- mobile_count (UInt64, 无默认值)
|
||||
- tablet_count (UInt64, 无默认值)
|
||||
- desktop_count (UInt64, 无默认值)
|
||||
- qr_scan_count (UInt64, 无默认值)
|
||||
- total_conversion_value (Float64, 无默认值)
|
||||
|
||||
获取表 link_events 的结构...
|
||||
表 link_events 的列:
|
||||
- event_id (UUID, 默认值: generateUUIDv4())
|
||||
- event_time (DateTime64(3), 默认值: now64())
|
||||
- date (Date, 默认值: toDate(event_time))
|
||||
- link_id (String, 无默认值)
|
||||
- channel_id (String, 无默认值)
|
||||
- visitor_id (String, 无默认值)
|
||||
- session_id (String, 无默认值)
|
||||
- event_type (Enum8('click' = 1, 'redirect' = 2, 'conversion' = 3, 'error' = 4), 无默认值)
|
||||
- ip_address (String, 无默认值)
|
||||
- country (String, 无默认值)
|
||||
- city (String, 无默认值)
|
||||
- referrer (String, 无默认值)
|
||||
- utm_source (String, 无默认值)
|
||||
- utm_medium (String, 无默认值)
|
||||
- utm_campaign (String, 无默认值)
|
||||
- user_agent (String, 无默认值)
|
||||
- device_type (Enum8('mobile' = 1, 'tablet' = 2, 'desktop' = 3, 'other' = 4), 无默认值)
|
||||
- browser (String, 无默认值)
|
||||
- os (String, 无默认值)
|
||||
- time_spent_sec (UInt32, 默认值: 0)
|
||||
- is_bounce (Bool, 默认值: true)
|
||||
- is_qr_scan (Bool, 默认值: false)
|
||||
- qr_code_id (String, 默认值: '')
|
||||
- conversion_type (Enum8('visit' = 1, 'stay' = 2, 'interact' = 3, 'signup' = 4, 'subscription' = 5, 'purchase' = 6), 默认值: 'visit')
|
||||
- conversion_value (Float64, 默认值: 0)
|
||||
- custom_data (String, 默认值: '{}')
|
||||
|
||||
获取表 link_hourly_patterns 的结构...
|
||||
表 link_hourly_patterns 的列:
|
||||
- date (Date, 无默认值)
|
||||
- hour (UInt8, 无默认值)
|
||||
- link_id (String, 无默认值)
|
||||
- visits (UInt64, 无默认值)
|
||||
- unique_visitors (UInt64, 无默认值)
|
||||
|
||||
获取表 links 的结构...
|
||||
表 links 的列:
|
||||
- link_id (String, 无默认值)
|
||||
- original_url (String, 无默认值)
|
||||
- created_at (DateTime64(3), 无默认值)
|
||||
- created_by (String, 无默认值)
|
||||
- title (String, 无默认值)
|
||||
- description (String, 无默认值)
|
||||
- tags (Array(String), 无默认值)
|
||||
- is_active (Bool, 默认值: true)
|
||||
- expires_at (Nullable(DateTime), 无默认值)
|
||||
- team_id (String, 默认值: '')
|
||||
- project_id (String, 默认值: '')
|
||||
|
||||
获取表 platform_distribution 的结构...
|
||||
表 platform_distribution 的列:
|
||||
- date (Date, 无默认值)
|
||||
- utm_source (String, 无默认值)
|
||||
- device_type (Enum8('mobile' = 1, 'tablet' = 2, 'desktop' = 3, 'other' = 4), 无默认值)
|
||||
- visits (UInt64, 无默认值)
|
||||
- unique_visitors (UInt64, 无默认值)
|
||||
|
||||
获取表 project_daily_stats 的结构...
|
||||
表 project_daily_stats 的列:
|
||||
- date (Date, 无默认值)
|
||||
- project_id (String, 无默认值)
|
||||
- total_clicks (UInt64, 无默认值)
|
||||
- unique_visitors (UInt64, 无默认值)
|
||||
- conversion_count (UInt64, 无默认值)
|
||||
- links_used (UInt64, 无默认值)
|
||||
- qr_scan_count (UInt64, 无默认值)
|
||||
|
||||
获取表 projects 的结构...
|
||||
表 projects 的列:
|
||||
- project_id (String, 无默认值)
|
||||
- team_id (String, 无默认值)
|
||||
- name (String, 无默认值)
|
||||
- created_at (DateTime, 无默认值)
|
||||
- created_by (String, 无默认值)
|
||||
- description (String, 默认值: '')
|
||||
- is_archived (Bool, 默认值: false)
|
||||
- links_count (UInt32, 默认值: 0)
|
||||
- total_clicks (UInt64, 默认值: 0)
|
||||
- last_updated (DateTime, 默认值: now())
|
||||
|
||||
获取表 qr_scans 的结构...
|
||||
表 qr_scans 的列:
|
||||
- scan_id (UUID, 默认值: generateUUIDv4())
|
||||
- qr_code_id (String, 无默认值)
|
||||
- link_id (String, 无默认值)
|
||||
- scan_time (DateTime64(3), 无默认值)
|
||||
- visitor_id (String, 无默认值)
|
||||
- location (String, 无默认值)
|
||||
- device_type (Enum8('mobile' = 1, 'tablet' = 2, 'desktop' = 3, 'other' = 4), 无默认值)
|
||||
- led_to_conversion (Bool, 默认值: false)
|
||||
|
||||
获取表 qrcode_daily_stats 的结构...
|
||||
表 qrcode_daily_stats 的列:
|
||||
- date (Date, 无默认值)
|
||||
- qr_code_id (String, 无默认值)
|
||||
- total_scans (UInt64, 无默认值)
|
||||
- unique_scanners (UInt64, 无默认值)
|
||||
- conversions (UInt64, 无默认值)
|
||||
- mobile_scans (UInt64, 无默认值)
|
||||
- tablet_scans (UInt64, 无默认值)
|
||||
- desktop_scans (UInt64, 无默认值)
|
||||
- unique_locations (UInt64, 无默认值)
|
||||
|
||||
获取表 qrcodes 的结构...
|
||||
表 qrcodes 的列:
|
||||
- qr_code_id (String, 无默认值)
|
||||
- link_id (String, 无默认值)
|
||||
- team_id (String, 无默认值)
|
||||
- project_id (String, 默认值: '')
|
||||
- name (String, 无默认值)
|
||||
- description (String, 默认值: '')
|
||||
- created_at (DateTime, 无默认值)
|
||||
- created_by (String, 无默认值)
|
||||
- updated_at (DateTime, 默认值: now())
|
||||
- qr_type (Enum8('standard' = 1, 'custom' = 2, 'dynamic' = 3), 默认值: 'standard')
|
||||
- image_url (String, 默认值: '')
|
||||
- design_config (String, 默认值: '{}')
|
||||
- is_active (Bool, 默认值: true)
|
||||
- total_scans (UInt64, 默认值: 0)
|
||||
- unique_scanners (UInt32, 默认值: 0)
|
||||
|
||||
获取表 sessions 的结构...
|
||||
表 sessions 的列:
|
||||
- session_id (String, 无默认值)
|
||||
- visitor_id (String, 无默认值)
|
||||
- link_id (String, 无默认值)
|
||||
- started_at (DateTime64(3), 无默认值)
|
||||
- last_activity (DateTime64(3), 无默认值)
|
||||
- ended_at (Nullable(DateTime64(3)), 无默认值)
|
||||
- duration_sec (UInt32, 默认值: 0)
|
||||
- session_pages (UInt8, 默认值: 1)
|
||||
- is_completed (Bool, 默认值: false)
|
||||
|
||||
获取表 team_daily_stats 的结构...
|
||||
表 team_daily_stats 的列:
|
||||
- date (Date, 无默认值)
|
||||
- team_id (String, 无默认值)
|
||||
- total_clicks (UInt64, 无默认值)
|
||||
- unique_visitors (UInt64, 无默认值)
|
||||
- conversion_count (UInt64, 无默认值)
|
||||
- links_used (UInt64, 无默认值)
|
||||
- qr_scan_count (UInt64, 无默认值)
|
||||
|
||||
获取表 team_members 的结构...
|
||||
表 team_members 的列:
|
||||
- team_id (String, 无默认值)
|
||||
- user_id (String, 无默认值)
|
||||
- role (Enum8('owner' = 1, 'admin' = 2, 'editor' = 3, 'viewer' = 4), 无默认值)
|
||||
- joined_at (DateTime, 默认值: now())
|
||||
- invited_by (String, 无默认值)
|
||||
- is_active (Bool, 默认值: true)
|
||||
- last_active (DateTime, 默认值: now())
|
||||
|
||||
获取表 teams 的结构...
|
||||
表 teams 的列:
|
||||
- team_id (String, 无默认值)
|
||||
- name (String, 无默认值)
|
||||
- created_at (DateTime, 无默认值)
|
||||
- created_by (String, 无默认值)
|
||||
- description (String, 默认值: '')
|
||||
- avatar_url (String, 默认值: '')
|
||||
- is_active (Bool, 默认值: true)
|
||||
- plan_type (Enum8('free' = 1, 'pro' = 2, 'enterprise' = 3), 无默认值)
|
||||
- members_count (UInt32, 默认值: 1)
|
||||
|
||||
ClickHouse数据库结构检查完成
|
||||
@@ -0,0 +1,483 @@
|
||||
使用PostgreSQL连接字符串连接数据库...
|
||||
成功连接到数据库
|
||||
|
||||
获取所有表...
|
||||
找到以下表:
|
||||
- ProjectTeams
|
||||
- attribute_schemas
|
||||
- channel
|
||||
- channel_tag
|
||||
- favorite
|
||||
- form_field_metadata
|
||||
- google_token
|
||||
- materials
|
||||
- permission_resources
|
||||
- permissions
|
||||
- platform_tokens
|
||||
- project_resources
|
||||
- projects
|
||||
- qr_code
|
||||
- queue_tasks
|
||||
- resource_tags
|
||||
- resources
|
||||
- roles
|
||||
- slide_presentations
|
||||
- subscription
|
||||
- sync_resource_typesense_queue
|
||||
- tags
|
||||
- team_invitation
|
||||
- team_join_request
|
||||
- team_membership
|
||||
- team_projects
|
||||
- teams
|
||||
- type_order
|
||||
- user_projects
|
||||
- users
|
||||
|
||||
所有PostgreSQL表:
|
||||
ProjectTeams, attribute_schemas, channel, channel_tag, favorite, form_field_metadata, google_token, materials, permission_resources, permissions, platform_tokens, project_resources, projects, qr_code, queue_tasks, resource_tags, resources, roles, slide_presentations, subscription, sync_resource_typesense_queue, tags, team_invitation, team_join_request, team_membership, team_projects, teams, type_order, user_projects, users
|
||||
|
||||
获取表 ProjectTeams 的结构...
|
||||
表 ProjectTeams 的列:
|
||||
- A (uuid, 不可为空, 默认值: NULL)
|
||||
- B (uuid, 不可为空, 默认值: NULL)
|
||||
主键: 无
|
||||
外键:
|
||||
- A -> projects.id
|
||||
索引: 无
|
||||
|
||||
获取表 attribute_schemas 的结构...
|
||||
表 attribute_schemas 的列:
|
||||
- version (integer(32,0), 可为空, 默认值: NULL)
|
||||
- id (uuid, 不可为空, 默认值: gen_random_uuid())
|
||||
- entity_type (text, 可为空, 默认值: NULL)
|
||||
- schema (jsonb, 可为空, 默认值: NULL)
|
||||
- updated_at (timestamp with time zone, 可为空, 默认值: CURRENT_TIMESTAMP)
|
||||
- created_at (timestamp with time zone, 可为空, 默认值: CURRENT_TIMESTAMP)
|
||||
- is_active (boolean, 可为空, 默认值: NULL)
|
||||
主键: id
|
||||
外键: 无
|
||||
索引: 无
|
||||
|
||||
获取表 channel 的结构...
|
||||
表 channel 的列:
|
||||
- name (text, 不可为空, 默认值: NULL)
|
||||
- path (text, 不可为空, 默认值: NULL)
|
||||
- shortUrlId (uuid, 可为空, 默认值: NULL)
|
||||
- qrCodeId (uuid, 可为空, 默认值: NULL)
|
||||
- id (uuid, 不可为空, 默认值: gen_random_uuid())
|
||||
- isUserCreated (boolean, 可为空, 默认值: false)
|
||||
主键: id
|
||||
外键:
|
||||
- qrCodeId -> qr_code.id
|
||||
- shortUrlId -> resources.id
|
||||
索引: 无
|
||||
|
||||
获取表 channel_tag 的结构...
|
||||
表 channel_tag 的列:
|
||||
- channelId (uuid, 不可为空, 默认值: NULL)
|
||||
- tagId (uuid, 不可为空, 默认值: NULL)
|
||||
- id (uuid, 不可为空, 默认值: gen_random_uuid())
|
||||
主键: id
|
||||
外键:
|
||||
- channelId -> channel.id
|
||||
- tagId -> tags.id
|
||||
索引: 无
|
||||
|
||||
获取表 favorite 的结构...
|
||||
表 favorite 的列:
|
||||
- id (uuid, 不可为空, 默认值: gen_random_uuid())
|
||||
- user_id (uuid, 不可为空, 默认值: NULL)
|
||||
- favoritable_id (uuid, 不可为空, 默认值: NULL)
|
||||
- favoritable_type (text, 不可为空, 默认值: NULL)
|
||||
- created_at (timestamp with time zone, 可为空, 默认值: CURRENT_TIMESTAMP)
|
||||
- updated_at (timestamp with time zone, 可为空, 默认值: CURRENT_TIMESTAMP)
|
||||
- deleted_at (timestamp with time zone, 可为空, 默认值: NULL)
|
||||
主键: id
|
||||
外键: 无
|
||||
索引: 无
|
||||
|
||||
获取表 form_field_metadata 的结构...
|
||||
表 form_field_metadata 的列:
|
||||
- field_name (text, 可为空, 默认值: NULL)
|
||||
- resource_type (text, 可为空, 默认值: NULL)
|
||||
- id (uuid, 不可为空, 默认值: gen_random_uuid())
|
||||
- default_value (jsonb, 可为空, 默认值: NULL)
|
||||
- options (jsonb, 可为空, 默认值: NULL)
|
||||
- validation_rules (jsonb, 可为空, 默认值: NULL)
|
||||
- is_required (boolean, 可为空, 默认值: NULL)
|
||||
- order_index (integer(32,0), 可为空, 默认值: NULL)
|
||||
- is_visible (boolean, 可为空, 默认值: NULL)
|
||||
- created_at (timestamp with time zone, 可为空, 默认值: CURRENT_TIMESTAMP)
|
||||
- updated_at (timestamp with time zone, 可为空, 默认值: CURRENT_TIMESTAMP)
|
||||
- placeholder (text, 可为空, 默认值: NULL)
|
||||
- label (text, 可为空, 默认值: NULL)
|
||||
- field_type (text, 可为空, 默认值: NULL)
|
||||
主键: id
|
||||
外键: 无
|
||||
索引: 无
|
||||
|
||||
获取表 google_token 的结构...
|
||||
表 google_token 的列:
|
||||
- id (text, 不可为空, 默认值: NULL)
|
||||
- googleEmail (text, 不可为空, 默认值: NULL)
|
||||
- created_at (timestamp without time zone, 不可为空, 默认值: CURRENT_TIMESTAMP)
|
||||
- updated_at (timestamp without time zone, 不可为空, 默认值: CURRENT_TIMESTAMP)
|
||||
- profile_data (jsonb, 可为空, 默认值: NULL)
|
||||
- tokens (jsonb, 不可为空, 默认值: NULL)
|
||||
- user_id (uuid, 不可为空, 默认值: NULL)
|
||||
主键: id
|
||||
外键:
|
||||
- user_id -> users.id
|
||||
索引: 无
|
||||
|
||||
获取表 materials 的结构...
|
||||
表 materials 的列:
|
||||
- id (uuid, 不可为空, 默认值: gen_random_uuid())
|
||||
- name (text, 不可为空, 默认值: NULL)
|
||||
- description (text, 可为空, 默认值: ''::text)
|
||||
- attributes (jsonb, 可为空, 默认值: '{}'::jsonb)
|
||||
- created_at (timestamp with time zone, 可为空, 默认值: CURRENT_TIMESTAMP)
|
||||
- updated_at (timestamp with time zone, 可为空, 默认值: CURRENT_TIMESTAMP)
|
||||
- deleted_at (timestamp with time zone, 可为空, 默认值: NULL)
|
||||
- schema_version (integer(32,0), 可为空, 默认值: 1)
|
||||
- type (text, 不可为空, 默认值: NULL)
|
||||
- is_active (boolean, 可为空, 默认值: true)
|
||||
- is_system (boolean, 可为空, 默认值: false)
|
||||
- team_id (uuid, 可为空, 默认值: NULL)
|
||||
- creator_id (uuid, 可为空, 默认值: NULL)
|
||||
主键: id
|
||||
外键: 无
|
||||
索引: 无
|
||||
|
||||
获取表 permission_resources 的结构...
|
||||
表 permission_resources 的列:
|
||||
- id (uuid, 不可为空, 默认值: gen_random_uuid())
|
||||
- created_at (timestamp with time zone, 可为空, 默认值: now())
|
||||
- updated_at (timestamp with time zone, 可为空, 默认值: now())
|
||||
- resource_type (USER-DEFINED, 可为空, 默认值: NULL)
|
||||
- resource_name (text, 不可为空, 默认值: NULL)
|
||||
- attributes_type (text, 可为空, 默认值: NULL)
|
||||
- description (text, 可为空, 默认值: NULL)
|
||||
主键: id
|
||||
外键: 无
|
||||
索引: 无
|
||||
|
||||
获取表 permissions 的结构...
|
||||
表 permissions 的列:
|
||||
- id (uuid, 不可为空, 默认值: gen_random_uuid())
|
||||
- created_at (timestamp with time zone, 可为空, 默认值: now())
|
||||
- updated_at (timestamp with time zone, 可为空, 默认值: now())
|
||||
- role_id (uuid, 不可为空, 默认值: NULL)
|
||||
- resource_id (uuid, 不可为空, 默认值: NULL)
|
||||
- action (USER-DEFINED, 不可为空, 默认值: NULL)
|
||||
- metadata (jsonb, 可为空, 默认值: NULL)
|
||||
主键: id
|
||||
外键:
|
||||
- resource_id -> permission_resources.id
|
||||
- role_id -> roles.id
|
||||
索引: 无
|
||||
|
||||
获取表 platform_tokens 的结构...
|
||||
表 platform_tokens 的列:
|
||||
- id (uuid, 不可为空, 默认值: gen_random_uuid())
|
||||
- user_id (uuid, 不可为空, 默认值: NULL)
|
||||
- platform (text, 不可为空, 默认值: NULL)
|
||||
- access_token (text, 不可为空, 默认值: NULL)
|
||||
- refresh_token (text, 可为空, 默认值: NULL)
|
||||
- expires_at (timestamp with time zone, 可为空, 默认值: NULL)
|
||||
- token_type (text, 可为空, 默认值: NULL)
|
||||
- scope (text, 可为空, 默认值: NULL)
|
||||
- created_at (timestamp with time zone, 不可为空, 默认值: CURRENT_TIMESTAMP)
|
||||
- updated_at (timestamp with time zone, 不可为空, 默认值: CURRENT_TIMESTAMP)
|
||||
- metadata (jsonb, 可为空, 默认值: NULL)
|
||||
主键: id
|
||||
外键:
|
||||
- user_id -> users.id
|
||||
索引: 无
|
||||
|
||||
获取表 project_resources 的结构...
|
||||
表 project_resources 的列:
|
||||
- project_id (uuid, 不可为空, 默认值: NULL)
|
||||
- resource_id (uuid, 不可为空, 默认值: NULL)
|
||||
- assigned_at (timestamp with time zone, 可为空, 默认值: CURRENT_TIMESTAMP)
|
||||
主键: project_id, resource_id
|
||||
外键:
|
||||
- project_id -> projects.id
|
||||
- resource_id -> resources.id
|
||||
索引: 无
|
||||
|
||||
获取表 projects 的结构...
|
||||
表 projects 的列:
|
||||
- id (uuid, 不可为空, 默认值: gen_random_uuid())
|
||||
- name (text, 不可为空, 默认值: NULL)
|
||||
- description (text, 可为空, 默认值: NULL)
|
||||
- attributes (jsonb, 可为空, 默认值: NULL)
|
||||
- created_at (timestamp with time zone, 可为空, 默认值: CURRENT_TIMESTAMP)
|
||||
- updated_at (timestamp with time zone, 可为空, 默认值: CURRENT_TIMESTAMP)
|
||||
- deleted_at (timestamp with time zone, 可为空, 默认值: NULL)
|
||||
- schema_version (integer(32,0), 可为空, 默认值: NULL)
|
||||
- creator_id (uuid, 可为空, 默认值: NULL)
|
||||
主键: id
|
||||
外键: 无
|
||||
索引: 无
|
||||
|
||||
获取表 qr_code 的结构...
|
||||
表 qr_code 的列:
|
||||
- scan_count (integer(32,0), 不可为空, 默认值: 0)
|
||||
- url (text, 不可为空, 默认值: NULL)
|
||||
- options (jsonb, 可为空, 默认值: NULL)
|
||||
- template_id (text, 可为空, 默认值: NULL)
|
||||
- msg_template_id (text, 可为空, 默认值: NULL)
|
||||
- template_name (text, 可为空, 默认值: NULL)
|
||||
- attributes (jsonb, 可为空, 默认值: NULL)
|
||||
- created_at (timestamp without time zone, 不可为空, 默认值: CURRENT_TIMESTAMP)
|
||||
- updated_at (timestamp without time zone, 不可为空, 默认值: NULL)
|
||||
- deleted_at (timestamp without time zone, 可为空, 默认值: NULL)
|
||||
- resource_id (uuid, 可为空, 默认值: NULL)
|
||||
- id (uuid, 不可为空, 默认值: gen_random_uuid())
|
||||
主键: id
|
||||
外键:
|
||||
- resource_id -> resources.id
|
||||
索引: 无
|
||||
|
||||
获取表 queue_tasks 的结构...
|
||||
表 queue_tasks 的列:
|
||||
- id (uuid, 不可为空, 默认值: gen_random_uuid())
|
||||
- task_id (text, 不可为空, 默认值: NULL)
|
||||
- type (text, 不可为空, 默认值: NULL)
|
||||
- status (text, 不可为空, 默认值: NULL)
|
||||
- data (jsonb, 可为空, 默认值: NULL)
|
||||
- result (jsonb, 可为空, 默认值: NULL)
|
||||
- error (text, 可为空, 默认值: NULL)
|
||||
- created_at (timestamp with time zone, 不可为空, 默认值: CURRENT_TIMESTAMP)
|
||||
- updated_at (timestamp with time zone, 不可为空, 默认值: CURRENT_TIMESTAMP)
|
||||
- started_at (timestamp with time zone, 可为空, 默认值: NULL)
|
||||
- finished_at (timestamp with time zone, 可为空, 默认值: NULL)
|
||||
- creator_id (uuid, 可为空, 默认值: NULL)
|
||||
主键: id
|
||||
外键:
|
||||
- creator_id -> users.id
|
||||
索引: 无
|
||||
|
||||
获取表 resource_tags 的结构...
|
||||
表 resource_tags 的列:
|
||||
- id (uuid, 不可为空, 默认值: gen_random_uuid())
|
||||
- tag_id (uuid, 不可为空, 默认值: NULL)
|
||||
- created_at (timestamp with time zone, 可为空, 默认值: CURRENT_TIMESTAMP)
|
||||
- resource_id (uuid, 不可为空, 默认值: NULL)
|
||||
主键: id
|
||||
外键:
|
||||
- resource_id -> resources.id
|
||||
- tag_id -> tags.id
|
||||
索引: 无
|
||||
|
||||
获取表 resources 的结构...
|
||||
表 resources 的列:
|
||||
- created_at (timestamp with time zone, 可为空, 默认值: CURRENT_TIMESTAMP)
|
||||
- updated_at (timestamp with time zone, 可为空, 默认值: CURRENT_TIMESTAMP)
|
||||
- deleted_at (timestamp with time zone, 可为空, 默认值: NULL)
|
||||
- schema_version (integer(32,0), 可为空, 默认值: NULL)
|
||||
- id (uuid, 不可为空, 默认值: gen_random_uuid())
|
||||
- external_id (text, 可为空, 默认值: NULL)
|
||||
- attributes (jsonb, 可为空, 默认值: NULL)
|
||||
- type (text, 可为空, 默认值: NULL)
|
||||
- creator_id (uuid, 可为空, 默认值: NULL)
|
||||
主键: id
|
||||
外键:
|
||||
- creator_id -> users.id
|
||||
索引: 无
|
||||
|
||||
获取表 roles 的结构...
|
||||
表 roles 的列:
|
||||
- id (uuid, 不可为空, 默认值: gen_random_uuid())
|
||||
- created_at (timestamp with time zone, 可为空, 默认值: now())
|
||||
- updated_at (timestamp with time zone, 可为空, 默认值: now())
|
||||
- name (text, 不可为空, 默认值: NULL)
|
||||
- description (text, 可为空, 默认值: NULL)
|
||||
主键: id
|
||||
外键: 无
|
||||
索引: 无
|
||||
|
||||
获取表 slide_presentations 的结构...
|
||||
表 slide_presentations 的列:
|
||||
- id (uuid, 不可为空, 默认值: gen_random_uuid())
|
||||
- slide_id (text, 不可为空, 默认值: NULL)
|
||||
- user_id (uuid, 不可为空, 默认值: NULL)
|
||||
- content (jsonb, 不可为空, 默认值: NULL)
|
||||
- created_at (timestamp with time zone, 可为空, 默认值: CURRENT_TIMESTAMP)
|
||||
- updated_at (timestamp with time zone, 可为空, 默认值: CURRENT_TIMESTAMP)
|
||||
- deleted_at (timestamp with time zone, 可为空, 默认值: NULL)
|
||||
主键: id
|
||||
外键:
|
||||
- user_id -> users.id
|
||||
索引: 无
|
||||
|
||||
获取表 subscription 的结构...
|
||||
表 subscription 的列:
|
||||
- id (text, 不可为空, 默认值: NULL)
|
||||
- team_id (uuid, 不可为空, 默认值: NULL)
|
||||
- customer_id (text, 不可为空, 默认值: NULL)
|
||||
- status (USER-DEFINED, 不可为空, 默认值: NULL)
|
||||
- plan_id (text, 不可为空, 默认值: NULL)
|
||||
- variant_id (text, 不可为空, 默认值: NULL)
|
||||
- next_payment_date (timestamp without time zone, 可为空, 默认值: NULL)
|
||||
主键: id
|
||||
外键: 无
|
||||
索引: 无
|
||||
|
||||
获取表 sync_resource_typesense_queue 的结构...
|
||||
表 sync_resource_typesense_queue 的列:
|
||||
- id (uuid, 不可为空, 默认值: gen_random_uuid())
|
||||
- resource_id (uuid, 不可为空, 默认值: NULL)
|
||||
- entity_type (text, 不可为空, 默认值: NULL)
|
||||
- action (text, 不可为空, 默认值: NULL)
|
||||
- status (text, 不可为空, 默认值: NULL)
|
||||
- error (text, 可为空, 默认值: NULL)
|
||||
- created_at (timestamp with time zone, 不可为空, 默认值: CURRENT_TIMESTAMP)
|
||||
- updated_at (timestamp with time zone, 不可为空, 默认值: CURRENT_TIMESTAMP)
|
||||
- processed_at (timestamp with time zone, 可为空, 默认值: NULL)
|
||||
主键: id
|
||||
外键:
|
||||
- resource_id -> resources.id
|
||||
索引: 无
|
||||
|
||||
获取表 tags 的结构...
|
||||
表 tags 的列:
|
||||
- id (uuid, 不可为空, 默认值: gen_random_uuid())
|
||||
- name (text, 可为空, 默认值: NULL)
|
||||
- type (text, 可为空, 默认值: NULL)
|
||||
- attributes (jsonb, 可为空, 默认值: NULL)
|
||||
- created_at (timestamp with time zone, 可为空, 默认值: CURRENT_TIMESTAMP)
|
||||
- updated_at (timestamp with time zone, 可为空, 默认值: CURRENT_TIMESTAMP)
|
||||
- deleted_at (timestamp with time zone, 可为空, 默认值: NULL)
|
||||
- parent_tag_id (uuid, 可为空, 默认值: NULL)
|
||||
- team_id (uuid, 可为空, 默认值: NULL)
|
||||
- is_shared (boolean, 可为空, 默认值: false)
|
||||
- schema_version (integer(32,0), 可为空, 默认值: NULL)
|
||||
- is_system (boolean, 可为空, 默认值: false)
|
||||
主键: id
|
||||
外键:
|
||||
- parent_tag_id -> tags.id
|
||||
索引: 无
|
||||
|
||||
获取表 team_invitation 的结构...
|
||||
表 team_invitation 的列:
|
||||
- id (text, 不可为空, 默认值: NULL)
|
||||
- team_id (uuid, 不可为空, 默认值: NULL)
|
||||
- email (text, 可为空, 默认值: NULL)
|
||||
- role (USER-DEFINED, 不可为空, 默认值: 'MEMBER'::limq.team_member_role)
|
||||
- created_at (timestamp without time zone, 不可为空, 默认值: CURRENT_TIMESTAMP)
|
||||
- expires_at (timestamp without time zone, 不可为空, 默认值: NULL)
|
||||
- type (text, 可为空, 默认值: NULL)
|
||||
- deleted_at (timestamp with time zone, 可为空, 默认值: NULL)
|
||||
- status (USER-DEFINED, 可为空, 默认值: NULL)
|
||||
主键: id
|
||||
外键:
|
||||
- team_id -> teams.id
|
||||
索引: 无
|
||||
|
||||
获取表 team_join_request 的结构...
|
||||
表 team_join_request 的列:
|
||||
- id (text, 不可为空, 默认值: NULL)
|
||||
- team_id (uuid, 不可为空, 默认值: NULL)
|
||||
- user_id (uuid, 不可为空, 默认值: NULL)
|
||||
- invitation_id (text, 不可为空, 默认值: NULL)
|
||||
- status (USER-DEFINED, 不可为空, 默认值: 'PENDING'::limq.request_status)
|
||||
- message (text, 可为空, 默认值: NULL)
|
||||
- created_at (timestamp with time zone, 不可为空, 默认值: CURRENT_TIMESTAMP)
|
||||
- updated_at (timestamp with time zone, 不可为空, 默认值: NULL)
|
||||
主键: id
|
||||
外键:
|
||||
- invitation_id -> team_invitation.id
|
||||
- user_id -> users.id
|
||||
索引: 无
|
||||
|
||||
获取表 team_membership 的结构...
|
||||
表 team_membership 的列:
|
||||
- id (text, 不可为空, 默认值: NULL)
|
||||
- team_id (uuid, 不可为空, 默认值: NULL)
|
||||
- user_id (uuid, 不可为空, 默认值: NULL)
|
||||
- is_creator (boolean, 不可为空, 默认值: false)
|
||||
- role (text, 不可为空, 默认值: NULL)
|
||||
主键: id
|
||||
外键:
|
||||
- team_id -> teams.id
|
||||
- user_id -> users.id
|
||||
索引: 无
|
||||
|
||||
获取表 team_projects 的结构...
|
||||
表 team_projects 的列:
|
||||
- team_id (uuid, 不可为空, 默认值: NULL)
|
||||
- project_id (uuid, 不可为空, 默认值: NULL)
|
||||
- assigned_at (timestamp with time zone, 可为空, 默认值: CURRENT_TIMESTAMP)
|
||||
主键: team_id, project_id
|
||||
外键:
|
||||
- project_id -> projects.id
|
||||
- team_id -> teams.id
|
||||
索引: 无
|
||||
|
||||
获取表 teams 的结构...
|
||||
表 teams 的列:
|
||||
- id (uuid, 不可为空, 默认值: gen_random_uuid())
|
||||
- name (text, 不可为空, 默认值: NULL)
|
||||
- description (text, 可为空, 默认值: NULL)
|
||||
- attributes (jsonb, 可为空, 默认值: NULL)
|
||||
- created_at (timestamp with time zone, 可为空, 默认值: CURRENT_TIMESTAMP)
|
||||
- updated_at (timestamp with time zone, 可为空, 默认值: CURRENT_TIMESTAMP)
|
||||
- deleted_at (timestamp with time zone, 可为空, 默认值: NULL)
|
||||
- schema_version (integer(32,0), 可为空, 默认值: NULL)
|
||||
- avatar_url (text, 可为空, 默认值: NULL)
|
||||
主键: id
|
||||
外键: 无
|
||||
索引: 无
|
||||
|
||||
获取表 type_order 的结构...
|
||||
表 type_order 的列:
|
||||
- id (uuid, 不可为空, 默认值: gen_random_uuid())
|
||||
- user_id (uuid, 不可为空, 默认值: NULL)
|
||||
- category (text, 不可为空, 默认值: NULL)
|
||||
- type (text, 不可为空, 默认值: NULL)
|
||||
- order (integer(32,0), 不可为空, 默认值: NULL)
|
||||
- created_at (timestamp with time zone, 可为空, 默认值: CURRENT_TIMESTAMP)
|
||||
- updated_at (timestamp with time zone, 可为空, 默认值: CURRENT_TIMESTAMP)
|
||||
主键: id
|
||||
外键:
|
||||
- user_id -> users.id
|
||||
索引: 无
|
||||
|
||||
获取表 user_projects 的结构...
|
||||
表 user_projects 的列:
|
||||
- user_id (uuid, 不可为空, 默认值: NULL)
|
||||
- project_id (uuid, 不可为空, 默认值: NULL)
|
||||
- role (text, 可为空, 默认值: NULL)
|
||||
- joined_at (timestamp with time zone, 可为空, 默认值: CURRENT_TIMESTAMP)
|
||||
主键: user_id, project_id
|
||||
外键:
|
||||
- project_id -> projects.id
|
||||
- user_id -> users.id
|
||||
索引: 无
|
||||
|
||||
获取表 users 的结构...
|
||||
表 users 的列:
|
||||
- id (uuid, 不可为空, 默认值: gen_random_uuid())
|
||||
- email (text, 不可为空, 默认值: NULL)
|
||||
- password_hash (text, 可为空, 默认值: NULL)
|
||||
- first_name (text, 可为空, 默认值: NULL)
|
||||
- last_name (text, 可为空, 默认值: NULL)
|
||||
- phone_number (text, 可为空, 默认值: NULL)
|
||||
- is_email_verified (boolean, 可为空, 默认值: NULL)
|
||||
- is_phone_verified (boolean, 可为空, 默认值: NULL)
|
||||
- last_login_at (timestamp with time zone, 可为空, 默认值: NULL)
|
||||
- attributes (jsonb, 可为空, 默认值: NULL)
|
||||
- external_id (text, 可为空, 默认值: NULL)
|
||||
- created_at (timestamp with time zone, 可为空, 默认值: CURRENT_TIMESTAMP)
|
||||
- updated_at (timestamp with time zone, 可为空, 默认值: CURRENT_TIMESTAMP)
|
||||
- deleted_at (timestamp with time zone, 可为空, 默认值: NULL)
|
||||
- schema_version (integer(32,0), 可为空, 默认值: NULL)
|
||||
- avatar_url (text, 可为空, 默认值: NULL)
|
||||
主键: id
|
||||
外键: 无
|
||||
索引: 无
|
||||
|
||||
数据库结构检查完成
|
||||
29
scripts/db/load-clickhouse-testdata.sh
Executable file
29
scripts/db/load-clickhouse-testdata.sh
Executable file
@@ -0,0 +1,29 @@
|
||||
#!/bin/bash
|
||||
# 脚本名称: load-clickhouse-testdata.sh
|
||||
# 用途: 将测试数据加载到ClickHouse数据库中
|
||||
|
||||
# 设置脚本目录路径
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
# 设置SQL文件路径
|
||||
SQL_FILE="$SCRIPT_DIR/sql/clickhouse/seed-clickhouse-analytics.sql"
|
||||
|
||||
# 检查SQL文件是否存在
|
||||
if [ ! -f "$SQL_FILE" ]; then
|
||||
echo "错误: SQL文件 '$SQL_FILE' 不存在"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 执行CH查询脚本
|
||||
echo "开始加载测试数据到ClickHouse数据库..."
|
||||
bash "$SCRIPT_DIR/sql/clickhouse/ch-query.sh" -f "$SQL_FILE"
|
||||
|
||||
# 检查执行结果
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "测试数据已成功加载到ClickHouse数据库"
|
||||
else
|
||||
echo "错误: 加载测试数据失败"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
exit 0
|
||||
102
scripts/db/sql/clickhouse/ch-query.sh
Executable file
102
scripts/db/sql/clickhouse/ch-query.sh
Executable file
@@ -0,0 +1,102 @@
|
||||
#!/bin/bash
|
||||
# 文件名: ch-query.sh
|
||||
# 用途: 执行ClickHouse SQL查询的便捷脚本
|
||||
|
||||
# 连接参数
|
||||
CH_HOST="localhost"
|
||||
CH_PORT="9000"
|
||||
CH_USER="admin"
|
||||
CH_PASSWORD="your_secure_password"
|
||||
|
||||
# 基本查询函数
|
||||
function ch_query() {
|
||||
clickhouse client --host $CH_HOST --port $CH_PORT --user $CH_USER --password $CH_PASSWORD -q "$1"
|
||||
}
|
||||
|
||||
# 显示帮助信息
|
||||
function show_help() {
|
||||
echo "ClickHouse 查询工具"
|
||||
echo "用法: $0 [选项] [SQL查询]"
|
||||
echo ""
|
||||
echo "选项:"
|
||||
echo " -t 显示所有表"
|
||||
echo " -d 显示所有数据库"
|
||||
echo " -s <表名> 显示表结构"
|
||||
echo " -p <表名> 显示表样本数据(前10行)"
|
||||
echo " -c <表名> 计算表中的记录数"
|
||||
echo " -h, --help 显示此帮助信息"
|
||||
echo " -q \"SQL查询\" 执行自定义SQL查询"
|
||||
echo " -f <文件名> 执行SQL文件"
|
||||
echo ""
|
||||
echo "示例:"
|
||||
echo " $0 -d # 显示所有数据库"
|
||||
echo " $0 -t # 显示所有表"
|
||||
echo " $0 -s limq.link_events # 显示link_events表结构"
|
||||
echo " $0 -q \"SELECT * FROM limq.link_events LIMIT 5\" # 执行自定义查询"
|
||||
}
|
||||
|
||||
# 没有参数时显示帮助
|
||||
if [ $# -eq 0 ]; then
|
||||
show_help
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# 处理命令行参数
|
||||
case "$1" in
|
||||
-t)
|
||||
ch_query "SHOW TABLES"
|
||||
;;
|
||||
-d)
|
||||
ch_query "SHOW DATABASES"
|
||||
;;
|
||||
-s)
|
||||
if [ -z "$2" ]; then
|
||||
echo "错误: 需要提供表名"
|
||||
exit 1
|
||||
fi
|
||||
ch_query "DESCRIBE TABLE $2"
|
||||
;;
|
||||
-p)
|
||||
if [ -z "$2" ]; then
|
||||
echo "错误: 需要提供表名"
|
||||
exit 1
|
||||
fi
|
||||
ch_query "SELECT * FROM $2 LIMIT 10"
|
||||
;;
|
||||
-c)
|
||||
if [ -z "$2" ]; then
|
||||
echo "错误: 需要提供表名"
|
||||
exit 1
|
||||
fi
|
||||
ch_query "SELECT COUNT(*) FROM $2"
|
||||
;;
|
||||
-q)
|
||||
if [ -z "$2" ]; then
|
||||
echo "错误: 需要提供SQL查询"
|
||||
exit 1
|
||||
fi
|
||||
ch_query "$2"
|
||||
;;
|
||||
-f)
|
||||
if [ -z "$2" ]; then
|
||||
echo "错误: 需要提供SQL文件"
|
||||
exit 1
|
||||
fi
|
||||
if [ ! -f "$2" ]; then
|
||||
echo "错误: 文件 '$2' 不存在"
|
||||
exit 1
|
||||
fi
|
||||
SQL=$(cat "$2")
|
||||
ch_query "$SQL"
|
||||
;;
|
||||
-h|--help)
|
||||
show_help
|
||||
;;
|
||||
*)
|
||||
echo "未知选项: $1"
|
||||
show_help
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
exit 0
|
||||
4
scripts/db/sql/clickhouse/clickhouse.md
Normal file
4
scripts/db/sql/clickhouse/clickhouse.md
Normal file
@@ -0,0 +1,4 @@
|
||||
```bash
|
||||
alias clickhouse-sql='clickhouse client --host localhost --port 9000 --user admin --password your_secure_password --database promote -q'
|
||||
clickhouse-sql "SHOW TABLES"
|
||||
```
|
||||
170
scripts/db/sql/clickhouse/create_limq.sql
Normal file
170
scripts/db/sql/clickhouse/create_limq.sql
Normal file
@@ -0,0 +1,170 @@
|
||||
-- 创建数据库
|
||||
CREATE DATABASE IF NOT EXISTS limq;
|
||||
|
||||
-- 切换到limq数据库
|
||||
USE limq;
|
||||
|
||||
-- 创建短链接访问事件表
|
||||
CREATE TABLE IF NOT EXISTS limq.link_events (
|
||||
event_id UUID DEFAULT generateUUIDv4(),
|
||||
event_time DateTime64(3) DEFAULT now64(),
|
||||
date Date DEFAULT toDate(event_time),
|
||||
link_id String,
|
||||
channel_id String,
|
||||
visitor_id String,
|
||||
session_id String,
|
||||
event_type Enum8(
|
||||
'click' = 1,
|
||||
'redirect' = 2,
|
||||
'conversion' = 3,
|
||||
'error' = 4
|
||||
),
|
||||
-- 访问者信息
|
||||
ip_address String,
|
||||
country String,
|
||||
city String,
|
||||
-- 来源信息
|
||||
referrer String,
|
||||
utm_source String,
|
||||
utm_medium String,
|
||||
utm_campaign String,
|
||||
-- 设备信息
|
||||
user_agent String,
|
||||
device_type Enum8(
|
||||
'mobile' = 1,
|
||||
'tablet' = 2,
|
||||
'desktop' = 3,
|
||||
'other' = 4
|
||||
),
|
||||
browser String,
|
||||
os String,
|
||||
-- 交互信息
|
||||
time_spent_sec UInt32 DEFAULT 0,
|
||||
is_bounce Boolean DEFAULT true,
|
||||
-- QR码相关
|
||||
is_qr_scan Boolean DEFAULT false,
|
||||
qr_code_id String DEFAULT '',
|
||||
-- 转化数据
|
||||
conversion_type String DEFAULT '',
|
||||
conversion_value Float64 DEFAULT 0,
|
||||
-- 其他属性
|
||||
custom_data String DEFAULT '{}'
|
||||
) ENGINE = MergeTree() PARTITION BY toYYYYMM(date)
|
||||
ORDER BY
|
||||
(date, link_id, event_time) SETTINGS index_granularity = 8192;
|
||||
|
||||
-- 短链接维度表
|
||||
CREATE TABLE IF NOT EXISTS limq.links (
|
||||
link_id String,
|
||||
original_url String,
|
||||
created_at DateTime,
|
||||
created_by String,
|
||||
title String,
|
||||
description String,
|
||||
tags Array(String),
|
||||
is_active Boolean DEFAULT true,
|
||||
expires_at Nullable(DateTime),
|
||||
team_id String DEFAULT '',
|
||||
project_id String DEFAULT '',
|
||||
PRIMARY KEY (link_id)
|
||||
) ENGINE = ReplacingMergeTree()
|
||||
ORDER BY
|
||||
link_id SETTINGS index_granularity = 8192;
|
||||
|
||||
-- 会话跟踪表
|
||||
CREATE TABLE IF NOT EXISTS limq.sessions (
|
||||
session_id String,
|
||||
visitor_id String,
|
||||
link_id String,
|
||||
started_at DateTime64(3),
|
||||
last_activity DateTime64(3),
|
||||
ended_at Nullable(DateTime64(3)),
|
||||
duration_sec UInt32 DEFAULT 0,
|
||||
session_pages UInt8 DEFAULT 1,
|
||||
is_completed Boolean DEFAULT false,
|
||||
PRIMARY KEY (session_id)
|
||||
) ENGINE = ReplacingMergeTree(last_activity)
|
||||
ORDER BY
|
||||
(session_id, link_id, visitor_id) SETTINGS index_granularity = 8192;
|
||||
|
||||
-- QR码统计表
|
||||
CREATE TABLE IF NOT EXISTS limq.qr_scans (
|
||||
scan_id UUID DEFAULT generateUUIDv4(),
|
||||
qr_code_id String,
|
||||
link_id String,
|
||||
scan_time DateTime64(3),
|
||||
visitor_id String,
|
||||
location String,
|
||||
device_type Enum8(
|
||||
'mobile' = 1,
|
||||
'tablet' = 2,
|
||||
'desktop' = 3,
|
||||
'other' = 4
|
||||
),
|
||||
led_to_conversion Boolean DEFAULT false,
|
||||
PRIMARY KEY (scan_id)
|
||||
) ENGINE = MergeTree() PARTITION BY toYYYYMM(scan_time)
|
||||
ORDER BY
|
||||
(qr_code_id, scan_time) SETTINGS index_granularity = 8192;
|
||||
|
||||
-- 每日链接汇总视图
|
||||
CREATE MATERIALIZED VIEW limq.link_daily_stats ENGINE = SummingMergeTree() PARTITION BY toYYYYMM(date)
|
||||
ORDER BY
|
||||
(date, link_id) SETTINGS index_granularity = 8192 AS
|
||||
SELECT
|
||||
toDate(event_time) AS date,
|
||||
link_id,
|
||||
count() AS total_clicks,
|
||||
uniqExact(visitor_id) AS unique_visitors,
|
||||
uniqExact(session_id) AS unique_sessions,
|
||||
sum(time_spent_sec) AS total_time_spent,
|
||||
avg(time_spent_sec) AS avg_time_spent,
|
||||
countIf(is_bounce) AS bounce_count,
|
||||
countIf(event_type = 'conversion') AS conversion_count,
|
||||
uniqExact(referrer) AS unique_referrers,
|
||||
countIf(device_type = 'mobile') AS mobile_count,
|
||||
countIf(device_type = 'tablet') AS tablet_count,
|
||||
countIf(device_type = 'desktop') AS desktop_count,
|
||||
countIf(is_qr_scan) AS qr_scan_count,
|
||||
sum(conversion_value) AS total_conversion_value
|
||||
FROM
|
||||
limq.link_events
|
||||
GROUP BY
|
||||
date,
|
||||
link_id;
|
||||
|
||||
-- 每小时访问模式视图
|
||||
CREATE MATERIALIZED VIEW limq.link_hourly_patterns ENGINE = SummingMergeTree() PARTITION BY toYYYYMM(date)
|
||||
ORDER BY
|
||||
(date, hour, link_id) SETTINGS index_granularity = 8192 AS
|
||||
SELECT
|
||||
toDate(event_time) AS date,
|
||||
toHour(event_time) AS hour,
|
||||
link_id,
|
||||
count() AS visits,
|
||||
uniqExact(visitor_id) AS unique_visitors
|
||||
FROM
|
||||
limq.link_events
|
||||
GROUP BY
|
||||
date,
|
||||
hour,
|
||||
link_id;
|
||||
|
||||
-- 平台分布视图
|
||||
CREATE MATERIALIZED VIEW limq.platform_distribution ENGINE = SummingMergeTree() PARTITION BY toYYYYMM(date)
|
||||
ORDER BY
|
||||
(date, utm_source, device_type) SETTINGS index_granularity = 8192 AS
|
||||
SELECT
|
||||
toDate(event_time) AS date,
|
||||
utm_source,
|
||||
device_type,
|
||||
count() AS visits,
|
||||
uniqExact(visitor_id) AS unique_visitors
|
||||
FROM
|
||||
limq.link_events
|
||||
WHERE
|
||||
utm_source != ''
|
||||
GROUP BY
|
||||
date,
|
||||
utm_source,
|
||||
device_type;
|
||||
146
scripts/db/sql/clickhouse/create_team_project_qrcode.sql
Normal file
146
scripts/db/sql/clickhouse/create_team_project_qrcode.sql
Normal file
@@ -0,0 +1,146 @@
|
||||
-- 添加team、project和qrcode表到limq数据库
|
||||
USE limq;
|
||||
|
||||
-- 团队表
|
||||
CREATE TABLE IF NOT EXISTS limq.teams (
|
||||
team_id String,
|
||||
name String,
|
||||
created_at DateTime,
|
||||
created_by String,
|
||||
description String DEFAULT '',
|
||||
avatar_url String DEFAULT '',
|
||||
is_active Boolean DEFAULT true,
|
||||
plan_type Enum8(
|
||||
'free' = 1,
|
||||
'pro' = 2,
|
||||
'enterprise' = 3
|
||||
),
|
||||
members_count UInt32 DEFAULT 1,
|
||||
PRIMARY KEY (team_id)
|
||||
) ENGINE = ReplacingMergeTree()
|
||||
ORDER BY
|
||||
team_id SETTINGS index_granularity = 8192;
|
||||
|
||||
-- 项目表
|
||||
CREATE TABLE IF NOT EXISTS limq.projects (
|
||||
project_id String,
|
||||
team_id String,
|
||||
name String,
|
||||
created_at DateTime,
|
||||
created_by String,
|
||||
description String DEFAULT '',
|
||||
is_archived Boolean DEFAULT false,
|
||||
links_count UInt32 DEFAULT 0,
|
||||
total_clicks UInt64 DEFAULT 0,
|
||||
last_updated DateTime DEFAULT now(),
|
||||
PRIMARY KEY (project_id)
|
||||
) ENGINE = ReplacingMergeTree()
|
||||
ORDER BY
|
||||
(project_id, team_id) SETTINGS index_granularity = 8192;
|
||||
|
||||
-- QR码表 (扩展现有的qr_scans表)
|
||||
CREATE TABLE IF NOT EXISTS limq.qrcodes (
|
||||
qr_code_id String,
|
||||
link_id String,
|
||||
team_id String,
|
||||
project_id String DEFAULT '',
|
||||
name String,
|
||||
description String DEFAULT '',
|
||||
created_at DateTime,
|
||||
created_by String,
|
||||
updated_at DateTime DEFAULT now(),
|
||||
qr_type Enum8(
|
||||
'standard' = 1,
|
||||
'custom' = 2,
|
||||
'dynamic' = 3
|
||||
) DEFAULT 'standard',
|
||||
image_url String DEFAULT '',
|
||||
design_config String DEFAULT '{}',
|
||||
is_active Boolean DEFAULT true,
|
||||
total_scans UInt64 DEFAULT 0,
|
||||
unique_scanners UInt32 DEFAULT 0,
|
||||
PRIMARY KEY (qr_code_id)
|
||||
) ENGINE = ReplacingMergeTree()
|
||||
ORDER BY
|
||||
(qr_code_id, link_id) SETTINGS index_granularity = 8192;
|
||||
|
||||
-- 团队成员表
|
||||
CREATE TABLE IF NOT EXISTS limq.team_members (
|
||||
team_id String,
|
||||
user_id String,
|
||||
role Enum8(
|
||||
'owner' = 1,
|
||||
'admin' = 2,
|
||||
'editor' = 3,
|
||||
'viewer' = 4
|
||||
),
|
||||
joined_at DateTime DEFAULT now(),
|
||||
invited_by String,
|
||||
is_active Boolean DEFAULT true,
|
||||
last_active DateTime DEFAULT now(),
|
||||
PRIMARY KEY (team_id, user_id)
|
||||
) ENGINE = ReplacingMergeTree()
|
||||
ORDER BY
|
||||
(team_id, user_id) SETTINGS index_granularity = 8192;
|
||||
|
||||
-- 团队每日统计视图
|
||||
CREATE MATERIALIZED VIEW limq.team_daily_stats ENGINE = SummingMergeTree() PARTITION BY toYYYYMM(date)
|
||||
ORDER BY
|
||||
(date, team_id) SETTINGS index_granularity = 8192 AS
|
||||
SELECT
|
||||
toDate(event_time) AS date,
|
||||
l.team_id AS team_id,
|
||||
count() AS total_clicks,
|
||||
uniqExact(e.visitor_id) AS unique_visitors,
|
||||
countIf(e.event_type = 'conversion') AS conversion_count,
|
||||
uniqExact(e.link_id) AS links_used,
|
||||
countIf(e.is_qr_scan) AS qr_scan_count
|
||||
FROM
|
||||
limq.link_events e
|
||||
JOIN limq.links l ON e.link_id = l.link_id
|
||||
WHERE
|
||||
l.team_id != ''
|
||||
GROUP BY
|
||||
date,
|
||||
l.team_id;
|
||||
|
||||
-- 项目每日统计视图
|
||||
CREATE MATERIALIZED VIEW limq.project_daily_stats ENGINE = SummingMergeTree() PARTITION BY toYYYYMM(date)
|
||||
ORDER BY
|
||||
(date, project_id) SETTINGS index_granularity = 8192 AS
|
||||
SELECT
|
||||
toDate(event_time) AS date,
|
||||
l.project_id AS project_id,
|
||||
count() AS total_clicks,
|
||||
uniqExact(e.visitor_id) AS unique_visitors,
|
||||
countIf(e.event_type = 'conversion') AS conversion_count,
|
||||
uniqExact(e.link_id) AS links_used,
|
||||
countIf(e.is_qr_scan) AS qr_scan_count
|
||||
FROM
|
||||
limq.link_events e
|
||||
JOIN limq.links l ON e.link_id = l.link_id
|
||||
WHERE
|
||||
l.project_id != ''
|
||||
GROUP BY
|
||||
date,
|
||||
l.project_id;
|
||||
|
||||
-- QR码每日统计视图
|
||||
CREATE MATERIALIZED VIEW limq.qrcode_daily_stats ENGINE = SummingMergeTree() PARTITION BY toYYYYMM(date)
|
||||
ORDER BY
|
||||
(date, qr_code_id) SETTINGS index_granularity = 8192 AS
|
||||
SELECT
|
||||
toDate(scan_time) AS date,
|
||||
qr_code_id,
|
||||
count() AS total_scans,
|
||||
uniqExact(visitor_id) AS unique_scanners,
|
||||
countIf(led_to_conversion) AS conversions,
|
||||
countIf(device_type = 'mobile') AS mobile_scans,
|
||||
countIf(device_type = 'tablet') AS tablet_scans,
|
||||
countIf(device_type = 'desktop') AS desktop_scans,
|
||||
uniqExact(location) AS unique_locations
|
||||
FROM
|
||||
limq.qr_scans
|
||||
GROUP BY
|
||||
date,
|
||||
qr_code_id;
|
||||
997
scripts/db/sql/clickhouse/mock_link_data.sql
Normal file
997
scripts/db/sql/clickhouse/mock_link_data.sql
Normal file
@@ -0,0 +1,997 @@
|
||||
-- 移动端点击访问事件
|
||||
INSERT INTO
|
||||
limq.link_events (
|
||||
event_id,
|
||||
event_time,
|
||||
date,
|
||||
link_id,
|
||||
channel_id,
|
||||
visitor_id,
|
||||
session_id,
|
||||
event_type,
|
||||
ip_address,
|
||||
country,
|
||||
city,
|
||||
referrer,
|
||||
utm_source,
|
||||
utm_medium,
|
||||
utm_campaign,
|
||||
user_agent,
|
||||
device_type,
|
||||
browser,
|
||||
os,
|
||||
time_spent_sec,
|
||||
is_bounce,
|
||||
is_qr_scan,
|
||||
conversion_type,
|
||||
conversion_value
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
generateUUIDv4(),
|
||||
'2025-03-15 10:25:30',
|
||||
'2025-03-15',
|
||||
'a71fcfe8-d293-4d6d-91c5-1528fa7f6294',
|
||||
'ch_main',
|
||||
'v-123',
|
||||
's-456',
|
||||
'click',
|
||||
'103.45.67.89',
|
||||
'China',
|
||||
'Shanghai',
|
||||
'https://www.google.com',
|
||||
'google',
|
||||
'organic',
|
||||
'none',
|
||||
'Mozilla/5.0 (iPhone)',
|
||||
'mobile',
|
||||
'Safari',
|
||||
'iOS',
|
||||
45,
|
||||
false,
|
||||
false,
|
||||
'visit',
|
||||
0
|
||||
);
|
||||
|
||||
INSERT INTO
|
||||
limq.link_events (
|
||||
event_id,
|
||||
event_time,
|
||||
date,
|
||||
link_id,
|
||||
channel_id,
|
||||
visitor_id,
|
||||
session_id,
|
||||
event_type,
|
||||
ip_address,
|
||||
country,
|
||||
city,
|
||||
referrer,
|
||||
utm_source,
|
||||
utm_medium,
|
||||
utm_campaign,
|
||||
user_agent,
|
||||
device_type,
|
||||
browser,
|
||||
os,
|
||||
time_spent_sec,
|
||||
is_bounce,
|
||||
is_qr_scan,
|
||||
conversion_type,
|
||||
conversion_value
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
generateUUIDv4(),
|
||||
'2025-03-15 11:32:21',
|
||||
'2025-03-15',
|
||||
'a71fcfe8-d293-4d6d-91c5-1528fa7f6294',
|
||||
'ch_main',
|
||||
'v-124',
|
||||
's-457',
|
||||
'click',
|
||||
'43.78.123.45',
|
||||
'Japan',
|
||||
'Tokyo',
|
||||
'https://twitter.com',
|
||||
'twitter',
|
||||
'social',
|
||||
'spring_promo',
|
||||
'Mozilla/5.0 (Android 10)',
|
||||
'mobile',
|
||||
'Chrome',
|
||||
'Android',
|
||||
15,
|
||||
true,
|
||||
false,
|
||||
'visit',
|
||||
0
|
||||
);
|
||||
|
||||
INSERT INTO
|
||||
limq.link_events (
|
||||
event_id,
|
||||
event_time,
|
||||
date,
|
||||
link_id,
|
||||
channel_id,
|
||||
visitor_id,
|
||||
session_id,
|
||||
event_type,
|
||||
ip_address,
|
||||
country,
|
||||
city,
|
||||
referrer,
|
||||
utm_source,
|
||||
utm_medium,
|
||||
utm_campaign,
|
||||
user_agent,
|
||||
device_type,
|
||||
browser,
|
||||
os,
|
||||
time_spent_sec,
|
||||
is_bounce,
|
||||
is_qr_scan,
|
||||
conversion_type,
|
||||
conversion_value
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
generateUUIDv4(),
|
||||
'2025-03-15 14:15:45',
|
||||
'2025-03-15',
|
||||
'a71fcfe8-d293-4d6d-91c5-1528fa7f6294',
|
||||
'ch_main',
|
||||
'v-125',
|
||||
's-458',
|
||||
'click',
|
||||
'72.34.67.81',
|
||||
'US',
|
||||
'New York',
|
||||
'https://www.facebook.com',
|
||||
'facebook',
|
||||
'social',
|
||||
'crypto_ad',
|
||||
'Mozilla/5.0 (iPhone)',
|
||||
'mobile',
|
||||
'Safari',
|
||||
'iOS',
|
||||
120,
|
||||
false,
|
||||
false,
|
||||
'interact',
|
||||
0
|
||||
);
|
||||
|
||||
-- 桌面设备点击事件
|
||||
INSERT INTO
|
||||
limq.link_events (
|
||||
event_id,
|
||||
event_time,
|
||||
date,
|
||||
link_id,
|
||||
channel_id,
|
||||
visitor_id,
|
||||
session_id,
|
||||
event_type,
|
||||
ip_address,
|
||||
country,
|
||||
city,
|
||||
referrer,
|
||||
utm_source,
|
||||
utm_medium,
|
||||
utm_campaign,
|
||||
user_agent,
|
||||
device_type,
|
||||
browser,
|
||||
os,
|
||||
time_spent_sec,
|
||||
is_bounce,
|
||||
is_qr_scan,
|
||||
conversion_type,
|
||||
conversion_value
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
generateUUIDv4(),
|
||||
'2025-03-15 08:45:12',
|
||||
'2025-03-15',
|
||||
'a71fcfe8-d293-4d6d-91c5-1528fa7f6294',
|
||||
'ch_main',
|
||||
'v-126',
|
||||
's-459',
|
||||
'click',
|
||||
'89.67.43.21',
|
||||
'Germany',
|
||||
'Berlin',
|
||||
'https://www.reddit.com',
|
||||
'reddit',
|
||||
'referral',
|
||||
'none',
|
||||
'Mozilla/5.0 (Windows NT 10.0)',
|
||||
'desktop',
|
||||
'Chrome',
|
||||
'Windows',
|
||||
300,
|
||||
false,
|
||||
false,
|
||||
'visit',
|
||||
0
|
||||
);
|
||||
|
||||
INSERT INTO
|
||||
limq.link_events (
|
||||
event_id,
|
||||
event_time,
|
||||
date,
|
||||
link_id,
|
||||
channel_id,
|
||||
visitor_id,
|
||||
session_id,
|
||||
event_type,
|
||||
ip_address,
|
||||
country,
|
||||
city,
|
||||
referrer,
|
||||
utm_source,
|
||||
utm_medium,
|
||||
utm_campaign,
|
||||
user_agent,
|
||||
device_type,
|
||||
browser,
|
||||
os,
|
||||
time_spent_sec,
|
||||
is_bounce,
|
||||
is_qr_scan,
|
||||
conversion_type,
|
||||
conversion_value
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
generateUUIDv4(),
|
||||
'2025-03-15 16:20:33',
|
||||
'2025-03-15',
|
||||
'a71fcfe8-d293-4d6d-91c5-1528fa7f6294',
|
||||
'ch_main',
|
||||
'v-127',
|
||||
's-460',
|
||||
'click',
|
||||
'178.65.43.12',
|
||||
'UK',
|
||||
'London',
|
||||
'https://www.linkedin.com',
|
||||
'linkedin',
|
||||
'social',
|
||||
'biz_campaign',
|
||||
'Mozilla/5.0 (Macintosh)',
|
||||
'desktop',
|
||||
'Safari',
|
||||
'MacOS',
|
||||
250,
|
||||
false,
|
||||
false,
|
||||
'stay',
|
||||
0
|
||||
);
|
||||
|
||||
-- 平板设备点击事件
|
||||
INSERT INTO
|
||||
limq.link_events (
|
||||
event_id,
|
||||
event_time,
|
||||
date,
|
||||
link_id,
|
||||
channel_id,
|
||||
visitor_id,
|
||||
session_id,
|
||||
event_type,
|
||||
ip_address,
|
||||
country,
|
||||
city,
|
||||
referrer,
|
||||
utm_source,
|
||||
utm_medium,
|
||||
utm_campaign,
|
||||
user_agent,
|
||||
device_type,
|
||||
browser,
|
||||
os,
|
||||
time_spent_sec,
|
||||
is_bounce,
|
||||
is_qr_scan,
|
||||
conversion_type,
|
||||
conversion_value
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
generateUUIDv4(),
|
||||
'2025-03-15 13:10:55',
|
||||
'2025-03-15',
|
||||
'a71fcfe8-d293-4d6d-91c5-1528fa7f6294',
|
||||
'ch_main',
|
||||
'v-128',
|
||||
's-461',
|
||||
'click',
|
||||
'156.78.34.12',
|
||||
'Canada',
|
||||
'Toronto',
|
||||
'https://www.youtube.com',
|
||||
'youtube',
|
||||
'video',
|
||||
'tutorial',
|
||||
'Mozilla/5.0 (iPad)',
|
||||
'tablet',
|
||||
'Safari',
|
||||
'iOS',
|
||||
180,
|
||||
false,
|
||||
false,
|
||||
'visit',
|
||||
0
|
||||
);
|
||||
|
||||
-- QR扫描访问事件
|
||||
INSERT INTO
|
||||
limq.link_events (
|
||||
event_id,
|
||||
event_time,
|
||||
date,
|
||||
link_id,
|
||||
channel_id,
|
||||
visitor_id,
|
||||
session_id,
|
||||
event_type,
|
||||
ip_address,
|
||||
country,
|
||||
city,
|
||||
referrer,
|
||||
utm_source,
|
||||
utm_medium,
|
||||
utm_campaign,
|
||||
user_agent,
|
||||
device_type,
|
||||
browser,
|
||||
os,
|
||||
time_spent_sec,
|
||||
is_bounce,
|
||||
is_qr_scan,
|
||||
conversion_type,
|
||||
conversion_value
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
generateUUIDv4(),
|
||||
'2025-03-15 09:30:22',
|
||||
'2025-03-15',
|
||||
'a71fcfe8-d293-4d6d-91c5-1528fa7f6294',
|
||||
'ch_qr',
|
||||
'v-129',
|
||||
's-462',
|
||||
'click',
|
||||
'101.56.78.90',
|
||||
'China',
|
||||
'Beijing',
|
||||
'direct',
|
||||
'qr',
|
||||
'print',
|
||||
'offline_event',
|
||||
'Mozilla/5.0 (iPhone)',
|
||||
'mobile',
|
||||
'Safari',
|
||||
'iOS',
|
||||
75,
|
||||
false,
|
||||
true,
|
||||
'visit',
|
||||
0
|
||||
);
|
||||
|
||||
-- 转化事件
|
||||
INSERT INTO
|
||||
limq.link_events (
|
||||
event_id,
|
||||
event_time,
|
||||
date,
|
||||
link_id,
|
||||
channel_id,
|
||||
visitor_id,
|
||||
session_id,
|
||||
event_type,
|
||||
ip_address,
|
||||
country,
|
||||
city,
|
||||
referrer,
|
||||
utm_source,
|
||||
utm_medium,
|
||||
utm_campaign,
|
||||
user_agent,
|
||||
device_type,
|
||||
browser,
|
||||
os,
|
||||
time_spent_sec,
|
||||
is_bounce,
|
||||
is_qr_scan,
|
||||
conversion_type,
|
||||
conversion_value
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
generateUUIDv4(),
|
||||
'2025-03-15 10:27:45',
|
||||
'2025-03-15',
|
||||
'a71fcfe8-d293-4d6d-91c5-1528fa7f6294',
|
||||
'ch_main',
|
||||
'v-123',
|
||||
's-456',
|
||||
'conversion',
|
||||
'103.45.67.89',
|
||||
'China',
|
||||
'Shanghai',
|
||||
'https://www.google.com',
|
||||
'google',
|
||||
'organic',
|
||||
'none',
|
||||
'Mozilla/5.0 (iPhone)',
|
||||
'mobile',
|
||||
'Safari',
|
||||
'iOS',
|
||||
120,
|
||||
false,
|
||||
false,
|
||||
'signup',
|
||||
50
|
||||
);
|
||||
|
||||
INSERT INTO
|
||||
limq.link_events (
|
||||
event_id,
|
||||
event_time,
|
||||
date,
|
||||
link_id,
|
||||
channel_id,
|
||||
visitor_id,
|
||||
session_id,
|
||||
event_type,
|
||||
ip_address,
|
||||
country,
|
||||
city,
|
||||
referrer,
|
||||
utm_source,
|
||||
utm_medium,
|
||||
utm_campaign,
|
||||
user_agent,
|
||||
device_type,
|
||||
browser,
|
||||
os,
|
||||
time_spent_sec,
|
||||
is_bounce,
|
||||
is_qr_scan,
|
||||
conversion_type,
|
||||
conversion_value
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
generateUUIDv4(),
|
||||
'2025-03-15 08:52:18',
|
||||
'2025-03-15',
|
||||
'a71fcfe8-d293-4d6d-91c5-1528fa7f6294',
|
||||
'ch_main',
|
||||
'v-126',
|
||||
's-459',
|
||||
'conversion',
|
||||
'89.67.43.21',
|
||||
'Germany',
|
||||
'Berlin',
|
||||
'https://www.reddit.com',
|
||||
'reddit',
|
||||
'referral',
|
||||
'none',
|
||||
'Mozilla/5.0 (Windows NT 10.0)',
|
||||
'desktop',
|
||||
'Chrome',
|
||||
'Windows',
|
||||
450,
|
||||
false,
|
||||
false,
|
||||
'purchase',
|
||||
150.75
|
||||
);
|
||||
|
||||
-- 第二天的数据 (3/16)
|
||||
INSERT INTO
|
||||
limq.link_events (
|
||||
event_id,
|
||||
event_time,
|
||||
date,
|
||||
link_id,
|
||||
channel_id,
|
||||
visitor_id,
|
||||
session_id,
|
||||
event_type,
|
||||
ip_address,
|
||||
country,
|
||||
city,
|
||||
referrer,
|
||||
utm_source,
|
||||
utm_medium,
|
||||
utm_campaign,
|
||||
user_agent,
|
||||
device_type,
|
||||
browser,
|
||||
os,
|
||||
time_spent_sec,
|
||||
is_bounce,
|
||||
is_qr_scan,
|
||||
conversion_type,
|
||||
conversion_value
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
generateUUIDv4(),
|
||||
'2025-03-16 11:15:30',
|
||||
'2025-03-16',
|
||||
'a71fcfe8-d293-4d6d-91c5-1528fa7f6294',
|
||||
'ch_main',
|
||||
'v-130',
|
||||
's-463',
|
||||
'click',
|
||||
'178.91.45.67',
|
||||
'France',
|
||||
'Paris',
|
||||
'https://www.google.com',
|
||||
'google',
|
||||
'organic',
|
||||
'none',
|
||||
'Mozilla/5.0 (Android 11)',
|
||||
'mobile',
|
||||
'Chrome',
|
||||
'Android',
|
||||
60,
|
||||
false,
|
||||
false,
|
||||
'visit',
|
||||
0
|
||||
);
|
||||
|
||||
INSERT INTO
|
||||
limq.link_events (
|
||||
event_id,
|
||||
event_time,
|
||||
date,
|
||||
link_id,
|
||||
channel_id,
|
||||
visitor_id,
|
||||
session_id,
|
||||
event_type,
|
||||
ip_address,
|
||||
country,
|
||||
city,
|
||||
referrer,
|
||||
utm_source,
|
||||
utm_medium,
|
||||
utm_campaign,
|
||||
user_agent,
|
||||
device_type,
|
||||
browser,
|
||||
os,
|
||||
time_spent_sec,
|
||||
is_bounce,
|
||||
is_qr_scan,
|
||||
conversion_type,
|
||||
conversion_value
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
generateUUIDv4(),
|
||||
'2025-03-16 14:22:45',
|
||||
'2025-03-16',
|
||||
'a71fcfe8-d293-4d6d-91c5-1528fa7f6294',
|
||||
'ch_main',
|
||||
'v-131',
|
||||
's-464',
|
||||
'click',
|
||||
'89.123.45.78',
|
||||
'Spain',
|
||||
'Madrid',
|
||||
'https://www.instagram.com',
|
||||
'instagram',
|
||||
'social',
|
||||
'influencer',
|
||||
'Mozilla/5.0 (iPhone)',
|
||||
'mobile',
|
||||
'Safari',
|
||||
'iOS',
|
||||
90,
|
||||
false,
|
||||
false,
|
||||
'interact',
|
||||
0
|
||||
);
|
||||
|
||||
INSERT INTO
|
||||
limq.link_events (
|
||||
event_id,
|
||||
event_time,
|
||||
date,
|
||||
link_id,
|
||||
channel_id,
|
||||
visitor_id,
|
||||
session_id,
|
||||
event_type,
|
||||
ip_address,
|
||||
country,
|
||||
city,
|
||||
referrer,
|
||||
utm_source,
|
||||
utm_medium,
|
||||
utm_campaign,
|
||||
user_agent,
|
||||
device_type,
|
||||
browser,
|
||||
os,
|
||||
time_spent_sec,
|
||||
is_bounce,
|
||||
is_qr_scan,
|
||||
conversion_type,
|
||||
conversion_value
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
generateUUIDv4(),
|
||||
'2025-03-16 16:40:12',
|
||||
'2025-03-16',
|
||||
'a71fcfe8-d293-4d6d-91c5-1528fa7f6294',
|
||||
'ch_main',
|
||||
'v-131',
|
||||
's-464',
|
||||
'conversion',
|
||||
'89.123.45.78',
|
||||
'Spain',
|
||||
'Madrid',
|
||||
'https://www.instagram.com',
|
||||
'instagram',
|
||||
'social',
|
||||
'influencer',
|
||||
'Mozilla/5.0 (iPhone)',
|
||||
'mobile',
|
||||
'Safari',
|
||||
'iOS',
|
||||
200,
|
||||
false,
|
||||
false,
|
||||
'subscription',
|
||||
75.50
|
||||
);
|
||||
|
||||
-- 第三天数据 (3/17)
|
||||
INSERT INTO
|
||||
limq.link_events (
|
||||
event_id,
|
||||
event_time,
|
||||
date,
|
||||
link_id,
|
||||
channel_id,
|
||||
visitor_id,
|
||||
session_id,
|
||||
event_type,
|
||||
ip_address,
|
||||
country,
|
||||
city,
|
||||
referrer,
|
||||
utm_source,
|
||||
utm_medium,
|
||||
utm_campaign,
|
||||
user_agent,
|
||||
device_type,
|
||||
browser,
|
||||
os,
|
||||
time_spent_sec,
|
||||
is_bounce,
|
||||
is_qr_scan,
|
||||
conversion_type,
|
||||
conversion_value
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
generateUUIDv4(),
|
||||
'2025-03-17 09:10:22',
|
||||
'2025-03-17',
|
||||
'a71fcfe8-d293-4d6d-91c5-1528fa7f6294',
|
||||
'ch_main',
|
||||
'v-132',
|
||||
's-465',
|
||||
'click',
|
||||
'45.67.89.123',
|
||||
'US',
|
||||
'Los Angeles',
|
||||
'https://www.google.com',
|
||||
'google',
|
||||
'cpc',
|
||||
'spring_sale',
|
||||
'Mozilla/5.0 (Windows NT 10.0)',
|
||||
'desktop',
|
||||
'Edge',
|
||||
'Windows',
|
||||
150,
|
||||
false,
|
||||
false,
|
||||
'visit',
|
||||
0
|
||||
);
|
||||
|
||||
INSERT INTO
|
||||
limq.link_events (
|
||||
event_id,
|
||||
event_time,
|
||||
date,
|
||||
link_id,
|
||||
channel_id,
|
||||
visitor_id,
|
||||
session_id,
|
||||
event_type,
|
||||
ip_address,
|
||||
country,
|
||||
city,
|
||||
referrer,
|
||||
utm_source,
|
||||
utm_medium,
|
||||
utm_campaign,
|
||||
user_agent,
|
||||
device_type,
|
||||
browser,
|
||||
os,
|
||||
time_spent_sec,
|
||||
is_bounce,
|
||||
is_qr_scan,
|
||||
conversion_type,
|
||||
conversion_value
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
generateUUIDv4(),
|
||||
'2025-03-17 12:30:45',
|
||||
'2025-03-17',
|
||||
'a71fcfe8-d293-4d6d-91c5-1528fa7f6294',
|
||||
'ch_main',
|
||||
'v-133',
|
||||
's-466',
|
||||
'click',
|
||||
'67.89.123.45',
|
||||
'Brazil',
|
||||
'Sao Paulo',
|
||||
'https://www.yahoo.com',
|
||||
'yahoo',
|
||||
'organic',
|
||||
'none',
|
||||
'Mozilla/5.0 (iPad)',
|
||||
'tablet',
|
||||
'Safari',
|
||||
'iOS',
|
||||
120,
|
||||
false,
|
||||
false,
|
||||
'stay',
|
||||
0
|
||||
);
|
||||
|
||||
INSERT INTO
|
||||
limq.link_events (
|
||||
event_id,
|
||||
event_time,
|
||||
date,
|
||||
link_id,
|
||||
channel_id,
|
||||
visitor_id,
|
||||
session_id,
|
||||
event_type,
|
||||
ip_address,
|
||||
country,
|
||||
city,
|
||||
referrer,
|
||||
utm_source,
|
||||
utm_medium,
|
||||
utm_campaign,
|
||||
user_agent,
|
||||
device_type,
|
||||
browser,
|
||||
os,
|
||||
time_spent_sec,
|
||||
is_bounce,
|
||||
is_qr_scan,
|
||||
conversion_type,
|
||||
conversion_value
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
generateUUIDv4(),
|
||||
'2025-03-17 15:45:33',
|
||||
'2025-03-17',
|
||||
'a71fcfe8-d293-4d6d-91c5-1528fa7f6294',
|
||||
'ch_main',
|
||||
'v-132',
|
||||
's-465',
|
||||
'conversion',
|
||||
'45.67.89.123',
|
||||
'US',
|
||||
'Los Angeles',
|
||||
'https://www.google.com',
|
||||
'google',
|
||||
'cpc',
|
||||
'spring_sale',
|
||||
'Mozilla/5.0 (Windows NT 10.0)',
|
||||
'desktop',
|
||||
'Edge',
|
||||
'Windows',
|
||||
300,
|
||||
false,
|
||||
false,
|
||||
'purchase',
|
||||
225.50
|
||||
);
|
||||
|
||||
-- 添加一周前的数据 (对比期)
|
||||
INSERT INTO
|
||||
limq.link_events (
|
||||
event_id,
|
||||
event_time,
|
||||
date,
|
||||
link_id,
|
||||
channel_id,
|
||||
visitor_id,
|
||||
session_id,
|
||||
event_type,
|
||||
ip_address,
|
||||
country,
|
||||
city,
|
||||
referrer,
|
||||
utm_source,
|
||||
utm_medium,
|
||||
utm_campaign,
|
||||
user_agent,
|
||||
device_type,
|
||||
browser,
|
||||
os,
|
||||
time_spent_sec,
|
||||
is_bounce,
|
||||
is_qr_scan,
|
||||
conversion_type,
|
||||
conversion_value
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
generateUUIDv4(),
|
||||
'2025-03-08 10:25:30',
|
||||
'2025-03-08',
|
||||
'a71fcfe8-d293-4d6d-91c5-1528fa7f6294',
|
||||
'ch_main',
|
||||
'v-140',
|
||||
's-470',
|
||||
'click',
|
||||
'103.45.67.89',
|
||||
'China',
|
||||
'Shanghai',
|
||||
'https://www.google.com',
|
||||
'google',
|
||||
'organic',
|
||||
'none',
|
||||
'Mozilla/5.0 (iPhone)',
|
||||
'mobile',
|
||||
'Safari',
|
||||
'iOS',
|
||||
30,
|
||||
false,
|
||||
false,
|
||||
'visit',
|
||||
0
|
||||
);
|
||||
|
||||
INSERT INTO
|
||||
limq.link_events (
|
||||
event_id,
|
||||
event_time,
|
||||
date,
|
||||
link_id,
|
||||
channel_id,
|
||||
visitor_id,
|
||||
session_id,
|
||||
event_type,
|
||||
ip_address,
|
||||
country,
|
||||
city,
|
||||
referrer,
|
||||
utm_source,
|
||||
utm_medium,
|
||||
utm_campaign,
|
||||
user_agent,
|
||||
device_type,
|
||||
browser,
|
||||
os,
|
||||
time_spent_sec,
|
||||
is_bounce,
|
||||
is_qr_scan,
|
||||
conversion_type,
|
||||
conversion_value
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
generateUUIDv4(),
|
||||
'2025-03-08 11:32:21',
|
||||
'2025-03-08',
|
||||
'a71fcfe8-d293-4d6d-91c5-1528fa7f6294',
|
||||
'ch_main',
|
||||
'v-141',
|
||||
's-471',
|
||||
'click',
|
||||
'89.67.43.21',
|
||||
'Germany',
|
||||
'Berlin',
|
||||
'https://www.reddit.com',
|
||||
'reddit',
|
||||
'referral',
|
||||
'none',
|
||||
'Mozilla/5.0 (Windows NT 10.0)',
|
||||
'desktop',
|
||||
'Chrome',
|
||||
'Windows',
|
||||
200,
|
||||
false,
|
||||
false,
|
||||
'visit',
|
||||
0
|
||||
);
|
||||
|
||||
INSERT INTO
|
||||
limq.link_events (
|
||||
event_id,
|
||||
event_time,
|
||||
date,
|
||||
link_id,
|
||||
channel_id,
|
||||
visitor_id,
|
||||
session_id,
|
||||
event_type,
|
||||
ip_address,
|
||||
country,
|
||||
city,
|
||||
referrer,
|
||||
utm_source,
|
||||
utm_medium,
|
||||
utm_campaign,
|
||||
user_agent,
|
||||
device_type,
|
||||
browser,
|
||||
os,
|
||||
time_spent_sec,
|
||||
is_bounce,
|
||||
is_qr_scan,
|
||||
conversion_type,
|
||||
conversion_value
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
generateUUIDv4(),
|
||||
'2025-03-08 13:10:55',
|
||||
'2025-03-08',
|
||||
'a71fcfe8-d293-4d6d-91c5-1528fa7f6294',
|
||||
'ch_main',
|
||||
'v-140',
|
||||
's-470',
|
||||
'conversion',
|
||||
'103.45.67.89',
|
||||
'China',
|
||||
'Shanghai',
|
||||
'https://www.google.com',
|
||||
'google',
|
||||
'organic',
|
||||
'none',
|
||||
'Mozilla/5.0 (iPhone)',
|
||||
'mobile',
|
||||
'Safari',
|
||||
'iOS',
|
||||
100,
|
||||
false,
|
||||
false,
|
||||
'purchase',
|
||||
100.00
|
||||
);
|
||||
193
scripts/db/sql/clickhouse/recreate_limq.sql
Normal file
193
scripts/db/sql/clickhouse/recreate_limq.sql
Normal file
@@ -0,0 +1,193 @@
|
||||
-- 删除现有的物化视图(需要先删除视图,因为它们依赖于表)
|
||||
DROP TABLE IF EXISTS limq.platform_distribution;
|
||||
|
||||
DROP TABLE IF EXISTS limq.link_hourly_patterns;
|
||||
|
||||
DROP TABLE IF EXISTS limq.link_daily_stats;
|
||||
|
||||
-- 删除现有的表
|
||||
DROP TABLE IF EXISTS limq.qr_scans;
|
||||
|
||||
DROP TABLE IF EXISTS limq.sessions;
|
||||
|
||||
DROP TABLE IF EXISTS limq.link_events;
|
||||
|
||||
DROP TABLE IF EXISTS limq.links;
|
||||
|
||||
-- 创建数据库(如果不存在)
|
||||
CREATE DATABASE IF NOT EXISTS limq;
|
||||
|
||||
-- 切换到limq数据库
|
||||
USE limq;
|
||||
|
||||
-- 创建短链接访问事件表
|
||||
CREATE TABLE IF NOT EXISTS limq.link_events (
|
||||
event_id UUID DEFAULT generateUUIDv4(),
|
||||
event_time DateTime64(3) DEFAULT now64(),
|
||||
date Date DEFAULT toDate(event_time),
|
||||
link_id String,
|
||||
channel_id String,
|
||||
visitor_id String,
|
||||
session_id String,
|
||||
event_type Enum8(
|
||||
'click' = 1,
|
||||
'redirect' = 2,
|
||||
'conversion' = 3,
|
||||
'error' = 4
|
||||
),
|
||||
-- 访问者信息
|
||||
ip_address String,
|
||||
country String,
|
||||
city String,
|
||||
-- 来源信息
|
||||
referrer String,
|
||||
utm_source String,
|
||||
utm_medium String,
|
||||
utm_campaign String,
|
||||
-- 设备信息
|
||||
user_agent String,
|
||||
device_type Enum8(
|
||||
'mobile' = 1,
|
||||
'tablet' = 2,
|
||||
'desktop' = 3,
|
||||
'other' = 4
|
||||
),
|
||||
browser String,
|
||||
os String,
|
||||
-- 交互信息
|
||||
time_spent_sec UInt32 DEFAULT 0,
|
||||
is_bounce Boolean DEFAULT true,
|
||||
-- QR码相关
|
||||
is_qr_scan Boolean DEFAULT false,
|
||||
qr_code_id String DEFAULT '',
|
||||
-- 转化数据
|
||||
conversion_type Enum8(
|
||||
'visit' = 1,
|
||||
'stay' = 2,
|
||||
'interact' = 3,
|
||||
'signup' = 4,
|
||||
'subscription' = 5,
|
||||
'purchase' = 6
|
||||
) DEFAULT 'visit',
|
||||
conversion_value Float64 DEFAULT 0,
|
||||
-- 其他属性
|
||||
custom_data String DEFAULT '{}'
|
||||
) ENGINE = MergeTree() PARTITION BY toYYYYMM(date)
|
||||
ORDER BY
|
||||
(date, link_id, event_time) SETTINGS index_granularity = 8192;
|
||||
|
||||
-- 短链接维度表
|
||||
CREATE TABLE IF NOT EXISTS limq.links (
|
||||
link_id String,
|
||||
original_url String,
|
||||
created_at DateTime64(3),
|
||||
created_by String,
|
||||
title String,
|
||||
description String,
|
||||
tags Array(String),
|
||||
is_active Boolean DEFAULT true,
|
||||
expires_at Nullable(DateTime64(3)),
|
||||
team_id String DEFAULT '',
|
||||
project_id String DEFAULT '',
|
||||
PRIMARY KEY (link_id)
|
||||
) ENGINE = ReplacingMergeTree()
|
||||
ORDER BY
|
||||
link_id SETTINGS index_granularity = 8192;
|
||||
|
||||
-- 会话跟踪表
|
||||
CREATE TABLE IF NOT EXISTS limq.sessions (
|
||||
session_id String,
|
||||
visitor_id String,
|
||||
link_id String,
|
||||
started_at DateTime64(3),
|
||||
last_activity DateTime64(3),
|
||||
ended_at Nullable(DateTime64(3)),
|
||||
duration_sec UInt32 DEFAULT 0,
|
||||
session_pages UInt8 DEFAULT 1,
|
||||
is_completed Boolean DEFAULT false,
|
||||
PRIMARY KEY (session_id)
|
||||
) ENGINE = ReplacingMergeTree(last_activity)
|
||||
ORDER BY
|
||||
(session_id, link_id, visitor_id) SETTINGS index_granularity = 8192;
|
||||
|
||||
-- QR码统计表
|
||||
CREATE TABLE IF NOT EXISTS limq.qr_scans (
|
||||
scan_id UUID DEFAULT generateUUIDv4(),
|
||||
qr_code_id String,
|
||||
link_id String,
|
||||
scan_time DateTime64(3),
|
||||
visitor_id String,
|
||||
location String,
|
||||
device_type Enum8(
|
||||
'mobile' = 1,
|
||||
'tablet' = 2,
|
||||
'desktop' = 3,
|
||||
'other' = 4
|
||||
),
|
||||
led_to_conversion Boolean DEFAULT false,
|
||||
PRIMARY KEY (scan_id)
|
||||
) ENGINE = MergeTree() PARTITION BY toYYYYMM(scan_time)
|
||||
ORDER BY
|
||||
scan_id SETTINGS index_granularity = 8192;
|
||||
|
||||
-- 每日链接汇总视图
|
||||
CREATE MATERIALIZED VIEW IF NOT EXISTS limq.link_daily_stats ENGINE = SummingMergeTree() PARTITION BY toYYYYMM(date)
|
||||
ORDER BY
|
||||
(date, link_id) SETTINGS index_granularity = 8192 AS
|
||||
SELECT
|
||||
toDate(event_time) AS date,
|
||||
link_id,
|
||||
count() AS total_clicks,
|
||||
uniqExact(visitor_id) AS unique_visitors,
|
||||
uniqExact(session_id) AS unique_sessions,
|
||||
sum(time_spent_sec) AS total_time_spent,
|
||||
avg(time_spent_sec) AS avg_time_spent,
|
||||
countIf(is_bounce) AS bounce_count,
|
||||
countIf(event_type = 'conversion') AS conversion_count,
|
||||
uniqExact(referrer) AS unique_referrers,
|
||||
countIf(device_type = 'mobile') AS mobile_count,
|
||||
countIf(device_type = 'tablet') AS tablet_count,
|
||||
countIf(device_type = 'desktop') AS desktop_count,
|
||||
countIf(is_qr_scan) AS qr_scan_count,
|
||||
sum(conversion_value) AS total_conversion_value
|
||||
FROM
|
||||
limq.link_events
|
||||
GROUP BY
|
||||
date,
|
||||
link_id;
|
||||
|
||||
-- 每小时访问模式视图
|
||||
CREATE MATERIALIZED VIEW IF NOT EXISTS limq.link_hourly_patterns ENGINE = SummingMergeTree() PARTITION BY toYYYYMM(date)
|
||||
ORDER BY
|
||||
(date, hour, link_id) SETTINGS index_granularity = 8192 AS
|
||||
SELECT
|
||||
toDate(event_time) AS date,
|
||||
toHour(event_time) AS hour,
|
||||
link_id,
|
||||
count() AS visits,
|
||||
uniqExact(visitor_id) AS unique_visitors
|
||||
FROM
|
||||
limq.link_events
|
||||
GROUP BY
|
||||
date,
|
||||
hour,
|
||||
link_id;
|
||||
|
||||
-- 平台分布视图
|
||||
CREATE MATERIALIZED VIEW IF NOT EXISTS limq.platform_distribution ENGINE = SummingMergeTree() PARTITION BY toYYYYMM(date)
|
||||
ORDER BY
|
||||
(date, utm_source, device_type) SETTINGS index_granularity = 8192 AS
|
||||
SELECT
|
||||
toDate(event_time) AS date,
|
||||
utm_source,
|
||||
device_type,
|
||||
count() AS visits,
|
||||
uniqExact(visitor_id) AS unique_visitors
|
||||
FROM
|
||||
limq.link_events
|
||||
WHERE
|
||||
utm_source != ''
|
||||
GROUP BY
|
||||
date,
|
||||
utm_source,
|
||||
device_type;
|
||||
828
scripts/db/sql/clickhouse/seed-clickhouse-analytics.sql
Normal file
828
scripts/db/sql/clickhouse/seed-clickhouse-analytics.sql
Normal file
@@ -0,0 +1,828 @@
|
||||
-- 清空现有数据(可选)
|
||||
TRUNCATE TABLE IF EXISTS limq.link_events;
|
||||
|
||||
TRUNCATE TABLE IF EXISTS limq.link_daily_stats;
|
||||
|
||||
TRUNCATE TABLE IF EXISTS limq.link_hourly_patterns;
|
||||
|
||||
TRUNCATE TABLE IF EXISTS limq.links;
|
||||
|
||||
-- 使用固定的UUID值插入链接
|
||||
INSERT INTO
|
||||
limq.links (
|
||||
link_id,
|
||||
original_url,
|
||||
created_at,
|
||||
created_by,
|
||||
title,
|
||||
description,
|
||||
tags,
|
||||
is_active
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
'11111111-1111-1111-1111-111111111111',
|
||||
'https://example.com/page1',
|
||||
now(),
|
||||
'user-1',
|
||||
'产品页面',
|
||||
'我们的主要产品页面',
|
||||
[ '产品',
|
||||
'营销' ],
|
||||
true
|
||||
);
|
||||
|
||||
INSERT INTO
|
||||
limq.links (
|
||||
link_id,
|
||||
original_url,
|
||||
created_at,
|
||||
created_by,
|
||||
title,
|
||||
description,
|
||||
tags,
|
||||
is_active
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
'22222222-2222-2222-2222-222222222222',
|
||||
'https://example.com/promo',
|
||||
now(),
|
||||
'user-1',
|
||||
'促销活动',
|
||||
'夏季特别促销活动',
|
||||
[ '促销',
|
||||
'活动' ],
|
||||
true
|
||||
);
|
||||
|
||||
INSERT INTO
|
||||
limq.links (
|
||||
link_id,
|
||||
original_url,
|
||||
created_at,
|
||||
created_by,
|
||||
title,
|
||||
description,
|
||||
tags,
|
||||
is_active
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
'33333333-3333-3333-3333-333333333333',
|
||||
'https://example.com/blog',
|
||||
now(),
|
||||
'user-2',
|
||||
'公司博客',
|
||||
'公司新闻和更新',
|
||||
[ '博客',
|
||||
'内容' ],
|
||||
true
|
||||
);
|
||||
|
||||
INSERT INTO
|
||||
limq.links (
|
||||
link_id,
|
||||
original_url,
|
||||
created_at,
|
||||
created_by,
|
||||
title,
|
||||
description,
|
||||
tags,
|
||||
is_active
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
'44444444-4444-4444-4444-444444444444',
|
||||
'https://example.com/signup',
|
||||
now(),
|
||||
'user-2',
|
||||
'注册页面',
|
||||
'新用户注册页面',
|
||||
[ '转化',
|
||||
'注册' ],
|
||||
true
|
||||
);
|
||||
|
||||
-- 为第一个链接创建500条记录
|
||||
INSERT INTO
|
||||
limq.link_events (
|
||||
event_id,
|
||||
event_time,
|
||||
date,
|
||||
link_id,
|
||||
channel_id,
|
||||
visitor_id,
|
||||
session_id,
|
||||
event_type,
|
||||
ip_address,
|
||||
country,
|
||||
city,
|
||||
referrer,
|
||||
utm_source,
|
||||
utm_medium,
|
||||
utm_campaign,
|
||||
user_agent,
|
||||
device_type,
|
||||
browser,
|
||||
os,
|
||||
time_spent_sec,
|
||||
is_bounce,
|
||||
is_qr_scan,
|
||||
qr_code_id,
|
||||
conversion_type,
|
||||
conversion_value,
|
||||
custom_data
|
||||
)
|
||||
SELECT
|
||||
generateUUIDv4() AS event_id,
|
||||
subtractDays(now(), rand() % 30) AS event_time,
|
||||
toDate(event_time) AS date,
|
||||
'11111111-1111-1111-1111-111111111111' AS link_id,
|
||||
'channel-1' AS channel_id,
|
||||
concat('visitor-', toString(rand() % 100 + 1)) AS visitor_id,
|
||||
concat('session-', toString(number % 50 + 1)) AS session_id,
|
||||
multiIf(
|
||||
rand() % 100 < 70,
|
||||
'click',
|
||||
rand() % 100 < 90,
|
||||
'redirect',
|
||||
rand() % 100 < 98,
|
||||
'conversion',
|
||||
'error'
|
||||
) AS event_type,
|
||||
concat('192.168.1.', toString(rand() % 255)) AS ip_address,
|
||||
multiIf(
|
||||
rand() % 100 < 60,
|
||||
'China',
|
||||
rand() % 100 < 85,
|
||||
'US',
|
||||
rand() % 100 < 95,
|
||||
'Japan',
|
||||
'Other'
|
||||
) AS country,
|
||||
multiIf(
|
||||
rand() % 100 < 60,
|
||||
'Beijing',
|
||||
rand() % 100 < 85,
|
||||
'New York',
|
||||
rand() % 100 < 95,
|
||||
'Tokyo',
|
||||
'Other'
|
||||
) AS city,
|
||||
multiIf(
|
||||
rand() % 100 < 30,
|
||||
'https://google.com',
|
||||
rand() % 100 < 50,
|
||||
'https://facebook.com',
|
||||
rand() % 100 < 65,
|
||||
'https://twitter.com',
|
||||
rand() % 100 < 75,
|
||||
'https://instagram.com',
|
||||
rand() % 100 < 85,
|
||||
'https://linkedin.com',
|
||||
rand() % 100 < 90,
|
||||
'https://bing.com',
|
||||
rand() % 100 < 95,
|
||||
'https://baidu.com',
|
||||
'direct'
|
||||
) AS referrer,
|
||||
multiIf(
|
||||
rand() % 100 < 40,
|
||||
'google',
|
||||
rand() % 100 < 70,
|
||||
'facebook',
|
||||
rand() % 100 < 90,
|
||||
'email',
|
||||
'direct'
|
||||
) AS utm_source,
|
||||
multiIf(
|
||||
rand() % 100 < 40,
|
||||
'cpc',
|
||||
rand() % 100 < 70,
|
||||
'social',
|
||||
rand() % 100 < 90,
|
||||
'email',
|
||||
'direct'
|
||||
) AS utm_medium,
|
||||
multiIf(
|
||||
rand() % 100 < 40,
|
||||
'summer_sale',
|
||||
rand() % 100 < 70,
|
||||
'product_launch',
|
||||
rand() % 100 < 90,
|
||||
'newsletter',
|
||||
'brand'
|
||||
) AS utm_campaign,
|
||||
'Mozilla/5.0' AS user_agent,
|
||||
multiIf(
|
||||
rand() % 100 < 60,
|
||||
'mobile',
|
||||
rand() % 100 < 85,
|
||||
'desktop',
|
||||
rand() % 100 < 95,
|
||||
'tablet',
|
||||
'other'
|
||||
) AS device_type,
|
||||
multiIf(
|
||||
rand() % 100 < 50,
|
||||
'Chrome',
|
||||
rand() % 100 < 80,
|
||||
'Safari',
|
||||
rand() % 100 < 95,
|
||||
'Firefox',
|
||||
'Edge'
|
||||
) AS browser,
|
||||
multiIf(
|
||||
rand() % 100 < 50,
|
||||
'iOS',
|
||||
rand() % 100 < 90,
|
||||
'Android',
|
||||
'Windows'
|
||||
) AS os,
|
||||
rand() % 300 AS time_spent_sec,
|
||||
rand() % 100 < 25 AS is_bounce,
|
||||
rand() % 100 < 20 AS is_qr_scan,
|
||||
concat('qr-', toString(rand() % 10 + 1)) AS qr_code_id,
|
||||
multiIf(
|
||||
rand() % 100 < 50,
|
||||
'visit',
|
||||
rand() % 100 < 70,
|
||||
'stay',
|
||||
rand() % 100 < 85,
|
||||
'interact',
|
||||
rand() % 100 < 93,
|
||||
'signup',
|
||||
rand() % 100 < 97,
|
||||
'subscription',
|
||||
'purchase'
|
||||
) AS conversion_type,
|
||||
rand() % 100 * 1.5 AS conversion_value,
|
||||
'{}' AS custom_data
|
||||
FROM
|
||||
numbers(500);
|
||||
|
||||
-- 为第二个链接创建300条记录
|
||||
INSERT INTO
|
||||
limq.link_events (
|
||||
event_id,
|
||||
event_time,
|
||||
date,
|
||||
link_id,
|
||||
channel_id,
|
||||
visitor_id,
|
||||
session_id,
|
||||
event_type,
|
||||
ip_address,
|
||||
country,
|
||||
city,
|
||||
referrer,
|
||||
utm_source,
|
||||
utm_medium,
|
||||
utm_campaign,
|
||||
user_agent,
|
||||
device_type,
|
||||
browser,
|
||||
os,
|
||||
time_spent_sec,
|
||||
is_bounce,
|
||||
is_qr_scan,
|
||||
qr_code_id,
|
||||
conversion_type,
|
||||
conversion_value,
|
||||
custom_data
|
||||
)
|
||||
SELECT
|
||||
generateUUIDv4() AS event_id,
|
||||
subtractDays(now(), rand() % 30) AS event_time,
|
||||
toDate(event_time) AS date,
|
||||
'22222222-2222-2222-2222-222222222222' AS link_id,
|
||||
'channel-1' AS channel_id,
|
||||
concat('visitor-', toString(rand() % 100 + 1)) AS visitor_id,
|
||||
concat('session-', toString(number % 40 + 1)) AS session_id,
|
||||
multiIf(
|
||||
rand() % 100 < 70,
|
||||
'click',
|
||||
rand() % 100 < 90,
|
||||
'redirect',
|
||||
rand() % 100 < 98,
|
||||
'conversion',
|
||||
'error'
|
||||
) AS event_type,
|
||||
concat('192.168.1.', toString(rand() % 255)) AS ip_address,
|
||||
multiIf(
|
||||
rand() % 100 < 60,
|
||||
'China',
|
||||
rand() % 100 < 85,
|
||||
'US',
|
||||
rand() % 100 < 95,
|
||||
'Japan',
|
||||
'Other'
|
||||
) AS country,
|
||||
multiIf(
|
||||
rand() % 100 < 60,
|
||||
'Beijing',
|
||||
rand() % 100 < 85,
|
||||
'New York',
|
||||
rand() % 100 < 95,
|
||||
'Tokyo',
|
||||
'Other'
|
||||
) AS city,
|
||||
multiIf(
|
||||
rand() % 100 < 30,
|
||||
'https://google.com',
|
||||
rand() % 100 < 50,
|
||||
'https://facebook.com',
|
||||
rand() % 100 < 65,
|
||||
'https://twitter.com',
|
||||
rand() % 100 < 75,
|
||||
'https://instagram.com',
|
||||
rand() % 100 < 85,
|
||||
'https://linkedin.com',
|
||||
rand() % 100 < 90,
|
||||
'https://bing.com',
|
||||
rand() % 100 < 95,
|
||||
'https://baidu.com',
|
||||
'direct'
|
||||
) AS referrer,
|
||||
multiIf(
|
||||
rand() % 100 < 40,
|
||||
'google',
|
||||
rand() % 100 < 70,
|
||||
'facebook',
|
||||
rand() % 100 < 90,
|
||||
'email',
|
||||
'direct'
|
||||
) AS utm_source,
|
||||
multiIf(
|
||||
rand() % 100 < 40,
|
||||
'cpc',
|
||||
rand() % 100 < 70,
|
||||
'social',
|
||||
rand() % 100 < 90,
|
||||
'email',
|
||||
'direct'
|
||||
) AS utm_medium,
|
||||
multiIf(
|
||||
rand() % 100 < 40,
|
||||
'summer_sale',
|
||||
rand() % 100 < 70,
|
||||
'product_launch',
|
||||
rand() % 100 < 90,
|
||||
'newsletter',
|
||||
'brand'
|
||||
) AS utm_campaign,
|
||||
'Mozilla/5.0' AS user_agent,
|
||||
multiIf(
|
||||
rand() % 100 < 60,
|
||||
'mobile',
|
||||
rand() % 100 < 85,
|
||||
'desktop',
|
||||
rand() % 100 < 95,
|
||||
'tablet',
|
||||
'other'
|
||||
) AS device_type,
|
||||
multiIf(
|
||||
rand() % 100 < 50,
|
||||
'Chrome',
|
||||
rand() % 100 < 80,
|
||||
'Safari',
|
||||
rand() % 100 < 95,
|
||||
'Firefox',
|
||||
'Edge'
|
||||
) AS browser,
|
||||
multiIf(
|
||||
rand() % 100 < 50,
|
||||
'iOS',
|
||||
rand() % 100 < 90,
|
||||
'Android',
|
||||
'Windows'
|
||||
) AS os,
|
||||
rand() % 300 AS time_spent_sec,
|
||||
rand() % 100 < 25 AS is_bounce,
|
||||
rand() % 100 < 15 AS is_qr_scan,
|
||||
concat('qr-', toString(rand() % 10 + 1)) AS qr_code_id,
|
||||
multiIf(
|
||||
rand() % 100 < 50,
|
||||
'visit',
|
||||
rand() % 100 < 70,
|
||||
'stay',
|
||||
rand() % 100 < 85,
|
||||
'interact',
|
||||
rand() % 100 < 93,
|
||||
'signup',
|
||||
rand() % 100 < 97,
|
||||
'subscription',
|
||||
'purchase'
|
||||
) AS conversion_type,
|
||||
rand() % 100 * 2.5 AS conversion_value,
|
||||
'{}' AS custom_data
|
||||
FROM
|
||||
numbers(300);
|
||||
|
||||
-- 为第三个链接创建200条记录
|
||||
INSERT INTO
|
||||
limq.link_events (
|
||||
event_id,
|
||||
event_time,
|
||||
date,
|
||||
link_id,
|
||||
channel_id,
|
||||
visitor_id,
|
||||
session_id,
|
||||
event_type,
|
||||
ip_address,
|
||||
country,
|
||||
city,
|
||||
referrer,
|
||||
utm_source,
|
||||
utm_medium,
|
||||
utm_campaign,
|
||||
user_agent,
|
||||
device_type,
|
||||
browser,
|
||||
os,
|
||||
time_spent_sec,
|
||||
is_bounce,
|
||||
is_qr_scan,
|
||||
qr_code_id,
|
||||
conversion_type,
|
||||
conversion_value,
|
||||
custom_data
|
||||
)
|
||||
SELECT
|
||||
generateUUIDv4() AS event_id,
|
||||
subtractDays(now(), rand() % 30) AS event_time,
|
||||
toDate(event_time) AS date,
|
||||
'33333333-3333-3333-3333-333333333333' AS link_id,
|
||||
'channel-2' AS channel_id,
|
||||
concat('visitor-', toString(rand() % 100 + 1)) AS visitor_id,
|
||||
concat('session-', toString(number % 30 + 1)) AS session_id,
|
||||
multiIf(
|
||||
rand() % 100 < 70,
|
||||
'click',
|
||||
rand() % 100 < 90,
|
||||
'redirect',
|
||||
rand() % 100 < 98,
|
||||
'conversion',
|
||||
'error'
|
||||
) AS event_type,
|
||||
concat('192.168.1.', toString(rand() % 255)) AS ip_address,
|
||||
multiIf(
|
||||
rand() % 100 < 60,
|
||||
'China',
|
||||
rand() % 100 < 85,
|
||||
'US',
|
||||
rand() % 100 < 95,
|
||||
'Japan',
|
||||
'Other'
|
||||
) AS country,
|
||||
multiIf(
|
||||
rand() % 100 < 60,
|
||||
'Beijing',
|
||||
rand() % 100 < 85,
|
||||
'New York',
|
||||
rand() % 100 < 95,
|
||||
'Tokyo',
|
||||
'Other'
|
||||
) AS city,
|
||||
multiIf(
|
||||
rand() % 100 < 30,
|
||||
'https://google.com',
|
||||
rand() % 100 < 50,
|
||||
'https://facebook.com',
|
||||
rand() % 100 < 65,
|
||||
'https://twitter.com',
|
||||
rand() % 100 < 75,
|
||||
'https://instagram.com',
|
||||
rand() % 100 < 85,
|
||||
'https://linkedin.com',
|
||||
rand() % 100 < 90,
|
||||
'https://bing.com',
|
||||
rand() % 100 < 95,
|
||||
'https://baidu.com',
|
||||
'direct'
|
||||
) AS referrer,
|
||||
multiIf(
|
||||
rand() % 100 < 40,
|
||||
'google',
|
||||
rand() % 100 < 70,
|
||||
'facebook',
|
||||
rand() % 100 < 90,
|
||||
'email',
|
||||
'direct'
|
||||
) AS utm_source,
|
||||
multiIf(
|
||||
rand() % 100 < 40,
|
||||
'cpc',
|
||||
rand() % 100 < 70,
|
||||
'social',
|
||||
rand() % 100 < 90,
|
||||
'email',
|
||||
'direct'
|
||||
) AS utm_medium,
|
||||
multiIf(
|
||||
rand() % 100 < 40,
|
||||
'summer_sale',
|
||||
rand() % 100 < 70,
|
||||
'product_launch',
|
||||
rand() % 100 < 90,
|
||||
'newsletter',
|
||||
'brand'
|
||||
) AS utm_campaign,
|
||||
'Mozilla/5.0' AS user_agent,
|
||||
multiIf(
|
||||
rand() % 100 < 60,
|
||||
'mobile',
|
||||
rand() % 100 < 85,
|
||||
'desktop',
|
||||
rand() % 100 < 95,
|
||||
'tablet',
|
||||
'other'
|
||||
) AS device_type,
|
||||
multiIf(
|
||||
rand() % 100 < 50,
|
||||
'Chrome',
|
||||
rand() % 100 < 80,
|
||||
'Safari',
|
||||
rand() % 100 < 95,
|
||||
'Firefox',
|
||||
'Edge'
|
||||
) AS browser,
|
||||
multiIf(
|
||||
rand() % 100 < 50,
|
||||
'iOS',
|
||||
rand() % 100 < 90,
|
||||
'Android',
|
||||
'Windows'
|
||||
) AS os,
|
||||
rand() % 600 AS time_spent_sec,
|
||||
rand() % 100 < 15 AS is_bounce,
|
||||
rand() % 100 < 10 AS is_qr_scan,
|
||||
concat('qr-', toString(rand() % 10 + 1)) AS qr_code_id,
|
||||
multiIf(
|
||||
rand() % 100 < 50,
|
||||
'visit',
|
||||
rand() % 100 < 70,
|
||||
'stay',
|
||||
rand() % 100 < 85,
|
||||
'interact',
|
||||
rand() % 100 < 93,
|
||||
'signup',
|
||||
rand() % 100 < 97,
|
||||
'subscription',
|
||||
'purchase'
|
||||
) AS conversion_type,
|
||||
rand() % 100 * 1.2 AS conversion_value,
|
||||
'{}' AS custom_data
|
||||
FROM
|
||||
numbers(200);
|
||||
|
||||
-- 为第四个链接创建400条记录
|
||||
INSERT INTO
|
||||
limq.link_events (
|
||||
event_id,
|
||||
event_time,
|
||||
date,
|
||||
link_id,
|
||||
channel_id,
|
||||
visitor_id,
|
||||
session_id,
|
||||
event_type,
|
||||
ip_address,
|
||||
country,
|
||||
city,
|
||||
referrer,
|
||||
utm_source,
|
||||
utm_medium,
|
||||
utm_campaign,
|
||||
user_agent,
|
||||
device_type,
|
||||
browser,
|
||||
os,
|
||||
time_spent_sec,
|
||||
is_bounce,
|
||||
is_qr_scan,
|
||||
qr_code_id,
|
||||
conversion_type,
|
||||
conversion_value,
|
||||
custom_data
|
||||
)
|
||||
SELECT
|
||||
generateUUIDv4() AS event_id,
|
||||
subtractDays(now(), rand() % 30) AS event_time,
|
||||
toDate(event_time) AS date,
|
||||
'44444444-4444-4444-4444-444444444444' AS link_id,
|
||||
'channel-2' AS channel_id,
|
||||
concat('visitor-', toString(rand() % 100 + 1)) AS visitor_id,
|
||||
concat('session-', toString(number % 60 + 1)) AS session_id,
|
||||
multiIf(
|
||||
rand() % 100 < 70,
|
||||
'click',
|
||||
rand() % 100 < 90,
|
||||
'redirect',
|
||||
rand() % 100 < 98,
|
||||
'conversion',
|
||||
'error'
|
||||
) AS event_type,
|
||||
concat('192.168.1.', toString(rand() % 255)) AS ip_address,
|
||||
multiIf(
|
||||
rand() % 100 < 60,
|
||||
'China',
|
||||
rand() % 100 < 85,
|
||||
'US',
|
||||
rand() % 100 < 95,
|
||||
'Japan',
|
||||
'Other'
|
||||
) AS country,
|
||||
multiIf(
|
||||
rand() % 100 < 60,
|
||||
'Beijing',
|
||||
rand() % 100 < 85,
|
||||
'New York',
|
||||
rand() % 100 < 95,
|
||||
'Tokyo',
|
||||
'Other'
|
||||
) AS city,
|
||||
multiIf(
|
||||
rand() % 100 < 30,
|
||||
'https://google.com',
|
||||
rand() % 100 < 50,
|
||||
'https://facebook.com',
|
||||
rand() % 100 < 65,
|
||||
'https://twitter.com',
|
||||
rand() % 100 < 75,
|
||||
'https://instagram.com',
|
||||
rand() % 100 < 85,
|
||||
'https://linkedin.com',
|
||||
rand() % 100 < 90,
|
||||
'https://bing.com',
|
||||
rand() % 100 < 95,
|
||||
'https://baidu.com',
|
||||
'direct'
|
||||
) AS referrer,
|
||||
multiIf(
|
||||
rand() % 100 < 40,
|
||||
'google',
|
||||
rand() % 100 < 70,
|
||||
'facebook',
|
||||
rand() % 100 < 90,
|
||||
'email',
|
||||
'direct'
|
||||
) AS utm_source,
|
||||
multiIf(
|
||||
rand() % 100 < 40,
|
||||
'cpc',
|
||||
rand() % 100 < 70,
|
||||
'social',
|
||||
rand() % 100 < 90,
|
||||
'email',
|
||||
'direct'
|
||||
) AS utm_medium,
|
||||
multiIf(
|
||||
rand() % 100 < 40,
|
||||
'summer_sale',
|
||||
rand() % 100 < 70,
|
||||
'product_launch',
|
||||
rand() % 100 < 90,
|
||||
'newsletter',
|
||||
'brand'
|
||||
) AS utm_campaign,
|
||||
'Mozilla/5.0' AS user_agent,
|
||||
multiIf(
|
||||
rand() % 100 < 60,
|
||||
'mobile',
|
||||
rand() % 100 < 85,
|
||||
'desktop',
|
||||
rand() % 100 < 95,
|
||||
'tablet',
|
||||
'other'
|
||||
) AS device_type,
|
||||
multiIf(
|
||||
rand() % 100 < 50,
|
||||
'Chrome',
|
||||
rand() % 100 < 80,
|
||||
'Safari',
|
||||
rand() % 100 < 95,
|
||||
'Firefox',
|
||||
'Edge'
|
||||
) AS browser,
|
||||
multiIf(
|
||||
rand() % 100 < 50,
|
||||
'iOS',
|
||||
rand() % 100 < 90,
|
||||
'Android',
|
||||
'Windows'
|
||||
) AS os,
|
||||
rand() % 400 AS time_spent_sec,
|
||||
rand() % 100 < 20 AS is_bounce,
|
||||
rand() % 100 < 25 AS is_qr_scan,
|
||||
concat('qr-', toString(rand() % 10 + 1)) AS qr_code_id,
|
||||
multiIf(
|
||||
rand() % 100 < 50,
|
||||
'visit',
|
||||
rand() % 100 < 70,
|
||||
'stay',
|
||||
rand() % 100 < 85,
|
||||
'interact',
|
||||
rand() % 100 < 93,
|
||||
'signup',
|
||||
rand() % 100 < 97,
|
||||
'subscription',
|
||||
'purchase'
|
||||
) AS conversion_type,
|
||||
rand() % 100 * 3.5 AS conversion_value,
|
||||
'{}' AS custom_data
|
||||
FROM
|
||||
numbers(400);
|
||||
|
||||
-- 插入link_daily_stats表数据
|
||||
INSERT INTO
|
||||
limq.link_daily_stats (
|
||||
date,
|
||||
link_id,
|
||||
total_clicks,
|
||||
unique_visitors,
|
||||
unique_sessions,
|
||||
total_time_spent,
|
||||
avg_time_spent,
|
||||
bounce_count,
|
||||
conversion_count,
|
||||
unique_referrers,
|
||||
mobile_count,
|
||||
tablet_count,
|
||||
desktop_count,
|
||||
qr_scan_count,
|
||||
total_conversion_value
|
||||
)
|
||||
SELECT
|
||||
subtractDays(today(), number) AS date,
|
||||
multiIf(
|
||||
number % 4 = 0,
|
||||
'11111111-1111-1111-1111-111111111111',
|
||||
number % 4 = 1,
|
||||
'22222222-2222-2222-2222-222222222222',
|
||||
number % 4 = 2,
|
||||
'33333333-3333-3333-3333-333333333333',
|
||||
'44444444-4444-4444-4444-444444444444'
|
||||
) AS link_id,
|
||||
50 + rand() % 100 AS total_clicks,
|
||||
30 + rand() % 50 AS unique_visitors,
|
||||
20 + rand() % 40 AS unique_sessions,
|
||||
(500 + rand() % 1000) * 60 AS total_time_spent,
|
||||
(rand() % 10) * 60 + rand() % 60 AS avg_time_spent,
|
||||
5 + rand() % 20 AS bounce_count,
|
||||
rand() % 30 AS conversion_count,
|
||||
3 + rand() % 8 AS unique_referrers,
|
||||
20 + rand() % 40 AS mobile_count,
|
||||
5 + rand() % 15 AS tablet_count,
|
||||
15 + rand() % 30 AS desktop_count,
|
||||
rand() % 10 AS qr_scan_count,
|
||||
rand() % 1000 * 2.5 AS total_conversion_value
|
||||
FROM
|
||||
numbers(30)
|
||||
WHERE
|
||||
number < 30;
|
||||
|
||||
-- 插入link_hourly_patterns表数据
|
||||
INSERT INTO
|
||||
limq.link_hourly_patterns (date, hour, link_id, visits, unique_visitors)
|
||||
SELECT
|
||||
subtractDays(today(), number % 7) AS date,
|
||||
number % 24 AS hour,
|
||||
multiIf(
|
||||
intDiv(number, 24) % 4 = 0,
|
||||
'11111111-1111-1111-1111-111111111111',
|
||||
intDiv(number, 24) % 4 = 1,
|
||||
'22222222-2222-2222-2222-222222222222',
|
||||
intDiv(number, 24) % 4 = 2,
|
||||
'33333333-3333-3333-3333-333333333333',
|
||||
'44444444-4444-4444-4444-444444444444'
|
||||
) AS link_id,
|
||||
5 + rand() % 20 AS visits,
|
||||
3 + rand() % 10 AS unique_visitors
|
||||
FROM
|
||||
numbers(672) -- 7天 x 24小时 x 4个链接
|
||||
WHERE
|
||||
number < 672;
|
||||
|
||||
-- 显示数据行数,验证插入成功
|
||||
SELECT
|
||||
'link_events 表行数:' AS metric,
|
||||
count() AS value
|
||||
FROM
|
||||
limq.link_events
|
||||
UNION
|
||||
ALL
|
||||
SELECT
|
||||
'link_daily_stats 表行数:',
|
||||
count()
|
||||
FROM
|
||||
limq.link_daily_stats
|
||||
UNION
|
||||
ALL
|
||||
SELECT
|
||||
'link_hourly_patterns 表行数:',
|
||||
count()
|
||||
FROM
|
||||
limq.link_hourly_patterns;
|
||||
331
scripts/db/sql/postgres/pg-query.js
Executable file
331
scripts/db/sql/postgres/pg-query.js
Executable file
@@ -0,0 +1,331 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// # 显示所有表
|
||||
// ./pg-query.js -t
|
||||
|
||||
// # 显示表结构
|
||||
// ./pg-query.js -d influencers
|
||||
|
||||
// # 显示样本数据,限制5行
|
||||
// ./pg-query.js -s posts -l 5
|
||||
|
||||
// # 查看表记录数
|
||||
// ./pg-query.js -c posts
|
||||
|
||||
// # 显示索引
|
||||
// ./pg-query.js -i posts
|
||||
|
||||
// # 显示外键
|
||||
// ./pg-query.js -f posts
|
||||
|
||||
// # 显示引用
|
||||
// ./pg-query.js -r influencers
|
||||
|
||||
// # 执行自定义查询
|
||||
// ./pg-query.js -q "SELECT * FROM influencers WHERE platform = 'Instagram' LIMIT 5"
|
||||
|
||||
// # 执行SQL文件
|
||||
// ./pg-query.js -e schema.sql
|
||||
|
||||
const { Client } = require('pg');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const yargs = require('yargs/yargs');
|
||||
const { hideBin } = require('yargs/helpers');
|
||||
|
||||
// 加载.env文件 - 使用正确的相对路径
|
||||
require('dotenv').config({ path: path.resolve(__dirname, '../../../.env') });
|
||||
|
||||
// 显示连接信息(不含密码)以便调试
|
||||
function getConnectionString() {
|
||||
// 使用.env中的DATABASE_URL
|
||||
const databaseUrl = process.env.DATABASE_URL;
|
||||
|
||||
if (!databaseUrl) {
|
||||
console.error('错误: 未找到DATABASE_URL环境变量');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 显示连接信息但隐藏密码
|
||||
const sanitizedUrl = databaseUrl.replace(/:[^:@]+@/, ':***@');
|
||||
console.log(`使用连接: ${sanitizedUrl}`);
|
||||
|
||||
return databaseUrl;
|
||||
}
|
||||
|
||||
// 创建一个新的客户端
|
||||
async function runQuery(query, params = []) {
|
||||
const client = new Client({
|
||||
connectionString: getConnectionString()
|
||||
});
|
||||
|
||||
try {
|
||||
await client.connect();
|
||||
console.log('数据库连接成功');
|
||||
const result = await client.query(query, params);
|
||||
return result.rows;
|
||||
} catch (err) {
|
||||
console.error('查询执行错误:', err.message);
|
||||
return null;
|
||||
} finally {
|
||||
await client.end();
|
||||
}
|
||||
}
|
||||
|
||||
// 显示所有表
|
||||
async function showTables() {
|
||||
const query = "SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' ORDER BY table_name;";
|
||||
const tables = await runQuery(query);
|
||||
|
||||
if (tables && tables.length > 0) {
|
||||
console.log('数据库中的表:');
|
||||
console.table(tables);
|
||||
} else {
|
||||
console.log('没有找到表或连接失败');
|
||||
}
|
||||
}
|
||||
|
||||
// 显示表结构
|
||||
async function showTableStructure(tableName) {
|
||||
const query = `
|
||||
SELECT
|
||||
column_name AS "列名",
|
||||
data_type AS "数据类型",
|
||||
CASE WHEN is_nullable = 'YES' THEN '允许为空' ELSE '不允许为空' END AS "是否可空",
|
||||
column_default AS "默认值",
|
||||
character_maximum_length AS "最大长度"
|
||||
FROM
|
||||
information_schema.columns
|
||||
WHERE
|
||||
table_schema = 'public' AND
|
||||
table_name = $1
|
||||
ORDER BY
|
||||
ordinal_position;
|
||||
`;
|
||||
|
||||
const columns = await runQuery(query, [tableName]);
|
||||
|
||||
if (columns && columns.length > 0) {
|
||||
console.log(`表 ${tableName} 的结构:`);
|
||||
console.table(columns);
|
||||
} else {
|
||||
console.log(`表 ${tableName} 不存在或连接失败`);
|
||||
}
|
||||
}
|
||||
|
||||
// 显示样本数据
|
||||
async function showSampleData(tableName, limit = 10) {
|
||||
const query = `SELECT * FROM "${tableName}" LIMIT ${limit};`;
|
||||
const data = await runQuery(query);
|
||||
|
||||
if (data && data.length > 0) {
|
||||
console.log(`表 ${tableName} 的样本数据 (${limit} 行):`);
|
||||
console.table(data);
|
||||
} else {
|
||||
console.log(`表 ${tableName} 为空或不存在`);
|
||||
}
|
||||
}
|
||||
|
||||
// 显示记录计数
|
||||
async function showRecordCount(tableName) {
|
||||
const query = `SELECT COUNT(*) AS "记录数" FROM "${tableName}";`;
|
||||
const count = await runQuery(query);
|
||||
|
||||
if (count) {
|
||||
console.log(`表 ${tableName} 的记录数:`);
|
||||
console.table(count);
|
||||
} else {
|
||||
console.log(`表 ${tableName} 不存在或连接失败`);
|
||||
}
|
||||
}
|
||||
|
||||
// 显示索引信息
|
||||
async function showIndexes(tableName) {
|
||||
const query = `
|
||||
SELECT
|
||||
indexname AS "索引名称",
|
||||
indexdef AS "索引定义"
|
||||
FROM
|
||||
pg_indexes
|
||||
WHERE
|
||||
tablename = $1
|
||||
ORDER BY
|
||||
indexname;
|
||||
`;
|
||||
|
||||
const indexes = await runQuery(query, [tableName]);
|
||||
|
||||
if (indexes && indexes.length > 0) {
|
||||
console.log(`表 ${tableName} 的索引:`);
|
||||
console.table(indexes);
|
||||
} else {
|
||||
console.log(`表 ${tableName} 没有索引或不存在`);
|
||||
}
|
||||
}
|
||||
|
||||
// 显示外键
|
||||
async function showForeignKeys(tableName) {
|
||||
const query = `
|
||||
SELECT
|
||||
conname AS "外键名称",
|
||||
pg_get_constraintdef(oid) AS "外键定义"
|
||||
FROM
|
||||
pg_constraint
|
||||
WHERE
|
||||
conrelid = $1::regclass AND contype = 'f';
|
||||
`;
|
||||
|
||||
const foreignKeys = await runQuery(query, [tableName]);
|
||||
|
||||
if (foreignKeys && foreignKeys.length > 0) {
|
||||
console.log(`表 ${tableName} 的外键:`);
|
||||
console.table(foreignKeys);
|
||||
} else {
|
||||
console.log(`表 ${tableName} 没有外键或不存在`);
|
||||
}
|
||||
}
|
||||
|
||||
// 显示引用当前表的外键
|
||||
async function showReferencingKeys(tableName) {
|
||||
const query = `
|
||||
SELECT
|
||||
c.conname AS "外键名称",
|
||||
t.relname AS "引用表",
|
||||
pg_get_constraintdef(c.oid) AS "外键定义"
|
||||
FROM
|
||||
pg_constraint c
|
||||
JOIN
|
||||
pg_class t ON c.conrelid = t.oid
|
||||
WHERE
|
||||
c.confrelid = $1::regclass AND c.contype = 'f';
|
||||
`;
|
||||
|
||||
const referencingKeys = await runQuery(query, [tableName]);
|
||||
|
||||
if (referencingKeys && referencingKeys.length > 0) {
|
||||
console.log(`引用表 ${tableName} 的外键关系:`);
|
||||
console.table(referencingKeys);
|
||||
} else {
|
||||
console.log(`没有找到引用表 ${tableName} 的外键关系`);
|
||||
}
|
||||
}
|
||||
|
||||
// 执行自定义查询
|
||||
async function executeQuery(query) {
|
||||
const result = await runQuery(query);
|
||||
|
||||
if (result) {
|
||||
console.log('查询结果:');
|
||||
console.table(result);
|
||||
} else {
|
||||
console.log('查询执行失败或无结果');
|
||||
}
|
||||
}
|
||||
|
||||
// 执行SQL文件
|
||||
async function executeSqlFile(filename) {
|
||||
try {
|
||||
const sql = fs.readFileSync(filename, 'utf8');
|
||||
console.log(`执行SQL文件: ${filename}`);
|
||||
await executeQuery(sql);
|
||||
} catch (err) {
|
||||
console.error(`执行SQL文件失败: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 主函数
|
||||
async function main() {
|
||||
try {
|
||||
const argv = yargs(hideBin(process.argv))
|
||||
.usage('PostgreSQL 查询工具\n\n用法: $0 [选项]')
|
||||
.option('t', {
|
||||
alias: 'tables',
|
||||
describe: '显示所有表',
|
||||
type: 'boolean'
|
||||
})
|
||||
.option('d', {
|
||||
alias: 'describe',
|
||||
describe: '显示表结构',
|
||||
type: 'string'
|
||||
})
|
||||
.option('s', {
|
||||
alias: 'sample',
|
||||
describe: '显示表样本数据',
|
||||
type: 'string'
|
||||
})
|
||||
.option('l', {
|
||||
alias: 'limit',
|
||||
describe: '样本数据行数限制',
|
||||
type: 'number',
|
||||
default: 10
|
||||
})
|
||||
.option('c', {
|
||||
alias: 'count',
|
||||
describe: '计算表中的记录数',
|
||||
type: 'string'
|
||||
})
|
||||
.option('i', {
|
||||
alias: 'indexes',
|
||||
describe: '显示表索引',
|
||||
type: 'string'
|
||||
})
|
||||
.option('f', {
|
||||
alias: 'foreign-keys',
|
||||
describe: '显示表外键关系',
|
||||
type: 'string'
|
||||
})
|
||||
.option('r', {
|
||||
alias: 'references',
|
||||
describe: '显示引用此表的外键',
|
||||
type: 'string'
|
||||
})
|
||||
.option('q', {
|
||||
alias: 'query',
|
||||
describe: '执行自定义SQL查询',
|
||||
type: 'string'
|
||||
})
|
||||
.option('e', {
|
||||
alias: 'execute-file',
|
||||
describe: '执行SQL文件',
|
||||
type: 'string'
|
||||
})
|
||||
.example('$0 -t', '显示所有表')
|
||||
.example('$0 -d influencers', '显示influencers表结构')
|
||||
.example('$0 -s posts -l 5', '显示posts表前5行数据')
|
||||
.epilog('更多信息请访问项目文档')
|
||||
.help()
|
||||
.alias('h', 'help')
|
||||
.argv;
|
||||
|
||||
if (argv.tables) {
|
||||
await showTables();
|
||||
} else if (argv.describe) {
|
||||
await showTableStructure(argv.describe);
|
||||
} else if (argv.sample) {
|
||||
await showSampleData(argv.sample, argv.limit);
|
||||
} else if (argv.count) {
|
||||
await showRecordCount(argv.count);
|
||||
} else if (argv.indexes) {
|
||||
await showIndexes(argv.indexes);
|
||||
} else if (argv.foreignKeys) {
|
||||
await showForeignKeys(argv.foreignKeys);
|
||||
} else if (argv.references) {
|
||||
await showReferencingKeys(argv.references);
|
||||
} else if (argv.query) {
|
||||
await executeQuery(argv.query);
|
||||
} else if (argv.executeFile) {
|
||||
await executeSqlFile(argv.executeFile);
|
||||
} else {
|
||||
yargs(hideBin(process.argv)).showHelp();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('程序执行错误:', err.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// 执行主函数
|
||||
main().catch(err => {
|
||||
console.error('程序执行错误:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
81
scripts/deploy.sh
Normal file
81
scripts/deploy.sh
Normal file
@@ -0,0 +1,81 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
set -x
|
||||
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
echo -e "${YELLOW}开始部署流程...${NC}"
|
||||
|
||||
# 首先加载环境变量
|
||||
if [ "$NODE_ENV" = "production" ]; then
|
||||
echo -e "${GREEN}加载生产环境配置...${NC}"
|
||||
set -a
|
||||
source .env.production
|
||||
set +a
|
||||
else
|
||||
echo -e "${GREEN}加载开发环境配置...${NC}"
|
||||
set -a
|
||||
source .env.development
|
||||
set +a
|
||||
fi
|
||||
|
||||
# 安装依赖
|
||||
echo -e "${GREEN}安装依赖...${NC}"
|
||||
NODE_ENV= pnpm install --ignore-workspace
|
||||
|
||||
# 生成 Prisma 客户端
|
||||
echo -e "${GREEN}生成 Prisma 客户端...${NC}"
|
||||
npx prisma generate
|
||||
|
||||
# 类型检查
|
||||
echo -e "${GREEN}运行类型检查...${NC}"
|
||||
pnpm tsc --noEmit
|
||||
|
||||
# 询问是否同步数据库架构
|
||||
echo -e "${YELLOW}是否需要同步数据库架构? (y/n)${NC}"
|
||||
read -r sync_db
|
||||
if [ "$sync_db" = "y" ] || [ "$sync_db" = "Y" ]; then
|
||||
echo -e "${GREEN}开始同步数据库架构...${NC}"
|
||||
if [ "$NODE_ENV" = "production" ]; then
|
||||
npx prisma db push
|
||||
else
|
||||
npx prisma db push
|
||||
fi
|
||||
else
|
||||
echo -e "${YELLOW}跳过数据库同步${NC}"
|
||||
fi
|
||||
|
||||
# 构建项目
|
||||
echo -e "${GREEN}构建项目...${NC}"
|
||||
pnpm build
|
||||
|
||||
# 检查并安装 PM2
|
||||
echo -e "${GREEN}检查 PM2...${NC}"
|
||||
if ! command -v pm2 &> /dev/null; then
|
||||
echo -e "${YELLOW}PM2 未安装,正在安装 5.4.3 版本...${NC}"
|
||||
pnpm add pm2@5.4.3 -g
|
||||
else
|
||||
PM2_VERSION=$(pm2 -v)
|
||||
if [ "$PM2_VERSION" != "5.4.3" ]; then
|
||||
echo -e "${YELLOW}错误: PM2 版本必须是 5.4.3,当前版本是 ${PM2_VERSION}${NC}"
|
||||
echo -e "${YELLOW}请运行以下命令更新 PM2:${NC}"
|
||||
echo -e "${YELLOW}pm2 kill && pnpm remove pm2 -g && rm -rf ~/.pm2 && pnpm add pm2@5.4.3 -g${NC}"
|
||||
exit 1
|
||||
else
|
||||
echo -e "${GREEN}PM2 5.4.3 已安装${NC}"
|
||||
fi
|
||||
fi
|
||||
|
||||
# 启动服务
|
||||
if [ "$NODE_ENV" = "production" ]; then
|
||||
echo -e "${GREEN}以生产模式启动服务...${NC}"
|
||||
pm2 start dist/src/main.js --name limq
|
||||
else
|
||||
echo -e "${GREEN}以开发模式启动服务...${NC}"
|
||||
pm2 start dist/src/main.js --name limq-dev --watch
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}部署完成!${NC}"
|
||||
Reference in New Issue
Block a user