Files
Snippets/mysql_binary_search.md
T

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.