Warum überhaupt ein SEO-Dashboard?
Du kennst das Spiel.
Die Agentur erzählt dir, die Rankings steigen. Der Social-Media-Manager sagt, die Klicks nehmen zu. Der Content-Schreiber meint, die Artikel performen gut.
Das Problem: Keiner zeigt dir harte Zahlen. Alle reden. Niemand beweist.
Du willst ein Dashboard. Eines, das dir zeigt:
- Wo stehen wir bei Google?
- Welche Keywords ranken?
- Wie entwickelt sich der Traffic?
- Was bringen die einzelnen Maßnahmen?
Und zwar nicht in fünf verschiedenen Tools, sondern an einem Ort.
Genau darum geht’s hier.
Welche Datenquellen brauche ich für ein SEO-Dashboard?
Ein vernünftiges SEO-Dashboard zapft mehrere Quellen an.
Die wichtigsten Datenquellen:
Google Search Console
Zeigt dir:
- Suchanfragen, über die User zu dir kommen
- Klicks, Impressionen, CTR, Position
- Indexierungsstatus
- Mobile Usability
API-Zugang: Ja, über Google API
Datenbasis: Google Cloud SQL (intern), du bekommst nur Lesezugriff
Google Analytics (GA4)
Zeigt dir:
- Traffic-Quellen
- Nutzerverhalten
- Conversions
- Sitzungsdauer, Absprungrate
API-Zugang: Ja, über Google Analytics Data API
Datenbasis: Google BigQuery (Export möglich)
SEO-Tools (Sistrix, Ahrefs, SEMrush)
Zeigen dir:
- Sichtbarkeitsindex
- Backlink-Profil
- Keyword-Rankings
- Wettbewerber-Analysen
API-Zugang: Ja, gegen Aufpreis
Datenbasis: Proprietäre Crawler-Datenbanken
Dein CMS (WordPress, Typo3, etc.)
Zeigt dir:
- Veröffentlichte Artikel
- Interne Verlinkung
- Meta-Daten (Title, Description)
Datenbasis: Meist MySQL oder PostgreSQL
Dein CRM/ERP
Zeigt dir:
- Anfragen aus organischem Traffic
- Conversions
- Umsatz pro Traffic-Quelle
Datenbasis: SQL Server, Access, oder was auch immer du hast
Wie hole ich Daten aus der Google Search Console?
Google bietet eine API. Die ist kostenfrei, aber limitiert.
Rate Limits (Stand 2024):
- 1.200 Abfragen pro Minute
- Max. 25.000 Zeilen pro Abfrage
Python-Script für den Datenabruf:
from google.oauth2 import service_account
from googleapiclient.discovery import build
import pyodbc
import json
from datetime import datetime, timedelta
# Authentifizierung
SCOPES = ['https://www.googleapis.com/auth/webmasters.readonly']
SERVICE_ACCOUNT_FILE = 'path/to/service-account.json'
credentials = service_account.Credentials.from_service_account_file(
SERVICE_ACCOUNT_FILE, scopes=SCOPES)
service = build('searchconsole', 'v1', credentials=credentials)
# Daten abrufen
site_url = 'https://www.deine-website.de/'
start_date = (datetime.now() - timedelta(days=30)).strftime('%Y-%m-%d')
end_date = datetime.now().strftime('%Y-%m-%d')
request = {
'startDate': start_date,
'endDate': end_date,
'dimensions': ['query', 'page', 'date'],
'rowLimit': 25000
}
response = service.searchanalytics().query(
siteUrl=site_url, body=request).execute()
# In SQL Server schreiben
conn = pyodbc.connect(
'DRIVER={ODBC Driver 17 for SQL Server};'
'SERVER=DEIN_SERVER;'
'DATABASE=SEO_DWH;'
'Trusted_Connection=yes;'
)
cursor = conn.cursor()
for row in response.get('rows', []):
query = row['keys'][0]
page = row['keys'][1]
date = row['keys'][2]
clicks = row.get('clicks', 0)
impressions = row.get('impressions', 0)
ctr = row.get('ctr', 0)
position = row.get('position', 0)
cursor.execute("""
INSERT INTO dbo.GSC_RawData
(Query, Page, Date, Clicks, Impressions, CTR, Position, ImportedAt)
VALUES (?, ?, ?, ?, ?, ?, ?, GETUTCDATE())
""", query, page, date, clicks, impressions, ctr, position)
conn.commit()
cursor.close()
conn.close()
SQL Server: Tabelle für Google Search Console Daten
-- Rohdaten-Tabelle
CREATE TABLE dbo.GSC_RawData (
RecordID BIGINT IDENTITY(1,1) PRIMARY KEY,
Query NVARCHAR(500) NOT NULL,
Page NVARCHAR(1000) NOT NULL,
Date DATE NOT NULL,
Clicks INT NOT NULL,
Impressions INT NOT NULL,
CTR FLOAT NOT NULL,
Position FLOAT NOT NULL,
ImportedAt DATETIME2 DEFAULT GETUTCDATE(),
INDEX IX_Date_Query (Date DESC, Query),
INDEX IX_Page_Date (Page, Date DESC)
);
-- Aggregierte Ansicht: Top Keywords nach Klicks
CREATE VIEW dbo.vw_TopKeywordsByClicks
AS
SELECT
Query,
SUM(Clicks) AS TotalClicks,
SUM(Impressions) AS TotalImpressions,
AVG(CTR) AS AvgCTR,
AVG(Position) AS AvgPosition,
COUNT(DISTINCT Date) AS DaysTracked
FROM dbo.GSC_RawData
WHERE Date >= DATEADD(MONTH, -3, GETDATE())
GROUP BY Query
HAVING SUM(Clicks) > 0;
-- Stored Procedure: Top 20 Keywords mit Entwicklung
CREATE PROCEDURE dbo.GetTopKeywordTrends
@DaysBack INT = 30
AS
BEGIN
WITH CurrentPeriod AS (
SELECT
Query,
SUM(Clicks) AS CurrentClicks,
AVG(Position) AS CurrentPosition
FROM dbo.GSC_RawData
WHERE Date >= DATEADD(DAY, -@DaysBack, GETDATE())
GROUP BY Query
),
PreviousPeriod AS (
SELECT
Query,
SUM(Clicks) AS PreviousClicks,
AVG(Position) AS PreviousPosition
FROM dbo.GSC_RawData
WHERE Date >= DATEADD(DAY, -(@DaysBack * 2), GETDATE())
AND Date < DATEADD(DAY, -@DaysBack, GETDATE())
GROUP BY Query
)
SELECT TOP 20
c.Query,
c.CurrentClicks,
c.CurrentPosition,
ISNULL(p.PreviousClicks, 0) AS PreviousClicks,
ISNULL(p.PreviousPosition, 0) AS PreviousPosition,
(c.CurrentClicks - ISNULL(p.PreviousClicks, 0)) AS ClickDelta,
CASE
WHEN ISNULL(p.PreviousClicks, 0) = 0 THEN NULL
ELSE ((c.CurrentClicks - p.PreviousClicks) * 100.0 / p.PreviousClicks)
END AS ClickChangePercent,
(c.CurrentPosition - ISNULL(p.PreviousPosition, 0)) AS PositionDelta
FROM CurrentPeriod c
LEFT JOIN PreviousPeriod p ON c.Query = p.Query
ORDER BY c.CurrentClicks DESC;
END;
Wie integriere ich Google Analytics 4?
GA4 hat die Datenstruktur komplett umgekrempelt.
Die alte Universal Analytics API funktioniert nicht mehr.
GA4 Data API (Python):
from google.analytics.data_v1beta import BetaAnalyticsDataClient
from google.analytics.data_v1beta.types import (
RunReportRequest,
DateRange,
Dimension,
Metric,
)
import pyodbc
from datetime import datetime, timedelta
# Authentifizierung
property_id = 'YOUR_GA4_PROPERTY_ID'
client = BetaAnalyticsDataClient()
# Report abrufen
request = RunReportRequest(
property=f"properties/{property_id}",
date_ranges=[DateRange(
start_date=(datetime.now() - timedelta(days=30)).strftime('%Y-%m-%d'),
end_date=datetime.now().strftime('%Y-%m-%d')
)],
dimensions=[
Dimension(name="date"),
Dimension(name="sessionSource"),
Dimension(name="sessionMedium"),
Dimension(name="landingPage"),
],
metrics=[
Metric(name="sessions"),
Metric(name="totalUsers"),
Metric(name="conversions"),
Metric(name="averageSessionDuration"),
],
)
response = client.run_report(request)
# In SQL Server schreiben
conn = pyodbc.connect(
'DRIVER={ODBC Driver 17 for SQL Server};'
'SERVER=DEIN_SERVER;DATABASE=SEO_DWH;Trusted_Connection=yes;'
)
cursor = conn.cursor()
for row in response.rows:
date = row.dimension_values[0].value
source = row.dimension_values[1].value
medium = row.dimension_values[2].value
landing_page = row.dimension_values[3].value
sessions = int(row.metric_values[0].value)
users = int(row.metric_values[1].value)
conversions = float(row.metric_values[2].value)
avg_duration = float(row.metric_values[3].value)
cursor.execute("""
INSERT INTO dbo.GA4_RawData
(Date, Source, Medium, LandingPage, Sessions, Users,
Conversions, AvgSessionDuration, ImportedAt)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, GETUTCDATE())
""", date, source, medium, landing_page, sessions, users,
conversions, avg_duration)
conn.commit()
cursor.close()
conn.close()
SQL Server: GA4-Tabellen
-- GA4 Rohdaten
CREATE TABLE dbo.GA4_RawData (
RecordID BIGINT IDENTITY(1,1) PRIMARY KEY,
Date DATE NOT NULL,
Source NVARCHAR(255),
Medium NVARCHAR(255),
LandingPage NVARCHAR(1000),
Sessions INT,
Users INT,
Conversions FLOAT,
AvgSessionDuration FLOAT,
ImportedAt DATETIME2 DEFAULT GETUTCDATE(),
INDEX IX_Date_Source (Date DESC, Source, Medium),
INDEX IX_LandingPage (LandingPage, Date DESC)
);
-- Organischer Traffic nach Landing Page
CREATE VIEW dbo.vw_OrganicTrafficByPage
AS
SELECT
LandingPage,
SUM(Sessions) AS TotalSessions,
SUM(Users) AS TotalUsers,
SUM(Conversions) AS TotalConversions,
AVG(AvgSessionDuration) AS AvgDuration,
COUNT(DISTINCT Date) AS DaysTracked
FROM dbo.GA4_RawData
WHERE Medium = 'organic'
AND Date >= DATEADD(MONTH, -3, GETDATE())
GROUP BY LandingPage;
-- Combined View: GSC + GA4
CREATE VIEW dbo.vw_SEO_Performance_Combined
AS
SELECT
gsc.Page,
gsc.Date,
SUM(gsc.Clicks) AS GSC_Clicks,
SUM(gsc.Impressions) AS GSC_Impressions,
AVG(gsc.Position) AS GSC_AvgPosition,
SUM(ga.Sessions) AS GA_Sessions,
SUM(ga.Conversions) AS GA_Conversions
FROM dbo.GSC_RawData gsc
LEFT JOIN dbo.GA4_RawData ga
ON gsc.Page = ga.LandingPage
AND gsc.Date = ga.Date
AND ga.Medium = 'organic'
WHERE gsc.Date >= DATEADD(MONTH, -3, GETDATE())
GROUP BY gsc.Page, gsc.Date;
Wie zapfe ich SEO-Tools wie Sistrix oder Ahrefs an?
Die meisten SEO-Tools bieten APIs. Aber nicht alle sind gleich gut.
Sistrix API
Datenbasis: Eigene Crawler-Datenbank, deutscher Markt gut abgedeckt
Funktionsumfang:
- Sichtbarkeitsindex
- Keyword-Rankings
- Backlink-Daten
- Konkurrenz-Analysen
API-Zugang: Ja, gegen Aufpreis (ca. 100-200 EUR/Monat extra)
Aktuell: Ja, wird aktiv weiterentwickelt
Nachfolger: Keine absehbar
Sistrix API-Abfrage (VBA für Access):
Public Function GetSistrixVisibility( _
strDomain As String, _
strAPIKey As String) As Double
On Error GoTo ErrorHandler
Dim objHTTP As Object
Dim strURL As String
Dim strResponse As String
Dim dblVisibility As Double
Set objHTTP = CreateObject("MSXML2.XMLHTTP")
strURL = "https://api.sistrix.com/visibility.index" & _
"?domain=" & strDomain & _
"&api_key=" & strAPIKey & _
"&format=json"
objHTTP.Open "GET", strURL, False
objHTTP.Send
strResponse = objHTTP.responseText
' JSON parsen (vereinfacht)
' In Produktion: JSON-Parser wie VBA-JSON verwenden
dblVisibility = ExtractVisibilityFromJSON(strResponse)
Set objHTTP = Nothing
GetSistrixVisibility = dblVisibility
Exit Function
ErrorHandler:
GetSistrixVisibility = 0
End Function
' Historische Sichtbarkeit in SQL Server speichern
Public Sub ImportSistrixHistory( _
strDomain As String, _
strAPIKey As String)
Dim cn As Object
Dim cmd As Object
Dim objHTTP As Object
Dim strURL As String
Dim strResponse As String
Set objHTTP = CreateObject("MSXML2.XMLHTTP")
strURL = "https://api.sistrix.com/visibility.history" & _
"?domain=" & strDomain & _
"&api_key=" & strAPIKey & _
"&format=json"
objHTTP.Open "GET", strURL, False
objHTTP.Send
strResponse = objHTTP.responseText
' In SQL Server schreiben
Set cn = CreateObject("ADODB.Connection")
cn.Open "Provider=SQLOLEDB;Data Source=DEIN_SERVER;" & _
"Initial Catalog=SEO_DWH;Integrated Security=SSPI;"
' JSON parsen und einfügen
' (hier vereinfacht, in Produktion robuster)
cn.Close
Set cn = Nothing
Set objHTTP = Nothing
End Sub
SQL Server: Sistrix-Daten
-- Sichtbarkeitsindex-Historie
CREATE TABLE dbo.Sistrix_VisibilityHistory (
RecordID INT IDENTITY(1,1) PRIMARY KEY,
Domain NVARCHAR(255) NOT NULL,
Date DATE NOT NULL,
VisibilityIndex FLOAT NOT NULL,
ImportedAt DATETIME2 DEFAULT GETUTCDATE(),
UNIQUE (Domain, Date),
INDEX IX_Domain_Date (Domain, Date DESC)
);
-- Keyword-Rankings
CREATE TABLE dbo.Sistrix_KeywordRankings (
RecordID BIGINT IDENTITY(1,1) PRIMARY KEY,
Domain NVARCHAR(255) NOT NULL,
Keyword NVARCHAR(500) NOT NULL,
Date DATE NOT NULL,
Position INT,
SearchVolume INT,
Competition FLOAT,
ImportedAt DATETIME2 DEFAULT GETUTCDATE(),
INDEX IX_Domain_Date (Domain, Date DESC),
INDEX IX_Keyword (Keyword, Date DESC)
);
-- Sichtbarkeits-Trend
CREATE PROCEDURE dbo.GetVisibilityTrend
@Domain NVARCHAR(255),
@MonthsBack INT = 12
AS
BEGIN
SELECT
Date,
VisibilityIndex,
LAG(VisibilityIndex, 1) OVER (ORDER BY Date) AS PreviousIndex,
VisibilityIndex - LAG(VisibilityIndex, 1) OVER (ORDER BY Date) AS Delta,
CASE
WHEN LAG(VisibilityIndex, 1) OVER (ORDER BY Date) = 0 THEN NULL
ELSE ((VisibilityIndex - LAG(VisibilityIndex, 1) OVER (ORDER BY Date))
* 100.0 / LAG(VisibilityIndex, 1) OVER (ORDER BY Date))
END AS PercentChange
FROM dbo.Sistrix_VisibilityHistory
WHERE Domain = @Domain
AND Date >= DATEADD(MONTH, -@MonthsBack, GETDATE())
ORDER BY Date ASC;
END;
Ahrefs API
Datenbasis: Eigene Crawler-Datenbank, weltweit größter Backlink-Index
Funktionsumfang:
- Backlink-Profil
- Keyword-Rankings (begrenzt)
- Domain Rating (DR)
- URL Rating (UR)
API-Zugang: Ja, ab Advanced Plan (ca. 200 USD/Monat)
Aktuell: Ja, sehr aktiv
Nachfolger: Keine absehbar
Ahrefs via T-SQL (mit SQLCLR oder Python Integration):
-- Backlink-Daten von Ahrefs
CREATE TABLE dbo.Ahrefs_Backlinks (
BacklinkID BIGINT IDENTITY(1,1) PRIMARY KEY,
TargetDomain NVARCHAR(255) NOT NULL,
SourceURL NVARCHAR(1000) NOT NULL,
TargetURL NVARCHAR(1000) NOT NULL,
AnchorText NVARCHAR(500),
DomainRating INT,
URLRating INT,
DoFollow BIT,
FirstSeen DATE,
LastSeen DATE,
ImportedAt DATETIME2 DEFAULT GETUTCDATE(),
INDEX IX_TargetDomain (TargetDomain, LastSeen DESC),
INDEX IX_DomainRating (DomainRating DESC)
);
-- Backlink-Qualität bewerten
CREATE PROCEDURE dbo.AnalyzeBacklinkQuality
@Domain NVARCHAR(255)
AS
BEGIN
SELECT
COUNT(*) AS TotalBacklinks,
SUM(CASE WHEN DoFollow = 1 THEN 1 ELSE 0 END) AS DoFollowCount,
SUM(CASE WHEN DoFollow = 0 THEN 1 ELSE 0 END) AS NoFollowCount,
AVG(CAST(DomainRating AS FLOAT)) AS AvgDomainRating,
SUM(CASE WHEN DomainRating >= 50 THEN 1 ELSE 0 END) AS HighQualityLinks,
SUM(CASE WHEN DomainRating < 20 THEN 1 ELSE 0 END) AS LowQualityLinks,
COUNT(DISTINCT LEFT(SourceURL, CHARINDEX('/', SourceURL, 9) - 1)) AS UniqueDomains
FROM dbo.Ahrefs_Backlinks
WHERE TargetDomain = @Domain;
END;
Wie integriere ich WordPress-Daten?
WordPress läuft meist auf MySQL.
Du willst wissen: Welche Artikel sind publiziert? Welche Meta-Daten haben sie?
Direkter MySQL-Zugriff via Linked Server (SQL Server):
-- Linked Server zu WordPress MySQL einrichten
EXEC sp_addlinkedserver
@server = 'WP_MySQL',
@srvproduct = 'MySQL',
@provider = 'MSDASQL',
@datasrc = 'WORDPRESS_SERVER',
@catalog = 'wordpress_db';
EXEC sp_addlinkedsrvlogin
@rmtsrvname = 'WP_MySQL',
@useself = 'FALSE',
@locallogin = NULL,
@rmtuser = 'wp_user',
@rmtpassword = 'wp_password';
-- WordPress-Posts abrufen
SELECT
ID,
post_title,
post_name, -- Slug
post_date,
post_status,
post_type
FROM OPENQUERY(WP_MySQL,
'SELECT ID, post_title, post_name, post_date, post_status, post_type
FROM wp_posts
WHERE post_status = ''publish''
AND post_type = ''post''')
WHERE post_date >= DATEADD(MONTH, -6, GETDATE());
-- WordPress Meta-Daten (Yoast SEO)
SELECT
p.ID,
p.post_title,
MAX(CASE WHEN pm.meta_key = '_yoast_wpseo_title' THEN pm.meta_value END) AS SEO_Title,
MAX(CASE WHEN pm.meta_key = '_yoast_wpseo_metadesc' THEN pm.meta_value END) AS SEO_Description,
MAX(CASE WHEN pm.meta_key = '_yoast_wpseo_focuskw' THEN pm.meta_value END) AS Focus_Keyword
FROM OPENQUERY(WP_MySQL, 'SELECT * FROM wp_posts WHERE post_status = ''publish''') p
LEFT JOIN OPENQUERY(WP_MySQL, 'SELECT * FROM wp_postmeta') pm ON p.ID = pm.post_id
WHERE p.post_type = 'post'
GROUP BY p.ID, p.post_title;
Alternativer Weg: WordPress REST API (Access VBA):
Public Function GetWordPressPosts(strSiteURL As String) As ADODB.Recordset
On Error GoTo ErrorHandler
Dim objHTTP As Object
Dim strURL As String
Dim strResponse As String
Dim rs As ADODB.Recordset
Set objHTTP = CreateObject("MSXML2.XMLHTTP")
Set rs = New ADODB.Recordset
' Recordset-Struktur definieren
rs.Fields.Append "PostID", adInteger
rs.Fields.Append "Title", adVarWChar, 500
rs.Fields.Append "Slug", adVarWChar, 200
rs.Fields.Append "PublishedDate", adDate
rs.Fields.Append "URL", adVarWChar, 1000
rs.Open
strURL = strSiteURL & "/wp-json/wp/v2/posts?per_page=100"
objHTTP.Open "GET", strURL, False
objHTTP.Send
strResponse = objHTTP.responseText
' JSON parsen (mit VBA-JSON Library)
Dim objJSON As Object
Set objJSON = ParseJson(strResponse)
Dim i As Long
For i = 1 To objJSON.Count
rs.AddNew
rs("PostID") = objJSON(i)("id")
rs("Title") = objJSON(i)("title")("rendered")
rs("Slug") = objJSON(i)("slug")
rs("PublishedDate") = CDate(Replace(objJSON(i)("date"), "T", " "))
rs("URL") = objJSON(i)("link")
rs.Update
Next i
rs.MoveFirst
Set GetWordPressPosts = rs
Set objHTTP = Nothing
Exit Function
ErrorHandler:
MsgBox "Fehler beim Abrufen: " & Err.Description
Set GetWordPressPosts = Nothing
End Function
Wie baue ich ein Data Warehouse für SEO-Daten?
Verschiedene Quellen, unterschiedliche Strukturen.
Du brauchst eine zentrale Stelle.
Dimensional Model (Kimball-Style):
-- Dimensions-Tabellen
-- Dim_Date
CREATE TABLE dbo.Dim_Date (
DateKey INT PRIMARY KEY,
Date DATE NOT NULL,
Year INT NOT NULL,
Quarter INT NOT NULL,
Month INT NOT NULL,
Week INT NOT NULL,
DayOfWeek INT NOT NULL,
DayName NVARCHAR(20),
MonthName NVARCHAR(20),
IsWeekend BIT
);
-- Dim_Page
CREATE TABLE dbo.Dim_Page (
PageKey INT IDENTITY(1,1) PRIMARY KEY,
PageURL NVARCHAR(1000) NOT NULL UNIQUE,
PageTitle NVARCHAR(500),
ContentType NVARCHAR(50), -- 'Article', 'Product', 'Category'
PublishedDate DATE,
LastModified DATE,
WordCount INT,
IsCurrent BIT DEFAULT 1,
EffectiveFrom DATE,
EffectiveTo DATE
);
-- Dim_Keyword
CREATE TABLE dbo.Dim_Keyword (
KeywordKey INT IDENTITY(1,1) PRIMARY KEY,
Keyword NVARCHAR(500) NOT NULL UNIQUE,
SearchVolume INT,
Competition FLOAT,
CPC FLOAT,
Intent NVARCHAR(50), -- 'Informational', 'Commercial', 'Transactional'
Category NVARCHAR(100)
);
-- Dim_Source
CREATE TABLE dbo.Dim_Source (
SourceKey INT IDENTITY(1,1) PRIMARY KEY,
SourceName NVARCHAR(100) NOT NULL UNIQUE,
SourceType NVARCHAR(50), -- 'Organic', 'Direct', 'Referral', 'Social', 'Paid'
Medium NVARCHAR(50)
);
-- Fact Table: SEO Performance
CREATE TABLE dbo.Fact_SEO_Performance (
FactID BIGINT IDENTITY(1,1) PRIMARY KEY,
DateKey INT NOT NULL,
PageKey INT NOT NULL,
KeywordKey INT,
SourceKey INT,
-- Measures from GSC
GSC_Clicks INT,
GSC_Impressions INT,
GSC_CTR FLOAT,
GSC_Position FLOAT,
-- Measures from GA4
GA_Sessions INT,
GA_Users INT,
GA_Conversions FLOAT,
GA_AvgSessionDuration FLOAT,
GA_BounceRate FLOAT,
-- Calculated
ConversionRate AS (CASE WHEN GA_Sessions > 0 THEN GA_Conversions * 100.0 / GA_Sessions ELSE 0 END),
FOREIGN KEY (DateKey) REFERENCES Dim_Date(DateKey),
FOREIGN KEY (PageKey) REFERENCES Dim_Page(PageKey),
FOREIGN KEY (KeywordKey) REFERENCES Dim_Keyword(KeywordKey),
FOREIGN KEY (SourceKey) REFERENCES Dim_Source(SourceKey),
INDEX IX_Date_Page (DateKey, PageKey),
INDEX IX_Keyword (KeywordKey, DateKey)
);
-- ETL: GSC-Daten ins DWH laden
CREATE PROCEDURE dbo.ETL_LoadGSCData
AS
BEGIN
SET NOCOUNT ON;
-- Neue Keywords in Dimension einfügen
INSERT INTO dbo.Dim_Keyword (Keyword)
SELECT DISTINCT Query
FROM dbo.GSC_RawData
WHERE Query NOT IN (SELECT Keyword FROM dbo.Dim_Keyword);
-- Neue Pages in Dimension einfügen
INSERT INTO dbo.Dim_Page (PageURL)
SELECT DISTINCT Page
FROM dbo.GSC_RawData
WHERE Page NOT IN (SELECT PageURL FROM dbo.Dim_Page);
-- Fact-Daten laden
INSERT INTO dbo.Fact_SEO_Performance (
DateKey, PageKey, KeywordKey,
GSC_Clicks, GSC_Impressions, GSC_CTR, GSC_Position
)
SELECT
CAST(CONVERT(VARCHAR(8), gsc.Date, 112) AS INT) AS DateKey,
p.PageKey,
k.KeywordKey,
SUM(gsc.Clicks) AS GSC_Clicks,
SUM(gsc.Impressions) AS GSC_Impressions,
AVG(gsc.CTR) AS GSC_CTR,
AVG(gsc.Position) AS GSC_Position
FROM dbo.GSC_RawData gsc
INNER JOIN dbo.Dim_Page p ON gsc.Page = p.PageURL
INNER JOIN dbo.Dim_Keyword k ON gsc.Query = k.Keyword
WHERE NOT EXISTS (
SELECT 1 FROM dbo.Fact_SEO_Performance f
WHERE f.DateKey = CAST(CONVERT(VARCHAR(8), gsc.Date, 112) AS INT)
AND f.PageKey = p.PageKey
AND f.KeywordKey = k.KeywordKey
)
GROUP BY gsc.Date, p.PageKey, k.KeywordKey;
END;
Wie erstelle ich aussagekräftige Reports?
Daten hast du jetzt. Reports fehlen noch.
Top-Seiten nach Performance:
CREATE PROCEDURE dbo.Report_TopPerformingPages
@StartDate DATE,
@EndDate DATE,
@TopN INT = 20
AS
BEGIN
SELECT TOP (@TopN)
p.PageURL,
p.PageTitle,
SUM(f.GSC_Clicks) AS TotalClicks,
SUM(f.GSC_Impressions) AS TotalImpressions,
AVG(f.GSC_Position) AS AvgPosition,
SUM(f.GA_Conversions) AS TotalConversions,
SUM(f.GA_Sessions) AS TotalSessions,
CASE
WHEN SUM(f.GA_Sessions) > 0
THEN SUM(f.GA_Conversions) * 100.0 / SUM(f.GA_Sessions)
ELSE 0
END AS ConversionRate
FROM dbo.Fact_SEO_Performance f
INNER JOIN dbo.Dim_Date d ON f.DateKey = d.DateKey
INNER JOIN dbo.Dim_Page p ON f.PageKey = p.PageKey
WHERE d.Date BETWEEN @StartDate AND @EndDate
GROUP BY p.PageURL, p.PageTitle
ORDER BY TotalClicks DESC;
END;
Keyword-Kannibalisierung erkennen:
CREATE PROCEDURE dbo.Detect_KeywordCannibalization
@MinPages INT = 2
AS
BEGIN
-- Finde Keywords, die von mehreren Seiten ranken
WITH KeywordPageCount AS (
SELECT
k.Keyword,
COUNT(DISTINCT p.PageURL) AS PageCount,
STRING_AGG(p.PageURL, '; ') WITHIN GROUP (ORDER BY AVG(f.GSC_Position) ASC) AS RankingPages,
AVG(f.GSC_Position) AS AvgPosition
FROM dbo.Fact_SEO_Performance f
INNER JOIN dbo.Dim_Keyword k ON f.KeywordKey = k.KeywordKey
INNER JOIN dbo.Dim_Page p ON f.PageKey = p.PageKey
INNER JOIN dbo.Dim_Date d ON f.DateKey = d.DateKey
WHERE d.Date >= DATEADD(MONTH, -1, GETDATE())
AND f.GSC_Clicks > 0
GROUP BY k.Keyword
HAVING COUNT(DISTINCT p.PageURL) >= @MinPages
)
SELECT
Keyword,
PageCount,
RankingPages,
AvgPosition,
CASE
WHEN PageCount > 3 THEN 'High Risk'
WHEN PageCount = 3 THEN 'Medium Risk'
ELSE 'Low Risk'
END AS RiskLevel
FROM KeywordPageCount
ORDER BY PageCount DESC, AvgPosition ASC;
END;
SEO-ROI berechnen:
CREATE PROCEDURE dbo.Calculate_SEO_ROI
@StartDate DATE,
@EndDate DATE,
@InvestmentAmount FLOAT
AS
BEGIN
-- Traffic-Value berechnen (vereinfacht)
DECLARE @OrganicConversions FLOAT;
DECLARE @ConversionValue FLOAT = 50; -- Durchschnittswert pro Conversion
SELECT @OrganicConversions = SUM(f.GA_Conversions)
FROM dbo.Fact_SEO_Performance f
INNER JOIN dbo.Dim_Date d ON f.DateKey = d.DateKey
INNER JOIN dbo.Dim_Source s ON f.SourceKey = s.SourceKey
WHERE d.Date BETWEEN @StartDate AND @EndDate
AND s.SourceType = 'Organic';
DECLARE @Revenue FLOAT = @OrganicConversions * @ConversionValue;
DECLARE @ROI FLOAT = ((@Revenue - @InvestmentAmount) / @InvestmentAmount) * 100;
SELECT
@StartDate AS PeriodStart,
@EndDate AS PeriodEnd,
@InvestmentAmount AS Investment,
@OrganicConversions AS Conversions,
@Revenue AS EstimatedRevenue,
@ROI AS ROI_Percent,
CASE
WHEN @ROI > 100 THEN 'Profitable'
WHEN @ROI BETWEEN 0 AND 100 THEN 'Break-even Zone'
ELSE 'Loss'
END AS Assessment;
END;
Wie automatisiere ich die Datenaktualisierung?
Manuell ist Quatsch. Das muss automatisch laufen.
SQL Server Agent Jobs:
-- Job 1: Täglich GSC-Daten abrufen (Python-Script)
USE msdb;
GO
EXEC dbo.sp_add_job
@job_name = N'Daily_GSC_Import',
@enabled = 1,
@description = N'Import Google Search Console data daily';
EXEC sp_add_jobstep
@job_name = N'Daily_GSC_Import',
@step_name = N'Run Python Import',
@subsystem = N'CmdExec',
@command = N'python C:\Scripts\import_gsc_data.py',
@retry_attempts = 3,
@retry_interval = 5;
EXEC sp_add_schedule
@schedule_name = N'DailyAt3AM',
@freq_type = 4, -- Daily
@freq_interval = 1,
@active_start_time = 030000; -- 03:00:00
EXEC sp_attach_schedule
@job_name = N'Daily_GSC_Import',
@schedule_name = N'DailyAt3AM';
EXEC sp_add_jobserver
@job_name = N'Daily_GSC_Import';
-- Job 2: ETL ins DWH
EXEC dbo.sp_add_job
@job_name = N'Daily_DWH_ETL',
@enabled = 1;
EXEC sp_add_jobstep
@job_name = N'Daily_DWH_ETL',
@step_name = N'Load GSC Data',
@subsystem = N'TSQL',
@command = N'EXEC dbo.ETL_LoadGSCData';
EXEC sp_add_jobstep
@job_name = N'Daily_DWH_ETL',
@step_name = N'Load GA4 Data',
@subsystem = N'TSQL',
@command = N'EXEC dbo.ETL_LoadGA4Data';
EXEC sp_attach_schedule
@job_name = N'Daily_DWH_ETL',
@schedule_name = N'DailyAt3AM';
EXEC sp_add_jobserver
@job_name = N'Daily_DWH_ETL';
Access: Automatisierung via VBA und Task Scheduler:
' In Access: Modul für automatischen Import
Public Sub AutomatedDailyImport()
On Error GoTo ErrorHandler
' 1. GSC-Daten abrufen
Call ImportGSCData
' 2. GA4-Daten abrufen
Call ImportGA4Data
' 3. Sistrix-Daten aktualisieren
Call UpdateSistrixData
' 4. Reports aktualisieren
Call RefreshAllReports
' 5. Log-Eintrag
Call LogImportSuccess(Now())
Exit Sub
ErrorHandler:
Call LogImportError(Now(), Err.Description)
End Sub
' Access-DB als Scheduled Task ausführen
' Batch-Datei: run_import.bat
' "C:\Program Files\Microsoft Office\root\Office16\MSACCESS.EXE" ^
' "C:\SEO_Dashboard\SEO_Dashboard.accdb" ^
' /x AutomatedDailyImport
Wie visualisiere ich die Daten?
Du hast jetzt ein DWH. Jetzt brauchst du ein Frontend.
Option 1: Power BI
Anbindung: Direkt an SQL Server
Vorteil: Interaktive Dashboards, keine Programmierung nötig
Nachteil: Lizenzkosten (ab 10 USD/User/Monat)
-- View für Power BI: SEO-Overview
CREATE VIEW dbo.PowerBI_SEO_Overview
AS
SELECT
d.Date,
d.Year,
d.Month,
d.MonthName,
p.PageURL,
p.PageTitle,
k.Keyword,
SUM(f.GSC_Clicks) AS Clicks,
SUM(f.GSC_Impressions) AS Impressions,
AVG(f.GSC_Position) AS AvgPosition,
SUM(f.GA_Sessions) AS Sessions,
SUM(f.GA_Conversions) AS Conversions
FROM dbo.Fact_SEO_Performance f
INNER JOIN dbo.Dim_Date d ON f.DateKey = d.DateKey
INNER JOIN dbo.Dim_Page p ON f.PageKey = p.PageKey
LEFT JOIN dbo.Dim_Keyword k ON f.KeywordKey = k.KeywordKey
GROUP BY d.Date, d.Year, d.Month, d.MonthName,
p.PageURL, p.PageTitle, k.Keyword;
Option 2: Access-Frontend mit VBA
' Form: Dashboard_Main
' Charts via MS Graph Control
Public Sub UpdateDashboard()
On Error GoTo ErrorHandler
Dim rs As ADODB.Recordset
Dim cn As ADODB.Connection
Set cn = CurrentProject.Connection
Set rs = New ADODB.Recordset
' Traffic-Entwicklung
rs.Open "SELECT Date, SUM(GSC_Clicks) AS Clicks " & _
"FROM vw_SEO_Daily_Summary " & _
"WHERE Date >= DateAdd('m', -3, Date()) " & _
"GROUP BY Date ORDER BY Date", cn
' Chart aktualisieren
Call UpdateChart(Me.ChartTraffic, rs, "Date", "Clicks")
rs.Close
' Top Keywords
rs.Open "SELECT TOP 10 Keyword, SUM(GSC_Clicks) AS Clicks " & _
"FROM vw_Keyword_Performance " & _
"GROUP BY Keyword ORDER BY SUM(GSC_Clicks) DESC", cn
Call UpdateChart(Me.ChartTopKeywords, rs, "Keyword", "Clicks")
rs.Close
Set rs = Nothing
Set cn = Nothing
Exit Sub
ErrorHandler:
MsgBox "Fehler beim Dashboard-Update: " & Err.Description
End Sub
Option 3: Excel mit Power Query
Anbindung: Power Query zu SQL Server
Vorteil: Vertraut, flexibel
Nachteil: Keine Echtzeit-Updates
' VBA: Export nach Excel mit Formatierung
Public Sub ExportSEOReportToExcel()
On Error GoTo ErrorHandler
Dim xlApp As Object
Dim xlWb As Object
Dim xlWs As Object
Dim rs As ADODB.Recordset
Dim cn As ADODB.Connection
Set cn = CurrentProject.Connection
Set rs = New ADODB.Recordset
rs.Open "EXEC dbo.Report_TopPerformingPages " & _
"@StartDate = '" & Format(DateAdd("m", -1, Date), "yyyy-mm-dd") & "', " & _
"@EndDate = '" & Format(Date, "yyyy-mm-dd") & "'", cn
Set xlApp = CreateObject("Excel.Application")
Set xlWb = xlApp.Workbooks.Add
Set xlWs = xlWb.Sheets(1)
xlWs.Name = "SEO Report"
' Header
Dim i As Integer
For i = 0 To rs.Fields.Count - 1
xlWs.Cells(1, i + 1).Value = rs.Fields(i).Name
Next i
' Daten
xlWs.Range("A2").CopyFromRecordset rs
' Formatierung
xlWs.Range("A1:" & Chr(65 + rs.Fields.Count - 1) & "1").Font.Bold = True
xlWs.Columns.AutoFit
xlApp.Visible = True
rs.Close
Set rs = Nothing
Set cn = Nothing
Exit Sub
ErrorHandler:
MsgBox "Fehler: " & Err.Description
End Sub
Was, wenn die Tools keine API haben?
Manche Tools sind stur. Keine API. Keine Schnittstelle.
Lösung: Power Automate Desktop (RPA)
Du kannst auch UI-Automation machen:
- Power Automate öffnet das Tool
- Navigiert zu Reports
- Exportiert CSV
- Speichert Datei
- Import-Script liest CSV ein
Power Automate Desktop Flow (Pseudo-Code):
1. Run application "C:\Programme\SEO-Tool\tool.exe"
2. Wait 5 seconds
3. Click element "Reports" (UI Element)
4. Wait 2 seconds
5. Click element "Export CSV"
6. Wait for download
7. Move file from Downloads to C:\Import\
8. Run Python script "import_csv.py"
9. Close application
Hinweis: RPA ist fehleranfällig. Nur als letzter Ausweg.
Mein Fazit aus der Praxis
Du brauchst kein teures All-in-One-Tool.
Was du wirklich brauchst:
- SQL Server als zentrales DWH
- Python-Scripts für API-Zugriffe
- Automatisierung via SQL Agent
- Power BI oder Access als Frontend
Zeitaufwand: 2-4 Wochen Setup, dann läuft’s.
Kosten: SQL Server Express (kostenlos), Power BI Desktop (kostenlos), Python (kostenlos).
Nur für API-Zugänge zahlst du (Sistrix, Ahrefs).
Wichtigste Lektion: Fang klein an. Erst GSC + GA4. Dann erweitern.
Fragen? Schreib mir.
Ich bin der Datenschäfer. Und ich baue Dashboards für Entscheider.