erDiagram
weather_spots ||--o{ weather_records : "has"
weather_spots {
uuid id PK
varchar city_name
varchar country_code
decimal latitude
decimal longitude
boolean is_favorite
text notes
timestamp created_at
timestamp updated_at
}
weather_records {
uuid id PK
uuid weather_spot_id FK
decimal temperature
varchar temperature_unit
varchar condition
integer humidity
decimal wind_speed
integer wind_direction
decimal pressure
timestamp recorded_at
timestamp created_at
}
Purpose: Stores user-tracked cities/locations for weather monitoring.
Columns:
| Column Name | Data Type | Constraints | Description |
|---|---|---|---|
id |
UUID | PRIMARY KEY, NOT NULL | Unique identifier for each weather spot |
city_name |
VARCHAR(255) | NOT NULL | Name of the city (e.g., "San Francisco") |
country_code |
VARCHAR(2) | NULL | ISO 3166-1 alpha-2 country code (e.g., "US", "GB") |
latitude |
DECIMAL(10, 8) | NOT NULL | Latitude coordinate (-90 to 90) |
longitude |
DECIMAL(11, 8) | NOT NULL | Longitude coordinate (-180 to 180) |
is_favorite |
BOOLEAN | NOT NULL, DEFAULT false | Whether the spot is marked as favorite |
notes |
TEXT | NULL | User-provided notes about the city |
created_at |
TIMESTAMP WITH TIME ZONE | NOT NULL, DEFAULT NOW() | When the spot was created |
updated_at |
TIMESTAMP WITH TIME ZONE | NOT NULL, DEFAULT NOW() | When the spot was last updated |
Indexes:
- Primary Key:
id - Index:
idx_weather_spots_favoriteonis_favorite(for filtering favorites) - Index:
idx_weather_spots_locationon(latitude, longitude)(for geospatial queries) - Index:
idx_weather_spots_created_atoncreated_at(for sorting)
Example Data:
INSERT INTO weather_spots (id, city_name, country_code, latitude, longitude, is_favorite, notes)
VALUES (
'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
'San Francisco',
'US',
37.7749,
-122.4194,
true,
'My favorite city!'
);Purpose: Stores historical weather data for each tracked spot.
Columns:
| Column Name | Data Type | Constraints | Description |
|---|---|---|---|
id |
UUID | PRIMARY KEY, NOT NULL | Unique identifier for each weather record |
weather_spot_id |
UUID | FOREIGN KEY, NOT NULL | Reference to weather_spots.id |
temperature |
DECIMAL(5, 2) | NOT NULL | Temperature value (e.g., 18.50) |
temperature_unit |
VARCHAR(1) | NOT NULL, DEFAULT 'C' | Unit: 'C' (Celsius) or 'F' (Fahrenheit) |
condition |
VARCHAR(50) | NOT NULL | Weather condition description (e.g., "Partly Cloudy", "Sunny") |
humidity |
INTEGER | NULL | Humidity percentage (0-100) |
wind_speed |
DECIMAL(5, 2) | NULL | Wind speed (km/h or mph) |
wind_direction |
INTEGER | NULL | Wind direction in degrees (0-360, where 0 is North) |
pressure |
DECIMAL(7, 2) | NULL | Atmospheric pressure in hPa (hectopascals) |
recorded_at |
TIMESTAMP WITH TIME ZONE | NOT NULL | When the weather data was recorded (from API) |
created_at |
TIMESTAMP WITH TIME ZONE | NOT NULL, DEFAULT NOW() | When the record was created in database |
Indexes:
- Primary Key:
id - Foreign Key:
weather_spot_id→weather_spots.idON DELETE CASCADE - Index:
idx_weather_records_spot_idonweather_spot_id(for joining) - Index:
idx_weather_records_recorded_atonrecorded_at(for time-based queries) - Composite Index:
idx_weather_records_spot_recordedon(weather_spot_id, recorded_at DESC)(for efficient history queries)
Example Data:
INSERT INTO weather_records (
id,
weather_spot_id,
temperature,
temperature_unit,
condition,
humidity,
wind_speed,
wind_direction,
pressure,
recorded_at
)
VALUES (
'b2c3d4e5-f6a7-8901-bcde-f12345678901',
'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
18.50,
'C',
'Partly Cloudy',
75,
15.00,
270,
1012.50,
'2024-01-15 10:30:00+00'
);- One weather spot can have many weather records (historical data)
- When a weather spot is deleted, all associated weather records are automatically deleted (CASCADE)
- Foreign key constraint ensures data integrity
Query Example:
-- Get all weather records for a specific spot
SELECT wr.*
FROM weather_records wr
WHERE wr.weather_spot_id = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'
ORDER BY wr.recorded_at DESC
LIMIT 7;SELECT
ws.*,
wr.temperature,
wr.condition,
wr.recorded_at as last_weather_update
FROM weather_spots ws
LEFT JOIN LATERAL (
SELECT temperature, condition, recorded_at
FROM weather_records
WHERE weather_spot_id = ws.id
ORDER BY recorded_at DESC
LIMIT 1
) wr ON true
ORDER BY ws.created_at DESC;SELECT
temperature,
condition,
humidity,
wind_speed,
recorded_at
FROM weather_records
WHERE weather_spot_id = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'
AND recorded_at >= NOW() - INTERVAL '7 days'
ORDER BY recorded_at ASC;SELECT
ws.*,
wr.temperature,
wr.condition
FROM weather_spots ws
LEFT JOIN LATERAL (
SELECT temperature, condition
FROM weather_records
WHERE weather_spot_id = ws.id
ORDER BY recorded_at DESC
LIMIT 1
) wr ON true
WHERE ws.is_favorite = true
ORDER BY ws.city_name;latitudeBETWEEN -90 AND 90longitudeBETWEEN -180 AND 180humidityBETWEEN 0 AND 100 (if not NULL)wind_directionBETWEEN 0 AND 360 (if not NULL)temperature_unitIN ('C', 'F')
weather_records.weather_spot_id→weather_spots.idON DELETE CASCADE
- None (multiple records per spot are expected)
-- Enable UUID extension
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
-- Create weather_spots table
CREATE TABLE weather_spots (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
city_name VARCHAR(255) NOT NULL,
country_code VARCHAR(2),
latitude DECIMAL(10, 8) NOT NULL CHECK (latitude BETWEEN -90 AND 90),
longitude DECIMAL(11, 8) NOT NULL CHECK (longitude BETWEEN -180 AND 180),
is_favorite BOOLEAN NOT NULL DEFAULT false,
notes TEXT,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
);
-- Create weather_records table
CREATE TABLE weather_records (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
weather_spot_id UUID NOT NULL REFERENCES weather_spots(id) ON DELETE CASCADE,
temperature DECIMAL(5, 2) NOT NULL,
temperature_unit VARCHAR(1) NOT NULL DEFAULT 'C' CHECK (temperature_unit IN ('C', 'F')),
condition VARCHAR(50) NOT NULL,
humidity INTEGER CHECK (humidity BETWEEN 0 AND 100),
wind_speed DECIMAL(5, 2),
wind_direction INTEGER CHECK (wind_direction BETWEEN 0 AND 360),
pressure DECIMAL(7, 2),
recorded_at TIMESTAMP WITH TIME ZONE NOT NULL,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
);
-- Create indexes
CREATE INDEX idx_weather_spots_favorite ON weather_spots(is_favorite);
CREATE INDEX idx_weather_spots_location ON weather_spots(latitude, longitude);
CREATE INDEX idx_weather_spots_created_at ON weather_spots(created_at);
CREATE INDEX idx_weather_records_spot_id ON weather_records(weather_spot_id);
CREATE INDEX idx_weather_records_recorded_at ON weather_records(recorded_at);
CREATE INDEX idx_weather_records_spot_recorded ON weather_records(weather_spot_id, recorded_at DESC);
-- Create function to update updated_at timestamp
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Create trigger for updated_at
CREATE TRIGGER update_weather_spots_updated_at
BEFORE UPDATE ON weather_spots
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();┌─────────────────────────────────────────────────────────┐
│ weather_spots │
│ ┌──────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ id (PK) │ │ city_name │ │ coordinates │ │
│ │ │ │ country_code │ │ is_favorite │ │
│ │ │ │ notes │ │ timestamps │ │
│ └────┬─────┘ └──────────────┘ └──────────────┘ │
└───────┼─────────────────────────────────────────────────┘
│
│ 1:N relationship
│ (CASCADE DELETE)
│
▼
┌─────────────────────────────────────────────────────────┐
│ weather_records │
│ ┌──────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ id (PK) │ │ spot_id (FK) │ │ temperature │ │
│ │ │ │ condition │ │ humidity │ │
│ │ │ │ wind data │ │ pressure │ │
│ │ │ │ recorded_at │ │ timestamps │ │
│ └──────────┘ └──────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────┘
- UUIDs: Using UUIDs for primary keys provides better scalability and avoids ID enumeration attacks
- Timestamps: Using
TIMESTAMP WITH TIME ZONEensures proper timezone handling - CASCADE Delete: Deleting a spot automatically removes all its weather records
- Indexes: Optimized for common query patterns (favorites, location, history)
- Normalization: Weather data is normalized to avoid duplication
- Flexibility: Optional fields allow for different weather API responses