Files
Snippets/mysql_binary_search.md
T
2026-07-06 11:32:17 +02:00

4.0 KiB
Raw Blame History

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 BY ist 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 ROLLBACK bei Fehler gekapselt werden.
  • Der Binary Search reduziert die Anzahl der Testläufe auf log₂(n) bei 1000 Zeilen also maximal 10 Schritte.