4.0 KiB
4.0 KiB
MySQL Binary Search – Fehlerhafte Zeile automatisch finden
Hintergrund
Bei der Migration von Daten zwischen zwei Tabellen treten gelegentlich Fehler auf. Mit LIMIT und OFFSET lässt sich der fehlerhafte Bereich manuell eingrenzen – dieser Prozess kann jedoch durch einen Binary-Search-Ansatz vollständig automatisiert werden.
Einfaches Skript (SELECT *)
Für ein simples SELECT * ohne komplexe Query:
#!/bin/bash
# Konfiguration
DB_USER="root"
DB_PASS="password"
DB_NAME="datenbank"
SOURCE_TABLE="quelle"
TARGET_TABLE="ziel"
# Gesamtanzahl Zeilen ermitteln
TOTAL=$(mysql -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -sNe \
"SELECT COUNT(*) FROM $SOURCE_TABLE;")
echo "Gesamt Zeilen: $TOTAL"
LOW=0
HIGH=$TOTAL
while [ $((HIGH - LOW)) -gt 1 ]; do
MID=$(( (LOW + HIGH) / 2 ))
FETCH=$((MID - LOW))
echo "Teste OFFSET=$LOW LIMIT=$FETCH ..."
ERROR=$(mysql -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" 2>&1 <<EOF
INSERT INTO $TARGET_TABLE
SELECT * FROM $SOURCE_TABLE
LIMIT $FETCH OFFSET $LOW;
EOF
)
if echo "$ERROR" | grep -qi "error"; then
HIGH=$MID
echo " → Fehler gefunden, eingrenzen bis Zeile $HIGH"
else
LOW=$MID
echo " → OK, weiter ab Zeile $LOW"
fi
done
echo ""
echo "=== Fehlerhafte Zeile gefunden ==="
echo "OFFSET: $LOW"
mysql -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -e \
"SELECT * FROM $SOURCE_TABLE LIMIT 1 OFFSET $LOW;"
Erweitertes Skript (komplexe Query mit JOINs, MAX etc.)
Wenn die Source-Query komplex ist (JOINs, Aggregatfunktionen, GROUP BY), wird sie als Variable definiert und als Subquery genutzt.
Wichtig: Ein
ORDER BYist zwingend erforderlich, damit die Zeilenreihenfolge zwischen den Aufrufen deterministisch bleibt.
#!/bin/bash
# Konfiguration
DB_USER="root"
DB_PASS="password"
DB_NAME="datenbank"
# Deine Source-Query OHNE LIMIT/OFFSET – als Subquery nutzbar
SOURCE_QUERY="SELECT a.id, MAX(b.wert), a.name
FROM quelle a
JOIN andere b ON a.id = b.ref_id
GROUP BY a.id, a.name
ORDER BY a.id"
TARGET_TABLE="ziel"
# Gesamtanzahl Zeilen der Query ermitteln
TOTAL=$(mysql -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -sNe \
"SELECT COUNT(*) FROM ($SOURCE_QUERY) AS _count_query;")
echo "Gesamt Zeilen: $TOTAL"
LOW=0
HIGH=$TOTAL
while [ $((HIGH - LOW)) -gt 1 ]; do
MID=$(( (LOW + HIGH) / 2 ))
FETCH=$((MID - LOW))
echo "Teste OFFSET=$LOW LIMIT=$FETCH ..."
ERROR=$(mysql -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" 2>&1 <<EOF
INSERT INTO $TARGET_TABLE
$SOURCE_QUERY
LIMIT $FETCH OFFSET $LOW;
EOF
)
if echo "$ERROR" | grep -qi "error"; then
HIGH=$MID
echo " → Fehler, eingrenzen bis Zeile $HIGH"
else
LOW=$MID
echo " → OK, weiter ab Zeile $LOW"
fi
done
echo ""
echo "=== Fehlerhafte Zeile ==="
mysql -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -e \
"$SOURCE_QUERY LIMIT 1 OFFSET $LOW;"
Gezielten Wert aus der fehlerhaften Zeile ausgeben
Variante 1 – Subquery-Wrapping
echo "=== Fehlerhafte Zeile ==="
mysql -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -e \
"SELECT a.id, a.name FROM ($SOURCE_QUERY) AS _fehler LIMIT 1 OFFSET $LOW;"
Variante 2 – Direkte Query (empfohlen bei Aggregaten)
echo "=== Fehlerhafte Zeile – Detailansicht ==="
mysql -u"$DB_USER" -p"$DB_PASS" "$DB_NAME" -e \
"SELECT
a.id AS 'ID',
a.name AS 'Name',
MAX(b.wert) AS 'Max-Wert'
FROM quelle a
JOIN andere b ON a.id = b.ref_id
GROUP BY a.id, a.name
ORDER BY a.id
LIMIT 1 OFFSET $LOW;"
Variante 2 ist bei MAX() und anderen Aggregatfunktionen übersichtlicher, da die Rohwerte direkt aus der Query kommen und nicht erst durch ein Subquery-Wrapping gehen.
Hinweise
- ORDER BY ist Pflicht für deterministische Ergebnisse bei wiederholten Aufrufen.
- Die erfolgreichen INSERT-Blöcke bleiben in der Zieltabelle erhalten – bei Bedarf kann jeder Block in eine Transaktion mit
ROLLBACKbei Fehler gekapselt werden. - Der Binary Search reduziert die Anzahl der Testläufe auf
log₂(n)– bei 1000 Zeilen also maximal 10 Schritte.