161 lines
4.0 KiB
Markdown
161 lines
4.0 KiB
Markdown
# 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:
|
||
|
||
```bash
|
||
#!/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 BY` ist zwingend erforderlich, damit die Zeilenreihenfolge zwischen den Aufrufen deterministisch bleibt.
|
||
|
||
```bash
|
||
#!/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
|
||
|
||
```bash
|
||
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)
|
||
|
||
```bash
|
||
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 `ROLLBACK` bei Fehler gekapselt werden.
|
||
- Der Binary Search reduziert die Anzahl der Testläufe auf `log₂(n)` – bei 1000 Zeilen also maximal 10 Schritte.
|