Den här sidan visar hur man gör varje moment i uppgiften från Python, med små exempel, så att du kan blanda SQL med vanlig kod.
Det här är fusklappar. Försök själv först, med dokumentationen och
DESCRIBE/SUMMARIZE som stöd. Kika här bara när du kört fast.
Börja med en fil hack.py med ett skripthuvud som uv läser. Inget venv,
ingen pip:
# /// script
# requires-python = ">=3.11"
# dependencies = ["duckdb"]
# ///
import duckdbuv run hack.pycon.sql("...") returnerar en relation. .show() skriver ut den,
.fetchall() ger en lista med tupler, .df() en pandas-DataFrame (lägg
till "pandas" i dependencies), .pl() polars.
1. Titta innan du gissar
DuckDB läser en CSV som om den vore en tabell. Ingen import behövs:
con = duckdb.connect()
con.sql("SELECT * FROM 'data/job_skills.csv' LIMIT 5").show()Kolumnerna och typerna DuckDB gissade sig till:
con.sql("DESCRIBE SELECT * FROM 'data/job_skills.csv'").show()SUMMARIZE ger min, max, antal unika och andel null per kolumn. Det är det
snabbaste sättet att hitta det som är stökigt:
con.sql("SUMMARIZE SELECT * FROM 'data/people_skills.csv'").show(max_width=120)Titta särskilt på kolumner som är VARCHAR men egentligen är något annat:
en lista, ett JSON-dokument, ett datum.
2. Spara arbetet i en fil
Öppna en databas på disk så finns tabellerna kvar mellan körningar, och kan
öppnas i terminalen (duckdb hack.db) och webgränssnittet (duckdb -ui hack.db).
Bara en process åt gången får skriva, så stäng CLI:t innan du kör skriptet.
con = duckdb.connect("hack.db")
con.sql("CREATE OR REPLACE TABLE jobs AS SELECT * FROM 'data/job_skills.csv'")
con.sql("CREATE OR REPLACE TABLE people AS SELECT * FROM 'data/people_skills.csv'")
con.sql("SHOW TABLES").show()CREATE OR REPLACE TABLE är praktiskt när du itererar: kör skriptet om och
om igen utan att först droppa något.
3. En sträng med kommatecken är en lista
skills ser ut så här: "python, sql, azure". Tre funktioner räcker:
con.sql("SELECT string_split('python, sql, azure', ',') AS lista").show()
con.sql("SELECT unnest(string_split('python, sql, azure', ',')) AS rad").show()
con.sql("SELECT trim(unnest(string_split('python, sql, azure', ','))) AS ren").show()Kör dem en i taget och titta på vad som händer med resultatet. unnest gör
en lista till rader, och de andra kolumnerna i SELECT följer med på varje
rad. Så här ser det ut på riktig data, för ett enda uppdrag:
con.sql("""
SELECT job_id, trim(unnest(string_split(skills, ','))) AS skill
FROM jobs
WHERE job_id = (SELECT min(job_id) FROM jobs)
""").show()Din uppgift: gör en tabell job_skill med kolumnerna job_id, skill, och
en person_skill på samma sätt. SELECT DISTINCT tar bort dubbletter.
Kontrollera med count(*) att antalet rader är rimligt:
print(con.sql("SELECT count(*) FROM job_skill").fetchone()[0])4. JSON i en sträng
skills_detailed i people är en JSON-lista med nivå per skill. DuckDB kan
parsa den, och ->> plockar ut ett fält som text:
con.sql("""
SELECT s ->> 'skill_name' AS skill, (s ->> 'level')::INT AS level
FROM unnest(from_json('[{"skill_name":"sql","level":4}]', '["json"]')) AS t(s)
""").show()Byt ut den hårdkodade strängen mot kolumnen och lägg till FROM people,
före unnest-delen (samma mönster som i steg 3), så har du nivå per person
och skill. Dokumentation: https://duckdb.org/docs/stable/data/json/overview
Alternativet är att parsa i Python med json.loads och loopa, men låt
DuckDB göra det: det är kortare och blir en tabell direkt.
5. Städa namn
Räkna först. Sorterat efter antal syns dubbletterna direkt:
con.sql("""
SELECT skill, count(*) AS n
FROM job_skill
GROUP BY skill
ORDER BY n DESC
""").show()Sök sedan efter varianter av samma sak. Här är det praktiskt att ta ut resultatet i Python och titta med vanlig kod:
rows = con.sql("SELECT DISTINCT skill FROM job_skill").fetchall()
for (skill,) in rows:
if "azure" in skill:
print(skill)Det finns inget rätt svar på om "azure devops" är "azure". Men det finns ett
bra sätt att uttrycka beslutet: en tabell skill som mappar råa namn till
ett kanoniskt, som du sedan joinar mot. I Python kan mappningen vara en
dict som du INSERT:ar med parametrar:
canon = {"microsoft azure": "azure", "azure": "azure"}
con.sql("CREATE OR REPLACE TABLE skill (raw VARCHAR, name VARCHAR)")
con.executemany("INSERT INTO skill VALUES (?, ?)", list(canon.items()))Verktyg att fylla den med: lower(), trim(), replace(), CASE WHEN ... ILIKE, och för det luddiga jaro_winkler_similarity(a, b) som ger ett tal
0–1 för hur lika två strängar är. Allt som inte finns i skill-tabellen
kan få behålla sitt råa namn (coalesce).
con.sql("SELECT jaro_winkler_similarity('microsoft azure', 'azure')").show()Funktioner: https://duckdb.org/docs/stable/sql/functions/text
6. Joina och räkna
När job_skill, person_skill och skill finns är analysfrågorna vanlig
SQL: JOIN, GROUP BY, count(*). Två saker att ha koll på:
dateär text i CSV:n.date_trunc('month', date::TIMESTAMP)ger en kolumn att gruppera per månad på.- Ett gap mellan marknad och team är en skill som finns i den ena tabellen
men inte i den andra:
LEFT JOIN ... WHERE ... IS NULL, ellerEXCEPT.
con.sql("""
SELECT date_trunc('month', date::TIMESTAMP) AS month, count(*) AS n
FROM jobs
GROUP BY month
ORDER BY month
""").show()Parametrar i stället för strängkonkatenering, när Python-värden ska in i frågan:
con.execute("SELECT count(*) FROM job_skill WHERE skill = ?", ["python"]).fetchone()7. Spara ut resultatet
con.sql("COPY person_skill TO 'output/person_skill.parquet'")
con.sql("COPY (SELECT ...) TO 'output/top_skills.csv'")Parquet-filen kan läsas tillbaka med SELECT * FROM 'output/person_skill.parquet',
från vilken maskin som helst. Vill du ha resultatet som Python-objekt i stället:
.fetchall(), .df() eller .pl() på relationen.