SQL erweitert und Bruno hinzugefügt
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user